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

사례2건

2건 중 1~2번째

  1. 증상

    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 를 만들고 있다고 밝혔다.

  2. 증상

    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 로 바로 옮기면 더 큰 문제가 생긴다고 보았다. 그래서 모놀리식 구조를 알맞은 선에서 유지하면서 일부 도메인만 분리하기로 했다.

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

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

원인별1