{"id":"IL-0312","org":"Gadget","date":"2024-12-13","docs":[],"title":{"en":"Gadget's main Postgres did not start again after a restart to raise max_connections, and the platform went down","ko":"max_connections 를 올리려고 재시작한 주 PostgreSQL 데이터베이스가 다시 올라오지 않아 Gadget 의 모든 앱 요청이 멈췄다"},"domain":["database"],"impact":{"scope":"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","users":"All Gadget applications","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"Gadget names three deeper factors. Choosing the in-place restart put the database into a state the team had not seen before and was not confident in. Capacity planning for Black Friday / Cyber Monday assumed a doubling of traffic instead of the almost 5x that came, so settings such as max_connections stopped fitting and had to be changed in an emergency instead of in a maintenance window practised in staging. And there was no way to test database configuration changes in a production-like environment: integration tests run against a synthetic Postgres database that never goes through the months of production load behind the memory fragmentation or the memory usage issues.","ko":"Gadget 은 그 자리에서 재시작하는 방법을 고른 것을 판단 착오로 보았다. 확신할 수 없고 겪어 본 적 없는 상태로 시스템을 몰아넣은 위험한 선택이었다는 것이다. Gadget 은 모든 실패 상태를 미리 알 수는 없어도 처음 보는 실패 상태로 들어가는 일은 대개 피할 수 있다고 했다. Black Friday 와 Cyber Monday 의 용량 계획이 두 배로 잡혀 거의 5배의 부하를 감당하지 못했다. 제대로 계획했다면 점검 시간을 미리 잡고 스테이징에서 연습할 수 있었다. 새 데이터베이스 설정을 사용자에게 영향 없이 운영과 비슷한 환경에서 시험할 방법도 없었다. 통합 테스트는 합성 Postgres 데이터베이스와 쿠버네티스 인프라에서 돌기 때문에, 몇 달 동안의 운영 부하가 만드는 메모리 조각화나 메모리 사용 문제를 재현하지 못한다."},"related":[],"sources":[{"url":"https://gadget.dev/blog/database-configuration-change-and-service-disruption-december-13-2024","kind":"blog","archived":"https://web.archive.org/web/20260808155851/https://gadget.dev/blog/database-configuration-change-and-service-disruption-december-13-2024"}],"summary":{"en":"Gadget runs one main Postgres database for all application data, with PgBouncer pooling connections from the API servers' Node.js processes. Gadget had planned for a doubling of traffic over Black Friday / Cyber Monday but saw almost 5x the load, and max_connections was no longer high enough. PgBouncer fetches each user's password with an auth_query at connection time, the only PgBouncer authentication mode Gadget's database vendor supports, so new connections doubled during traffic spikes and caused small disruptions. Raising max_connections requires a full restart. The vendor offers an in-place restart or a failover to a new replica built with the new configuration; because the database holds many terabytes and a replica takes a long time to build, Gadget chose the in-place restart, expecting less downtime.\n\nPostgres shut down at 11:25 AM and failed to come back at 11:26, because months of uptime had fragmented the kernel's memory and the huge_pages allocations could not be met. The cause was identified at 11:45, and a hardware reset of the database server cleared the memory so Postgres started at 11:50. The platform was fully unavailable for 35 minutes. Compute that the autoscalers had removed during the outage then had to be scaled back up almost 4x, and all metrics recovered at 12:10 PM. Shopify webhooks were delayed but not lost, because Shopify retried delivery and Gadget's nightly reconciliation re-ran model actions for missed deliveries.","ko":"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 에 긴급 재시작을 결정했다.\n\n업체가 지원하는 재시작은 두 가지였다. 하나는 Postgres 프로세스를 SIGTERM 으로 내리고 같은 호스트에서 다시 띄우는 것이고, 다른 하나는 새 설정으로 replica 를 만들어 leader 를 따라잡게 한 뒤 장애 조치하는 것이다. 데이터베이스가 수 테라바이트라 replica 를 다시 만드는 데 오래 걸리므로, Gadget 은 중단 시간이 더 짧을 것으로 보고 그 자리에서 재시작하는 방법을 골랐다. 11:25 AM 에 재시작하자 Postgres 가 내려간 뒤 다시 올라오지 않았다. Postgres 는 huge_pages=on 설정에 따라 수 기가바이트의 연속 메모리를 요청했다. 그런데 몇 달 동안 쓴 서버의 커널 메모리가 조각나 있어, 전체 여유 메모리는 충분해도 연속 메모리가 모자랐다. 11:45 AM 에 메모리 조각화가 원인이라는 것을 찾았다. 데이터베이스 서버 자체를 하드웨어 리셋해 메모리를 비우자 11:50 AM 에 Postgres 가 올라왔다.\n\n데이터베이스가 멈춘 동안 오토스케일러가 컴퓨트를 줄였기 때문에 서비스가 돌아올 때 거의 4배의 컴퓨트를 다시 띄워야 했다. 이 때문에 12:10 PM EST 까지 지연이 높은 상태가 20분 더 이어졌다. 전체 서비스 저하는 약 45분이었다. Shopify 웹훅은 잃지 않았고 늦게 처리됐을 뿐이다. Shopify 가 웹훅을 재전송했고, Gadget 의 야간 대조 작업이 빠진 전송을 다시 처리했다."},"trigger":{"en":"An emergency in-place restart of the main Postgres server at 11:25 AM EST, decided at 11:00 AM, to raise max_connections, which Postgres cannot change without a full restart.","ko":"Gadget 은 플랫폼이 커지며 Postgres 의 max_connections 한도에 닿아 생기던 짧은 장애를 끝내려고, 이 값을 올리는 긴급 설정 변경을 결정했다. max_connections 는 서버를 재시작해야만 바뀌므로 11:25 AM EST 에 데이터베이스를 그 자리에서 재시작했다."},"severity":"unknown","symptoms":["service-unavailable","high-latency"],"timeline":{"detected_after":"1m","resolved_after":"45m"},"date_kind":"incident","detection":{"en":"The team carrying out the planned restart saw at 11:26 AM EST that Postgres had shut down and did not start again.","ko":"Gadget 은 11:25 AM EST 에 재시작한 데이터베이스가 기대대로 다시 올라오지 않는 것을 11:26 AM 에 확인했다."},"components":["database","linux-kernel","database-proxy"],"root_cause":"capacity","remediation":[{"text":{"en":"Restarted the database server with a hardware reset, which cleared the fragmented memory so Postgres could start","ko":"데이터베이스 서버를 하드웨어 리셋해 메모리 조각화를 없앴고, 11:50 AM EST 에 데이터베이스가 다시 올라왔다."}},{"text":{"en":"Provisioned almost 4x the compute after the autoscalers had scaled it down during the outage","ko":"데이터베이스가 멈춘 동안 오토스케일러가 줄인 컴퓨트를 당시의 거의 4배로 다시 띄웠고, 이 확장은 성공했지만 서비스 저하가 20분 더 이어졌다."}}],"action_items":[{"text":{"en":"Use only the replica-failover approach for disruptive database operations such as restarts","ko":"이번처럼 중단을 일으키는 데이터베이스 작업은 앞으로 replica 를 만들어 장애 조치하는 방법으로만 한다. 새 replica 를 먼저 띄우면 넘어가기 전에 운영 인프라 안에서 동작을 확인할 수 있다."}},{"text":{"en":"Run a more aggressive capacity planning process before Black Friday / Cyber Monday, preparing for a 6x increase in load and proving it with load tests well in advance","ko":"Black Friday 와 Cyber Monday 전의 용량 계획을 훨씬 공격적으로 세워 부하 6배 증가에 대비하고, 충분한 시간을 두고 종합 부하 시험을 해서 준비가 됐는지 확인한다."}},{"text":{"en":"Announce planned downtime ahead of time on the status page and community Discord, and make the operating procedures tell planned maintenance apart from service disruptions","ko":"계획된 중단은 상태 페이지와 커뮤니티 Discord 로 미리 알리고, 계획된 점검과 서비스 장애의 차이를 분명히 알리도록 표준 운영 절차를 다듬는다."}},{"text":{"en":"Finish the database sharding project so apps can use different databases and configuration changes can go through canary and blue/green setups","ko":"계획해 온 데이터베이스 샤딩 프로젝트를 끝내 Gadget 앱마다 다른 데이터베이스를 쓰게 한다. 그러면 데이터베이스 설정 변경에 카나리 배포와 블루/그린 구성을 써서 고객 앱에 닿기 전에 충분히 시험하고, 장애 범위도 좁힐 수 있다."}},{"text":{"en":"Finish building a zero-downtime database failover system","ko":"이미 진행 중인 무중단 데이터베이스 장애 조치 시스템을 완성해 어떤 앱도 멈추지 않고 데이터베이스 크기와 설정을 바꿀 수 있게 한다."}},{"text":{"en":"Set much higher scaling floors on the compute autoscalers","ko":"오토스케일러의 최소 규모를 크게 높였다. 앞으로 장애가 나도 컴퓨트가 훨씬 덜 줄어 복구가 빨라진다."}}],"contributing":["assumption-unverified","staging-not-representative","automation-amplified","blast-radius-too-wide"],"recurrence_of":null,"symptoms_text":{"en":"At 11:25 AM EST on 13 December 2024, Gadget restarted its main PostgreSQL server in place to raise max_connections. Postgres shut down but did not start again, and every request to Gadget applications failed, including frontend requests, GraphQL API requests, webhook processing and syncs.\n\nThe new Postgres process could not start because the kernel could not give it the memory it asked for. Postgres runs with huge_pages=on and allocates many gigabytes for its shared_buffer pools at startup. After months of continuous operation the server's memory had become fragmented, so there was enough free memory in total but not enough contiguous free memory for the huge page allocations. Neither Gadget nor its database vendor knew this could happen, and the cause was found at 11:45 AM.\n\nAfter the database came back at 11:50 AM, requests were processed with elevated latency until 12:10 PM. During the outage Gadget's compute autoscalers had seen less load and scaled down, so almost 4x the compute had to be provisioned after recovery, which strained the provisioning systems and added about 20 minutes in which some applications' requests were much slower.\n\nThe restart was itself a response to earlier, very small disruptions. Behind PgBouncer, each new gadget_api connection briefly opens two Postgres connections, one as superuser for the auth_query that fetches the user's password and one for the pooled connection, so traffic spikes quickly reached max_connections and returned errors to PgBouncer. PgBouncer retries such errors very slowly, so frontend API processes timed out on queries, and an API server whose SELECT 1 health check cannot run fast enough is marked unhealthy and removed from the load balancer.","ko":"2024-12-13 11:25 AM EST 에 Gadget 은 커넥션 한도를 올리려고 주 Postgres 데이터베이스를 그 자리에서 재시작했는데, 서버가 내려간 뒤 다시 올라오지 않았다. 그동안 Gadget 앱으로 오는 모든 요청이 실패했다.\n\n재시작 직후 35분 동안 프런트엔드 요청, GraphQL API 요청, 웹훅 처리, 동기화가 모두 멈췄다. 11:50 AM 에 데이터베이스가 올라온 뒤에도 요청은 지연이 높은 채로 처리됐고, 12:10 PM EST 에 모든 지표가 돌아왔다.\n\nPostgres 는 huge_pages=on 설정으로 시작할 때 shared_buffer 용으로 수 기가바이트의 연속 메모리를 커널에 요청한다. 몇 달 동안 계속 돌던 서버는 커널 메모리가 조각나 있었다. 전체 여유 메모리는 충분했지만 huge_pages 할당을 채울 연속 메모리가 없어, 새 Postgres 프로세스가 시작하지 못했다. Gadget 도 데이터베이스 업체도 이런 일이 생길 수 있다는 것을 몰라 원인을 찾는 데 시간이 걸렸다. 11:45 AM 에 메모리 조각화가 원인이라는 것을 찾았다.\n\n데이터베이스가 멈춘 동안 오토스케일러가 부하가 줄었다고 보고 컴퓨트를 줄였다. 서비스가 돌아왔을 때는 그때 떠 있던 컴퓨트의 거의 4배를 띄워야 했다. 이 확장이 프로비저닝 시스템에 부담을 주면서 일부 앱의 요청이 느리게 처리되는 시간이 20분 더 이어졌다.\n\n이 재시작은 플랫폼이 빠르게 커지면서 커넥션 풀 압박으로 생긴 아주 짧은 장애들에 대응하는 작업이었다. 트래픽이 크게 몰리면 PgBouncer 가 auth_query 로 비밀번호를 가져오는 연결까지 만들어 새 gadget_api 연결 하나가 Postgres 연결 둘이 됐고, 그래서 max_connections 한도에 빠르게 닿았다. PgBouncer 는 연결을 만들다 오류가 나면 아주 느리게 재시도했고, 프런트엔드 API 프로세스의 쿼리가 타임아웃됐다. API 서버의 헬스 체크인 SELECT 1 이 제때 돌지 못하면 그 서버는 로드 밸런서에서 빠졌다."},"updated_at":"2026-11-14T12:33:56.596898Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0312","org_slug":"gadget"}