中階閱讀時間 3 分鐘

DynamoDB 熱分割

DynamoDB 會把資料分散到許多實體分割上,每個分割都有自己的一份輸送量。熱分割指的是某個鍵吸引到的讀取或寫入,遠超過它那一份所能服務的量——於是針對該鍵的請求遭到節流,而表的其餘部分卻閒置著。

什麼是 DynamoDB 熱分割?

DynamoDB 熱分割指的是某個分割鍵吸收到的讀取或寫入,遠超過它那一份輸送量所能服務的量——於是針對該鍵的請求遭到節流,而表的其餘部分卻閒置著。根因在於鍵的設計——一個名人項目、一個低基數的鍵、今天的日期——而不是表的大小。解法是把寫入分散開來。

  • 根因是鍵的設計,不是表的大小。 某個分割鍵把流量集中起來——一位名人使用者、一個 status="OPEN" 旗標、今天的日期——這就是陷阱。
  • 自適應容量有幫助,但它不是解方。 DynamoDB 會自動重新平衡熱度,然而單一項目或單一鍵仍可能超過一個分割所能服務的量。
  • 解法是把寫入分散開來。 在鍵裡加入亂度(寫入分片),或把熱讀取路徑改到分佈得更好的存取模式上。
  • 從 SQL 過來的人,這沒有對應概念。 關聯式表沒有「某一列的索引值太熱門」這種說法——DynamoDB 那種扁平的「每鍵輸送量」模型才有。

為什麼一開始就要有分割

DynamoDB 是 2007 年 Amazon Dynamo 論文的生產級後繼者,那篇論文用一個分割化、水平擴展的模型,換掉了單節點的 SQL 模型。資料依分割鍵的雜湊值,跨實體儲存節點分片。

每個分割都持有有上限的資料量,並服務有上限的輸送量。AWS 明文記載了一個硬上限:每個分割每秒 3,000 個讀取單位與 1,000 個寫入單位AWS — 分割行為)。

那個上限就是整個故事的全部。你的表佈建的輸送量是所有分割的總和——但任何一個鍵永遠只會落在一個分割上。

點出陷阱:流量堆在一個鍵上

輸送量被平均分享,只有在你的存取均勻分散到各個鍵上時才成立。一旦某個鍵拿到過高比例的流量,它就會獨自遭到節流,而表的整體容量卻沒被用上。

典型的熱鍵樣貌:

  • 名人項目——一位人人都讀的使用者、產品或租戶。
  • 低基數的分割鍵——statuscountrytype。不同值很少,意味著只有少數分割在扛全部的工作。
  • 時間分桶的鍵——PK = "2026-06-23"。今天的每一次寫入都猛擊同一個分割;昨天的那個則永遠冷掉。

從 SQL 過來,這些都無關緊要。一棵 B-tree 索引落在熱門值上沒問題。在 DynamoDB 裡,那個熱門值就是實體放置的單位,所以熱門度變成了一道輸送量懸崖。

一個實作範例:名人排行榜

假設你經營一個全球遊戲排行榜。分數存在一張這樣設鍵的表裡:

PK = "BOARD#global"
SK = "PLAYER#<playerId>"

讀取抓出分數最高的前 N 名;寫入在每場比賽後更新某位玩家的 currentScore。全球排行榜裡的每一列共用同一個分割鍵——BOARD#global——所以每一次讀取和寫入都落在單一分割上。

再加進一位有兩百萬名即時觀眾的直播主,他們狂按重新整理來看自己的排名,那一個分割就會衝破 3,000 個讀取單位。你會在全球排行榜上拿到 ProvisionedThroughputExceededException,而表裡其他每個排行榜都閒著。

地雷就是 BOARD#global 這個崩塌:你把一個邏輯上的單一排行榜,建模成了一個實體上的單一鍵。

把寫入分散開來:對鍵做分片

解法是製造出基數。在分割鍵後面接一個分片後綴,讓一個邏輯排行榜扇出到 N 個實體分割:

PK = "BOARD#global#<shard>"  -- shard = playerId mod 10
SK = "PLAYER#<playerId>"

寫入現在散布到十個分割,而不是一個——十倍的寫入餘裕。代價是:讀取整個排行榜時,必須打到全部十個分片再合併,因為沒有任何單一 Query 能跨分片邊界。你用讀取的單純性換來寫入的分佈。

AWS 稱此為寫入分片(write sharding),並正是針對高速、低基數的鍵推薦這麼做(AWS — 使用寫入分片)。

這跟單表設計背後那種鍵超載的直覺一模一樣——你是依存取模式來塑形鍵,而不是依資料「自然」該怎麼擺。

讓自適應容量去做簡單的那部分

DynamoDB 內建自適應容量(adaptive capacity),在 re:Invent 2018 場次「Amazon DynamoDB Under the Hood」(DAT401)中有介紹。它會持續把表的輸送量重新分配到正在受熱的分割上,並會把一個持續發熱的鍵隔離到它自己的分割(鍵層級隔離,AWS — 突發與自適應容量)。

它即時且免費——但它被物理定律限制著。自適應容量能在各個鍵之間移動熱度;它無法把單一鍵推過每分割的上限。一個真正的名人鍵仍然會節流。分片才是帶你越過那道牆的辦法。

一旦你在某個繁忙的鍵上看到節流,決策路徑如下:

否,多個鍵同一前綴單一鍵發生節流?單一項目太熱?對鍵分片或快取讀取低基數分割鍵?對前綴做寫入分片自適應容量多半能處理

大多數熱分割最後不是落在「對鍵分片」就是「讓自適應容量吸收掉它」——圖只是告訴你你在哪一個分支上。

重新設計之前先診斷

看不見的東西修不了。節流會以 ProvisionedThroughputExceededException(佈建模式)的形式出現,或在 CloudWatch 裡以 ThrottledRequests 與每分割的 ThrottleCount 出現(AWS — CloudWatch 指標)。

把這個搭配 CloudWatch Contributor Insights for DynamoDB 一起看,它會直接把你存取最頻繁的鍵排名出來——是依名稱確認名人鍵的最快辦法(AWS — Contributor Insights)。

當你在測試分片後的讀取路徑時,你會為每個分片手刻 KeyConditionExpression。用 DynamoDB Expression Builder 把它們生成出來而不打錯字——它會為每個分片產出確切的 PK = :pk AND begins_with(SK, :sk) 形狀。

要避開的陷阱

  • 自動遞增或單調遞增的鍵。 把序列 ID 和時間戳當分割鍵,會把連續的寫入導向同一個分割。加一個雜湊前綴。
  • 沒必要地對讀取繁重的路徑分片。 如果讀取為主且項目很小,一個快取或一個帶有更佳分佈鍵的 GSI,往往勝過分片那種分散收集的讀取成本。
  • 把熱分割和慢 Scan 搞混。 Scan 慢是因為它讀了所有東西;熱分割節流是因為某個鍵超載了。是不同的問題——見 Query 與 Scan

下一步

把分片後的鍵草擬出來,再用真實資料驗證讀取路徑。在 DynamoDB Expression Builder 裡建好每個分片的條件,然後下載 DynoTable,對你自己的表執行它們,看看實際是哪些分割在受熱。

已更新