Ruby on Rails의 창시자 DHH가 "시타델(Citadel)"이라는 용어를 만들어낸 순간, 우리는 마침내 AppSignal의 기술 접근 방식을 설명할 완벽한 이름을 얻게 되었습니다. "이거 바로 우리잖아! 드디어 우리 방식에도 이름이 생겼네!"라고 외쳤던 기억이 납니다.
장엄한 모놀리스(Majestic Monolith)에 더해, 누군가 '시타델(The Citadel)'이라는 패턴도 정리해주면 좋겠습니다. 단 하나의 장엄한 모놀리스가 애플리케이션의 대부분을 담당하고, 매우 특수하고 이질적인 요구사항을 처리하기 위한 소수의 보조 아웃포스트(전초기지) 앱이 이를 보완하는 구조 말입니다.
— DHH (@dhh) 2020년 4월 7일
AppSignal이 시타델 패턴을 어떻게 활용하고 있는지 설명하기 위해, 우리 시스템이 작동하는 방식을 조금 공유해 보려 합니다. AppSignal은 사용자 대상 애플리케이션과, 모니터링 에이전트가 데이터를 전송하는 API로 구성된 모니터링 제품입니다. 수집된 데이터는 처리 과정을 거쳐 그래프와 인사이트로 변환됩니다.
모놀리스
고객이 직접 상호작용하는 애플리케이션은 모놀리식 Rails 앱이며, 프론트엔드 일부는 React로 작성되어 있습니다. 백엔드는 전부 Ruby로 구현되었고, 여러 데이터베이스와 통신합니다(확장성을 위해 고객별 데이터를 별도 클러스터로 분리해 운영합니다). 이 Rails 앱은 외부 서비스로 알림을 전송하는 등 다양한 부가 작업도 함께 처리합니다.
사실 초기에는 이 Rails 앱이 모니터링 에이전트로부터 들어오는 데이터마저 직접 처리했습니다. 하지만 데이터 유입이 병목 지점이 될 것임을 예감했고, 그래서 서브도메인에서 실행되는 Sinatra 앱을 두어 데이터를 수집하고 Sidekiq 잡을 생성하도록 했습니다. 해당 잡들은 Rails 앱이 이어서 처리하는 구조였습니다.
성장통
이 아키텍처는 수년간 잘 작동했습니다. 그러나 비즈니스가 성장하면서, 에이전트로부터 유입되는 데이터를 처리하는 작업에는 특별한 대응이 필요하다는 사실이 분명해졌습니다. 수십억 건의 요청을 모니터링하다 보면 어렵지 않게 한계에 부딪히게 됩니다. 결정적인 제약 요인은 Ruby가 느려서가 아니라(느리지 않다는 건 다들 아시죠 😉), 당시의 아키텍처 구조가 데이터베이스에 과도한 락킹(locking)을 유발했다는 점이었습니다.
아웃포스트의 등장
여러 가지 대안을 검토한 끝에 Kafka가 우리 상황에 가장 적합하다고 판단했습니다. 팀에는 Rust 사용 경험이 있었고, 속도와 안정성 면에서 이 시스템에 아주 잘 맞을 것이라 확신했습니다. 결국 데이터 수집 및 처리 시스템을 Rust로 재작성했으며, Kafka는 큐와 저장소의 역할을 동시에 수행하도록 활용했습니다.
중요한 점은 Rails 앱 중에서 '유입 데이터 처리' 부분만 이 아웃포스트 서비스로 옮겼다는 것입니다. 나머지 시스템은 모놀리식 앱 형태로 안정적으로 작동하고 있습니다. 우리는 자체 시스템을 깊이 이해하고 있으며, 단순함을 유지하는 것을 선호합니다. 모놀리스는 여전히 대부분의 로직을 담당하며 Kafka와 긴밀하게 상호작용합니다. 모놀리스를 지키고자 하는 의지 덕분에 Kafka 젬(gem)을 직접 만들기도 했는데, 덕분에 메인 앱이 아웃포스트와 손쉽게 통신할 수 있게 되었습니다.
AppSignal에서 Kafka가 어떻게 작동하는지 더 궁금하시다면, 제가 RailsConf에서 발표한 세션을 확인해 보시길 추천합니다.
시타델에서의 삶
이렇게 해서 우리는 지금 시타델에서 무척 만족스럽게 살아가고 있습니다. DHH의 말처럼요:
"단 하나의 장엄한 모놀리스가 애플리케이션의 대부분을 담당하고, 매우 특수하고 이질적인 요구사항을 처리하기 위한 소수의 보조 아웃포스트 앱이 이를 보완한다."
우리의 경우, 고도로 특수한 요구사항을 처리하는 단 하나의 아웃포스트 서비스를 운영 중입니다. 만약 올해 RailsConf가 열렸더라면, 이 패턴에 이름을 붙여준 것에 대한 감사의 표시로 DHH에게 스트룹와펠(stroopwafel)을 한 번 더 선물했을 텐데 말이죠. 🍪