作者 |?陳潔(墨封) ?阿里云開發(fā)工程師
導(dǎo)讀:etcd 作為?K8s 集群中的存儲組件,讀寫性能方面會受到很多壓力,而?etcd?3.4?中的新特性將有效緩解壓力,本文將從
etcd?數(shù)據(jù)讀寫機(jī)制的發(fā)展歷史著手,深入解讀?etcd?3.4?新特性。
背景
etcd 是 Kubernetes 集群中存儲元數(shù)據(jù),保證分布式一致性的組件,它的性能往往影響著整個集群的響應(yīng)時間。而在 K8s
的使用中,我們發(fā)現(xiàn)除了日常的讀寫壓力外,還存在某些特殊的場景會對 etcd 造成巨大的壓力,比如 K8s 下 apiserver 組件重啟或是其他組件繞過
apiserver cache 直接查詢 etcd 最新數(shù)據(jù)的情況時,etcd 會收到大量的 expensive read(后文會介紹該概念)請求,這對
etcd 讀寫會造成巨大的壓力。更為嚴(yán)重的是,如果客戶端中存在失敗重試邏輯或客戶端數(shù)目較多,會產(chǎn)生大量這樣的請求,嚴(yán)重情況可能造成 etcd crash。
etcd 3.4 中增加了一個名為“Fully Concurrent
Read”的特性,較大程度上解決了上述的問題。在這篇文章中我們將重點(diǎn)解讀它。本篇文章首先回顧 etcd
數(shù)據(jù)讀寫機(jī)制發(fā)展的歷史,之后剖析為何這個特性能大幅提升?expensive read 場景下 etcd 的讀寫性能,最后通過真實(shí)實(shí)驗(yàn)驗(yàn)證該特性的效果。
etcd 讀寫發(fā)展歷史
etcd v3.0 及之前早期版本
etcd 利用 Raft 算法實(shí)現(xiàn)了數(shù)據(jù)強(qiáng)一致性,它保證了讀操作的線性一致性。在 raft 算法中,寫操作成功僅僅以為著寫操作被 commit
到日志上,并不能確保當(dāng)前全局的狀態(tài)機(jī)已經(jīng) apply 了該寫日志。而狀態(tài)機(jī) apply 日志的過程相對于 commit 操作是異步的,因此在 commit
后立即讀取狀態(tài)機(jī)可能會讀到過期數(shù)據(jù)。
為了保證線性一致性讀,早期的 etcd(etcd v3.0 )對所有的讀寫請求都會走一遍 Raft 協(xié)議來滿足強(qiáng)一致性。然而通常在現(xiàn)實(shí)使用中,讀請求占了
etcd 所有請求中的絕大部分,如果每次讀請求都要走一遍 raft 協(xié)議落盤,etcd 性能將非常差。
etcd v3.1
因此在 etcd v3.1 版本中優(yōu)化了讀請求(PR#6275 <https://github.com/etcd-io/etcd/pull/6275>
),使用的方法滿足一個簡單的策略:每次讀操作時記錄此時集群的 commit index,當(dāng)狀態(tài)機(jī)的 apply index 大于或者等于 commit
index 時即可返回?cái)?shù)據(jù)。由于此時狀態(tài)機(jī)已經(jīng)把讀請求所要讀的 commit index 對應(yīng)的日志進(jìn)行了 apply
操作,符合線性一致讀的要求,便可返回此時讀到的結(jié)果。
根據(jù) Raft 論文 6.4 章 <https://raft.github.io/raft.pdf>的內(nèi)容,etcd 通過 ReadIndex
優(yōu)化讀取的操作核心為以下兩個指導(dǎo)原則:
* 讓 Leader 處理 ReadIndex 請求,Leader 獲取的 commit index 即為狀態(tài)機(jī)的 read index,follower
收到 ReadIndex 請求時需要將請求 forward 給 Leader;
* 保證 Leader 仍然是目前的 Leader,防止因?yàn)榫W(wǎng)絡(luò)分區(qū)原因,Leader 已經(jīng)不再是當(dāng)前的 Leader,需要 Leader 廣播向
quorum 進(jìn)行確認(rèn)。
ReadIndex 同時也允許了集群的每個 member 響應(yīng)讀請求。當(dāng) member 利用 ReadIndex 方法確保了當(dāng)前所讀的 key
的操作日志已經(jīng)被 apply 后,便可返回客戶端讀取的值。對 etcd ReadIndex 的實(shí)現(xiàn),目前已有相對較多的文章介紹,本文不再贅述。
etcd v3.2
即便 etcd v3.1 中通過 ReadIndex 方法優(yōu)化了讀請求的響應(yīng)時間,允許每個 member 響應(yīng)讀請求,但當(dāng)我們把視角繼續(xù)下移到底層 k/v
存儲 boltdb 層,每個獨(dú)立的 member 在獲取 ReadIndex 后的讀取任然存在性能問題。
v3.1 中利用 batch 來提高寫事務(wù)的吞吐量,所有的寫請求會按固定周期 commit 到 boltDB。當(dāng)上層向底層 boltdb
層發(fā)起讀寫事務(wù)時,都會申請一個事務(wù)鎖(如以下代碼片段),該事務(wù)鎖的粒度較粗,所有的讀寫都將受限
。對于較小的讀事務(wù),該鎖僅僅降低了事務(wù)的吞吐量,而對于相對較大的讀事務(wù)(后文會有詳細(xì)解釋),則可能阻塞讀、寫,甚至 member 心跳都有可能出現(xiàn)超時。
// release-3.2: mvcc/kvstore.go func (s *store) TxnBegin() int64 { ... s.tx =
s.b.BatchTx() // boltDB 事務(wù)鎖,所有的讀寫事務(wù)都需要申請?jiān)撴i s.tx.Lock() ... }
針對以上提到的性能瓶頸,etcd v3.2 版本中對 boltdb 層讀寫進(jìn)行優(yōu)化,包含以下兩個核心點(diǎn):
* 實(shí)現(xiàn)“N reads 或 1 write”的并行,將上文提到的粗粒度鎖細(xì)化成一個讀寫鎖,所有讀請求間相互并行;
* 利用 buffer 來提高了吞吐量。3.2 中對 readTx,batchTx 分別增加了一個 buffer,所有讀事務(wù)優(yōu)先從 buffer
進(jìn)行讀取,未命中再通過事務(wù)訪問 boltDB。同樣,寫事務(wù)在寫 boltDB 的同時,也會向 batchTx 的 buffer 寫入數(shù)據(jù),而 batch
commit 結(jié)束時,batchTx 的 buffer 會 writeBack 回 readTx 的 buffer 防止臟讀。 // release-3.3:
mvcc/kvstore_txn.go func (s *store) Read() TxnRead { tx := s.b.ReadTx() //
獲取讀事務(wù)的 RLock 后進(jìn)行讀操作 tx.RLock() } // release-3.3: mvcc/backend/batch_tx.go func
(t *batchTxBuffered) commit(stop bool) { // 獲取讀事務(wù)的 Lock 以確保 commit
之前所有的讀事務(wù)都已經(jīng)被關(guān)閉 t.backend.readTx.Lock() t.unsafeCommit(stop)
t.backend.readTx.Unlock() }
完全并發(fā)讀
etcd v3.2 的讀寫優(yōu)化解決了大部分讀寫場景的性能瓶頸,但我們再從客戶端的角度出發(fā),回到文章開頭我們提到的這種場景,仍然有導(dǎo)致 etcd
讀寫性能下降的危險(xiǎn)。
這里我們先引入一個?expensive read 的概念,在 etcd 中,所有客戶端的讀請求最后都是轉(zhuǎn)化為 range 的請求向 KV
層進(jìn)行查詢,我們以一次range 請求的 key 數(shù)量以及 value size 來衡量一次 read 請求的壓力大小。綜合而言,當(dāng) range 請求的
key 數(shù)量越多,平均每個 key 對應(yīng)的 value size 越大,則該 range 請求對 DB 層的壓力就越大。而實(shí)際劃分 expensive
read 和 cheap read 邊界視 etcd 集群硬件能力而定。
從客戶端角度,在大型集群中的?apiserver ?進(jìn)行一次 pod、node、pvc 等 resource 的全量查詢,可以視為一次 expensive
read。簡要分析下為何 expensive read 會對 boltDB 帶來壓力。上文提到,為了防止臟讀,需要保證每次 commit
時沒有讀事務(wù)進(jìn)行,因此寫事務(wù)每次 commit 之前,需要將當(dāng)前所有讀事務(wù)進(jìn)行回滾,所以 commit interval 時間點(diǎn)上需要申請?
readTx.lock?,會將該鎖從?RLock()?升級成?Lock()?,該讀寫鎖的升級會可能導(dǎo)致所有讀操作的阻塞。
如下圖(以下圖中,藍(lán)色條為讀事務(wù),綠色條為寫事務(wù),紅色條為事務(wù)因鎖問題阻塞),t1 時間點(diǎn)會觸發(fā) commit,然而有事務(wù)未結(jié)束,T5 commit
事務(wù)因申請鎖被阻塞到 t2 時間點(diǎn)才進(jìn)行。理想狀態(tài)下大量的寫事務(wù)會在一個 batch 中結(jié)束,這樣每次 commit
的寫事務(wù)僅僅阻塞少部分的讀事務(wù)(如圖中僅僅阻塞了? T6 這個事務(wù))。
然而此時如果 etcd 中有非常大的讀請求,那么該讀寫鎖的升級將被頻繁阻塞。如下圖,T3 是一個非常長的讀事務(wù),跨過了多個 commit batch。每個
commit batch 結(jié)束時間點(diǎn)照常觸發(fā)了 commit 的寫事務(wù),然而由于讀寫鎖無法升級,寫事務(wù) T4 被推遲,同樣 t2 commit 點(diǎn)的寫事務(wù)
T7 因?yàn)樯暾埐坏綄戞i一樣也被推遲。
此外,在寫事務(wù)的 commit 進(jìn)行了之后,需要將寫緩存里的 bucket 信息寫入到讀緩存中,此時同樣需要升級?readTx.lock?到?Lock()
?。而上層調(diào)用?backend.Read()?獲取 readTx 時,需要確保這些 bucket 緩存已經(jīng)成功寫過來了,需要申請讀鎖?
readTx.RLock()?,而如果這期間存在寫事務(wù),該鎖則無法得到,這些讀事務(wù)都無法開始。如上的情形下,在第三個
batch(t2-t3)中其他讀事務(wù)因?yàn)榈貌坏阶x鎖都無法進(jìn)行了。
總結(jié)而言,因 expensive read 造成讀寫鎖頻繁升級,導(dǎo)致寫事務(wù)的 commit 不斷被后移(通常我們將這種問題叫做 head-of-line
blocking),從而導(dǎo)致 etcd 讀寫性能雪崩。
etcd v3.4 中,增加了一個?“Fully Concurrent Read”?的 feature,核心指導(dǎo)思想是如下兩點(diǎn):
* 將上述讀寫鎖去除(事實(shí)上是對該鎖再次進(jìn)行細(xì)化),使得所有讀和寫操作不再因該鎖而頻繁阻塞;
* 每個 batch interval 不再 reset 讀事務(wù) readTxn?,而是創(chuàng)建一個新的 concurrentReadTxn
?實(shí)例去服務(wù)新的讀請求,而原來的readTxn?在所有事務(wù)結(jié)束后會被關(guān)閉。每個 concurrentReadTxn? 實(shí)例擁有一片自己的 buffer 緩存。
除了以上兩點(diǎn)變動外,fully concurrent read 在創(chuàng)建新的 ConcurrentReadTx?實(shí)例時需要從 ReadTx?copy 對應(yīng)的
buffer map,會存在一定的額外開銷,社區(qū)也在考慮將這個 copy buffer 的操作 lazy 化,在每個寫事務(wù)結(jié)束后或者每個 batch
interval 結(jié)束點(diǎn)進(jìn)行。然而在我們的實(shí)驗(yàn)中發(fā)現(xiàn),該 copy 帶來的影響并不大。改動的核心代碼如以下片段所示:
// release-3.4: mvcc/backend/read_tx.go type concurrentReadTx struct { // 每個
concurrentReadTx 實(shí)例保留一份 buffer,在創(chuàng)建時從 readTx 的 buffer 中獲得一份 copy buf
txReadBuffer ... } // release-3.4: mvcc/backend/backend.go func (b *backend)
ConcurrentReadTx() ReadTx { // 由于需要從 readTx 拷貝 buffer,創(chuàng)建 concurrentReadTx
時需要對常駐的 readTx 上讀鎖。 b.readTx.RLock() defer b.readTx.RUnlock() ... } //
release-3.4: mvcc/backend/read_tx.go // concurrentReadTx 的 RLock
中不做任何操作,不再阻塞讀事務(wù) func (rt *concurrentReadTx) RLock() {} // release-3.4:
mvcc/kvstore_tx.go func (s *store) Read() TxnRead { // 調(diào)用 Read 接口時,返回
concurrentReadTx 而不是 readTx tx := s.b.ConcurrentReadTx() // concurrentReadTx 的
RLock 中不做任何操作 tx.RLock() }
我們再回到上文提到的存在 expensive read 的場景。在 fully concurrent read 的改動之后,讀寫場景如下圖所示。
首先在 mvcc 創(chuàng)建 backend 時會創(chuàng)建一個常駐的 readTx?實(shí)例,和之后的寫事務(wù) batchTx
?存在鎖沖突的也僅僅只有這一個實(shí)例。之后的所有讀請求(例如 T1,T2,T3 等),會創(chuàng)建一個新的concurrentReadTx?實(shí)例進(jìn)行服務(wù),同時需要從
readTx?拷貝 buffer;當(dāng)出現(xiàn) expensive read 事務(wù) T3 時,T4 不再被阻塞并正常執(zhí)行。同時 T5 需要等待 T4 commit
完成后,readTx?的 buffer 被更新后,再進(jìn)行 buffer 拷貝,因此阻塞一小段時間。而 t2、t3 commit 時間點(diǎn)的寫事務(wù) T7、T8
也因?yàn)闆]有被阻塞而順利進(jìn)行。
在 fully concurrent read 的讀寫模式下, concurrentReadTx?僅在創(chuàng)建時可能存在阻塞(因?yàn)橐蕾噺?readTx?進(jìn)行
buffer 拷貝的操作),一旦創(chuàng)建后則不再有阻塞的情況,因此整個流程中讀寫吞吐量有較大的提升。
讀寫性能驗(yàn)證實(shí)驗(yàn)
針對 etcd v3.4 fully concurrent read 的新 feature,我們在集群中進(jìn)行了實(shí)驗(yàn)對比增加該 feature
前后讀寫性能的變化。為了排除網(wǎng)絡(luò)因素干擾,我們做了單節(jié)點(diǎn) etcd 的測試,但是已經(jīng)足以從結(jié)果上看出該 feature 的優(yōu)勢。以下是驗(yàn)證實(shí)驗(yàn)的設(shè)置:
* 讀寫設(shè)置
* 模擬集群已有存儲量,預(yù)先寫入** 100k KVs**,每個 KV 由一個128B key和 一個 1~32KB 隨機(jī)的 values 組成(平均
16KB)
* expensive read:每次 range 20k keys,每秒 1 并發(fā)。
* cheap read:每次 range 10 keys,每秒 100 并發(fā)。
* write:每次 put 1 key,每秒 20 并發(fā)。
* 對照組
* 普通讀寫場景:cheap read + write;
* 模擬存在較重的讀事務(wù)的場景:cheap read + expensive read + write。
* 對比版本:
* etcd - ali2019rc2 未加入該優(yōu)化
* etcd - ali2019rc3 加入該優(yōu)化
* 防止偶然性:每組 test case 跑 5 次,取 99 分位(p99)的響應(yīng)時間的平均值作為該組 test case 的結(jié)果。
實(shí)驗(yàn)結(jié)果如下表所示。對于普通讀寫場景,3.4 中的讀寫性能和 3.3 近似;對于存在較重的讀事務(wù)的場景,3.4 中的 fully concurrent
read feature 一定程度降低了 expensive read 的響應(yīng)時間。而在該場景下的 cheap read 和 write,rc2
中因讀寫鎖導(dǎo)致讀寫速度非常緩慢,而 rc3 中實(shí)現(xiàn)的完全并行使得讀寫響應(yīng)時間減少到約為原來的 1/7。
| etcd
version | cheap
read + write | ?| expensive
read + cheap read + write
p99 read (ms) p99 write (ms)
read (ms) p99 cheap read (ms) p99 write (ms)
3.3 14.1 15.1
3.4 (with FCR) 16.1 14.2
其他場景下,如在 Kuberentes?5000節(jié)點(diǎn)性能測試
<https://prow.k8s.io/view/gcs/kubernetes-jenkins/logs/ci-kubernetes-e2e-gce-scale-performance/1130745634945503235>
,也表明在大規(guī)模讀壓力下,P99 寫的延時降低 97.4%。
總結(jié)
etcd fully concurrent read 的新 feature 優(yōu)化 expensive 降低了近 85% 的寫響應(yīng)延遲以及近 80%
的讀響應(yīng)延遲,同時提高了 etcd 的讀寫吞吐量,解決了在讀大壓力場景下導(dǎo)致的 etcd
性能驟降的問題。調(diào)研和實(shí)驗(yàn)的過程中感謝宇慕的指導(dǎo),目前我們已經(jīng)緊跟社區(qū)應(yīng)用了該新能力,經(jīng)過長時間測試表現(xiàn)穩(wěn)定。未來我們也會不斷優(yōu)化 etcd
的性能和穩(wěn)定性,并將優(yōu)化以及最佳實(shí)踐經(jīng)驗(yàn)反饋回社區(qū)。
參考文獻(xiàn)
* Etcd Fully Concurrent Read Design Proposal
<https://docs.google.com/document/d/1V9UuL8BnpZF2xFpHhvE1lO2hWvtdiLml3Ukr-m4jcdI>
* Strong consistency models?
<https://aphyr.com/posts/313-strong-consistency-models>
* Attiya H, Welch J L. Sequential consistency versus linearizability[J]. ACM
Transactions on Computer Systems (TOCS), 1994, 12(2): 91-122.
* Ongaro D, Ousterhout J. In search of an understandable consensus
algorithm[C]//2014 {USENIX} Annual Technical Conference ({USENIX}{ATC} 14).
2014: 305-319. [raft paper <https://raft.github.io/raft.pdf>]
“ 阿里巴巴云原生微信公眾號(ID:Alicloudnative)關(guān)注微服務(wù)、Serverless、容器、Service
Mesh等技術(shù)領(lǐng)域、聚焦云原生流行技術(shù)趨勢、云原生大規(guī)模的落地實(shí)踐,做最懂云原生開發(fā)者的技術(shù)公眾號?!?br>
熱門工具 換一換