홈으로 돌아가기

특수 ClickHouse 엔진: MergeTree가 필요하지 않은 경우

이 문서는 표준 MergeTree가 최적이 아닌 작업을 위한 특수 ClickHouse 엔진을 설명합니다: 임시 데이터 및 캐시용 Memory, 고빈도 삽입 버퍼링(Too many parts 방지)용 Buffer, Materialized View를 통한 스트림 처리 구성용 Null, 작은 참조 테이블용 Log 계열, 외부 데이터용 URL/File/S3, 실시간 액세스용 PostgreSQL ENGINE. 초당 10,000개 이상의 이벤트를 위한 Kafka → Buffer → MergeTree 아키텍처 패턴이 제시됩니다.

특수 ClickHouse 엔진: Memory, Buffer, Null 등
Advertisement 728x90

특별한 ClickHouse 엔진: MergeTree가 적합하지 않을 때

1. Memory 엔진 — 임시 데이터를 위한 RAM 테이블

배치를 빠르게 처리해야 한다고 상상해 보세요. 그룹화하고 중간 합계를 계산한 후 메인 테이블로 보내야 합니다. 데이터가 일시적이고 쿼리 기간 동안만 필요하기 때문에 디스크에 쓰고 싶지 않습니다.

Memory 엔진은 데이터를 전적으로 RAM에 저장합니다. 디스크 작업, 압축, 인덱스(기본 키 제외)가 없으므로 가장 빠른 엔진입니다. 하지만 대가가 있습니다: ClickHouse가 재시작되면 테이블이 비워집니다. 데이터가 유지되지 않습니다.

사용 시기:

Google AdInline article slot
  • ETL 프로세스의 스테이징 테이블. 예를 들어, Kafka에서 백만 개의 배치를 로드하고 중복을 제거한 후에야 메인 MergeTree 테이블에 삽입합니다.
  • 실시간 배당률 캐시 — 배당률이 매초 변경되며, 기록을 저장할 필요 없이 현재 스냅샷만 필요합니다.
  • 작은 조회 테이블(최대 1,000만~1,500만 행)로, 스크립트 실행 시마다 다시 생성됩니다.

실시간 배당률 캐시 예시:

-- 현재 배당률 테이블 (RAM에 상주)
CREATE TABLE live_odds_cache
(
    event_id     UInt64,          -- 이벤트 ID (경기)
    market_id    UInt32,          -- 마켓 ID
    selection_id UInt32,          -- 선택 ID
    odds         Decimal(10,3),   -- 배당률
    updated_at   DateTime
)
ENGINE = Memory()
ORDER BY (event_id, market_id, selection_id);   -- ORDER BY는 필수지만 인덱스는 비효율적

데이터 삽입 (예: 스트림에서):

-- 새 배당률 도착, 삽입
INSERT INTO live_odds_cache VALUES (100500, 10, 200, 1.85, now());

-- 배팅을 위한 현재 배당률 읽기
SELECT odds FROM live_odds_cache 
WHERE event_id = 100500 AND market_id = 10 AND selection_id = 200;

주의사항:

Google AdInline article slot
  • Memory 테이블은 병합을 지원하지 않습니다. UPDATE(취소와 함께 삽입)를 많이 하면 메모리가 부풀어 오릅니다. TRUNCATE로 지우세요.
  • ClickHouse 재시작 시 데이터가 손실됩니다. 중요한 데이터를 여기에 저장하지 마세요.
  • 테이블 크기는 사용 가능한 RAM에 의해 제한됩니다. 64GB RAM 서버에서 테이블이 50GB로 커지면 서버가 다운됩니다.

비유: Memory 엔진은 화이트보드와 같습니다. 쓰기도 빠르고 읽기도 빠르지만, 청소부(재시작)가 오면 보드는 비워집니다.

2. Buffer 엔진 — 쓰기 전 INSERT 버퍼링

초당 10,000개의 배치가 있습니다. 각 배치는 별도의 INSERT입니다. 각각을 직접 MergeTree 테이블에 쓰면 ClickHouse가 수천 개의 작은 파트를 생성하여 백그라운드 병합 속도를 늦추고 성능을 저하시킵니다.

Buffer 엔진이 이 문제를 해결합니다: 메모리 버퍼에 INSERT를 모으고 조건(행 수, 크기 또는 시간)이 충족되면 대상 테이블에 큰 배치로 씁니다.

Google AdInline article slot

매개변수가 있는 구문:

CREATE TABLE bets_buffer AS bets   -- bets 테이블의 구조 복사
ENGINE = Buffer(
    'default',        -- 대상 테이블 데이터베이스 이름
    'bets',           -- 대상 테이블 이름 (데이터가 여기로 플러시됨)
    16,               -- 병렬 플러시 스레드 수
    10,               -- 최소 지연 시간(초) (min_time)
    100,              -- 최대 지연 시간(초) (max_time)
    10000,            -- 플러시를 위한 최소 행 수
    1000000,          -- 플러시를 위한 최대 행 수
    10000000,         -- 플러시를 위한 최소 바이트 크기
    100000000         -- 플러시를 위한 최대 바이트 크기
);

플러시 매개변수:

매개변수 의미
min_time 10초 10초 이전에는 플러시하지 않음
max_time 100초 100초 이후에는 반드시 플러시
min_rows 10,000 10,000행이 쌓이면 플러시 가능
max_rows 1,000,000 100만 행이 쌓이면 긴급 플러시
min_bytes 10MB 10MB가 쌓이면 플러시 가능
max_bytes 100MB 100MB가 쌓이면 긴급 플러시

실제 작동 방식:

  1. bets_buffer에 삽입합니다(빠름, 메모리에만 쓰기).
  2. ClickHouse는 충분한 데이터가 쌓일 때까지 기다립니다(예: 100,000행 또는 30초 경과).
  3. 그런 다음 비동기적으로(백그라운드에서) 배치를 메인 bets 테이블(MergeTree)로 플러시합니다.
  4. 결과적으로 bets는 큰 파트(100,000행)를 받아 백그라운드 병합 속도가 빨라집니다.

MergeTree에 직접 삽입하지 않는 이유는? MergeTree에 각 INSERT는 미니 파트를 생성합니다. 초당 10,000개의 INSERT를 수행하면 1분 후에 600,000개의 파트가 생깁니다. 백그라운드 병합이 따라잡을 수 없습니다. ClickHouse는 Too many parts 오류를 내고 삽입 속도가 느려집니다.

주의사항: ClickHouse 재시작 시 버퍼가 손실됩니다. bets로 플러시되지 않은 데이터는 사라집니다. 따라서 Buffer 엔진은 몇 초의 데이터 손실이 허용되는 경우에만 사용하세요(예: 잔액이 아닌 분석용).

3. Null 엔진 — 데이터 블랙홀

Null 엔진은 데이터를 그냥 흡수합니다. 어디에도 기록되지 않고 저장되지 않으며 인덱싱되지 않습니다. 하지만 트릭이 있습니다: Null 엔진이 있는 테이블에 Materialized View가 있으면 해당 뷰가 데이터를 받아 처리합니다.

패턴: Kafka → Null + Materialized View → MergeTree

이것은 Kafka에서 높은 부하의 삽입을 위한 고전적인 아키텍처입니다.

-- 1단계: 싱크 테이블 (블랙홀)
CREATE TABLE bets_null
(
    user_id     UInt64,
    amount      Decimal(18,2),
    created_at  DateTime
)
ENGINE = Null;   -- 아무것도 저장하지 않음

-- 2단계: 대상 테이블 (실제 저장되는 곳)
CREATE TABLE bets
(
    user_id     UInt64,
    amount      Decimal(18,2),
    created_at  DateTime
)
ENGINE = MergeTree()
ORDER BY (created_at, user_id);

-- 3단계: Materialized View (브리지)
CREATE MATERIALIZED VIEW bets_mv TO bets AS
SELECT * FROM bets_null;   -- bets_null에 들어가는 모든 데이터는 bets에 도착

이제 일어나는 일:

-- 클라이언트(또는 Kafka consumer)가 bets_null에 삽입
INSERT INTO bets_null VALUES (123, 100.00, now());   -- 즉시

-- 데이터는 Materialized View를 통과하여 bets에 저장됨
-- bets_null 자체에는 저장되지 않음

왜 이렇게 하나요?

  • bets_null은 매우 가벼운 테이블입니다. 디스크에 파일을 생성하지 않습니다.
  • 모든 구독자(Materialized View)가 동시에 데이터를 받습니다.
  • 하나의 Null 테이블에 여러 Materialized View를 연결할 수 있습니다: 하나는 MergeTree에 원시 데이터, 다른 하나는 AggregatingMergeTree에 집계, 또 다른 하나는 ReplacingMergeTree에 중복 제거.

비유: Null 엔진은 바닥에 구멍이 뚫린 우편함과 같습니다. 편지가 들어오지만 머물지 않습니다. 하지만 모든 비서(Materialized View)가 편지를 읽고 자신의 폴더에 복사합니다.

4. Log/TinyLog/StripeLog — 소규모 데이터를 위한 단순 엔진

이 엔진군은 성능과 인덱스가 필요 없는 소규모 테이블(최대 100만~200만 행)을 위한 것입니다.

엔진 특징 사용 시기
TinyLog 컬럼당 하나의 파일 매우 작은 테이블(<10만 행), 스테이징
Log 각 컬럼이 별도 파일, 병렬 읽기를 위한 마커 있음 최대 100만 행 테이블, 빠른 읽기 필요
StripeLog 모든 컬럼이 하나의 파일(컴팩트) 공간 절약, 드문 읽기

리그 조회 테이블(200행) 예시:

-- 축구 리그 조회 (한 달에 한 번 변경)
CREATE TABLE leagues_ref
(
    league_id   UInt32,
    name        String,
    country     String,
    updated_at  Date
)
ENGINE = TinyLog();   -- 가능한 한 단순, ORDER BY 없음

MergeTree를 사용하지 않는 이유? MergeTree는 인덱스, 파티션, 압축을 생성하는데, 200행에는 과합니다. TinyLog는 공간을 덜 차지하고 유지보수가 더 간단합니다.

주의사항: 이 엔진들은 ALTER DELETEALTER UPDATE를 지원하지 않습니다. 데이터를 수정해야 한다면 테이블을 다시 생성해야 합니다.

5. URL 엔진 — HTTP 엔드포인트로서의 테이블

URL 엔진을 사용하면 HTTP 소스(API)에서 직접 데이터를 읽고 PUT을 통해 데이터를 삽입할 수도 있습니다.

CREATE TABLE currency_rates_url
(
    base   String,
    rate   Decimal(10,4),
    date   Date
)
ENGINE = URL('https://api.exchangerate.com/latest?base=USD', CSV)
SETTINGS 
    method = 'GET',
    format = 'CSV',
    headers = 'Authorization: Bearer token123';

사용법:

-- API에서 직접 현재 환율 읽기
SELECT * FROM currency_rates_url;

실제 시나리오: ETL을 설정하고 싶지 않은 소규모 분석 작업. 예를 들어, 한 시간에 한 번 무료 API에서 환율을 읽고 배치와 조인하여 금액을 재계산합니다.

주의사항:

  • 인덱스가 없습니다. 각 쿼리는 소스 전체를 스캔합니다.
  • API가 오류를 반환하면 쿼리가 실패합니다.
  • 높은 부하의 쿼리에는 적합하지 않습니다(데이터가 ClickHouse 내부에 캐시되고 API에서 매번 읽지 않는다고 가정).

6. File 엔진 — 디스크의 파일로서의 테이블

ClickHouse 서버의 로컬 파일 시스템에 있는 파일을 읽고 쓸 수 있습니다. CSV, TSV, JSONEachRow, Parquet 형식을 지원합니다.

-- CSV 파일을 읽는 테이블
CREATE TABLE imported_players
(
    user_id UInt64,
    username String
)
ENGINE = File(CSV, '/var/lib/clickhouse/user_files/players.csv');

사용 시기:

  • 파일에서 데이터 로드(관리자가 새 사용자가 포함된 CSV를 배치함).
  • INSERT INTO ... SELECT를 통해 쿼리 결과를 파일로 내보내기.

주의사항: ClickHouse는 폴더에 접근할 수 있어야 합니다(보안상 일반적으로 /var/lib/clickhouse/user_files/).

7. S3 엔진 — S3에 대한 직접 쿼리

Amazon S3 버킷(또는 MinIO, Yandex Object Storage)에서 직접 데이터를 읽습니다. 데이터를 ClickHouse로 복사하지 않습니다.

CREATE TABLE logs_s3
(
    timestamp DateTime,
    message String
)
ENGINE = S3(
    'https://mybucket.s3.amazonaws.com/logs/*.parquet',
    'AWS_ACCESS_KEY', 'AWS_SECRET_KEY',
    'Parquet'
);

사용 시기:

  • S3에 테라바이트 규모의 로그가 있고 ClickHouse로 복사하지 않고 가끔 분석 쿼리를 실행하려는 경우.
  • 콜드 데이터(S3가 ClickHouse 디스크보다 저렴).

주의사항: 각 쿼리는 S3에서 데이터를 다운로드하므로 느리고 비용이 많이 들 수 있습니다(아웃바운드 트래픽). 드문 쿼리에만 적합합니다.

8. PostgreSQL 엔진 — PostgreSQL의 실시간 데이터

PostgreSQL 엔진을 사용하면 PostgreSQL 테이블을 ClickHouse 테이블처럼 읽고 쓸 수 있습니다.

CREATE TABLE pg_players
(
    user_id UInt64,
    balance Decimal(18,2)
)
ENGINE = PostgreSQL(
    'postgres-host:5432',   -- 호스트와 포트
    'betting',               -- 데이터베이스
    'players',               -- PostgreSQL의 테이블
    'clickhouse_user',       -- 사용자
    'password'               -- 비밀번호
);

사용법:

-- PostgreSQL에서 현재 잔액 읽기
SELECT * FROM pg_players WHERE user_id = 123;

-- ClickHouse 테이블과 JOIN도 가능
SELECT b.user_id, b.amount, p.balance
FROM bets b
JOIN pg_players p ON b.user_id = p.user_id;

사용 시기:

  • PostgreSQL에서 ClickHouse로 점진적으로 마이그레이션 중이며 일부 데이터가 여전히 기존 DB에 있는 경우.
  • 외부 애플리케이션에 의해 업데이트되는 실시간 데이터가 필요하고 ETL을 설정하고 싶지 않은 경우.

주의사항:

  • 각 쿼리는 PostgreSQL로 이동하므로 대량 데이터에 대해 느릴 수 있습니다.
  • ClickHouse는 이러한 테이블을 포함하는 효율적인 실행 계획을 수립할 수 없습니다(푸시다운 없음).

9. 배팅을 위한 아키텍처 패턴: Kafka → Buffer → MergeTree

이제 모든 것을 종합해 봅시다. 각 배치 이벤트에 대해 Kafka에서 초당 10,000개 이상의 메시지를 받는다고 상상해 보세요. 최소 지연 시간으로 ClickHouse에 저장하고 수천 개의 작은 파트를 생성하지 않아야 합니다.

준비된 아키텍처:

-- 1. Kafka용 싱크 테이블 (Null)
CREATE TABLE bets_kafka
(
    user_id     UInt64,
    event_id    UInt64,
    amount      Decimal(18,2),
    bet_time    DateTime
)
ENGINE = Null;

-- 2. 대상 MergeTree 테이블
CREATE TABLE bets
(
    user_id     UInt64,
    event_id    UInt64,
    amount      Decimal(18,2),
    bet_time    DateTime
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(bet_time)
ORDER BY (bet_time, user_id);

-- 3. 삽입을 완화하는 Buffer 테이블
CREATE TABLE bets_buffer AS bets
ENGINE = Buffer('default', 'bets', 16, 5, 60, 10000, 1000000, 10000000, 100000000);

-- 4. Materialized View: Kafka → Null → Buffer (뷰를 통해)
CREATE MATERIALIZED VIEW bets_kafka_mv TO bets_buffer AS
SELECT * FROM bets_kafka;

-- 5. 실시간 집계를 위한 또 다른 뷰 (선택 사항)
CREATE MATERIALIZED VIEW bets_stats_mv TO bets_hourly_agg AS
SELECT 
    toStartOfHour(bet_time) AS hour,
    countState() AS bet_count,
    sumState(amount) AS total_amount
FROM bets_kafka
GROUP BY hour;

데이터 흐름:

  1. Kafka Connectbets_kafka(Null 엔진)에 메시지를 삽입합니다.
  2. bets_kafka_mv(Materialized View)가 데이터를 bets_buffer로 리디렉션합니다.
  3. bets_buffer가 메모리에 배치를 축적합니다(예: 100,000행 또는 60초).
  4. 버퍼가 큰 파트로 bets(MergeTree)에 플러시됩니다.
  5. 동시에 두 번째 Materialized View가 대시보드용 시간별 집계를 구축합니다.

이것이 최적인 이유:

  • Kafka는 Null에 씁니다(즉시, 오버헤드 없음).
  • Buffer는 수천 개의 작은 파트가 생기는 것을 방지합니다.
  • MergeTree는 큰 파트를 받아 병합이 효율적으로 작동합니다.
  • 집계는 두 번째 뷰를 통해 실시간으로 구축됩니다.

Kafka에서 MergeTree로 직접 쓰면 어떻게 될까요? 초당 10,000개 이벤트에서 1분에 600,000개의 파트가 생성됩니다. ClickHouse는 Too many parts 오류로 다운됩니다. Buffer 엔진이 이를 방지합니다.

엔진 선택 요령 — 치트 시트

작업 엔진 이유
영구 저장, 분석 MergeTree (또는 *MergeTree) ClickHouse의 기초, 인덱스, 압축
높은 부하 버퍼링 Buffer 작은 INSERT를 큰 파트로 결합
임시 데이터 (세션, 스테이징) Memory 최대 속도, 데이터 중요하지 않음
저장 없는 Kafka consumer Null + MV 뷰만을 위한 데이터
작은 정적 조회 테이블 TinyLog / Log 단순함, 적은 메타데이터
PostgreSQL 연결 PostgreSQL 엔진 ETL 없는 실시간 데이터
S3에 대한 드문 쿼리 S3 엔진 저렴한 콜드 스토리지
파일에서 가져오기 File 엔진 일회성 복사

다음 단계

일반 MergeTree로는 해결되지 않는 문제를 해결하는 특별한 엔진에 대해 배웠습니다. 이제 다음을 알게 되었습니다:

  • Memory — 캐시 및 스테이징용,
  • Buffer — 너무 빈번한 INSERT로부터 보호,
  • Null — MV를 통해 데이터를 여러 스트림으로 "분기",
  • URL / File / S3 / PostgreSQL — 외부 데이터용.

다음에 다룰 주제:

  • Kafka 엔진 설정 방법 — 별도의 커넥터 없이 Kafka에 내장된 커넥터.
  • 고급 Materialized View — 복잡한 ETL을 위한 뷰 체인.
  • 분산 테이블 — 서버 간 데이터 샤딩 방법.

결론: ClickHouse의 모든 작업이 MergeTree로 해결되는 것은 아닙니다. 때로는 Buffer가 삽입으로 서버를 죽이지 않도록, 때로는 Null + MV가 데이터를 다른 집계로 분배하도록, 때로는 Memory가 임시 해싱을 위해 필요합니다. 주요 규칙: 데이터 흐름을 먼저 설계한 다음 엔진을 선택하세요. 그 반대가 아닙니다.


이전 글:
다음 글: ClickHouse의 구체화된 뷰: 증분 처리의 힘

— Editorial Team

Advertisement 728x90

다음 읽기