Cloudflare의 Images 서비스를 Unix 소켓 기반 아키텍처로 재구성한 후, 대용량 이미지 응답이 중간에 잘리는 버그가 발생했다. 14.8MB 응답에서 219KB만 전달되고 HTTP 200 OK는 정상 반환되어 애플리케이션 레벨에서 탐지가 불가능했다. 원인은 hyper 라이브러리의 dispatch 루프에서 flush 완료 여부를 확인하지 않고 연결을 종료하는 경쟁 조건이었으며, strace로 커널 호출 순서를 추적해 root cause를 특정했다. 최종 수정은 upstream PR #4018로 hyper 레포에 병합됐다.
핵심 포인트- 대용량 응답(14.8MB)이 219KB만 전송되고 나머지는 손실, HTTP 200 OK는 정상 반환되어 애플리케이션 레벨 로그/추적에서는 오류가 없었다.
- 원인은 hyper dispatch.rs의 poll_flush(cx) 결과를 무시하는 코드 — flush가 Pending이어도 연결을 종료해 내부 버퍼 데이터가 유실됐다.
- 소켓 버퍼가 가득 차는 상황(새 아키텍처에서 reader가 약간 느려짐)이 생기면서 기존에 숨어있던 경쟁 조건이 노출됐다.
- strace로 커널 수준 syscall 추적 — 실패 요청은 sendto 1회(219KB) 직후 shutdown, 정상 요청은 sendto 여러 번 후 shutdown임을 확인했다.
- 최종 수정: poll_shutdown에서 poll_flush를 먼저 완료한 후 소켓을 종료하도록 변경, upstream PR #4018로 병합됐다.
상세 정리- 발단: December 2025 아키텍처 변경 — FL 중간 서비스를 Unix 소켓 기반 내부 바인딩으로 교체했고, 새 reader가 이전보다 약간 느려졌다.
- 증상: 25개 테스트 요청 중 19개가 응답 잘림, 타임아웃이나 5xx 없이 200 OK 반환으로 애플리케이션 레벨 로그/추적에서 오류가 없었다.
- 초기 가설 — 타임아웃: 요청 기간과 무관하게 버그 재현, 기각됐다.
- 초기 가설 — hyper 버전 문제: 0.14, 1.7, 1.8 모두 동일 버그 확인, 버전 업그레이드로 해결 불가였다.
- 로컬 재현 불가: 로컬 환경에서는 reader가 충분히 빠르게 소켓을 비워 버그가 노출되지 않았다.
- strace 투입: 커널 수준 syscall을 추적해 실패 요청에서 sendto 1회(219KB) 직후 즉시 shutdown이 호출됨을 확인했다.
- 코드 분석: dispatch.rs의 let _ = self.poll_flush(cx)? 구문이 Poll::Pending 반환값을 _ 로 무시하고 있었다.
- flush가 Pending을 반환하면 내부 버퍼에 아직 데이터가 남아있는 상태인데, 이를 무시하고 연결을 종료해 나머지 데이터가 유실됐다.
- 소켓 버퍼가 가득 찬 상황(느린 reader)에서만 flush가 Pending을 반환하므로 로컬에서는 재현되지 않았다.
- 초기 수정: dispatch 루프에서 flush_result가 Pending이면 Poll::Pending을 반환하도록 변경했다.
- 최종 수정(upstream): poll_shutdown에서 먼저 poll_flush를 완료하는 방식으로 변경 — keepalive 연결 처리와의 상호작용을 고려한 더 적절한 구조였다.
- upstream PR #4018이 hyperium/hyper에 병합됐고, Cloudflare 내부 포크에 즉시 적용해 Images 서비스를 안정화했다.
왜 읽나Rust async I/O 라이브러리의 숨겨진 경쟁 조건을 strace로 추적한 실전 디버깅 케이스로, 대용량 스트리밍 응답이나 async Rust I/O를 다루는 백엔드 엔지니어에게 유용한 레퍼런스다.