← Chương trước: Phần 2 — Geospatial Indexing: H3, S2 Geometry & Redis GEO | Mục lục Series | Chương tiếp theo: Thuật Toán Gọi Xe: Cách Uber DISCO & Grab DispatchGym Ghép Tài Xế →


Answer-first: Xương sống Event Streaming với Apache Kafka và Apache Flink xử lý hàng triệu sự kiện GPS/giây với độ trễ dưới 100ms. Thiết kế phân vùng theo H3 cell hoặc geohash đảm bảo thứ tự sự kiện, kết hợp Flink stateful stream joins tính toán giá bão và bảo đảm exactly-once billing.

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, dữ liệu công bố kỹ thuật):
  - 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 hệ sinh thái mở rộng với Data Lake hiện đại:

Kafka (Events) → Flink (Stream Processing) → Apache Pinot / Apache Iceberg (Analytics)

Sự kết hợp này cho phép:
  - Truy vấn dữ liệu siêu tốc độ cho Ops Dashboard
  - Lưu trữ petabyte scale trên Iceberg phục vụ đào tạo mô hình ML dự đoán
  - Latency: p99 < 100ms cho aggregation queries tại Pinot

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.

📚 Đào sâu hơn: Với fraud detection trong Fintech (Core Banking) sử dụng Apache Flink CEP — pattern 3 failed logins + high-value transaction, RocksDB state backend, và async ML inference với SLA 50-100ms — xem Phần 7 — Streaming Fraud Detection trong Core Banking Architecture.


Đả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.


🔗 Đọc thêm các chuyên đề liên quan:


← Chương trước: Phần 2 — Geospatial Indexing: H3, S2 Geometry & Redis GEO | Mục lục Series | Chương tiếp theo: Thuật Toán Gọi Xe: Cách Uber DISCO & Grab DispatchGym Ghép Tài Xế →


❓ Câu Hỏi Thường Gặp (FAQ)

Xử lý hàng triệu GPS/s bằng Kafka: thiết kế topic, partition, consumer groups, stream processing với Flink cho surge pricing, và exactly-once semantics.

Q2: Những lưu ý quan trọng nhất khi triển khai thực tế là gì?

Cần chú trọng phân tầng ranh giới trách nhiệm (bounded context), thiết lập cơ chế fallback dự phòng, và giám sát chặt chẽ qua metrics OpenTelemetry để phát hiện sớm các điểm nghẽn.

Q3: Làm sao để kiểm thử và đánh giá hiệu quả sau khi áp dụng?

Áp dụng kiểm thử tải (load test), benchmark độ trễ P95/P99 trước và sau triển khai, kết hợp tracing phân tán để xác minh tính ổn định dưới tải cao.