免費完整指南

Uber (Ride Hailing)

設計Uber (Ride Hailing)。請涵蓋Geospatial indexing trade-off: geohash vs quadtree vs S2/H3 cells for storing and querying driver locations, High-frequency driver location ingestion: update interval vs GPS...

00

練習檢查點

面試節奏保持精簡,讓頁面能把注意力花在真正的設計決策上。

  1. 01
    釐清範圍
  2. 02
    需求 + 規模
  3. 03
    API + 資料模型
  4. 04
    畫出架構
  5. 05
    深入探討
  6. 06
    取捨決策
01

形塑設計的需求

不要只是陳述需求,要主動問出來。每張卡把設計限制和一句你可以在畫架構前說出口的釐清問句配成一組。

功能需求

01乘客最先看到什麼——在決定之前先看到價格嗎?

乘客輸入上車點與目的地,在叫車前先得到一個車資估算(價格 + ETA)。

02他們按下叫車之後會怎樣——媒合必須多快回來?

乘客以預估車資叫車,並在大約一分鐘內配對到一位附近的可用司機。如果沒有司機可用,他們會拿到一個明確的失敗,而不是空等。

03司機端要做什麼?

司機上線、送出位置 ping,一次接收一個叫車邀約來接受或拒絕。接受後,他們導航去接客與送客。

04乘客可以在媒合後取消嗎,司機會經歷什麼?

要。取消之後,司機立刻變回可接新單,而系統記錄是誰、在什麼時候取消的。

05乘客會即時看著司機接近嗎?

要。司機的位置只在行程進行中串流給配對到的乘客。其他所有人看到的是粗略的可用狀態,絕不是一條可追蹤的軌跡。

06在媒合時間窗內沒有司機接受——那該怎麼辦?

請求會明確失敗並附上重試指引——在這一分鐘內給一個確定的「不行」,勝過一個永遠轉不完的圈圈。

範圍外評分(乘客與司機) · 預約行程與車型分級(X/XL/Comfort) · 動態加價機制與付款結算

非功能需求

01一位司機有可能同時拿到兩趟行程嗎?

媒合是強一致的:一位司機同一時間最多持有一筆有效邀約或行程——永遠不會重複派遣。

02司機位置需要多新?

司機在線時每隔幾秒 ping 一次;鄰近搜尋看到的資料可能是幾秒前的,但絕不會是幾分鐘前的。

03司機位置更新應該以多大的尖峰速率來設計?

我們按一個刻意設定的壓力上限來設計:全球 10M 名司機在線,每個大約每 5 秒 ping 一次。這算下來大約是每秒 2M 次位置寫入。

04當一座體育場散場時會發生什麼?

來自單一區域約 10 萬筆叫車請求的突發會排隊並優雅地消化——鄰近區域與其他城市不受影響。

05每一步要感覺多快?

車資估算在數秒內;媒合(或一個明確的失敗)端到端在大約一分鐘內。

持續追問 —— 面試是一場對話

真實面試探得比一份整齊清單深得多。這些範圍問句,區分出真正拷問問題的人和只是背誦的人。

  • 行程歷史與司機位置軌跡必須存多久——司機的軌跡必須能應要求刪除嗎?
  • 我們需要處理 GPS 偽造或司機鑽媒合漏洞嗎,還是詐欺在別處過濾?
  • 上線時是一座城市,還是第一天就全球——而且每個區域的資料必須留在那個區域嗎?
  • 有沒有一個我們在合約上被要求的媒合成功率,還是一分鐘內給一個明確的失敗就可以接受?
  • 動態加價與評分不在範圍內嗎?
02

逼出架構決策的數字

把每個估算都當成一種壓力,用來合理化一個元件:快取、佇列、分片、副本、worker pool 或退路。

01

位置寫入洪流

壓力上限:全球 1,000 萬名司機在線 × 每 5 秒 1 次 ping10,000,000 ÷ 5 ≈ 2M writes/s

沒有關聯式資料庫吸收得了這個——位置進入一個記憶體內的地理索引(geohash/H3 桶)並以 TTL 逐出。

02

鄰近查詢成本

上車點落在一個 geohash cell 裡;要涵蓋搜尋半徑,就是那個 cell 加上它的 8 個鄰居——一個 3 × 3 網格,9 個 cell9 次 cell 讀取 × 每次記憶體內查詢遠低於 1 ms ≈ 個位數毫秒

geohash 把「誰在我附近」變成少數幾次 key 查詢,而不是一次全表掃描。

03

演唱會突發

約 10 分鐘內來自單一街區約 10 萬筆請求100,000 ÷ 600 秒 ≈ 單一區域每秒 170 次媒合

依地理區域對媒合佇列分片——尖峰只讓一個區域的消費者飽和,而不是整座城市。

04

邀約預算

60 秒內媒合;每位被邀約的司機約有 10 秒回應60 秒 ÷ 10 秒 ≈ 在期限前試 5-6 位司機

10 秒的邀約鎖定,限制了你在這一分鐘內能嘗試幾位司機。所以要把候選人排好順序,而不是亂灑邀約。

05

幽靈司機清理

位置條目在最後一次 ping 之後 15-30 秒過期——以 5 秒的節奏算,就是漏掉 3-6 次 ping15-30 s ÷ 每次 ping 5 s = 3-6 個沉默區間 → 條目過期;當機的司機最糟在約 30 秒內就不再收到邀約

當機或離線的司機會自己從索引中消失——不需清理工作,也不會把邀約發給幽靈。

決策範例

數字

這套系統扛著兩種非常不同的負載。司機以每秒約 2M 次位置寫入串流進來。而每次配對只需要一分鐘內附近的少數幾個候選人。

我的選擇

把司機位置放在一個帶短 TTL 的記憶體地理索引裡(geohash/H3 分格),這樣停止 ping 的司機就自然消失。配對拉出附近的候選人,一次把車派給一位司機,並把那位司機鎖住大約 10 秒。接受就贏得這趟;逾時或拒絕就釋放鎖,下一位候選人拿到邀約。尖峰期間,叫車請求在一個依區域分區的佇列裡等,所以一座散場的體育館拖慢的是那一帶,而不是整座城市。

避免

我不會做的事:把每一次位置 ping 都寫進主資料庫、再掃它來找附近的司機。在每秒 2M 次寫入下,這加上那些掃描會整個垮掉。我也不會在沒有鎖的情況下一次把同一趟車派給好幾位司機,因為兩個接受可能一起到,兩位司機都以為自己拿到了單。而且我不會第一天就把配對系統分片。先量每個區域的速率;太早分片只會加故障模式,不會加你真正需要的容量。

何時改變

如果配對品質比速度更重要,像是共乘或批次配對,我會先收集幾秒的請求,再以小批次配對,而不是一次配一個。

03

架構路徑

先看一張完整的圖,再把每條路徑各自畫成一張圖 —— 寫入路徑與讀取路徑承載不同流量,合理化不同的元件。

完整全貌

總覽 —— 每個元件

Rider / DriverAppsAPI GatewayRide ServiceMatching ServiceGeo Index(記憶體內)Ride DB(狀態)行程生命週期NotificationService邀約 → 司機Mapping Provider車資 + ETA

司機 ping 直接流進記憶體內的地理索引;媒合從中讀取附近的候選人,並一次一位司機地邀約行程。行程的真相活在資料庫裡——索引是可拋棄的。

路徑 1

請求路徑——估算、叫車、媒合

Rider AppRide ServiceMatching ServiceGeo Index(附近司機)Driver Offer(10秒)

先車資,再叫車。媒合撈出少數幾位附近的候選人,一次邀約一位司機;這個 10 秒的鎖防止重複派遣,並在司機忽略邀約時自動往下走。

路徑 2

位置路徑——寫入洪流

Driver AppAPI GatewayLocation ServiceGeo Index(geohash/H3)TTL Eviction

每秒約 200 萬次 ping 落在記憶體裡,分桶進 geohash/H3 cell;條目在 15-30 秒後過期,所以停止 ping 的司機會自己消失。

04

API 與資料模型

在最佳化之前,先讓契約可被檢視:端點、實體、擁有權、重試與狀態。

POST/fare

請求{ pickup, destination }

回應200 { fare_id, estimated_fare, eta }

呼叫地圖供應商取得路線 + ETA;估算會被存起來,讓叫車請求可以引用它。

POST/rides

請求{ fare_id }

回應201 { ride_id, state: requested } · 404 fare 已過期

啟動媒合;乘客會被推播媒合結果(或自行輪詢)。

POST/drivers/location

請求{ lat, lng }

回應200

司機身分來自 session token,絕不來自請求主體。高頻寫入直接進入地理索引。

PATCH/rides/{ride_id}

請求{ accept | decline }

回應200 ride · 409 邀約已過期

接受會原子性地翻轉邀約;拒絕或 10 秒逾時會釋放司機,下一位候選人拿到邀約。

核心實體

Rider

rider_id (PK) · payment_profile

Driver

driver_id (PK) · vehicle · status: offline/available/offered/on_trip

status 欄位就是防重複派遣的守門員:只有 available 的司機能收到邀約,而這個翻轉是原子的。

Fare

fare_id (PK) · pickup · destination · estimated_fare · eta

在估算時建立;叫車請求引用 fare_id,讓報出的價格不會被悄悄更動。

Ride

ride_id (PK) · rider_id · driver_id · fare_id · state: requested/matched/in_progress/completed

DriverLocation

driver_id · lat/lng · updated_at

活在記憶體內的地理索引(帶 TTL 的 geohash/H3 cell)裡,不在關聯式儲存中——它是可拋棄的資料。

05

深入方向

在面試最後三分之一挑一條路線。每條路線給你主題、它該回答的面試官問題,以及要避免的失敗模式。

重點

位置洪流

1,000 萬名司機每 5 秒 ping 一次。每秒 200 萬次寫入去哪裡,你又如何回答「誰在這個上車點附近」?

回答

定位 ping 進的是記憶體內的地理索引,絕不是關聯式資料庫。它吃不下每秒 2 百萬筆寫入。把每個位置分桶進一個 geohash 或 H3 cell,配短 TTL。要找誰在上車點附近,就讀那個 cell 加它的 8 個鄰居——九次記憶體查找,個位數毫秒,不是全表掃描。

避免

把 ping 寫進主資料庫再掃描鄰近——在這個速率下它會掛掉。

重點

絕不重複派遣同一位司機

兩筆叫車請求在同一瞬間都要同一位附近的司機。如何讓剛好一個贏?

回答

用一個短暫的獨占鎖抓住司機。它是一次原子的 compare-and-set,所以剛好一個請求贏,輸的那個換去它的下一個候選。資料庫保管真正的行程狀態:接受會把司機翻成 on-ride,取消則把他釋放回 available,不讓任何過時的 hold 殘留。

避免

沒有鎖就同時邀約多位司機——兩個接受都以為自己贏了。

重點

司機不回應

被邀約的司機忽略了請求。第 10 秒時發生什麼,行程又如何仍在一分鐘內媒合成功?

回答

這個媒合邀約是一個 10 秒的鎖,不是一個阻塞式的等待。到第 10 秒它自己過期,媒合就換到下一個排名的司機。每個約 10 秒,60 秒的預算裡塞得下 5、6 個司機。這就是為什麼你要仔細排候選的順序,而不是把邀約到處亂灑。

避免

讓一位沒回應的司機卡住整趟行程,而不是用一個會往下走的鎖 TTL。

重點

一座體育場散場

幾分鐘內 10 萬人從單一街區叫車。你如何讓城市的其他部分不受影響?

回答

把媒合佇列依地理區分區。10 分鐘 10 萬個請求,其實只是每秒約 170 次媒合。它會塞爆那一區的 consumer,而其他每一區都正常消化。熱區看到的是誠實的排隊——等久一點,或一個明確的失敗。城市其他地方運作正常。

避免

單一全域媒合佇列——一個局部尖峰變成全市性的故障。

重點

幽靈司機

一個司機 App 在標記為 available 時當機。他們還能收到邀約多久,又是什麼把他們清掉?

回答

每一筆位置紀錄在最後一次 ping 後 15 到 30 秒過期。以 5 秒的頻率算,那只是漏掉 3 到 6 次 ping,所以當機的司機會自己從索引裡掉出去。不需要清理工作排程。一個幽靈最久能繼續收到邀約的時間大約是 30 秒。

避免

位置條目沒有 TTL——邀約一直發給一小時前就離開的司機。

準備好練習了嗎?

把 Uber (Ride Hailing) 大聲講一遍,讓 AI 為你的說明評分。

用 AI 練習這題 →