
컨테이너 통신용 Rust 라이브러리 ttrpc-rust에서 스트림의 마지막 데이터가 누락되는 문제의 원인과 수정 방식이 공개됐다. 코드 기여자 시브 보살레가 10월 3일 공개한 기술 분석과 containerd 공식 저장소의 수정 PR에 따르면, 수신 프레임을 병렬 처리하면서 종료 신호가 데이터를 앞지르는 경쟁 조건이 원인이었다.
ttrpc는 같은 호스트의 프로세스 사이에서 쓰는 경량 원격 프로시저 호출(RPC) 프로토콜이다. 컨테이너 런타임 구성요소 간 통신 등에 사용되며, ttrpc-rust는 이를 Rust로 구현한 라이브러리다. 보살레는 Rust 기반 컨테이너 런타임에서 로그가 간헐적으로 끝까지 나오지 않는 현상을 추적하다 이 문제를 발견했다고 설명했다.
문제는 비동기 클라이언트의 수신 경로에 있었다. 기존 구현은 들어오는 프레임마다 별도의 Tokio 작업을 생성했다. 같은 스트림의 작업도 실행 순서가 보장되지 않아, 마지막 데이터 프레임보다 종료 프레임이 먼저 처리될 수 있었다. 종료 처리가 스트림 식별자를 제거하면 뒤늦게 실행된 데이터 작업은 전달할 대상을 찾지 못해 내용을 누락시켰다.
프레임을 읽은 순서대로 직접 처리하면 이 경쟁 조건은 줄일 수 있다. 그러나 기존의 용량 제한 채널에 데이터를 보내는 과정에서 다른 문제가 생겼다. 한 스트림의 소비자가 데이터를 읽지 않아 채널이 가득 차면 연결 전체의 수신 처리가 멈추고, 같은 연결을 사용하는 다른 RPC까지 대기하게 되는 구조였다.
공식 수정 PR은 각 RPC에 순서를 보존하는 별도의 수신 큐를 두는 방식을 택했다. 연결의 수신기는 프레임을 도착 순서대로 큐에 넣고, RPC마다 배치된 디스패처 작업이 이를 기존 소비자 채널로 전달한다. 느린 소비자는 자신의 디스패처만 막을 수 있고, 공유 연결의 수신기는 다른 스트림을 계속 처리한다. 데이터와 종료 신호, 오류도 같은 순서 보존 경로를 따른다.
이 설계에는 메모리 관리상의 절충이 있다. 입력 큐에 상한을 두지 않으므로 소비자가 읽지 않는 스트림은 메모리 사용량이 늘어날 수 있다. PR 작성자는 ttrpc에 스트림별 수신량을 조절하는 장치가 없어, 큐를 강제로 제한하려면 연결 전체를 막거나 개별 스트림을 실패 처리해야 한다고 설명했다.
수정 PR에는 마지막 데이터와 종료 신호의 순서를 확인하는 2,000회 호출, 400개 동시 스트림의 데이터 순서 검사, 읽지 않는 500프레임 스트림과 별개 RPC를 같은 연결에서 처리하는 회귀 테스트가 포함됐다. 공개 API와 통신 형식은 유지한다는 것이 PR의 설명이다.
GitHub 공식 메타데이터에 따르면 PR #312는 9월 22일 병합됐다. 이번에 새로 공개된 것은 문제를 발견하고 수정한 과정을 설명하는 기술 분석이다. 컨테이너 통신처럼 여러 스트림이 하나의 연결을 공유하는 환경에서는 실행 순서뿐 아니라 느린 소비자가 다른 요청에 미치는 영향도 함께 다뤄야 한다는 점을 보여준다.
출처: 시브 보살레 기술 분석 ‘Data loss in ttrpc-rust’
https://notes.shvbsle.in/ttrpc-data-loss/
containerd 공식 GitHub 수정 PR #312
https://github.com/containerd/ttrpc-rust/pull/312
GitHub 공식 PR 메타데이터
https://api.github.com/repos/containerd/ttrpc-rust/pulls/312