Infra

[ClickHouse] Langfuse 쿼리 스톨 오진단 24.10 타임아웃 슬롯 버그

jykim23 2026. 7. 1. 21:42
반응형

부제: 그럴듯한 우회로 때우다 진짜 원인을 놓칠 뻔한 이야기

ClickHouse 쿼리 스톨 오진단 — 대용량 쿼리가 아닌 24.10 타임아웃 슬롯 버그가 원인이라는 일러스트

관측용으로 Langfuse를 쓰고, 그 뒤에서 ClickHouse가 트레이스를 저장한다. 어느 날 이 ClickHouse가 12시간 동안 먹통이 됐다. 사고를 어떻게 잘못 짚었다가 바로잡았는지 적어둔다.

증상

프로세스는 살아 있었다. 크래시도 OOM도 아니었다. 그런데 모든 쿼리가 멈췄다. 헬스체크로 날리는 SELECT 1조차 응답을 못 했다. 타임아웃을 60초로 걸어 뒀는데, 그 한 줄짜리 쿼리가 로그상 75분 넘게 슬롯을 붙들고 있었다. 설정한 타임아웃이 그냥 무시된 거다. host swap도 9 GiB 가까이 올라 있었다.

그럴듯했던 오답

사고 직전에 서버가 PDF를 잔뜩 처리하던 참이었다. 그 과정에서 14MB짜리 base64 데이터가 트레이스에 통째로 박혔다.

여기서 그럴듯한 그림이 그려졌다. 거대한 row가 쌓이면서 디스크 I/O가 몰리고, 그 탓에 swap이 나빠지고, 끝내 쿼리가 얼어붙는다. swap 수치도 비대한 row도 눈에 보이는 증거였으니 그럴듯했다.

그래서 우회로 때웠다. 미디어 업로드를 끄고, 트레이스에 들어가는 큰 데이터를 마스킹했다. 실제로 swap이 좀 내려갔다. 신규 14MB row도 0건이 됐다. 문제가 풀린 것처럼 보였다.

반전

그런데 사고가 계속 재발했다. 다음 날도 그다음 날도 한두 시간마다 같은 스톨이 반복됐다. 거대 row를 막았는데도 멈춤이 계속된다면, 그게 직접 원인은 아니라는 소리였다.

우회로 swap 정도는 잡았지만 진짜 원인은 못 건드린 채 덮어만 둔 셈이다.

진짜 원인

로그를 다시 좁혀 봤다. 멈춘 쿼리는 대부분(80% 넘게) 헬스체크 SELECT 1이었다. 하나같이 75분 안팎에서 한꺼번에 타임아웃돼 있었다. 60초로 걸어둔 게 75분을 잡아먹었다면, 데이터 크기가 아니라 타임아웃 처리 자체가 망가진 거다.

원인은 그때 쓰던 ClickHouse 버전(24.10)이었다. 이 버전엔 쿼리가 타임아웃돼도 붙잡고 있던 슬롯을 제때 반납하지 못하는 버그가 있었다. 슬롯이 고갈되니 새 쿼리가 줄줄이 막혔다. 헬스체크가 실패해 컨테이너가 재시작되고, 재시작하자마자 같은 식으로 다시 고갈되는 악순환이었다. 공식 이슈로도 올라와 있었고 다음 LTS에서 고쳐졌다.

14MB base64가 이 와중에 I/O를 더 무겁게 만든 건 맞다. 다만 그게 없었어도 타임아웃만 제대로 돌았으면 쿼리가 느렸을 뿐 슬롯은 반납됐다. 거들긴 했어도 방아쇠는 아니었다. 이 차이를 처음엔 못 봤다.

해결

  • ClickHouse를 버그가 고쳐진 LTS 버전(25.8)으로 올렸다. 이게 직접 원인 제거.
  • 헬스체크를 별도 workload로 격리했다. 슬롯이 포화돼도 헬스체크만큼은 우선 응답하도록. 컨테이너 재시작 폭주의 고리를 끊는 안전장치다.
  • 타임아웃·동시성·메모리 한도 같은 방어용 설정도 함께 조였다.

이후로 같은 스톨은 없었다.

정리

  • 우회로 증상이 가라앉으면 "해결됐다"고 착각하기 쉽다. 증상 완화와 원인 제거는 다르다.
  • 무언가 "거들었다"는 것과 그게 "방아쇠"라는 건 다르다. 둘을 뭉뚱그리면 엉뚱한 데를 고친다.
  • 설정값(60초)이 실측(75분)과 어긋나면, 데이터가 아니라 그 처리 메커니즘부터 의심한다.
  • 의존성을 정기적으로 올리는 게 이런 버그를 가장 싸게 피하는 길이었다.

가장 그럴듯한 용의자가 가장 눈에 잘 띈다는 게 함정이었다.

반응형