IL-0330 incident.io · 2023-04-20 (게시일)database
Go database/sql 커넥션 풀을 기다리던 요청이 2주 동안 간헐적으로 타임아웃됐고, 한 요청은 거의 20초를 기다렸다
2023년 초 incident.io 애플리케이션은 2주 동안 데이터베이스를 쓸 때 간헐적으로 타임아웃을 겪었다. 첫 신호는 고객 신고와 Sentry 의 context canceled 오류였다. 트레이스를 보니 요청이 Go 의 database/sql 패키지가 관리하는 클라이언트 쪽 커넥션 풀에서 커넥션을 얻으려고 거의 20초를 기다렸다. 이 패키지는 커넥션 풀로 한 번에 데이터베이스와 통신하는 앱 프로세스 수를 제한한다. 쓸 수 있는 커넥션이 다 쓰이고 있으면 작업이 커넥션을 얻을 때까지 막힌다. 팀은 go.patch 파일로 database/sql 을 고쳐 ConnectionPoolWait 에 트레이스를 넣어 두었기 때문에 이 대기를 볼 수 있었다. 대기는 특정 풀 하나가 아니라 여러 곳에 퍼져 있었다. 트래픽, PubSub 지표, Heroku 의 Postgres 통계도 평소와 같았다.
뚜렷한 출발점이 없어 팀은 느려 보이는 쿼리를 빠르게 고쳤다. 정책 위반 계산을 머티리얼라이즈드 뷰로 옮기고, 인덱스를 더하고, 인덱스 없는 열로 거르는 조인 쿼리를 다시 썼다. 며칠 안에 타임아웃이 다시 나자 모든 트랜잭션에 1초 락 타임아웃을 걸었다. 또 Slack 웹훅을 받으면 데이터베이스 쿼리 없이 바로 PubSub 로 넘기도록 바꿨다. 다음 날 또 타임아웃이 나자 팀은 관측성을 키우는 데 집중했다. ngrok/sqlmw 패키지로 미들웨어를 만들고, trackUsage 메서드로 작업마다 데이터베이스 커넥션을 쥐고 있던 시간을 세어 로그와 트레이스에 남겼다. gorm 의 훅은 커넥션 풀 대기 시간까지 포함해 재기 때문에 쓰지 않았다. 2주 동안 성능과 관측성을 겨냥한 변경 24개를 배포했다.
로그를 살피던 중 팀은 Slack 모달 제출을 처리할 때마다 필요 없는 트랜잭션을 열고 있다는 것을 발견했다. 이 HTTP 엔드포인트는 사용자가 /inc 같은 슬래시 명령의 화면에서 확인 버튼을 누를 때마다 Slack 이 호출한다. 대부분의 모달 제출에서 트랜잭션을 없애고, 트랜잭션 보장이 필요한 코드 경로에만 명시적으로 넣었다. 타임아웃이 매일 나지는 않아서 이것이 근본 원인이었다는 것은 며칠 뒤에야 확인됐다. 느린 것 하나가 아니라 짧은 트랜잭션 여러 개가 쌓여 문제를 만들었다. 글을 쓴 시점까지 4개월 동안 데이터베이스 타임아웃은 없었다.
- 심각도
- –
- 영향 범위
- Intermittent timeouts on API requests and other operations that used the database, over a period of two weeks
- 영향 사용자
- –
- 지역
- –
- 감지까지
- –
- 복구까지
- –
증상
2023년 초 incident.io 애플리케이션이 데이터베이스를 쓰는 동안 간헐적으로 타임아웃이 났다. 고객 신고가 들어왔고, 오류 수집 도구 Sentry 에 context canceled 오류가 한꺼번에 쏟아졌다.
Grafana 의 API 대시보드에서는 일부 API 요청이 타임아웃된 것이 보였지만 1분이 안 돼 정상으로 돌아왔다. 실패한 요청 하나의 트레이스를 열어 보니 HTTP 요청이 커넥션 풀에서 쓸 수 있는 커넥션을 얻으려고 거의 20초를 기다렸다. 이 대기는 database.sql.ConnectionPoolWait 스팬에 나타났다.
Kibana 에서 커넥션을 기다린 요청의 종류를 나눠 보니 특정 커넥션 풀 하나가 아니라 여러 곳에 퍼져 있었다. 이전 HTTP 트래픽과 PubSub 지표에는 평소와 다른 점이 없었다. Heroku 의 Postgres 통계에서도 데이터베이스 부하는 평범한 수준이었다. 데이터베이스 사용을 크게 바꾼 코드 변경도, 갑작스러운 트래픽 변화도 없었다. 로그, 트레이스, 대시보드에도 눈에 띄는 것이 없었다.
팀은 느려 보이고 빨리 고칠 수 있는 쿼리부터 손봤고 장애를 닫았다. 하지만 며칠 안에 다시 커넥션 풀 대기로 타임아웃이 났다. Kibana 와 Google Cloud Trace 에서 느린 요청 사이에 뚜렷한 규칙은 보이지 않았다. 모든 트랜잭션에 1초 락 타임아웃을 걸어 봤지만 아무것도 깨지지 않았고 원인에 가까워지지도 않았다. Slack 이벤트를 비동기로 처리하게 바꾼 다음 날에도 타임아웃이 또 났다.
원인
- 촉발
- –
감지
고객 신고가 들어왔고 Sentry 에 context canceled 오류가 쏟아지면서 알아챘다. 온콜 엔지니어가 장애를 열고 Grafana 의 API 대시보드에서 타임아웃을 확인했다. 두 번째 타임아웃 때는 장애가 자동으로 열리고 온콜 엔지니어가 호출됐다.
조치
- 정책 위반은 요청마다 정책과 관련 장애를 모두 가져와 계산해 왔는데, 이 계산을 머티리얼라이즈드 뷰로 옮겼다.
- 느린 쿼리를 빠르게 하려고 데이터베이스 인덱스를 새로 더했다.
- 인덱스 없는 열로 조인한 뒤 거르던 쿼리를, 조인에 이미 들어 있는 테이블의 인덱스 있는 같은 열로 거르도록 다시 썼다.
- 어떤 요청이 트랜잭션을 오래 쥐고 있는지 빨리 찾으려고 모든 트랜잭션에 1초 락 타임아웃을 걸었다.
- Slack 웹훅이 들어오면 데이터베이스 쿼리 없이 바로 PubSub 로 넘겨 Slack 이벤트를 비동기로 처리하게 바꿨다.
- ngrok/sqlmw 패키지로 미들웨어를 만들고, trackUsage 메서드로 작업마다 데이터베이스 커넥션을 쥔 시간을 세어 로그와 트레이스에서 거를 수 있게 했다.
- Slack 모달 제출 대부분에서 필요 없던 트랜잭션을 없애고, 트랜잭션 보장이 필요한 코드 경로에만 명시적으로 넣었다.
후속 조치
–
교훈
한 번에 모든 것이 풀리는 결정적인 발견은 없었다. 대신 그 과정에서 이런 문제를 진단할 준비가 훨씬 잘 됐고, 실제 성능 개선도 함께 이뤄졌다. 원인은 눈에 띄게 느린 것 하나가 아니라 짧은 트랜잭션 여러 개가 합쳐진 것이었다. 애플리케이션의 부분마다 다른 데이터베이스를 썼다면 아마 더 빨리 찾았을 것이다. 하지만 그러면 분산 트랜잭션을 고민해야 하고 개발 환경도 덜 편해지므로, 팀은 지금의 선택을 유지한다.
출처
관련 사례
IL-0451Aurora MySQL 에서 인덱스를 INVISIBLE 로 바꿀 때마다 Reader 3대의 버퍼풀이 무효화돼 CPU 와 ReadIOPS 가 튀었다Daangn · 2026같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
IL-0281PostHog US 리전 Logs 제품의 ClickHouse 클러스터가 S3 에 둔 로그 데이터를 거의 다 지워 3일보다 오래된 로그가 사라졌다PostHog · 2026같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
IL-0150DynamoDB DNS 자동화의 경쟁 조건이 us-east-1 리전 엔드포인트의 DNS 레코드를 비워 DynamoDB 와 EC2 인스턴스 시작이 실패했고, 그 여파로 EKS, ECS, Fargate 의 컨테이너 시작과 클러스터 스케일링이 14시간 넘게 막혔다Amazon Web Services · 2025같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
IL-0527OpenStreetMap.org 의 복제 diff 생성이 invalid memory alloc request size 오류로 16시간 멈춤OpenStreetMap Foundation · 2025같은 원인 · 공통 데이터베이스
IL-0329Postgres 17 업그레이드 뒤 다시 켠 PGAudit 가 마이그레이션 중 락을 쥔 채 멈춰, 서비스 전반이 느려지고 데이터베이스가 2분 동안 내려갔다incident.io · 2025같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
IL-0024무거운 API 요청이 MongoDB 에 과부하를 걸고 취소되지 않은 요청이 쌓여 Hugging Face Hub 가 사흘 동안 세 번 멈췄다Hugging Face · 2024같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
IL-0385큰 MySQL 테이블의 스키마 마이그레이션 마지막 단계에서 읽기 복제본 다수가 semaphore deadlock 에 걸려 GitHub 핵심 서비스가 2시간 50분 저하됐다GitHub · 2021같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
IL-0196Redis Operator 로 레플리카를 늘리면서 메모리 한도를 낮추자 데이터를 가진 유일한 Redis 파드가 재시작됐고, Sentinel 이 빈 레플리카를 마스터로 올려 데이터가 모두 지워졌다.Palark · 2020같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
IL-0388Redis 고가용 클러스터의 primary 노드가 내려간 뒤 Discord API 가 느려지고 길드 일부가 오프라인으로 보이며 접속이 실패했다Discord · 2017같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
내 클러스터에도 있는지 확인하기
AI 도구와 스크립트용
- 이 사례의 JSON: https://incidentlib-api.benchlet.com/cases/IL-0330
- 가장 비슷한 사례들: https://incidentlib-api.benchlet.com/cases/IL-0330/similar
- MCP 서버(Streamable HTTP, 키 없음):
https://incidentlib-mcp.benchlet.com/mcp
API 는 키가 필요 없습니다. MCP 서버는 search_incidents, get_incident, list_incidents, get_topic, vocab 도구를 내놓습니다. 사용법은 API 와 MCP.