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 이 선언됐다.
조치
- 잃은 실시간 용량을 다시 띄우고 장애가 난 노드를 고쳤다.
- 서비스 복구를 돕기 위해 Discord 로 들어오는 접속을 조절하기 시작했다.
- 모두가 다시 접속하는 동안 자원이 바닥나지 않도록 ScyllaDB 클러스터 두 곳의 데이터베이스 유지 작업인 anti-entropy repair 를 멈췄다.
- 서비스가 회복되는 대로 접속 허용량을 계속 늘려 더 많은 사용자가 다시 접속하게 했다.
후속 조치
- 값이 잘못된 키가 쓰일 가능성을 줄이려고, etcd 클라이언트가 자신을 등록하는 요청을 TCP 패킷 두 개가 아니라 하나로 보내게 바꿨다.
- 키 값이 깨진 서비스는 무시하도록 오류 처리를 더해, 같은 일이 다시 나도 그 서비스가 발견되지 않을 뿐 크래시 루프에 빠지지 않게 했다.
- Go 프로젝트에 이 문제를 버그로 보고한다.
교훈
서비스 디스커버리 시스템은 etcd 안의 여러 장애에는 견디도록 만들었지만, 일시적인 네트워크 오류로 값 없는 키가 쓰일 수 있다는 것은 예상하지 못했다. 이 경우 Go 의 net/http 는 HTTP 1.1 규격을 어겨, Content-Length 헤더가 있고 본문이 불완전한 요청을 거부하지 않는다. 이 때문에 예상하지 못한 장애 상황이 하나 더 생겼다.
출처
- https://discordstatus.com/incidents/62gt9cgjwdgf · official-status · 보관본
참고 문서
관련 사례
같은 원인 · 공통 코디네이션 키-값 저장소, 서비스 디스커버리
같은 원인 · 공통 코디네이션 키-값 저장소, 서비스 디스커버리
같은 원인 · 공통 서비스 디스커버리
IL-0277Elastic Cloud us-east-1 의 ZooKeeper 조정 계층이 쿼럼을 잃어 Elasticsearch 배포에 6시간 넘게 접속하지 못하고 Kibana 는 18:44 UTC 까지 불안정했다Elastic · 2019같은 원인 · 공통 코디네이션 키-값 저장소
같은 원인 · 공통 코디네이션 키-값 저장소
내 클러스터에도 있는지 확인하기
AI 도구와 스크립트용
- 이 사례의 JSON: https://incidentlib-api.benchlet.com/cases/IL-0271
- 가장 비슷한 사례들: https://incidentlib-api.benchlet.com/cases/IL-0271/similar
- MCP 서버(Streamable HTTP, 키 없음):
https://incidentlib-mcp.benchlet.com/mcp
API 는 키가 필요 없습니다. MCP 서버는 search_incidents, get_incident, list_incidents, get_topic, vocab 도구를 내놓습니다. 사용법은 API 와 MCP.