etcd:增加 30- 的寫入性能
本文最終的解決方式很簡單,就是將現有卷升級爲支持更高 IOPS 的卷,但解決問題的過程值得推薦。
譯自:etcd: getting 30% more write/s
我們的團隊看管着大約 30 套自建的 Kubernetes 集羣,最近需要針對 etcd 集羣進行性能分析。
每個 etcd 集羣有 5 個成員,實例型號爲 m6i.xlarge,最大支持 6000 IOPS。每個成員有 3 個卷:
-
root 卷
-
write-ahead-log 的卷
-
數據庫卷
每個卷的型號爲 gp2,大小爲 300gb,最大支持 900 IOPS:
測試寫性能
首先 (在單獨的實例上執行) 執行 etcdctl check perf 命令,模擬 etcd 集羣的負載,並打印結果。可以通過 --load 參數來模擬不同大小的集羣負載,支持參數爲:s(small), m(medium), l(large), xl(xLarge)。
當 load 爲 s 時,測試是通過的。
但當 load 爲 l 時,測試失敗。可以看到,集羣可執行 6.6K/s 的寫操作,可以認爲我們的集羣介於中等集羣和大型集羣之間。
下面是使用 iostat 展示的磁盤狀態,其中 nvme1n1 是 etcd 的 write-ahead-log 卷,其 IO 使用率已經達到 100%,導致 etcd 的線程等待 IO。
下面使用 fio 來查看 fdatasync 的延遲 (見附錄):
fio --rw=write --ioengine=sync --fdatasync=1 --directory=benchmark --size=22m --bs=2300 --name=sandbox
...
Jobs: 1 (f=1): [W(1)][100.0%][w=1594KiB/s][w=709 IOPS][eta 00m:00s]
...
fsync/fdatasync/sync_file_range:
sync (usec): min=476, max=10320, avg=1422.54, stdev=727.83
sync percentiles (usec):
| 1.00th=[ 523], 5.00th=[ 545], 10.00th=[ 570], 20.00th=[ 603],
| 30.00th=[ 660], 40.00th=[ 775], 50.00th=[ 1811], 60.00th=[ 1909],
| 70.00th=[ 1975], 80.00th=[ 2057], 90.00th=[ 2180], 95.00th=[ 2278],
| 99.00th=[ 2671], 99.50th=[ 2933], 99.90th=[ 4621], 99.95th=[ 5538],
| 99.99th=[ 7767]
...
Disk stats (read/write):
nvme1n1: ios=0/21315, merge=0/11364, ticks=0/13865, in_queue=13865, util=99.40%
可以看到 fdatasync 延遲的 99th 百分比爲 2671 usec (或 2.7ms),說明集羣足夠快 (etcd 官方建議最小 10ms)。從上面的輸出還可以看到報告的 IOPS 爲 709,相比 gp2 EBS 卷宣稱的 900 IOPS 來說並不算低。
升級爲 GP3
下面將卷升級爲 GP3(支持最小 3000 IOPS)。
Jobs: 1 (f=1): [W(1)][100.0%][w=2482KiB/s][w=1105 IOPS][eta 00m:00s]
...
iops : min= 912, max= 1140, avg=1040.11, stdev=57.90, samples=19
...
fsync/fdatasync/sync_file_range:
sync (usec): min=327, max=5087, avg=700.24, stdev=240.46
sync percentiles (usec):
| 1.00th=[ 392], 5.00th=[ 429], 10.00th=[ 457], 20.00th=[ 506],
| 30.00th=[ 553], 40.00th=[ 603], 50.00th=[ 652], 60.00th=[ 709],
| 70.00th=[ 734], 80.00th=[ 857], 90.00th=[ 1045], 95.00th=[ 1172],
| 99.00th=[ 1450], 99.50th=[ 1549], 99.90th=[ 1844], 99.95th=[ 1975],
| 99.99th=[ 3556]
...
Disk stats (read/write):
nvme2n1: ios=5628/10328, merge=0/29, ticks=2535/7153, in_queue=9688, util=99.09%
可以看到 IOPS 變爲了 1105,但遠低於預期,通過查看磁盤的使用率,發現瓶頸仍然是 EBS 卷。
鑑於實例類型支持的最大 IOPS 約爲 6000,我決定冒險一試,看看結果如何:
Jobs: 1 (f=1): [W(1)][100.0%][w=2535KiB/s][w=1129 IOPS][eta 00m:00s]
...
fsync/fdatasync/sync_file_range:
sync (usec): min=370, max=3924, avg=611.54, stdev=126.78
sync percentiles (usec):
| 1.00th=[ 420], 5.00th=[ 453], 10.00th=[ 474], 20.00th=[ 506],
| 30.00th=[ 537], 40.00th=[ 562], 50.00th=[ 594], 60.00th=[ 635],
| 70.00th=[ 676], 80.00th=[ 717], 90.00th=[ 734], 95.00th=[ 807],
| 99.00th=[ 963], 99.50th=[ 1057], 99.90th=[ 1254], 99.95th=[ 1336],
| 99.99th=[ 2900]
...
可以看到的確遇到了瓶頸,當 IOPS 規格從 900 變爲 3000 時,實際 IOPS 增加了 30%,但 IOPS 規格從 3000 變爲 6000 時卻沒有什麼變化。
IOPS 到哪裏去了?
操作系統通常會緩存寫操作,當寫操作結束之後,數據仍然存在緩存中,需要等待刷新到磁盤。
數據庫則不同,它需要知道數據寫入的時間和地點。假設一個執行 EFTPOS(電子錢包轉帳) 交易的數據庫被突然重啓,僅僅知道數據被 "最終" 寫入是不夠的。
AWS 在其文檔中提到:
事務敏感的應用對 I/O 延遲比較敏感,適合使用 SSD 卷。可以通過保持低隊列長度和合適的 IOPS 數量來保持高 IOPS,同時降低延遲。持續增加捲的 IOPS 會導致 I/O 延遲的增加。
吞吐量敏感的應用則對 I/O 延遲增加不那麼敏感,適合使用 HDD 卷。可以通過在執行大量順序 I/O 時保持高隊列長度來保證 HDD 卷的高吞吐量。
etcd 在每個事務之後都會使用一個 fdatasync 系統調用,這也是爲什麼在 fio 命令中指定—fdatasync=1
的原因。
fsync() 會將文件描述符 fd 引用的所有 (被修改的) 核心數據刷新到磁盤設備(或其他永久存儲設備),這樣就可以檢索到這些信息(即便系統崩潰或重啓)。該調用在設備返回前會被阻塞,此外,它還會刷新文件的元數據(參見 stat(2))
fdatasync() 類似 fsync(),但不會刷新修改後的元數據 (除非需要該元數據才能正確處理後續的數據檢索)。例如,修改 st_atime 或 st_mtime 並不會刷新,因爲它們不會影響後續數據的讀取,但對文件大小 (st_size) 的修改,則需要刷新元數據。
可以看到這種處理方式對性能的影響比較大。
下表展示了各個卷類型的最大性能,與 etcd 相關的是 Max synchronous write:
可以看到 etcd 的 iops 一方面和自身實現有關,另一方面受到存儲本身的限制。
附錄
使用 Fio 來測試 Etcd 的存儲性能
etcd 集羣的性能嚴重依賴存儲的性能,爲了理解相關的存儲性能,etcd 暴露了一些 Prometheus 指標,其中一個爲
wal_fsync_duration_seconds
,etcd 建議當 99% 的指標值均小於 10ms 時說明存儲足夠快。可以使用 fio 來驗證 etcd 的處理速度,在下面命令中,test-data 爲測試的掛載點目錄:fio --rw=write --ioengine=sync --fdatasync=1 --directory=test-data --size=22m --bs=2300 --name=mytest
在命令輸出中,只需關注 fdatasync 的 99th 百分比是否小於 10ms,在本場景中,爲 2180 微秒,說明存儲足夠快:
fsync/fdatasync/sync_file_range: sync (usec): min=534, max=15766, avg=1273.08, stdev=1084.70 sync percentiles (usec): | 1.00th=[ 553], 5.00th=[ 578], 10.00th=[ 594], 20.00th=[ 627], | 30.00th=[ 709], 40.00th=[ 750], 50.00th=[ 783], 60.00th=[ 1549], | 70.00th=[ 1729], 80.00th=[ 1991], 90.00th=[ 2180], 95.00th=[ 2278], | 99.00th=[ 2376], 99.50th=[ 9634], 99.90th=[15795], 99.95th=[15795], | 99.99th=[15795]
注意:
可以根據特定的場景條件
--size
和--bs
在本例中,fio 是唯一的 I/O,但在實際場景中,除了和
wal_fsync_duration_seconds
相關聯的寫入之外,很可能還會有其他寫入存儲的操作,因此,如果從 fio 觀察到的 99th 百分比略低於 10ms 時,可能並不是因爲存儲不夠快。fio 的版本不能低於 3.5,老版本不支持 fdatasync
Etcd WALs
數據庫通常都會使用 WAL,etcd 也不例外。etcd 會將針對 key-value 存儲的特定操作 (在 apply 前) 寫入 WAL 中,當一個成員崩潰並重啓,就可以通過 WAL 恢復事務處理。
因此,在客戶端添加或更新 key-value 存儲前,etcd 都會將操作記錄到 WAL,在進一步處理前,etcd 必須 100% 保證 WAL 表項被持久化。由於存在緩存,因此僅僅使用
write
系統調用是不夠的。爲了保證數據能夠寫入持久化存儲,需要在write
之後執行fdatasync
系統調用 (這也是 etcd 實際的做法)。使用 fio 訪問存儲
爲了獲得有意義的結果,需要保證 fio 生成的寫入負載和 etcd 寫入 WAL 文件的方式類似。因此 fio 也必須採用順序寫入文件的方式,並在執行 write 系統調用之後再執行
fdatasync
系統調用。爲了達到順序寫的目的,需要指定--rw=write
,爲了保證 fio 使用的是 write 系統調用,而不是其他系統調用 (如 pwrite),需要使用--ioengine=sync
,最後,爲了保證每個 write 調用之後都執行fdatasync
,需要指定--fdatasync=1
,另外兩個參數--size
和--bs
需要根據實際情況進行調整。
鏈接:https://www.cnblogs.com/charlieroro/p/17379469.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/BgUIF37dx-X5t_dhEBDC6g