Ken
ClickHouse의 async_insert 기능 사용 시 wait_for_async_insert=0 설정은 INSERT 응답 시간을 수 밀리초로 단축시키지만, 문서상 "데이터 유실 가능성"이 명시되어 있어 실제 운영 환경에서의 안정성 검증이 필요했다고 판단하였고, 다양한 파라미터를 조절해가며 테스트를 진행하였습니다.
- 1. 결론
- 1.1 테스트 결과 요약
- 1.2 권장 설정
- 1.3 기대 효과
- 1.4 파라미터별 리소스 영향 요약
- 1.5 유실이 발생하지 않은 이유
- 2. 배경
- 2.1 Async Insert란?
- 2.2 핵심 파라미터
- 2.3 wait_for_async_insert = 0의 유실 가능성
- 2.4 테스트의 필요성
- 3. 테스트 환경 구성
- 3.1 테이블 스키마
- 3.2 테스트 방법
- 4. 테스트 케이스 및 결과
- 4.1 기본 파라미터 테스트 (각 1,000건)
- 4.2 고강도 스트레스 테스트 (각 100,000건)
- 4.3 연속 INSERT 테스트
- 5. 실패 유도 테스트
- 5.1 타입 에러
- 5.2 테이블 부재
- 5.3 발견사항
- 6. 리소스 효율성 분석
- 6.1 파라미터별 리소스 영향
- 6.2 Flush 파라미터별 트레이드오프
- 6.3 파트 생성 및 Merge 효율
- 6.4 사용 케이스별 권장 설정
- 7. 모니터링 쿼리
- 7.1 현재 버퍼 상태 확인
- 7.2 INSERT 성공/실패 통계
- 7.3 Async Insert Log 확인
- 8. 부록: 테스트에 사용된 SQL 문
- 8.1 환경 생성
- 8.2 테스트 INSERT
- 8.3 결과 확인
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 후 이 시간 경과 시 flushasync_insert_max_data_size(기본 10MB): 버퍼 크기가 이 값에 도달 시 flushasync_insert_max_query_number(기본 450): 버퍼의 쿼리 수가 이 값에 도달 시 flushasync_insert_stale_timeout_ms(기본 0, 비활성): 마지막 INSERT 후 이 시간 경과 시 flush
2.3 wait_for_async_insert = 0의 유실 가능성
wait_for_async_insert = 0 설정 시 클라이언트는 데이터가 버퍼에 저장되는 즉시 "OK" 응답을 받는다. 이 시점에 데이터는 아직 메모리 버퍼에만 존재하며 디스크에 기록되지 않은 상태이다.
이론적 유실 시나리오:
- 클라이언트가 INSERT 요청을 보냄
- 서버가 데이터를 버퍼에 저장하고 "OK" 응답 반환
- 클라이언트는 성공으로 인지
- flush 전에 서버가 비정상 종료
- 버퍼에 있던 데이터는 디스크에 기록되지 않고 유실
반면 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;