1. Uber 의 Push Platform 소개

Uber 의 App 은 여러가지가 있는데 여기서 소개하는 Push 앱은 카카오 T 와 유사한 동작을 한다.

 

택시 탑승을 원하는 탑승자 (Rider) 가 요청을 날리면 택시 드라이버 (Driver) 가 이 제안을 받아들이는 앱이다.

 

여기서 Push Platform 은 택시 드라이버 (Driver) 가 탑승 제안을 수락했을 때 탑승자에게 알림이 가는 것과 같은 기능을 제공해준다.

 

2. High Level Architecture from Push Platform

Uber 의 Push Platform 아키텍처는 이런식으로 구성되어 있다:

  • 각 Platform 의 구성요소는 역할을 기반으로 나눠져있다:
    • Fireball Service: Push 를 언제해야 하는지를 담당한다.
    • API Gateway: 필요한 API 들을 호출해서 Push Message 를 생성하는 역할을 담당한다.
    • RAMEN (Realtime Asynchronous MEssaging Network): Push 메시지를 사용자에게 전달하는 역할을 담당한다.

 

 

API Gateway 에서 특이했던 점은 데이터를 가지고 오기 위한 API 호출의 엔드포인트와 Push 메시지를 보내기 위한 엔드포인트만 다를 뿐 내부적인 비즈니스 로직은 동일하다는 것.

 

API Gatway 를 사용한 목적 자체는 관심사의 분리의 이점을 누리기 위해서 사용했음 (e.g rate limiting, routing, and schema validations of push messages)

 

 

3. Push Message 에 대한 Metadata

Push Message 를 효율적으로 전달하기 위해서 사용하고 있는 여러가지 Metadata 들이 있는데 관심있어서 정리해보았음. 아마 Push Platform 도메인에 대한 이해를 하는데 도움이 될 수도 있을 것 같다:

  • 우선순위 (Priority):
    • Push 메시지를 우선적으로 전달하기 위해서 또 중요한 메시지는 전달을 보장하기 위해서 사용한다.
    • Uber 의 우선순위는 세 종류로 나눠져있고 Bucket 마다 나눠서 저장된다. (e.g High, Medium, Low)
  • Time to live:
    • Push 메시지는 실시간성이 중요한 메시지라서 오래된 메시지는 보낼 필요가 없음.
    • 기본적으로 Push 메시지에 대한 TTL 은 30분이라서 30분동안 보내지 못한 메시지는 보내지지 않음.
  • Deduplication:
    • Push 메시지는 여러건을 중복으로 보내지 않게 중복 제거를 관리하기도 한다고 함.
    • 메시지에 대한 중복은 네트워크 기반으로 통신을 할 때 재전송 매커니즘 때문에 생길 수 밖에 없긴 함.

 

4. Push Message Delivery Protocol 변천사

4.1 Polling 방식의 문제점

Uber App 초기에 실시간 메시지 전달을 위한 별도의 플랫폼이 없었기 때문에 메시지를 가져오기 위해서 Polling 방식이 사용되었습니다.  

 

일반적으로, 메시지를 Pull하는 Polling 방식은 연결을 유지하며 메시지를 Push하는 방식(e.g WebSocket, SSE)보다 처리할 수 있는 양이 적습니다. 

 

예시로, 러시아의 Mail.Ru 는 Polling 방식을 사용할 당시에는 초당 최대 50,000개의 요청을 처리할 수 있었으나, WebSocket으로 전환 후에는 3,000,000개의 커넥션을 관리하며 효율적으로 메시지를 전달할 수 있었다고 합니다.

 

또한, Polling 방식은 네이티브 앱에서도 부정적인 영향을 미칩니다. 앱의 배터리 소모 증가, 응답 지연, 다양한 API 조회로 인한 앱 시작 속도 저하 등의 문제가 발생할 수 있습니다.

 

 

4.2. Polling 에서의 SSE 로의 전환

Uber 에서는 SSE 를 도입한 주요 이유로 보안, 모바일 SDK 지원, HTTP 사용의 용이성 등을 꼽았습니다. 

 

SSE 를 사용하면서, Uber 에서는 메시지 전달의 신뢰성을 ‘적어도 한번 (at-least once)’ 을 보장하고자 했습니다. 

 

불확실한 네트워크 환경에서 메시지 전달을 보장하기 위해서는 메시지 수신에 대한 응답을 받아야만 합니다. 

 

이를 Uber 에서는 acknowledgment mechanism 이라는 표현을 사용했는데 다음과 같은 정책으로 구현했습니다. 

  • 메시지를 전달할 때 Sequence Number 를 부여해서 전달합니다.
  • 서버와의 연결이 끊겼다가 다시 연결될 때 마지막으로 받은 Sequence Number 를 서버에 전달하여, 그 이후의 메시지를 다시 받습니다.
  • 30초마다 서버에 받은 메시지에 대한 확인 응답(Ack)을 전송하여 수신한 메시지들을 알려줍니다. 이 응답으로 인해서 이제 불필요한 메시지는 수신되지 않습니다. 

 

또한, Uber 에서는 실시간 메시지 전달을 위해 연결 상태를 지속적으로 확인합니다:

  • 서버는 4초마다 상태 확인 메시지를 전송하고, 클라이언트는 7초마다 이 메시지를 받았는지 확인합니다. 만약 메시지를 받지 못했다면 재연결을 시도합니다.

 

 

4.3. SSE의 한계와 gRPC 의 필요성

Uber 는 SSE 를 이렇게 잘 쓰고 있다가도 다음의 이유로 gRPC 를 적극적으로 검토했습니다: 

  • 양방향 연결의 필요성:
    • SSE는 단방향 연결이기 때문에, 30초마다 보내는 확인 응답(Acknowledgment) 메시지가 유실될 경우, 이미 전송된 메시지를 다시 보내야 하는 문제가 발생했습니다.
    • Uber는 메시지의 우선순위에 따라 분류하고, 중요한 메시지의 즉각적인 전달 여부를 확인하는 필요성이 있었습니다. 이를 위해 양방향 연결이 필수적이라고 판단했습니다.
  • Binary Encoding 방식의 필요성: 
    • 전 세계적으로 서비스를 제공하는 Uber는 3G와 같은 네트워크 환경에서도 효율적으로 서비스를 제공해야 합니다. 이를 위해 데이터 전송을 최대한 Compact하게 유지할 필요가 있습니다.
    • SSE는 Text based Encoding 방식을 사용하는 반면, Binary Encoding 은 데이터 크기를 크게 줄일 수 있습니다. 예를 들어, 일반적으로 간단한 JSON 데이터를 Protocol Buffer 와 같은 Binary Encoding 을 사용하면 데이터 크기를 절반 이상 줄일 수 있습니다.
  • Stream 전송의 필요성: 
    • SSE 에서는 큰 메시지 전송 후에 Health Check 메시지를 보내는 경우, head-of-line blocking 문제로 인해 Health Check 메시지가 지연되어 연결이 끊길 수 있습니다.
    • HTTP/2 와 같은 스트림 형식으로 메시지를 전달할 경우, Health Check 메시지와 같은 작은 데이터는 더 빠르게 수신할 수 있습니다.
  • QUIC/HTTP 3와 같은 고급 기능의 필요성:
    • Uber 는 QUIC/HTTP3 의 streams, multiplexing, heartbeat mechanism, binary encoding, flow control 같은 고급 기능들이 필요하다고 판단했습니다

 

4.4. Uber의 gRPC 적용 과정: 고려 사항 및 구현 전략

Payload Compression:

  • 3G 환경에서 데이터 전송 시간을 최소화하기 위해 gzip을 사용하여 메시지 압축을 적용했습니다. 실제로 적용 결과 1MB 데이터의 다운로드 시간이 20-50초에서 gzip 적용 후 5초로 단축되었습니다.
  • (사실 gzip 을 왜 고려헀는지는 잘 모르겠긴 합니다. 압축 알고리즘의 선택은 압축율, 압축 속도, 압축 해제 속도, 사용하는 CPU 리소스를 고려해야하는데 gzip 은 zstandard 의 완전 하위호환 이라서..) 

 

Fallback Mechanism: 

  • gRPC 기반 메시지 전송에 문제가 발생할 경우를 대비해 기존 SSE 기반 메시지 전송을 대체 방법으로 사용했습니다.

 

Handling Missing Callbacks:

  • 클라이언트가 연결을 종료할 때, 적절한 종료 콜백이 수신되지 않아 Health Check 메시지를 불필요하게 전송하는 문제가 발생했습니다. 이를 해결하기 위해 메시지 전송 전 커넥션 검증 로직을 추가했습니다.

 

Message Flow Control:

  • streamObserver 대신 ServerCallStreamObserver 를 사용하여, 클라이언트와 서버 간의 연결이 성공적으로 이루어진 후에만 메시지를 수신하도록 설정했습니다. 이 방법은 서버가 준비되지 않은 상태에서 메시지를 받아 처리하지 못하는 문제를 방지했습니다.

 

Graceful Shutdown Handling: 

  • 종료되어야 할 streamObservers 를 쉽게 추적할 수 있도록 ShutdownHandler 의 Wrapper 를 별도로 개발했다고 합니다. 

 

References: 

+ Recent posts