一篇文章帶你搞懂 etcd 3.5 的核心特性

騰訊雲原生發表於2021-06-17

作者

唐聰,騰訊雲資深工程師,極客時間專欄《etcd實戰課》作者,etcd活躍貢獻者,主要負責騰訊雲大規模k8s/etcd平臺、有狀態服務容器化、在離線混部等產品研發設計工作。

etcd 3.5釋出

美東時間2021年6月15號18點,繼 etcd 3.4 版本釋出近兩年之後,etcd 社群官宣釋出了3.5 穩定版本,其主要貢獻者來自 Google、AWS、Tencent、Red Hat、ByteDance、IBM 等公司的開發者。etcd 3.5 版本的釋出,將極大提升開發者體驗、更快、更穩的支撐 kubernetes。

騰訊雲容器團隊(tangcong/wswcfan/mlmhl等)一直致力於參與 etcd社群開源的貢獻,參與以及貢獻了大量提升 etcd 穩定性、效能優化的 PR 和 QoS Proposal 等,是目前國內最活躍的貢獻團隊,未來我們也將持續將內部大規模 kubernetes 和 etcd 叢集的治理經驗回饋給社群,為 kubernetes 和 etcd 社群添磚加瓦!

etcd 3.5 特性分析

那麼在 etcd 3.5 版本中有哪些令人期待的特性呢?

下圖我從模組化/開發者體驗、效能、穩定性、運維和安全、文件及 RoadMap 方面為你總結了 etcd 3.5核心要點。

首先,在我看來,在etcd 3.5版本,最令開發者期待的當屬對 Go Module 的版本號語義的支援,並將之前大的 etcd 模組按功能進行了拆分,實現了 etcd 的模組化等,解決了飽受社群吐槽的 “go get fail”、依賴複雜、迴圈依賴、強制依賴過低的 gRPC 版本等痛點,將大大提升 etcd 開發者幸福度。

其次,從效能及穩定性上看,etcd 3.5 版本包含了若干對etcd讀寫效能優化、啟動耗時優化、重要 Bug 修復、記憶體佔用優化等特性,將顯著提升叢集穩定性、吞吐量、延時,將更好的支撐大規模 kubernetes 叢集。

最後,從運維、安全上,etcd 3.5 版本包含了 etcd 日誌輪轉/壓縮、叢集降級、etcdutl、expensive request 定位、本地 trace 及分散式 trace OpenTelemetry 等特性支援,以及一系列安全問題優化,將顯著提升問題定位效率。

接下來我就從以上各方面為你解讀 etcd 3.5 中核心特性。

etcd 3.5 核心特性解讀

支援 Go Module 版本號語義及模組化

自從 Go 社群在 Go 1.11 版本開始推出官方的包依賴管理解決方案 Go Module,並在 Go 1.14 版本達到生產環境可用標準後,絕大部分的專案已經使用 Go Module 來解決專案中的包依賴管理的痛點,並且 Go 在1.16版本後 Go Module 已經預設開啟了。

然而,如果你的 Go 專案依賴 etcd,原以為一個"go get go.etcd.io/etcd/"命令能下好所有依賴,結果卻是要經歷一波三折才能下好各種依賴,一開始你會遇到如下的 boltdb 錯誤。通過 Go Modules 的如下 replace 命令解決後,又會遇到因為 etcd 依賴過低的 gRPC 版本,導致的 gRPC 錯誤等。

% go get go.etcd.io/etcd/
go get: github.com/coreos/bbolt@none updating to
	github.com/coreos/bbolt@v1.3.6: parsing go.mod:
	module declares its path as: go.etcd.io/bbolt
	        but was required as: github.com/coreos/bbolt
go.etcd.io/etcd imports
	github.com/coreos/etcd/etcdmain imports
	github.com/coreos/etcd/proxy/grpcproxy imports
	google.golang.org/grpc/naming: cannot find module providing package google.golang.org/grpc/naming

當你解決完各種依賴問題,滿心歡喜的開始編譯時,你又可能會遇到版本過低,導致編譯錯誤,又陷入崩潰中。

為什麼會版本過低呢? 檢視 go.mod 檔案你會發現,原來你最後 go get 下載得版本是 v3.3.25,如下所示。

% cat go.mod | grep etcd
module github.com/tangcong/etcd-lab
	github.com/coreos/etcd v3.3.25+incompatible // indirect
	go.etcd.io/etcd v3.3.25+incompatible

首先為什麼v3.3.25後面含有個 incompatible 呢? 這主要是因為 etcd 3.3 分支還未支援 Go Module(未含有go mod檔案),incompatible 表示它的主版本號大於2,與較低的版本號屬於同一模組。go 命令可能會將其自動升級到更高的 incompatible 版本,即使它會導致構建失敗。

那如何通過 go get 來下載 etcd 3.4 最新穩定版本呢?

首先在etcd 3.4中引入了 go mod 檔案,然而 Go 社群在設計 Go Module 的時,定了一些可能會導致原有 Go 服務遷移到 Go Module 時需要進行適配的規則。etcd 含有三大版本,v0系列、v2系列、v3系列,在 Go Module 的設計實現中,如果主版本是 2 或更高版本釋出的模組必須在其模組路徑上具有匹配的主版本字尾。比如,如果模組在 v0.5.0 中具有路徑 go.etcd.io/etcd/client,則在 v2.x.y 版本中它必須具有路徑 go.etcd.io/etcd/client/v2,在 v3.x.y 版本中它必須含有路徑 go.etcd.io/etcd/client/v3。

很顯然,接觸過 etcd 的小夥伴們都知道,etcd 在3.4分支的程式碼與 Go Module 版本號語義是不相容的,並未遵從Go Module 的以上設計。因此當你通過如下 go get 命令下載指定版本號時就會報如下錯誤,這是因為 etcd 3.4 分支 go mod 定義的模組路徑是 go.etcd.io/etcd,它是使用舊版的 v0 module 命名。

% go get go.etcd.io/etcd@v3.4.9
go get: go.etcd.io/etcd@v3.4.9: invalid version: module contains a go.mod file, so major version must be compatible: should be v0 or v1, not v3

針對這種情況,我們可以通過 Go Module 提供的偽版本號(pseudo-versions)來實現對 etcd 3.4 的依賴管理,比如 kubernetes 專案中 go mod 中管理 etcd 的依賴如下所示:

go.etcd.io/etcd => go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 

那上面的一連串字串分別是含義呢?

  • v0.5.0-alpha.5 是祖先語義版本標記,go 命令下載依賴時會進行驗證檢查。

  • 20200910180754 表示 commmit 提交記錄時間戳

  • dd1b699fc489 是 commit 記錄 hash 值的前12位,go get 會通過此 hash 值從 git 中下載對應的版本的程式碼。

那以上資訊如何生成呢? 你可以通過如下的命令進行檢視:

% TZ=UTC git --no-pager show \
  --quiet \
  --abbrev=12 \
  --date='format-local:%Y%m%d%H%M%S' \
  --format="%cd-%h"
20200824191128-ae9734ed278b

也就是如果你要下載 etcd 3.4 版本的程式碼,你可以通過參考 kubernetes 專案指定偽版本號來實現 etcd 3.4 庫的依賴管理。

為了解決以上各種痛點、吐槽,etcd 社群首先通過 name packages with go.etcd.io/etcd/v3 pr,遵循 Go Module 版本號語義規範支援了v3語義,解決了無法通過"go get go.etcd.io/etcd/v3"下載最新版本的問題。

但是 etcd 依賴問題遠遠不止此,從 kubernetes 專案中的 go mod 檔案中你可以看到,竟然出現了多個 etcd 相關的依賴包,K8s 的程式碼庫依賴也與 etcd 依賴存在若干衝突,甚至還在有些專案出現瞭如下迴圈依賴。

	github.com/coreos/etcd => github.com/coreos/etcd v3.3.13+incompatible
	go.etcd.io/etcd => go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 // ae9734ed278b is the SHA for git tag v3.4.13
etcd -> prometheous-client -> prometheus-common -> go-kit -> etcd

為了解決以上問題,社群又提出了模組化的解決方案,也就是將 etcd 整個大的模組,按角色與功能拆分成 client、server、raft、api、tests、etcdctl、etcdutl 相關的子模組,具體如下圖。

通過這樣的模組化拆分後,各個業務只需要下載對應的模組就可以,比如你的專案需要使用 etcd client v3 庫對etcd 進行讀寫操作,你只需要執行如下 go get go.etcd.io/etcd/client/v3 命令即可,執行完後 go mod 內容如下。

% go get  go.etcd.io/etcd/client/v3
require go.etcd.io/etcd/client/v3 v3.5.0

如果你需要通過 etcdctl 訪問 etcd server,也只需要執行如下 go get 命令安裝即可。

% go get go.etcd.io/etcd/etcdctl/v3go get: added go.etcd.io/etcd/etcdctl/v3 v3.5.0

效能及穩定性提升

etcd 讀寫效能優化

重點介紹完 etcd 3.5 版本對 Go Module 版本號語義的支援以及模組化後,接著我們再看看 etcd 有哪些令人期待的效能及穩定性提升呢?

首先是 etcd 的讀寫效能優化。在介紹 etcd 讀寫效能優化之前,我先和你簡單介紹下背景知識,也就是在 etcd 中讀寫一個 key hello 的基礎原理。

啟動一個空叢集后,當你通過 etcdctl 執行 put key hello 寫流程核心如下圖所示:

  • 根據 etcd 的全域性邏輯時鐘版本號(空叢集啟動預設為1)自增生成 key hello 版本號 revision{2,0},並從treeIndex/B-tree 中查詢索引項,若不存在則插入 key hello 索引項,存在則更新。

  • key是 revision 版本號{2,0}, value 是個儲存使用者請求原始 key、value、版本號等的結構體,etcd 基於 boltdb 的 kv API,將以上 key-value 寫入到 boltdb 和 buffer(儲存暫未持久化到 boltdb 的資料)。

  • 注意 etcd 為了提升寫效能,一般情況下(pending 事務過多才會觸發同步提交)是非同步批量(backend goroutine 每隔 100ms)將boltdb的事務進行提交,持久化到磁碟的。

介紹完寫流程,我們再看看讀流程。etcdctl get hello 其讀流程核心如下圖所示(引用自我的 etcd 極客時間專欄 《基礎架構:etcd 一個讀請求是如何執行的?》)。

  • 首先根據 key hello 從 treeIndex/B-tree 中查詢索引項,若存在則返回其版本號{2,0}.

  • 其次根據版本號{2,0}優先從 buffer 中查詢,若命中則直接訪問。

  • 若 buffer 中未命中則從 boltdb 查詢。

etcd 3.5 的優化重點就是以上讀寫流程中的 buffer,我們再來看看 etcd 不同版本對其的優化歷史。

  • 在 etcd 3.2 為了提升寫吞吐量,引入了 buffer。在 etcd 3.2 到 etcd 3.3 版本,讀事務會加讀鎖,寫事務結束時要升級鎖更新 buffer,但是 expensive request 導致讀事務長時間持有鎖,最終導致寫請求超時。

  • 在 etcd 3.4 中,為了解決這個這個問題,實現了全併發讀,建立各個讀事務的時候都會全量拷貝 buffer, 讀寫事務不再因為 buffer 阻塞,大大緩解了 expensive request 對 etcd 效能的影響。尤其是 Kubernetes List Pod 等資源場景來說,etcd 穩定性顯著提升。

然而 etcd 3.4 各個讀事務拷貝 buffer 的行為,帶來了不可避免的開銷,並對寫入密集型的事務效能產生了負面影響。為了優化各讀事務拷貝的帶來的開銷,etcd 社群在 etcd 3.5 版本中通過如下兩個優化方案進一步提升事務併發效能。

第一是多個讀事務在 buffer 未變的場景下,共享同一個讀 buffer 的解決方案,其原理如下圖所示。

其原理如上圖所示,優化後的建立讀事務時流程如下:

  • 若共享讀 buffer 為空,則從寫事務所維護的常駐 buffer 中拷貝當前最新資料到共享讀 buffer。

  • 若共享讀 buffer 不為空,則判斷當前常駐 buffer 中的版本號與共享讀 buffer 中的版本號是否一致,若不一致則說明共享讀 buffer 是陳舊的,則全量拷貝常駐 buffer。由此可見,此優化方案在讀多寫少的場景,將表現較好。若寫請求較頻繁,將退化成之前的每次建立讀事務時都需拷貝一次 buffer 模式。

  • 若共享讀 buffer 中的版本號與常駐 buffer 版本號一致,說明共享讀 buffer 在上一次被建立之後,並無寫請求更新它,可直接使用當前共享讀 buffer 即可。

優化後的效能對比資料如下圖讀寫熱力圖所示,橫軸為連線數 /client,縱軸為 key-value 數,key-value 大小為256 個位元組,在 K8s 場景下(讀/寫為4:1),有2倍左右的效能提升。

第二是針對 kubernetes 場景頻繁使用的 Txn 的介面,支援在 Txn 介面中指定讀事務型別(通過 experimental-txn-mode-write-with-shared-buffer 引數),詳情如下:

  • 針對只讀工作負載,依然使用 concurrentReadTx,也就是建立讀事務時,需拷貝寫事務所維護的常駐 read buffer,給讀事務使用。不過得益於上面第一點對 concurrentReadTx 的優化,多個讀事務可共享一個 buffer,讀請求非常多場景,將極大減少拷貝次數和開銷。
  • 當 txn 事務包含寫操作時,針對 kubernetes 場景,預設使用 ReadTx 而不是 ConcurrentReadTx 以避免拷貝 buffer 的額外開銷(預設 experimental-txn-mode-write-with-shared-buffer 為 true), ReadTx 也就是通過加讀寫鎖,直接訪問寫事務的維護的常駐 read Buffer。

優化後的效能對比資料如下圖讀寫熱力圖所示,橫軸為連線數 /client,縱軸為 key-value 數,key-value 大小為256 個位元組,在 K8s 場景下(讀/寫為4:1),有1倍左右的效能提升。

etcd 啟動耗時的優化

介紹完 etcd 3.5 對讀寫效能優化的改進,我們在看看 etcd 啟動耗時的優化。

之前在大資料量的壓測場景下,我們發現 etcd 的啟動耗時高達5分鐘,隨後通過對 etcd 的啟動耗時進行深入分析,發現etcd為了獲取 consistent index 校驗快照檔案對有效性,會進行兩次重建 treeIndex 的操作。優化方案在重構consistent index 相關邏輯後,形成獨立 conistent index(cindex) 模組,就可以非常簡單地通過 cindex 模組,獲取到 consistent index 值,進行快照校驗工作,避免了多次重建 treeIndex 操作,詳情可參考 pr #11779pr #11699

etcd 穩定性優化

最後是 etc d的穩定性優化。在 etcd 3.5 版本中,我們修復了 etcd 3.4 社群版本中 lease 模組存在的一個記憶體洩露 bug,這個 bug 在 K8s 場景中 event 較多時非常容易觸發,並 cherry-pick 到了 etcd 3.4 版本中。同時,在大規模使用 etcd 叢集過程中,我們多次遇到磁碟io抖動導致的死鎖 bug,經過深入定位我們發現這個死鎖 bug 的觸發條件是落後的 follower 節點基於快照重建、並同時進行壓縮操作時導致的。針對此 bug 的修復方案,也 cherry-pick 到了 etcd 3.4 模組中。針對這類磁碟io抖動導致的 etcd,我們基於 etcd 的 functional test 測試框架,增加了模擬磁碟 io 抖動的測試 case,便於更加及時發現此類磁碟 io 導致的 bug。

另外 etcd 社群還發現 kube-apiserver 在使用 etcd 的時候,記憶體出現異常增長,佔據了 30% 的記憶體,通過 go pprof 工具分析發現是列印日誌時 protobuf marshal 導致的,如下圖所示。優化方案很簡單,避免 protobuf marshal,使用 rangeResponse.Size()。

叢集運維

最後則是關於 etcd 3.5 運維能力相關的介紹。

從前面我們介紹 etcd 模組化時提到過 etcdutl 工具,在etcd 3.5中,etcd 將一些直接操作 etcd 儲存檔案的管理命令單獨獨立成了 etcdutl 工具,它包括快照備份、快照重建、碎片整理功能。得益於 etcd 3.5 模組化的設計,你可以非常方便的通過如下的 go get go.etcd.io/etcd/etcdutl/v3 命令下載安裝它。

% go get go.etcd.io/etcd/etcdutl/v3go get: upgraded go.etcd.io/etcd/etcdutl/v3 v3.5.0

其次是 etcd 3.5 廢棄了 capnslog 日誌,預設使用 zap logger, 並支援配置日誌是否壓縮、輪轉、日誌檔案最大大小、保留副本數等,詳細配置資訊可參考下面點引數。

Logging:  --logger 'zap'    Currently only supports 'zap' for structured logging.  --log-outputs 'default'    Specify 'stdout' or 'stderr' to skip journald logging even when running under systemd, or list of comma separated output targets.  --log-level 'info'    Configures log level. Only supports debug, info, warn, error, panic, or fatal.  --enable-log-rotation 'false'    Enable log rotation of a single log-outputs file target.  --log-rotation-config-json '{"maxsize": 100, "maxage": 0, "maxbackups": 0, "localtime": false, "compress": false}'    Configures log rotation if enabled with a JSON logger config. MaxSize(MB), MaxAge(days,0=no limit), MaxBackups(0=no limit), LocalTime(use computers local time), Compress(gzip)".

同時,你可以通過 trace 日誌特性高效的定位問題。另外我們針對鑑權耗時操作、expensive request 來源ip定位困難等問題,在 etcd gRPC 入口增加了 expensive request 日誌,可列印任意 expensive request 和其來源 ip 等資訊。 同時,針對在磁碟io效能較差、頻繁備份等場景下,備份可能會影響業務正常讀寫的問題,在 etcd 3.5 中你可以通過 learner 實現備份能力。

接著,etcd 3.4 版本是使用 go 1.12 編譯的,go runtime 的預設記憶體管理策略是 MADV_FREE, 它的效能較好,但是會導致你看到的etcd記憶體虛高,監控指標異常、使用者體驗不佳等問題,原因是這種策略,在系統記憶體有壓力的時候,核心才會釋放佔用的記憶體。

從 go v1.16 起,Go 在 Linux 下的預設記憶體管理策略變成了 MADV_DONTNEED 策略。MADV_DONTNEED 雖然效率相比 MADV_FREE 策略較低,但是會讓 rss 記憶體下降較快,更加符合直觀感受,能避免 MADV_FREE 相關的副作用。

然後針對叢集升級可能會觸發bug,需要回滾的問題,之前 etcd 升級後不允許降級,在 etcd 3.5 中提供了叢集降級的功能。比如你從 etcd 3.4 升級到 etcd 3.5 後,若遇到 crash bug 則可以通過叢集降級功能回退到3.4。因 etcd 涉及到資料安全,建議先在測試環境升級進行驗證,現網升級後若遇到問題,也不要急於回滾,先可看看是否屬於配置問題等。目前降級功能實現上整體還並不完備,未通過大規模生產環境檢驗,建議謹慎操作。

最後安全性上,etcd 在 cncf 的贊助下,邀請第三方安全公司做了非常詳細的安全審計報告,針對發現的若干潛在安全問題,進行了修復。

etcd 未來規劃

針對 kubernetes 叢集中 List Pod 等 expensive request 導致 etcd OOM 等不穩定現象,未來對 etcd 3.6 版本計劃實現 etcd Range Stream 特性和 QoS 特性,其中 QoS 特性可參考我們之前提的 QoS Proposal

參考資料

容器服務 TKE:無需自建,即可在騰訊雲上使用穩定, 安全,高效,靈活擴充套件的 Kubernetes 容器平臺。

相關文章