Incident Library

IL-0312 Gadget · 2024-12-13database

max_connections 를 올리려고 재시작한 주 PostgreSQL 데이터베이스가 다시 올라오지 않아 Gadget 의 모든 앱 요청이 멈췄다

Gadget 은 모든 앱 데이터를 PostgreSQL 데이터베이스 하나에 두고, API 서버의 Node.js 프로세스들은 PgBouncer 로 커넥션 풀을 나눠 쓴다. 이 데이터베이스 업체는 PgBouncer 가 연결할 때마다 auth_query 로 사용자 비밀번호를 가져오는 인증 방식만 지원한다. auth_query 도 연결 하나를 쓰기 때문에, 새 gadget_api 연결 하나가 잠깐 Postgres 연결 둘이 된다. Gadget 은 Black Friday 와 Cyber Monday 에 트래픽이 두 배로 늘 것으로 보고 계획했지만, 실제 부하는 거의 5배로 늘었다. 트래픽이 몰릴 때마다 max_connections 한도에 빠르게 닿아 아주 짧은 장애가 났다. max_connections 는 Postgres 를 재시작해야만 바뀐다. Gadget 은 짧은 장애를 오래 겪기보다 한 번 짧게 멈추고 한도를 크게 올리기로 했다. 그래서 12월 13일 11:00 AM EST 에 긴급 재시작을 결정했다.

업체가 지원하는 재시작은 두 가지였다. 하나는 Postgres 프로세스를 SIGTERM 으로 내리고 같은 호스트에서 다시 띄우는 것이고, 다른 하나는 새 설정으로 replica 를 만들어 leader 를 따라잡게 한 뒤 장애 조치하는 것이다. 데이터베이스가 수 테라바이트라 replica 를 다시 만드는 데 오래 걸리므로, Gadget 은 중단 시간이 더 짧을 것으로 보고 그 자리에서 재시작하는 방법을 골랐다. 11:25 AM 에 재시작하자 Postgres 가 내려간 뒤 다시 올라오지 않았다. Postgres 는 huge_pages=on 설정에 따라 수 기가바이트의 연속 메모리를 요청했다. 그런데 몇 달 동안 쓴 서버의 커널 메모리가 조각나 있어, 전체 여유 메모리는 충분해도 연속 메모리가 모자랐다. 11:45 AM 에 메모리 조각화가 원인이라는 것을 찾았다. 데이터베이스 서버 자체를 하드웨어 리셋해 메모리를 비우자 11:50 AM 에 Postgres 가 올라왔다.

데이터베이스가 멈춘 동안 오토스케일러가 컴퓨트를 줄였기 때문에 서비스가 돌아올 때 거의 4배의 컴퓨트를 다시 띄워야 했다. 이 때문에 12:10 PM EST 까지 지연이 높은 상태가 20분 더 이어졌다. 전체 서비스 저하는 약 45분이었다. Shopify 웹훅은 잃지 않았고 늦게 처리됐을 뿐이다. Shopify 가 웹훅을 재전송했고, Gadget 의 야간 대조 작업이 빠진 전송을 다시 처리했다.

심각도
–
영향 범위
All requests to Gadget applications: fully unavailable for 35 minutes after the restart, then elevated latency until 12:10 PM EST; Shopify webhook data delayed but not lost
영향 사용자
All Gadget applications
지역
–
감지까지
1m
복구까지
45m

증상

2024-12-13 11:25 AM EST 에 Gadget 은 커넥션 한도를 올리려고 주 Postgres 데이터베이스를 그 자리에서 재시작했는데, 서버가 내려간 뒤 다시 올라오지 않았다. 그동안 Gadget 앱으로 오는 모든 요청이 실패했다.

재시작 직후 35분 동안 프런트엔드 요청, GraphQL API 요청, 웹훅 처리, 동기화가 모두 멈췄다. 11:50 AM 에 데이터베이스가 올라온 뒤에도 요청은 지연이 높은 채로 처리됐고, 12:10 PM EST 에 모든 지표가 돌아왔다.

Postgres 는 huge_pages=on 설정으로 시작할 때 shared_buffer 용으로 수 기가바이트의 연속 메모리를 커널에 요청한다. 몇 달 동안 계속 돌던 서버는 커널 메모리가 조각나 있었다. 전체 여유 메모리는 충분했지만 huge_pages 할당을 채울 연속 메모리가 없어, 새 Postgres 프로세스가 시작하지 못했다. Gadget 도 데이터베이스 업체도 이런 일이 생길 수 있다는 것을 몰라 원인을 찾는 데 시간이 걸렸다. 11:45 AM 에 메모리 조각화가 원인이라는 것을 찾았다.

데이터베이스가 멈춘 동안 오토스케일러가 부하가 줄었다고 보고 컴퓨트를 줄였다. 서비스가 돌아왔을 때는 그때 떠 있던 컴퓨트의 거의 4배를 띄워야 했다. 이 확장이 프로비저닝 시스템에 부담을 주면서 일부 앱의 요청이 느리게 처리되는 시간이 20분 더 이어졌다.

이 재시작은 플랫폼이 빠르게 커지면서 커넥션 풀 압박으로 생긴 아주 짧은 장애들에 대응하는 작업이었다. 트래픽이 크게 몰리면 PgBouncer 가 auth_query 로 비밀번호를 가져오는 연결까지 만들어 새 gadget_api 연결 하나가 Postgres 연결 둘이 됐고, 그래서 max_connections 한도에 빠르게 닿았다. PgBouncer 는 연결을 만들다 오류가 나면 아주 느리게 재시도했고, 프런트엔드 API 프로세스의 쿼리가 타임아웃됐다. API 서버의 헬스 체크인 SELECT 1 이 제때 돌지 못하면 그 서버는 로드 밸런서에서 빠졌다.

증상 서비스 중단지연 시간 증가

원인

촉발
Gadget 은 플랫폼이 커지며 Postgres 의 max_connections 한도에 닿아 생기던 짧은 장애를 끝내려고, 이 값을 올리는 긴급 설정 변경을 결정했다. max_connections 는 서버를 재시작해야만 바뀌므로 11:25 AM EST 에 데이터베이스를 그 자리에서 재시작했다.

감지

Gadget 은 11:25 AM EST 에 재시작한 데이터베이스가 기대대로 다시 올라오지 않는 것을 11:26 AM 에 확인했다.

조치

후속 조치

교훈

Gadget 은 그 자리에서 재시작하는 방법을 고른 것을 판단 착오로 보았다. 확신할 수 없고 겪어 본 적 없는 상태로 시스템을 몰아넣은 위험한 선택이었다는 것이다. Gadget 은 모든 실패 상태를 미리 알 수는 없어도 처음 보는 실패 상태로 들어가는 일은 대개 피할 수 있다고 했다. Black Friday 와 Cyber Monday 의 용량 계획이 두 배로 잡혀 거의 5배의 부하를 감당하지 못했다. 제대로 계획했다면 점검 시간을 미리 잡고 스테이징에서 연습할 수 있었다. 새 데이터베이스 설정을 사용자에게 영향 없이 운영과 비슷한 환경에서 시험할 방법도 없었다. 통합 테스트는 합성 Postgres 데이터베이스와 쿠버네티스 인프라에서 돌기 때문에, 몇 달 동안의 운영 부하가 만드는 메모리 조각화나 메모리 사용 문제를 재현하지 못한다.

출처

관련 사례

내 클러스터에도 있는지 확인하기

kubegap 은 Claude Code 나 Codex 에서 쿠버네티스 클러스터를 읽기만 하며 이 사례와 같은 상태를 점검합니다. 이 사례의 구성 요소: 데이터베이스, 리눅스 커널, 데이터베이스 프록시와 커넥션 풀러. 점검 항목의 근거는 이 사례 같은 공개 보고서입니다.

AI 도구와 스크립트용

API 는 키가 필요 없습니다. MCP 서버는 search_incidents, get_incident, list_incidents, get_topic, vocab 도구를 내놓습니다. 사용법은 API 와 MCP.