[IT 심층 분석]
gRPC 양방향 스트리밍에서 백프레셔(Backpressure)를 무시한 설계가 부른 메모리 폭발 사고
김민준 · IT 시스템 엔지니어|
마이크로서비스 간 실시간 데이터 스트리밍을 위해 gRPC 양방향 스트리밍을 도입한 직후 데이터 생성량이 많은 피크 시간대에 컨슈머 서비스가 자꾸 OOM으로 종료되는 사건이 연속으로 발생했습니다. 쿠버네티스 로그에는 더없이 단순한 메시지가 찍혀있었습니다. killed process - out of memory. 그러나 컨슈머 서비스가 처리하는 데이터 양 자체가 갑자기 늘어난 것도 아니었고 코드에 명백한 메모리 누수도 없었습니다. 이 문제를 쫓아가다 보니 결국 모든 분산 스트리밍 시스템에서 반드시 고려해야 하는 백프레셔(Backpressure)라는 개념과 정면으로 마주하게 되었습니다.
백프레셔는 데이터를 소비하는 쪽이 처리할 수 있는 속도보다 생성하는 쪽의 속도가 더 빠를 때 소비자가 생산자에게 속도를 늦춰달라고 신호를 보내는 메커니즘입니다. gRPC 스트리밍 구조에서 서버(프로듀서)는 클라이언트(컨슈머)가 준비되어 있건 없건 상관없이 stream.Send() 를 자신의 속도로 마구 호출할 수 있습니다. gRPC 채널은 전송 버퍼를 가지고 있어서 수신 측이 처리하지 못한 메시지들이 이 버퍼에 무한정 쌓이게 됩니다. 컨슈머가 CPU 집약적인 후처리를 하느라 조금만 느려져도 버퍼에는 수백만 개의 메시지가 쌓이고 이것들이 힙 메모리를 포식하면서 결국 OOM으로 건물이 무너지는 구조였습니다.
해결은 명시적인 흐름 제어를 코드로 직접 구현하는 것이었습니다. 컨슈머 측에서 메시지를 처리하는 속도에 맞추어 프로듀서에게 처리 완료 확인(Acknowledgment) 메시지를 역방향 스트림으로 보내도록 프로토콜 설계를 변경했습니다. 프로듀서는 컨슈머로부터 ACK를 받을 때마다 다음 배치를 전송하는 크레딧 기반 흐름 제어 로직을 구현했습니다. 추가로 gRPC 채널의 max send message size와 Flow control window 크기를 명시적으로 설정하여 무한 버퍼링 자체를 제한했습니다. 이 변경 이후 컨슈머 서비스의 메모리 사용량은 피크타임에도 안정적인 상수에 가까운 수준을 유지하며 단 한 번의 OOM도 발생하지 않고 있습니다.