JuiceFS 在攜程海量冷數據場景下的實踐
作者|妙成,攜程云原生研發工程師,主要從事Elasticsearch、JuiceFS的研發運維,關注分布式數據庫、NoSQL。小峰, 攜程云原生研發工程師,主要專注于數據庫容器化領域,對分布式存儲有濃厚興趣。
一、摘要
攜程的冷數據規模在 10PB+,包括備份數據、圖片語音訓練數據和日志數據等,存儲方案主要是本地磁盤和GlusterFS。在實際使用中這些方案遇到了不少痛點:
- GlusterFS 在單目錄下文件眾多時,ls命令速度很慢;
- 受疫情期間機器采購周期的制約,無法靈活地根據實際需求彈性擴縮容,存儲成本控制困難;
- 磁盤損壞等故障帶來的機器替換和擴縮容操作,使得運維成本居高不下。
隨著云計算技術的發展,公有云廠商為混合云客戶提供了海量冷數據的廉價存儲方案,經過嚴謹的成本計算,我們發現使用公有云的對象存儲可以顯著降低存儲和運維成本。為了減少遷移成本,我們一直在尋找后端存儲能支持各類公有云對象存儲、高性能的文件系統,直到JuiceFS 出現在我們的視野中。JuiceFS有以下優勢:
- POSIX 接口,對應用無侵入
- 強一致性,文件修改立刻可見,為同一個 volume 被多臺機器掛載的場景提供 了close-to-open 保證
- 支持了主流的公有云對象存儲,支持開源軟件作為元數據引擎(Redis、TiKV)等
- 支持云原生,能夠將volume以 CSI 的方式掛載到Pod上
- 社區活躍,代碼更新快
經過大半年的測試和使用,我們已經對接了數據庫備份和 ElasticSearch 冷數據存儲,將2PB+的數據遷移到了JuiceFS,預計后續還會有10PB+的數據接入。目前JuiceFS系統穩定,在降低運維成本和存儲成本方面取得了良好的效果。本文將對JuiceFS原理以及我們在使用中所遇到的問題和采取的優化方案進行簡單介紹。
二、JuiceFS 架構與POC 測試
2.1 架構簡介
JuiceFS 將元數據信息和真實數據塊分開管理,通過 FUSE 實現 POSIX 接口,允許用戶像本地文件系統一樣使用。用戶將文件寫入JuiceFS掛載的目錄后,文件數據會存儲到對象存儲,相應的元數據(文件名、文件大小、權限組、創建修改時間和目錄結構等)會存到元數據引擎中。在該架構下,ls、數據刪除等操作只是對元數據引擎的操作,不受到對象存儲的速度限制,性能上會有較好的保證。
2.2 元數據引擎選型與測試
JuiceFS 的元數據引擎有很多選擇,包括開源的Redis、TiKV以及官方提供的閉源的企業版元數據引擎。考慮到攜程的數據規模較大并且后續會有更多的數據接入,元數據引擎需要能夠支持TB 級元數據的存儲并且能橫向擴容。因此TiKV和官方的企業版元數據引擎成了我們的備選方案。
為了驗證TiKV的性能,我們使用 go-ycsb做了一些性能測試。
機器 | CPU | Memory | Storage | Network |
Node1 | 2 Socket / 20 Core / 40 Thread | 128G | 1.9T SSD | bond0 25G |
Node2 | 2 Socket / 20 Core / 40 Thread | 128G | 960G SSD | bond0 25G |
Node3 | 2 Socket / 20 Core / 40 Thread | 128G | 1.2T SSD | bond0 25G |
測試結果:
1)Write 事務寫入操作,隨著客戶端線程數增加,TPS上升,峰值超過30000
2)Get事務讀取操作, 隨著客戶端線程數增加,QPS上升,單節點峰值接近70000
從測試結果看,TiKV有較高的讀寫吞吐量,并且單次操作的響應時間P99<10ms,在冷數據場景中性能表現可滿足業務需求。
官方的企業版元數據引擎比TiKV有更好的性能表現,但是考慮到冷數據存儲對性能要求并不苛刻,而且相比于對象存儲20~200ms的訪問速度,元數據引擎并不會明顯降低整個系統響應的速度。為了減少技術黑箱,我們選擇了TiKV作為元數據引擎。
2.3 JuiceFS 整體POC測試
在交付生產之前,為了明確SLA指標和最佳使用場景,我們使用mdtest對以TiKV為元數據引擎的JuiceFS進行了整體POC 測試,部署使用如下架構:
1)單線程寫入,測試文件大小與吞吐量的關系
測試結果表明隨著文件大小增大,吞吐量也隨之增大。在單文件為 128MB~256MB 左右時,原先的吞吐量與文件大小的增長曲線明顯放緩。可以理解為當文件較小時,JuiceFS客戶端與元數據引擎和對象存儲的交互成本與有效數據傳輸成本相比,占比較高,限制了吞吐量;當文件較大時,交互成本占比降低,吞吐量上升。為了發揮充分JuiceFS的吞吐能力,建議存儲128MB以上的文件。
2)目錄深度與 JuiceFS IOPS 的關系
測試結果表明目錄深度與 JuiceFS IOPS 沒有明顯關系。研究JuiceFS代碼可知,雖然深度越深,文件路徑變長,但 JuiceFS在創建文件/目錄時在TiKV里的Key是父目錄 inode + 新條目的名字,所以目錄深度不影響TiKV里的鍵值對大小,就不影響TiKV的查詢性能,符合測試結果。
3)目錄大小與 ls速度的關系
單目錄下文件個數 | ls耗時(ms) |
1025 | 25 |
24269 | 31 |
測試結果表明目錄下文件個數對ls幾乎沒有影響。
2.4 元數據引擎故障測試
理論上TiKV 節點中 Region 通過 Raft 保證一致性,即非 Leader Region 故障完全不影響上層應用,Leader Region 故障則會在該 Region 的副本中重新選舉出一個 Leader Region,選舉需要時間,并且需要上報 PD 節點進行處理,因此會影響到上層應用的部分請求。
PD 集群用來管理 TiKV 集群,PD 的非 Leader 節點故障完全不影響上層應用,Leader 節點故障則需要重新選舉新 PD Leader,選舉過程 JuiceFS 客戶端的請求無法得到響應,新 Leader 節點確立后 JuiceFS 重新建立連接也需要一定耗時,該時間段內會對上層應用的請求產生影響。
據此我們模擬節點故障的場景,測試實際應用過程中元數據引擎故障后恢復所需時間,計算正常場景中讀寫一定數量文件與異常情況下的耗時差異。結果表明故障影響時間可以控制在秒級。
1)TiKV 故障
File size/count | 正常 | 異常 | Diff(ms) |
單線程寫 4MiB/1024 | 237035.52 | 249333.76 | 12298.24 |
單線程讀 4MiB/1024 | 360222.72 | 362577.92 | 2355.2 |
2)PD 故障
File size/count | 正常 | 異常 | Diff(ms) |
單線程寫4MiB/1024 | 237035.52 | 247531.52 | 10496 |
單線程讀 4MiB/1024 | 362332.16 | 362577.92 | 245.76 |
三、JuiceFS原理解析
3.1 文件寫入
JuiceFS 接收到寫請求會先將數據寫入 Buffer,并按照 Chunk、Slice、Block 的規則進行數據塊管理,最后以 Slice 為維度Flush到對象存儲。一次 Flush 實質上是對 Slice 中的每個 Block 進行 PUT 操作,將數據寫到對象存儲,并完成元數據修改。如下圖:
- 大文件先經過 FUSE 處理成 128K 的塊,在JuiceFS內部拼成一個個4M大小的Block,Slice 管理的Block不斷增加,直到 Slice 達到 64M(即一個 Chunk 的大小),觸發一次 flush操作。Chunk、Slice、Block 的拼裝使用的是內存buffer,其大小受JuiceFS啟動參數buffer-size 的限制。
- 小文件由新的 Slice 單獨管理,在文件寫入完成時被上傳到對象存儲。
- 如果客戶端設置 writeback 模式,JuiceFS 不會直接寫數據到 Object Storage,而是寫到 JuiceFS 所在機器的本地磁盤,后續異步寫到對象存儲。這種方式存在丟數據的風險,但是可以提升數據寫入速度。
3.2 文件讀取
讀取流程數據處理方式與寫入流程類似,讀取請求被 JuiceFS 進程接收到后會先訪問元數據引擎,找到需要讀取的 Block,向對象存儲并發發出 GET 請求。由于 Block 數據不變性,讀取出的 4M 的 Block 會寫到本地的緩存目錄中。讀取過程中按照 4M(Block) 的方式實現了一定程度的預讀,可以通過調整 prefetch 參數,將預讀窗口設置的更大,默認 prefetch = 1。如下圖:
- 大文件順序讀場景下,會讀取對象存儲中4M 大小的對象,經過 FUSE 處理成 128K 的塊返回給用戶。此場景中緩存命中率會很高,由于預讀和本地Block緩存,吞吐性能較好。
- 大文件隨機讀場景下流程和順序讀一致,該場景下的預讀、緩存被命中的概率很低,這些邏輯反而可能影響讀取性能(需要將讀取到的數據寫入本地緩存目錄),可以通過設置 cache-size = 0 關閉緩存。
- 小文件(例如 4K)讀取場景下,會讀取當前文件的 Block,經過 FUSE 后響應給用戶程序。獲取到的數據也會寫到本地緩存目錄中。
四、故障處理與性能優化
4.1 TiKV CPU使用率過高,導致拒絕服務
現象:TiKV kv_scan請求數突然上升,unified_read_po 線程池CPU使用率被打滿
分析:客戶端運行cleanTrash任務導致的。Beta 1版本的客戶端會同時進行該任務,當同一個volume掛載的客戶端較多,并且trash中的數據量非常多的時候,該任務會對元數據引擎造成突發的壓力。
解決方案:
- 增加客戶端對元數據引擎各個接口的調用量監控,便于快速診斷是哪些客戶端導致的問題;
- 將后臺任務從客戶端中剝離,客戶端只需要執行用戶的請求,cleanTrash這樣的后臺任務交給單獨的組件執行,便于JuiceFS管理員控制;
- 升級客戶端,Beta3開始增加了分布式鎖,并且增加了no-bgjob啟動參數。
4.2 TiKV 數據泄露
現象:文件數目和OSS中的數據量沒有增加的情況下,region數目不斷增加,store size不斷增加
分析:通過tikv-ctl查看TiKV里的數據,發現MVCC的修改和刪除記錄沒有被清除。完整的TiDB部署會10min觸發一次數據GC。但是單獨部署TiKV,數據GC需要由其他程序觸發。另一方面5.0.1版本的TiKV有bug,數據GC沒有清理刪除記錄,相關issue。
解決方案:
- 參考https://github.com/tikv/client-go/blob/v2.0.0/examples/gcworker/gcworker.go單獨實現一個組件,定期調用GC功能
- 升級TiKV到5.0.6
4.3 CSI 掛載場景中,PV 清理后數據 OSS 中數據無法回收
現象:k8s中的ElasticSearch 所有Pod、PVC、PV 下線一天后 OSS 數據仍沒被清理。
分析:PV 被清理時 CSI 執行了 JuiceFS rmr 指令,將 volume 數據全部放到回收站,根據默認配置 trash-day=1,即一天后開始回收數據。由于環境中的 JuiceFS mount Pod 已經全部下線,即沒有 JuiceFS 進程掛載了 CSI 的 volume,于是出現了沒有清理回收站的情況。
解決方案:由于 CSI 模式使用 JuiceFS 是模擬了 subdir 的過程,即整個 CSI 管理 Pod 掛載的 volume 是同一個,通過寫到子目錄的方式進行數據隔離。我們停止了mount pod的所有后臺任務,另外找了一臺機器掛載該 volume來完成自動清理回收站數據等后臺任務,該方法也消除了后臺任務帶來的客戶端性能抖動。
4.4 客戶端使用內存過高
現象:部分使用 JuiceFS 的機器占用內存過高,達到了 20GB+。
分析:
- 通過 cat /proc/$pid/smaps 查看,發現占用的內存都是 Private_Dirty,說明是被 JuiceFS 進程長期持有,不是 Page Cache 緩存占用導致。
- 通過使用 pprof 工具分析 heap 占用情況,可推測出是 dump meta(backup)導致的異常。
解決方案:將客戶端的啟動參數backup-meta默認值改為0,元數據備份參考官方的實現思路通過另外的組件統一實現,客戶端不執行元數據備份任務。
4.5 優化后架構
生產實踐過程中涉及數PB級的數據,業務場景相差巨大,經過規劃與調優,最終演進成如下架構。
- 量級較小的業務由用戶掛載的JuiceFS client治理session、trash等數據。
- 量級較大的業務(PB級、數百client級)掛載的client不處理session、trash等數據的清理(開啟no-bgjob參數),由admin 提供一個client單獨處理,并提供清理加速的能力。
- 提供了一個client統一做同一tikv集群內所有volume的backup-meta操作。
- 提供訪問OSS和TIKV集群的限流能力,通過命令下發修改client的限流能力,用于在必要情況下保護專線帶寬、元數據庫,實現服務降級。
- 使用多套元數據集群用來隔離行為差異較大的業務。
- 提供服務觸發TiKV的GC。
五、總結與展望
通過 JuiceFS 將冷數據上公有云, Elasticsearch 實現了一定程度的存算分離,去除了副本帶來的內存需求,提升整體集群數據存儲能力。DBA 備份數據從 GlusterFS 遷移到 JuiceFS 后 ls 等行為的性能大幅提高,不僅運維人員不再需要投入精力進行磁盤擴容維修,大大降低了運維成本,而且用戶還能夠按照保留時間快速地控制存儲成本。
目前已有2PB 來自 ElasticSearch、DBA 備份的數據存儲到JuiceFS,后續我們會推動更多的數據上JuiceFS,當然后續也很多需要進一步探索和優化的地方,例如:
- 進一步優化元數據引擎 TiKV 的性能與提升 JuiceFS 的穩定性,以應對10PB+的數據量
- 探索JuiceFS在ClickHouse冷數據存儲上的使用方法
- 公有云場景下使用JuiceFS替換HDFS,以降低云上的存儲成本