Incident Library

IL-0214 PagerDuty · 2025-08-28database

Kafka 브로커들이 JVM 힙을 다 써 클러스터 전체로 장애가 번지며 PagerDuty 미국 리전의 이벤트 수집과 알림이 멈추거나 늦어졌다

PagerDuty 의 비동기 구조는 Kafka 를 중심에 둔다. Kafka 브로커는 활성 프로듀서마다 트랜잭션과 메시지 순번 같은 메타데이터를 추적한다. 고객에게 API 와 키 사용 내역을 보여 주려는 새 기능은 하루 수억 건인 API 사용량만큼의 메시지를 Kafka 토픽에 쓰는데, 논리 오류 때문에 프로듀서 하나를 다시 쓰지 않고 요청마다 새 프로듀서를 만들었다. pekko-connectors-kafka Scala 라이브러리에 미리 만든 프로듀서 대신 Kafka 설정을 넘기면 send() 를 부를 때마다 새 프로듀서가 생겼고, 명시적인 new 가 없어 코드에서 눈에 띄지 않았다. 정점에서는 평소의 84배인 시간당 약 420만 개의 프로듀서를 추가로 추적했고, 이 속도에서는 transaction.max.timeout.ms 와 producer.id.expiration.ms 를 조정해도 막을 수 없었다.

2025년 8월 28일 03:53 UTC 에 브로커 하나에서 JVM 힙이 바닥났고 문제는 클러스터 전체로 번졌다. 대응 팀은 하드웨어를 의심해 클러스터를 늘리고 그 브로커를 뺐다가, 다른 브로커도 메모리가 바닥나자 소프트웨어 문제임을 알았다. 힙 크기를 두 배로 늘려 클러스터 전체를 롤링 재시작하자 Kafka 가 안정됐고, 그 뒤로는 강하게 엮인 하위 서비스들을 되살리는 데 시간을 썼다. 몇몇 애플리케이션은 손으로 복구해야 했고, ksqlDB 스트림 관리 런북에 확인 단계가 없어 생긴 데이터 손실은 최근에 시험하지 않은 스크립트로 백필했다. 공개 상태 페이지 자동 게시가 실패해 백업 절차로 손으로 공지를 올렸다. 모든 서비스는 10:10 UTC 에 정상으로 돌아왔다. 처음에는 Kafka 를 안정시키는 데 집중해 촉발 원인이 보이지 않았으므로 기능을 되돌리지 않았다.

새 기능을 맡은 팀은 자기 변경이 장애와 관련 있다는 것을 몰랐고, 시스템이 안정되자 롤아웃을 25% 에서 75% 로 올렸다. 트래픽이 늘어 힙이 훨씬 빨리 찼고, 16:38 UTC 에 두 번째 장애가 시작됐다. 당번 대응자는 힙을 다시 두 배로 늘려 롤링 재시작했고 고객 영향은 50분으로 줄었다. 대응 팀은 기능 롤아웃과 힙 증가의 강한 상관관계를 찾아 Kafka 메시지를 더는 보내지 않게 하는 풀 리퀘스트로 기능을 되돌렸고, 20:24 UTC 에 모든 서비스가 복구됐다. Transactional Outbox 패턴 덕분에 이미 받은 이벤트나 데이터는 잃지 않았고, Kafka 가 돌아온 뒤 쌓인 메시지가 발행되면서 알림이 늦게 갔다.

심각도
–
영향 범위
Event ingestion and asynchronous processing: up to about 95% of API events rejected with 502 at the peak for 38 minutes, 18.87% of create requests and 4.35% of update requests failed with 5xx, notifications, webhooks, integrations, chat messages and the REST API delayed, duplicated or failing; no accepted events lost
영향 사용자
some customers in the US Service Regions; fewer than 10% of customers saw errors in non-event incident creation and 6.06% of users could not acknowledge or resolve incidents through the mobile app
지역
US Service Regions
감지까지
–
복구까지
6h 17m

증상

2025년 8월 28일 03:53 UTC 에 PagerDuty 의 Kafka 에서 장애가 시작됐다. 처음 온 알림은 Kafka 브로커 하나의 고장을 가리켰지만, 곧 다른 브로커들도 메모리가 바닥났다. 미국 서비스 리전의 일부 고객은 새 이벤트가 처리되지 않거나 늦게 처리됐다.

브로커의 JVM 힙 사용량이 천천히 꾸준하게 올라 가비지 컬렉션이 쉬지 않고 도는 GC thrash 가 생겼고, 결국 힙이 바닥났다. 브로커 하나에서 시작한 문제가 클러스터 전체로 번졌고, Kafka 에 기대는 서비스들은 끝내 Kafka 와 전혀 통신하지 못해 하위 서비스에서 오류가 줄줄이 났다. 힙이 찬 것은 브로커가 정점에서 시간당 약 420만 개의 프로듀서를 추가로 추적했기 때문이었고, 이는 평소 새 프로듀서 수의 84배였다.

영향이 가장 클 때 이벤트 API 는 들어오는 이벤트를 502 응답 코드로 거부했고, 정점에서는 약 95% 를 38분 동안 거부했다. 생성 요청의 18.87% 와 갱신 요청의 4.35% 가 5xx 응답을 받았고, REST API 오류율이 올랐다. 알림의 약 23% 가 5분 넘게 늦었다. 웹훅은 늦거나 빠지거나 두 번 갔고, iOS 와 Android 앱은 인시던트를 볼 때 400 오류를 더 많이 받았다. Jira, ServiceNow, Salesforce, Zendesk 연동 이벤트가 늦거나 빠졌고, 채팅 메시지가 두 번 가거나 늦게 처리됐다. 이미 받은 이벤트는 잃지 않았다. 보낼 메시지를 먼저 데이터베이스에 적어 두는 Transactional Outbox 에 남은 메시지가 Kafka 가 돌아온 뒤 발행되면서 처리가 늦어졌다.

대응 팀은 처음에 브로커 하나의 하드웨어 문제를 의심해 클러스터를 늘리고 그 브로커를 뺐다. 다른 브로커들도 메모리가 바닥나고서야 하드웨어가 아니라 시스템 전체의 소프트웨어 문제라는 것을 알았다.

장애 중 high-urgency 호출 19건 가운데 18건이 웹훅 관련 알림이어서 대응 팀은 핵심 API 오류 신호를 놓쳤다. Kafka 의 프로듀서 추적과 JVM 힙 사용량이 잘 보이지 않아 원인을 찾기 어려웠다.

공개 상태 페이지의 자동 게시가 실행되지 않아 내부에서 쓴 공지가 처음 두 시간 동안 페이지에 나타나지 않았다.

16:38 UTC 에 Kafka 성능이 다시 떨어지며 알림이 울렸다. 기능 롤아웃을 25% 에서 75% 로 올린 뒤 약 두 시간 만에 늘려 둔 힙마저 바닥난 것이었다.

증상 메모리 압박오류율 상승처리량 저하

원인

촉발
API 키 사용 내역을 Kafka 토픽에 남기는 새 기능의 논리 오류로 API 요청마다 새 Kafka 프로듀서가 만들어졌다. 이 기능은 8월 21일 1% 에서 시작해 27일 5%, 같은 날 저녁 25%, 28일 75% 로 롤아웃됐다.

감지

03:53 UTC 에 장애가 시작되자 처음 온 알림은 Kafka 브로커 하나의 고장을 가리켰다. 두 번째 장애는 16:38 UTC 에 Kafka 성능이 다시 떨어지며 울린 알림으로 알았다. 핵심 API 오류를 알리는 중요한 알림은 웹훅 호출에 묻혔다.

조치

후속 조치

교훈

PagerDuty 는 연쇄 장애가 본래 예측하기 어렵고, 한 서비스의 작은 문제가 시스템 도표로는 보이지 않는 길로 다른 서비스에 번진다고 봤다. 관측성의 빈틈 때문에 대응자는 하위 시스템 여러 개가 망가질 때까지 처음 무너진 것을 보지 못했다. 사람은 의존 관계를 직선으로 생각하지만 운영 시스템은 그렇게 움직이지 않는다. 통화에 참여한 누구도 시스템 전체를 머릿속에 담고 있지 않아 의존 관계를 잇는 데 시간이 걸렸다.

새 기능은 안전하게 내려고 별도 배포 대신 설정으로 조금씩 켰는데, 이 때문에 장애와 변경을 바로 잇기 어려웠다. 이 기능은 잘 쓰지 않는 프레임워크와 라이브러리로 만들어서, 요청마다 프로듀서를 새로 만든다는 것이 코드에 드러나지 않았다. 여기에 Kafka 알림이 적었던 것이 겹쳐 점진적 롤아웃 절차가 있어도 장애를 막지 못했다. 중요한 알림이 우선순위 낮은 웹훅 알림에 묻혔고, 백업 절차로 상태 페이지에 공지하는 일처럼 일부 운영 절차는 더 자주 연습해야 했다. 미리 만들어 둔 백필 스크립트와, 처리를 일시 정지했다가 재개하는 런북은 완벽하지 않았지만 대응자에게 출발점을 줬다.

출처

관련 사례

–

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

kubegap 은 Claude Code 나 Codex 에서 쿠버네티스 클러스터를 읽기만 하며 이 사례와 같은 상태를 점검합니다. 이 사례의 구성 요소: 메시지 브로커. 점검 항목의 근거는 이 사례 같은 공개 보고서입니다.

AI 도구와 스크립트용

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