進階閱讀時間 3 分鐘

DynamoDB 的請求路由是如何工作的

你發出的每一次讀取或寫入,都會先命中一隊無狀態的請求路由器。 路由器對你的 做雜湊,把雜湊值對映到擁有該鍵資料的儲存節點, 再把請求轉發過去。正是這一跳,讓一次按鍵查詢的成本保持一致 —— 無論表裡存的是一千個項還是十億個項。

DynamoDB 的請求路由是如何工作的?

DynamoDB 會將每個請求都經由一隊無狀態的請求路由器轉發:路由器對你的 做雜湊,把雜湊值對映到擁有該分割區的那一個儲存節點,再把讀取或寫入轉發過去。路由是鍵雜湊的純函式,因此一次查詢的成本保持一致 —— 無論表裡存的是一千個項還是十億個項。

  • 請求路由器是入口大門。 它是一隊無狀態的路由器,接收你的 請求,對分割區索引鍵做雜湊,然後把它路由到持有該分割區的儲存節點 —— 無需掃描,也無需瞭解整張表。
  • 分割區索引鍵決定一切。 路由是分割區索引鍵雜湊的 純函式 —— 同一個鍵始終路由到擁有它的分割區,所以 GetItem 是 O(1),而不是 O(表大小)。
  • 一主兩副。 一次寫入落在該分割區的主節點上, 主節點在法定多數(三個副本中的兩個)持久化之後確認寫入。
  • 糟糕的鍵會擊垮這套設計。 一個低基數或 鍵會把 流量灌向單個節點 —— 路由本身沒問題,問題出在你的鍵上。

先從路由要解決的問題說起

從 SQL 過來,你腦中浮現的是一個查詢規劃器:它讀取統計資訊、挑選索引, 也許還會掃描。成本隨它觸及的資料量而增長。這套模型並不適合 一個必須在任意規模下都以個位數毫秒作答的鍵值儲存。

DynamoDB 的答案是把單項查詢變成一次直接定址,而不是一次 搜尋。分割區索引鍵不是一個你用來過濾的列 —— 它是一個雜湊 函式的輸入,用來算出_資料在物理上存放在哪裡_。沒有統計資訊,也沒有規劃器。

這就是你從關係式思維遷移出來時所接受的權衡:你放棄了 臨時查詢的靈活性,換來了常數時間的定址。

認識請求路由器

請求到達時,並不會直奔儲存。它會先命中一個請求 路由器 —— 一隊無狀態、可橫向擴充套件的路由器,擋在整個服務的最前面。 (USENIX ATC '22 的 DynamoDB 論文描述了這隊請求路由器。

路由器做三件事,自身不持有任何資料:

  • 對照 IAM 鑑權與授權該請求。
  • 對分割區索引鍵做雜湊以找到擁有它的分割區。
  • 把請求轉發給該分割區的儲存節點。

因為路由器是無狀態的,服務會在負載上升時增加更多路由器。它們中沒有一個是 瓶頸,也沒有一個是單點故障 —— 這正是 2007 年 Amazon Dynamo 論文 圍繞原始系統所構建的那個特性。

跟隨一次讀取穿過路由器

設想一張記錄無人機機隊遙測資料的表。項以 DroneId(分割區 鍵)和 ReadingTs(排序索引鍵)為鍵,帶有 BatteryPctAltitudeM 等屬性。

你請求某架無人機在 6 月 23 日的讀數:

PK = "DRONE#A19F"
SK begins_with "2026-06-23"

下面是路由器對它做的處理。下方的引導文字自上而下地追蹤這個請求 —— 請把它當作一條向下流動的路徑來讀。

用戶端:QueryPK = DRONE#A19F請求路由器(無狀態叢集)Hash(DRONE#A19F) 鍵空間槽位把槽位對映到擁有該鍵的分割區該分割區的主節點讀取項DRONE#A19F

路由器對 DRONE#A19F 做雜湊,把它對映到擁有該鍵的分割區,再把 讀取轉發給該分割區的主儲存節點,由它返回項。

關鍵洞見:雜湊指向的是_一個_分割區,而無論這張表有 多少個分割區。路由器從不檢視其他分割區,所以增加無人機 —— 以及分割區 —— 並不會拖慢這次查詢。

弄清分割區到底是什麼

分割區是儲存與吞吐量的單位。每個分割區都有上限(大約 10 GB 加上讀/寫容量的一個固定切片),當分割區超出任一上限時,DynamoDB 就會 拆分它。帶有某個給定分割區索引鍵的每個項一開始都落在一個 分割區上;隨後的按熱度拆分可以按排序索引鍵範圍切分這個集合(除非 有 LSI 或單調遞增的排序索引鍵把它釘住),這正是讓針對 同一個分割區索引鍵的 Query 保持廉價的原因。

每個分割區都被複制到分散在多個可用區的三個儲存節點上:一個主節點 和兩個副節點

節點角色處理可提供的一致性
主節點所有寫入;強一致讀取強(能看到自己的最新寫入)
副節點最終一致讀取;故障轉移最終(可能落後於主節點)

一次寫入交給主節點,主節點在法定多數(三個 副本中的兩個)持久化之後確認寫入。一次讀取會被路由到主節點, 因此它反映最新寫入。一次讀取可能由 一個尚未追上的副節點來提供 —— 成本減半,可能讀到舊值。

點名一個陷阱:熱分割區索引鍵

路由的好壞,取決於你的分割區索引鍵。雜湊會把鍵均勻鋪開,所以如果 你的鍵具有高基數且流量均勻,負載就會分散到所有 節點上。破壞其中任一屬性,你就會得到一個熱分割區

假設你用 Region 而不是 DroneId 作為那張遙測表的鍵。現在 us-east-1 裡的每架無人機都共享同一個分割區索引鍵 —— 於是它們的讀寫會雜湊到同一個 鍵空間槽位,全都堆到同一個項集合上。路由器仍在完美地 履行它的職責;只是你把整個機隊都灌進了單個分割區的容量裡。

你沒法盯著路由器挑選節點,但你_可以_設計出路由良好的鍵。 當你在 運算式構建器裡構建一個鍵條件時,你放在 PK = … 左側的分割區索引鍵,正是路由器將要雜湊的那個值 —— 讓這個 值保持高基數,正是讓讀取落在不同節點上的關鍵。

這如何回扣到你的訪問模式

請求路由正是讓單表設計 規則不容商量的那套機制:你圍繞分割區索引鍵建模,因為分割區索引鍵 _就是_地址。這也是為什麼 Query 勝過 Scan —— Query 經由路由器命中一個分割區,而 Scan 會依次走遍每一個 分割區。

次要索引擁有各自的分割區和各自的路由:一個 GSI 由它自己的分割區索引鍵路由,與基表的分割區索引鍵無關, 這正是為什麼即便表不熱,GSI 也可能變熱。

後續步驟

設計出能路由到多個節點、而非單個節點的鍵。在 運算式構建器裡草擬 PK = … 條件,看清究竟是哪個值 會被雜湊,然後下載 DynoTable,針對你自己的 表執行那些查詢,看清每個鍵條件究竟返回什麼。

已更新