Incident Library
  • 원인
  • 구성 요소
  • 증상
  • 기여 요인
  • 사례

사례4건

4건 중 1~4번째

  1. 증상

    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 를 고려하라고 했다.

  2. 증상

    2023년 7월 24일 밤 저장소와 쿼리 엔진인 Retriever 의 클러스터를 바꾼 지 몇 시간 뒤 수집 서비스 Shepherd 의 내부 SLO 에러 버짓이 천천히 소진되기 시작했다. 엔지니어들은 소진이 느리고 들쭉날쭉하며 뚜렷한 설명이 없어서 아침에 조사해도 되는 작은 문제라고 판단했다.

    촉발

    7월 24일 밤 미묘한 버그를 피하려고 Retriever 클러스터 두 개 사이를 전환했다. 전환 뒤 Retriever 가 마지막 쓰기 시각 기록을 데이터베이스에 쓰지 않았고, 그 뒤 쓰기만 되돌리려고 기능 플래그를 바꾸자 남은 쓰기마저 완전히 멈췄다.

    원인
    구성 요소
    기여 요인
    감지

    Shepherd 의 내부 SLO 에러 버짓이 천천히 소진되는 것을 엔지니어들이 봤지만 작은 문제로 판단해 아침으로 미뤘다. 아침에 Retriever 의 데이터베이스 호출이 끊긴 차트를 찾아 성능 문제의 원인을 알았다.

    조치
    • 클러스터의 쓰기에 문제가 있다고 보고 자주 쓰던 기능 플래그를 바꿔 쓰기만 다른 클러스터로 되돌렸지만 고쳐지지 않았다.
    • 데이터베이스를 보호하려고 서킷 브레이커를 걸어 모든 Shepherd 트래픽을 5xx 오류로 거부했다.
    • 데이터베이스 호스트가 완전히 잠겨 살릴 수 없자 복제본으로 페일오버했고, 데이터베이스와 그에 기대는 서비스들이 돌아왔다.
    • 데이터베이스가 살아난 뒤 캐시에 데이터를 채우려고 Retriever 호스트를 모두 재시작했다.
    • 지난 하루 동안 트래픽이 있던 모든 스키마의 마지막 쓰기 시각을 지금으로 손으로 바꿔 스키마 저장소가 캐시를 모두 다시 읽게 했다.
    • 서킷 브레이커를 풀어 수집을 완전히 되살렸고, 데이터베이스 오류로 내부 점검에 실패한 호스트를 다시 재시작해 쿼리 용량을 되찾았다.
    후속 조치
    • 클러스터 이전을 마쳤고, 캐시 뒤의 쓰기를 끌 수 있는 코드를 모두 지웠다.
    • 캐시를 더 튼튼하게 하는 방법을 찾는다.
    • 새 스키마로 갱신이 몰릴 때 데이터베이스에 거는 경합을 줄이는 방법을 찾는다.
    • 시스템 안 일부 작업의 성능 비용이 들쭉날쭉하지 않게 한다.
    • 이런 드문 경우를 더 잘 찾고 다루기 위한 계측과 실험 방법을 검토한다.
    교훈

    Honeycomb 은 기능 플래그, 잦은 배포, 장애 중 배포 중단, 지난 아차 사고에서 배운 것처럼 시스템을 안전하게 만드는 가장 믿는 관행들이 함께 이번 장애의 원인이 됐다고 봤다. 저마다 합리적인 결정들이 간접 의존 관계를 따라 예상하지 못하게 얽혔고, 환경 한쪽의 문제가 다른 쪽 서비스를 망가뜨렸다. 최근 여러 장애와 아차 사고에서 미묘한 버그를 피하려던 조치가 더 큰 예상 밖 장애로 이어진 일이 자주 있었다. 이런 드문 데이터베이스 잠김에는 페일오버가 가장 빠른 복구 방법이라는 것도 배웠다.

  3. 증상

    2022년 6월 27일 13:00 UTC 에 Basecamp 3 가 갑자기 아주 느려졌고 곧 오류 페이지를 보였다. 같은 30초 사이에 Basecamp 3 의 주 데이터베이스가 정상에서 완전한 정체 상태로 넘어갔다.

    촉발

    13:00 UTC 에 예약 알림을 대량으로 묶어 보내는 시각에 맞춰 부하가 큰 데이터베이스 쿼리가 한꺼번에 몰려들었고, 이 쿼리들이 주 데이터베이스에서 락 경합을 연쇄로 일으켰다.

    원인
    구성 요소
    기여 요인
    감지

    37signals 는 13:00:00 과 13:00:30 UTC 사이에 Basecamp 3 의 주 데이터베이스가 완전히 정체되는 것을 보고 곧바로 대응을 시작했다.

    조치
    • 주 데이터베이스의 부하를 덜려고 Basecamp 3 의 일부 기능을 멈췄다. 진행 보고 이메일을 그날 늦게 보내도록 미룬 것이 그 예다.
    • 막힌 주 데이터베이스에서, 이런 장애에 대비해 대기 중이던 복제본 데이터베이스로 절차대로 페일오버했다.
    • 페일오버 뒤 데이터베이스가 다시 무너지자 Basecamp 3 를 곧바로 유지보수 모드로 바꿨다.
    • 데이터베이스 쿼리 시간에 강제 상한을 걸었다.
    • 쿼리 시간 상한을 안전장치로 두고 기능을 한 단계씩 다시 켜서 서비스를 모두 되살렸다.
    후속 조치
    • 데이터베이스 쿼리에 강제 시간 상한을 둔다. 평소 빠르던 쿼리가 일정 수준 넘게 느려지면 기다리며 쌓이게 두지 않고 실패하게 한다.
    • 동시 실행 한도를 조정한다. 일정 수준을 넘으면 병렬로 도는 작업이 많을수록 시스템 전체가 느려지기 때문이다.
    • 서비스 수준 기대치를 더 엄격하게 잡고, 예약 알림과 보고서를 몇 분 안에 대량으로 보내며 생기던 정시 데이터베이스 부하 급증을 더는 용인하지 않는다. 제품 설계부터 데이터베이스 내부까지 이 부분을 살핀다.
    • 일부 예약 작업을 돌리는 병렬 워커 수를 줄여 동시 실행과 락 경합을 낮춘다.
    • 일부 예약 작업을 한꺼번에 끝내지 않고 몇 분에 걸쳐 돌려 데이터베이스 부하를 시간에 나눈다.
    • 한꺼번에 몰려드는 쿼리 다수를 주 데이터베이스에서 읽기 전용 복제본 데이터베이스로 옮긴다.
    • 지난 1년의 데이터베이스 지표에서 찾은 성능 저하 여러 건을 하나씩 고친다. 이 저하는 알림을 울리지 않았지만 주 데이터베이스 부하를 서서히 늘렸다.
    • 한꺼번에 몰려드는 다른 작업을 찾아 분산해, 데이터베이스의 특정 지점에 부하가 몰리지 않게 한다.
    • 높은 부하와 높은 동시성에서 더 예측 가능하게 동작하도록 데이터베이스를 조정한다.
    • 데이터베이스 내부의 락과 트랜잭션 상태를 더 잘 볼 수 있도록 관측성을 높인다.
    • 장애 중에 부하가 큰 Basecamp 3 기능을 골라 끌 수 있는 더 쓰기 편한 도구를 만든다.
    • 멈춘 데이터베이스에 대응하는 운영 훈련을 잡아 대응을 연습하고 다듬는다.
    • 앞으로 몇 년의 Basecamp 성장에 맞춰 확장할 수 있도록 데이터베이스 소프트웨어와 하드웨어 업그레이드를 검토한다.
    • Basecamp 3 를 받치는 구성 요소에 서비스 수준 목표, 곧 SLO 를 정해 데이터베이스 성능 변화를 더 일찍, 더 분명하게 알아챈다.
    • 분기마다 성능과 부하를 검토해 눈에 띄지 않게 진행되는 성능 변화를 찾는다.
    교훈

    37signals 는 이번 장애에 원인 하나가 없었다고 밝혔다. 시스템이 한계를 조금만 넘어도 순식간에 안정에서 불안정으로 넘어가는 지점이 있고, 그 지점은 특별한 계기 없이 성장과 함께 서서히 다가온다. 막힌 쿼리를 종료하는 일은 트랜잭션 롤백 때문에 같은 락에서 다시 막혀 정체를 키울 수 있다. 37signals 는 이 시스템을 잘 알면서도 결정적인 단서가 없고 데이터베이스 내부를 깊이 들여다볼 수단이 없어 확신을 갖고 대응하지 못했다고 썼다. 그래서 문제 하나를 고치는 대신 위험을 줄이고 확신을 쌓아 가는 길을 택했다.

  4. 증상

    2020-01-22 오전 9시 45분 PST부터 데이터베이스 지표에서 쓰기 IOPS와 락 경합이 늘었다. 전날 사고와 달리 데이터베이스 CPU는 평소보다 낮았다. 오전 10시에 자동 알림이 왔고, 일부 사용자는 Figma를 불러오지 못했다.

    촉발

    사고가 시작된 바로 그 시각에 테이블 하나의 자동 ANALYZE가 끝나 통계가 바뀌었고, PostgreSQL 9 쿼리 플래너가 ORM이 만든 질의 하나의 행 수를 크게 잘못 추정했다.

    원인
    구성 요소
    기여 요인
    감지

    2020-01-22 오전 10시 PST에 자동 알림을 받고 조사를 시작했다. 데이터베이스 지표의 변화는 9시 45분부터 보였다. 전날 사고도 오전 6시 11분 PST의 오류율 상승 자동 알림으로 알아챘다.

    조치
    • 데이터베이스 부하를 줄이려고 꼭 필요하지 않은 질의 몇 개를 취소하고 할당 IOPS를 늘렸다.
    • 가장 큰 테이블들에서 돌던 autovacuum 문을 취소했다. 지표가 잠시 나아졌지만 autovacuum이 다시 시작됐다.
    • 공격적인 autovacuum을 완전히 막으려고 autovacuum_freeze_max_age 파라미터를 바꾸고 데이터베이스를 재부팅했다.
    • 오후 7시 PST에 서비스를 내려 긴급 유지보수를 하고, 스테이징에서 미리 시험한 PostgreSQL 11로 데이터베이스를 올렸다. 오후 8시 15분 PST에 사이트를 다시 열었다.
    후속 조치
    • 장시간 도는 비싼 질의가 끊기도록 트랜잭션 타임아웃과 한도를 다시 정한다.
    • autovacuum, 락, 질의 성능과 관련된 지표, 모니터링, 데이터베이스 설정을 개선한다.
    • 쿼리 플래너를 헷갈리게 할 수 있는 불필요하거나 지나친 조인과 하위 질의가 있는지 질의를 점검한다.
    • 업그레이드한 PostgreSQL에서 쓸 수 있는 새 성능 분석 도구를 장애 대응 계획에 넣어 문제 질의를 더 빨리 가려낸다.
    • PostgreSQL에 들어 있는 데이터 종류별 구조와 접근 방식을 점검하고, 일부 데이터는 접근 방식에 맞는 저장소로 옮기는 것을 검토한다.
    • 예정에 없던 유지보수 시간을 앱 안에서 알리는 방식을 개선한다.
    • 앞으로 중단 시간이 필요 없는 온라인 데이터베이스 업그레이드로 옮겨 간다.
    • 오프라인이 된 뒤 다시 연결되기 전에 브라우저를 닫아도, 이미 열어 둔 파일에서 계속 안전하게 작업할 수 있게 하는 일을 시작한다.
    교훈

    스테이징 데이터베이스를 미리 PostgreSQL 11로 올려 몇 달 시험하고 운영 업그레이드 계획을 써 둔 덕분에 좋지 않은 상황에서도 업그레이드를 결정할 수 있었다. autovacuum을 막은 뒤에도 성능 문제가 남았으므로 autovacuum은 기여 요인이었을 뿐 유일한 원인이 아니었다. 같은 질의의 행 추정이 시점마다 크게 달랐다는 것은 통계 문제의 신호였다.

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

이 4건에서 가장 많이 나오는 구성 요소: 데이터베이스, PostgreSQL vacuum. kubegap 은 Claude Code 나 Codex 에서 쿠버네티스 클러스터를 읽기만 하며 이런 틈을 점검합니다. 점검 항목의 근거는 이 라이브러리와 같은 공개 보고서입니다.

원인별3