原文連結:https://www.jianshu.com/p/f31ef5e7bdd0
etcd 是一個分布式一致性鍵值存儲。其主要功能有服務註冊與發現、消息發布與訂閱、負載均衡、分布式通知與協調、分布式鎖、分布式隊列、集群監控與 leader 選舉等。
官方文檔原文:https://github.com/etcd-io/etcd/blob/master/Documentation/tuning.md
譯文參考:https://skyao.gitbooks.io/learning-etcd3/content/documentation/op-guide/performance.html
理解 etcd 的性能決定 etcd 性能的關鍵因素,包括:
在通常的雲環境,比如 Google Compute Engine (GCE) 標準的 n-4 或者 AWS 上相當的機器類型,一個三成員 etcd 集群在輕負載下可以在低於 1 毫秒內完成一個請求,並在重負載下可以每秒完成超過 30000 個請求。
etcd 使用 Raft 一致性算法來在成員之間複製請求並達成一致。一致性性能,特別是提交延遲,受限於兩個物理約束:網絡 IO 延遲和磁碟 IO 延遲。完成一個 etcd 請求的最小時間是成員之間的網絡往返時延 (Round Trip Time / RTT),加需要提交數據到持久化存儲的 fdatasync 時間。在一個數據中心內的 RTT 可能有數百毫秒。在美國典型的 RTT 是大概 50ms, 而在大陸之間可以慢到 400ms。旋轉硬碟(註:指傳統機械硬碟) 的典型 fdatasync 延遲是大概 10ms。對於 SSD 硬碟, 延遲通常低於 1ms。為了提高吞吐量, etcd 將多個請求打包在一起並提交給 Raft。這個批量策略讓 etcd 在重負載試獲得高吞吐量。也有其他子系統影響到 etcd 的整體性能。每個序列化的 etcd 請求必須通過 etcd 的 boltdb 支持的(boltdb-backed) MVCC 存儲引擎, 它通常需要 10 微秒來完成。etcd 定期遞增快照它最近實施的請求,將他們和之前在磁碟上的快照合併。這個過程可能導致延遲尖峰(latency spike)。雖然在 SSD 上這通常不是問題,在 HDD 上它可能加倍可觀察到的延遲。而且,進行中的壓縮可以影響 etcd 的性能。幸運的是,壓縮通常無足輕重,因為壓縮是錯開的,因此它不和常規請求競爭資源。RPC 系統,gRPC,為 etcd 提供定義良好,可擴展的 API,但是它也引入了額外的延遲,尤其是本地讀取。
Etcd 的默認配置在本地網絡環境(localhost)下通常能夠運行的很好,因為延遲很低。然而,當跨數據中心部署 Etcd 或網絡延時很高時,etcd 的心跳間隔或選舉超時時間等參數需要根據實際情況進行調整。
網絡並不是導致延時的唯一來源。不論是 Follower 還是 Leader,其請求和響應都受磁碟 I/O 延時的影響。每個 timeout 都代表從請求發起到成功返迴響應的總時間。
時間參數Etcd 底層的分布式一致性協議依賴兩個時間參數來保證節點之間能夠在部分節點掉錢的情況下依然能夠正確處理主節點的選舉。第一個參數就是所謂的心跳間隔,即主節點通知從節點它還是領導者的頻率。實踐數據表明,該參數應該設置成節點之間 RTT 的時間。Etcd 的心跳間隔默認是 100 毫秒。第二個參數是選舉超時時間,即從節點等待多久沒收到主節點的心跳就嘗試去競選領導者。Etcd 的選舉超時時間默認是 1000 毫秒。
調整這些參數值是有條件的,此消波長。心跳間隔值推薦設置為臨近節點間 RTT 的最大值,通常是 0.5~1.5 倍 RTT 值。如果心跳間隔設得太短,那麼 Etcd 就會發送沒必要的心跳信息,從而增加 CPU 和網絡資源的消耗;如果設得太長,就會導致選舉等待時間的超時。如果選舉等待時間設置的過長,就會導致節點異常檢測時間過長。評估 RTT 值的最簡單的方法是使用 ping 的操作。
選舉超時時間應該基於心跳間隔和節點之間的平均 RTT 值。選舉超時必須至少是 RTT 10 倍的時間以便對網絡波動。例如,如果 RTT 的值是 10 毫秒,那麼選舉超時時間必須至少是 100 毫秒。選舉超時時間的上線是 50000 毫秒(50 秒),這個時間只能只用於全球範圍內分布式部署的 Etcd 集群。美國大陸的一個 RTT 的合理時間大約是 130 毫秒,美國和日本的 RTT 大約是 350~400 毫秒。如果算上網絡波動和重試的時間,那麼 5 秒是一次全球 RTT 的安全上線。因為選舉超時時間應該是心跳包廣播時間的 10 倍,所以 50 秒的選舉超時時間是全局分布式部署 Etcd 的合理上線值。
心跳間隔和選舉超時時間的值對同一個 Etcd 集群的所有節點都生效,如果各個節點都不同的話,就會導致集群發生不可預知的不穩定性。Etcd 啟動時通過傳入啟動參數或環境變量覆蓋默認值,單位是毫秒。示例代碼具體如下:
$ etcd --heartbeat-interval=100 --election-timeout=500
$ ETCD_HEARTBEAT_INTERVAL=100 ETCD_ELECTION_TIMEOUT=500 etcd
Etcd 總是向日誌文件中追加 key,這樣一來,日誌文件會隨著 key 的改動而線性增長。當 Etcd 集群使用較少時,保存完整的日誌歷史記錄是沒問題的,但如果 Etcd 集群規模比較大時,那麼集群就會攜帶很大的日誌文件。為了避免攜帶龐大的日誌文件,Etcd 需要做周期性的快照。快照提供了一種通過保存系統的當前狀態並移除舊日誌文件的方式來壓縮日誌文件。
快照調優為 v2 後端存儲創建快照的代價是很高的,所以只用當參數累積到一定的數量時,Etcd 才會創建快照文件。默認情況下,修改數量達到 10000 時才會建立快照。如果 Etcd 的內存使用和磁碟使用過高,那麼應該嘗試調低快照觸發的閾值,具體請參考如下命令。
啟動參數:
$ etcd --snapshot-count=5000
環境變量:
$ ETCD_SNAPSHOT_COUNT=5000 etcd
etcd 的存儲目錄分為 snapshot 和 wal,他們寫入的方式是不同的,snapshot 是內存直接 dump file。而 wal 是順序追加寫,對於這兩種方式系統調優的方式是不同的,snapshot 可以通過增加 io 平滑寫來提高磁碟 io 能力,而 wal 可以通過降低 pagecache 的方式提前寫入時序。因此對於不同的場景,可以考慮將 snap 與 wal 進行分盤,放在兩塊 SSD 盤上,提高整體的 IO 效率,這種方式可以提升 etcd 20% 左右的性能。
etcd 集群對磁碟 I/O 的延時非常敏感,因為 Etcd 必須持久化它的日誌,當其他 I/O 密集型的進程也在佔用磁碟 I/O 的帶寬時,就會導致 fsync 時延非常高。這將導致 Etcd 丟失心跳包、請求超時或暫時性的 Leader 丟失。這時可以適當為 Etcd 服務賦予更高的磁碟 I/O 權限,讓 Etcd 更穩定的運行。在 Linux 系統中,磁碟 I/O 權限可以通過 ionice 命令進行調整。
nux 默認 IO 調度器使用 CFQ 調度算法,支持用 ionice 命令為程序指定 IO 調度策略和優先級,IO 調度策略分為三種:
Idle :其他進程沒有磁碟 IO 時,才進行磁碟 IO
Best Effort:預設調度策略,可以設置 0-7 的優先級,數值越小優先級越高,同優先級的進程採用 round-robin 算法調度;
Real Time :立即訪問磁碟,無視其它進程 IO
None 即 Best Effort,進程未指定策略和優先級時顯示為 none,會使用依據 cpu nice 設置計算出優先級
Linux 中 etcd 的磁碟優先級可以使用 ionice 配置:
$ ionice -c2 -n0 -p `pgrep etcd`
etcd 中比較複雜的是網絡的調優,因此大量的網絡請求會在 peer 節點之間轉發,而且整體網絡吞吐也很大,但是還是再次強調不建議大家調整系統參數,大家可以通過修改 etcd 的 --heartbeat-interval 與 --election-timeout 啟動參數來適當提高高吞吐網絡下 etcd 的集群魯棒性,通常同步吞吐在 100MB 左右的集群可以考慮將 --heartbeat-interval 設置為 300ms-500ms,--election-timeout 可以設置在 5000ms 左右。此外官方還有基於 TC 的網絡優先傳輸方案,也是一個比較適用的調優手段。
如果 etcd 的 Leader 服務大量並發客戶端,這就會導致 follower 的請求的處理被延遲因為網絡延遲。follower 的 send buffer 中能看到錯誤的列表,如下所示:
dropped MsgProp to 247ae21ff9436b2d since streamMsg's sending buffer is full
dropped MsgAppResp to 247ae21ff9436b2d since streamMsg's sending buffer is full
這些錯誤可以通過提高 Leader 的網絡優先級來提高 follower 的請求的響應。可以通過流量控制機制來提高:
// 針對 2379、2380 埠放行
$ tc qdisc add dev eth0 root handle 1: prio bands 3
$ tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip sport 2380 0xffff flowid 1:1
$ tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip dport 2380 0xffff flowid 1:1
$ tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip sport 2379 0xffff flowid 1:1
$ tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip dport 2379 0xffff flowid 1:1
// 查看現有的隊列
$ tc -s qdisc ls dev enp0s8
qdisc prio 1: root refcnt 2 bands 3 priomap 1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
Sent 258578 bytes 923 pkt (dropped 0, overlimits 0 requeues 0)
backlog 0b 0p requeues 0
// 刪除隊列
$ tc qdisc del dev enp0s8 root
etcd 的硬碟存儲上限(默認是 2GB), 當 etcd 數據量超過默認 quota 值後便不再接受寫請求,可以通過設置 --quota-backend-bytes 參數來增加存儲大小,quota-backend-bytes 默認值為 0,即使用默認 quota 為 2GB,上限值為 8 GB,具體說明可參考官方文檔:dev-guide/limit.md。
The default storage size limit is 2GB, configurable with `--quota-backend-bytes` flag. 8GB is a suggested maximum size for normal environments and etcd warns at startup if the configured value exceeds it.
以下摘自 當 K8s 集群達到萬級規模,阿里巴巴如何解決系統各組件性能問題?
阿里進行了深入研究了 etcd 內部的實現原理,並發現了影響 etcd 擴展性的一個關鍵問題在底層 bbolt db 的 page 頁面分配算法上:隨著 etcd 中存儲的數據量的增長,bbolt db 中線性查找 「連續長度為 n 的 page 存儲頁面」 的性能顯著下降。
為了解決該問題,我們設計了基於 segregrated hashmap 的空閒頁面管理算法,hashmap 以連續 page 大小為 key, 連續頁面起始 page id 為 value。通過查這個 segregrated hashmap 實現 O(1) 的空閒 page 查找,極大地提高了性能。在釋放塊時,新算法嘗試和地址相鄰的 page 合併,並更新 segregrated hashmap。更詳細的算法分析可以見已發表在 CNCF 博客的博文。
通過這個算法改進,我們可以將 etcd 的存儲空間從推薦的 2GB 擴展到 100GB,極大地提高了 etcd 存儲數據的規模,並且讀寫無顯著延遲增長。
pull request :https://github.com/etcd-io/bbolt/pull/141
目前社區已發布的 v3.4 系列版本並沒有說明支持數據規模可達 100 G。
測試環境:本機 mac 使用 virtualbox 安裝 vm,所有 etcd 實例都是運行在在 vm 中的 docker 上
參考官方文檔:https://github.com/etcd-io/etcd/blob/master/Documentation/op-guide/performance.md
安裝 etcd 壓測工具 benchmark:
$ go get go.etcd.io/etcd/tools/benchmark
$ ls $GOPATH/bin
benchmark
本文僅對 etcd v3.3.10 以及 v3.4.1 進行壓測。
部署 etcd 集群以下為腳本示例:
#!/bin/bash
docker ps -a | grep etcd | grep -v k8s
docker rm -f etcd
ETCD_VERSION=3.3.10
TOKEN=my-etcd-token
CLUSTER_STATE=new
NAME_1=etcd-node-0
NAME_2=etcd-node-1
NAME_3=etcd-node-2
HOST_1=192.168.74.36
HOST_2=192.168.74.36
HOST_3=192.168.74.36
CLUSTER=${NAME_1}=http://${HOST_1}:23801,${NAME_2}=http://${HOST_2}:23802,${NAME_3}=http://${HOST_3}:23803
THIS_NAME=${NAME_1}
THIS_IP=${HOST_1}
sudo docker run -d --net=host --name ${THIS_NAME} k8s.gcr.io/etcd:${ETCD_VERSION} \
/usr/local/bin/etcd \
--data-dir=data.etcd --name ${THIS_NAME} \
--initial-advertise-peer-urls http://${THIS_IP}:23801 --listen-peer-urls http://${THIS_IP}:23801 \
--advertise-client-urls http://${THIS_IP}:23791 --listen-client-urls http://${THIS_IP}:23791 \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
THIS_NAME=${NAME_2}
THIS_IP=${HOST_2}
sudo docker run -d --net=host --name ${THIS_NAME} k8s.gcr.io/etcd:${ETCD_VERSION} \
/usr/local/bin/etcd \
--data-dir=data.etcd --name ${THIS_NAME} \
--initial-advertise-peer-urls http://${THIS_IP}:23802 --listen-peer-urls http://${THIS_IP}:23802 \
--advertise-client-urls http://${THIS_IP}:23792 --listen-client-urls http://${THIS_IP}:23792 \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
THIS_NAME=${NAME_3}
THIS_IP=${HOST_3}
sudo docker run -d --net=host --name ${THIS_NAME} k8s.gcr.io/etcd:${ETCD_VERSION} \
/usr/local/bin/etcd \
--data-dir=data.etcd --name ${THIS_NAME} \
--initial-advertise-peer-urls http://${THIS_IP}:23803 --listen-peer-urls http://${THIS_IP}:23803 \
--advertise-client-urls http://${THIS_IP}:23793 --listen-client-urls http://${THIS_IP}:23793 \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
本文主要對不同場景下 etcd 的讀寫操作進行測試,儘管環境有限,但在不同場景下 etcd 的表現還是有區別的。對於寫入測試,按照官方文檔的測試方法指定不同數量的客戶端和連接數以及 key 的大小,對於讀取操作,分別測試了線性化讀取以及串行化讀取,由於 etcd 是強一致性的,其默認讀取測試就是線性化讀取。
etcd v3.3.10寫入測試
// 查看 leader
$ etcdctl member list
// leader
$ benchmark --endpoints="http://192.168.74.36:23791" --target-leader --conns=1 --clients=1 put --key-size=8 --sequential-keys --total=10000 --val-size=256
$ benchmark --endpoints="http://192.168.74.36:23791" --target-leader --conns=100 --clients=1000 put --key-size=8 --sequential-keys --total=100000 --val-size=256
// 所有 members
$ benchmark --endpoints="http://192.168.74.36:23791,http://192.168.74.36:23792,http://192.168.74.36:23793" --target-leader --conns=1 --clients=1 put --key-size=8 --sequential-keys --total=10000 --val-size=256
$ benchmark --endpoints="http://192.168.74.36:23791,http://192.168.74.36:23792,http://192.168.74.36:23793" --target-leader --conns=100 --clients=1000 put --key-size=8 --sequential-keys --total=100000 --val-size=256
讀取測試
$ benchmark --endpoints="http://192.168.74.36:23791,http://192.168.74.36:23792,http://192.168.74.36:23793" --conns=1 --clients=1 range foo --consistency=l --total=10000
$ benchmark --endpoints="http://192.168.74.36:23791,http://192.168.74.36:23792,http://192.168.74.36:23793" --conns=1 --clients=1 range foo --consistency=s --total=10000
$ benchmark --endpoints="http://192.168.74.36:23791,http://192.168.74.36:23792,http://192.168.74.36:23793" --conns=100 --clients=1000 range foo --consistency=l --total=100000
$ benchmark --endpoints="http://192.168.74.36:23791,http://192.168.74.36:23792,http://192.168.74.36:23793" --conns=100 --clients=1000 range foo --consistency=s --total=100000
寫入測試
// 查看 etcd leader
$ etcdctl --write-out=table --endpoints="http://192.168.74.36:23791,http://192.168.74.36:23801" endpoint status
// leader
$ benchmark --endpoints="http://192.168.74.36:23791" --target-leader --conns=1 --clients=1 put --key-size=8 --sequential-keys --total=10000 --val-size=256
$ benchmark --endpoints="http://192.168.74.36:23791" --target-leader --conns=100 --clients=1000 put --key-size=8 --sequential-keys --total=100000 --val-size=256
// 所有 members
$ benchmark --endpoints="http://192.168.74.36:23791,http://192.168.74.36:23792,http://192.168.74.36:23793" --target-leader --conns=1 --clients=1 put --key-size=8 --sequential-keys --total=10000 --val-size=256
$ benchmark --endpoints="http://192.168.74.36:23791,http://192.168.74.36:23792,http://192.168.74.36:23793" --target-leader --conns=100 --clients=1000 put --key-size=8 --sequential-keys --total=100000 --val-size=256
讀取測試
$ benchmark --endpoints="http://192.168.74.36:23791,http://192.168.74.36:23792,http://192.168.74.36:23793" --conns=1 --clients=1 range foo --consistency=l --total=10000
$ benchmark --endpoints="http://192.168.74.36:23791,http://192.168.74.36:23792,http://192.168.74.36:23793" --conns=1 --clients=1 range foo --consistency=s --total=10000
$ benchmark --endpoints="http://192.168.74.36:23791,http://192.168.74.36:23792,http://192.168.74.36:23793" --conns=100 --clients=1000 range foo --consistency=l --total=100000
$ benchmark --endpoints="http://192.168.74.36:23791,http://192.168.74.36:23792,http://192.168.74.36:23793" --conns=100 --clients=1000 range foo --consistency=s --total=100000
由於僅在本地進行測試,所受網絡帶寬影響不大,所以僅調整 io。
可以看到,測試結果中寫入操作與以上列出的幾種因素關聯比較大。讀取指標的時候,串行化要比線性化要好,但為了一致性,線性化 (Linearizable) 讀取請求要通過集群成員的法定人數來獲取最新的數據。串行化 (Serializable) 讀取請求比線性化讀取要廉價一些,因為他們是通過任意單臺 etcd 伺服器來提供服務,而不是成員的法定人數,代價是可能提供過期數據。
本文在力所能及的範圍內對 etcd 的性能進行了一定的評估,所得到的數據並不能作為最終的參考數據,應當根據自己的環境進行評估,結合以上性能優化的方法得到最終的結論。
掃碼關注公眾號
後臺回復◉圖譜◉領取史上最強 Kubernetes 知識圖譜