안녕하세요 주간SaaS 입니다. 오늘은 Datadog 기술 블로그에서 찾은 Introducing Husky, Datadog's Third-Generation Event Store라는 글을 소개 합니다. 새로운 버전의 로그 저장 시스템을 멀티테넌트 구조로 설계하면서 고려했던 내용들을 소개하는 글인데 내용이 다소 길고 추상적이지만 멀티테넌트 스토리지 설계시 고려사항과 접근법을 배울수 있는 좋을 글이라 생각됩니다.
본문에도 언급이 되지만 Datadog이 이 로그 스토리지를 설계하면서 Snowflake와 Google의 Youtube 데이베이스 엔진인 Procella로부터 영감을 받았다고 나오는데요, 두 스토리지에 관한 논문도 링크되어 있으니 꼭 한번 읽어보시면 좋을것 같아요.
그럼 오늘도 좋은 하루 보내세요!
이것은 데이터독에서 만든 새로운 이벤트 저장 시스템 'Husky 허스키'에 관한 이야기입니다.
새로운 저장 시스템을 만드는 일은 재미있고 설레는 일이지만, 결코 가볍게 다뤄져선 안 됩니다. 가장 중요한 점은, 이런 일이 아무런 이유 없이 일어난게 아니라는 겁니다. 새 시스템이 어떤 선택을 했는지 이해하려면, 그 배경을 알아야 합니다: 그 전에 무엇이 있었는지, 그리고 우리가 왜 새로운 것을 만들기로 결정 했는지를요."
메트릭에서 로그까지
몇 년 전, Datadog은 로그 관리 제품의 정식 출시를 발표했습니다. 이는 우리 플랫폼에 중요한 추가 기능이었습니다. 그 이전까지, 이 회사에서 가장 널리 알려진 제품은 인프라 모니터링(속칭 “메트릭”)이었습니다. 메트릭 시스템은 <timeseries_id, timestamp, float64>의 튜플 형태로 사전 집계된 시계열 데이터를 저장하는 아이디어에 기반을 두고 있습니다.
그러나 이러한 메트릭 시스템은 많은 유사한 이벤트를 하나의 집계된 데이터 포인트로 사전 집계함으로써 효율성을 달성하기 때문에 로그 데이터를 저장하기에는 적합하지 않습니다. 예를 들어, 웹 히트율을 얻기 위해 클릭 스트림을 읽어 같은 정보를 도출하는 것보다 웹 서버에서 초당 카운트를 캡처하는 것이 비용과 에너지 면에서 훨씬 더 효과적입니다.
이 최적화는 메트릭 시스템에서 압축된 시계열을 생산하는 데 매우 바람직하지만, 로그 제품에는 재앙과 같습니다. 1초의 세밀도로, 같은 초 내에 발생한 백만 개의 “이벤트”를 <timestamp, float64>의 단일 16바이트 튜플로 압축할 수 있습니다. 또한, 대부분의 현대 메트릭 데이터베이스는 델타-오브-델타 인코딩을 활용하여, 대부분의 경우 각 16바이트 튜플의 실제 저장 비용을 2바이트 미만으로 줄입니다.
효율성을 위해 메트릭 시스템은 태그 형태로 컨텍스트를 저장하는 능력에 한계가 있습니다. 메트릭 데이터를 강력하게 필터링하고 그룹화할 수 있지만, 제한 없는 태그 카디널리티(unbounded tag cadinality)와 쿼리 반환 시간 사이에는 트레이드오프가 있습니다. *(🐶tag수가 무제한으로 많아질 경우 tag로 필터링 의미가 무색해져 결국 데이터를 효율적으로 쿼리하고 처리하는데 어려움이 있을 수 있다는 의미로 해석됩니다)
따라서 데이터센터, 서비스 또는 pod_name과 같이 수명이 긴 dimension 기준으로 메트릭에 태그를 지정하고, transaction_id 또는 packet_id와 같이 수명이 짧고 빠르게 변경되는 dimension을 미리 집계하는 것이 좋습니다.
따라서, 데이터센터, 서비스 또는 pod_name과 같은 장기적인 차원으로 메트릭을 태그하고, transaction_id나 packet_id와 같은 단기적이고 빠르게 변하는 차원을 사전에 집계하는 것이 실용적인 권장사항입니다.
반면, 로그 제품은 매우 다른 요구사항을 가지고 있습니다:
- 로그 크기는 바이트가 아닌 킬로바이트로 측정되는 경향이 있습니다. 이는 로그를 효율적으로 저장하고 쿼리하는 데 필요한 것에 극적인 영향을 미칩니다.
- 스택 트레이스나 UUID와 같은 고 카디널리티 데이터를 지원하지 못하는 로그 제품은 크게 유용하지 않습니다.
즉, 메트릭의 매력은 많은 이벤트에서 충분한 컨텍스트를 가진 시계열을 매우 효율적으로 계산하는 능력에 있습니다. 반면 로그의 매력은 많은 세부적인 이벤트와 그 모든 컨텍스트를 유지하고, 쿼리 시 임의의 차원 집계를 생산할 수 있는 능력에 있습니다.
Datadog Logs의 초기 버전
아래 그림은 Datadog의 첫 번째 로그 시스템의 모습입니다.
이 모습의 시스템이 처음에는 꽤 잘 작동했지만 문제가 나타나는 데는 그리 오래 걸리지 않았습니다. 가장 큰 문제는 멀티테넌트 클러스터 내에서 오작동 하는 노드 하나가 모든 테넌트의 서비스에도 영향을미쳐, 최악의 경우 전체 클러스터를 사용할 수 없게 만들 수 있다는 것이었습니다.
이런 일이 발생할 때마다 문제를 조치하기 어려웠습니다. 과부하가 걸린 클러스터를 확장하거나 스케일아웃하면 상황이 개선되기는 커녕 더 악화되는 경우가 많았습니다. 예를 들어, 이미 쓰기 또는 읽기로 인해 과부하가 걸린 노드들은 이미 수행 중이던 모든 작업에 더해 갑자기 서로에게 데이터를 스트리밍하기 시작했죠.
자체 클러스터링 구축
로그 시스템 개발 두 번째 단계에서 클러스터링으로부터 스토리지를 분리했습니다. 모든 클러스터링을 스토리지와 분리하여 개별적으로 처리함으로써 새로운 클러스터링 시스템을 저희의 멀티테넌트 기술과 긴밀하게 통합할 수 있었습니다. 동일한 단일 노드 스토리지 엔진을 유지하면서 데이터 배포와 토폴로지 변경을 세밀하게 제어하는 데 집중했습니다. 새로운 설정은 다음과 같습니다:
이 다이어그램과 이전 다이어그램 사이에는 몇 가지 주요 차이점이 있습니다:
- 개별 노드 중 어느 것도 서로에 대해 알지 못합니다. 각각의 노드는 마치 하나의 '클러스터'인 것처럼 작동합니다. 즉, 하나의 노드가 잘못 작동하거나 건강하지 않은 경우 해당 노드가 소유한 샤드에 할당된 만큼만 테넌트를 중단시킬 수 있으며, 나머지 '클러스터'에 연쇄적인 장애를 일으킬 수 있는 방법이 없습니다.
- 우리는 "샤드 라우터(Shard Router)"라는 새로운 서비스를 만들었는데, 이 서비스는 Kafka에서 읽고 새로운 Kafka 클러스터에 다시 쓰지만 이번에는 데이터를 "샤드"(파티션 그룹)로 구성합니다. 테넌트는 지난 5분 동안의 데이터 볼륨에 따라 적절한 수의 샤드로 자동 분할됩니다.
- 두 개의 스토리지 노드 복제본이 각 샤드를 사용하며, 각 샤드의 관련 파티션에서 이벤트만 사용합니다.
- 각 테넌트의 데이터가 어느 샤드에 분산되어 있는지 파악하고, 모든 관련 샤드/복제본을 쿼리하고, 부분 집계를 병합하고, 최종 쿼리 결과를 생성하는 사용자 정의 쿼리 엔진(Custom Query Engine)을 더했습니다.
플랫폼의 비약적인 성장
새로운 아키텍처로 마이그레이션하면서 안정성이 눈에 띄게 개선덕분에 당직 교대 근무(On-call)가 훨씬 견딜만 해졌고, 엔지니어들은 더 많은 수면을 취할 수 있게 되었습니다. 마침 이 때, 우리 플랫폼(내부적으로는 '이벤트 플랫폼'이라고 부름)의 범위가 더욱 빠른 속도로 확장될때였습니다!
다른 팀에서는 네트워크 성능 모니터링(NPM), 실제 사용자 모니터링(RUM), 데이터독 Continous Profiler와 같은 새로운 제품을 출시 했습니다. 이러한 신제품 중 상당수는 로그 관리 제품과 비슷한 스토리지 요구 사항을 가지고 있었습니다. 즉, 멀티킬로바이트 시계열 '이벤트'를 저장하고 색인(Index)해야 했습니다. 이러한 새로운 제품에서 생성되는 이벤트가 구조화된 로그와 거의 비슷하다는 것을 인지하고, 저희의 플랫폼의 범위를 확장하여 이러한 이벤트도 저장하기 시작했습니다.
이렇게 이벤트 플랫폼에 새로운 제품이 늘어나기 시작하면서 다시 문제가 나타나기 시작했습니다. 예를 들어, 한 테넌트가 단기간에 엄청난 수의 이벤트를 만들어내면 같은 샤드에 배치된 다른 모든 테넌트의 쿼리 성능이 저하될 수 있었습니다.
이러한 안정성 문제 외에도, 제품 팀에서는 당시의 아키텍처로는 지원하기 어려운 새로운 기능을 요청하기 시작했습니다:
- 일부 로깅 고객은 중요하지만 가끔씩만 쿼리할 필요가 있는 데이터를 가지고 있는데도 불구하고, 기존 아키텍처 환경에서 비용 효율적이지 않지만 훨씬 더 오래 보관하고 즉시 쿼리할 수 있기를 원했습니다.
- 고객과 제품 팀은 사전에 인덱싱(indexing)할 필드를 지정하지 않고도 이벤트의 모든 필드에 대해 쿼리 및 집계할 수 있는 기능을 요청했습니다.
- 제품 팀에서는 배열 함수, 윈도우 기능, 스토리지 엔진에 직접 DDSketches를 저장하는 기능을 지원하여 미리 집계된 스케치를 전송한 다음 쿼리 시점에 다시 집계할 수 있기를 원했습니다.
그리고 이것들은 빙산의 일각에 불과했습니다! 두 가지 사실이 분명해졌습니다:
- 수집, 저장, 쿼리 경로를 독립적으로 확장하고, 고도로 멀티테넌트화된 환경에서 격리, 성능, 서비스 품질을 보다 유연하게 제어하기 위해 컴퓨팅과 스토리지를 분리하는 완전히 새로운 아키텍처가 필요하다는 것이었습니다.
- 우리는 스토리지 엔진을 처음부터 끝까지 직접 소유하면서 서비스 운명을 직접 결정하고 제품 팀이 요구하는 기능을 제공해야 했습니다.
새로운 것을 디자인하다
새로운 관점과 새로운 요구사항에 따라 한 걸음 물러나 스토리지 스택을 처음부터 다시 생각했습니다. 스토리지를 컴퓨팅에서 분리하여 큰 효과를 거둔 최근의 최신 기술(예: Snowflake 및 Procella)로 부터 영감을 받아 비슷한 접근 방식을 따르기로 결정했습니다. 이러한 접근 방식이 예측할 수 없고 과부하를 만드는 멀티테넌트 워크로드에도 탄력적이고 성능이 뛰어난 시스템을 구축하는 데 최고의 유연성을 제공할 것이라고 믿었기 때문입니다. 또한 플랫폼이 실시간 데이터 레이크처럼 보이기 시작했기 때문에 새로운 시스템은 벡터화된 컬럼 저장소(비록 바늘을 찾는 듯한 상세 검색에 최적화된 많은 기능을 갖춘 벡터화된 컬럼 스토어이긴 하지만)가 되어야 한다고 결정했습니다.
허스키 소개
우리는 '허스키'라는 애칭으로 알려진 새로운 스토리지 시스템을 구축하기 시작했습니다. 저희는 허스키를 "하이브리드 분석/검색 기능을 갖춘 모듈화된 분산형, 스키마 없는, 벡터화된 컬럼 저장소로, 처음부터 상품 객체 저장소를 중심으로 설계되었습니다."라고 설명합니다. 물론 그림이 천 마디 말보다 중요하듯이, 여기 Husky의 단순화된 아키텍처 다이어그램이 있습니다:
역할
이전 다이어그램에서 많은 부분이 변경 되었기 때문에 이를 세분화해 보겠습니다. 각각의 개별 스토리지 노드에 존재하던 '스토리지 시스템'은 이제 세 가지 역할로 분리되었습니다: Writers, Reders, Compactors
Writer는 Kafka에서 읽고, (간단히 말해) 메모리에 이벤트를 버퍼링하고, 사용자 정의 파일 형식으로 이벤트를 Blob storage에 업로드한 다음, 이러한 새 파일의 존재를 메타데이터 저장소에 커밋합니다. 이것이 허스키의 스토리지 시스템에 데이터가 들어오는 방식입니다. 이러한 노드들은 완전히 stateless이며 자동 확장이 가능합니다. 중요한 점은 쿼리 시 Writer 노드와 통신을 하지 않는다는 것입니다. 이렇게 하면 쿼리 실행이 데이터 입력 과정에 영향을 미치는 것을 크게 감소 시키며, 데이터 입력 과정이 쿼리 실행을 방해하는 경우도 마찬가지로 감소 시킵니다
Compactors는 메타데이터 저장소에서 Writer와 이전 압축에 의해 생성된 작은 파일을 스캔하여 더 큰 파일로 압축합니다. 이러한 노드는 LSM 트리 데이터베이스의 압축 시스템과 똑같은 역할을 수행하지만, 로컬 디스크에서 데이터를 압축하는 대신 분산형 자동 확장 서비스로 실행됩니다. 압축된 파일을 Blob storage에 업로드한 다음, 이 결과를 메타데이터 저장소에 '커밋'합니다. 그런 다음 메타데이터 저장소는 이전 입력 파일을 삭제하고 atomic transaction으로 새 출력 파일을 생성하므로 쿼리에서 데이터의 일관성을 유지 합니다.
Reader(leaf) 노드는 Blob storage의 개별 파일에 대해 쿼리를 실행하여 부분 집계값을 반환하고, 이 집계값은 분산 쿼리 엔진에서 재 집계됩니다. 이러한 노드는 stateless 이기 때문에 문제 없이 확장하거나 축소할 수 있습니다.
메타데이터
Writers, Compactors, Readers는 메타데이터 저장소를 통해 공통된 데이터 뷰 를 가져갑니다. 허스키의 메타데이터 저장소에는 여러 가지 책임이 있지만, 가장 중요한 것은 현재 각 고객에게 표시되는 파일 집합에 대한 강력하고 일관된 Source of Truth 역할을 하는 것입니다. 향후 블로그 게시물에서 메타데이터 저장소에 대해 자세히 살펴보겠지만, 이 메타데이터 저장소는 아래에 나열된 우리의 요구 사항을 충족하는 몇 안 되는 오픈 소스 OLTP 데이터베이스 시스템 중 하나로, FoundationDB를 중심으로 간략하게 추상화되어 있습니다:
- 기본적으로 분산되어 있고 수평적 확장이 가능
- 사용자가 상호 작용할 수 있는 트랜잭션을 제공하며, 이러한 트랜잭션들은 매우 엄격한 일관성 수준을 가지고, 어떠한 숨겨진 제한이나 조건 없이 이러한 특성을 완전히 보장
- FoundationDB는 철저한 시뮬레이션 테스트를 통해 strictly serialization transaction isolation model 을 위반하는 일이 거의 발생하지 않도록 보장합니다. 따라서 많은 어려운 분산 시스템 문제를 훨씬 쉽게 해결할 수 있습니다.(*🐶 Strictly serialization의 의미는 복수의 트랜잭션이 동시에 실행되더라도, 트랜잭션의 결과는 순차적으로 실행된 것과 동일하게 유지된다는 의미 입니다)
- Apple에 운영 서비스에 사용하고 있기도 하고, 자체 내부 테스트에서 입증된 바와 같이, 극도로 공격적인 워크로드에 직면했을 때 복원력이 매우 뛰어나야 함
스토리지
샤드와 복제본(replicas)의 개념은 시스템에서 대부분 사라졌습니다. 또한, 아키텍처의 어느 곳에서도 '기록'(historical) 데이터와 '최신(fresh)' 데이터를 구분하지 않습니다. '메타데이터 저장소'와 'blob storage'라는 추상화만이 유일한 상태 저장 구성 요소로 남아 있으며, '저장하고 이 바이트들을 잃지 말아야 한다'는 까다로운 확장성, 복제, 내구성 문제는 FoundationDB와 S3와 같은 실전 테스트를 거친 시스템으로 이양했습니다. 이를 통해 실제로 해결하고자 하는 문제, 즉 고도로 멀티테넌트화된 환경에서 방대한 양의 데이터를 수집, 색인, 쿼리하는 방법에 집중할 수 있게 되었습니다. 예를 들어, 하나의 쿼리에 대한 답을 얻기 위해 수 페타바이트에 달하는 이벤트를 검색해야 할 때도 있습니다!
원시 데이터 스토리지와 메타데이터 스토리지를 모두 실전 테스트를 거친 시스템으로 오프로드할 수 있었기 때문에(*🐶FoundationDB와 S3가 그 역할을 하기 때문에) 새 시스템을 빠르게 구축하고 프로덕션에 출시할 수 있었지만, 그럼에도 불구하고 첫 번째 코드 줄을 작성하고 제품을 Husky로 완전히 마이그레이션하는 데까지 1년 반이 넘게 걸렸습니다.
격리(Isolation)
이 새로운 구조에서 또 다른 중요한 차이점은 고객의 데이터 양과 우리가 그들의 쿼리에 할당할 수 있는 하드웨어 양 사이에 관계가 없다는 것입니다. 고객의 데이터 양이 적더라도, 우리는 일년치 데이터를 대상으로 하는 쿼리를 일시적으로 많은 리더 노드로 "확장"시킬 수 있습니다. 이는 고객별 쿼리 강도의 변동을 평준화하고 모든 고객에게 더 나은 경험을 제공할 수 있게 해줍니다. 마찬가지로, 고객이 많은 양의 데이터를 보내더라도, 낮은 지연시간의 쿼리가 필요하지 않다면, 그 쿼리에 사용될 컴퓨팅 자원을 제한할 수 있습니다. 이는 우리의 온라인 아카이브 로깅 계층의 기술적 기반이 됩니다.
허스키는 또한 쿼리를 데이터 수집 및 저장 방식과 독립적으로 분리할 수 있게 해줍니다. 위 다이어그램에서, Reader pools 1과 2는 모두 메타데이터 스토어와 blob storage에 접근할 수 있습니다. 이를 통해 우리는 쿼리를 원하는 대로 분리할 수 있습니다. 모든 쿼리를 하나의 거대한 머신 풀에서 실행하거나, 제품별로, 또는 단일 테넌트처럼 세밀하게 분리할 수 있습니다. 허스키의 구조는 데이터 수집 시 결정된 제약에 구애받지 않고, 신뢰성, 비용, 제품 목표에 따라 쿼리 경로를 분리할 수 있는 유연성을 제공합니다.
이 접근 방식에서 나타나는 가장 눈에 띄는 신뢰성 이점 중 하나는 (그리고 이전에는 불가능했던 것은) 자동화된 모니터링에 의해 생성된 쿼리와 인간에 의해 생성된 쿼리를 분리할 수 있다는 것입니다. 인간이 생성한 쿼리가 고객의 자동 모니터링 평가 능력을 해치지 않을 것이라는 사실을 알고 우리 시스템의 신뢰성에 대해 더 안심할 수 있습니다.
Huskey로의 마이그레이션 임팩트
물론 이 모든 것이 이론상으로는 완벽해 보일 수 있지만, 실제로는 어떨까요? Datadog에서는 모든 마이그레이션의 최종 목표가 안정성, 성능 및 효율성을 이 순서대로 향상시키는 것입니다.
안정성(Reliability)
안정성을 측정하기란 쉽지 않습니다. 데이터 수집과 쿼리 서비스 수준 목표(SLO)를 비교함으로써 객관성을 유지하려고 하지만, SLO도 한계가 있습니다. 예를 들어, 99.9% 가용성을 제공하는 시스템이라도 연간 거의 9시간의 다운타임이 허용될 수 있지만, 사용자들은 이러한 다운타임이 언제 발생하는지에 대해 매우 민감할 수 있습니다. 현재까지 허스키는 기존 시스템에 비해 안정성 면에서 좋은 평가를 받고 있습니다:
- 새 시스템임에도 불구하고 문제 발생률은 증가하지 않았습니다.
- 자연스러운 성장과 예상치 못한 대규모 이벤트 유입도 인간의 개입 없이 자동으로 확장하며 잘 처리하고 있습니다.
- 특정 테넌트나 노드의 쿼리 성능 문제를 해결하는데 시간을 거의 들이지 않았습니다.
성능(Performace)
성능은 측정하기 가장 어렵습니다. 복잡하고 변동이 심한 워크로드를 벤치마킹하는 것은 매우 어려운 일입니다. 마이그레이션 전에 모든 데이터를 기존 시스템과 허스키에 동시에 저장하고, 몇 달 동안 쿼리 부하의 100%를 실시간으로 모니터링하면서 정확도와 성능을 비교합니다. 마이그레이션 후에는 다음과 같은 경험을 합니다:
이 차트에서 허스키(보라색)의 지연 시간은 기존 시스템(파란색)과 비교됩니다. p95/p99/최대 지연 시간은 허스키에서 현저히 낮습니다.
허스키의 평균 지연 시간은 이전 시스템보다 약간 높지만, 이는 주로 원격 스토리지가 로컬 SSD에 비해 상대적으로 높은 지연 시간을 가지기 때문입니다.
결국, 평균 지연 시간이 수백 밀리초 증가하는 것이 고객에게 수 초, 수 십 초, 수 백 초의 p95/p99/최대 지연 시간을 절약할 수 있는 옳은 결정이었다고 판단했습니다.
효율성(Efficiency)
허스키로의 마이그레이션은 전체적인 효율성을 크게 향상시켰고, 이를 통해 더 나은 쿼리 성능과 새로운 쿼리 기능 개발에 더 많은 시간을 투자할 수 있게 되었습니다. 또한, 비용 할당 방식의 유연성 덕분에 온라인 아카이브와 같은 새로운 제품을 출시할 수 있었습니다.
앞으로의 계획
이제 거의 마무리 단계에 이르렀고, 허스키의 스토리지 엔진에 대한 세부 사항을 자세히 다루지 않아 실망하신 분들도 계실 것입니다! 하지만 다행히 이번 글은 허스키의 스토리지 시스템에 대한 자세한 내용을 다룬 시리즈 중 첫 번째 글입니다. 향후 글에서는 구체적인 작동 방식과 구현 세부 사항에 대해 더 깊이 있게 다룰 예정이지만, 먼저 허스키를 만들게 된 이유부터 공유하고자 했습니다.
마지막으로, Datadog에서 함께 일하고 싶으시다면, 입사 지원을 부탁드립니다!
댓글
의견을 남겨주세요