Too Many Parts와 Async Insert
🏗️

Too Many Parts와 Async Insert

ClickHouse 분류
Core Architecture
Type
Research
작성자

Ken

ClickHouse MergeTree는 각 INSERT마다 새로운 Part를 생성하며, 300개 초과 시 "Too Many Parts" 에러 발생시킵니다. 이런 Part 개수 증가는 쿼리 성능 저하, 메모리 오버헤드, Merge 부하를 야기하며, 성능에 직접적인 영향을 줍니다. 이번 글에서는 그에 대해서 다루어 보았습니다.

  • ClickHouse의 MergeTree 구조
  • 빈번한 INSERT가 야기하는 문제
  • Too Many Parts 에러
  • 성능 저하의 근본 원인
  • 빈번한 INSERT 시나리오 예시
  • 대응 방안
  • 1. Batch Insert 활용
  • 2. Async Insert 설정
  • 서버 레벨 설정
  • 쿼리 레벨 설정
  • 주요 파라미터 설명
  • 3. 테이블 설정 최적화
  • 4. 모니터링 및 알람
  • 5. 실전 아키텍처 패턴
  • 패턴 A: Buffer 테이블 활용
  • 패턴 B: Async Insert + Materialized View
  • 핵심 체크리스트
  • 결론

ClickHouse의 MergeTree 구조

ClickHouse의 MergeTree 엔진은 데이터를 Part라는 불변(immutable) 단위로 저장합니다. 각 INSERT 쿼리는 기본적으로 새로운 Part를 생성하며, 이러한 Part들은 백그라운드에서 주기적으로 병합(merge)되어 더 큰 Part로 통합됩니다.

Part의 구조적 특징:

  • 각 Part는 정렬된 데이터 블록으로 구성
  • Primary key 순서로 정렬되어 저장
  • 불변성: 한번 생성되면 수정되지 않고, 병합을 통해서만 변경

빈번한 INSERT가 야기하는 문제

Too Many Parts 에러

ClickHouse는 테이블당 Part 개수를 제한합니다. 기본값으로 active part가 300개를 초과하면 다음과 같은 에러가 발생합니다:

DB::Exception: Too many parts (304).
Merges are processing significantly slower than inserts.

성능 저하의 근본 원인

1. 쿼리 성능 저하

  • 각 SELECT는 모든 active parts를 읽어야 함
  • Part 개수에 비례하여 I/O 증가
  • 파일 디스크립터(file descriptor) 사용량 급증

2. 메모리 오버헤드

  • 각 Part마다 메타데이터 유지 필요
  • Primary index를 메모리에 로드
  • 100개 Parts vs 10개 Parts: 메모리 사용량 10배 차이 발생 가능

3. Merge 부하

  • 백그라운드 merge가 INSERT 속도를 따라가지 못함
  • Merge 과정에서 CPU, Disk I/O 집중 사용
  • Merge 중 임시 디스크 공간 2배 필요

빈번한 INSERT 시나리오 예시

-- 안티패턴: 개별 row INSERT
INSERT INTO events VALUES (1, 'user1', now());
INSERT INTO events VALUES (2, 'user2', now());
INSERT INTO events VALUES (3, 'user3', now());
-- → 3개의 Parts 생성

-- 스트리밍 데이터 파이프라인
-- Kafka, Kinesis 등에서 micro-batch로 데이터 유입
-- 초당 수십~수백 건의 INSERT → Part 폭증

대응 방안

1. Batch Insert 활용

가장 기본적이고 효과적인 방법입니다.

-- 권장: 배치로 묶어서 INSERT
INSERT INTO events VALUES
    (1, 'user1', now()),
    (2, 'user2', now()),
    (3, 'user3', now()),
    ...
    (10000, 'user10000', now());
-- → 1개의 Part만 생성

권장 배치 크기:

  • 최소: 1,000 rows
  • 권장: 10,000 ~ 100,000 rows
  • 또는 데이터 크기 기준 10MB ~ 100MB per batch

2. Async Insert 설정

ClickHouse 21.11 버전부터 도입된 Async Insert는 서버 측에서 자동으로 작은 INSERT들을 버퍼링하여 배치로 처리합니다.

서버 레벨 설정

/etc/clickhouse-server/config.xml 또는 config.d/async_insert.xml:

<clickhouse>
    <async_insert_threads>16</async_insert_threads>
    <async_insert_max_data_size>10485760</async_insert_max_data_size> <!-- 10MB -->
    <async_insert_busy_timeout_ms>1000</async_insert_busy_timeout_ms>
</clickhouse>

쿼리 레벨 설정

-- 쿼리마다 async insert 활성화
INSERT INTO events
SETTINGS async_insert=1, wait_for_async_insert=0
VALUES (1, 'user1', now());

-- 동기식 대기 (INSERT 완료 확인 후 반환)
INSERT INTO events
SETTINGS async_insert=1, wait_for_async_insert=1
VALUES (1, 'user1', now());

주요 파라미터 설명

파라미터
기본값
설명
async_insert
0
async insert 활성화 여부
wait_for_async_insert
1
클라이언트가 실제 insert 완료까지 대기할지 여부
async_insert_max_data_size
1MB
버퍼 크기 (바이트). 이 크기 도달 시 flush
async_insert_busy_timeout_ms
200
첫 INSERT 후 대기 시간 (밀리초). 타임아웃 시 flush
async_insert_threads
16
async insert 처리 스레드 수

3. 테이블 설정 최적화

4. 모니터링 및 알람

Part 개수를 지속적으로 모니터링하는 것이 중요합니다.

5. 실전 아키텍처 패턴

패턴 A: Buffer 테이블 활용

장점: 애플리케이션 코드 변경 최소화

단점: 메모리 사용, 서버 재시작 시 버퍼 데이터 유실 가능

패턴 B: Async Insert + Materialized View

핵심 체크리스트

INSERT 패턴 점검

  • 개별 row insert 피하기
  • 최소 1,000 rows 이상 배치로 묶기
  • Async insert 활성화 검토

모니터링 설정

  • system.parts로 part 개수 추적
  • 200개 이상 시 알람 설정
  • system.merges로 병합 상태 확인

테이블 설정 검토

  • 적절한 partition key 설정 (날짜/시간 기반 권장)
  • parts_to_delay_insert 임계값 조정
  • Merge 정책 최적화

아키텍처 개선

  • 스트리밍 데이터는 Buffer 테이블 또는 Async insert 활용
  • 실시간 집계는 Materialized View 활용
  • 배치 크기와 빈도의 균형 찾기

결론

Too many parts 문제는 ClickHouse의 아키텍처적 특성에서 비롯되지만, 적절한 INSERT 패턴과 Async Insert 설정으로 충분히 예방 가능합니다. 특히 스트리밍 데이터나 고빈도 INSERT 환경에서는 Async Insert가 개발 편의성과 성능을 동시에 제공하는 강력한 솔루션입니다. 지속적인 모니터링과 함께 워크로드에 맞는 최적의 설정을 찾아 적용하는 것이 중요합니다.