Skip to content

fix(db): 커넥션 풀 고갈의 범인을 이름으로 남긴다 — leak detection + 풀 상향 - #312

Merged
dldnsgkr merged 1 commit into
developfrom
unhak/hikari-leak-detection
Sep 7, 2026
Merged

fix(db): 커넥션 풀 고갈의 범인을 이름으로 남긴다 — leak detection + 풀 상향#312
dldnsgkr merged 1 commit into
developfrom
unhak/hikari-leak-detection

Conversation

@dldnsgkr

@dldnsgkr dldnsgkr commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

auth/refresh 500 원인 조사 결과. 그 엔드포인트의 버그가 아니었다.

실제 원인: HikariCP 커넥션 풀 고갈

HikariPool-1 - Connection is not available, request timed out after 30000ms
(total=10, active=10, idle=0, waiting=0)

DB 커넥션이 필요한 모든 것이 함께 죽는다. 같은 시간대에 스케줄 워커가 전부 실패했다 — findExpirable · recoverExpiredLeases · findByStatus · findStalePendingTaskIds

FE 가 본 것들이 전부 이 하나의 증상이었다:

  • POST /auth/refresh → 500 (→ 로그인 풀림)
  • GET /projects/{id}/approvals → 간헐 500

누수다, 폭주가 아니다

01:52:02 부터 22 분간 끊이지 않고 이어졌고 앱 재기동으로만 풀렸다(타임스탬프 있는 줄 기준 120 건, 매 분 2 건씩 균일).

2026-09-08T02:00  2
2026-09-08T02:01  2
...
2026-09-08T02:14  1     ← 재기동

순간 폭주면 저절로 풀린다. 반납되지 않은 커넥션이 있었다는 뜻이다.

정황: 첫 희생자가 agent-event 스레드였고, 그 시각 SSE 스트림이 비동기 인가에서 거부되고 있었다(#309 에서 고침). 다만 지금 로그만으로는 어느 코드가 커넥션을 쥐었는지 특정할 수 없다.

특정할 수 없다는 것 자체가 고칠 지점

  • leak-detection-threshold: 60s — 임계를 넘으면 그 커넥션을 빌려간 지점의 스택이 WARN 으로 찍힌다. 반납을 막지는 않으므로 동작은 그대로고, 다음 발생 때 범인이 이름으로 남는다.

  • maximum-pool-size: 10 → 20 — 기본값 10 은 에이전트 동시 실행을 4 로 올린 뒤(fix(agent): 에이전트 동시 실행을 2 → 4 로 — max5 는 도달할 수 없는 값이었다 #311) 빠듯하다. MySQL 기본 max_connections 는 151 이라 여유가 있다.

    다만 이건 완화지 해결이 아니다 — 누수가 있으면 고갈까지 걸리는 시간만 늘린다. 근본 원인은 다음 WARN 스택을 잡아 따로 고친다.

  • 둘 다 환경변수로 뺐다(DB_LEAK_DETECTION_THRESHOLD_MS · DB_MAX_POOL_SIZE). dev·prod 양쪽 적용.

검증

전체 테스트 통과. 다음 고갈 때 WARN 스택이 실제로 찍히는지가 이 PR 의 진짜 검증이다.

🤖 Generated with Claude Code

https://claude.ai/code/session_013y8USoCXTsRTATAhy88M93

dev 에서 auth/refresh 와 approvals 가 간헐 500 을 내고 FE 세션 로그인이 풀렸다. 조사해 보니
두 엔드포인트의 버그가 아니라 <b>HikariCP 커넥션 풀 고갈</b>이었다. DB 커넥션이 필요한 모든
것이 함께 죽는다 — 스케줄 워커 전부(findExpirable · recoverExpiredLeases · findByStatus ·
findStalePendingTaskIds …)가 같은 시간대에 실패했다.

  HikariPool-1 - Connection is not available, request timed out after 30000ms
  (total=10, active=10, idle=0, waiting=0)

01:52:02 부터 22 분간 끊이지 않고 이어졌고 앱 재기동으로만 풀렸다. <b>순간 폭주면 저절로
풀린다</b> — 반납되지 않은 커넥션이 있었다는 뜻이다. 첫 희생자가 agent-event 스레드였고,
그 시각 SSE 스트림이 비동기 인가에서 거부되고 있었다(#309 에서 고친 것)는 점은 정황이지만,
지금 로그만으로는 어느 코드가 커넥션을 쥐었는지 특정할 수 없다.

특정할 수 없다는 것 자체가 고칠 지점이다.

- leak-detection-threshold 60s: 임계를 넘으면 그 커넥션을 <b>빌려간 지점의 스택</b>이 WARN 으로
  찍힌다. 반납을 막지는 않으므로 동작은 그대로고, 다음 발생 때는 범인이 이름으로 남는다.
- maximum-pool-size 10 → 20: 기본값 10 은 에이전트 동시 실행을 4 로 올린 뒤(#311) 빠듯하다.
  MySQL 기본 max_connections 는 151 이라 여유가 있다. 다만 이건 완화지 해결이 아니다 —
  누수가 있으면 고갈까지 걸리는 시간만 늘린다.
- 둘 다 환경변수로 뺐다(DB_LEAK_DETECTION_THRESHOLD_MS · DB_MAX_POOL_SIZE).

dev·prod 양쪽에 넣었다. 다음 고갈 때 WARN 스택을 잡아 근본 원인을 따로 고친다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y8USoCXTsRTATAhy88M93
@dldnsgkr
dldnsgkr merged commit dd53128 into develop Sep 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant