Tại sao cần Event Streaming?

Hàng triệu sự kiện xảy ra mỗi giây trong hệ thống gọi xe:

  • Tài xế A cập nhật tọa độ GPS.
  • Khách hàng B mở app và đặt xe.
  • Tài xế C nhận cuốc và bắt đầu di chuyển.
  • Khách hàng D huỷ cuốc.
  • Surge pricing cập nhật hệ số nhân tại khu vực Quận 1.

Nếu mỗi service gọi trực tiếp lẫn nhau (synchronous), hệ thống sẽ trở nên chặt chẽ (tightly coupled)dễ vỡ — một service chậm sẽ kéo sập cả chuỗi. Giải pháp là Event Streaming: mọi sự kiện đều được đẩy vào một “đường ống” trung tâm, và các service tự đăng ký lắng nghe những sự kiện mình cần.


Apache Kafka — Bộ xương sống

Uber xử lý hơn 1 nghìn tỉ (trillion) message mỗi ngày qua Kafka. Grab xử lý hàng trăm tỉ message. Kafka được chọn vì:

  1. Throughput cực cao: Hàng triệu message/giây trên một cluster.
  2. Durable: Message được ghi xuống disk, không mất khi server restart.
  3. Ordered: Message trong cùng partition được đảm bảo thứ tự (quan trọng cho GPS timeline).
  4. Replayable: Consumer có thể đọc lại message từ quá khứ (giúp debug, retrain ML models).
  5. Decoupled: Producer và Consumer hoàn toàn độc lập.

Thiết kế Topic cho Ride-Hailing

Topic chính

TopicProducerConsumersPartition Key
driver.location.updatesLocation ServiceRedis GEO, Flink, Analyticsdriver_id
ride.requestsDemand ServiceMatching Engine, Pricingrider_id
ride.assignedMatching EngineRAMEN Push, Analyticsdriver_id
ride.status.changesTrip ServiceBilling, Analytics, Pushtrip_id
surge.pricing.updatesPricing EngineAPI Gateway, Driver Apph3_cell_id

Partitioning Strategy — Chìa khóa của hiệu năng

Kafka chia mỗi topic thành nhiều partition. Message có cùng key sẽ luôn vào cùng partition → đảm bảo thứ tự.

Topic: driver.location.updates (12 partitions)

driver_id = "abc123" → hash("abc123") % 12 = Partition 3
driver_id = "def456" → hash("def456") % 12 = Partition 7
driver_id = "ghi789" → hash("ghi789") % 12 = Partition 3

Partition 3: [abc123-t1] [ghi789-t1] [abc123-t2] [ghi789-t2] ...
             ↑ Thứ tự GPS của mỗi driver được đảm bảo trong partition

Tại sao partition key = driver_id?

  • Đảm bảo tất cả GPS updates của cùng một tài xế đi vào cùng partition.
  • Consumer xử lý partition 3 sẽ thấy timeline GPS liên tục của driver abc123.
  • Nếu không, GPS có thể đến lộn xộn: timestamp 10:00:03 đến trước 10:00:01.

Hot Partition Problem

Vấn đề: Nếu một driver nổi tiếng (hoặc một khu vực nhỏ) tạo ra quá nhiều events, partition chứa nó sẽ bị quá tải trong khi các partition khác rảnh rỗi.

Giải pháp: Composite Key + Salting

Thay vì: key = "driver_id"
Dùng:    key = "driver_id" + "_" + random(0-3)

→ Events của 1 driver được rải đều 4 partitions
→ Mất thứ tự tuyệt đối nhưng mỗi batch 4 giây vẫn có timestamp để sắp xếp lại

Dữ liệu thô từ Kafka cần được xử lý, làm giàu (enrich), tổng hợp (aggregate) trước khi các service downstream có thể dùng. Đây là công việc của Apache Flink — framework stream processing phân tán.

Use Case 1: Đếm cung-cầu real-time (cho Surge Pricing)

Flink Job: Supply-Demand Counter

Input:  Kafka topic "driver.location.updates"
        Kafka topic "ride.requests"

Sliding Window: 5 phút, cập nhật mỗi 30 giây

Logic:
  Với mỗi H3 cell (resolution 7):
    supply_count = Đếm số driver CÓ MẶT trong cell (status = AVAILABLE)
    demand_count = Đếm số ride request TRONG 5 phút qua tại cell

    supply_demand_ratio = supply_count / demand_count

Output: Kafka topic "surge.pricing.input"
        { h3_cell: "872a100d6ffffff", supply: 12, demand: 45, ratio: 0.27 }

Use Case 2: ETA Enrichment

Flink Job: ETA Calculator

Input:  Kafka topic "ride.assigned" (chứa driver_id, rider_location)
        Redis (vị trí driver hiện tại)
        Routing Service API (tính ETA dựa trên giao thông)

Logic:
  1. Nhận event "ride.assigned"
  2. Lấy vị trí driver từ Redis
  3. Gọi Routing Service: ETA = f(driver_pos, rider_pos, traffic)
  4. Enrich event với ETA

Output: Kafka topic "ride.assigned.enriched"
        { trip_id, driver_id, eta_seconds: 180, route_polyline: "..." }

Use Case 3: Anomaly Detection (Phát hiện bất thường)

Flink Job: GPS Anomaly Detector

Input: Kafka topic "driver.location.updates"

Logic:
  Stateful processing: giữ vị trí trước đó của mỗi driver
  
  Kiểm tra:
  1. Tốc độ > 200 km/h → GPS giả (spoofing)
  2. Teleportation: di chuyển > 5km trong 4 giây → GPS nhảy
  3. Đứng yên > 30 phút liên tục → Driver offline nhưng chưa tắt app
  
Output:
  - Flag giao dịch bất thường
  - Tự động chuyển driver sang trạng thái INACTIVE

Kiến trúc Kafka Cluster tại Uber

Uber công bố kiến trúc Kafka của họ trong nhiều blog kỹ thuật:

                    ┌─────────────────────────────────┐
                    │        Kafka Cluster             │
                    │                                   │
  Producers ──────► │  Topic: driver.location.updates  │ ──────► Consumers
  (Location Svc)    │    Partitions: 128                │    (Redis, Flink,
                    │    Replication Factor: 3           │     Analytics)
                    │    Retention: 72 hours             │
                    │                                   │
                    │  Topic: ride.requests              │
                    │    Partitions: 64                  │
                    │    Replication Factor: 3           │
                    │                                   │
                    │  Topic: ride.status.changes        │
                    │    Partitions: 64                  │
                    │    Replication Factor: 3           │
                    └─────────────────────────────────┘

Các con số thực tế (Uber, công bố 2023):
  - Cluster: hàng chục nghìn broker nodes
  - Throughput: hơn 30 triệu message/giây
  - Storage: Petabytes dữ liệu
  - Topics: hàng chục nghìn

Grab sử dụng combo đặc biệt cho Operational Analytics:

Kafka (Events) → Flink (Stream Processing) → Apache Pinot (Real-time OLAP)

Apache Pinot cho phép:
  - Query SQL trên dữ liệu streaming gần real-time
  - Dashboard Ops: "Số cuốc xe hoàn thành trong 5 phút qua tại Quận 1?"
  - Latency: p99 < 100ms cho aggregation queries

Consumer Group Design

Mỗi use case = Một Consumer Group

Topic: driver.location.updates

Consumer Group "redis-geo-updater"     → Cập nhật Redis GEO (3 instances)
Consumer Group "flink-surge-calculator" → Tính surge pricing (Flink cluster)
Consumer Group "analytics-pipeline"    → Ghi vào Data Lake (5 instances)
Consumer Group "fraud-detector"        → Phát hiện GPS giả (2 instances)

Mỗi group đọc TOÀN BỘ topic nhưng xử lý riêng biệt.
Nếu fraud-detector bị chậm, không ảnh hưởng redis-geo-updater.

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

At-Least-Once vs Exactly-Once

Delivery GuaranteeÝ nghĩaDùng cho
At-Least-OnceMessage có thể bị xử lý lặpGPS updates (idempotent: ghi đè vị trí cũ)
Exactly-OnceMỗi message chỉ xử lý đúng 1 lầnBilling, Payment (không được tính tiền 2 lần)

Đối với GPS updates, At-Least-Once là đủ vì nhận lại cùng tọa độ thì chỉ ghi đè lên vị trí cũ trong Redis — không gây hại gì.

Đối với billing (tính tiền cuốc xe), bắt buộc phải dùng Exactly-Once (Kafka transactions + idempotent consumers) hoặc thiết kế consumer idempotent bằng cách dùng trip_id làm deduplicate key.

Tiếp theo, chúng ta sẽ đi vào bộ não thực sự của hệ thống — DISCO Matching Engine — nơi quyết định tài xế nào sẽ nhận cuốc xe nào. Đọc tiếp Phần 4 — DISCO & Matching Engine: Thuật toán ghép cuốc xe.