etcd:增加30%的寫入性能

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:

image

測試寫性能

首先(在單獨的實例上執行)執行etcdctl check perf命令,模擬etcd集羣的負載,並打印結果。可以通過--load參數來模擬不同大小的集羣負載,支持參數爲:s(small), m(medium), l(large), xl(xLarge)

當load爲s時,測試是通過的。

image

但當load爲l時,測試失敗。可以看到,集羣可執行6.6K/s的寫操作,可以認爲我們的集羣介於中等集羣和大型集羣之間。

image

下面是使用iostat展示的磁盤狀態,其中nvme1n1是etcd的write-ahead-log卷,其IO使用率已經達到100%,導致etcd的線程等待IO。

image

下面使用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)的修改,則需要刷新元數據。

image

可以看到這種處理方式對性能的影響比較大。

下表展示了各個卷類型的最大性能,與etcd相關的是Max synchronous write:

image

可以看到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需要根據實際情況進行調整。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章