서비스 장애 회고 · 27
한 판의 오류가 연쇄 서비스 회귀를 드러내기까지
8월 30일, 지원하지 않는 덤이 든 한 판이 먼저 KataGo parameter 오류를 일으켰습니다. 같은 조사에서 비대해진 stream 완료 event, 가입 runtime 불일치, Token 반복 생성과 구형 client 동기화 실패도 발견했습니다. 시간선, 측정한 영향, 복구 증거와 남은 일을 공개합니다.
첫 가시적 오류는 20시 47분에 나타났다
2026년 8월 30일 20시 47분, Web 전체 대국 복기가 정수도 반집 단위도 아닌 덤을 Modal로 보냈습니다. KataGo는 이를 거부했고 job은 GPU 사용 시간 0밀리초로 실패했습니다. 화면은 query 식별자와 탐색량을 남겼지만 가져온 기보의 덤을 지원하지 않는다는 사실을 사전에 설명하지 못했습니다.
20시 47분의 잘못된 덤 요청을 시작점으로 두고 재시도와 두 번째 provider 장애를 별도 선으로 기록합니다. 실패 30회는 두 개의 재시도 연쇄였다
20시 47분부터 21시 13분까지 잘못된 덤 오류는 같은 user와 같은 한 판에서 5회, 응답 크기 초과는 다른 user에게서 25회 발생했습니다. 같은 시간대 Modal은 job 43건을 완료했습니다. 두 장애가 자동 또는 수동 재시도로 증폭된 것이며, 30명이 영향을 받았거나 분석 service 전체가 계속 중단된 것은 아닙니다.
user, 대국과 성공 job으로 영향을 다시 세어 30번의 시도를 30명의 피해로 표현하지 않습니다. 잘못된 덤은 provider 호출 전에 멈췄어야 했다
KataGo는 -400부터 400까지 정수 또는 반집 단위 덤만 받습니다. 이전 경로는 잘못된 값을 provider까지 보내 내부 오류를 노출했습니다. 이제 Web, REST와 streaming 입구가 하나의 validation 계약을 공유하고 과금이나 GPU dispatch 전에 명확한 HTTP 400을 반환합니다. 덤을 바꾸면 분석할 대국도 바뀌므로 몰래 반올림하지 않습니다.
validation을 과금과 GPU dispatch 앞으로 옮겨 지원하지 않는 값이 Worker 경계를 넘지 못하게 했습니다. 완료 event가 stream 전체를 다시 담고 있었다
두 번째 장애는 Modal stream에서 생겼습니다. 중간 event가 각 result를 이미 보낸 뒤 완료 event가 전체 result 배열을 다시 첨부했습니다. 긴 전체 대국 복기는 provider 응답 크기 제한을 넘었습니다. 이제 result는 계속 streaming되고 완료 event는 usage 같은 작은 요약만 담습니다. 배포된 production App은 64 visits 검증 query에서 result 1개와 완전한 usage를 반환했습니다.
result는 순차 도착을 유지하고 complete event만 중복 payload에서 작은 usage 영수증으로 줄였습니다. 같은 조사에서 독립된 호환성 회귀 세 개를 찾았다
가입 실패는 잘못된 덤 때문이 아니었습니다. password derivation 횟수가 Cloudflare Worker runtime 상한을 넘어 account row 생성 전에 멈췄습니다. client 동기화는 Edge가 지원하지 않는 redirect mode를 썼고 일부 구형 client는 안정적인 device slot이 없어 재연결 때마다 API Token을 만들었습니다. 직접 인과는 없지만 모두 server 업데이트와 구형 client 사이 release 계약의 부재를 드러냈습니다.
가입, redirect와 Token rotation은 한 review에서 발견한 별도 갈래이며 같은 root cause를 공유하지 않습니다. 복구는 주 경로와 fallback을 각각 검증했다
8월 31일 15시 54분 호환성 수정이 Cloudflare에 반영됐습니다. 16시 15분에는 REST fallback이 항상 RunPod stream을 읽고 terminal output이 없을 때 result를 재구성하도록 바뀌었고 이어 Modal Fast를 다시 배포했습니다. 16시 18분 NVIDIA L4의 production RPC가 result 1개와 70 visits를 반환했습니다. Worker release 뒤 새 가입 성공 row도 확인했습니다.
Cloudflare, Modal과 RunPod를 따로 검증했고 deploy receipt를 production 동작 증거로 대신하지 않았습니다. 다음 release에는 하나의 호환성 matrix가 필요하다
release gate는 Web, 신구 native client, REST, WSS, 가입, 자동 Token rotation, Modal stream과 RunPod fallback을 포함하고 긴 기보의 응답 크기도 시험합니다. 가입에는 database 쓰기 전 privacy-safe event를 더해 ‘account 기록 없음’과 ‘request 없음’을 구분합니다. 이번 release에는 새 frontend source map이 upload되지 않아 service에는 영향이 없지만 symbolication이 늦어지므로 release credential 점검도 후속 작업에 넣습니다.
client, protocol, provider, fallback과 observability를 하나의 release 전 matrix에 배치합니다.
적용된 수정
- 덤을 과금과 provider dispatch 전에 검증
- Modal 완료 event에서 전체 result set 중복 제거
- RunPod fallback이 streaming output을 항상 읽고 재구성
- 가입 parameter를 Cloudflare Worker runtime 제한에 맞춤
- 같은 device는 Token을 자동 rotation하고 상한을 5에서 20으로 조정
- Edge redirect와 구형 IGS byte stream 호환 유지
- WebGL context가 사라져도 빈 화면 대신 바둑판 표시