Incident Library

IL-0271 Discord · 2020-03-16databasegcp

서비스 디스커버리에 잘못된 항목이 들어가 Elixir 서비스들이 한꺼번에 죽으면서 Discord 가 대부분의 사용자에게 한 시간 동안 열리지 않았다

Discord 는 서비스 디스커버리 정보를 etcd 에 저장하고 etcd v2 API 를 쓴다. 서비스는 자기 클러스터 디렉터리에 TTL 60초짜리 키를 쓰고 하트비트로 다시 등록한다. 실시간 채팅 서비스는 사용자가 접속하거나 guild 를 처음 만들거나 데이터베이스에서 다시 불러올 때, 이 서비스 디스커버리로 데이터를 읽는 RPC 엔드포인트를 찾는다.

14:18 PDT 에 오토스케일러가 API 클러스터에 새 노드를 넣었다. 하루에 수백 번 일어나는 평범한 일이다. Discord 의 etcd 클라이언트는 Python 내장 HTTP 클라이언트로 PUT 요청과 Content-Length 헤더를 한 패킷에, 요청 본문을 다른 패킷에 보낸다. 이 노드가 자신을 등록할 때 첫 패킷을 보낸 뒤 연결이 끊겼다. etcd 는 Go 표준 라이브러리 net/http 의 Request.ParseForm() 으로 본문을 읽는데, 이 메서드는 본문 길이가 Content-Length 와 맞는지 확인하지 않는다. 그래서 키가 빈 문자열 값으로 쓰였다. 이 잘못된 키를 읽은 Elixir 노드 대부분에서 서비스 감시 프로세스가 JSON 파싱에 실패해 크래시 루프에 빠졌다. "max restart intensity" 가 발동해 노드 전체가 재시작되면서 실시간 시스템 용량이 한순간에 사라졌다. API 노드가 다시 등록하며 키를 고칠 때까지 이 상태가 이어졌다.

엔지니어들은 잃은 용량을 다시 띄우고, Discord 접속을 조절하고, ScyllaDB 클러스터 두 곳의 anti-entropy repair 를 멈췄다. 처음에는 etcd watcher 한도를 의심해 런타임 설정으로 올려 보려 했다. 서비스가 회복되는 대로 접속 허용량을 15:07 부터 15:26 까지 계속 늘렸고, 15:32 에 상태 페이지 사고를 해결했다. 원인을 다 이해한 뒤 23:00 에 재발을 막는 완화책을 운영에 배포했다. 개발 환경에서 같은 문제를 재현했다.

심각도
sev1
영향 범위
Discord real-time connections and chat; Discord was unavailable for most users for about an hour and the API returned increased errors
영향 사용자
most Discord users
지역
–
감지까지
3m
복구까지
1h 14m

증상

2020년 3월 16일 14:19 PDT 에 실시간 연결과 채팅 메시지를 처리하는 Elixir 서비스 대부분이 죽어 용량이 한순간에 사라졌고, 여기에 의존하는 다른 시스템으로 장애가 번졌다. 14:21 에 핵심 지표가 떨어졌다는 알림과 클러스터 사용률 이상 알림이 여럿 울렸고 SEV1 이 선언됐다. 대부분의 사용자가 Discord 에 접속하지 못했다.

장애 직전인 14:18 에 API 트래픽을 처리하는 노드 api-prd-main-m1ds 가 API 클러스터에 들어오며 etcd 에 자신을 등록하다가 "http.client.RemoteDisconnected: Remote end closed connection without response" 오류를 로그에 남겼다.

거의 동시에 Elixir 노드 대부분의 로그에, "discord_api" 서비스를 감시하는 프로세스가 etcd 키 값에 담긴 JSON 메타데이터를 파싱하다 죽었다는 기록이 남았다. 이 프로세스들은 예상하지 못한 역직렬화 오류로 크래시 루프에 빠졌다. 프로세스가 너무 자주 죽는다고 슈퍼바이저가 판단해 "max restart intensity" 가 발동했고, 노드 전체가 재시작됐다. 여러 클러스터에 걸쳐 API 노드를 감시하던 노드의 약 50% 에서 이 일이 동시에 일어났다.

14:55 에 엔지니어들은 서비스 디스커버리 모듈의 오류 급증이 장애와 시점이 거의 일치한다는 것을 찾았다. 처음에는 etcd 의 watcher 수 한도에 걸렸다고 보고, 런타임 설정으로 한도를 올려 봤다.

증상 서비스 중단오류율 상승

원인

촉발
2020년 3월 16일 14:18 PDT 에 Google Cloud 관리형 인스턴스 그룹 오토스케일러가 API 클러스터에 새 노드를 넣었고, 이 노드가 etcd 에 자신을 등록하는 도중 연결이 끊겨 값이 빈 키가 서비스 디스커버리에 쓰였다.

감지

14:21 PDT 에 핵심 지표가 떨어졌다는 알림과 클러스터 사용률 이상 알림이 여럿 울렸고, SEV1 이 선언됐다.

조치

후속 조치

교훈

서비스 디스커버리 시스템은 etcd 안의 여러 장애에는 견디도록 만들었지만, 일시적인 네트워크 오류로 값 없는 키가 쓰일 수 있다는 것은 예상하지 못했다. 이 경우 Go 의 net/http 는 HTTP 1.1 규격을 어겨, Content-Length 헤더가 있고 본문이 불완전한 요청을 거부하지 않는다. 이 때문에 예상하지 못한 장애 상황이 하나 더 생겼다.

출처

참고 문서

관련 사례

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

kubegap 은 Claude Code 나 Codex 에서 쿠버네티스 클러스터를 읽기만 하며 이 사례와 같은 상태를 점검합니다. 이 사례의 구성 요소: 코디네이션 키-값 저장소, 서비스 디스커버리. 점검 항목의 근거는 이 사례 같은 공개 보고서입니다.

AI 도구와 스크립트용

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