Distributed System/Grpc

gRPC Flow Control & Backpressure

여백의 기록 2025. 12. 26. 17:15
반응형

gRPC Flow Control & Backpressure

이 글은 gRPC 공식 문서와 HTTP/2 전송 방식을 기준으로, Streaming RPC에서 응답이 에러 없이 지연되거나 멈춘 것처럼 보이는 현상이 왜 발생하는지와, 실무에서 이를 어떻게 설계로 해결하는지에 대해 정리한 글입니다.

기준 문서: https://grpc.io/docs/guides/concepts/ , https://grpc.io/docs/guides/performance/

📌 카테고리: Dev > gRPC


1. Streaming RPC에서 자주 겪는 현상

  • 에러는 발생하지 않는다
  • 연결은 유지되고 있다
  • 응답이 일정 시점부터 오지 않는다

이 현상은 네트워크 오류나 서버 장애가 아니라, gRPC와 HTTP/2의 흐름 제어 메커니즘에 의해 발생하는 경우가 많다.

2. gRPC는 HTTP/2 위에서 동작한다

gRPC는 전송 계층으로 HTTP/2를 사용한다. 따라서 gRPC Streaming 동작은 HTTP/2의 제약을 그대로 따른다.

  • 하나의 Connection 위에 여러 Stream
  • Stream 단위의 독립적인 흐름
  • Connection 전체에 대한 전송 제한

3. Flow Control의 역할

Flow Control은 송신자가 수신자의 처리 속도를 초과하지 않도록 전송량을 제한하는 메커니즘이다.

HTTP/2에는 다음 두 단계의 전송 윈도우가 존재한다.

  • Connection-level window
  • Stream-level window

둘 중 하나라도 여유 공간이 없으면, 데이터 전송은 자연스럽게 중단된다.

4. 왜 에러 없이 멈춘 것처럼 보일까?

Flow Control에 의해 전송이 중단되는 것은 프로토콜 오류가 아니다.

  • Status Code를 반환하지 않는다
  • 연결을 끊지 않는다
  • 윈도우가 회복되면 전송을 재개한다

애플리케이션 관점에서는 아무 일도 일어나지 않는 것처럼 보이기 때문에 “멈춘 것 같다”는 인상을 받게 된다.

5. Backpressure는 어디서 발생하는가?

Backpressure는 항상 수신자 측 상태에 의해 발생한다.

  • 클라이언트가 메시지를 소비하지 못함
  • 서버가 메시지를 처리하지 못함
  • 내부 버퍼가 가득 참

이 상태가 되면, 송신자는 더 이상 데이터를 보낼 수 없다.

6. 문제가 되는 Streaming 구현 예시

override fun streamData(
  request: Request,
  responseObserver: StreamObserver<Response>
) {
  while (true) {
    responseObserver.onNext(fetchNext())
  }
}

이 구현은 수신자의 처리 속도나 버퍼 상태를 고려하지 않는다. 결과적으로 Flow Control에 의해 전송이 중단되고 호출이 멈춘 것처럼 보이게 된다.

7. 실무에서 가장 많이 쓰이는 해결 방식: 배치 스트리밍

서버 스트리밍을 사용하더라도, 한 요청에서 전송할 수 있는 최대량을 애플리케이션 레벨에서 명시적으로 제한하는 방식이 실무에서 가장 안전하다.

요청 메시지 정의

message ListUsersRequest {
  string cursor = 1;
  int32 limit = 2; // 한 요청에서 받을 최대 개수
}

rpc ListUsers(ListUsersRequest) returns (stream User);

서버 구현 예시

override fun listUsers(
  request: ListUsersRequest,
  responseObserver: StreamObserver<User>
) {
  val limit = if (request.limit > 0) request.limit else 500
  val users = userRepository.findUsers(request.cursor, limit)

  for (user in users) {
    responseObserver.onNext(user.toGrpc())
  }
  responseObserver.onCompleted()
}

이 방식의 특징은 다음과 같다.

  • 한 요청에서 전송량 상한이 명확하다
  • 무한 스트리밍을 하지 않는다
  • Flow Control에 과도하게 의존하지 않는다
  • Retry, Deadline, Observability와 잘 맞는다

8. Unary RPC와 Streaming RPC의 차이

  • Unary RPC는 요청 단위로 종료된다
  • Streaming RPC는 연결 단위로 유지된다
  • Streaming은 리소스를 장시간 점유한다

따라서 Streaming RPC는 반드시 필요한 경우에만 사용하는 것이 바람직하다.

9. 관측 지표로 판단하는 방법

Flow Control 문제는 로그만으로는 파악하기 어렵다.

  • Streaming RPC의 평균/최대 지속 시간
  • In-flight Streaming RPC 수
  • 메시지 처리 속도 변화

에러율은 낮은데 처리량만 감소한다면, Flow Control 또는 Backpressure를 의심할 수 있다.

10. 실무 체크리스트

  • Streaming RPC가 꼭 필요한 경우인가?
  • 한 요청에서 전송량 상한이 정의되어 있는가?
  • 무제한 전송 로직이 존재하지 않는가?
  • Flow Control 관련 지표를 관측하고 있는가?

반응형