부록

부록 (Appendix)

이 부록은 Postgres에서 ClickHouse로 옮겨 오는 사용자들이 알아야 할 개념들을 정리해요. 샤드·레플리카, 이벤트 일관성, 트랜잭션(ACID) 지원, 압축, 그리고 Postgres↔ClickHouse 데이터 타입 매핑을 다뤄요.

출처: Appendix

본문

Postgres vs ClickHouse: 동등하고 다른 개념

ACID 트랜잭션에 익숙한 OLTP 시스템 사용자들은 ClickHouse가 성능을 위해 이것을 완전히 제공하지 않는 의도적인 절충을 한다는 점을 알아야 해요. ClickHouse의 의미(semantics)는 잘 이해하면 높은 내구성 보장과 높은 쓰기 처리량을 제공할 수 있어요. Postgres에서 ClickHouse로 작업하기 전에 알아야 할 몇 가지 핵심 개념을 아래에 강조할게요.

샤드 vs 레플리카

샤딩과 복제는 스토리지/컴퓨트가 성능 병목이 될 때 하나의 Postgres 인스턴스를 넘어 확장하기 위해 사용되는 두 전략이에요. Postgres의 샤딩은 큰 데이터베이스를 여러 노드에 걸쳐 더 작고 관리하기 쉬운 조각으로 나누는 것이에요. 하지만 Postgres는 샤딩을 네이티브로 지원하지 않아요. 대신 Citus 같은 확장으로 샤딩을 달성할 수 있으며, Postgres가 수평 확장 가능한 분산 데이터베이스가 돼요. 이 접근으로 Postgres는 여러 머신에 부하를 분산해 더 높은 트랜잭션 처리율과 더 큰 데이터셋을 처리할 수 있어요. 샤드는 워크로드 유형(트랜잭션 또는 분석)에 유연성을 제공하기 위해 행 또는 스키마 기반일 수 있어요. 샤딩은 여러 머신에 걸친 조정과 일관성 보장이 필요하므로 데이터 관리와 쿼리 실행 측면에서 상당한 복잡성을 도입할 수 있어요.

샤드와 달리 레플리카는 프라이머리 노드의 데이터 전체 또는 일부를 포함하는 추가 Postgres 인스턴스예요. 레플리카는 향상된 읽기 성능과 HA(고가용성) 시나리오를 포함한 여러 이유로 사용돼요. 물리적 복제는 전체 데이터베이스나 상당 부분(모든 데이터베이스, 테이블, 인덱스 포함)을 다른 서버로 복사하는 Postgres의 네이티브 기능이에요. 이것은 TCP/IP로 프라이머리 노드에서 레플리카로 WAL 세그먼트를 스트리밍하는 것을 포함해요. 반면 논리적 복제는 INSERT, UPDATE, DELETE 작업을 기반으로 변경을 스트리밍하는 더 높은 수준의 추상화예요. 물리적 복제와 같은 결과가 적용될 수 있지만, 특정 테이블과 작업을 대상으로 하고, 데이터 변환과 다른 Postgres 버전 지원에 더 큰 유연성이 가능해요.

이와 대조적으로 ClickHouse 샤드와 레플리카는 데이터 분산과 중복(redundancy)과 관련된 두 가지 핵심 개념이에요. ClickHouse 레플리카는 Postgres 레플리카와 유사하다고 생각할 수 있지만, 복제 방식은 다르다.

ClickHouse Cloud는 S3에 백업된 단일 데이터 복사본과 여러 컴퓨트 레플리카를 사용해요. 데이터는 각 레플리카 노드에서 사용 가능하며, 각 노드는 로컬 SSD 캐시를 가져요. 이것은 ClickHouse Keeper를 통한 메타데이터 복제에만 의존해요.

이벤트 일관성 (Eventual consistency)

ClickHouse는 내부 복제 메커니즘을 관리하기 위해 ClickHouse Keeper(C++ ZooKeeper 구현, ZooKeeper도 사용 가능)를 사용하며, 주로 메타데이터 저장에 초점을 맞추고 이벤트 일관성(eventual consistency)을 보장해요. Keeper는 분산 환경에서 각 삽입에 고유한 순차 번호를 할당하는 데 사용돼요. 이것은 작업 전체에 걸쳐 순서와 일관성을 유지하는 데 중요해요. 이 프레임워크는 병합과 mutation 같은 백그라운드 작업도 처리하며, 작업이 모든 레플리카에서 같은 순서로 실행되도록 보장하면서 작업이 분산되게 해요. Keeper는 메타데이터 외에도 저장된 데이터 파트의 체크섬 추적을 포함한 복제의 포괄적인 제어 센터 역할을 하고, 레플리카 사이의 분산 알림 시스템으로 기능해요.

ClickHouse의 복제 프로세스는 (1)데이터가 어떤 레플리카에 삽입될 때 시작돼요. 이 데이터는 원시 삽입 형태로 (2)체크섬과 함께 디스크에 쓰여요. 쓰여진 후 레플리카는 (3)고유 블록 번호를 할당하고 새 파트의 세부 정보를 기록해 Keeper에 이 새 데이터 파트를 등록하려 시도해요. 다른 레플리카들은 (4)복제 로그의 새 항목을 감지하면 (5)내부 HTTP 프로토콜로 해당 데이터 파트를 다운로드하고, ZooKeeper에 나열된 체크섬에 대해 검증해요. 이 방법은 처리 속도나 잠재적 지연이 달라도 모든 레플리카가 결국 일관되고 최신의 데이터를 보유하도록 보장해요. 게다가 이 시스템은 여러 작업을 동시에 처리하고, 데이터 관리 프로세스를 최적화하며, 하드웨어 차이에 대한 시스템 확장성과 견고성을 허용해요. ClickHouse Cloud는 스토리지와 컴퓨트 분리 아키텍처에 맞춰 적응된 클라우드 최적화 복제 메커니즘을 사용한다는 점을 주의하세요. 데이터를 공유 오브젝트 스토리지에 저장함으로써 데이터는 모든 컴퓨트에 자동으로 사용 가능해져요.

사용자에 미치는 영향 (User implications)

ClickHouse에서 더티 리드(dirty read) 가능성 — 한 레플리카에 데이터를 쓰고 다른 레플리카에서 잠재적으로 복제되지 않은 데이터를 읽는 것 — 은 Keeper를 통해 관리되는 이벤트 일관성 복제 모델에서 발생해요. 이 모델은 분산 시스템에서 성능과 확장성을 강조해서, 레플리카가 독립적으로 운영되고 비동기로 동기화되게 해요. 결과적으로 새로 삽입된 데이터는 복제 지연과 변경이 시스템 전체에 전파되는 시간에 따라 모든 레플리카에서 즉시 보이지 않을 수 있어요.

반대로 PostgreSQL의 스트리밍 복제 모델은 일반적으로 동기 복제 옵션을 사용해 더티 리드를 방지할 수 있어요. 프라이머리가 트랜잭션을 커밋하기 전에 최소한 하나의 레플리카가 데이터 수신을 확인할 때까지 기다리죠. 이렇게 하면 트랜잭션이 커밋되면 데이터가 다른 레플리카에서 사용 가능하다는 보장이 생겨요. 프라이머리 장애 시 레플리카는 쿼리가 커밋된 데이터를 볼 수 있게 해서 더 엄격한 수준의 일관성을 유지해요.

권장사항 (Recommendations)

ClickHouse에 새로 온 사용자들은 복제 환경에서 나타날 이러한 차이점을 알고 있어야 해요. 일반적으로 이벤트 일관성은 수십억, 때로는 수조 개의 데이터 포인트에 대한 분석에서 충분해요 — 메트릭이 더 안정적이거나 새 데이터가 높은 속도로 지속적으로 삽입되므로 추정으로 충분한 곳이죠. 읽기의 일관성을 높여야 한다면 몇 가지 옵션이 존재해요. 두 예시 모두 복잡성이나 오버헤드가 증가해서 — 쿼리 성능을 줄이고 ClickHouse 확장을 더 어렵게 만들어요. 이 접근들은 정말 필요할 때만 권장해요.

일관된 라우팅 (Consistent routing)

이벤트 일관성의 일부 한계를 극복하려면 클라이언트가 같은 레플리카로 라우팅되도록 보장할 수 있어요. 여러 사용자가 ClickHouse를 쿼리하고 결과가 요청 간에 결정적이어야 할 때 유용해요. 결과는 다를 수 있지만, 새 데이터가 삽입됨에 따라 같은 레플리카를 쿼리하면 일관된 뷰를 보장해요. 이것은 아키텍처와 ClickHouse OSS 또는 ClickHouse Cloud 사용 여부에 따라 여러 접근으로 달성할 수 있어요.

ClickHouse Cloud

ClickHouse Cloud는 S3에 백업된 단일 데이터 복사본과 여러 컴퓨트 레플리카를 사용해요. 데이터는 각 레플리카 노드에서 사용 가능하며(로컬 SSD 캐시 보유), 일관된 결과를 보장하려면 사용자는 같은 노드로의 일관된 라우팅만 보장하면 돼요. ClickHouse Cloud 서비스의 노드와의 통신은 프록시를 통해 발생해요. HTTP와 Native 프로토콜 연결은 열려 있는 동안 같은 노드로 라우팅돼요. 대부분의 클라이언트의 HTTP 1.1 연결의 경우 이는 Keep-Alive 윈도우에 따라 달라져요. 대부분의 클라이언트(예: Node JS)에서 구성할 수 있어요. 이것은 클라이언트보다 높은 서버 측 구성도 필요하며, ClickHouse Cloud에서 10초로 설정돼 있어요. 연결 풀을 사용하거나 연결이 만료되는 경우처럼 연결 간 일관된 라우팅을 보장하려면 같은 연결이 사용되도록(네이티브에서 쉬움) 하거나 스티키 엔드포인트(sticky endpoints) 노출을 요청할 수 있어요. 이것은 클러스터의 각 노드에 대한 엔드포인트 집합을 제공해서 클라이언트가 쿼리를 결정적으로 라우팅할 수 있게 해 줘요.

스티키 엔드포인트 접근은 지원(support)에 문의하세요.

ClickHouse OSS

OSS에서 이 동작을 달성하는 것은 샤드·레플리카 토폴로지와 쿼리에 Distributed 테이블을 사용하는지에 따라 달라져요. 샤드 하나와 레플리카만 있을 때(ClickHouse가 수직 확장하므로 흔함), 사용자는 클라이언트 레이어에서 노드를 선택하고 레플리카를 직접 쿼리해서 결정적으로 선택되도록 해요. distributed 테이블 없이도 여러 샤드·레플리카 토폴로지가 가능하지만, 이런 고급 배포는 보통 자체 라우팅 인프라를 가져요. 따라서 두 개 이상의 샤드가 있는 배포는 Distributed 테이블을 사용한다고 가정해요 (distributed 테이블은 단일 샤드 배포에서 사용할 수 있지만 보통 불필요). 이 경우 session_iduser_id 같은 속성을 기반으로 일관된 노드 라우팅이 수행되도록 해야 해요. prefer_localhost_replica=0, load_balancing=in_order 설정은 쿼리에서 설정해야 해요. 그러면 샤드의 로컬 레플리카가 선호되고, 그렇지 않으면 구성에 나열된 대로 레플리카가 선호되며 — 에러 수가 같다면 — 에러가 더 많으면 무작위 선택으로 장애 조치가 발생해요. load_balancing=nearest_hostname도 이 결정적 샤드 선택의 대안으로 사용될 수 있어요.

Distributed 테이블을 만들 때 클러스터를 지정해요. config.xml에 지정된 이 클러스터 정의는 샤드(그리고 그 레플리카)를 나열해서, 사용자가 각 노드에서 사용되는 순서를 제어할 수 있게 해 줘요. 이것으로 선택이 결정적이도록 보장할 수 있어요.

순차 일관성 (Sequential consistency)

예외적인 경우 순차 일관성이 필요할 수도 있어요. 데이터베이스의 순차 일관성은 데이터베이스에 대한 연산이 어떤 순차적 순서로 실행되는 것처럼 보이고, 이 순서가 데이터베이스와 상호작용하는 모든 프로세스에 걸쳐 일관된 것을 말해요. 이는 모든 연산이 호출과 완료 사이에 순간적으로 적용되는 것처럼 보이고, 어떤 프로세스에도 모든 연산이 관찰되는 단일한 합의된 순서가 있다는 뜻이에요. 사용자 관점에서 이것은 보통 데이터를 ClickHouse에 쓰고 읽을 때 최신 삽입 행이 반환되도록 보장해야 하는 필요로 나타나요. 다음과 같은 여러 방법으로 달성할 수 있어요 (선호 순서대로):

  1. 같은 노드에 읽기/쓰기 — 네이티브 프로토콜을 사용하거나 HTTP로 쓰기/읽기를 하는 세션을 사용한다면, 같은 레플리카에 연결되어 있어야 해요: 이 시나리오에서는 쓰는 노드에서 직접 읽으므로 읽기는 항상 일관적이에요.
  2. 레플리카 수동 동기화 — 한 레플리카에 쓰고 다른 레플리카에서 읽는다면, 읽기 전에 SYSTEM SYNC REPLICA LIGHTWEIGHT를 발행할 수 있어요.
  3. 순차 일관성 활성화 — 쿼리 설정 select_sequential_consistency = 1을 통해. OSS에서는 insert_quorum = 'auto' 설정도 지정해야 해요.

이 설정을 활성화하는 자세한 내용은 여기를 참고하세요.

순차 일관성 사용은 ClickHouse Keeper에 더 큰 부하를 가할 거예요. 결과적으로 삽입과 읽기가 더 느려질 수 있어요. ClickHouse Cloud의 주요 테이블 엔진인 SharedMergeTree는 순차 일관성 오버헤드가 더 적고 더 잘 확장됩니다. OSS에서는 이 접근을 신중하게 사용하고 Keeper 부하를 측정해야 해요.

트랜잭션 (ACID) 지원

PostgreSQL에서 마이그레이션하는 사용자는 ACID(원자성, 일관성, 격리성, 내구성) 속성에 대한 강력한 지원에 익숙할 수 있으며, 트랜잭션 데이터베이스에 신뢰할 수 있는 선택이에요. PostgreSQL의 원자성은 각 트랜잭션이 완전히 성공하거나 완전히 롤백되는 단일 단위로 취급되어 부분 업데이트를 방지함을 보장해요. 일관성은 모든 데이터베이스 트랜잭션이 유효한 상태로 이끌도록 보장하는 제약, 트리거, 규칙을 강제함으로써 유지돼요. Read Committed부터 Serializable까지의 격리 수준은 PostgreSQL에서 지원되어, 동시 트랜잭션의 변경 가시성에 대한 세밀한 제어를 허용해요. 마지막으로 내구성은 쓰기-어헤드 로깅(WAL)으로 달성되어, 트랜잭션이 커밋되면 시스템 장애가 발생해도 유지됨을 보장해요. 이러한 속성은 진실의 원천(source of truth)으로 작동하는 OLTP 데이터베이스에 일반적이에요.

강력하지만 이는 본질적인 한계와 함께 오며 PB 규모를 어렵게 만들어요. ClickHouse는 높은 쓰기 처리량을 유지하면서 규모에서 빠른 분석 쿼리를 제공하기 위해 이러한 속성을 절충해요. ClickHouse는 제한된 구성에서 ACID 속성을 제공해요 — 가장 간단하게는 파티션 하나를 가진 비복제 MergeTree 테이블 엔진 인스턴스를 사용할 때예요. 이러한 경우를 벗어나서는 이 속성을 기대하지 말고 요구사항이 되지 않도록 해야 해요.

압축 (Compression)

ClickHouse의 컬럼 지향 저장은 Postgres와 비교해 압축이 종종 훨씬 더 우수하다는 것을 의미해요. 두 데이터베이스의 모든 Stack Overflow 테이블에 대한 저장 요구사항을 비교할 때 다음이 설명돼요:

Query (Postgres):

SELECT
    schemaname,
    tablename,
    pg_total_relation_size(schemaname || '.' || tablename) AS total_size_bytes,
    pg_total_relation_size(schemaname || '.' || tablename) / (1024 * 1024 * 1024) AS total_size_gb
FROM
    pg_tables s
WHERE
    schemaname = 'public';

Query (ClickHouse):

SELECT
        `table`,
        formatReadableSize(sum(data_compressed_bytes)) AS compressed_size
FROM system.parts
WHERE (database = 'stackoverflow') AND active
GROUP BY `table`

Response:

┌─table───────┬─compressed_size─┐
│ posts       │ 25.17 GiB       │
│ users       │ 846.57 MiB      │
│ badges      │ 513.13 MiB      │
│ comments    │ 7.11 GiB        │
│ votes       │ 1.28 GiB        │
│ posthistory │ 40.44 GiB       │
│ postlinks   │ 79.22 MiB       │
└─────────────┴─────────────────┘

압축 최적화와 측정에 대한 자세한 내용은 여기에서 확인할 수 있어요.

데이터 타입 매핑 (Data type mappings)

다음 표는 Postgres에 대한 동등한 ClickHouse 데이터 타입을 보여줘요.

Postgres 데이터 타입 ClickHouse 타입
DATE Date
TIMESTAMP DateTime
REAL Float32
DOUBLE Float64
DECIMAL, NUMERIC Decimal
SMALLINT Int16
INTEGER Int32
BIGINT Int64
SERIAL UInt32
BIGSERIAL UInt64
TEXT, CHAR, BPCHAR String
INTEGER Nullable(Int32)
ARRAY Array
FLOAT4 Float32
BOOLEAN Bool
VARCHAR String
BIT String
BIT VARYING String
BYTEA String
NUMERIC Decimal
GEOGRAPHY Point, Ring, Polygon, MultiPolygon
GEOMETRY Point, Ring, Polygon, MultiPolygon
INET IPv4, IPv6
MACADDR String
CIDR String
HSTORE Map(K, V), Map(K,Variant)
UUID UUID
ARRAY<T> ARRAY(T)
JSON String, Variant, Nested, Tuple
JSONB String

더 알아보기 (Learn more)