Bài toán: Đẩy thông báo tức thì tới hàng triệu thiết bị

Khi DISCO quyết định ghép bạn với tài xế Nguyễn Văn A, hệ thống phải:

  1. Gửi cuốc xe đến đúng điện thoại của tài xế A (trong số hàng triệu điện thoại đang kết nối).
  2. Gửi trong mili-giây (không phải giây).
  3. Đảm bảo tài xế nhận được dù mạng 4G đang yếu.
  4. Đồng thời gửi vị trí tài xế ngược về app của bạn để hiển thị xe di chuyển trên bản đồ.

Có hai cách tiếp cận: Polling (hỏi liên tục) và Push (đẩy chủ động).


Polling vs Push

Polling (Cách cũ — không hiệu quả)

Driver App cứ mỗi 3 giây hỏi server: "Có cuốc nào cho tôi không?"

GET /api/v1/offers?driver_id=abc123
→ Response: { "offers": [] }  ← Không có gì

GET /api/v1/offers?driver_id=abc123  (3 giây sau)
→ Response: { "offers": [] }  ← Vẫn không có gì

GET /api/v1/offers?driver_id=abc123  (3 giây sau)
→ Response: { "offers": [...] }  ← Có cuốc! Nhưng đã chậm 0-3 giây

Vấn đề:
  - 5 triệu tài xế × 1 request/3s = 1.67 triệu request/giây (chỉ để hỏi)
  - 99% request trả về rỗng → Lãng phí tài nguyên server
  - Latency 0-3 giây → Tài xế khác có thể nhận trước
  - Tốn pin: radio chip phải hoạt động liên tục

Push (RAMEN — Hiệu quả)

Server duy trì kết nối MỞ với mỗi driver app.
Khi có cuốc xe, server CHỦ ĐỘNG đẩy xuống ngay lập tức.

Driver App ◄═══ gRPC Stream (kết nối sống) ═══► RAMEN Server

Ưu điểm:
  - Latency: < 100ms (gần như tức thì)
  - Không có request lãng phí
  - Radio chip chỉ hoạt động khi có dữ liệu thực sự
  - Tiết kiệm pin đáng kể

RAMEN — Real-time Asynchronous Messaging Network

RAMEN là hạ tầng push messaging mà Uber tự xây dựng, duy trì hàng triệu kết nối sống (persistent connections) đồng thời để đẩy dữ liệu real-time tới rider và driver apps.

Kiến trúc ba tầng

┌────────────────────────────────────────────────────────────┐
│                     RAMEN Architecture                      │
│                                                              │
│  ┌──────────────────┐                                       │
│  │  Fireball Service │  "Khi nào cần push?"                 │
│  │  (Decision Engine)│  • Consume Kafka events               │
│  │                    │  • Evaluate business rules            │
│  │                    │  • Priority, localization             │
│  └────────┬─────────┘                                       │
│           │                                                  │
│           ▼                                                  │
│  ┌──────────────────┐                                       │
│  │  API Gateway      │  "Push cái gì?"                      │
│  │  (Payload Builder)│  • Aggregate data from services       │
│  │                    │  • Build message payload              │
│  │                    │  • Serialize (Protobuf)               │
│  └────────┬─────────┘                                       │
│           │                                                  │
│           ▼                                                  │
│  ┌──────────────────┐                                       │
│  │  RAMEN Server     │  "Push bằng cách nào?"               │
│  │  (Delivery Layer) │  • Manage millions of connections     │
│  │                    │  • Route to correct device            │
│  │                    │  • Guarantee at-least-once delivery   │
│  └──────────────────┘                                       │
│           │                                                  │
│           ▼                                                  │
│     Mobile Devices (Millions)                                │
└────────────────────────────────────────────────────────────┘

Sự tiến hóa của giao thức truyền tải

Thế hệ 1: Server-Sent Events (SSE) qua HTTP/1.1

Client ──── HTTP/1.1 Connection ────► RAMEN Server
       ◄═══ SSE (Server → Client only) ═══

Đặc điểm:
  ✓ Đơn giản, browser-friendly
  ✗ Một chiều: chỉ Server → Client
  ✗ ACK (xác nhận nhận) phải gửi qua HTTP POST riêng (tốn connection)
  ✗ Head-of-line blocking: message lớn chặn heartbeat
  ✗ Text-based JSON: payload nặng

Thế hệ 2: gRPC over QUIC/HTTP3 (Hiện tại)

Client ◄══ gRPC Bidirectional Stream ══► RAMEN Server
              (HTTP/3 + QUIC)

Đặc điểm:
  ✓ Full-duplex: cả hai bên gửi đồng thời trên cùng connection
  ✓ Binary framing (Protobuf): payload nhỏ, CPU thấp
  ✓ Multiplexing: nhiều stream trên 1 connection, không head-of-line blocking
  ✓ QUIC: UDP-based, tốt hơn trên mạng di động yếu
  ✓ ACK ngay trên stream hiện tại (không cần connection riêng)
  ✓ Connection migration: đổi mạng (WiFi → 4G) không mất connection

Scalability: Quản lý hàng triệu kết nối

Apache Helix + ZooKeeper

RAMEN không thể chạy trên một server — nó cần một cluster gồm hàng trăm server, mỗi server giữ hàng chục nghìn kết nối sống.

RAMEN Cluster Management:

ZooKeeper: Lưu topology metadata (server nào đang sống?)
Apache Helix: Quản lý sharding, rebalancing tự động

User UUID "abc123" → hash() → Shard 42 → RAMEN Server #7
User UUID "def456" → hash() → Shard 18 → RAMEN Server #3

Khi server #7 chết:
  1. Helix phát hiện (heartbeat timeout)
  2. Helix chuyển Shard 42 sang server #9
  3. Client "abc123" reconnect → Load Balancer → server #9
  4. Tiếp tục nhận messages

Stateful Servers — Thách thức đặc biệt

Khác với REST API servers (stateless), RAMEN servers là stateful — mỗi server giữ TCP/gRPC sockets cụ thể cho từng user. Routing phải chính xác: message cho user “abc123” phải đến đúng server đang giữ socket của user đó.

Routing Flow:

Fireball: "Push ride offer to driver abc123"
  │
  ▼
Routing Layer: hash("abc123") → Server #7
  │
  ▼
Server #7: Tìm socket của abc123 trong memory → Push message

Đảm bảo tin cậy (Reliability)

At-Least-Once Delivery

Mạng di động không đáng tin cậy — 4G có thể mất tín hiệu vài giây rồi lại có. RAMEN đảm bảo message được gửi ít nhất một lần bằng:

Persistence Layer:

  Cassandra (Durable Storage)     Redis (In-Memory Cache)
  ┌────────────────────┐         ┌────────────────────┐
  │ Source of truth     │         │ Absorb traffic     │
  │ Lưu message vĩnh   │◄───────│ bursts             │
  │ viễn cho retry      │         │ Thundering herd    │
  └────────────────────┘         │ protection         │
                                  └────────────────────┘

Flow:
  1. Message đến → Ghi vào Cassandra (backup)
  2. Cache vào Redis (fast access)
  3. Push qua gRPC stream tới device
  4. Device gửi ACK
  5. Nếu không nhận ACK trong 10s → Retry từ Cassandra
  6. Nhận ACK → Đánh dấu delivered

Sequence Numbers — Xử lý reconnect

Khi client mất kết nối rồi kết nối lại, làm sao biết nó đã nhận message nào rồi?

Mỗi message có sequence number tăng dần:

Server → Client:
  [seq=1] Ride offer
  [seq=2] ETA update
  [seq=3] Driver location  ← Connection mất ở đây

Client reconnect:
  "Last received: seq=2"

Server:
  → Gửi lại từ seq=3 trở đi (không gửi lại seq=1, seq=2)
  [seq=3] Driver location (retry)
  [seq=4] Driver location (new)
  ...

Connection Draining — Tránh Thundering Herd

Khi cần deploy code mới hoặc scale down cluster, RAMEN không thể ngắt hàng triệu kết nối cùng lúc — sẽ gây thundering herd (hàng triệu client reconnect đồng thời, làm sập hệ thống).

Graceful Shutdown Flow:

1. RAMEN Server #7 cần shutdown
2. Server #7 ngừng nhận kết nối MỚI
3. Gửi "Graceful Disconnect" tới tất cả client đang kết nối
   Message: { type: "DISCONNECT", backoff_hint_ms: random(1000, 30000) }
4. Mỗi client nhận backoff_hint khác nhau:
   - Client A: chờ 2.3 giây rồi reconnect
   - Client B: chờ 15.7 giây rồi reconnect
   - Client C: chờ 8.1 giây rồi reconnect
5. Reconnections rải đều trong 30 giây → không thundering herd

Fallback: Silent Push Notification

Khi app tài xế bị hệ điều hành đẩy xuống background (Android/iOS power management), gRPC stream sẽ bị đóng. Lúc này RAMEN dùng Silent Push Notification qua APNs (Apple) / FCM (Google) để “đánh thức” app:

Luồng chính (App foreground):
  RAMEN Server ══► gRPC Stream ══► Driver App  ✓ (< 100ms)

Luồng fallback (App background):
  RAMEN Server ──► FCM/APNs ──► OS wakes app ──► App reconnect gRPC
                                                    (1-5 giây)

Grab’s Approach: WebSocket + Istio

Grab không xây hệ thống phức tạp như RAMEN mà dùng cách tiếp cận đơn giản hơn:

  • WebSocket cho real-time bidirectional communication.
  • Istio Service Mesh quản lý routing, load balancing, mTLS.
  • FCM/APNs làm fallback khi app bị background.

Ưu điểm: Đơn giản hơn, dùng chuẩn mở. Nhược điểm: WebSocket trên HTTP/1.1 có head-of-line blocking, không mạnh bằng gRPC/QUIC trên mạng yếu.


Tổng kết luồng dữ liệu Real-time End-to-End

 TOÀN BỘ LUỒNG REAL-TIME:

 ① Tài xế di chuyển
    → GPS Sensor → Kalman Filter → Batch 3 điểm
    → gRPC Stream → Load Balancer → Location Service

 ② Location Service
    → Convert GPS → H3 Index
    → Produce Kafka "driver.location.updates"

 ③ Kafka → Consumers:
    ├── Redis GEO (cập nhật vị trí driver)
    ├── Flink (tính supply-demand → Surge Pricing)
    └── Analytics Pipeline (Data Lake)

 ④ Khách đặt xe
    → Demand Service → Kafka "ride.requests"

 ⑤ DISCO Matching Engine
    → Query Redis (tài xế gần nhất)
    → Routing Service (tính ETA thực tế)
    → Hungarian Algorithm (batch matching)
    → Chọn tài xế tối ưu

 ⑥ RAMEN Push
    → Fireball (decision) → API Gateway (payload)
    → RAMEN Server → gRPC Stream → Driver Phone
    → "Bạn có cuốc xe mới!"

 ⑦ Tài xế nhận cuốc
    → Trip Service → Kafka "ride.status.changes"
    → RAMEN Push → Rider App: "Tài xế đang đến!"
    → Location stream bắt đầu đẩy vị trí tài xế về Rider App
    → Xe di chuyển mượt mà trên bản đồ 🚗

 Tổng thời gian: < 2 giây

Nguồn tham khảo chính thức

NguồnNội dung
Uber Eng: H3 Hexagonal IndexThuật toán chia lưới lục giác
Uber Eng: RAMENKiến trúc push messaging
Uber Eng: DISCOMatching Engine
Uber Eng: DeepETAML model dự đoán ETA
Grab Eng: Fulfilment PlatformKiến trúc dispatch platform
Grab Eng: DispatchGymRL framework cho dispatch
Lyft Eng: Real-time Map MatchingKalman Filter + Map Matching
H3 DocumentationAPI reference cho H3
Google S2 GeometryAPI reference cho S2

Chúc mừng bạn đã hoàn thành chuỗi bài! Bây giờ bạn đã hiểu rõ từng lớp kiến trúc đằng sau chiếc xe chạy mượt mà trên bản đồ. Từ GPS sensor → Kalman Filter → Kafka → H3 → DISCO → RAMEN → App của bạn. Mỗi lớp là một bài toán kỹ thuật phân tán cực kỳ thú vị.