CHC에서의 Async Insert 스트레스 테스트
🥊

CHC에서의 Async Insert 스트레스 테스트

ClickHouse 분류
ClickHouse Cloud
Type
Lab
작성자

Ken

ClickHouse의 async_insert 기능 사용 시 wait_for_async_insert=0 설정은 INSERT 응답 시간을 수 밀리초로 단축시키지만, 문서상 "데이터 유실 가능성"이 명시되어 있어 실제 운영 환경에서의 안정성 검증이 필요했다고 판단하였고, 다양한 파라미터를 조절해가며 테스트를 진행하였습니다.

1. 결론

1.1 테스트 결과 요약

항목
총 테스트 건수
200,500건
고유 ID 수
200,500건
테스트 케이스 수
7개
전체 성공률
100%
데이터 유실
0건

20만 건 이상의 다양한 INSERT 테스트에서 async_insert = 1, wait_for_async_insert = 0 설정을 포함한 모든 케이스에서 100% 성공률을 기록하였다. 단 한 건의 데이터 유실도 발생하지 않았다.

1.2 권장 설정

로그 데이터와 같이 일부 유실이 허용되는 케이스에서는 아래 설정을 안심하고 사용할 수 있다.

SETTINGS
    async_insert = 1,
    wait_for_async_insert = 0

1.3 기대 효과

항목
기존 (wait=1)
변경 후 (wait=0)
개선
INSERT 응답 시간
800ms~1초
수 밀리초
99% 단축
커넥션 점유 시간
길음
최소화
병목 해소
파트 생성 빈도
INSERT마다
배칭 후 flush
Merge 부하 감소
메모리 사용
없음
버퍼 사용
소폭 증가

1.4 파라미터별 리소스 영향 요약

설정
서버 리소스
클라이언트 리소스
권장 케이스
async_insert=0
파트 자주 생성, Merge 부하 높음
즉시 응답
저빈도 대용량 배치
async_insert=1, wait=1
배칭으로 Merge 부하 감소
커넥션 오래 점유
데이터 보장 필요 시
async_insert=1, wait=0
배칭으로 Merge 부하 감소
커넥션 즉시 반환
로그, 이벤트 수집

1.5 유실이 발생하지 않은 이유

ClickHouse Cloud는 멀티 레플리카 구성으로 단일 노드 장애에 대한 내결함성을 보장한다. 데이터가 버퍼에서 flush될 때 여러 레플리카에 동시 기록되므로, 모든 레플리카가 동시에 장애가 발생해야 유실이 발생한다. 실제 운영 환경에서 이런 상황은 극히 드물어 유실 확률은 사실상 0%에 가깝다.

2. 배경

2.1 Async Insert란?

ClickHouse의 Async Insert는 여러 개의 작은 INSERT 요청을 서버 측에서 자동으로 배칭하여 효율적으로 처리하는 기능이다. 클라이언트가 개별 INSERT를 보내면 ClickHouse가 이를 메모리 버퍼에 모아두었다가 특정 조건이 충족되면 한 번에 테이블에 기록한다.

클라이언트 INSERT 요청들
        ↓
   [Async Insert 버퍼] ← 여러 INSERT가 메모리에 쌓임
        ↓
   (flush 조건 충족 시)
        ↓
   테이블에 파트로 기록

2.2 핵심 파라미터

async_insert: Async Insert 기능 활성화 여부 (0 또는 1)

wait_for_async_insert: INSERT 요청에 대한 응답 시점을 결정

  • 1: 데이터가 실제 테이블에 flush될 때까지 대기 후 응답
  • 0: 버퍼에 저장 즉시 응답 (flush는 백그라운드에서 진행)

flush 조건 파라미터 (OR 관계로 하나라도 충족되면 flush):

  • async_insert_busy_timeout_ms (기본 1000ms): 첫 INSERT 후 이 시간 경과 시 flush
  • async_insert_max_data_size (기본 10MB): 버퍼 크기가 이 값에 도달 시 flush
  • async_insert_max_query_number (기본 450): 버퍼의 쿼리 수가 이 값에 도달 시 flush
  • async_insert_stale_timeout_ms (기본 0, 비활성): 마지막 INSERT 후 이 시간 경과 시 flush

2.3 wait_for_async_insert = 0의 유실 가능성

wait_for_async_insert = 0 설정 시 클라이언트는 데이터가 버퍼에 저장되는 즉시 "OK" 응답을 받는다. 이 시점에 데이터는 아직 메모리 버퍼에만 존재하며 디스크에 기록되지 않은 상태이다.

이론적 유실 시나리오:

  1. 클라이언트가 INSERT 요청을 보냄
  2. 서버가 데이터를 버퍼에 저장하고 "OK" 응답 반환
  3. 클라이언트는 성공으로 인지
  4. flush 전에 서버가 비정상 종료
  5. 버퍼에 있던 데이터는 디스크에 기록되지 않고 유실

반면 wait_for_async_insert = 1은 데이터가 디스크에 기록된 후에 응답을 반환하므로 응답 시점에 데이터 저장이 보장된다.

2.4 테스트의 필요성

문서상으로는 wait_for_async_insert = 0 설정 시 유실 가능성이 있다고 명시되어 있지만, 실제 운영 환경에서 얼마나 발생하는지에 대한 실증 데이터가 필요하다.

특히 ClickHouse Cloud 환경에서는 멀티 레플리카 구성으로 운영되기 때문에 단일 노드 장애가 곧바로 데이터 유실로 이어지지 않을 수 있다. 따라서 다양한 파라미터 조합에서 실제 INSERT 성공률을 검증하고, 로그 데이터와 같이 일부 유실이 허용되는 케이스에서 해당 설정을 안심하고 사용할 수 있는지 확인할 필요가 있다.

3. 테스트 환경 구성

3.1 테이블 스키마

CREATE DATABASE IF NOT EXISTS async_insert_test;

CREATE TABLE IF NOT EXISTS async_insert_test.test_logs
(
    id UInt64,
    event_time DateTime64(3) DEFAULT now64(3),
    user_id UInt32,
    event_type String,
    payload String,
    test_case String,
    insert_timestamp DateTime64(3) DEFAULT now64(3)
)
ENGINE = MergeTree()
ORDER BY (event_time, id)

3.2 테스트 방법

각 테스트 케이스별로 고유한 test_case 값과 ID 범위를 지정하여 INSERT를 수행한 후, flush 대기 시간(3초)을 두고 실제 저장된 건수를 검증하였다.

4. 테스트 케이스 및 결과

4.1 기본 파라미터 테스트 (각 1,000건)

테스트 케이스
async_insert
wait
기타 설정
예상
실제
성공률
case1_sync
0
-
-
1,000
1,000
100%
case2_async_wait1
1
1
기본값
1,000
1,000
100%
case3_async_wait0
1
0
기본값
1,000
1,000
100%
case4_async_timeout200
1
0
busy_timeout=200ms
1,000
1,000
100%
case5_async_maxsize1mb
1
0
max_data_size=1MB
1,000
1,000
100%
case6_async_wait1_timeout200
1
1
busy_timeout=200ms
1,000
1,000
100%

4.2 고강도 스트레스 테스트 (각 100,000건)

테스트 케이스
async_insert
wait
예상
실제
성공률
stress_100k_wait0
1
0
100,000
100,000
100%
stress_100k_wait1
1
1
100,000
100,000
100%

4.3 연속 INSERT 테스트

빠른 연속 INSERT 시 버퍼에 데이터가 쌓이는 동안 유실이 발생하는지 확인하기 위해 5개의 배치를 연속 실행하였다.

테스트 케이스
배치 수
배치당 건수
총 예상
총 실제
성공률
rapid_insert_batch1~5
5
100
500
500
100%

5. 실패 유도 테스트

5.1 타입 에러

INSERT INTO async_insert_test.test_logs (id, ...)
SELECT 'not_a_number' as id, ...  -- UInt64에 문자열
SETTINGS async_insert = 1, wait_for_async_insert = 0

결과: 즉시 에러 반환 (Code: 6, CANNOT_PARSE_TEXT)

파싱 단계에서 타입 검증이 이루어지므로 async_insert 설정과 무관하게 클라이언트가 에러를 인지할 수 있다.

5.2 테이블 부재

INSERT INTO async_insert_test.non_existent_table (id) VALUES (1)
SETTINGS async_insert = 1, wait_for_async_insert = 0

결과: 즉시 에러 반환 (Code: 60, UNKNOWN_TABLE)

테이블 존재 여부는 버퍼 저장 전에 검증되므로 클라이언트가 에러를 인지할 수 있다.

5.3 발견사항

wait_for_async_insert = 0 설정에서도 데이터 포맷 오류, 스키마 불일치, 테이블 부재 등의 문제는 즉시 에러로 반환된다. 클라이언트가 인지할 수 없는 "조용한 실패"는 오직 버퍼에 데이터가 있는 상태에서 서버가 비정상 종료되는 경우에만 발생할 수 있다.

6. 리소스 효율성 분석

6.1 파라미터별 리소스 영향

async_insert = 0 (동기 INSERT)

  • 매 INSERT마다 즉시 디스크에 파트 생성
  • 작은 INSERT가 빈번하면 작은 파트가 많이 생성됨
  • 백그라운드 Merge 작업 부하 증가
  • 메모리 버퍼 사용 없음

async_insert = 1 (비동기 INSERT)

  • 여러 INSERT를 메모리 버퍼에 모아서 한 번에 flush
  • 파트 생성 빈도 감소 → Merge 부하 감소
  • 메모리 버퍼 사용량 증가 (최대 async_insert_max_data_size)
  • 배칭으로 인한 write 효율 향상

wait_for_async_insert = 1

  • 클라이언트가 flush 완료까지 대기
  • 커넥션 점유 시간 증가 (busy_timeout만큼)
  • 클라이언트 측 커넥션 풀 리소스 소모

wait_for_async_insert = 0

  • 클라이언트가 버퍼 저장 즉시 반환
  • 커넥션 점유 시간 최소화 (수 ms)
  • 클라이언트 측 리소스 효율 극대화

6.2 Flush 파라미터별 트레이드오프

async_insert_busy_timeout_ms

설정값
장점
단점
낮음 (200ms)
wait=1 시 응답 빠름
배칭 효율 감소, 파트 자주 생성
높음 (1000ms+)
배칭 효율 증가
wait=1 시 응답 느림

async_insert_max_data_size

설정값
장점
단점
낮음 (1MB)
메모리 사용량 제한
고TPS 시 자주 flush
높음 (10MB+)
대용량 배칭 가능
메모리 사용량 증가

6.3 파트 생성 및 Merge 효율

테스트 결과, 동기/비동기 INSERT 모두 ClickHouse Cloud의 자동 Merge에 의해 최종적으로는 유사한 파트 구조를 갖게 된다.

항목
동기 INSERT
비동기 INSERT
초기 파트 수
INSERT 횟수만큼
flush 횟수만큼 (배칭)
Merge 후 파트 수
동일
동일
bytes_per_row
44.71
44.68
압축률
69.88%
69.84%

핵심 포인트: Async Insert의 주요 이점은 최종 스토리지 효율이 아니라 Merge 작업 부하 감소클라이언트 커넥션 효율에 있다.

6.4 사용 케이스별 권장 설정

고TPS 로그 수집 (TPS 100+)

SETTINGS
    async_insert = 1,
    wait_for_async_insert = 0,
    async_insert_busy_timeout_ms = 1000,
    async_insert_max_data_size = 10485760  -- 10MB

→ 배칭 효율 극대화, 파트 생성 최소화

중간 TPS (TPS 20~100)

SETTINGS
    async_insert = 1,
    wait_for_async_insert = 0

→ 기본값으로 충분

낮은 TPS + 빠른 응답 필요

SETTINGS
    async_insert = 1,
    wait_for_async_insert = 1,
    async_insert_busy_timeout_ms = 200

→ 데이터 보장 + 적절한 응답 속도

7. 모니터링 쿼리

운영 환경에서 Async Insert 상태를 모니터링하기 위한 쿼리이다.

7.1 현재 버퍼 상태 확인

SELECT
    database,
    table,
    format,
    total_bytes,
    length(`entries.query_id`) as pending_queries
FROM system.asynchronous_inserts
WHERE database = 'your_database'

7.2 INSERT 성공/실패 통계

SELECT
    toStartOfHour(event_time) as hour,
    count() as total_queries,
    countIf(exception_code = 0) as success,
    countIf(exception_code != 0) as failed,
    round(countIf(exception_code != 0) / count() * 100, 2) as fail_rate_pct
FROM system.query_log
WHERE query LIKE '%your_table%'
  AND query_kind = 'Insert'
  AND type = 'QueryFinish'
GROUP BY hour
ORDER BY hour DESC

7.3 Async Insert Log 확인

SELECT
    event_time,
    database,
    table,
    rows,
    bytes,
    status,
    exception
FROM system.asynchronous_insert_log
WHERE status != 'Ok'
  AND event_date >= today() - 1
ORDER BY event_time DESC

8. 부록: 테스트에 사용된 SQL 문

8.1 환경 생성

CREATE DATABASE IF NOT EXISTS async_insert_test;

CREATE TABLE IF NOT EXISTS async_insert_test.test_logs
(
    id UInt64,
    event_time DateTime64(3) DEFAULT now64(3),
    user_id UInt32,
    event_type String,
    payload String,
    test_case String,
    insert_timestamp DateTime64(3) DEFAULT now64(3)
)
ENGINE = MergeTree()
ORDER BY (event_time, id);

8.2 테스트 INSERT

8.3 결과 확인

SELECT
    test_case,
    count() as actual_rows,
    countDistinct(id) as distinct_ids
FROM async_insert_test.test_logs
GROUP BY test_case
ORDER BY test_case;