그래프
사례5건
5건 중 1~5번째
- 증상
learn.mit.edu 가 멈췄다는 보고가 Sentry 이슈 링크와 함께 올라왔다. MIT Learn 의 운영 데이터베이스인 RDS PostgreSQL 인스턴스가 쓸 수 있는 저장 공간을 모두 써 버린 상태였고, 사이트는 503 과 504 오류를 냈다.
- 촉발
정기 릴리스 절차로 배포한 변경이 페이지 응답의 메인 쿼리 안에 집계를 넣었다. DRF 는 페이지 응답의 count 칸을 SELECT COUNT * ($QUERY) 로 계산하므로, 운영의 데이터 규모에서 이 쿼리가 임시 파일을 대량으로 만들었다.
- 원인
- 구성 요소
- 기여 요인
- 감지
엔지니어가 장애를 보고하며 Sentry 이슈를 연결했다. 이어 다른 엔지니어가 운영 데이터베이스의 저장 공간이 바닥난 것을 확인했고, FreeStorageSpace 지표와 Sentry 에 쌓인 504 도 같은 상황을 보여 줬다.
- 조치
- 쿼리 패턴 변경을 쓴 엔지니어가 그 변경이 원인임을 확인하고 릴리스를 롤백했다.
- 데이터베이스가 버퍼에 쌓인 WAL 파일을 따라잡아 비울 여유를 주려고 읽기 복제본을 삭제했다.
- AWS 지원의 도움을 받아 백업이 끝난 뒤 오래 돌던 쿼리를 안전하게 종료했다. pg_stat_activity 에서 state = 'active' 이고 시작한 지 10분이 넘은 쿼리를 pg_terminate_backend(pid) 로 끝내자 저장 공간과 처리 여유가 돌아왔다.
- 후속 조치
- 운영 learn 데이터베이스에 enhanced analytics 를 켜는 방안을 검토한다.
- 플랫폼 엔지니어링 문서 사이트에 롤백 절차를 적는다.
- 장기적으로는 새로 만드는 릴리스 관리 체계에서 롤백을 쉽게 할 수 있게 한다.
- 운영 데이터베이스의 DiskQueueDepth 에 알림을 걸되 너무 자주 울리지 않게 한다.
- API 와 사이트의 응답 시간이 평소보다 30% 넘게 높아지면 알림을 보낸다.
- 운영 데이터베이스 장애를 예제로 삼아 게임데이 훈련을 한다.
- 장애 관리 도구 Rootly 를 써서 장애 때 소통을 한 채널에 자동으로 모으고 장애 기록을 자동으로 남긴다.
- 장애 대응 런북을 가볍게 만든다. 장애가 나면 Zoom 통화를 열고, 당번 엔지니어가 지표가 평소 수준으로 돌아오는지 지켜보며, 소통 담당을 따로 두어 이해관계자의 문의가 핵심 대응자를 방해하지 않게 한다.
- 오래 도는 쿼리를 종료하는 SQL 과, 이 SQL 을 고쳐 아주 오래 도는 쿼리만 골라 종료하는 SQL 을 운영 Postgres 런북에 넣는다.
- Learn Resource View Event 모델이 지금은 건수를 세는 데만 쓰이므로 다시 설계해야 한다는 이슈를 쓴다.
- 익명화한 운영 데이터 생성 도구로 RC 환경의 데이터 양을 운영 수준까지 늘린다. 운영 데이터를 익명화해 담은 운영 규모의 부하 시험 환경을 만들고, 이해관계자도 시험에 쓰는 RC 에 영향이 없도록 인프라를 분리한다.
- 조회 수를 미리 계산해 둔 열에서 읽도록 고쳐 배포했다. 영향을 받던 API 의 운영 응답 시간이 약 13~15초에서 150~200ms 로 줄었다.
- 교훈
이 변경은 가장 시니어인 엔지니어 두 명이 꼼꼼히 리뷰했지만 로컬과 RC 환경에서는 아무 문제도 드러나지 않았다. 운영의 데이터 규모와 시스템 부하가 크게 달랐고, 효율적이라고 여긴 COUNT 집계가 디스크 사용량을 폭증시킬 것이라는 점은 당시 시스템 상태만 보고는 알기 어려웠다. 그래서 운영과 같은 규모의 데이터로 부하를 시험할 환경이 필요하다고 팀은 결론지었다. AWS 지원은 ORM 쿼리 변경을 운영 수준의 데이터를 담은 스테이징에서 시험하고, EXPLAIN ANALYZE 로 실행 계획과 예상 자원 사용을 보며, 시험 중 log_temp_files 로 임시 파일 생성을 지켜보라고 권했다. 폭주하는 쿼리를 막도록 temp_file_limit 를 설정하고, 실행 계획에 따라 work_mem 이나 shared_buffers 를 조정하되 메모리를 너무 쓰면 out of memory 로 장애 조치가 일어날 수 있다는 점도 짚었다. 임시 파일을 많이 쓰는 작업에는 임시 파일을 EBS 대신 로컬 NVMe 에 두는 RDS Optimized Reads 를 고려하라고 했다.
- 증상
2026-02-19 8:11 AM PST 에 데이터베이스 성능이 갑자기 떨어졌다. 요청 핸들러에 요청이 쌓였고, Clerk 트래픽의 95% 넘게 처리되지 못한 채 429 를 받았다. 데이터베이스까지 간 일부 요청은 200 을 받았지만 아주 느렸다.
- 촉발
Postgres 의 일상적인 자동 ANALYZE 가 표본을 너무 적게 뽑아 한 컬럼의 값이 100% NULL 이라고 잘못 판단했다. 그 결과 자주 실행되는 쿼리의 플랜이 비효율적인 것으로 바뀌었다.
- 원인
- 구성 요소
- 기여 요인
- 감지
16:15 UTC 에 사고가 내부에 보고되고 에스컬레이션됐고, 팀이 그룹 통화를 열어 근본 원인을 찾기 시작했다.
- 조치
- 17:08 UTC 에 핵심 Session API 밖에서 세션 토큰을 만드는 새 장애 조치를 켜서 백엔드 부하를 줄였고, 17:10 UTC 에 많은 사용자가 고객 앱에 다시 들어갔다.
- 17:27 UTC 에 해당 테이블에 ANALYZE 를 수동으로 다시 돌렸고, 쿼리가 이전 플랜으로 돌아가 데이터베이스가 회복하기 시작했다.
- 17:43 UTC 에 시스템이 안정되고 데이터베이스가 정상 수준으로 돌아오자 새 장애 조치를 껐다.
- 사고 직후 해당 테이블의 statistics target 을 올려 쿼리 플래너가 더 크고 정확한 표본을 보게 했다.
- 그날 저녁 플래너가 결정론적인 플랜을 세울 수 있게 쿼리를 다시 짰다.
- 후속 조치
- 데이터베이스 쿼리 플랜이 바뀌는 것을 잡는 전용 알림을 더한다.
- 안정성 팀이 새 세션 토큰 장애 조치를 켜는 방법을 익히게 하고, 자동 작동 조건을 건다. 몇 주 안에 끝내는 것이 목표다.
- 불안정한 통계로 비효율적인 플랜이 생길 위험이 있는 쿼리가 더 있는지 모든 쿼리를 점검한다.
- 사고 때 소통 전담자를 둔다.
- 상황이 그대로여도 정해진 간격으로 상태 페이지를 갱신한다.
- 상태 페이지의 심각도 표시가 고객이 겪는 영향과 맞게 한다.
- 사고 도구를 고쳐 공지가 소셜 채널에도 함께 올라가게 한다.
- 교훈
Clerk 는 쿼리 플랜이 바뀌는 문제가 갑자기 심하게 성능을 떨어뜨리므로, 뒤따르는 증상에 기대지 말고 바로 잡아내야 한다고 보았다. 이런 알림이 있었다면 고객의 트래픽 패턴이 원인이라고 잘못 보는 일을 피할 수 있었다. 데이터베이스가 내려가지 않고 느려지기만 하자 Postgres 장애를 기준으로 만든 Session API 의 자동 장애 조치는 작동하지 않았다. 고객들은 상태 페이지 공지가 너무 드물고, 첫 심각도 표시가 실제 영향과 맞지 않았으며, 첫 공지까지 시간이 너무 걸렸다고 지적했다.
- 증상
패키지 매출 랭킹 기능을 배포한 뒤 버그 픽스를 몇 차례 더 배포하고 유저를 재접속시키자 Database 알람이 울렸고, Database CPU 그래프가 100%에서 내려오지 않았다. Database 접속에 문제가 생기면서 유저들이 게임에 정상적으로 접속하거나 플레이할 수 없었다.
- 촉발
새 패키지 매출 랭킹 기능의 집계 쿼리가 들어간 배포 뒤 버그 픽스를 추가로 배포하고 유저를 재접속시키면서, 캐시가 빈 서버들이 이 쿼리를 한꺼번에 실행했다.
- 원인
- 구성 요소
- 기여 요인
- 감지
버그 픽스를 추가로 배포하고 유저를 재접속시킨 뒤 Database 알람이 울려 CPU 그래프를 보고 알았다.
- 조치
- 서버 점검을 걸었다.
- 문제가 된 패키지 매출 랭킹 기능을 켜고 끌 수 있는 설정을 빠르게 추가해 꺼 둔 뒤 서비스를 정상화했다.
- 판매 시작 시간을 공유하는 상품들의 product_id 조건을 하나로 묶고, 의미가 없는 현재 시각 조건을 지운 쿼리로 바꿔 바로 적용했다.
- 모든 서버가 함께 쓰는 캐시를 스케줄러 서버에 따로 두었다. 각 서버는 Database 대신 원격 저장소의 결과를 읽고, 요청은 큐에 넣는다. 스케줄러 서버는 주기적으로 마지막 요청으로 쿼리해 결과를 원격 저장소에 캐싱한다. 이렇게 해서 쿼리 실행 횟수를 줄였다.
- 쿼리 최적화와 캐시 사용 방식 개선을 적용한 뒤 패키지 매출 랭킹 기능을 다시 켰고, CPU가 문제 없이 유지되는 것을 확인했다.
- 교훈
무거운 쿼리와 분산 서버의 캐시 구조가 겹치면 예상하지 못한 문제가 생기므로, 문제가 될 만한 지점을 미리 파악해야 한다. 장애가 나면 원인 파악보다 서비스 정상화가 먼저다. 문제가 되는 기능을 끄고 켤 수 있는 안전장치를 미리 만들어 두면 서비스를 더 안정적으로 운영하는 데 도움이 된다. Production 환경의 online alter에는 Percona Toolkit 같은 유용한 툴을 잘 활용하는 것이 중요하다.
- 증상
2023-08-11 13:36 BST 에 여러 서비스에서 모든 데이터베이스 트랜잭션이 실패한다는 알림이 들어왔다. 데이터베이스를 다시 시작하려 했지만 공간이 없다며 올라오지 않았다. 메일도 나가지 않았다.
- 촉발
13:15 BST 에 cron 인스턴스 메모리를 1gb 에서 8gb 로 늘려 밀려 있던 보존 기간 정리 작업을 돌렸다. destroy_all 이 연결된 레코드까지 한꺼번에 많이 지우면서 데이터베이스 디스크가 빠르게 찼다.
- 원인
- 구성 요소
- 기여 요인
- 감지
13:00 BST 정기 점검에서 데이터베이스가 평소보다 큰 것을 먼저 봤다. 13:36 에는 여러 서비스에서 모든 데이터베이스 트랜잭션이 실패한다는 알림이 곧바로 들어왔다. 상태 페이지 두 개는 장애를 잡지 못했다. 업스트림 제공자는 저장 공간이 99% 찼을 때 자동으로 알림을 받았다.
- 조치
- Postgres 가 관리형 서비스였으므로 13:37 BST 에 곧바로 업스트림 제공자 지원팀에 연락했다.
- 13:40 에 Twitter 와 Mastodon 으로 사용자에게 장애를 알렸다. 상태 페이지가 자동으로 바뀔 것으로 생각했지만 바뀌지 않았다.
- 제공자의 답을 기다리는 동안 13:45 에 전체 장애 복원 절차를 시작해 6시간 전 오프사이트 백업을 새 데이터베이스 인스턴스에 준비했다.
- 15:50 에 제공자의 이전이 끝나 더 큰 데이터베이스 인스턴스로 옮겨졌고 메일이 다시 나갔다.
- 백업에서 시험해 고친 쿼리로 운영의 오래된 데이터를 하루 단위로 직접 지웠다. 그동안 메모리, 데이터베이스 크기, 지표를 가까이 지켜봤다.
- 후속 조치
- 매일 도는 정리 작업을 in_batches(of: 50) 로 바꿔 한 번에 50건씩만 지우게 했다. ruby 메모리 요구량과 postgres 부하가 줄었다.
- 정리 작업을 하루 한 번이 아니라 네 번 돌게 바꿔 한 번 실행의 영향을 줄였다.
- 성장 여유가 훨씬 많은 더 큰 데이터베이스 인스턴스로 옮겼다.
- 모든 cron 작업에 cron 모니터링을 적용했다.
- 상태 페이지가 헬스 체크 엔드포인트 /health 를 보게 바꿨다. 이 엔드포인트는 데이터베이스 접근을 포함해 여러 가지를 확인하고 하나라도 실패하면 500 오류로 응답한다.
- 자동 장애 조치를 위한 핫 레플리카 데이터베이스를 검토한다.
- 교훈
장애는 여러 원인이 겹쳐 났다. 오래된 데이터를 지우는 cron 작업이 조용히 실패해 데이터가 쌓였다. 이 작업은 평소 하루치 양이었다면 문제없었겠지만 지나치게 단순하게 짜여 있었다. 데이터베이스 요금제는 처음부터 성장 여유가 부족했다. 상태 페이지는 장애를 잡지 못했다. MailPace 는 중요한 서비스마다 제공자를 여럿 두라고 권했다. 또 장애를 감지해 메일을 예비 제공자로 보내는 오픈 소스 아웃바운드 SMTP 프록시 Outgoer 를 만들고 있다고 밝혔다.
- 증상
2022년 1월 3일부터 17일까지 이어진 100% 할인 이벤트 기간에, 인프런은 1월 3일, 4일, 7일, 17일 오전 9시와 오후 10시 무렵마다 쓸 수 없을 만큼 느려졌다. 트래픽은 평소의 5~20배였고, 매번 데이터베이스의 슬로우 쿼리와 부하가 함께 나타났다.
- 촉발
2022년 1월 강의 125개를 0원에 등록할 수 있는 100% 할인 이벤트가 크게 퍼지면서 평소 대비 5~20배의 트래픽이 들어왔다.
- 원인
- 구성 요소
- 기여 요인
- 감지
모니터링과 슬로우 쿼리 알람으로 문제를 알아챘고, 문제가 되는 쿼리를 30분 안에 찾았다. 1월 17일에는 오후 10시부터 RDS 의 여러 수치가 요동치는 것을 보았다.
- 조치
- 관심회사 조회 API 의 쿼리 일부가 인덱스를 쓰지 않는 것을 로그와 실행계획으로 확인하고 바로 인덱스를 추가했다.
- 용량이 큰 캐시 행의 캐시 초기화와 재생성 주기를 바꿨다.
- 쓰기 DB 에 부하를 준 조회용 쿼리부터 읽기 DB 로 옮겼다.
- 시간을 벌려고 데이터베이스를 r5.8xlarge 8대로 늘렸다.
- 느린 강의 조회 쿼리에 인덱스를 추가하거나 쿼리를 바꿔 인덱스를 쓰게 했고, 개선할 수 없는 쿼리를 쓰는 기능은 껐다.
- select * 로 읽던 강의 조회 쿼리에서 실제로 쓰지 않는 고용량 강의 소개 컬럼을 모두 뺐다.
- IN 절에 담는 강의 ID 를 20개씩 나눠 Promise.all 로 처리하게 바꿔, 쿼리가 다시 인덱스를 쓰게 했다.
- PostgreSQL 을 10.16 에서 11.13 으로 올렸다.
- 5초 넘게 도는 쿼리를 강제로 끝내도록 쿼리 타임아웃을 5초로 걸었다.
- Max Connection 을 늘렸다.
- 강의 테이블에 수동으로 vacuum 을 돌려 Dead Tuple 을 정리했다.
- 용량이 큰 강의 소개 컬럼을 강의 테이블에서 떼어 별도 테이블로 옮겼다.
- 후속 조치
- 한 테이블의 장애가 전체 서비스의 장애로 번지는 구조를 개선한다.
- 성능 개선이 쉽지 않은 테이블과 캐시 구조를 개선한다.
- 직관적이지 않은 SQL 코드를 개선한다.
- 타입과 테스트 코드가 없어 안심하고 고치기 어려운 코드를 개선한다.
- 배치 작업을 안정적으로 돌릴 환경을 갖춘다.
- 모놀리식 구조는 알맞은 선에서 유지하고, 장애가 잘 번지는 일부 도메인만 따로 분리한다.
- 교훈
그동안 데이터베이스 병목이 모두 읽기 DB 의 슬로우 쿼리로 보여 팀은 읽기 DB 만 모니터링했다. 이 때문에 근본 원인인 쓰기 DB 강의 테이블의 Dead Tuple 을 늦게 찾았다. 원인 쿼리는 빨리 찾았지만, 무분별한 쿼리 추상화와 타입과 테스트 코드가 없는 코드 때문에 수정과 배포가 늦어졌다. 그만큼 장애 시간도 길어졌다. 첫 주 장애를 돌아보면 어느 페이지가 문제가 될지 예상할 수 없었고, 대부분의 페이지가 트래픽이 몰리면 언제든 장애가 날 수 있는 상태였다. 팀은 조직의 구조와 성숙도가 갖춰지지 않은 상태에서 마이크로서비스 아키텍처인 MSA 로 바로 옮기면 더 큰 문제가 생긴다고 보았다. 그래서 모놀리식 구조를 알맞은 선에서 유지하면서 일부 도메인만 분리하기로 했다.
이 5건에서 가장 많이 나오는 구성 요소: 데이터베이스, PostgreSQL vacuum. kubegap 은 Claude Code 나 Codex 에서 쿠버네티스 클러스터를 읽기만 하며 이런 틈을 점검합니다. 점검 항목의 근거는 이 라이브러리와 같은 공개 보고서입니다.