譯者 | 陳峻
審校 | 重樓
您也許沒有注意到,數(shù)據(jù)建模錯(cuò)誤往往是導(dǎo)致系統(tǒng)性能出現(xiàn)問題的根本原因之一。人們往往簡單地認(rèn)為,只需根據(jù)應(yīng)用的訪問模式,對(duì)NoSQL數(shù)據(jù)建模即可。然而,事實(shí)上,在真正處理對(duì)于性能極其敏感的工作負(fù)載時(shí),應(yīng)用就很容易出現(xiàn)性能瓶頸。
例如:如果您的數(shù)據(jù)建模從根本上就效率低下的話,那么一旦被擴(kuò)展到某個(gè)臨界點(diǎn),應(yīng)用的性能就會(huì)受到嚴(yán)重影響。即使您在強(qiáng)大的基礎(chǔ)架構(gòu)上采用了最快的數(shù)據(jù)庫,也無法充分利用其潛力。本文將和您探討三種影響NoSQL數(shù)據(jù)庫性能的最常見建模方式,以及避免或解決這些問題的技巧。
不處理大分區(qū)(Large Partition)
在您的開發(fā)團(tuán)隊(duì)著手擴(kuò)展分布式數(shù)據(jù)庫時(shí),大分區(qū)通常會(huì)隨之出現(xiàn)。此處的大分區(qū)是指:那些可能會(huì)在整個(gè)集群的副本中,產(chǎn)生性能問題的過大分區(qū)。具體而言,它往往取決于:
- 延遲預(yù)期:通常,分區(qū)越大,檢索所需的時(shí)間也就越長。為此,我們需要考慮頁面的大小、以及完整掃描一個(gè)分區(qū)所需的客戶端與服務(wù)器往返交互的次數(shù)。
- 平均載荷大?。狠^大的載荷通常會(huì)導(dǎo)致較高的延遲。畢竟,它們需要更多的服務(wù)器端處理時(shí)間,來進(jìn)行序列化和反序列化,同時(shí)也會(huì)產(chǎn)生更高的網(wǎng)絡(luò)數(shù)據(jù)傳輸開銷。
- 工作負(fù)載需求:有時(shí)候,一些工作負(fù)載需要比平時(shí)用到更大的載荷。例如,Web3區(qū)塊鏈公司可能會(huì)將多個(gè)交易存儲(chǔ)為單鍵之下的BLOB,其每個(gè)鍵的大小就很容易超過1兆字節(jié)。
- 從分區(qū)中讀取數(shù)據(jù)的方式:例如,時(shí)間序列用例通常會(huì)包含時(shí)間戳聚類組件。在這種情況下,與掃描整個(gè)分區(qū)相比,僅從特定時(shí)間窗口讀取的數(shù)據(jù)就少得多。
鑒于上述方面,下表展示了大分區(qū)在不同載荷大?。ㄈ?/span>1、2和4KB)下的影響。
可以看出,在相同行數(shù)的情況下,負(fù)載越高,分區(qū)就越大。不過,如果您的應(yīng)用需要經(jīng)常掃描整體分區(qū)的話,那么請(qǐng)注意對(duì)數(shù)據(jù)庫予以限制,以防止內(nèi)存被無限制地消耗。例如,ScyllaDB在每隔1MB就會(huì)切分開不同頁面。這正是為了防止系統(tǒng)內(nèi)存的耗盡。其他數(shù)據(jù)庫(甚至是關(guān)系型數(shù)據(jù)庫)也有類似的保護(hù)機(jī)制,以防止無限制的查詢,導(dǎo)致數(shù)據(jù)庫資源的枯竭。
若要使用ScyllaDB檢索大小為4KB和1萬行數(shù)的負(fù)載,您需要檢索至少40頁,才能通過單次查詢掃描完整個(gè)分區(qū)。起初,這似乎不是什么大問題。但是,隨著時(shí)間的推移,客戶端的整體延遲會(huì)逐漸受到影響。
另一個(gè)值得考慮的因素是:在ScyllaDB和Cassandra等數(shù)據(jù)庫中,寫入數(shù)據(jù)庫的數(shù)據(jù)往往會(huì)被存儲(chǔ)在提交日志(commit log)和名為“memtable”的內(nèi)存數(shù)據(jù)結(jié)構(gòu)中。
如上圖所示,提交日志是一個(gè)提前寫入的日志,除非服務(wù)器崩潰或服務(wù)中斷,否則它不會(huì)被真正地讀取到。由于memtable存在于內(nèi)存中,因此最終它會(huì)被填滿。因此,為了釋放內(nèi)存空間,數(shù)據(jù)庫會(huì)將內(nèi)存表刷到磁盤上。這一過程會(huì)產(chǎn)生排序字符串表(Sorted Strings Tables,SSTables),這就是數(shù)據(jù)被持久化的過程。
那么這些又與大分區(qū)有何關(guān)系呢?實(shí)際上,SSTables有著一些需要在數(shù)據(jù)庫啟動(dòng)時(shí),保存在內(nèi)存中的特定組件。它們可以確保讀取效率,并在查找數(shù)據(jù)時(shí)盡量減少存儲(chǔ)磁盤I/O的浪費(fèi)。因此,當(dāng)您擁有超大分區(qū)時(shí)(例如,2.5Terabyte的分區(qū)),這些SSTable組件就需要減少沉重的內(nèi)存壓力,從而縮小數(shù)據(jù)庫的緩存空間,進(jìn)一步限制延遲。
具體而言,我們?cè)撊绾瓮ㄟ^數(shù)據(jù)建模,來解決大分區(qū)問題呢?通常,我們可以從主鍵入手。畢竟主鍵決定了數(shù)據(jù)在集群中的分布方式,可以被用來提高性能和資源利用率。如您所知,一個(gè)好的分區(qū)鍵應(yīng)該具有基數(shù)性(Cardinality)且能夠大致均勻地分布。例如,User Name、User ID或Sensor ID等基數(shù)性較高的屬性,都可能是很好的分區(qū)鍵。而像“State(州)”這樣的屬性則不太合適,畢竟像加利福尼亞州和德克薩斯州這樣的州,可能會(huì)比懷俄明州和佛蒙特州之類人口較少的州,擁有更多的數(shù)據(jù)。
讓我們來看一個(gè)例子。下表可被用于帶有多個(gè)傳感器的分布式空氣質(zhì)量監(jiān)測系統(tǒng):
CREATE TABLE air_quality_data (
sensor_id text,
time timestamp,
co_ppm int,
PRIMARY KEY (sensor_id, time)
);
由于time是該表格的聚類鍵(clustering key),因此不難想象,每個(gè)傳感器的分區(qū)可能會(huì)變得非常大,尤其是在每幾毫秒就收集一次數(shù)據(jù)的情況下。長此以往,這個(gè)看似無害的表最終會(huì)變得大到無法使用。而在本例中,我們只需要約 50 天。
一個(gè)標(biāo)準(zhǔn)的解決方案是修改數(shù)據(jù)模型,以減少每個(gè)分區(qū)鍵的聚類鍵數(shù)量。下面,讓我們來看看更新后的air_quality_data表:
CREATE TABLE air_quality_data (
sensor_id text,
date text,
time timestamp,
co_ppm int,
PRIMARY KEY ((sensor_id, date), time)
);
完成更改后,一個(gè)分區(qū)僅保存一天內(nèi)收集的數(shù)據(jù)值。這樣就不容易溢出了。由于它允許我們控制分區(qū)中存儲(chǔ)的數(shù)據(jù)量,因此這種技術(shù)被稱為“分桶(Bucketing)”。如果您對(duì)此方面感興趣的話,請(qǐng)參考鏈接--https://discord.com/blog/how-discord-stores-trillions-of-messages,以了解如何應(yīng)用分桶技術(shù),來避免出現(xiàn)大分區(qū)。
引入熱點(diǎn)(Hot Spot)
如果您有一個(gè)大分區(qū)(即存儲(chǔ)了絕大部分的數(shù)據(jù)集),那么您的應(yīng)用訪問模式,很可能會(huì)更頻繁地訪問該分區(qū)。因此,當(dāng)有問題的數(shù)據(jù)訪問模式,導(dǎo)致集群中的數(shù)據(jù)訪問方式失衡時(shí),就會(huì)出現(xiàn)熱點(diǎn)。此類熱點(diǎn)很可能會(huì)給大分區(qū)帶來副作用。而其中一個(gè)罪魁禍?zhǔn)拙驮从冢簯?yīng)用程序未能在客戶端采取任何限制,以至于允許“租戶”側(cè)對(duì)給定的鍵進(jìn)行“大肆”訪問。
例如,某個(gè)消息應(yīng)用中的機(jī)器人經(jīng)常在某個(gè)頻道中發(fā)送大量信息。那么,不穩(wěn)定的客戶端配置就會(huì)以重試風(fēng)暴(Retry Storms)的形式引入熱點(diǎn)。也就是說,客戶端在嘗試著查詢特定數(shù)據(jù)時(shí),會(huì)在數(shù)據(jù)庫超時(shí)之前,以及在數(shù)據(jù)庫仍在處理上一次查詢的同時(shí),進(jìn)行反復(fù)地重試。
通過監(jiān)控儀表盤,您可以輕松地找到集群中的熱點(diǎn)。如下圖所示,該儀表盤顯示了讀取量過大的20個(gè)碎片。
再比如,下圖展示了三個(gè)利用率較高的分片,它們都與針對(duì)鍵空間配置的三個(gè)復(fù)制因子有關(guān)。
由上圖可知,大量的讀寫傳播,給碎片 7 帶來了更高的負(fù)載。
那么,我們?cè)撊绾谓鉀Q熱點(diǎn)問題呢?首先,我們可以在其中一個(gè)受影響的節(jié)點(diǎn)上,對(duì)最常被命中的鍵進(jìn)行取樣。同時(shí),您也可以使用概率跟蹤(Probabilistic Tracing)等跟蹤工具,來分析有哪些查詢會(huì)命中哪些分片,以便后續(xù)采取行動(dòng)。一旦發(fā)現(xiàn)了熱點(diǎn),我們就應(yīng)當(dāng)考慮如下方面:
- 審查應(yīng)用程序的訪問模式。您可能會(huì)發(fā)現(xiàn)前面提到的桶技術(shù)等需要改變的數(shù)據(jù)模型。如果需要重新排序,您可以使用諸如Snowflake之類的單調(diào)遞增組件,或是采用并發(fā)限制器,來限制不良因素。
- 指定每個(gè)分區(qū)的速率限制,以便數(shù)據(jù)庫拒絕那些訪問同一分區(qū)的查詢。
- 確??蛻舳顺瑫r(shí)高于服務(wù)器端超時(shí),以防止客戶端在服務(wù)器有機(jī)會(huì)處理之前,不斷地發(fā)出重試查詢(也就是“重試風(fēng)暴”)。
濫用集合
雖然不常被使用,但是團(tuán)隊(duì)往往無法恰當(dāng)?shù)厥褂眉?。集合是指那些用于存?chǔ)和規(guī)范化相對(duì)較小的數(shù)據(jù)量。由于被存儲(chǔ)在單個(gè)單元格中,因此它們序列化與反序列化的成本極高。
在使用集合時(shí),我們可以定義相關(guān)問題字段為:凍結(jié)和非凍結(jié)。凍結(jié)的集合只能作為一個(gè)整體被寫入,不能向其中添加或刪除元素。而非凍結(jié)的集合則可以被追加,而這正是人們最常濫用的集合類型。而且,更糟糕的是,人們甚至可以嵌套集合。比如:讓一個(gè)映射包含另一個(gè)映射,而后者又包含了一個(gè)列表等結(jié)構(gòu)。
由于濫用集合會(huì)比大分區(qū)更快地帶來性能問題,因此集合不應(yīng)過大。例如,我們創(chuàng)建了一個(gè)簡單的鍵/值(key:value)表,其中的鍵是sensor_id,值是在一段時(shí)間內(nèi)記錄到的樣本集合。那么一旦我們開始捕獲數(shù)據(jù),性能就會(huì)逐漸下降。
CREATE TABLE IF NOT EXISTS {table} (
sensor_id uuid PRIMARY KEY,
events map<timestamp, FROZEN<map<text, int>>>,
)
下圖的監(jiān)控快照顯示了,該應(yīng)用同時(shí)向集合追加多個(gè)項(xiàng)目時(shí)發(fā)生的情況。
如您所看,在整體吞吐量降低的同時(shí),99%的延遲卻在增加。其根本原因就可能來自如下方面:
- 集合單元以分類矢量的形式存儲(chǔ)在內(nèi)存中。
- 添加元素需要合并兩個(gè)新舊集合。
- 添加元素的成本與整個(gè)集合的大小成正比。
- 樹(Tree,非矢量Vector)雖然可以提高性能,但是會(huì)降低小型集合的效率。
回到上述例子,由于不再需要向其追加數(shù)據(jù),我們的解決方法是將時(shí)間戳移至聚類鍵,并將映射轉(zhuǎn)換為凍結(jié)的集合。如下代碼所示,如下面這樣非常簡單的更改,就能大幅提高用例的性能。
CREATE TABLE IF NOT EXISTS {table} (
sensor_id uuid,
record_time timestamp,
events FROZEN<map<text, int>>,
PRIMARY KEY(sensor_id, record_time)
)
譯者介紹
陳峻(Julian Chen),51CTO社區(qū)編輯,具有十多年的IT項(xiàng)目實(shí)施經(jīng)驗(yàn),善于對(duì)內(nèi)外部資源與風(fēng)險(xiǎn)實(shí)施管控,專注傳播網(wǎng)絡(luò)與信息安全知識(shí)與經(jīng)驗(yàn)。
原文標(biāo)題:NoSQL Data Modeling Mistakes that Ruin Performance,作者:Felipe Cardeneti Mendes