2015년 8월, 하와이 코할라 코스트에서 열린 제41회 VLDB 학회에 페이스북 엔지니어 7명이 낸 논문 한 편이 발표됐어요. 제목은 “Gorilla: A Fast, Scalable, In-Memory Time Series Database”였고, 저자 목록엔 Tuomas Pelkonen, Scott Franklin, Justin Teller 등의 이름이 올라 있었죠. 이 논문이 밝힌 숫자는 꽤 인상적이에요. 2015년 봄 기준 페이스북은 20억 개의 고유 시계열을 운영 중이었고 초당 1,200만 개, 하루로 치면 1조 개가 넘는 데이터 포인트가 쏟아지고 있었어요. 이 정도 규모의 데이터를 왜 디스크가 아니라 메모리에 통째로 올리기로 했을까요? 그리고 메모리는 프로세스가 죽으면 다 날아가는 휘발성 공간인데, 크래시가 나도 데이터를 지키기 위해 Gorilla는 어떤 체크포인팅과 복제 전략을 썼을까요?
Gorilla는 왜 디스크 기반 TSDB를 버리고 인메모리를 택했을까?
Gorilla는 페이스북의 기존 HBase 기반 모니터링 저장소(ODS)가 감당하지 못한 읽기 지연시간 문제를 해결하려고 처음부터 메모리 위에 설계된 시계열 데이터베이스예요. 논문은 ODS의 실제 쿼리 로그를 분석한 결과 전체 조회의 최소 85%가 최근 26시간 이내 데이터만 찾고 있다는 사실을 발견했다고 밝혀요. 문제는 이 26시간 구간조차 디스크 기반 HBase에서 읽다 보니 평균 지연시간은 그럭저럭 괜찮았지만 90번째 백분위수 조회 시간은 수 초 단위로 늘어났고, 수천 개의 시계열을 걸치는 조금 큰 쿼리는 수십 초씩 걸려 타임아웃이 나기 일쑤였다는 점이에요.
여기서 끝이 아니에요. 팀은 처음엔 Memcache 기반의 읽기 전용 캐시를 얹는 방법도 검토했지만, 기존 시계열에 새 데이터를 추가하려면 캐시 서버에 읽기-쓰기 사이클이 필요해 트래픽이 지나치게 늘어난다는 이유로 기각했어요. 결국 남은 선택지는 “가장 최근 데이터만이라도 디스크 I/O 자체를 없애버리자”는 것이었고, 그렇게 나온 결론이 최근 26시간 데이터를 HBase 앞단의 쓰기 관통(write-through) 캐시로 통째로 메모리에 올려두는 구조, 즉 Gorilla였어요.
26시간치 데이터, 실제로 메모리에 어떻게 다 올릴까?
26시간 분량의 원본 데이터를 그대로 메모리에 올리면 감당이 안 되기 때문에, Gorilla는 압축을 전제로 인메모리 설계를 성립시켜요. 논문에 따르면 한 데이터 포인트(타임스탬프 8바이트 + 값 8바이트)는 원래 16바이트인데, 20억 개 시계열에 하루 1조 개가 넘는 포인트가 쌓이는 규모에서 압축 없이 16바이트씩 메모리에 두면 16TB에 달해 실전 배포에 부담스러운 수준이 돼요. Gorilla는 델타-오브-델타 타임스탬프 압축과 XOR 기반 부동소수점 압축을 결합해 포인트당 평균 1.37바이트, 즉 원본 대비 12배 축소된 크기로 데이터를 담아요. 이 압축 알고리즘 자체의 원리는 이 포스트의 범위를 벗어나므로 XOR 압축과 delta-of-delta 인코딩 포스트에서 별도로 다루고, 여기서는 이 압축률 덕분에 “메모리에 다 올린다”는 설계가 실제로 가능해졌다는 결과만 짚고 가요.
실제 배포 수치도 이 계산을 뒷받침해요. 논문 발표 기준 18개월 전 Gorilla를 처음 프로덕션에 투입했을 때 26시간치 데이터셋은 1.3TB RAM에 담겨 20대 머신에 고르게 분산돼 있었고, 이후 데이터 증가에 따라 클러스터 크기를 두 차례 두 배씩 늘려 논문 발표 시점엔 클러스터당 80대 머신으로 운영되고 있었다고 해요. 압축률이 그대로 메모리 예산과 서버 대수로 직결되는 구조라는 걸 보여주는 대목이에요.
시계열 20억 개를 메모리 안에서 어떻게 찾아낼까?
Gorilla는 시계열의 문자열 키를 해시해 샤드로 나누고, 각 샤드를 다시 ShardMap → TimeseriesMap(TSmap) → 개별 시계열(TS) 구조로 계층화해 조회 속도와 스캔 속도를 동시에 잡아요. ShardMap은 샤드 ID를 TSmap 포인터에 매핑하는 벡터이고, 각 TSmap은 시계열 전체를 순회하는 벡터와, 이름으로 특정 시계열을 상수 시간에 찾는 unordered_map을 동시에 들고 있어요. 벡터는 전체 데이터를 훑는 대량 스캔에 유리하고 맵은 개별 조회를 빠르게 해주는데, 이 두 구조를 하나의 TSmap 안에서 공유 포인터로 함께 관리한 게 설계의 핵심이에요.
동시성 제어도 무겁게 가지 않았어요. TSmap과 ShardMap 자체는 읽기-쓰기 스핀락 하나로 보호하고, 개별 시계열(TS) 각각에는 1바이트짜리 스핀락만 걸어요. 개별 시계열의 쓰기 처리량이 낮아 락 경합이 크지 않다는 관찰에서 나온 선택인데, 덕분에 초당 수천만 건의 쓰기가 들어와도 락 대기로 병목이 생기지 않아요. 시계열이 삭제되면 벡터 항목은 즉시 메모리를 반환하지 않고 “죽었다”는 표시만 남긴 뒤 새 시계열이 생성될 때 그 자리를 재사용하는 방식으로, 잦은 할당·해제로 인한 메모리 파편화도 피해가요.
크래시가 나도 데이터가 안 날아가는 체크포인팅 전략
Gorilla는 GlusterFS라는 3중 복제 POSIX 호환 분산 파일시스템에 4종류의 파일을 꾸준히 기록해 프로세스 재시작이나 크래시 이후에도 메모리 상태를 복원할 수 있게 만들어요. 한 Gorilla 호스트가 담당하는 샤드마다 별도 디렉터리를 두고, 그 안에 시계열 키 목록을 담는 key list, 실시간으로 들어오는 데이터를 그때그때 기록하는 append-only log, 2시간마다 만들어지는 완성된 block 파일, 그리고 block 파일이 정상적으로 기록됐는지 표시하는 checkpoint 파일을 각각 관리해요. 흥미로운 점은 이 로그가 전통적인 write-ahead log가 아니라는 거예요. Gorilla는 ACID를 보장하지 않기 때문에 데이터를 최대 64KB(보통 1~2초 분량)까지 버퍼에 모아뒀다가 한꺼번에 디스크로 내려써요. 정상 종료라면 버퍼가 깨끗이 플러시되지만, 크래시가 나면 그 순간 버퍼에 남아있던 소량의 데이터는 잃을 수 있어요. 논문은 이 트레이드오프를 두고 더 높은 쓰기 처리량과 가용성을 얻는 대가로 몇 초 분량의 데이터 유실을 감수하기로 의도적으로 선택한 것이라고 명시해요.
2시간마다는 이보다 훨씬 압축된 block 파일을 새로 만들어요. 이 block 파일은 메모리에 있는 형태 그대로 복사한 64KB 슬랩들의 집합과, 각 시계열 ID가 어느 블록을 가리키는지 알려주는 포인터 목록 두 부분으로 구성돼요. block 파일 기록이 끝나면 Gorilla는 checkpoint 파일을 “터치”해 이 구간이 안전하게 디스크에 반영됐음을 표시하고, 더는 필요 없어진 로그 파일을 지워요. 만약 프로세스가 block 파일을 다 쓰기 전에 크래시했다면 checkpoint 파일이 존재하지 않을 테니, 재시작한 새 프로세스는 그 block 파일을 신뢰하지 않고 로그 파일만 읽어 상태를 복원해요. 체크포인트가 있고 없고로 “이 블록을 믿어도 되는지”를 판단하는, 아주 단순하지만 확실한 복구 규칙이에요.
노드 장애부터 리전 재난까지, 이중화는 어떻게 버틸까?
Gorilla는 단일 노드 장애는 물론 리전 단위의 대규모 장애까지 버티기 위해 지리적으로 분리된 두 데이터센터 리전에 완전히 독립적인 인스턴스 두 벌을 동시에 운영해요. 쓰기가 들어오면 두 인스턴스에 각각 스트리밍되는데, 이때 두 인스턴스 간 일관성을 맞추려는 시도는 전혀 하지 않아요. 한쪽 리전이 통째로 죽으면 읽기 요청은 자동으로 살아있는 리전으로 넘어가고, 장애 리전이 복구돼 26시간 분량을 다시 채울 때까지 그 상태가 유지돼요. 장애가 1분 넘게 지속되면 그 구간의 데이터는 재시도 없이 그냥 버려지는데, 이 역시 “오래된 과거 데이터보다 최신 데이터의 가용성이 더 중요하다”는 Gorilla의 설계 철학을 그대로 보여주는 대목이에요.
리전 내부의 장애 대응은 한 단계 더 촘촘해요. Paxos 기반 시스템인 ShardManager가 노드에 샤드를 배정하는데, 노드 하나가 죽으면 그 샤드들을 클러스터 내 다른 노드로 재배정해요. 이 재배정이 일어나는 동안 쓰기 클라이언트는 들어오는 데이터를 최대 1분치까지 버퍼에 담아두고, 그보다 오래된 데이터는 버려요. 클러스터가 건강하다면 샤드 이동은 보통 30초 이내에 끝나 데이터 유실이 전혀 없고, 새로 샤드를 넘겨받은 노드는 GlusterFS에서 해당 데이터를 읽어와야 하는데 샤드 하나당 약 16GB 분량의 데이터를 여러 물리 호스트에 분산된 파일에서 읽어와 완전히 정상 동작하기까지 약 5분이 걸린다고 논문은 보고해요. 그사이 해당 샤드는 읽기 요청에 부분적인 결과만 돌려주고 “partial” 플래그를 함께 표시하는데, 읽기 클라이언트 라이브러리는 이 플래그를 보고 반대편 리전에 같은 시계열을 다시 요청해 더 완전한 결과로 채워 넣어요.
실제로 얼마나 효과가 있었을까
논문이 제시한 성능 수치는 이 설계 전환이 헛수고가 아니었다는 걸 분명히 보여줘요. HBase 기반 저장소와 비교했을 때 Gorilla는 조회 지연시간을 73배 줄이고 조회 처리량은 14배 늘렸다고 논문 초록에 명시돼 있고, 결론부에서는 “이전 디스크 기반 TSDB 대비 프로덕션 쿼리 지연시간을 70배 넘게 줄였다”고 재확인해요. 백분위수별로 보면 개선폭이 더 극적인데, P90 기준 113배, P99 기준으로는 최대 350배까지 벌어지는 구간도 있었다고 그래프로 제시돼 있어요. 지연시간이 줄어든 결과 90번째 백분위수 쿼리 시간은 10ms 수준까지 내려갔고, Gorilla 도입 초기 초당 450건이던 조회량은 논문 발표 시점 기준 정상 상태에서 초당 5,000건, 피크 시에는 초당 최대 40,000건까지 늘었다고 해요.
신뢰성 측면의 실적도 함께 공개됐어요. 논문이 다루는 6개월 동안 네트워크 절단으로 인한 예기치 못한 장애가 3건 있었지만 서비스 중단 없이 자동으로 우회됐고, 단일 머신 장애 5건에서는 데이터 유실이나 별도 조치가 전혀 필요하지 않았다고 밝혀요. 같은 기간 이상 탐지나 알림 기능에 지장을 준 사건은 단 한 건도 없었다는 점도 이 아키텍처가 실전에서 검증됐다는 근거로 제시돼요.
홈랩 모니터링에는 어떤 의미가 있을까
Gorilla를 그대로 홈랩에 가져다 쓸 일은 거의 없지만, “최신 데이터의 가용성을 과거 데이터의 완전성보다 우선한다”는 이 논문의 설계 철학은 홈랩에서 흔히 쓰는 모니터링 도구에도 그대로 녹아 있어요. 예를 들어 Netdata가 기본적으로 최근 데이터를 메모리 링 버퍼에 담아 초 단위 해상도로 즉시 보여주는 방식이나, Prometheus와 Grafana 조합에서 최신 데이터일수록 조회가 빠르고 오래된 데이터는 보존 기간을 정해 걷어내는 운영 관행 모두 “최근 데이터가 훨씬 가치 있다”는 같은 전제를 공유해요. 다만 Prometheus의 TSDB는 Gorilla와 달리 로컬 디스크에 블록을 쓰는 구조를 기본으로 하는데, 두 설계의 구체적인 차이는 Prometheus TSDB와 Gorilla 압축 비교 포스트에서 따로 다뤄요.
결론
Gorilla 논문이 던진 질문은 결국 하나였어요. 모니터링 데이터의 대부분이 “지금 이 순간”에 대한 답을 구하는 데 쓰인다면, 굳이 모든 데이터를 디스크에 안전하게 쌓아두려고 지연시간을 희생할 이유가 있을까요. 페이스북의 답은 최근 26시간을 통째로 메모리에 올리고, 그 대신 GlusterFS 위의 로그·블록·체크포인트 파일로 재시작 복구 경로를 만들고, 지리적으로 분리된 두 리전에 합의 없는 이중 쓰기를 해두는 것이었어요. 압축률 12배, 지연시간 73배 개선이라는 숫자 뒤에는 결국 “무엇을 지키고 무엇을 포기할 것인가”라는 아주 실용적인 트레이드오프 설계가 있었던 셈이에요.
참고 자료
- Pelkonen, T., Franklin, S., Cavallaro, P., Huang, Q., Meza, J., Teller, J., Veeraraghavan, K. (2015). Gorilla: A Fast, Scalable, In-Memory Time Series Database. Proceedings of the VLDB Endowment, Vol. 8, No. 12, 41st International Conference on Very Large Data Bases, Kohala Coast, Hawaii.
자주 묻는 질문
Q1. Gorilla는 왜 디스크 기반 데이터베이스를 쓰지 않았나요?
페이스북이 기존에 쓰던 HBase 기반 저장소(ODS)를 분석해보니 전체 조회의 최소 85%가 최근 26시간 이내 데이터만 찾고 있었는데, 이 구간을 디스크에서 읽다 보니 90번째 백분위수 조회 시간이 수 초까지 늘어났기 때문이에요. 최근 데이터만이라도 메모리에 올리면 디스크 I/O 자체가 사라져 지연시간 문제를 근본적으로 없앨 수 있다고 판단한 거예요.
Q2. 메모리에만 데이터를 두면 서버가 꺼졌을 때 다 사라지는 거 아닌가요?
아니요, Gorilla는 GlusterFS라는 3중 복제 분산 파일시스템에 append-only 로그와 2시간 단위 블록 파일, 체크포인트 파일을 계속 기록해두기 때문에 프로세스가 재시작돼도 로그와 블록 파일을 읽어 메모리 상태를 복원할 수 있어요.
Q3. Gorilla의 로그는 일반적인 write-ahead log와 어떻게 다른가요?
Gorilla의 로그는 ACID를 보장하는 전통적인 WAL이 아니라 최대 64KB(보통 1~2초 분량)까지 버퍼링한 뒤에야 디스크에 쓰는 구조예요. 크래시 시 그 몇 초 분량의 데이터는 잃을 수 있지만, 그 대가로 훨씬 높은 쓰기 처리량과 가용성을 얻는 트레이드오프를 의도적으로 선택한 거예요.
Q4. 서버 한 대가 크래시하면 그 안의 데이터는 완전히 사라지나요?
정상적인 재시작이나 소프트웨어 업그레이드라면 종료 전에 모든 데이터를 디스크에 플러시하므로 유실이 없고, 예기치 못한 크래시라 해도 마지막 버퍼링 구간(최대 몇 초)만 날아갈 뿐 나머지는 로그 파일에서 복구돼요. 실제로 논문은 이런 방식으로 6개월간 발생한 5건의 단일 노드 장애에서 데이터 유실이 전혀 없었다고 밝혔어요.
Q5. 리전 하나가 통째로 사라지면 어떻게 되나요?
Gorilla는 서로 다른 지리적 리전에 완전히 독립된 인스턴스 두 개를 두고 합의 없이 각각에 데이터를 스트리밍하기 때문에, 한 리전이 완전히 죽으면 읽기 요청이 자동으로 살아있는 리전으로 전환돼요. 장애 리전이 복구돼 26시간 분량의 데이터를 다시 채울 때까지는 계속 반대편 리전이 요청을 받아줘요.
Q6. 노드가 죽었다가 재시작하면 데이터를 다시 채우는 데 얼마나 걸리나요?
논문 기준으로 한 샤드가 GlusterFS에 저장하는 데이터양은 약 16GB였고, 여러 물리 호스트에 파일이 분산돼 있어 한 호스트가 필요한 모든 샤드 데이터를 읽어 다시 서비스 가능한 상태가 되기까지 약 5분이 걸렸다고 보고했어요.
Q7. 이 논문에서 나온 압축 알고리즘 자체는 이 글에서 다루나요?
아니요, 이 글은 인메모리 아키텍처와 장애 복구 설계에 집중하고 있어 델타-오브-델타 타임스탬프 압축과 XOR 부동소수점 압축의 세부 구현은 다루지 않아요. 알고리즘 내용은 별도로 정리한 [XOR 압축 포스트](/blog/gorilla-tsdb-xor-compression)와 [delta-of-delta 인코딩 포스트](/blog/timeseries-delta-of-delta-encoding)에서 확인할 수 있어요.
관련 글


