為什么數(shù)據(jù)庫(kù)調(diào)整大小如此困難?
為什么規(guī)劃數(shù)據(jù)庫(kù)容量如此困難?那么怎么簡(jiǎn)化?可以使用開源NoSQL數(shù)據(jù)庫(kù)ScyllaDB來(lái)演示示例。
調(diào)整數(shù)據(jù)庫(kù)的大小看起來(lái)很簡(jiǎn)單:用數(shù)據(jù)集的大小和所需吞吐量除以節(jié)點(diǎn)的容量。很容易,不是嗎?
如果有人曾經(jīng)嘗試規(guī)劃數(shù)據(jù)庫(kù)容量,就會(huì)知道這有多難。即使是做出粗略的估計(jì)也很具挑戰(zhàn)性。那么為什么這么難?
以下是估算集群大小的步驟:
(1) 對(duì)使用模式做出假設(shè)。
(2) 估計(jì)所需工作量。
(3) 決定數(shù)據(jù)庫(kù)的高級(jí)配置。
(4) 將工作負(fù)載、配置和使用模式提供給數(shù)據(jù)庫(kù)的性能模型。
(5) 收入。
這個(gè)流程雖然容易理解,但在實(shí)踐工作中卻不那么容易。
例如,在對(duì)數(shù)據(jù)庫(kù)配置(例如復(fù)制因子和一致性級(jí)別)做出決策時(shí),做出的決策會(huì)受到預(yù)先設(shè)想答案的影響。而當(dāng)成本變得非常昂貴時(shí),而進(jìn)行一些復(fù)制似乎有點(diǎn)過分。
將數(shù)據(jù)庫(kù)的規(guī)模調(diào)整看作一個(gè)設(shè)計(jì)過程,需要意識(shí)它是迭代的,并且支持需求和使用的發(fā)現(xiàn)和研究。與任何設(shè)計(jì)過程一樣,最佳規(guī)模在經(jīng)濟(jì)上和操作上都不理想,其投入的時(shí)間和資源也很有限。
在設(shè)計(jì)過程的簡(jiǎn)單性和成本與準(zhǔn)確性之間存在內(nèi)在的權(quán)衡。畢竟,復(fù)雜的模型可以更好地預(yù)測(cè)數(shù)據(jù)庫(kù)性能,但其成本可能與構(gòu)建數(shù)據(jù)庫(kù)本身一樣高,而且需要太多的輸入,以至于其應(yīng)用不切實(shí)際。
帶來(lái)哪些問題?
數(shù)據(jù)庫(kù)的工作負(fù)載通常被描述為查詢的吞吐量和它必須支持的數(shù)據(jù)集大小。這將會(huì)引發(fā)出一些問題:
•這個(gè)數(shù)字是最大吞吐量還是平均值?(工作量通常有周期性變化和峰值)
•是否應(yīng)該分離某些類型的查詢?(例如讀取和寫入)
•如果還沒有使用這個(gè)數(shù)據(jù)庫(kù),那么如何估計(jì)查詢的數(shù)量?數(shù)據(jù)集多大?
•熱門數(shù)據(jù)集是什么?數(shù)據(jù)庫(kù)存儲(chǔ)的數(shù)據(jù)比它們?cè)谌魏谓o定時(shí)間點(diǎn)所能提供的數(shù)據(jù)多得多。
•數(shù)據(jù)模型呢?從經(jīng)驗(yàn)中知道,數(shù)據(jù)模型對(duì)查詢數(shù)量、性能和存儲(chǔ)大小有很大的影響。
•預(yù)期增長(zhǎng)是多少?希望構(gòu)建一個(gè)可以隨工作負(fù)載擴(kuò)展的數(shù)據(jù)庫(kù)。
•查詢的服務(wù)等級(jí)目標(biāo)(SLO)是什么?設(shè)計(jì)的延遲目標(biāo)是什么?
有些人很幸運(yùn),擁有一個(gè)可以正常工作的系統(tǒng),也許還有運(yùn)行正常的數(shù)據(jù)庫(kù),他們可以很容易地從中提取或推斷出這些問題的答案。但在通常情況下,一些被迫使用猜測(cè)方法和粗略計(jì)算。這并不像聽起來(lái)那么糟糕。例如,可以了解這個(gè)使用蒙特卡羅工具的模型,如下圖所示。

估計(jì)工作量很有趣。但是出于簡(jiǎn)單的分級(jí)目的,人們感興趣的是吞吐量和數(shù)據(jù)集的最大值,并將只區(qū)分兩種查詢:讀取和寫入,其原因?qū)⒃诤竺娼忉尅?/p>
還可以假設(shè)對(duì)數(shù)據(jù)模型的控制程度很高。對(duì)于任何NoSQL數(shù)據(jù)庫(kù),其目標(biāo)是通過一個(gè)查詢來(lái)處理所有需要的數(shù)據(jù)--如果用戶想優(yōu)化讀取或?qū)懭耄枰龀鰶Q定。
通常的基本步驟是:
(1) 估計(jì)峰值數(shù)據(jù)集大小和工作負(fù)載。
(2) 初步繪制數(shù)據(jù)模型,對(duì)優(yōu)化目標(biāo)進(jìn)行高層決策。
(3) 根據(jù)數(shù)據(jù)模型估計(jì)讀/寫比率。
構(gòu)建數(shù)據(jù)庫(kù)的性能模型
數(shù)據(jù)庫(kù)的性能模型必須在一些有時(shí)相互沖突的需求之間取得平衡,它必須考慮足夠的性能和容量安全裕度,但需要在成本、可靠性與性能、持續(xù)和峰值負(fù)載之間實(shí)現(xiàn)平衡,但仍然可以簡(jiǎn)化,即使在沒有特定應(yīng)用程序的情況下使用,同時(shí)提供明確的結(jié)果。
這確實(shí)是一項(xiàng)具有挑戰(zhàn)性的任務(wù)。但它可以簡(jiǎn)單得多。例如,以下是它如何與Scylla一起工作,Scylla是一個(gè)兼容Apache Cassandra的開源NoSQL數(shù)據(jù)庫(kù)。
查詢vs操作
工作負(fù)載是根據(jù)查詢(通常是CQL)指定的。CQL查詢可能非常復(fù)雜,并生成數(shù)量不一的基本操作,這些操作的性能相對(duì)可預(yù)測(cè)。以下面的CQL查詢?yōu)槔?/p>
- SELECT * FROM user_stats WHERE id=UUID
- SELECT * FROM user_stats WHERE username=USERNAME
- SELECT * FROM user_stats WHERE city=”New York” ALLOW FILTERING
查詢#1將使用主鍵定位記錄,因此會(huì)立即在正確的分區(qū)上執(zhí)行--根據(jù)一致性級(jí)別,它仍然可能分解為幾個(gè)操作,因?yàn)閷⒉樵兌鄠€(gè)副本(稍后詳細(xì)介紹)。
盡管查詢#2看起來(lái)非常相似,但它會(huì)基于二級(jí)索引查找記錄,分解為兩個(gè)子查詢:一個(gè)子查詢到全局二級(jí)索引以查找主鍵,另一個(gè)子查詢從分區(qū)檢索行。此外,根據(jù)一致性級(jí)別,這可能會(huì)生成多個(gè)操作。
查詢#3甚至更極端,因?yàn)樗绶謪^(qū)掃描;它的表現(xiàn)將是糟糕的和不可預(yù)測(cè)的。
另一個(gè)例子是UPDATE查詢:
- UPDATE user_stats SET username=USERNAME, rank=231, score=3432 WHERE ID=UUID
- UPDATE user_stats SET username=USERNAME, rand=231, score=3432 WHERE ID=UUID IF EXISTS
查詢#1可能會(huì)直觀地分解為讀取和寫入這兩個(gè)操作--但在CQL UPDATE查詢中是UPSERT查詢,只會(huì)生成1個(gè)寫入操作。
然而,查詢#2盡管看起來(lái)很相似,但它不僅要求在所有副本上先讀取后寫入,而且還要求進(jìn)行編排的輕量級(jí)事務(wù)(LWT)。
類似地,查詢生成的磁盤操作數(shù)量可能會(huì)有很大的不同。大多數(shù)數(shù)據(jù)庫(kù)在排序字符串表(SSTable)存儲(chǔ)格式中使用日志結(jié)構(gòu)的合并樹(LSM)結(jié)構(gòu)。他們從不修改磁盤上的SSTable文件,它們是不變的。寫入被持久化到只追加提交日志以進(jìn)行恢復(fù),并寫入內(nèi)存緩沖區(qū)(memtable)。當(dāng)memtable變得太大時(shí),它會(huì)被寫入磁盤上的一個(gè)新的SSTable文件,并從內(nèi)存中刷新。這使得寫路徑非常高效,但在讀取時(shí)引入了一個(gè)問題:數(shù)據(jù)庫(kù)必須在多個(gè)SSTables中搜索一個(gè)值。
為了防止這種讀取失控放大,數(shù)據(jù)庫(kù)定期將多個(gè)SSTables壓縮到數(shù)量更少的文件中,只保留最新的數(shù)據(jù)。這減少了完成查詢所需的讀取操作數(shù)量。
這意味著將磁盤操作歸因于單個(gè)查詢實(shí)際上是不可能的。磁盤操作的確切數(shù)量取決于SSTables的數(shù)量、它們的排列、壓縮策略等。開源的NoSQL數(shù)據(jù)庫(kù)旨在最大限度地利用存儲(chǔ)空間,所以只要磁盤速度足夠快并且不是瓶頸,就可以忽略這個(gè)維度。這就是推薦快速本地NVMe磁盤的原因。
雖然這個(gè)示例主要關(guān)注CQL,但預(yù)測(cè)查詢成本并不是唯一的問題。事實(shí)上,查詢語(yǔ)言越豐富、功能越強(qiáng)大,就越難以預(yù)測(cè)其性能。例如,由于SQL的強(qiáng)大功能,它的性能可能難以預(yù)測(cè)。因此,復(fù)雜的查詢優(yōu)化器是RDBMS的重要組成部分,它確實(shí)提高了性能,但其代價(jià)是使性能更加難以預(yù)測(cè)。這是NoSQL采取的另一種折衷方法:優(yōu)先考慮可預(yù)測(cè)的性能和可擴(kuò)展性,而不是功能豐富的查詢語(yǔ)言。
一致性的難題
分布式可用數(shù)據(jù)庫(kù)需要可靠地將數(shù)據(jù)復(fù)制到多個(gè)節(jié)點(diǎn)。每個(gè)鍵空間可以配置副本的數(shù)量,稱之為復(fù)制因子。從客戶端的角度來(lái)看,這可以同步發(fā)生,也可以異步發(fā)生,這取決于寫入的一致性級(jí)別。
例如,當(dāng)一致性級(jí)別為1時(shí),數(shù)據(jù)最終會(huì)寫入所有副本,但客戶端只會(huì)等待一個(gè)副本確認(rèn)寫入。即使有些節(jié)點(diǎn)暫時(shí)不可用,數(shù)據(jù)庫(kù)也應(yīng)該在這些節(jié)點(diǎn)可用時(shí)緩存寫入和復(fù)制(這稱為暗示切換)。在實(shí)踐中,可以假設(shè)每個(gè)寫查詢都會(huì)在每個(gè)副本上生成至少一個(gè)寫入操作。
然而,對(duì)于讀取來(lái)說,情況有些不同。一致性級(jí)別為ALL的查詢必須從所有副本讀取數(shù)據(jù),從而生成與集群的復(fù)制因子一樣多的讀取操作,但一致性級(jí)別為1的查詢只從單個(gè)節(jié)點(diǎn)讀取數(shù)據(jù),從而實(shí)現(xiàn)更便宜、更快的讀取。這允許用戶以犧牲一致性為代價(jià)從集群中擠出更多的讀吞吐量,因?yàn)橐粋€(gè)節(jié)點(diǎn)在被讀取時(shí)可能還沒有獲得最近的更新。
最后,還有輕量級(jí)事務(wù)(LWT)需要考慮。如上所述,輕量級(jí)事務(wù)(LWT)需要利用Paxos算法對(duì)所有副本進(jìn)行編排。輕量級(jí)事務(wù)(LWT)不僅強(qiáng)制每個(gè)副本讀取然后寫入該值,而且還需要維護(hù)事務(wù)的狀態(tài),直到Paxos輪結(jié)束。由于輕量級(jí)事務(wù)(LWT)的行為方式不同,需要將其視為每個(gè)核心能夠支持的吞吐量的獨(dú)立性能模型。
所有操作都是平等的,但有些操作比其他操作更平等
現(xiàn)在已經(jīng)將CQL查詢分解為基本操作,那么可以詢問一些問題:每個(gè)操作需要多少時(shí)間(和資源)?CPU核心能支持的容量是多少?同樣,其答案有點(diǎn)復(fù)雜。作為一個(gè)例子,可以考慮一個(gè)簡(jiǎn)單的讀取并遵循節(jié)點(diǎn)中的執(zhí)行步驟:
(1) 在內(nèi)存表中查找值。
(2) 在緩存中查找值。
(3) 在SSTables中逐層查找值并合并值。
(4) 響應(yīng)協(xié)調(diào)者。
顯然,如果它們?cè)诨趦?nèi)存的memtable或行級(jí)緩存中,讀取將更快地完成,這沒什么好奇怪的。此外,步驟#3和#4可能會(huì)產(chǎn)生更高的成本,這取決于從磁盤讀取、處理和通過網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)的大小。如果需要讀取10MB的數(shù)據(jù),這可能需要相當(dāng)長(zhǎng)的時(shí)間。這可能是因?yàn)榇鎯?chǔ)在單個(gè)單元格中的值很大,也可能是因?yàn)榉秶鷴呙璺祷卦S多結(jié)果。但是,在值很大的情況下,Scylla不能將它們分成更小的塊,必須將整個(gè)單元格加載到內(nèi)存中。
一般來(lái)說,建議對(duì)數(shù)據(jù)進(jìn)行建模,使分區(qū)、行和單元不要增長(zhǎng)得太大,以確保減少性能的差異。然而在現(xiàn)實(shí)中,總會(huì)有一些差異。
當(dāng)涉及到分區(qū)訪問模式時(shí),這種差異尤其顯著。許多數(shù)據(jù)庫(kù)用于跨節(jié)點(diǎn)擴(kuò)展和傳播數(shù)據(jù)的策略是將數(shù)據(jù)分塊到彼此獨(dú)立的分區(qū)中。但獨(dú)立也意味著分區(qū)可能會(huì)經(jīng)歷不均勻的負(fù)載,導(dǎo)致所謂的“熱分區(qū)”問題,即單個(gè)分區(qū)會(huì)遇到節(jié)點(diǎn)容量限制,盡管集群的其余部分有足夠的資源。這個(gè)問題的發(fā)生在很大程度上取決于數(shù)據(jù)模型、數(shù)據(jù)集中分區(qū)鍵的分布以及工作負(fù)載中鍵的分布。由于預(yù)測(cè)熱分區(qū)通常需要分析整個(gè)數(shù)據(jù)集和工作負(fù)載,因此在設(shè)計(jì)階段是不切實(shí)際的,因此可以提供某些已知的分布作為模型的輸入,或者對(duì)分區(qū)的相對(duì)熱進(jìn)行一些假設(shè)。另外,nodetool toppartitions命令可以幫助定位熱分區(qū)。
物化視圖、二級(jí)索引和其他
數(shù)據(jù)庫(kù)具有自動(dòng)二級(jí)索引和物化視圖以及變更數(shù)據(jù)捕獲(CDC)功能。這些表本質(zhì)上是由數(shù)據(jù)庫(kù)本身維護(hù)的輔助表,只允許使用一個(gè)寫操作以多種形式寫入數(shù)據(jù)。
而在幕后,Scylla根據(jù)用戶提供的模式派生要寫入的新值,并將這些新值寫入不同的表。在這方面,Scylla和RDBMS之間的主要區(qū)別在于,派生數(shù)據(jù)是異步寫入的,并且作為索引和物化視圖跨網(wǎng)絡(luò)寫入,而不局限于單個(gè)分區(qū)。這是另一個(gè)需要考慮的寫入的來(lái)源。在Scylla中,它是可以預(yù)測(cè)的,并且在性能上與用戶生成的操作相似。對(duì)于寫入的每一項(xiàng),CDC和從寫入單元格派生的每個(gè)物化視圖或二級(jí)索引都將觸發(fā)一次寫入。在某些情況下,物化視圖和CDC可能需要額外的讀取,例如,如果啟用了CDC的“預(yù)映像”功能,就會(huì)發(fā)生這種情況。另外需要記住的是,一個(gè)CQL查詢可以觸發(fā)多個(gè)寫操作。
CDC、二級(jí)索引和物化視圖被實(shí)現(xiàn)為由Scylla本身管理的常規(guī)表,但這也意味著它們消耗的磁盤空間與用戶表相當(dāng),因此必須在容量計(jì)劃中考慮到這一點(diǎn)。
高峰和數(shù)據(jù)庫(kù)維護(hù)
所有數(shù)據(jù)庫(kù)都需要執(zhí)行各種維護(hù)操作。RDBMS需要清理重做日志(例如Postgre SQL VACUUM),轉(zhuǎn)儲(chǔ)快照到磁盤(查看Redis),或執(zhí)行內(nèi)存垃圾收集(Cassandra、Elastic、HBase)。使用LSM存儲(chǔ)的數(shù)據(jù)庫(kù)(如Cassandra、HBase、Scylla)也需要定期壓縮SSTables,并將memtable刷新到新的SSTables中。
如果數(shù)據(jù)庫(kù)足夠智能,可以將壓縮和修復(fù)等維護(hù)操作推遲到稍后、負(fù)載更少的時(shí)間,那么可以在短時(shí)間內(nèi)獲得最佳性能。然而,最終將不得不為這些維護(hù)操作預(yù)留資源。這對(duì)于大多數(shù)系統(tǒng)來(lái)說非常有用,因?yàn)橐惶靸?nèi)的負(fù)載的分配并不是均勻的。但是,企業(yè)的計(jì)劃應(yīng)該為數(shù)據(jù)庫(kù)的持續(xù)長(zhǎng)期操作提供足夠的容量。
此外,僅根據(jù)吞吐量進(jìn)行規(guī)劃是不夠的。在某種程度上,可以使數(shù)據(jù)庫(kù)過載以獲得更高的吞吐量,但其代價(jià)是更高的延遲。
在這個(gè)意義上,基準(zhǔn)往往具有誤導(dǎo)性,通常時(shí)間太短而無(wú)法達(dá)到有意義的持續(xù)運(yùn)行。延遲/吞吐量的權(quán)衡通常更容易度量,甚至在較短的基準(zhǔn)測(cè)試中也可以觀察到。
另一個(gè)重要但經(jīng)常被忽視的問題是降級(jí)操作。作為一個(gè)本地冗余和高可用的數(shù)據(jù)庫(kù),Scylla被設(shè)計(jì)為平滑地處理節(jié)點(diǎn)故障(根據(jù)用戶設(shè)置的一致性約束)。但是,雖然故障在語(yǔ)義上是一致的,但這并不意味著它們是動(dòng)態(tài)透明的,而其容量的顯著損失將影響集群的性能,以及故障節(jié)點(diǎn)的恢復(fù)或替換。在調(diào)整集群規(guī)模時(shí)也需要考慮這些因素。
選擇適當(dāng)規(guī)模的節(jié)點(diǎn)
由于Scylla的容量可以通過增加節(jié)點(diǎn)或選擇更大的節(jié)點(diǎn)來(lái)增加,一個(gè)有趣的問題出現(xiàn)了:應(yīng)該選擇哪種擴(kuò)展策略?一方面,更大的節(jié)點(diǎn)效率更高,因?yàn)榭梢元?dú)立于服務(wù)Scylla的內(nèi)核分配CPU核來(lái)處理網(wǎng)絡(luò)和其他任務(wù),并減少節(jié)點(diǎn)協(xié)調(diào)的相對(duì)開銷。另一方面,擁有的節(jié)點(diǎn)越多,當(dāng)其中一個(gè)節(jié)點(diǎn)出現(xiàn)故障時(shí),損失的部分容量就越少--盡管丟失節(jié)點(diǎn)的概率稍微高一些。
對(duì)于非常大的工作負(fù)載,解決這個(gè)問題是沒有意義的,因?yàn)榇笮凸?jié)點(diǎn)是不夠的。但是對(duì)于許多較小的工作負(fù)載來(lái)說,3個(gè)中大型節(jié)點(diǎn)就足夠了。這個(gè)決定與工作量相關(guān)。但是,對(duì)于可靠性降級(jí)操作,建議至少運(yùn)行6~9個(gè)節(jié)點(diǎn)(假設(shè)復(fù)制因子為3)。
結(jié)論
容量規(guī)劃和調(diào)整集群規(guī)模可能非常復(fù)雜和具有挑戰(zhàn)性。本文已經(jīng)討論了如何考慮安全裕度、維護(hù)操作和使用模式。重要的是要記住,任何猜測(cè)都只是迭代的初始估計(jì)。一旦投入生產(chǎn)并有了真實(shí)的數(shù)據(jù),可以讓它指導(dǎo)實(shí)施容量計(jì)劃。