scalalang2

Citus | Postgres 분산 데이터베이스 A-Z 소개

오랜만에 논문 한편을 읽었다.

사실 난 논문 읽는 것을 좋아한다고 하면 너무 찐인 것 같아서, 싫어하진 않는다고 말하겠다. 조직 생활을 시작하면서 대학원에서는 일상이었던 논문 리딩을 거의 하지 못했다. 마치 전공수업으로만 한 학기 다 채우다가 다음 학기에 교양과목을 들으면서 힐링하듯이, 코드만 매일 작성하다가 힐링으로 논문을 읽고 싶다고 생각했었는데, 2022년 If Kakao에서 엘라스틱 서치의 대안으로 Citus(이하 시투스라고 쓰겠음)를 쓰고 있다는 내용을 보고 난 후, 흥미가 생겨서 오랜만에 논문 한 편을 읽었다. 바로 알아가보자.

엘라스틱 서치 대신에 PostgreSQL을 쓴다고?! 흥미롭지 않을 수 없다.

Citus 논문 및 저자 소개

Citus는 2021년 SIGMOD 에서 산업 트랙으로 소개된 논문이다. 제목은 Citus: 데이터 중심 애플리케이션을 위한 분산 포스트그레스큐엘 로 포스트그레스큐엘을 분산 데이터베이스로 만들었다는 것을 나타낸다. 논문의 저자들의 소속이 마이크로소프트 라고 나와 있지만, 논문의 저자들은 이전 소속이 Citus Data란 회사에서 각각 CEO, CTO 등을 맡고 있는 것으로 봐서는 마이크로 소프트로 인수되고 나서 논문으로 발표하게 된 것 같다. Citus는 발표 시기만 2021년일 뿐 사실은 예전부터 개발되고 사용되어 왔다.

왜 Citus인가?

① 왜 분산 포스트그레스큐엘(PostgreSQL) 이 필요한가?

PostgreSQL은 RDBMS의 한 종류이며 매우 강력한 커뮤니티를 가지고 있다. 주요 데이터베이스 시스템에서 사용할 수 있으며, 다양한 산업군에서 필요한 기능을 제공한다. (e.g. geospatial query, gin index 등)

PostgreSQL를 이용하는 사례가 점점 늘어나고, 데이터의 규모가 확장되면서 기존 단일 서버 구조를 수평 확장 해야 한다는 요구사항이 있었고, 그래서 우리 Citus가 이를 해내었다.

② 다른 제품들과는 무엇이 다른가?

기존 RDBMS를 분산 확장하고자 한 프로젝트는 굉장히 많았다. ( e.g. Vitess, Cockroach, Spanner 등 ), 그들은 보통 아래 3가지 접근법 중 하나를 이용해서 개발되었다.

  1. 새로운 데이터베이스를 만들고 기존 RDBMS와 호환되도록 드라이버와 SQL을 제공한다.
  2. 기존 데이터베이스를 포크해서 기능을 추가한다.
  3. 애플리케이션과 데이터베이스의 미들웨어 성격으로 개발해서 기능을 제공한다.

그러나 이 세 방법들은, 데이터베이스의 발전 속도를 따라가기 힘들다는 문제가 있다. PostgreSQL은 발전이 멈춘게 아니다. 새로운 SQL의 표준을 개발하고, 새로운 기능을 만들기 때문에 이것을 모두 호환하도록 만드는 것은 물리적으로 매우 어려운 일이다.

Citus는 확장 API를 이용해서 개발했기 때문에 기존 PostgreSQL의 모든 기능과 호환되면서 분산 데이터베이스를 만들었다는 것이 특징이다. 물론 데이터베이스를 분산 시키는 것이 모든 경우에 대해서 성능 향상을 시켜주는 건 아니다. 논문에서는, 아래 네 가지 경우에 해당한다면 Citus가 매우 유용할 것이라고 강조한다.

목차

시투스가 필요한 4가지 워크로드 패턴

4가지 워크로드의 목표 성능

위 테이블은 4가지 워크로드 패턴들의 목표 성능을 나타낸다. 차례로 다음과 같다.

멀티 테넌트 서비스

멀티테넌트 서비스의 간단한 메시징 서비스 구조

멀티 테넌트는 하나의 데이터베이스를 여러명의 고객들에게 제공하는 것을 말한다. 가장 쉽게 멀티 테넌트 서비스를 구현하는 방법은 매뉴얼하게 샤딩해서 제공하는 것이다. 조금 더 어렵게 구현한다면 모든 테이블에 Tenant-ID 라는 고객을 구분할 수 있는 컬럼을 추가해서 같은 데이터베이스에서 구분하는 것이다. 이런 방식을 이용해서 구현하려면 아래 같은 기능을 제공해야 한다.

그리고 Citus는 이런 기능들을 잘 제공하고 있다.

실시간 분석

실시간 분석 시스템으로는 이상 감지, 시스템 모니터링, 행동 분석, 지리/공간 쿼리, 대시보드 등이 있다. 이런 실시간 환경에서는 높은 쓰기 처리량(High write throughput) 과 대규모의 분석 쿼리를 데이터베이스가 감당할 수 있어야 한다. 쿼리는 데이터 볼륨과 상관없이 1초 이내(sub seconds) 내로 응답을 줘야 한다. 다행히도, 대 부분의 경우에서는 어떤 쿼리를 수행하는지 미리 개발자가 알 수 있기 때문에, 인덱스와 롤업(ROLLUP) 을 이용해서 응답 시간을 줄일 수 있다.

PostgreSQL은 힙 스토리지와 COPY 명령어로 데이터를 아주 빠르게 저장할 수 있고, MVCC는 분석 쿼리와 쓰기 요청이 공존할 수 있게 해준다. 또한, 배열, JSON, 커스텀 타입 등 다양한 데이터 타입을 지원한다. 단 하나, PostgeSQL 혼자 못하는 것은 이런 대규모의 데이터를 저장하면 쉽게 단일 서버의 용량을 넘긴다는 것 뿐이다. 그래서 Citus는 아래 기능을 제공해서 PostgreSQL이 실시간 분석 시스템에서 활용 될 수 있게 돕는다.

대규모 CRUD

단일 서버의 PostgreSQL는 대규모 CRUD 요청을 받기 힘들다.

이를 해결하는 시투스의 방법은 단순하다. 분산 키를 기준으로 테이블을 각 워커로 분산시켜서 CRUD 성능을 높인다. 쿼리 종류에 따라 어느 샤드에서 데이터를 가져올 지 매우 빠르게 결정하고 가져올 수 있다. 또한, Auto-vacuuming이 모든 코어에 대해서 병렬적으로 작동하게 된다.

데이터 웨어하우스

데이터웨어 하우스는 보통 조직에서 애플리케이션 레이어와 독립적으로 구축해서 분석 쿼리를 날리는 곳을 지칭한다. 데이터 웨어하우스는 보통 레이턴시나 목표 성능이 없는 것이 특징이지만, 대용량의 데이터를 빠르게 쿼리할 수 있어야 하고 쿼리 플랜도 효율적으로 만들어줘야한다.

논문에서는 솔직히 AWS Redshift나 다른 웨어하우스 전용 서비스들이 훨씬 성능이 높다고 고백한다. 다만, PostgreSQL로도 충분히 할 수 있는 일이고 조금 느려도 된다면 Why not? 이라는 질문을 던진다.

데이터 웨어하우징 서비스를 스케일 업을 하려면 아래 기능이 필요하다.

Citus는 지금 대부분의 기능을 지원하나, non-co-located join 에 대해서는 몇 가지 한계가 있다고 한다.

우리는 이렇게 시투스(Citus)를 개발했다.

여기서부터는 논문에서 어떻게 시투스를 개발했는지, 시투스는 어떤 구조를 가지고 있는지 설명한다.

강력한 확장 API

PostgreSQL이 커뮤니티로부터 엄청난 호응을 받을 수 있던 이유 중 하나는 아마 확장 API 때문일 것이다. 확장 API는 데이터베이스의 원본 코드를 고치지 않더라도 기능을 추가할 수 있게 제공한다. 시투스는 이 확장 API를 이용해서 만들어 졌기 때문에 PostgreSQL의 대부분의 기능과 호환된다. 단순하지만 매우 강력한 특징이다.

PostgreSQL은 파서 빼고는 모든 것이 모듈식 구조로 구성되어 있어서 거의 모든 부분에 커스텀한 기능을 추가할 수 있다.

이걸 다 활용하면 바로 너, 여러분도 시투스를 만들 수 있다.

시투스 아키텍처

시투스 데이터베이스의 구성을 보여준다.

위 그림은 기본적인 시투스 아키텍처를 나타낸다. 아키텍처는, 하나의 코디네이터(Coordinator)와 다수의 워커 노드(Worker-node)가 존재한다. 시투스를 처음 설치할 때 서버 하나를 추가하면 자동으로 코디네이터가 된다.

우리가 데이터베이스를 이용하려고 DSN을 입력해서 연결을 맺는다면, 코디네이터의 주소를 이용해야 한다. 코디네이터는 1️⃣ 사용자의 요청을 받아서 쿼리 플랜을 만들고, 각 워커노드로 요청을 내린다. 2️⃣ 필요에 따라서는 분산 트랜잭션을 실행하고 2PC의 라이프 사이클을 책임진다. 3️⃣ 각 워커노드로 어떤 데이터를 저장할지 결정하고 필요에 따라 리밸런싱을 수행하는 주체이기도 하다 워커 노드는 실제로 데이터를 샤드 단위로 저장하는 역할을 한다. 코디네이터로 부터 받은 쿼리 명령을 수행하고 결과를 코디네이터로 전송한다.

클러스터가 작은 경우엔 코디네이터가 혼자 워커노드의 역할 까지 담당한다. 그래서 시투스 클러스터의 가장 작은 단위는 노드 1대 이다.

Q. 코디네이터가 병목이 될 수 있지 않나?
A. 코디네이터가 관리하는 메타 데이터를 전부 워커 노드로 복제하고 모든 워커노드를 코디네이터로 만드는 옵션이 있다고 한다. 이 경우엔 클라이언트 사이드에서 로드 밸런싱을 다수의 워커 노드에 걸어서 사용해야 한다.
일단 논문에서는 이 옵션을 되도록이면 사용하지 말라고 말한다. 그 이유는 각 노드를 코디네이터로 만들고 서로, 서로 연결을 맺어야 하는 분산 트랜잭션을 계속 날리면 커넥션 수가 부족하고 오히려 병목이 된다고 한다.

시투스의 데이터 타입 ❗️❕

사실, 시투스를 이용하는 애플리케이션 개발자 입장에서는 이 내용이 가장 중요하다. 시투스는 분산 테이블레퍼런스 테이블, 이 2가지 데이터 타입을 지원한다.

분산 테이블

분산 테이블은 분산 컬럼 항목에 대해서 해시 파티션을 수행해서 각 샤드에 논리적으로 배치하는 것을 말한다. 해시 파티션을 이용하면 샤드 간에 리밸런싱을 자주 하지 않아도 데이터 분산이 고르게 잘되고 locality를 줄 수 있다. 예를 들어 유저 키를 분산 컬럼으로 지정하면 동일한 유저의 데이터는 동일한 샤드와 워커 노드에 저장될 것이다.

분산 테이블을 만들 때 코로케이션(co-location)을 지정할 수 있다. 코로케이션이란 서로 다른 테이블에 저장된 데이터가 동일한 워커 노드에 저장되었으면 좋겠다고 선언하는 것이다.

CREATE TABLE other_table (...);
SELECT create_distributed_table(

‘other-table’,
‘distribution_column’, colocate_with := ‘my_table’
);

레퍼런스 테이블

모든 워커노드에 복제되는 테이블을 레퍼런스 테이블이라고 부른다. 마찬가지로 조인이나 각종 쿼리에 있어서 디펜던시를 가지고 있을 때 유용하다.

CREATE TABLE dimensions (...);
SELECT create_reference_table ('dimensions');

데이터 리밸런싱

잘 운영되고 있는 클러스터에 용량이 부족해서 새로운 워커 노드를 추가하고 싶을 수 있다. 혹은 애플리케이션 특성에 따라 핫스팟이 발생할 수 있다. 시투스는 필요한 경우 샤드 리밸런서가 샤드를 각 노드간 재배치를 수행한다. 샤드가 재배치 되고 있는 와중에도 read/write 요청은 그대로 수행이 가능하다. 새로운 워커노드가 기존 샤드를 모두 복제하고 나서 요청을 모두 위임받게된다.

분산 쿼리 플래너

쿼리를 날리면 플랜에 따라 3가지 종류로 나뉜다.

쿼리 특징에 따라 실제로 동작하는 방식을 4가지로 구분한 그림

분산 쿼리 실행

CustomScan은 Adpative Executor라는 서브 플랜을 생성해서 분산 쿼리를 날린다.

만약, Logical Pushdown Planner 처럼 여러 워커노드에 걸쳐서 쿼리 요청을 보내고 데이터를 받아야 하는 경우에는 Adaptive Executor라는 서브 플랜을 만든다. 논문에서는 Adaptive Executor의 성능을 높이기 위해 slow start 라는 알고리즘을 좀 더 소개하는데 여기서는 커넥션을 관리하는 알고리즘이라고만 말하고 스킵하겠다.

slow start : Adaptive executor가 만들어 질 때마다 커넥션을 새로 맺으면 매우 느리고, 그렇다고 커넥션을 만들어 놓고 대기하면 트래픽이 없을 때는 커넥션이 놀고 있을 것이다. 그걸 관리하는 방법이다.

분산 트랜잭션

시투스는 분산 트랜잭션을 제공한다. 그래서 트랜잭션 기능을 편하게 이용하면 된다.

시투스 분산 트랜잭션은 2PC를 이용한다.

2PC는 단순히 코디네이터가 모든 워커 노드로 요청을 보내고 투표를 받아서 처리하고, 그 결과를 커밋하는 방식을 말한다. 데이터 중심 애플리케이션 설계 라는 책에서는 2PC를 결혼식에서 주례사가 신랑 신부에게 이 결혼을 동의하냐는 질문을 하고 응답을 받으면 결혼이 성사되었다고 말하는 과정에 비유한다.

2PC가 모든것이 잘 동작하는 해피 패스에서는 유용하나, 장애 상황이 발생하면 매우 까다롭기 때문에 이에 대한 대비가 필요하다. 아래는 2PC 분산 트랜잭션의 제약사항을 나열한 것이다.

분산 교착 상태 감지

교착상태(dead-lock)은 서로가 서로에게 필요한 자원을 달라고 하는 상황을 말한다. 시투스 처럼 분산 시스템에서는 재수 없으면 아래와 같이 분산된 형태의 교착 상태가 발생할 수 있다고 한다.

분산 교착 상태의 예시

트랜잭션을 계속 날리다가 분산 교착상태가 발생한다면 연결된 트랜잭션 중 하나를 취소(Abort) 시켜야 한다. Google의 Spanner나 Cockroach DB는 wound-wait 라는 알고리즘을 사용한다고는 하는데 시투스는 단순한 방법을 선택한다.

주기적으로 모든 워커 노드에게서 락이 걸린 관계 $A -> B$를 받는다. 코디네이터에서 이를 그래프로 만드는데 만약 사이클을 형성한다면 사이클이 되는 간선 중에서 하나를 선택해 해당 트랜잭션을 취소 시킨다.

다중 노드에 걸친 트랜잭션의 한계

시투스에서 분산 트랜잭션은 원자성, 일관성, 영속성은 지키지만 스냅샷 격리 보장 은 안된다.

고가용성과 백업 (HA and Backup)

고가용성(HA)

기본적으로 PostgreSQL의 기본 복제 기능을 이용한다.

백업

논문에서는 백업도 지원한다고 하는데, 시투스 클러스터를 대규모로 운영하면 백업이나 복구가 쉽지는 않을 것 같다. 논문을 읽어봐도 무슨 소린지 제대로 이해를 못해서 여기서는 그냥 적지 않겠다.

성능

지금부터 논문에서 소개한 성능 지표를 차례로 후려쳐서 소개할 것이다. 먼저, 성능 지표의 대략적인 기준은 아래와 같다.

성능 측정 기준

비교군

개인적으로 데이터 비교군이 4개 밖에 없는게 아쉽다. 마이크로소프트면 돈 좀 더 써서 워커노드 16개, 32개, 64개 등 비교 집단을 늘려보지.. 8개는 이것만 보고 데이터베이스를 선택하기에는 조금 부족한 데이터라고 느껴졌다.

① 멀티 테넌트 서비스

멀티 테넌트 서비스의 성능

시투스를 1대만 이용할 경우는 PostgreSQL 1대를 이용하는 경우보다는 무조건 느리다. 쿼리 플랜이 더 추가되었기 때문에 오버헤드가 있기 때문이다. 그렇지만 클러스터에 워커 노드를 4대만 추가해도 1대의 경우보다 13배의 성능 향상이 있었다고 한다. 물론 그 이후부터는 8개로 노드를 늘려도 sub-linear 하게 증가했다고 한다.

② 분산 트랜잭션 성능

50GB의 데이터를 일단 생성해본다.

BEGIN TRANSACTION;\

UPDATE a1 SET v = v + :d WHERE key = :key1;
UPDATE a2 SET v = v + :d WHERE key = :key2;

COMMIT TRANSACTION;

위 쿼리에서 key1, key2를 랜덤으로 생성해보면서 트랜잭션을 날려보면서 성능을 측정했다고 한다. 아래 그림에서 Same Key는 키가 동일해서 하나의 워커 노드에서 처리가 가능한 트랜잭션이다. 키가 다른 경우에는 분산 트랜잭션으로 실행되는 경우가 된다. 실험 결과 분산 트랜잭션이 약 20~30% 정도 느리다고 한다.

분산 트랜잭션의 성능

주요 포인트는, 그래도 노드를 늘리면 스케일 아웃은 된다는 점이다.

③ 실시간 벤치 마크

깃허브에서 공개한 데이터를 기준으로 실험했다고 한다. 단순하게 말해서 시투스가 성능이 좋았다고 한다.

깃허브 데이터를 이용한 실시간 분석 성능

④ 대규모 CRUD 측정

YCSB 데이터 측정 도구를 이용해서 미리 1억 건의데이터를 저장하고, 1시간 동안 256개의 스레드로 절반의 쓰기 요청과 절반의 읽기 요청을 보내면서 측정했다.

대규모 CRUD 측정 결과

특히 CRUD 같은 경우는 하나의 워커 노드에 위임하는 비율이 높을 것이라서 성능 체감이 잘될 것 같다. 분산 트랜잭션을 쓰는 경우라면 그렇게 드라마틱한 성능 향상은 기대하지 못할 수도 있다. 그리고 CRUD처럼 단순한 요청이 아니라 비즈니스가 조금 복잡한데 강력한 일관성 모델이 필요한 경우라면 시투스를 쓰기 어려울 수도 있다. 그래서 토스의 코어 뱅킹 서버도 데이터베이스 1대로 이루어져 있다고 한다.

그리고 데이터 웨어하우징에 대한 실험 결과도 논문에 나오는데, 똑같이 성능 좋았다라고 반복하고 있어서 여기서는 스킵한다.

마이크로소프트는 시투스를 어떻게 쓰고 있는가?

마이크로스프트는 시투스를 윈도우즈 데이터를 분석하는데 쓰고 있다. 만약 여러분이 윈도우 PC를 키고 데이터 공유를 허용했다면 시투스 데이터베이스로 메트릭이 전송되고 있을지도 모른다

VeniceDB

VeniceDB를 초기 설계할 때 많은 분산 DB와 데이터 처리 시스템을 고려했는데 최종적으로는 시투스를 쓰게 되었다고 한다. 이유는 아래와 같음 👇

결론 및 내 생각

오랜만에 논문을 읽고 한 번 가볍게 정리해 보았다. 시투스는 강력한 분산 데이터베이스 시스템으로 수평 확장이 된다는 것(비록 sublinear하게 증가하지만)과 PostgreSQL과의 호한성을 가진다는 점에서 매력적인 시스템이다.

하지만, 현실적으로는 Managed DB 형태의 서비스를 Azure에서만 제공한다는 점 때문에, AWS나 다른 클라우드를 주로 쓰는 팀이라면 회사가 전폭적인 지원(DBA도 붙여주고, DevOps도 붙여주고..)을 해주지 않는다면 실제로 사용해보기에는 매우 어려울 것 같다.

데이터 웨어하우스 처럼 구축 하면서 조금 잘 안되거나 망가져도 실제 서비스에 영향을 주지 않으면 시도 해볼만 할텐데 실제 비즈니스와 연동되는 로직을 시투스로 쓴다면 리더의 결단과 많은 검증이 필요해 보인다.