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) và 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ì:
- Throughput cực cao: Hàng triệu message/giây trên một cluster.
- Durable: Message được ghi xuống disk, không mất khi server restart.
- Ordered: Message trong cùng partition được đảm bảo thứ tự (quan trọng cho GPS timeline).
- Replayable: Consumer có thể đọc lại message từ quá khứ (giúp debug, retrain ML models).
- Decoupled: Producer và Consumer hoàn toàn độc lập.
Thiết kế Topic cho Ride-Hailing
Topic chính
| Topic | Producer | Consumers | Partition Key |
|---|---|---|---|
driver.location.updates | Location Service | Redis GEO, Flink, Analytics | driver_id |
ride.requests | Demand Service | Matching Engine, Pricing | rider_id |
ride.assigned | Matching Engine | RAMEN Push, Analytics | driver_id |
ride.status.changes | Trip Service | Billing, Analytics, Push | trip_id |
surge.pricing.updates | Pricing Engine | API Gateway, Driver App | h3_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
Stream Processing: Apache Flink
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 Stack: Kafka + Flink + Apache Pinot
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ĩa | Dùng cho |
|---|---|---|
| At-Least-Once | Message có thể bị xử lý lặp | GPS updates (idempotent: ghi đè vị trí cũ) |
| Exactly-Once | Mỗi message chỉ xử lý đúng 1 lần | Billing, 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.