TSDB as a Service, TSCoke 개발기

안녕하세요. 카카오 분산기술셀에서 분산스토리지, DBMS 등을 개발하고 있는 yanoo.kim(김연우)이라고 합니다.

여기에서는 카카오 사내의 TSDB as a sevice인 TSCoke를 개발하게 된 이유, TSCoke를 기획할 때 고려했던 점, 그리고 개발하면서 겪게 된 문제들과 각각의 해결 방법 등 TSCoke 서비스 개발기를 이야기하고자 합니다.

 

 

TSCoke를 개발하게 된 이유

가끔 TV로 IT 관련 뉴스를 보면, 항상 검은 바탕에 하얀색 글씨들이 정신없이 올라가는 모니터와, 이러한 화면을 얼굴 찌푸리며 보고 있는 IT 기술자들의 모습이 보이곤 했습니다. 그러나 이는 현실과 다릅니다.

현실에서는 Wiki나 Jira를 작성하거나, IDE를 통해 코드를 멍하니 바라보기도 하지요. 그리고, 웹 브라우저로 어드민 페이지나 Kibana, Grafana 등의 대시보드로 여러 로그와 지표들을 살펴보곤 합니다. 

Grafana dashboard[1] 

 

서비스를 모니터링하는데 대시보드는 정말 유용한 도구입니다. 이벤트에 대한 알람, 이벤트에 대한 원인 추적, 구매 계획, 서비스 품질 측정 등 여러 모니터링 목적을 해결하는데 있어 이러한 지표 데이터를 쉽게 열람하고 분석할 수 있는 환경은 매우 중요합니다.[2]

제가 소속한 분산기술셀은 카카오의 이미지, 동영상, 파일들을 저장하는 분산스토리지 Kage[3]를 운영하고 있습니다.

Kage는 셀 수 없이 많은 서버와 스토리지를 사용하는 서비스로, 이를 모니터링하기 위해 여러 지표 서비스를 사용하고 있었습니다. 내부 커스텀 메트릭을 관리하기 위한 Kage 전용 대시보드도 있었고, 여러 서버들을 모니터링하기 위해 Prometheus[4]를 사용했고, 계절 이벤트 등의 장기적인 지표 변화를 확인하고 비용 산정 및 구매 계획을 수립하기 위해 Apache Hadoop[5]을 바탕으로한 내부 BI 서비스를 이용했습니다.

처음부터 이렇게 많은 지표들을 이용했던 건 아닙니다. 약 10년의 시간 동안 Kage를 운영하면서 서비스는 점점 방대하며 복잡해졌고, 이를 커버하기 위해 하나둘 추가된 지표들이 누적되어 쌓이면서 나타난 결과였던 것이죠.

이렇게 지표들이 많다 보니 지표를 관리하는 것도 하나의 일이 되었고, 또 각각의 다른 지표들을 조합하여 분석하고자 하는 욕구도 생겨났습니다. 그래서 저희는 여러 지표들을 정리하고 하나로 통합하고 싶어졌습니다.

이를 위해 저희 셀은 수많은 장/단기 지표들을 보관하고, 지표들을 빠르게 조회, 분석 할 수 있는 지표 저장소를 찾아보게 되었습니다.

이를 위해 여러 TSDB[6]들을 찾아보고 테스트해 보았습니다. 하지만 어떤 제품은 싱글 노드(Single node)만 지원했고, 어떤 제품은 클러스터링 시 비용부담이 있었으며, 어떤 제품은 조회가 느렸고, 어떤 제품은 저희가 원하는 기능이 없었습니다.

 

TSDB+Redicoke

당시 저희 셀은 Kage뿐만 아니라, Redicoke라는 이름의 Distributed Key-value store[7][8]를 운영하고 있었습니다.

Redicoke의 접미사인 redi는 redis[9]에서 따왔습니다. 편리한 사용성을 제공하기 위해, Redicoke와 동일한 프로토콜과 자료구조를 제공했거든요. 이를 통해, 사용자는 Redis client와 Redis third party를 사용해 Redicoke를 이용할 수 있게 되었습니다.

 

즉, Redicoke는 redis 프로토콜을 제공하지만, redis는 아닙니다. In-memory store인 redis와 달리 NAND 기반으로 동작하며, 데이터 삼중화[10]를 통해 안정성을 제공합니다. 이에 따라 redis는 빠른 latency를, Redicoke는 넓은 저장공간을 얻었지요.

특히, Redicoke는 서비스 중단 시간이 없는 Online scale out[11] 을 제공하기 때문에, 성능과 공간이 필요하면 확장하면 되는거라, 실질적으로는 무한에 가까운 저장공간을 서비스할 수 있습니다.

하지만, 초기 Redicoke의 사용자들은 대부분 redis를 사용하다가, 공간과 안전성에 대한 요구 때문에 Redicoke로 옮긴 경우가 대부분이었습니다. 이는 Redicoke의 이용 패턴에 매우 큰 영향을 주었습니다. 왜냐하면 기존에 redis를 사용하던 사용자들의 이용 패턴은, redis에 맞도록 고성능/저용량 패턴이었기 때문입니다.

그런데 Redicoke는 redis에 비해 저성능/고용량이라, 추가적인 성능은 필요한데 용량은 남게 됩니다. 물론, Scale out을 하면 추가적인 성능을 얻을 수 있습니다. 이를 통해 사용자들의 요구에 충족할 수 있지만, Scale out한 만큼 저장공간이 늘어나게 되니, 안그래도 남는 저장공간이 더 남게 됩니다.

이렇게 남아도는 저장공간에, 저희는 지표 데이터를 저장하기로 마음먹었습니다. 중간에 데몬을 두어 연산들을 취합하여 Redicoke 성능에 대한 Throughput 소모량은 최소화하고, 방대한 저장공간은 최대한 활용함으로써, 장단기 데이터는 물론 여러 종류의 지표를 모두 저장해도 되는 넓은 공간을 제공할 수 있도록요. 

 

빠르게 프로토타이핑해 본 결과는 매우 만족스러웠습니다. 데이터의 저장 공간, 조회 성능은 요구사항을 만족했습니다. 여기에 추가적으로 다양한 분석을 편리하고 ad-hoc하게 제공할 수 있도록, 널리 사용해서 익숙했던 Prometheus의 PromQL[12]을 제공하기로 하였습니다.

이 과정에서 저희는 우리와 비슷한 고민을 하고 있던 사내의 다른 팀들을 알게 되었습니다. 저희는 그분들을 만나 요구사항을 수렴하였고, 최종적으로 여러가지 고민 끝에 카카오의 요구사항에 맞는 카카오의 TSDB를 만들기로 결정하였습니다.

 

TSCoke 기획하기

저희가 TSCoke를 기획하면서 잡은 목표는 크게 네가지였습니다.

 

쉽게 시작하기 위한 웹 콘솔을 제공하자

사람들은 보통 Grafana와 같은 대시보드를 통해 TSDB의 데이터를 조회합니다. 따라서, 웹으로 TSCoke의 계정 생성, 동작 및 사용량 확인, 로그 확인 등을 쉽게 사용할 수 있어야 사용자들이 친숙하고 편하게 TSCoke를 사용할 수 있을 거라고 생각했습니다.

 

쉽게 사용하기 위한 오픈소스 연동성을 제공하자

자체 솔루션의 문제는 익숙하지 않다는 점입니다. 거기에 써드파티도 부족하죠. 프로토콜도 생소합니다. 사용자 입장에서는 여러가지로 곤욕입니다. 그래서 저희는 널리 사용되는 모니터링 솔루션인 prometheus의 API에 대한 호환성을 제공하여, 이 문제들을 해결하기로 하였습니다.

 

데이터 손실에 대한 고민을 덜어드리기 위해 내구성/고가용성을 제공하자

지표 데이터는 없다고 해서 서비스에 지장이 가거나 큰 문제가 발생하는 것은 아닙니다. 하지만 지표 데이터는 의사결정에 중요한 역할을 합니다. 따라서 지표 데이터의 예상치 못한 손실은 없어야 합니다.  그리고 장애 대응과 같은 촌각을 다투는 상황에서 갑작스레 지표 데이터가 보이지 않는 등의 문제는 많이 곤혹스러운 부분이기에, 고가용성 제공은 반드시 필요한 부분입니다.

 

성능/공간 확장에 대한 고민을 덜 수 있도록 무중단 수평확장을 제공하자

사용하다 보면 처음 생각했던 것보다 빠른 성능이 필요할 수도 있고, 공간 확장에 대한 요구가 필요할 수도 있습니다. 그래서 장비만 추가하면 데몬이 자동으로 Shard를 분배하고 자동으로 Migration하며, 그 과정에서 서비스가 중단되는 일은 없어야 합니다.

 

 

저희는 이러한 요구사항에 맞추어 아키텍쳐를 그렸습니다. 전반적인 자료구조를 설계하고, 필요한 모듈들을 배치하고, 필요한 로직들도 개발하였습니다. 이미 프로토타이핑하였으니, 개발은 매우 수월했고 별다른 문제 없이 잘 진척되었습니다.

 

 

구현된 TSCoke는 초기 기획했던 생각 그대로, 사내 클라우드 포털인 KCP(Kakao Cloud Portal)을 통해 웹으로 제어 가능했고, Prometheus와의 호환성은 PromQL Compliance test[13]를 100% 통과할 정도였고, Sharding과 Replication을 통해 내구성과 무중단 확장을 제공할 수 있었습니다. 무중단 확장을 통한 성능 향상도 확인하였습니다.

구현 결과는 만족스러웠고, 곧 사내 서비스 오픈이 가능해 보였습니다.

 

큰 문제에 봉착하다

앞서 이야기했듯, TSCoke는 저희 부서만이 아닌 타 부서로부터 요구사항을 수렴해 개발한 TSDB입니다. 구현한 버전을 바탕으로, 타부서와 함께 TSCoke를 테스트해 보았습니다.

하지만, 결과는 실망스러웠습니다. 일단 동작조차 제대로 돌아가지 않았기 때문입니다. 특히 두가지 문제, Out-of-memory와 Timestamp 역전 현상이 제일 심각했습니다.

 

Series index와 Out-of-memory

Promql은 강력하고 다양한 기능이 많습니다. 그중에 Label 검색기능[14]이 있는데, 이 기능을 통해 특정 String과 일치하는 Label, 또는 일치하지 않는 Label은 물론, Regular expression[15]을 통해 Label을 찾는 것도 가능합니다. 이를 위해서 각 Label에는 Index가 걸려있어야 합니다. 그런데 이 Index가 너무 크고 방대했습니다.

데이터가 많은 만큼 O(logN)의 시간복잡도도 무시하지 못할 만큼 증가하였습니다. 늘어난 부담을 In-memory indexing을 통해 해결하였는데, 이 Index의 크기가 너무 크다보니 곧잘 Out-of-memory가 발생하곤 하였습니다.

 

Timestamp 역전 현상

보통 TSDB의 데이터는 시간 순서대로 삽입되기 마련입니다. 시간이 흘러감에 따라, 현재의 지표, 1분 후의 지표, 2분 후의 지표가 삽입될 테니까요. 그런데 이러한 시간 축이 가끔 뒤집어지는 일이 발생하였습니다.

 

원인은 이중화 때문이었습니다. TSCoke는 Master-Master Replication[16]을 기본으로하되, 데이터 정합성 처리를 쉽게 할 수 있도록, 두 노드 중 우선순위를 둬서 어지간하면 Primary로 먼저 기록하고, 그 기록한 내용이 Secondary로 동기화되도록 구현하였습니다.

문제는 약간의 네트워크 지연, 또는 Primary node의 동작 지연 등으로 Write 연산이 Secondary로 Failover되면, Primary와 Secondary는 각기 다른 최신 데이터를 갖게 됩니다. 이 두 데이터를 동기화하는 과정에서, 한 노드는 자기가 가진 최신 데이터보다 옛날 데이터를 받아야 했고, 이것이 그 노드 입장에서는 Timestamp 역전 현상으로 나타났습니다.

 

이러한 Timestamp 역전 현상이 문제되는 이유는, 대량의 I/O를 불러오기 때문입니다. TSDB는 압축 효율이 매우 좋은 DB입니다. 시계열 데이터는 시간 순대로 데이터가 삽입되며, 또 삽입된 데이터들이 어느 정도는 비슷한 특성을 갖습니다.

가령 CPU 점유율 같은 값은 0에서 100 사이의 값을 보일 것이며, DISK 사용량은 시간에 따라 쭉쭉 단조 증가하겠지요. 그래서 시계열 데이터의 압축 효율이 꽤 괜찮습니다. 이에 따라 저희는 Double Delta, Gorilla[17]를 이용해 데이터를 압축하여 저장하였지요. 특히 메모리에 압축 문맥을 유지함으로써, 올바른 시간순대로 오는 데이터는 기존 데이터 수정없이 Append Only로 데이터를 Local storage에 기록할 수 있었습니다.

하지만 Timestamp 역전이 발생하면, 압축 데이터 중간에 데이터를 삽입해야 하는 일이 발생합니다. 이때는 압축 문맥이 없기 때문에, 전체 데이터를 모두 읽어 다시금 압축 문맥을 만들어 데이터를 중간에 삽입하고 다시 저장하는 일이 있어야 합니다. 이 과정에서 평상시 대비 많은 I/O가 발생하게 됩니다.

그러나 안타깝게도 높은 고가용성을 제공하기 위한 Master-Master 구조는 동시에 클러스터 내 여러 노드에 대해 Timestamp 역전 현상을 일으켰고, 이에 따라 한번에 I/O는 크게 튀었고, 이것이 클러스터의 성능 전반에 영향을 주었습니다.

 

문제 해결을 위한 추가 개발

당시 TSCoke의 OutOfMemory와 Timestamp 역전 현상 등을 해결 하기 위해서는 추가 개발이 필요했습니다.

 

Adaptive Radix Tree와 Dictionary coding

Radix Tree는 노드 내에서 목적하는 Child node를 바로 찾을 수 있어서, O(logn)의 시간복잡성을 가진 BinarySearch를 사용하는 BTree 대비, 탐색 성능 상 이점이 있습니다. 다만, Key들을 촘촘히 배치하는 BTree와 달리, 빈 Slot이 있을 수 있어서 메모리가 낭비된다는 점이 약점이지요.

이 약점을 극복한 것이 ART(Adaptive radix tree)[18]로, TSCoke는 빠른 Label 탐색에 ART를 사용합니다.

 

ART는 노드의 크기가 적응적입니다. 노드가 작게는 4부터, 16, 48, 256 등 노드의 크기가 변하기 때문에, 일반 Radixtree보다는 데이터를 촘촘하게 기록할 수 있습니다.

 

그리고 Path compression(경로 압축) 기능이 있는데, 이는 위 그림의 왼쪽 부분에 표현되었듯, BAR, BAZ 등 BA라는 동일한 Prefix를 지닌 Key들이 여럿 있을 때, 이에 대한 Child node를 생략시킬 수 있습니다. 원래의 Radix tree는 이를 표현하기 위해 Child를 하나만 갖는 Internal node가 있어야 하는 메모리 낭비가 있는데, 이를 절약할 수 있는 것이지요.

또한 Lazy expansion은, 이와 반대로, 무의미한 Suffix 표현을 위한 Internal node를 생략하는 기능입니다.

즉, Path compression과 Lazy expansion은 Key의 Prefix와 Suffix를 압축함으로써 메모리 효율성을 높이는 기능입니다.

 

하지만 ART도 Key 분포가 촘촘하지 않으면 비효율적이라는 단점은 여전히 남습니다. 위 그림처럼 A와 B로만 구성된 4byte key들을 삽입한다면, 한 노드의 Child는 단 두개밖에 존재하지 않기 때문에, Radix tree의 효율성은 Binary tree 수준으로 떨어집니다.

 안타깝게도 TSCoke에 저장된 Label들은 위와 비슷한 패턴이었고, 이에 따라 TSCoke Index의 메모리 효율성은 떨어졌습니다. 이에 대한 해결책을 찾아야 했지요.

 

그래서 도입한 것이 Dictionary coding[19]입니다. ART 논문에 나와있듯, 가장 Key 분포가 촘촘한 데이터셋은 Integer입니다. 그래서 Integer를 Key로 두면 Radix tree의 공간효율성은 급격히 올라갑니다. 그리고 Dictionary coding은 문자열을 Integer로 매핑(mapping)하는 압축법입니다. 위 그림처럼 Dictionary coding을 통해 Label들을 숫자로 쉽게 압축할 수 있고, 그 효율은 매우 우수합니다.

 

 

이렇게 되면 아까의 ART를 Binary tree로 만든 문제도 쉽게 해결됩니다. Dictionary coding을 통해 Key가 숫자가 되니까요. 거기에 ART의 Path compression 등의 특징이 결합되면, Index의 메모리 효율은 크게 올라갑니다.

 

위 두 그래프는 이 효과를 매우 잘 보여줍니다. 두 그래프는 TSCoke에 저장된 실데이터 10만 건을 바탕으로 한 통계입니다. 여기서 ‘기본’ 항목은 sync.Map을 바탕으로한 구현의 결과인데, 이를 ART로 변경하면, 성능에서는 이점이 있지만 메모리 효율성은 오히려 떨어지는 것을 볼 수 있습니다.

하지만, 여기에 Dictionary coding을 추가로 도입하면, 기본 sync.Map에 Dictionary coding을 도입한 것보다 더 우수한 메모리 효율성을 보인다는걸 확인할 수 있습니다. 추가로, 메모리 효율성 증대로 인한 성능 향상도 덤으로 얻을 수 있었지요.

이 데이터는 고작 10만 건을 바탕으로한 통계인데, 실제로는 억 단위의 Series들이 TSCoke에 저장되어 있으며, 개수가 많아지면 많아지는 만큼 Dictionary coding의 압축비는 더더욱 올라갑니다. 따라서 실제 메모리 효율은 위 그래프 이상이란 걸 알 수 있지요.

 

LSS(Log-structured Segment Store 도입)

원래 TSCoke는 Bolt[21], Badger[22]같은 Golang으로 구현된 Embedded Key-value store를 사용하였습니다. 그런데 Timestamp 역전으로 인해 I/O가 튈 때마다, Bolt, Badger는 실망스러운 모습을 보였습니다. 이에 저희는 해결 방법을 고민했었죠.

그 결과, Bolt와 Badger는 저희가 내부 저장소로 사용하기에는, 사용하지 않는 기능이 많아 무거운 Local KV Store라는 결론을 내렸습니다. 그래서 저희는 NAND의 I/O 특성에 맞게, Append Only style의 LSS(Log-structured Segment Store)를 구현하였습니다.

 

LSS는 복잡하고 화려한 기능이 없었습니다. 오히려 간단하고 코드가 짧았지요. 단지 2차 Buffer 역할을 하는 Segment store에 맞도록, 만료 시간이 비슷한 Slot들을 묶어 Chunking함으로써, 생성과 만료시간을 크게 줄이고, 또 거기에 소모되는 메모리 소모량을 크게 줄였던 것뿐이었습니다.

 

 

그리고 그 결과는 생각보다 많이 놀라웠습니다.

IOPS는 수십 배로 크게 증가했고, 메모리소모량은 비교할 수 없이 작아졌죠. Write와 Delete에 소모되는 연산과 자료구조가 거의 없어지다시피 했고, 이를 표현하기 위한 자료구조는 TSCoke의 구조에 맞게 크게 줄었기 때문입니다. 즉, 추가로 구현하기보다는 오히려 구현을 빼는 식으로 설계하였지요. 이에 따라 IOPS 비용이 크게 줄어든 만큼, Timestamp 역전으로 인한 I/O 병목은 지표 상 보이지 않을 정도로 줄어 들었습니다.

 

TSCoke 베타 오픈

위와 같은 개선 후 몇 번의 테스트 결과 저희들은 제법 만족스러운 결과를 얻었습니다.

그리고 올해 2022년 6월 9일, TSCoke는 카카오의 kubernates[23] 서비스인 DKOS를 바탕으로 사내에 베타 서비스를 오픈하였습니다. 이후 여러 계정이 생성되며 데이터가 삽입되며 서비스되어, 오늘(2022년 8월 4일)기준 3억 개가 넘는 Series가 삽입되어 활발히 이용 중입니다.

지금도 TSCoke는 하루하루가 다르게 발전하고 있습니다. 처음 이 글을 작성했을 때는 존재하지 않았던, 분산 쿼리 기능과 Label 추출 성능 향상 등은 추가되어, 이 글을 다시 작성해야 하나, 라는 고민을 하게끔 할 정도입니다.

2020년 말에 시작한 TSCoke는 아직 갈길이 멉니다. 뒤로 밀어 놓은 성능 튜닝 요소도 한가득이고, Prometheus의 Alram, Recoding rule도 미구현 상태이며, 외부 스토리지 연동 기능 같은 추가하고 싶은 기능들도 한가득입니다.

이 글을 읽고 TSCoke에 관심이 생기셨다면, 아니면 “나 저것보다 훨씬 좋은 아이디어가 있는데?”하시는 분이 있으시면, 카카오 클라우드플랫폼팀 분산기술셀 지원을 부탁드립니다😀.

감사합니다.

 


Reference

[1] https://play.grafana.org/

[2] Joel Bastos, Pedro Araujo. Infrastructure Monitoring with Prometheus

[3] https://tech.kakao.com/2017/01/12/kage/

[4] https://prometheus.io/

[5] https://hadoop.apache.org/

[6] https://en.wikipedia.org/wiki/Time_series_database

[7] https://en.wikipedia.org/wiki/Distributed_data_store

[8] https://en.wikipedia.org/wiki/Key%E2%80%93value_database

[9] https://redis.io/

[10] https://en.wikipedia.org/wiki/Replication_(computing)

[11] https://en.wikipedia.org/wiki/Scalability#Horizontal_or_scale_out

[12] https://prometheus.io/docs/prometheus/latest/querying/basics/

[13] https://promlabs.com/promql-compliance-tests/

[14] https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors

[15] https://en.wikipedia.org/wiki/Regular_expression

[16] https://en.wikipedia.org/wiki/Multi-master_replication

[17] Pelkonen, Tuomas, et al. “Gorilla: A fast, scalable, in-memory time series database.” Proceedings of the VLDB Endowment 8.12 (2015): 1816-1827.

[18] Leis, Viktor, Alfons Kemper, and Thomas Neumann. “The adaptive radix tree: ARTful indexing for main-memory databases.” 2013 IEEE 29th International Conference on Data Engineering (ICDE). IEEE, 2013.

[19] https://go-compression.github.io/algorithms/dictionary/

[20] https://en.wikipedia.org/wiki/Dictionary_coder

[21] https://github.com/boltdb/bolt

[22] https://github.com/dgraph-io/badger

[23] https://kubernetes.io/

Latest Posts

[get Server!] [커머스CIC] 채널개발파트 소개 드려요!

평소 커머스 도메인에 관심이 많았다면? 톡딜을 통해 핫템을 득해본적이 있다면? 한번이라도 라이브커머스를 넋놓고 쳐다본적이 있다면? 라이언이랑 춘식이랑 함께하는 카카오 커머스CIC에서 개발자의 꿈을