Uber , Push , Architecture ,

Uber의 푸시 플랫폼 아키텍처

by Mimul FollowOctober 08, 2021 · 14 min read · Last Updated:
Share this

요즘 비즈니스에 푸시 기술은 필수 기능이라서 도움이 될 거 같아 Uber 실시간 푸시 플랫폼 기술에 대한 설명과 Uber가 어떻게 푸시 플랫폼을 진화시켰는지에 대해 기술되어 있는 Uber's Real-Time Push Platform라는 글을 번역해 보았습니다.

Uber는 매일 전세계에서 수백만 건의 여행 데이터를 처리를 통해 다방면의 마켓플레이스를 구축하고 있다. 우리는 모든 사용자를 위해 실시간 환경을 구축하기 위해 노력하고 있다.

실시간 마켓플레이스의 특성은 모든 사용자들에게 매우 활기차게 만든다. 여행중에서는 진행 중인 여행의 상태를 수정하고 볼 수 있으며 실시간 업데이트가 필요한 사용자들이 많다. 픽업 시간, 도착 시간, 화면 경로 등 모든 엑티브한 참가자들의 앱을 실시간으로 정보를 동기화가 필요하고 앱이 활성화가 되었을 경우에는 가까운 운전자 정보가 제공 되어야 한다.

사용자 화면에서 많은 중요한 기능의 증가와 더불어 사용자의 앱 화면에 분산된 방식으로 실시간 환경의 모바일 앱 기능을 구축해야하는 개발자들의 필요성이 성장을 위한 핵심 키포인트이다.

이번 포스트는 우리가 앱의 새로고침 기능을 폴링 방식에서 gRPC 기반의 양방향 스트리밍 방식으로 우리 앱에 어쩧게 구축해 갔는지에 대해 기술한다.

폴링 방식의 업데이트

Uber 트립은 라이더와 같은 참여 주체들과 실세계에서 이동하는 운전자들 사이의 조율이다. 이 두 엔티티(라이더와 운전자)는 백엔드 시스템과 여행이 진행되는 동안 서로 최신 상태를 유지해야 한다.

탑승자가 승차 요청을 했고 운전자가 서비스를 제공하기 위해 온라인 상태인 시나리오를 생각해 보자. 백엔드에 있는 Uber의 매칭 시스템은 일치하는 것을 식별하고 운전자에게 여행 정보를 제공한다. 이제 모든 사람(라이더, 드라이버, 백엔드)이 서로의 의도에 맞게 동기화되어야 한다.

드라이버(운전자) 앱은 몇 초마다 서버로 폴링하여 이용 가능한 새로운 오퍼(요청)가 있는지 확인한다. 라이더(탑승자) 앱은 몇 초마다 서버로 폴링하여 드라이버(운전자)가 할당되었는지 확인할 수 있다.

앱들의 폴링 빈도는 폴링하는 데이터의 변경률에 따라 달라진다. Uber 앱과 같은 대형 앱에서, 변경율에 대한 변화폭은 몇 초에서 몇 시간까지 매우 다양하다.

모바일 앱에서 폴링하는 문제

어느 시점에서는 백엔드 API 게이트웨이 요청의 80%가 폴링 호출이었다.

적극적인 폴링은 앱의 응답성을 유지하지만, 서버 리소스 이용률이 높아진다. 폴링 빈도에 버그가 있으면 백엔드 로드가 증가하고 성능이 크게 저하된다. 실시간 동적 데이터가 필요한 기능의 수가 증가함에 따라 백엔드에 상당한 부하가 계속 증가되기 때문에 이 접근 방식은 실현 불가능했다.

폴링은 더 빠른 배터리 소모, 앱에 느려지고, 네트워크의 혼잡으로 이어진다. 이는 특히 2G/3G 네트워크에서나 도시 전체에 스팟 네트워크가 있는 장소에서는 앱이 폴링을 여러번 시도를 한다.

기능의 수가 증가함에 따라 개발자들은 기존 폴링 API를 오버로드하거나 새로운 폴링 API를 만들려고 했다. 피크시에는 그 앱은 수십 개의 API를 폴링하고 있었다. 각 API는 여러 기능으로 오버로드 되어 있다. 이러한 폴링 API는 결국 앱이 기능을 폴링하기 위한 페이로드 공유 API의 집합이 되었다. API 수준에서 일관성 및 논리적 관심 분리를 유지하는 것은 난제가 되어갔다.

앱의 초기 구동시에 폴링 전략에서 가장 어려운 시나리오였다. 앱을 열 때마다 모든 기능이 백엔드에서 최신 상태를 끌어와 UI를 렌더링하기를 원했다. 이로 인해 여러 개의 API를 동시에 호출해 경쟁이 발생했으며 중요한 콤포넌트가 서버에서 도달될때까지 앱을 렌더링할 수 없었다. 우선 순위 지정 없이 모든 API에 중요한 정보가 일부 포함되어 있었기 때문에 앱 로드 시간이 계속 증가했다. 열악한 네트워크 조건은 초기 구동시간을 더욱 악화시킬 것이다.

마켓플레이스의 다양한 참여자들 사이에서 상태를 동기화하는 방법에 대한 완전한 혁신이 분명하게 필요했다. 우리는 서버가 온디맨드 방식으로 앱에 데이터를 전송할 수 있는 기능을 갖춘 푸시 메시징 플랫폼을 구축하기 위한 여정에 착수했다.

이 아키텍처를 채택함에 따라 효율성이 크게 향상 되었지만 다양한 문제 및 과제들을 해결해야 했다. 다음 섹션에서는 다양한 세대의 푸시 인프라와 이 플랫폼이 어떻게 발전했는지 설명하고자 한다.

폴링 방식 제거 및 RAMEN 도입

폴링을 없애기 위해 푸시 메시지를 사용하는 것은 당연한 선택이었지만, 이를 어떻게 설계할 것인지에 대한 고려가 많았다. 네가지 주요 설계 원리는 다음과 같다.

폴링에서 푸시로의 간단한 마이그레이션

비즈니스에 동력을 제공하는 많은 기존 폴링 엔드포인트가 있었다. 새로운 시스템은 기존의 폴링 API에서 페이로드 빌드 비즈니스 로직을 모두 다시 작성할 필요 없이 활용해야 했다.

개발 용이성

개발자는 폴링 API를 개발하려는 노력에 비례하여 데이터를 푸시하기 위해 크게 다른 노력을 해서는 안된다.

신뢰성

모든 메시지는 네트워크를 통해 안정적으로 전송되어야 하며 전달에 실패할 경우 다시 시도해야 한다.

전송 효율성

Uber가 개발도상국에서 빠르게 성장함에 따라 데이터 사용 비용은 우리 사용자들에게 어려운 문제였으며, 이는 특히 하루에 여러 시간 동안 플랫폼에 연결되어 있는 운전자들에게는 더욱 그렇다. 이 프로토콜은 서버와 모바일 앱 간의 데이터 전송량을 최소화해야 했다.

우리는 새로운 시스템을 RAMEN(실시간 비동기식 메시지 네트워크)라고 이름을 지었다.

Uber-Architecture 그림 : Figure 1: High-level architecture of the overall system.

메시지 생성 결정

언제든지 실시간 정보는 수백 명의 라이더, 운전자, 식당, 식사하는 사람, 여행에 걸쳐 변화하고 있다. 메시지의 라이프 사이클은 사용자에 대한 메시지 페이로드를 생성할 시기를 결정하는 것으로 시작된다.

Fireball은 "메시지를 언제 푸쉬할 것인가?"라는 문제를 해결하는 마이크로 서비스이다. 결정의 상당 부분은 구성 요소들을 파악하는 것이다. 시스템 전체에서 발생하는 다양한 유형의 이벤트를 파악하고 관련 사용자에게 푸시가 필요한지 여부를 판단한다.

예를 들어, 드라이버(운전자)가 제안을 수락하면 드라이버와 트립 엔티티 상태가 변경된다. 이 변경은 Fireball 서비스를 트리거 한다. 그런 다음, 구성에 따라 Fireball은 관련 마켓플레이스 참가자에게 어떤 유형의 푸시 메시지를 보내야 할지 결정한다. 종종 단일 트리거는 여러 사용자에게 여러 메시지 페이로드를 보내는 것을 보장되어야 한다.

트리거는 푸시 페이로드를 생성해야 하는 여러 종류의 중요한 이벤트들이다. 예를 들어, 승차 요청, 앱 열기, 일정한 간격으로 똑딱거리는 타이머, 메시지 버스의 백엔드 비즈니스 이벤트 또는 지리적 입/출력 이벤트와 같은 사용자 작업이 있다.

이러한 모든 트리거는 필터링되고 다양한 API 게이트웨이 엔드포인트에 대한 호출로 변환된다. API 게이트웨이는 적절하게 현지화된 응답 페이로드를 생성하기 위해 디바이스 위치, 디바이스 운영 체제 및 앱 버전과 같은 사용자 컨텍스트 정보가 필요로 한다. Fireball은 API 게이트웨이로 호출할 때 디바이스 컨텍스트를 RAMEN 서버로부터 가져와 헤더에 추가한다.

메시지 페이로드 생성

Uber 앱에서 발생하는 모든 서버 호출은 API 게이트웨이에 의해 처리된다(여기서 게이트웨이의 진화에 대해 좀 더 알아보라). 푸시 페이로드도 동일한 방식으로 생성된다.

API 게이트웨이는 Fireball이 메시지를 푸시할 대상과 시기를 결정하면 "푸시할 내용"을 결정하는 역할을 한다. 게이트웨이는 다양한 도메인 서비스를 호출하여 올바른 푸시 페이로드를 생성한다.

게이트웨이의 모든 API는 페이로드를 생성하는 방법이 매우 유사하다. 그러나 API는 Pull 및 Push API로 분류된다. Pull API는 HTTP 작업을 통해 모바일 디바이스에서 호출되는 엔드포인트이다. Push API는 Fireball에서 호출되는 엔드포인트이며 Pull API의 응답을 가로채서 푸시 메시지 전달 시스템으로 전달할 수 있는 추가의 푸시 미들웨어가 있다.

API 게이트웨이를 중간에 놓는 것은 장점이 있다.

  • Pull 및 Push API는 엔드포인트의 비즈니스 로직 대부분을 공유한다. 주어진 페이로드는 Pull API에서 Push API로 원활하게 전환될 수 있다. 예를 들어, 앱이 Pull API 호출을 통해 "사용자" 개체를 끌어오거나 Fireball이 Push API 호출을 통해 "사용자" 개체를 보내는 경우에도 동일한 로직이 사용된다.
  • 게이트웨이는 트래픽 제한, 라우팅 및 푸시 메시지의 스키마 유효성 검사와 같은 많은 공통의 문제를 처리한다.

Fireball과 게이트웨이는 적절한 시간에 사용자에게 전송될 푸시 메시지를 함께 생성한다. 이를 모바일 디바이스에 전달하는 것은 "푸시 메시지 전달 시스템"의 책임이다.

푸시 메시지 페이로드에 대한 메타데이터

각 푸시 메시지는 최적화를 위해 정의된 다양한 구성을 가진다.

우선 순위

다양한 사용 사례에 대해 수백 개의 서로 다른 페이로드가 생성되므로 먼저 앱으로 전송해야 할 항목의 우선 순위가 필요하다. 다음 섹션에서 볼 수 있듯이, 채택한 프로토콜은 단일 연결에서 여러개의 동시 페이로드를 보내는 것을 제한했다. 게다가, 수신 디바이스의 대역폭은 제한된다.

상대적 우선 순위의 느낌을 주기 위해, 메시지는 영향을 이해하는 세가지 다른 우선 순위 버킷으로 폭넓게 분류했다.

  • 높음 : 핵심 사용자 경험을 주는 메시지는 중요
  • 중간 : 점증적으로 사용자 경험에 중요한 기능 메시지
  • 낮음 : 높은 데이터 페이로드 크기, 중요하지 않고 작은 빈도를 가진 메시지

이 우선 순위 구성을 사용하여 플랫폼의 다양한 동작을 제어했다. 예를 들어, 연결이 되면 메시지는 우선순위가 높은 순서대로 소켓에 들어가게 된다. 높은 우선 순위 메시지는 RPC 실패 시 서버 측에서 재시도 메카니즘을 통해 메세지 도달 확률을 높이며, 지역 간 복제도 지원한다.

TTL(메세지 지속 시간)

푸시 메시지는 실시간 경험을 향상시키기 위한 것이다. 따라서 각 메시지에는 몇 초에서 최대 30분의 TTL 값이 정의되어 있다. 메시지 전달 시스템은 메시지 지속 시간 값이 만료될 때까지 메시지 전달을 다시 시도한다.

중복 제거

이 설정은 다양한 트리거나 재시도를 통해 동일한 메시지 유형이 여러번 생성되는 경우 푸시 메시지를 중복을 제거할지 여부를 결정한다. 대부분의 사용 사례에서, 주어진 유형의 가장 최근의 푸시 메시지를 보내는 것은 사용자 경험을 만족시키기에 충분했고 이것은 우리가 전반적인 데이터 전송 속도를 줄일 수 있게 해주었다.

메시지 전달

푸시 메시지 시스템의 마지막 구성 요소는 실제 페이로드 전달 서비스이다. 전 세계 수백만 개의 모바일 앱에 대한 연결을 유지하고 메시지 페이로드를 도착하자마자 동시에 결합하는 서비스다.

전 세계의 모바일 네트워크는 다양한 수준의 신뢰성을 제공하므로, 전달 시스템은 장애를 대응하기 위해 견고해야 한다. 저희 시스템은 전달에 대해 "at-least-once(최소한 한번)" 보증 메커니즘(QOS)을 제공하고 있다.

RAMEN 전달 규약

신뢰할 수 있는 전송 채널을 제공하려면 애플리케이션에서 데이터 센터의 전송 서비스로 TCP 기반 영구 연결을 사용해야 했다. 2015년 애플리케이션 프로토콜의 경우, 우리의 옵션은 Long Polling, 웹 소켓, Server-Sent Events(SSE)을 HTTP/1.1을 활용하는 것이었다.

보안, 모바일 SDK 지원, 바이너리 크기 영향 등 다양한 고려 사항을 바탕으로 SSE를 사용하기로 했다. Uber에서 이미 지원되는 HTTP + JSON API 스택의 단순성과 운용성으로 인해 당시 우리는 그것을 선택했다.

그러나 SSE는 단방향 프로토콜이다. 즉, 데이터는 서버에서 앱으로만 보낼 수 있다. 앞에서 언급한 최소 한 번 이상의 보증을 제공하기 위해, 승인과 재시도가 애플리케이션 프로토콜 위에 전송 프로토콜로 구축되어야 할 필요가 있었다.

매우 우아하고 간단한 프로토콜 체계가 SSE 위에 정의되었다.

message-diagram 그림: Figure 2: Server-client interaction for the SSE protocol.

클라이언트가 첫번째 HTTP 요청 /ramen/receive?seq=0에서 메시지를 수신하기 시작하는데 새로운 세션 시작시 시퀀스 번호가 0이다. 서버는 SSE 연결을 유지하기 위해 HTTP 200 및 'Content-Type: text/event-stream'으로 응답한다.

다음 단계로, 서버는 모든 보류 중인 메시지를 우선순위의 내림차순으로 발송하고 시퀀스 번호를 증가 시킨다. 기본 전송 프로토콜은 TCP 연결이기 때문에 seq#3의 메시지가 전달되지 않으면 연결이 끊어지거나, 시간 초과되거나, 연결이 실패했다는 것이 된다.

이제 클라이언트는 다음 메세지를 위해 발견된 가장 큰 시퀀스 번호(이 경우 seq=2)로 다시 연결해야 한다. 그것은 서버에 3번이 소켓에 적혀있음에도 불구하고 배달되지 않았다는 것을 말해준다. 그런 다음 서버는 동일한 메시지 또는 seq=3으로 시작하는 높은 우선순위 메시지를 다시 발송한다. 이 프로토콜은 저장소의 대부분을 처리하는 서버와의 스트리밍 연결에 필요한 재개 가능성에 필요한 기능을 구축하며 클라이언트 측에서 구현하기도 매우 간단하다.

연결이 활성 상태인지 확인하기 위해 서버는 4초마다 1바이트 크기의 heartbeat 메시지를 보낸다. 클라이언트에 heartbeat 또는 최대 7초까지 메시지가 표시되지 않으면 연결이 끊긴 것으로 간주하고 다시 연결한다.

위의 프로토콜에서 클라이언트가 더 높은 시퀀스 번호로 다시 연결할 때마다 서버는 오래된 메시지를 플러시하기 위한 확인 메커니즘 역할을 한다. 양호한 네트워크에서는 사용자가 최대 몇 분 동안 연결 상태를 유지할 수 있으므로 서버가 이전 메시지를 계속 축적할 수 있다. 이 문제를 완화하기 위해 앱은 연결 품질에 관계없이 30초마다 /ramen/ack?seq=N을 호출한다.

프로토콜의 단순성은 클라이언트를 다양한 언어와 플랫폼으로 매우 빠르게 작성할 수 있게 했다.

디바이스 컨텍스트 저장소

RAMEN 서버는 연결이 설정될 때마다 매번 디바이스 컨텍스트를 추가로 저장한다. 이 컨텍스트는 사용자의 디바이스 컨텍스트에 액세스하기 위해 Fireball에 노출된다. 각 디바이스 컨텍스트의 ID는 사용자 및 해당 디바이스 매개 변수를 사용하여 고유한 해시로 생성된다. 이를 통해 사용자가 설정이 다른 여러 기기나 앱을 동시에 사용하는 경우에도 푸시 메시지를 격리할 수 있다.

메시지 저장소

RAMEN 서버는 모든 메시지를 메모리에 저장하고 데이터베이스에 백업한다. 연결이 불안정할 경우, 서버는 TTL이 만료될 때까지 전송을 계속 재시도할 수 있다.

구현 상세 정보

이 1세대 RAMEN 서버는 Uber의 일관된 해싱/샤딩 프레임워크인 "Ringpop"을 사용하여 Node.js로 작성되었다. Ringpop은 분산된 샤딩 시스템이다. 모든 연결은 사용자 UUID로 샤딩되었으며 Redis를 데이터스토어로 사용했다.

RAMEN을 글로벌하게 확장

그 후 1년 반 동안 푸시 플랫폼은 회사 전체에서 엄청나게 많은 시스템에서 사용되었다. 피크시에 이 시스템은 최대 60만 개의 동시 스트리밍 연결을 유지함으로써 초당 7만개 이상의 QPS 푸시 메시지를 세가지 다른 유형의 앱으로 푸시했다. 이 시스템은 서버 클라이언트 API 인프라의 가장 필수적인 부분이 되었다.

트래픽과 지속적인 연결이 증가함에 따라 기술 선택도 확장이 필요 했다. Ringpop 기반 분산 샤딩은 매우 단순한 아키텍처이지만 링의 노드수가 증가함에 따라 확장되지 않는다. Ringpop 라이브러리는 회원 자격을 평가하기 위해 가십 프로토콜을 사용했다. 고리의 크기가 커질수록 가십 프로토콜의 수렴 시간도 증가한다.

거기에 더해 Node.js workers는 단일 스레드였으며 높은 수준의 이벤트 루프 지연으로 인해 멤버쉽 정보의 수렴이 더 지연될 수 있었다. 이러한 문제로 인해 토폴로지 정보가 일관되지 않고 메시지 손실, 시간 초과 및 오류가 발생할 수 있다.

2017년 초, 우리는 RAMEN 프로토콜의 서버 구현부를 계속 확장할 수 있는 구조로 재작성해야 할 때라고 결정했다. 이번 작업에서는 다음 기술을 사용했다. Netty, Apache Zookeeper, Apache Helix, Redis, Apache Cassandra.

Netty: Netty는 네트워크 서버와 클라이언트를 구축하기 위해 널리 사용되는 고성능 라이브러리. Netty의 bytebuf는 시스템을 매우 효율적으로 만드는 제로 카피 버퍼를 허용한다.

Apache ZooKeeper: 네트워크 연결에 대한 consistent hashing 알고리즘을 통해 스토리지 계층이 필요 없이 데이터를 직접 스트리밍할 수 있다. 그러나 분산된 토폴로지 관리 대신 ZooKeeper와의 중앙 집중식 공유를 선택했다. ZooKeeper는 분산 동기화 및 구성 관리를 위한 매우 강력한 시스템이며 연결된 노드의 장애를 빠르게 감지할 수 있다.

Apache Helix: Helix는 ZooKeeper 위에서 작동하는 강력한 클러스터 관리 프레임워크로 사용자 지정 토폴로지를 정의하고 알고리즘을 재조정할 수 있다. 또한 비즈니스 로직에서 토폴로지 로직을 훌륭하게 추상화한다. 연결된 워커를 모니터링하고 샤딩 상태 정보 변경을 전파하기 위해 ZooKeeper를 사용한다. 또한 사용자 정의 "리더-팔로워" 토폴로지와 사용자 정의 점진적으로 재조정 알고리즘을 작성할 수 있게 해준다.

Redis & Apache Cassandra: 멀티 리젼 클라우드 아키텍처를 준비하는 과정에서 메시지가 올바르게 복제되고 저장되어야 했다. Cassandra는 내구성이 뛰어나고 여러 리젼에 걸쳐 복제가 되는 스토리지다. Redis는 배포 또는 페일오버 이벤트에서 일반적인 샤드 시스템과 관련된 문제를 방지하기 위해 카산드라 위에서 캐시 형태로 사용된다.

RAMEN-Architectue 그림 : Figure 3: Architecture for the new RAMEN backend server.

Streamgate: 이 서비스는 Nety에서 RAMEN 프로토콜을 구현하고 연결과 메시지 및 저장과 관련된 모든 처리 로직을 가지고 있다. 이 서비스는 ZooKeeper와 연결을 설정하고 하트비트를 관리하는 Apache Helix 참가자를 추가로 구현한다.

StreamgateFE(Streamgate Front End): 이 서비스는 Apache Helix 스펙테이터 역할을 하며 ZooKeeper의 토폴로지 변경을 모니터링한다. 이것은 역방향 프록시로 구현되어 있다. 클라이언트(Fireball, 게이트웨이 또는 모바일 앱)에서 오는 모든 요청은 토폴로지 정보를 사용하여 분할되고 올바른 Streamgate 작업자에게 전달된다.

Helix Controller: 이름에서 알 수 있듯이 Apache Helix Controller 프로세스 실행을 전담하는 5개 노드로 단일 실행(스텐드얼론) 방식의 서비스며 토폴로지 관리의 두뇌이다. Streamgate 노드가 시작되거나 중지될 때마다 변경 사항을 감지하고 샤딩 파티션을 다시 할당한다.

우리는 지난 몇년동안 이 아키텍처를 운영해 왔으며, 99.99%의 서버 사이드 인프라 안정성을 달성할 수 있었다. 이 푸시 인프라의 채택은 iOS, Android 및 웹 플랫폼에서 10가지 이상의 다른 유형의 애플리케이션을 지원하면서 계속 증가하고 있다. 우리는 이 시스템을 150만개 이상의 동시 연결로 운영했고 초당 25만개 이상의 메시지를 전달하고 있다.

gRPC를 통한 푸시 인프라의 미래

이 서버 사이드 인프라는 안정성이 지속되어 왔다. 다양한 네트워크 조건과 앱을 가지고 새로운 도시에 서비스를 제공함에 있어서 우리의 촛점은 모바일 디바이스에 대한 푸시 메시지 전달의 롱테일 관점에서 신뢰성을 지속적으로 높이는 데 있다. 우리는 지속적으로 새로운 프로토콜을 실험하고 격차를 해소하기 위한 방법론을 개발하고 있다. 갭을 점검할 때, 다음 영역은 신뢰성 하락에 기여하고 있다.

ACK(전송 완료 응답) 유실(Loss of acknowledgements)

위에서 정의한 RAMEN 프로토콜은 데이터 전송을 줄이기 위해 최적화되었으며, 따라서 ACK에 대한 인식은 30초마다 또는 클라이언트가 다시 연결되었을 때에만 알 수 있다. 이로 인해 확인이 지연되는 경우에 따라 메시지 전달을 확인하지 못하게 된다. 이는 진정한 메시지 손실과 확인 요청 실패를 구별하기 어렵게 만든다.

연결 안정성 불신(Poor connection stability)

서버와 클라이언트 간의 정상적인 연결을 유지하는 것이 중요하다. 서로 다른 플랫폼과 연결된 클라이언트 구현은 오류 처리, 시간 초과, 백오프 또는 애플리케이션 수명 주기 이벤트(개방 또는 종료), 네트워크 상태 변경, 호스트 이름 및 데이터 센터 페일 오버에서 미묘한 차이를 보이게 된다. 따라서 버전에 따라 성능이 달라질 수 있다.

전송 제한(Transport limitations)

프로토콜이 SSE상에서 구현 되었기 때문에 데이터 전송은 단방향이다. 여러 가지 새로운 앱 경험을 통해 양방향 메시지 전송을 활성화해야 했다. 실시간 왕복 시간 측정이 없으면 네트워크 상태, 전송 속도 및 라인 헤드 차단 감소가 불가능해진다. SSE는 또한 base64와 같은 텍스트 인코딩 없이 이진 페이로드를 전송할 수 있는 능력을 제한하는 텍스트 기반 프로토콜로이어서 페이로드 크기가 더 커지게 된다.

2019년말 우리는 위의 단점을 해결하기 위해 차세대 RAMEN 프로토콜 개발을 시작했다. 많은 고민 끝에, 우리는 이것을 gRPC위에 구축하기로 결정했다. gRPC는 여러 언어에 걸쳐 클라이언트와 서버의 표준화된 구현이 있는 널리 채택된 RPC 스택이다. 수많은 다양한 RPC 메서드에 대해 첫번째로 지원을 제공했으며 QUIC 전송 계층 프로토콜과의 상호 운용성을 갖추고 있다.

새로운 gRPC 기반 RAMEN 프로토콜은 이전 SSE 기반 프로토콜에서 확장 되었으며 몇가지 주요 차이점이 있다:

  • 이제 ACK(확인) 메시지가 역스트림으로 즉시 전송된다. 이로 인해 데이터 전송량은 약간 증가했지만, 응답의 신뢰성이 향상되었다.
  • 실시간 확인을 통해 RTT를 측정하고 네트워크 상태를 실시간으로 파악할 수 있다. 우리는 진짜 메시지 손실과 네트워크 손실을 구별할 수 있게 되었다.
  • 이것은 스트림 다중화와 같은 기능을 지원하기 위해 프로토콜 위에 추상화 계층을 제공한다. 또한 애플리케이션 레벨 네트워크 우선 순위 지정 및 흐름 제어 알고리듬을 실험하여 데이터 사용 및 통신 지연 시간 측면에서 더 많은 효율성을 가져올 수 있었다.
  • 프로토콜은 메시지 페이로드를 추상화하여 다양한 유형의 직렬화를 지원한다. 미래에는 다른 직렬화를 탐색할 수 있지만, gRPC는 전송 계층은 유지된다.
  • 다른 언어로 클라이언트를 강력하게 구현하면 다양한 유형의 앱과 디바이스를 신속하게 지원할 수 있다.

이 작업은 현재 베타 릴리스 중이며 처음부터 gRPC로 시작할 수 있다면 더 좋다.

최종적인 생각

푸시 플랫폼은 Uber의 여행 경험에서 필수적인 부분이다. 오늘날 수백가지 기능이 이 플랫폼을 통해 제공되고 있다. 이 플랫폼이 Uber에서 크게 성공한 몇가지 주요 이유는 다음과 같다.

관심 분리

메시지 트리거링, 생성 및 전달 시스템 간의 명확한 책임 분리를 통해 비즈니스 요구 변화에 따라 플랫폼의 다른 부분에 집중할 수 있게 되었다. 전달 구성 요소를 아파치 헬릭스, 토폴로지 로직, 스트리밍의 핵심 비즈니스 로직으로 분리는 매우 잘 되어 있다. 이를 통해 정확히 동일한 아키텍처로 다른 네트워크 프로토콜로 gRPC를 지원할 수 있었다.

업계 표준 기술

업계 표준 기술을 기반으로 구축하면 구현이 훨씬 강력하고 장기적이며 비용 효율적이다. 상기 시스템의 유지보수 오버헤드는 매우 적었다. 우리는 매우 효율적인 팀 규모로도 이 플랫폼으로써의 가치를 제공할 수 있다. 우리의 경험으로 볼 때, Helix와 Zookeeper는 매우 안정적이었다.

더 단순한 디자인

우리는 이 프로토콜을 사용하여 다양한 네트워크 조건, 수백 개의 기능 및 수십 개의 앱을 통해 수백만 명의 사용자로 확장할 수 있다. 프로토콜의 단순성으로 인해 쉽게 확장하고 빠르게 이동할 수 있었다.