etcd:增加 30- 的寫入性能

本文最終的解決方式很簡單,就是將現有卷升級爲支持更高 IOPS 的卷,但解決問題的過程值得推薦。

譯自:etcd: getting 30% more write/s

我們的團隊看管着大約 30 套自建的 Kubernetes 集羣,最近需要針對 etcd 集羣進行性能分析。

每個 etcd 集羣有 5 個成員,實例型號爲 m6i.xlarge,最大支持 6000 IOPS。每個成員有 3 個卷:

每個卷的型號爲 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_atimest_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