
拿到kata container virtiofs這個測試任務時我的第一反應是終于不用再跟9p那套共享存儲死磕了。之前線上跑Kata容器凡是涉及掛載宿主機目錄的場景基本都踩過9p的坑幾百個文件的編譯任務能卡到懷疑人生Java進程在共享盤上起不來mmap相關調(diào)用要么報錯要么性能慘不忍睹。這次virtiofs從原理到壓測完整跑了一遍中間還撞了幾個比較隱蔽的穩(wěn)定性問題整理成這篇技術分析給正在做Kata容器存儲選型的同學一個參考。這篇文章不是簡單夸virtiofs有多快而是把測試方案、結(jié)果數(shù)據(jù)、原理拆解和踩坑記錄都攤開講。適合正好在調(diào)Kata運行時、計劃把hostPath遷移到virtiofs、或者在考慮容器和虛擬機混合存儲方案的工程師。1. 為什么Kata Containers的共享存儲會選virtiofs從9p的痛點說起1.1 Kata的存儲路徑到底特殊在哪Kata Containers的本質(zhì)是用輕量虛擬機隔離容器每個Pod其實是一個小虛擬機有自己的內(nèi)核。這個設計帶來一個很直接的問題宿主機目錄要怎么讓虛擬機里的進程看到。普通容器比如純runc靠Linux內(nèi)核共享文件系統(tǒng)層namespace一切目錄直接掛進來性能接近本地盤。Kata不行它和宿主機之間的邊界是虛擬化層文件系統(tǒng)必須穿過這條邊界。早期方案是9p九號項目文件協(xié)議在QEMU里做了個fsdev后端Kata這邊作為客戶端掛載。聽起來不難對吧但實際用起來凡是正經(jīng)業(yè)務負載9p幾乎都能把性能打崩。我把線上幾個真實案例搬過來測試問題立刻暴露。1.2 9p的三大痛點mmap、協(xié)議棧、元數(shù)據(jù)先說mmap。Java服務、Node.js、Nginx這類進程對mmap依賴很重比如JVM加載jar包、共享內(nèi)存映射。9p的mmap實現(xiàn)沒有真正的內(nèi)存映射底層是靠read/write系統(tǒng)調(diào)用模擬的也就是說每次page fault都要向host發(fā)起一輪完整請求。線上Java應用在9p共享盤上偶發(fā)卡頓壓測時直接出現(xiàn)長時間停頓這本身就是不可接受的。再就是協(xié)議開銷。9p是傳統(tǒng)的request/response模型每個操作在guest內(nèi)核、QEMU進程、host VFS之間來回穿梭。單次操作的延遲比本地文件系統(tǒng)高一個量級一旦并發(fā)量上來guest側(cè)的內(nèi)核線程和QEMU進程的CPU就會被協(xié)議處理占滿吞吐上不去。最致命的是元數(shù)據(jù)操作。創(chuàng)建、刪除、stat、readdir這些小文件場景9p基本暴露全部缺陷。我這邊跑過一個簡單的編譯模擬測試創(chuàng)建一萬個小文件再逐個stat9p用了將近三分鐘本地盤不到兩秒。這種差距已經(jīng)不屬于調(diào)優(yōu)能解決的范疇是協(xié)議和應用場景不匹配。1.3 virtiofs的方向轉(zhuǎn)變從通用協(xié)議到專用虛擬設備virtiofs和9p最大的區(qū)別在于它不再把自己當成一個通用網(wǎng)絡文件系統(tǒng)來設計而是專門為“宿主機和虛擬機之間共享文件”這個場景定制的虛擬設備。它走的是virtio設備框架guest里有一個內(nèi)核驅(qū)動通過virtqueue和QEMU通信。文件操作請求采用FUSE協(xié)議格式但不需要經(jīng)過/dev/fuse設備而是直接由內(nèi)核模塊對接virtqueue。host側(cè)則由一個用戶態(tài)守護進程virtiofsd來接收請求轉(zhuǎn)換成普通的Linux系統(tǒng)調(diào)用訪問宿主機文件系統(tǒng)。這個架構上的改變把原來QEMU進程內(nèi)的協(xié)議處理拆出來了。virtiofsd可以獨立調(diào)度、獨立調(diào)優(yōu)而且DAX直通能力也是在virtio框架下才有空間實現(xiàn)。9p是給Plan 9遺產(chǎn)做的通用文件協(xié)議virtiofs則是給虛擬化場景專門畫的一條新賽道。從Kata 2.x開始默認把shared_fs切到virtiofs不是沒有道理的。2. virtiofs的核心機制拆解DAX緩存與FUSE協(xié)議的配合邏輯2.1 FUSE協(xié)議在virtiofs里被重新實現(xiàn)了一遍很多人以為virtiofs就是“虛擬機里掛了個FUSE”這個理解不夠準確。標準FUSE的路徑是用戶態(tài)進程 - /dev/fuse - 內(nèi)核FUSE模塊 - 用戶態(tài)daemon。virtiofs把這條鏈路簡化了guest內(nèi)核的virtio_fs驅(qū)動直接扮演FUSE模塊的角色請求封裝成FUSE消息格式通過virtqueue直接發(fā)給host的virtiofsd。這樣做的好處是省掉了一層字符設備和用戶態(tài)上下文切換。FUSE消息結(jié)構本身已經(jīng)被大量文件系統(tǒng)驗證過協(xié)議字段齊全特別適合做文件語義的精細化控制比如lookup緩存、屬性超時、目錄項的負緩存。virtiofsd收到請求后翻譯成open、read、write、getattr、readdir這些系統(tǒng)調(diào)用命中宿主機的VFS和page cache。所以從功能視角看guest里的文件系統(tǒng)體驗非常接近本地文件系統(tǒng)從性能視角看合理的緩存策略下很多操作根本不需要穿透到host在guest內(nèi)就能完成。2.2 DAX window為什么能繞開guest page cache這是virtiofs最有技術含量的部分。普通文件共享路徑里guest讀文件時數(shù)據(jù)要經(jīng)過guest page cache - virtqueue - QEMU內(nèi)存拷貝 - virtiofsd - host VFS - host page cache。每一層都有開銷尤其是大塊數(shù)據(jù)傳輸內(nèi)存拷貝和CPU占用非??捎^。DAXDirect Access的思路是在虛擬機地址空間里開辟一塊PCI BAR窗口把host端文件頁直接映射進去。guest里的進程mmap一個virtiofs文件時內(nèi)核可以把guest物理地址直接映射到這塊BAR區(qū)域CPU訪問它就像訪問普通內(nèi)存一樣。真正讀到的其實是host page cache里的文件頁寫的時候也直接寫進host page cache。整條鏈路省掉了virtqueue通信和QEMU內(nèi)部的拷貝動作。當然DAX不是萬能的。窗口大小是有限資源由Kata配置里的virtio_fs_cache_size控制映射以2MB大頁粒度對齊文件多而碎的時候很可能把窗口耗盡。窗口耗盡后virtiofs會回退到普通virtqueue路徑性能斷崖下跌。我后面壓測的時候就真實遇到了這個問題一會兒細說。2.3 cache三檔到底怎么選none、auto、alwaysvirtiofs的緩存策略直接決定數(shù)據(jù)一致性和性能表現(xiàn)生產(chǎn)環(huán)境不能只開一個默認值完事。cachenone是所有讀寫都穿透到hostguest不持有page cache。語義最強性能最差適合數(shù)據(jù)庫、分布式存儲這類絕對不能讀到舊數(shù)據(jù)的場景。cacheauto是Kata的默認策略guest可以緩存文件和目錄的屬性以及目錄項但文件數(shù)據(jù)還是要走host。它引入了attr超時機制定期向host校驗文件變化類似NFS的attribute cache行為。大多數(shù)配置類、日志類共享目錄用auto就夠了。cachealways則是最大緩存策略guest對文件數(shù)據(jù)和元數(shù)據(jù)都全面緩存寫回延遲交給guest內(nèi)核管理。性能最好但多客戶端共享同一文件時一致性風險很高。我通常只建議用在單寫多讀、只讀共享這類場景。選擇策略時記住一句話緩存放guest性能好但一致性弱緩存放host一致性穩(wěn)但性能回調(diào)。沒有絕對最優(yōu)要按目錄用途拆分掛載。3. 測試環(huán)境與測試矩陣設計不能只看帶寬和IOPS3.1 壓測環(huán)境說明我這次測試用的是一臺x86服務器32核、128G內(nèi)存系統(tǒng)盤和數(shù)據(jù)盤都是NVMe SSD。宿主機內(nèi)核版本5.15系列Kata Containers版本跑過2.4.x和2.5.x兩代QEMU用的7.x系列。普通容器對照組直接跑在宿主機上。測試方式是用Kata拉起一個完整的容器在里面掛載virtiofs共享目錄然后跑各類基準和模擬負載。對照組則把Kata的shared_fs切回9p同樣基準再跑一遍。最后還有一個本地盤的基準值作為底限參照。這個環(huán)境不算特殊理論上能在大部分主流服務器上復現(xiàn)。3.2 為什么拒絕只跑fio如果只跑fio的順序讀、順序?qū)憊irtiofs和9p的差距并不會顯得特別夸張因為順序IO更多受限于設備本身。真實業(yè)務里共享目錄的痛點基本都在隨機小文件、元數(shù)據(jù)操作和應用對mmap的依賴上這些恰好在純fio簡單模式下會被掩蓋。所以我設計了九個場景覆蓋典型業(yè)務負載方向大文件順序讀寫、4K隨機讀寫、元數(shù)據(jù)密集操作、小文件并發(fā)創(chuàng)建刪除、mmap隨機訪問、帶fsync的數(shù)據(jù)庫模擬、多線程混合讀寫、跨容器同目錄并發(fā)訪問、git倉庫和代碼編譯這種高頻小IO場景。每個場景都有明確的工具、指標和模擬目標。工具方面用了fio做塊IO和混合負載sysbench做文件IO模擬git和編譯任務模擬真實開發(fā)場景Python腳本控制小文件并發(fā)。指標不只記錄帶寬和IOPS還記錄了P99延遲、CPU占用率、耗時總量因為生產(chǎn)上更關心長尾延遲。3.3 Kata配置改動細節(jié)在Kata的configuration.toml里核心配置就三塊shared_fs virtio-fs virtio_fs_cache_size 8192 virtio_fs_extra_args [-o, cacheauto]shared_fs指定使用virtiofsvirtio_fs_cache_size控制DAX窗口大小單位MBextra_args里可以傳virtiofsd的參數(shù)。對照組我把shared_fs改回9p即可其他參數(shù)保持不動。這里特別提醒一點修改cache策略后Kata需要重啟運行時和Pod才能生效不能熱更新。測試之前務必確認running Pod已經(jīng)被新配置接管否則結(jié)果會串。4. 性能實測對比九種場景下virtiofs與9p的真實差距4.1 結(jié)果總覽virtiofs全面領先但仍不是本地盤所有場景我都以本地盤作為100%基準對比9p和virtiofs的表現(xiàn)。表格里的數(shù)據(jù)是我這個環(huán)境下的實測值用來定性和看趨勢沒問題換到不同硬件會有浮動但比例關系基本可以參考。測試場景9p相對性能virtiofs相對性能備注順序讀大文件35%88%virtiofs接近本地盤順序?qū)懘笪募?2%64%寫路徑開銷明顯4K隨機讀9%72%9p災難級表現(xiàn)4K隨機寫7%41%寫放大問題需關注元數(shù)據(jù)密集操作3%58%9p基本不可用小文件并發(fā)創(chuàng)建刪除4%52%目錄項緩存有作用mmap隨機訪問失敗/極慢67%DAX直通效果明顯帶fsync的數(shù)據(jù)庫模擬2%38%fsync是最大瓶頸混合讀寫業(yè)務負載12%61%綜合表現(xiàn)參考9p在4K隨機讀上只有本地盤的9%這解釋了很多線上卡頓。virtiofs在隨機讀、mmap上表現(xiàn)最好幾乎追上本地盤但隨機寫和fsync場景還是明顯落后這反映了寫路徑的固有成本。4.2 好消息和壞消息哪些場景能放心用先說好消息。隨機讀、順序讀、mmap三個場景virtiofs表現(xiàn)非常接近本地盤尤其是DAX直通模式下讀取大文件幾乎沒有協(xié)議開銷。這類負載包括代碼目錄共享、模型文件讀取、只讀配置加載、日志歸集讀取直接用virtiofs放心。元數(shù)據(jù)操作方面virtiofs比9p提升了一個數(shù)量級。它借助FUSE協(xié)議的屬性緩存和目錄項緩存在cacheauto下表現(xiàn)已經(jīng)可接受。代碼編譯這種大量創(chuàng)建臨時文件再刪除的場景virtiofs跑下來耗時大約是本地盤的1.8倍9p則要慢將近二十倍。壞消息集中在寫路徑。順序?qū)懙南鄬π阅苤挥?4%隨機寫只有41%fsync場景更是掉到38%。寫操作在DAX模式下直接寫host page cachevirtiofsd后續(xù)還要維護一致性這里面有額外開銷。再加上guest和host兩層page cache寫放大問題在隨機小寫時比較明顯。4.3 并發(fā)上去了性能反而掉下來測試中我特別觀察了并發(fā)線程數(shù)從1逐步增加到32時的表現(xiàn)。virtiofs在小并發(fā)下表現(xiàn)優(yōu)秀但并發(fā)超過一定閾值后性能曲線先漲后跌出現(xiàn)了明顯的吞吐回退。host側(cè)的virtiofsd進程CPU占用率一路上漲最終成為瓶頸。9p在同樣并發(fā)測試中更慘線程數(shù)即使只有8就出現(xiàn)大量的IO等待整個容器幾乎卡死。virtiofs至少能撐到更高并發(fā)但也不是無限可擴展的。這個問題在Kata容器密度較高、多個Pod同時掛載相同共享目錄時會放大。后來排查發(fā)現(xiàn)virtiofsd在較老版本里線程模型是受限的大量并發(fā)請求集中在同一個工作線程上處理CPU單核跑滿后其余請求全部排隊。官方后來的實現(xiàn)逐步改進了并發(fā)模型但如果你還在跑老版本壓測時最好把并發(fā)摸清楚別被小并發(fā)數(shù)據(jù)迷惑。5. 穩(wěn)定性與異常場景踩坑記錄鎖語義、崩潰恢復和緩存一致性5.1 多容器同目錄并發(fā)flock能用但寫緩存會打架測試時我用兩個Kata容器同時掛載同一宿主目錄通過flock對同一個文件做互斥寫。實測下來flock在virtiofs上是能工作的因為virtiofsd最終在host上以普通文件方式打開文件所有鎖都匯聚到host內(nèi)核處理跨虛擬機的鎖語義是可靠的。但有個問題容易踩坑當兩個容器都用cacheauto或always時guest各自保留文件數(shù)據(jù)緩存。一個容器寫入數(shù)據(jù)后另一個容器可能在一段時間內(nèi)看不到最新內(nèi)容即使鎖已經(jīng)釋放。鎖解決的是寫互斥并不解決緩存刷新。這在分布式任務共享狀態(tài)文件時會帶來詭異問題。我的處理方案是涉及多客戶端讀寫的目錄強制用cachenone寧可比性能也不賭一致性。5.2 fuse request timeout導致的I/O error高并發(fā)壓測時guest里偶爾報出I/O error應用直接異常退出。起初以為是磁盤問題后來反復復現(xiàn)發(fā)現(xiàn)是FUSE請求超時機制在起作用。當virtiofsd處理不過來guest發(fā)出的請求長時間得不到響應guest內(nèi)核FUSE模塊就會標記連接異常并返回EIO。這個問題的觸發(fā)點有兩個一是virtiofsd單線程瓶頸二是guest側(cè)請求隊列積壓。我臨時把并發(fā)和iodepth降下來才能穩(wěn)定跑完測試后來調(diào)整了virtiofsd的并發(fā)配置并監(jiān)控host側(cè)CPU才把它壓住。碰到I/O error時不要急著懷疑文件系統(tǒng)損壞先看host側(cè)virtiofsd進程是不是已經(jīng)CPU跑滿再檢查guest內(nèi)核日志里的fuse超時記錄。有條件的情況下?lián)Q用新版運行時并適當增大FUSE超時配置能有效規(guī)避這類假故障。5.3 mmap MAP_SHARED在業(yè)務里的真實表現(xiàn)DAX直通對mmap的支持是virtiofs相對9p的最大優(yōu)勢之一。我在測試里用兩個進程共享一個映射文件做狀態(tài)同步運行良好互相修改立即可見性能接近本地共享內(nèi)存映射。但分布式場景下要清醒認識一點mmap寫的數(shù)據(jù)先落在host page cache真正落盤要等host內(nèi)的pdflush機制觸發(fā)。如果host突然斷電所有落在page cache里但未寫盤的數(shù)據(jù)都會丟失。9p時代大家習慣了慢反而不會丟太多virtiofs快了數(shù)據(jù)丟失窗口也變大了。數(shù)據(jù)庫和消息隊列這類數(shù)據(jù)不能丟的組件哪怕virtiofs支持mmap也不建議把數(shù)據(jù)目錄放上來。mmap共享文件做狀態(tài)同步、鎖協(xié)調(diào)這類非關鍵數(shù)據(jù)場景才是它的正確戰(zhàn)場。5.4 數(shù)據(jù)庫類負載能跑但別勉強我用sysbench在virtiofs上模擬了數(shù)據(jù)庫IO結(jié)果能跑但絕不驚艷。O_DIRECT支持方面測試下來virtiofs在打開文件時會傳遞對齊的direct IO請求能正常工作但隨機寫和fsync的短板仍然存在。數(shù)據(jù)庫的崩潰恢復高度依賴fsync速度和寫順序這恰好是virtiofs最弱的地方。如果業(yè)務方堅持把MySQL這類組件放到共享目錄我的建議是數(shù)據(jù)目錄用塊存儲或本地盤只有配置文件、臨時目錄這類非關鍵數(shù)據(jù)放到virtiofs。拿virtiofs扛數(shù)據(jù)庫性能上去了但可靠性在下降屬于拿核心資產(chǎn)換便利不劃算。6. 生產(chǎn)配置建議與最后的選型邊界6.1 我們線上最終采用的Kata配置經(jīng)歷了這輪測試后我線上環(huán)境給出了一個相對保守但穩(wěn)定的配置方案shared_fs virtio-fs virtio_fs_cache_size 4096 virtio_fs_extra_args [-o, cacheauto]cache選auto保證默認情況下元數(shù)據(jù)有緩存文件數(shù)據(jù)由host控制一致性。如果業(yè)務目錄是單寫多讀而且對性能有極致要求再單獨加一份extra_args切換成cachealways。DAX窗口我設了4G。窗口太大會占掉guest內(nèi)存地址空間太小在大文件并行讀寫時很快耗盡回退到普通路徑。4G對大部分共享目錄已經(jīng)夠用。如果你需要看具體填充情況可以在guest里用mmap映射后觀察后端命中的比例但我實測很難拿到準確數(shù)字更多是靠壓測判斷窗口是否夠用。6.2 掛載規(guī)劃不同用途目錄用不同策略生產(chǎn)上不建議一個共享目錄包打天下。我現(xiàn)在的做法是把共享存儲按用途拆成三個掛載點配置和只讀數(shù)據(jù)用cachealways追求最快的重復讀。代碼目錄和工作目錄用cacheauto兼顧性能和一致性??缛萜鲄f(xié)作的狀態(tài)目錄用cachenone保證任何寫入都能被其他消費者盡快看到。三種目錄落在相同的物理路徑池里只是掛載參數(shù)不同。這樣拆下來每一類目錄都能在自己的策略下達到最優(yōu)狀態(tài)也避免了緩存語義互相干擾。6.3 什么場景還是別用virtiofsvirtiofs不是萬能的。數(shù)據(jù)庫數(shù)據(jù)目錄、分布式存儲WAL日志、內(nèi)容尋址存儲的臨時寫緩沖這幾類對寫延遲和fsync可靠性要求極高的場景我仍然堅持用本地塊設備或者專門的存儲方案。虛擬化文件共享層天然存在協(xié)議轉(zhuǎn)換和雙緩存無論怎么優(yōu)化都不可能比直通塊設備更穩(wěn)。同樣如果你需要多個Kata虛擬機強一致地共享同一個文件并且要求實時可見virtiofs即使配cachenone也不是最佳選擇。這種場景最好的歸宿仍然是分布式存儲用戶態(tài)協(xié)議或者直接共享塊設備由guest自己跑文件系統(tǒng)。6.4 測試過程中值得繼續(xù)深挖的方向virtiofs還在快速演進。新的virtiofsd實現(xiàn)改了線程模型高并發(fā)吞吐相比早期版本有明顯提升。DAX機制上也有針對不同文件粒度做單獨緩存控制的嘗試未來配置會更靈活。如果后續(xù)還有精力我打算做兩件事一是把virtiofs和宿主機本地存儲做一層組合只把熱數(shù)據(jù)放共享盤冷數(shù)據(jù)回本地看看能否兼顧共享和性能二是針對多Kata實例同時掛載同一大文件做并發(fā)模式測試把剛才提到的緩存一致性問題量化成工程指標。這兩個方向弄明白Kata共享存儲這塊就算徹底盤清楚了。寫這篇分析的時候我回想最開始被9p折磨的日子還是要感慨一句virtiofs出現(xiàn)之后Kata Container的共享目錄體驗確實是質(zhì)的飛躍。但所有性能優(yōu)勢都必須建立在正確的cache策略和使用邊界之上。把benchmark數(shù)據(jù)當成衡量標準之前先想清楚你的業(yè)務到底在哪個象限里這才是在生產(chǎn)環(huán)境真正穩(wěn)妥的做法。