hyper HTTP 라이브러리 버그, 6주 추적 끝에 4줄 코드로 해결!

by DD
2시간 전
조회수 0

Cloudflare Workers의 Images 바인딩에서 대용량 이미지 처리 시 응답 데이터 잘림 현상 발생

문제의 원인은 Rust의 hyper HTTP 라이브러리 내 타이밍 의존적인 경쟁 조건(Timing-Dependent Race Condition)으로 밝혀짐

6주간의 추적 끝에 커널 레벨 도구(strace)를 활용하여 미완료된 버퍼 플러시(Incomplete Buffer Flush) 문제를 발견하고 4줄 코드로 해결함

Images 바인딩의 데이터 흐름과 버그 발생 지점

Images 바인딩은 Workers 런타임과 Images 서비스 간의 통신을 위해 소켓 연결(Socket Connection)과 버퍼(Buffer)를 활용함. Images 서비스는 인코딩된 이미지 데이터를 hyper 라이브러리로 전달하고, hyper는 이를 자체 버퍼에 저장 후 소켓으로 플러시함. 문제는 hyper가 데이터 플러시(Data Flush)가 완료되기 전에 연결 종료(Connection Shutdown)를 선언하면서 발생함. 특히, 이전 FL 중계 서비스 대비 새로운 내부 바인딩 서비스의 데이터 소비 속도(Data Consumption Rate)가 느려지면서 소켓 버퍼가 차는 상황이 빈번해졌고, 이로 인해 대용량 응답 시 데이터 잘림 현상이 두드러짐.

커널 레벨 디버깅(strace)을 통한 버그 규명

애플리케이션 레벨 도구로는 오류를 찾을 수 없었으나, 커널 시스템 콜(Kernel System Call)을 추적하는 strace를 통해 문제의 근원을 파악함. 성공적인 요청에서는 모든 데이터가 전송된 후 `shutdown`이 호출되었지만, 실패한 요청에서는 단일 `sendto` 호출 직후 `shutdown`이 실행되어 대량의 데이터가 hyper 내부 버퍼에 남은 채 연결이 종료됨. 이는 hyper의 `poll_flush` 결과(Poll::Pending)를 무시하고 `poll_loop`가 즉시 종료되는 Rust의 `let _` 구문과 맞물려 발생한 타이밍 문제였음. 이로써 버그가 애플리케이션 로직이 아닌 연결 계층(Connection Layer)의 경쟁 조건임을 확신하게 됨.

hyper 라이브러리의 `let _` 버그와 해결 방안

버그의 핵심은 hyper의 `dispatch.rs` 파일 내 `poll_loop` 함수에서 `poll_flush`의 반환 값(Poll::Pending)을 무시하는 `let _` 구문에 있었음. 이로 인해 플러시가 완료되지 않았음에도 불구하고 루프가 종료되고 연결이 조기 종료되는 문제가 발생함. 초기 해결책으로 `poll_flush` 결과를 확인하고 `Poll::Pending`일 경우 즉시 반환하도록 수정했으나, 이는 다른 연결 작업에 영향을 줄 수 있음. 최종적으로는 연결 종료 직전 `poll_shutdown` 함수 내에서 `poll_flush`를 먼저 호출하여 모든 데이터가 전송된 후에만 연결을 종료하도록 수정함. 이 변경으로 데이터 유실 없이 안정적인 응답 전송이 가능해짐.

경쟁 조건 재현을 위한 커스텀 TCP 스트림 테스트

실제 운영 환경과 유사한 소켓 버퍼 제한 조건을 만들기 위해 커스텀 TCP 스트림 래퍼를 개발함. 이 래퍼는 첫 번째 쓰기 작업에는 8KB를 수락하고 이후 모든 쓰기 작업에는 `Poll::Pending`을 반환하여, 버퍼가 채워진 후 더 이상 데이터를 처리하지 못하는 상황을 시뮬레이션함. 이 환경에서 500KB 응답을 전송했을 때, 수정 전 hyper는 492KB가 버퍼에 남은 상태에서 `shutdown`을 호출했지만, 수정 후에는 모든 데이터가 전송될 때까지 기다렸음. 이 제어된 테스트 환경은 버그의 재현성을 높이고 수정의 유효성을 검증하는 데 결정적인 역할을 함.

애플리케이션 레벨 관측 가능성의 한계

이번 버그는 `strace`와 같은 커널 레벨 도구(Kernel-Level Tooling) 없이는 발견하기 어려웠음. 기존의 분산 추적(Distributed Tracing), 중앙화 로깅(Centralized Logging), 메트릭(Metrics) 등 애플리케이션 레벨 관측 가능성(Observability) 도구들은 시스템이 정상이라고 판단했기 때문임. 이는 연결 계층(Connection Layer)이나 소켓 버퍼(Socket Buffer) 수준의 미묘한 타이밍 문제는 애플리케이션의 인지 범위를 벗어날 수 있음을 시사함. 따라서 복잡한 분산 시스템에서는 다양한 레벨의 관측 도구를 조합하여 잠재적인 문제를 탐지하는 것이 중요함.

How we found a bug in the hyper HTTP library