資料偏移、分割槽陷阱……我們這樣避開DynamoDB的5個坑

TalkingData發表於2019-10-15

DynamoDB 是 Amazon 基於《 Dynamo: Amazon’s Highly Available Key-value Store 》實現的 NoSQL 資料庫服務。它可以滿足資料庫無縫的擴充套件,可以保證資料的永續性以及高可用性。開發人員不必費心關注 DynamoDB 的維護、擴充套件、效能等一系列問題,它由 Amazon 完全託管,開發人員可以將更多的精力放到架構和業務層面上。

本文主要介紹作者所在團隊在具體業務中所遇到的挑戰,基於這些挑戰為何最終選型使用 Amazon DynamoDB,在實踐中遇到了哪些問題以及又是如何解決的。文中不會詳細討論 Amazon DynamoDB 的技術細節,也不會涵蓋 Amazon DynamoDB 的全部特性。

背景與挑戰

TalkingData 移動廣告效果監測產品(TalkingData Ad Tracking)作為廣告主與媒體之間的一個廣告投放監測平臺,每天需要接收大量的推廣樣本資訊和實際效果資訊,並最終將實際的效果歸因到推廣樣本上。

舉個例子,我們通過手機某新聞類 APP 瀏覽資訊,會在資訊流中看到穿插的廣告,廣告可能是文字形式、圖片形式、視訊形式的,而不管是哪種形式的廣告它們都是可以和使用者互動的。

如果廣告推送比較精準,剛好是使用者感興趣的內容,使用者可能會去點選這個廣告來了解更多的資訊。一旦點選了廣告,監測平臺會收到這一次使用者觸發的點選事件,我們將這個點選事件攜帶的所有資訊稱為樣本資訊,其中可能包含點選的廣告來源、點選廣告的時間等等。通常,點選了廣告後會引導使用者進行相關操作,比如下載廣告推薦的 APP,當使用者下載並開啟 APP 後,移動廣告監測平臺會收到這個 APP 發來的效果資訊。到此為止,對於廣告投放來說就算做一次成功轉化。
微信圖片_20191015164915.png

DynamoDB實踐:當資料量巨大而不可預知,如何保證高可用與實時性?

移動廣告監測平臺需要接收源源不斷的樣本資訊和效果資訊,並反覆、不停的實時處理一次又一次轉化。對於監測平臺來講,它的責任重大,不能多記錄,也不能少記錄,如果轉化資料記多了廣告主需要多付廣告費給媒體,記少了媒體會有虧損。這樣就為平臺帶來了幾大挑戰:

  • 資料量大:有的媒體為了利益最大化可能會採取非正常手段製造假的樣本,產生“虛假流量”,所以廣告監測平臺除了會收到真實使用者樣本外,也會收到大量造假的樣本,影響正常的監測和歸因。在最“瘋狂”的時候,我們的平臺會在一天內收到 40 億 + 的點選樣本事件請求。要知道,這些點選樣本事件是要保留下來作為後續效果歸因用的,而且樣本有效期大不相同,從最短 12 小時到最長 90 天不等。
  • 資料量不可預知:對於廣告主的大推、應用商店競價排名等一系列的推廣,會導致突發大量樣本資料流入。面對這些流量不可預知的情況,我們仍要保證系統的正確、穩定、實時。
  • 實時處理:廣告主依賴廣告監測平臺實時處理的結果來調整廣告推廣策略。因此廣告監測平臺需要支援資料實時處理,才能為廣告主更快的優化推廣策略提供有力支撐。與此同時,廣告監測平臺的處理結果也要實時回傳給媒體方以及廣告主。可以看到,準確性和實時性是系統必須要滿足的基本條件。
  • 樣本儲存:我們的業務最核心的功能就是歸因,我們要明確例如使用者下載開啟 APP 的這個轉化效果是由哪一個推廣活動樣本帶來的——也就是上圖中的第 7 步,當使用者安裝 APP 後,監測平臺要對應找到第 1 步中樣本所在推廣活動,這個是一個查詢匹配的過程。對於龐大的歸因樣本資料,有效期又各不相同,我們應該怎樣儲存樣本才能讓系統快速歸因,不影響實時結果,這也是一個很大的挑戰。

最初形態

在 2017 年 6 月前我們的業務處理服務部署在機房,使用 Redis Version 2.8 儲存所有樣本資料。Redis 使用多節點分割槽,每個分割槽以主從方式部署。在最開始我們 Redis 部署了多個節點,分成多個分割槽,每個分割槽一主一從。剛開始這種方式還沒有出現什麼問題,但隨著使用者設定的樣本有效期加長、監測樣本增多,當時的節點數量逐漸已經不夠支撐業務儲存量級了。如果使用者監測推廣量一旦暴增,我們系統儲存將面臨崩潰,業務也會癱瘓。於是我們進行了第一次擴容。

由於之前的部署方式我們只能將 Redis 的多個節點翻倍擴容,這一切都需要人為手動操作,並且在此期間我們想盡各種辦法來保護使用者的樣本資料。
微信圖片_20191015165202.png
DynamoDB實踐:當資料量巨大而不可預知,如何保證高可用與實時性?

這種部署方式隨著監測量的增長以及使用者設定有效期變長會越來越不堪重負,當出現不可預知的突發量時就會產生嚴重的後果。而且,手動擴容的方式容易出錯,及時性低,成本也是翻倍的增長。在當時由於機器資源有限,不僅 Redis 需要擴容,廣告監測平臺的一系列服務和叢集也需要進行擴容。

化解挑戰

經過討論和評估,我們決定將樣本處理等服務遷移到雲端處理,同時對儲存方式重新選型為 Amazon DynamoDB,它能夠滿足我們的絕大部分業務需求。經過結構調整後系統大概是下圖的樣子:
微信圖片_20191015165206.png
DynamoDB實踐:當資料量巨大而不可預知,如何保證高可用與實時性?

  • 應對資料量大且不可預知:我們的平臺需要接受推廣監測連線請求,並進行持久化用於後續的資料歸因處理。理論上來說系統進來多少廣告監測資料請求,DynamoDB 就能存多少資料,只需要一張表就可以儲存任意數量級的資料。不用關心 DynamoDB 擴容問題,在系統執行時我們感知不到儲存正在擴容。這也是 Amazon 官方宣稱的完全託管、無縫擴充套件。
  • 高可用:Amazon DynamoDB 作為儲存服務提供了極高的可用性,對於寫入 DynamoDB 的全部資料都會儲存到固態硬碟中,並且自動同步到 AWS 多個可用區,以達到資料的高可用。這些工作同樣完全由 Amazon DynamoDB 服務託管,使用者可以將精力放到業務架構及編碼上。
  • 實時處理:Amazon DynamoDB 提供了極高的吞吐效能,並且支援按秒維度配置任意級別的吞吐量。對於寫多讀少的應用可以將每秒寫入資料的數量調整成 1000 甚至更高,將每秒讀取的數量降低到 10 甚至更少。吞吐量支援使用者任意設定,在設定吞吐量時除了可以隨時在 Web 管理後臺調整外,還可以通過 DynamoDB 提供的客戶端動態調整。比如系統在執行時寫入能力不足了,我們可以選擇到 Web 管理後臺手動上調或在程式碼中通過呼叫客戶端 API 的方式實現自動上調。使用客戶端動態調整的方式會讓系統具備較高的收縮能力,同時還可以保證資料的實時處理,系統資料流量變高了就動態調整上去,資料流量變低了再動態調整下來。相比手動調整來看,動態調整的方式更為靈活。基於以上幾點,我們認為 Amazon DynamoDB 可以很輕鬆的支撐系統的核心業務能力。對於業務側需要做的就是,整理好業務邏輯把資料寫到 DynamoDB 就可以了,剩下的就交給 DynamoDB 去做。

此外還有:

  • TTL:我們利用了 Amazon DynamoDB 提供的 TTL 特性管理那些有生命週期的資料。TTL 是對錶中要過期的資料設定特定時間戳的一種機制,一旦時間戳過期 DynamoDB 在後臺會刪除過期的資料,類似於 Redis 中的 TTL 概念。藉助 TTL 的能力,我們減少了很多業務上不必要的邏輯判定,同時還降低了因儲存量帶來的成本。
  • 流:在我們的業務中沒有啟用流來捕獲表的動作,但我們認為 DynamoDB 流是一個非常好的特性,當儲存在 DynamoDB 表中的資料發生變更(新增、修改、刪除)時,通知到相關的服務 / 程式。比如我們修改了一條記錄的某個欄位,DynamoDB 可以捕獲到這個欄位的變更,並將變更前後的結果編寫成一條流記錄。

實踐出真知

我們在使用一些開源框架或服務時總會遇到一些“坑”,這些“坑”其實也可以理解為沒有很好的理解和應對它們的一些使用規則。DynamoDB 和所有服務一樣,也有著它自己的使用規則。在這裡主要分享我們在實際使用過程中遇到的問題以及解決辦法。

資料偏移

在 DynamoDB 中建立表時需要指定表的主鍵,這主要為了資料的唯一性、能夠快速索引、增加並行度。主鍵有兩種型別,「單獨使用分割槽鍵」作為主鍵和「使用分割槽鍵 + 排序鍵」作為主鍵,後者可以理解為組合主鍵(索引),它由兩個欄位唯一確定 / 檢索一條資料。DynamoDB 底層根據主鍵的值對資料進行分割槽儲存,這樣可以負載均衡,減輕單獨分割槽壓力,同時 DynamoDB 也會對主鍵值嘗試做“合理的”分割槽。

在開始我們沒有對主鍵值做任何處理,因為 DynamoDB 會將分割槽鍵值作為內部雜湊函式的輸入,其輸出會決定資料儲存到具體的分割槽。但隨著執行,我們發現資料開始出現寫入偏移了,而且非常嚴重,帶來的後果就是導致 DynamoDB 表的讀寫效能下降,具體原因在後面會做詳細討論。發現這類問題之後,我們考慮了兩種解決辦法:

微信圖片_20191015165206.png

所以我們選擇了第二種方法,調整業務程式碼,在寫入時將主鍵值做雜湊,查詢時將主鍵條件做雜湊進行查詢。

自動擴容潛規則

在解決了資料偏移之後讀 / 寫效能恢復了,但是執行了一段時間之後讀寫效能卻再次下降。查詢了資料寫入並不偏移,當時我們將寫入效能提升到了 6 萬 +/ 秒,但沒起到任何作用,實際寫入速度也就在 2 萬 +/ 秒。最後發現是我們的分割槽數量太多了,DynamoDB 在後臺自動維護的分割槽數量已經達到了 200+ 個,嚴重影響了 DynamoDB 表的讀寫效能。

DynamoDB 自動擴容、支援使用者任意設定的吞吐量,這些都是基於它的兩個自動擴容規則:單分割槽大小限制和讀寫效能限制。

單分割槽大小限制

DynamoDB 會自動維護資料儲存分割槽,但每個分割槽大小上限為 10GB,一旦超過該限制會導致 DynamoDB 拆分割槽。這也正是資料偏移帶來的影響,當資料嚴重偏移時,DynamoDB 會默默為你的偏移分割槽拆分割槽。我們可以根據下面的公式計算分割槽數量:
資料總大小 / 10GB 再向上取整 = 分割槽總數

比如表裡資料總量為 15GB,15 / 10 = 1.5,向上取整 = 2,分割槽數為 2,如果資料不偏移均勻分配的話兩個分割槽每個儲存 7.5GB 資料。

讀寫效能限制

DynamoDB 為什麼要拆分割槽呢?因為它要保證使用者預設的讀 / 寫效能。怎麼保證呢?依靠將每個分割槽資料控制在 10G 以內。另一個條件就是當分割槽不能滿足預設吞吐量時,DynamoDB 也會將分割槽進行擴充。DynamoDB 對於每個分割槽讀寫容量定義如下:

  • 寫入容量單位:寫入容量單位(WCU:write capacity units),以每條資料最大 1KB 計算,最大每秒寫入 1000 條。
  • 讀取容量單位:讀取容量單位(RCU:read capacity units),以每條資料最大 4KB 計算,最大每秒讀取 3000 條。

也就是說,一個分割槽的最大寫入容量單位和讀取容量單位是固定的,超過了分割槽最大容量單位就會拆分割槽。因此我們可以根據下面的公式計算分割槽數量:

(預設讀容量 /3000)+(預設寫容量 /1000)再向上取整 = 分割槽總數

比如預設的讀取容量為 500,寫入容量為 5000,(500 / 3000) + (5000 / 1000) = 5.1,再向上取整 = 6,分割槽數為 6。

需要注意的是,對於單分割槽超過 10G 拆分後的新分割槽是共享原分割槽讀寫容量的,並不是每個表單獨的讀寫容量。
因為預設的讀寫容量決定了分割槽數量,但由於單分割槽資料量達到上限而拆出兩個新的分割槽。
所以當資料偏移嚴重時,讀寫效能會急劇下降。

冷熱資料

產生上面的問題是由於我們一開始是單表操作。這樣就算資料不偏移,但隨著時間推移資料量越來越多,自然拆出的分割槽也越來越多。

因此,我們根據業務做了合理的拆表、設定了冷熱資料表。這樣做有兩大好處:

  1. 提升效能:這一點根據上面的規則顯而易見,熱表中資料量不會持續無限增長,因此分割槽也穩定在一定數量級內,保證了讀寫效能。
  2. 降低成本:無謂的為單表增加讀寫效能不僅效果不明顯,而且費用也會急劇增高,使用成本的增加對於誰都是無法接受的。DynamoDB 儲存也是需要成本的,所以可以將冷表資料儲存到 S3 或其他持久化服務中,將 DynamoDB 的表刪除,也是降低成本的一種方式。

表限制

表對於資料的大小以及數量並沒有限制,可以無限制的往一張表裡寫入資料。但對於 AWS 的一個賬戶,每個 DynamoDB 使用區域的限制為 256 張表。對於一個公司來說,如果共用同一個賬號的話可能會存在建立表受限的風險。所以如果啟用了冷熱表策略,除了刪冷表降低成本外,也是對 256 張表限制的一種解決辦法。

屬性名長度

上面提到了寫入單位每條資料最大 1KB、讀取單位每條最大 4KB 的限制。單條資料的大小除了欄位值佔用位元組外,屬性名也會佔用位元組,因此在保證可讀性的前提下應儘量縮減表中的屬性名。

總結

DynamoDB 的使用也是存在成本的,主要體現在寫入和讀取的費用。我們自己研發了一套按照實際流量實時調整讀、寫上限的策略。隨著發展 DynamoDB 也推出了 Auto Scaling 功能,它實現了自定義策略動態調整寫入與讀取上限的能力,對於開發者來說又可以省去了不少研發精力。目前我們也有部分業務使用了 Auto Scaling 功能,但由於該功能的限制,實際使用上動態調整的實時性略顯欠缺。


作者介紹:
史天舒,資深 Java 工程師,碩士畢業於北京郵電大學。任職於 TalkingData,目前從事移動廣告監測產品 Ad Tracking 相關架構設計與開發。喜歡研究程式碼,注重系統高擴充套件設計,略有程式碼潔癖。

相關文章