사내 중앙 로깅 서비스 구축

게시 됨: 2022-03-10
빠른 요약 ↬ 올바른 프레임워크와 도구가 없으면 디버깅 프로세스가 악몽이 될 수 있습니다. 이 기사에서 Akhil Labudubariki는 자신의 팀이 자체 CLS(중앙 로깅 서비스) 도구를 개발할 때 고려한 여러 단계와 고려 사항을 안내합니다.

디버깅이 애플리케이션 성능과 기능을 개선하는 데 얼마나 중요한지 우리는 모두 알고 있습니다. BrowserStack은 고도로 분산된 애플리케이션 스택에서 하루에 백만 세션을 실행합니다! 클라이언트의 단일 세션이 여러 지리적 지역에 걸쳐 여러 구성 요소에 걸쳐 있을 수 있으므로 각각에는 여러 움직이는 부분이 포함됩니다.

올바른 프레임워크와 도구가 없으면 디버깅 프로세스가 악몽이 될 수 있습니다. 우리의 경우 세션 동안 발생하는 모든 것을 심층적으로 이해하기 위해 각 프로세스의 다른 단계에서 발생하는 이벤트를 수집하는 방법이 필요했습니다. 우리 인프라를 사용하면 각 구성 요소가 요청 처리 수명 주기에서 여러 이벤트를 가질 수 있기 때문에 이 문제를 해결하는 것이 복잡해졌습니다.

이것이 우리가 세션 중에 기록되는 모든 중요한 이벤트를 기록하기 위해 자체 내부 CLS(중앙 로깅 서비스) 도구를 개발한 이유입니다. 이러한 이벤트는 개발자가 세션에서 문제가 발생하는 조건을 식별하는 데 도움이 되고 특정 주요 제품 메트릭을 추적하는 데 도움이 됩니다.

디버깅 데이터는 API 응답 지연과 같은 단순한 것부터 사용자의 네트워크 상태 모니터링에 이르기까지 다양합니다. 이 기사에서는 2개의 M3.large EC2 인스턴스를 사용하여 100개 이상의 구성 요소에서 매일 70G의 관련 시간순 데이터를 대규모로 안정적으로 수집하는 CLS 도구를 구축한 이야기를 공유합니다.

점프 후 더! 아래에서 계속 읽기 ↓

사내 구축 결정

먼저 기존 솔루션을 사용하지 않고 사내에서 CLS 도구를 구축한 이유를 살펴보겠습니다. 각 세션은 여러 구성 요소에서 서비스로 평균 15개의 이벤트를 전송합니다. 이는 하루에 약 1,500만 개의 이벤트로 변환됩니다.

우리 서비스에는 이 모든 데이터를 저장할 수 있는 기능이 필요했습니다. 우리는 이벤트 전반에 걸쳐 이벤트 저장, 전송 및 쿼리를 지원하는 완벽한 솔루션을 찾았습니다. Amplitude 및 Keen과 같은 타사 솔루션을 고려할 때 평가 메트릭에는 비용, 높은 병렬 요청 처리 성능 및 채택 용이성이 포함되었습니다. 불행히도 예산 내에서 모든 요구 사항을 충족하는 적합성을 찾을 수 없었습니다. 비록 시간 절약과 경고 최소화 등의 이점이 있었을 것입니다. 추가 노력이 필요하지만 자체 솔루션을 자체 개발하기로 결정했습니다.

사내 구축
사내 구축의 가장 큰 문제 중 하나는 유지 관리에 필요한 리소스의 양입니다. (이미지 크레디트: 출처: Digiday)

기술적 세부 사항

구성 요소의 아키텍처 측면에서 다음과 같은 기본 요구 사항을 설명했습니다.

  • 클라이언트 성능
    이벤트를 보내는 클라이언트/구성 요소의 성능에 영향을 주지 않습니다.
  • 규모
    많은 수의 요청을 병렬로 처리할 수 있습니다.
  • 서비스 성과
    전송되는 모든 이벤트를 빠르게 처리합니다.
  • 데이터에 대한 통찰력
    기록된 각 이벤트에는 구성 요소나 사용자, 계정 또는 메시지를 고유하게 식별할 수 있고 개발자가 더 빠르게 디버그하는 데 도움이 되는 추가 정보를 제공할 수 있도록 일부 메타 정보가 있어야 합니다.
  • 쿼리 가능한 인터페이스
    개발자는 특정 세션에 대한 모든 이벤트를 쿼리하여 특정 세션을 디버그하거나 구성 요소 상태 보고서를 작성하거나 시스템의 의미 있는 성능 통계를 생성하는 데 도움을 줄 수 있습니다.
  • 더 빠르고 쉬운 채택
    팀에 부담을 주고 리소스를 차지하지 않고 기존 또는 새로운 구성 요소와 쉽게 통합됩니다.
  • 낮은 유지 보수
    우리는 소규모 엔지니어링 팀이므로 경고를 최소화하는 솔루션을 찾았습니다!

CLS 솔루션 구축

결정 1: 노출할 인터페이스 선택

CLS를 개발할 때 우리는 분명히 데이터를 잃고 싶지 않았지만 구성 요소 성능도 저하되는 것을 원하지 않았습니다. 전반적인 채택 및 출시가 지연될 수 있으므로 기존 구성 요소가 더 복잡해지는 것을 방지하는 추가 요소는 말할 것도 없습니다. 인터페이스를 결정할 때 다음 선택 사항을 고려했습니다.

  1. 백그라운드 프로세서가 이벤트를 CLS로 푸시하므로 각 구성 요소의 로컬 Redis에 이벤트를 저장합니다. 그러나 이를 위해서는 모든 구성 요소를 변경해야 하며, 이미 포함하지 않은 구성 요소에 대해 Redis를 도입해야 합니다.
  2. 게시자 - 구독자 모델로 Redis가 CLS에 더 가깝습니다. 모든 사람이 이벤트를 게시할 때 다시 전 세계에서 실행되는 구성 요소 요소가 있습니다. 트래픽이 많은 시간에는 구성 요소가 지연됩니다. 또한, 이 쓰기는 간헐적으로 최대 5초까지 점프할 수 있습니다(인터넷만으로 인해).
  3. UDP를 통해 이벤트를 전송하여 애플리케이션 성능에 미치는 영향이 적습니다. 이 경우 데이터가 전송되고 잊어버리게 되지만 여기서 단점은 데이터 손실입니다.

흥미롭게도 UDP를 통한 데이터 손실은 0.1% 미만으로 이러한 서비스 구축을 고려하기에 적합한 양이었습니다. 우리는 모든 팀에게 이 정도의 손실이 성능에 가치가 있다고 확신할 수 있었고 전송되는 모든 이벤트를 수신하는 UDP 인터페이스를 활용하기로 했습니다.

한 가지 결과는 응용 프로그램 성능에 미치는 영향이 적었지만 모든 네트워크, 대부분 사용자의 UDP 트래픽이 허용되지 않아 문제가 발생하여 어떤 경우에는 데이터가 전혀 수신되지 않았습니다. 해결 방법으로 HTTP 요청을 사용하여 이벤트 로깅을 지원했습니다. 사용자 측에서 오는 모든 이벤트는 HTTP를 통해 전송되는 반면 구성 요소에서 기록되는 모든 이벤트는 UDP를 통해 전송됩니다.

결정 2: ​​기술 스택(언어, 프레임워크 및 스토리지)

저희는 루비샵입니다. 그러나 Ruby가 우리의 특정 문제에 더 나은 선택인지 확신할 수 없었습니다. 우리 서비스는 들어오는 많은 요청을 처리하고 많은 양의 쓰기를 처리해야 합니다. Global Interpreter 잠금을 사용하면 Ruby에서 다중 스레딩 또는 동시성을 달성하는 것이 어려울 수 있습니다. 그래서 우리는 이러한 종류의 동시성을 달성하는 데 도움이 되는 솔루션이 필요했습니다.

우리는 또한 기술 스택에서 새로운 언어를 평가하고 싶었고 이 프로젝트는 새로운 것을 실험하기에 완벽해 보였습니다. 그 때 Golang은 동시성과 경량 스레드 및 go-routines에 대한 내장 지원을 제공했기 때문에 기회를 주기로 결정했습니다. 기록된 각 데이터 포인트는 '키'가 이벤트이고 '값'이 관련 값으로 사용되는 키-값 쌍과 유사합니다.

그러나 간단한 키와 값을 갖는 것만으로는 세션 관련 데이터를 검색하기에 충분하지 않습니다. 여기에는 더 많은 메타데이터가 있습니다. 이 문제를 해결하기 위해 기록해야 하는 모든 이벤트에 키 및 값과 함께 세션 ID가 있어야 한다고 결정했습니다. 또한 타임스탬프, 사용자 ID 및 데이터를 로깅하는 구성 요소와 같은 추가 필드를 추가하여 데이터를 가져오고 분석하기가 더 쉬워졌습니다.

이제 페이로드 구조를 결정했으므로 데이터 저장소를 선택해야 했습니다. Elastic Search를 고려했지만 키에 대한 업데이트 요청도 지원하고 싶었습니다. 이렇게 하면 전체 문서의 색인이 다시 생성되어 쓰기 성능에 영향을 미칠 수 있습니다. MongoDB는 추가될 데이터 필드를 기반으로 모든 이벤트를 쿼리하는 것이 더 쉽기 때문에 데이터 저장소로 더 적합했습니다. 이것은 쉬웠다!

결정 3: DB 크기는 거대하고 쿼리 및 보관은 형편없습니다!

유지 보수를 줄이기 위해 우리 서비스는 가능한 한 많은 이벤트를 처리해야 합니다. BrowserStack이 기능과 제품을 출시하는 속도를 감안할 때 우리는 이벤트 수가 시간이 지남에 따라 더 높은 속도로 증가할 것이라고 확신했습니다. 공간이 늘어남에 따라 읽기 및 쓰기에 더 많은 시간이 소요되며 이는 서비스 성능에 큰 타격을 줄 수 있습니다.

우리가 탐색한 첫 번째 솔루션은 데이터베이스에서 특정 기간의 로그를 이동하는 것이었습니다(저희의 경우 15일로 결정했습니다). 이를 위해 매일 다른 데이터베이스를 생성하여 모든 작성된 문서를 스캔하지 않고도 특정 기간보다 오래된 로그를 찾을 수 있습니다. 이제 우리는 만일의 경우를 대비하여 백업을 유지하면서 Mongo에서 15일이 지난 데이터베이스를 지속적으로 제거합니다.

유일하게 남은 부분은 세션 관련 데이터를 쿼리하는 개발자 인터페이스였습니다. 솔직히 이게 제일 풀기 쉬운 문제였습니다. 우리는 사람들이 특정 세션 ID를 가진 모든 데이터에 대해 MongoDB의 해당 데이터베이스에서 세션 관련 이벤트를 쿼리할 수 있는 HTTP 인터페이스를 제공합니다.

건축물

다음 사항을 고려하여 서비스의 내부 구성 요소에 대해 이야기해 보겠습니다.

  1. 이전에 논의한 바와 같이 UDP를 통해 수신하는 인터페이스와 HTTP를 통해 수신하는 인터페이스의 두 가지 인터페이스가 필요했습니다. 그래서 우리는 이벤트를 수신하기 위해 각 인터페이스마다 하나씩 두 개의 서버를 구축했습니다. 이벤트가 도착하는 즉시 구문 분석하여 필수 필드(세션 ID, 키 및 값)가 있는지 확인합니다. 그렇지 않으면 데이터가 삭제됩니다. 그렇지 않으면 데이터는 Go 채널을 통해 다른 고루틴으로 전달되며, 이 고루틴의 유일한 책임은 MongoDB에 작성하는 것입니다.
  2. 여기서 가능한 문제는 MongoDB에 쓰는 것입니다. MongoDB에 대한 쓰기가 데이터 수신 속도보다 느리면 병목 현상이 발생합니다. 이는 차례로 다른 들어오는 이벤트를 굶주리게 하고 데이터가 삭제됨을 의미합니다. 따라서 서버는 들어오는 로그를 빠르게 처리하고 다가오는 로그를 처리할 준비가 되어 있어야 합니다. 이 문제를 해결하기 위해 서버를 두 부분으로 나눕니다. 첫 번째 부분은 모든 이벤트를 수신하고 두 번째 부분을 위해 큐에 넣습니다. 두 번째 부분은 이를 처리하고 MongoDB에 기록합니다.
  3. 대기열을 위해 우리는 Redis를 선택했습니다. 전체 구성 요소를 이 두 부분으로 나누어 서버의 작업 부하를 줄여 더 많은 로그를 처리할 수 있는 공간을 확보했습니다.
  4. 우리는 주어진 매개변수로 MongoDB를 쿼리하는 모든 작업을 처리하기 위해 Sinatra 서버를 사용하여 작은 서비스를 작성했습니다. 특정 세션에 대한 정보가 필요할 때 개발자에게 HTML/JSON 응답을 반환합니다.

이러한 모든 프로세스는 단일 m3.large 인스턴스에서 행복하게 실행됩니다.

CLS v1
CLS v1: 시스템의 첫 번째 아키텍처를 나타냅니다. 모든 구성 요소는 하나의 단일 시스템에서 실행됩니다.

기능 요청

시간이 지남에 따라 CLS 도구가 더 많이 사용되면서 더 많은 기능이 필요했습니다. 아래에서는 이러한 항목과 추가 방법에 대해 설명합니다.

누락된 메타데이터

BrowserStack의 구성 요소 수가 점차 증가함에 따라 우리는 CLS에 더 많은 것을 요구했습니다. 예를 들어 세션 ID가 없는 구성 요소의 이벤트를 기록하는 기능이 필요했습니다. 그렇지 않으면 애플리케이션 성능에 영향을 미치고 주 서버에서 트래픽을 발생시키는 형태로 인프라에 부담을 줄 것입니다.

터미널 및 사용자 ID와 같은 다른 키를 사용하여 이벤트 로깅을 활성화하여 이 문제를 해결했습니다. 이제 세션이 생성되거나 업데이트될 때마다 CLS에 세션 ID와 함께 해당 사용자 및 터미널 ID가 알려집니다. MongoDB에 쓰는 과정에서 검색할 수 있는 맵을 저장합니다. 사용자 또는 터미널 ID를 포함하는 이벤트가 검색될 때마다 세션 ID가 추가됩니다.

스팸 처리(다른 구성 요소의 코드 문제)

또한 CLS는 스팸 이벤트를 처리하는 데 있어 일반적인 어려움에 직면했습니다. 우리는 종종 CLS로 전송되는 엄청난 양의 요청을 생성하는 구성 요소에 배포되는 것을 발견했습니다. 서버가 처리하기에 너무 바빠서 중요한 로그가 삭제되었기 때문에 다른 로그는 프로세스에서 문제가 발생했습니다.

대부분의 경우 기록되는 데이터의 대부분은 HTTP 요청을 통해 이루어졌습니다. 이를 제어하기 위해 우리는 nginx에서 속도 제한을 활성화합니다(limit_req_zone 모듈 사용). 이는 짧은 시간에 특정 수 이상의 요청을 치는 것으로 발견된 모든 IP의 요청을 차단합니다. 물론 차단된 모든 IP에 대한 상태 보고서를 활용하고 담당 팀에 알립니다.

스케일 v2

하루 세션이 증가함에 따라 CLS에 기록되는 데이터도 증가했습니다. 이것은 개발자가 매일 실행하는 쿼리에 영향을 미쳤고 곧 병목 현상이 시스템 자체에 발생했습니다. 우리의 설정은 위의 모든 구성 요소를 실행하는 두 개의 핵심 시스템과 Mongo를 쿼리하고 각 제품에 대한 주요 메트릭을 추적하는 많은 스크립트로 구성되었습니다. 시간이 지남에 따라 시스템의 데이터가 크게 증가했고 스크립트에 CPU 시간이 많이 걸리기 시작했습니다. Mongo 쿼리를 최적화하려고 시도한 후에도 항상 동일한 문제가 발생했습니다.

이 문제를 해결하기 위해 상태 보고서 스크립트를 실행하기 위한 다른 시스템과 이러한 세션을 쿼리하는 인터페이스를 추가했습니다. 이 프로세스에는 새 머신을 부팅하고 메인 머신에서 실행되는 Mongo의 슬레이브를 설정하는 작업이 포함되었습니다. 이는 이러한 스크립트로 인해 매일 발생하는 CPU 스파이크를 줄이는 데 도움이 되었습니다.

CLS v2
CLS v2: 현재 시스템의 아키텍처를 나타냅니다. 로그는 마스터 시스템에 기록되고 슬레이브 시스템에서 동기화됩니다. 개발자의 쿼리는 슬레이브 머신에서 실행됩니다.

결론

데이터 로깅만큼 간단한 작업에 대한 서비스를 구축하는 것은 데이터 양이 증가함에 따라 복잡해질 수 있습니다. 이 문서에서는 이 문제를 해결하는 동안 직면한 문제와 함께 우리가 탐색한 솔루션에 대해 설명합니다. 우리는 Golang이 우리 생태계에 얼마나 잘 맞는지 확인하기 위해 실험했고 지금까지 만족했습니다. 외부 서비스에 비용을 지불하지 않고 내부 서비스를 생성하기로 한 우리의 선택은 놀라울 정도로 비용 효율적이었습니다. 또한 세션의 양이 증가할 때까지 다른 시스템으로 설정을 확장할 필요가 없었습니다. 물론 CLS 개발에 대한 우리의 선택은 전적으로 우리의 요구 사항과 우선 순위를 기반으로 했습니다.

오늘날 CLS는 최대 70GB의 데이터를 구성하는 매일 최대 1,500만 개의 이벤트를 처리합니다. 이 데이터는 세션 중에 고객이 직면하는 문제를 해결하는 데 사용됩니다. 우리는 또한 이 데이터를 다른 목적으로 사용합니다. 각 세션의 데이터가 다양한 제품 및 내부 구성 요소에 대해 제공하는 통찰력을 감안할 때 우리는 이 데이터를 활용하여 각 제품을 추적하기 시작했습니다. 이는 모든 중요한 구성 요소에 대한 핵심 메트릭을 추출하여 달성됩니다.

대체로 우리는 자체 CLS 도구를 구축하는 데 큰 성공을 거두었습니다. 그것이 당신에게 의미가 있다면 똑같이하는 것을 고려하는 것이 좋습니다!