DNS 순간 장애가 WebSocket을 종일 죽인 이유: 자동 복구 경로가 없었다
새벽 5시 DNS 순간 장애로 KIS WebSocket이 재연결 5회를 소진하고 영구 차단됐다. DNS가 복구된 후에도 WebSocket은 살아나지 않았다. 자동 복구 경로가 설계에 없었기 때문이다.
새벽 5시 DNS 순간 장애로 KIS WebSocket이 재연결 5회를 소진하고 영구 차단됐다. DNS가 복구된 후에도 WebSocket은 살아나지 않았다. 자동 복구 경로가 설계에 없었기 때문이다.
Phase 124에서 DB 컬럼을 TIMESTAMP → TIMESTAMPTZ로 올렸는데, 대응하는 SQLModel 필드에 timezone=True를 빠뜨렸다. asyncpg가 매도 체결 저장을 거부하면서 9거래일간 체결이 한 건도 기록되지 않았다. /health는 계속 200이었다.
uvicorn을 네이티브로 띄웠다고 생각했는데 헬스체크가 200을 반환한다. 서버가 잘 떴다고 믿고 계속 개발했다. 알고 보니 OrbStack의 Docker 컨테이너가 같은 포트를 점유하고 있었고, 내가 수정한 코드는 실행 중이 아니었다.
로컬에서는 멀쩡히 돌아가는 alembic upgrade head가 GitHub Actions CI에서만 SIGSEGV로 죽는다. 12번의 시도 끝에 greenlet C 확장과 Python 3.12 계열 런타임 조합을 의심했고, subprocess 호출을 in-process Alembic API로 바꿔 우회했다.
morning_batch가 정상 완료됐는데 leader_groups는 항상 0개였다. KIS paper API rate limit이 비동기 일괄 요청 28개 중 22개를 조용히 거부하고 있었다. 에러는 있었다. 배치는 멈추지 않았다. 결과만 빈 채로 아침을 맞이했다.
코드 마이그레이션 후 특정 엔드포인트에서 간헐적 500 에러가 발생했다. 원인은 마이그레이션 중 일부 코드가 timezone-aware datetime을 naive로 변환해서 빼려고 했던 것. TypeError 한 줄이 모니터링을 통과한 이유까지 정리했다.
매도 주문 API가 성공 응답을 반환했다. 그런데 실제 체결이 되지 않았고, 시스템은 이미 체결로 기록했다. KIS API 응답과 실제 체결 사이의 간극, 그리고 이를 방어하는 KIS-truth 재검증 패턴을 정리했다.
KIS API 클라이언트에 connect timeout을 60초로 설정했더니, 연결이 느려지는 순간 요청이 수십 개 쌓였다. 타임아웃을 길게 잡을수록 안전하다는 착각이 만든 사고다.
KIS API는 개장 직후 09:00~09:05 KST에 연결 불안정 구간이 존재한다. 이 구간에서 2-상태 서킷 브레이커는 회복하지 못하고 silent block에 빠진다. CLOSED/OPEN/HALF_OPEN 3-상태 서킷 브레이커와 에스컬레이션 open_seconds_schedule로 프로브 기반 자동 회복을 구현한 과정을 정리했다.
FastAPI lifespan에서 Telegram 봇을 시작할 때 TLS 연결 지연으로 start()가 실패했다. 예외는 잡혔고, 백엔드는 멀쩡히 돌았다. 알림은 한 건도 오지 않았다. 5분 후 봇이 살아 돌아오는 것도 몰랐다.