中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

怎么注入I/O故障

發布時間:2022-01-14 17:49:27 來源:億速云 閱讀:143 作者:iii 欄目:云計算

這篇“怎么注入I/O故障”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“怎么注入I/O故障”文章吧。

在生產環境中,時常會因為磁盤故障、誤操作等原因出現文件系統的錯誤。Chaos Mesh 很早就提供了注入文件系統錯誤的能力。用戶只需要添加一個 IOChaos 資源,就能夠讓對指定文件的文件系統操作失敗或返回錯誤的數據。在 Chaos Mesh 1.0 之前,使用 IOChaos 需要對 Pod 注入 sidecar 容器,并且需要改寫啟動命令;哪怕沒有注入錯誤,被注入 sidecar 的容器也總是有較大的性能開銷。隨著 Chaos Mesh 1.0 的發布,提供了運行時注入文件系統錯誤的功能,使得 IOChaos 的使用和其他所有類型的 Chaos 一樣簡單方便。

前置

本文的內容假定你已經掌握以下知識。當然,你不必在此時就去閱讀;但當遇到沒見過的名詞的時可以回過頭來搜索學習。

我會盡我所能提供相關的學習資料,但我不會將它們提煉和復述,一是因為這些知識通過簡單的 Google 就能學到;二是因為大部分時候學習一手的知識效果遠比二手要好,學習 n 手的知識效果遠比(n+1)手的要好。

  1. FUSE. Wikipedia, man(4)

  2. mount_namespaces. man, k8s Mount propagation

  3. x86 assembly language. Wikipedia

  4. mount. man(2) 特別是 MS_MOVE

  5. Mutating admission webhooks. k8s Document

  6. syscall. man(2) 注意瀏覽一下調用約定

  7. ptrace. man(2)

  8. Device node, char devices Device names, device nodes, and major/minor numbers

閱讀與 TimeChaos 相關的 文章 對理解本文也有很大的幫助,因為它們使用著相似的技術。

此外,我希望在閱讀這份文檔時,讀者能夠主動地思考每一步的原因和效果。這之中沒有復雜的需要頭腦高速運轉的知識,只有一步一步的(給計算機的)行動指南。也希望你能夠在大腦里不斷地構思“如果我自己要實現運行時文件系統注入,應該怎樣做?”,這樣這篇文章就從單純的灌輸變為了見解的交流,會有趣很多。

錯誤注入

尋找錯誤注入方式的一個普遍方法就是先觀察未注入時的調用路徑:我們在 TimeChaos 的實現過程當中,通過觀察應用程序獲取時間的方式,了解到大部分程序會通過 vDSO 訪問時間,從而選取了修改目標程序 vDSO 部分內存來修改時間的方式。

那么在應用程序發起 read, write 等系統調用,到這些請求到達目標文件系統,這之間是否存在可供注入的突破口呢?事實上是存在的,你可以使用 bpf 的方式注入相關的系統調用,但它無法被用于注入延遲。另一種方式就是在目標文件系統前再加一層文件系統,我們暫且稱之為 ChaosFS:

ChaosFS 以本來的目標文件系統作為后端,接受來自操作系統的寫入請求,使得整個調用鏈路變為 Targer Program syscall -> Linux Kernel -> ChaosFS -> Target Filesystem. 由于我們可以自定義 ChaosFS 文件系統的實現,所以可以任意地添加延遲、返回錯誤。

如果你在此時已經開始構思自己的文件系統錯誤注入實現,聰明的你一定已經發現了一些問題:

  1. ChaosFS 如果也要往目標文件系統里讀寫文件,這意味著它的掛載路徑與目標文件夾不同。因為掛載路徑幾乎是訪問一個文件系統唯一的方式了。

即,如果目標程序想要寫入 /mnt/a,于是 ChaosFS 也得掛載于 /mnt/a,那么目標文件夾就不能是 /mnt/a 了!但是 pod 的配置里寫了要把目標文件系統掛載在 /mnt 呀,這可怎么辦。

  1. 這不能滿足運行時注入的要求。因為如果目標程序已經打開了一些原目標系統的文件,那么新掛載的文件系統只對新 open 的文件有效。(更何況還有上述文件系統路徑覆蓋的問題)。想要能夠對目標程序注入文件系統錯誤,必須得在目標進程啟動之前將 ChaosFS 掛載好。

  2. 還得想辦法把文件系統給掛載進目標容器的 mnt namespace 中去。

對于這三個問題,原初的 IOChaos 都是使用 Mutating Webhook 來達成的:

  1. 使用 Mutating Webhook 在目標容器中先運行腳本移動目錄。比如將 /mnt/a 移動至 /mnt/a_bak。這樣一來 ChaosFS 的存儲后端就可以是 /mnt/a_bak 目錄,而自己掛載在 /mnt/a 下了。

  2. 使用 Mutating Webhook 修改 Pod 的啟動命令,比如本身啟動命令是 /app,我們要將它修改成 /waitfs.sh /app,而我們提供的 waitfs.sh 會不斷檢查文件系統是否已經掛載成功,如果已經成功就再啟動 /app

  3. 自然的,我們依舊使用 Mutating Webhook 來在 Pod 中多加入一個容器用于運行 ChaosFS。運行 ChaosFS 的容器需要與目標容器共享某個 volume,比如 /mnt。然后將它掛載至目標目錄,比如 /mnt/a。同時開啟適當的 mount propagation ,來讓 ChaosFS 容器的 volume 中的掛載穿透(share)至 host,再由 host 穿透(slave)至目標。(如果你了解 mnt namespace 和 mount ,那么一定知道 share 和 slave 是什么意思)。

這樣一來,就完成了對目標程序 IO 過程的注入。但它是如此的不好用:

  1. 只能對某個 volume 的子目錄注入,而無法對整個 volume 注入。

  2. 要求 Pod 中明文寫有 command,而不能是隱含使用鏡像的 command 。因為如果使用鏡像隱含的 command 的話,/waitfs.sh 就不知道在掛載成功之后應該如何啟動應用了。

  3. 要求對應容器有足夠的 mount propagation 的配置。當然我們可以在 Mutating Webhook 里偷偷摸摸加上,但動用戶的容器總是不太妙的(甚至可能引發安全問題)。

  4. 注入配置要填的東西太多啦!配置起來真麻煩。而且在配置完成之后還得新建 pod 才能被注入。

  5. 無法在運行時撤出 ChaosFS,所以哪怕不施加延遲或錯誤,仍然對性能有不小的影響。

其中第一個問題是可以克服的,只要用 mount move 來代替 mv(rename),就可以移動目標 volume 的掛載點。而后面幾個問題就不那么好克服了。

運行時注入錯誤

結合使用你擁有的其他知識(比如 namespace 的知識和 ptrace 的用法),重新審視這兩點,就能找到解決的辦法。我們完全依賴 Mutating Webhook 來構造了這個實現,但大部分的糟糕之處也都是由 Mutating Webhook 的方法帶來的。(如果你喜歡,可以管這種方法叫做 Sidecar 的方法。很多項目都這么叫,但是這種稱呼將實現給隱藏了,也沒省太多字,我不是很喜歡)。接下來我們將展示如何不使用 Mutating Webhook 來達到以上目的。

侵入命名空間

我們使用 Mutating Webhook 添加一個用于運行 ChaosFS 的容器的目的是為了通過 mount propagation 的機制將文件系統掛載至目標容器內。而要達到這個目的并非只有這一種選擇 —— 我們還可以直接使用 Linux 提供的 setns 系統調用來修改當前進程的 namespace。事實上在 Chaos Mesh 的大部分實現中都使用了 nsenter 命令、setns 系統調用等方式來進入目標容器的 namespace,而非向 Pod 中添加容器。這是因為前者在使用時更加方便,開發時也更加靈活。

也就是說可以先通過 setns 來讓當前線程進入目標容器的 mnt namespace,然后在這個 namespace 中調用 mount 等系統調用完成 ChaosFS 的掛載。

假定我們需要注入的文件系統是 /mnt

  1. 通過 setns 讓當前線程進入目標容器的 mnt namespace;

  2. 通過 mount --move 將 /mnt 移動至 /mnt_bak

  3. 將 ChaosFS 掛載至 /mnt,并以 /mnt_bak 為存儲后端。

可以看到,這時注入流程已經大致完成了,目標容器如果再次打開、讀寫 /mnt 中的文件,就會通過 ChaosFS,從而被它注入延遲或錯誤。

而它還剩下兩個問題:

  1. 目標進程已經打開的文件該怎么辦?

  2. 該如何恢復?畢竟在有文件被打開的情況下是無法 umount 的。

后文將用同一個手段解決這兩個問題:使用 ptrace 的方法在運行時替換已經打開的 fd。(本文以 fd 為例,事實上除了 fd 還有 cwd,mmap 等需要替換,實現方式是相似的,就不單獨描述了)

動態替換 fd

我們主要使用 ptrace 來對 fd 進行動態地替換,在介紹具體的方法之前,不妨先感受一下 ptrace 的威力:

  1. 使用 ptrace 能夠讓 tracee(被 ptrace 的線程) 運行任意系統調用這是怎么做到的呢?綜合運用 ptrace 和 x86_64 的知識來看這個問題并不算難。由于 ptrace 可以修改寄存器,同時 x86_64 架構中 rip 寄存器(instruction pointer)總是指向下一個要運行的指令的地址,所以只需要將當前 rip 指向的部分內存修改為 0x050f (對應 syscall 指令),再依照系統調用的調用約定將各個寄存器的值設為對應的系統調用編號或參數,然后使用 ptrace 單步執行,就能從 rax 寄存器中拿到系統調用的返回值。在完成調用之后記得將寄存器和修改的內存都復原。

在以上過程中使用了 ptrace 的 POKE_TEXTSETREGSGETREGSSINGLESTEP 等功能,如果不熟悉可以查閱 ptrace 的手冊。

  1. 使用 ptrace 能夠讓 tracee(指 ptrace 的目標進程) 運行任意二進制程序。

    運行任意二進制程序的思路是類似的。可以與運行系統調用一樣,將 rip 后一部分的內訓修改為自己想要運行的程序,并在程序末尾加上 int3 指令以觸發斷點。在執行完成之后恢復目標程序的寄存器和內存就好了。

    而事實上我們可以選用一種稍稍干凈些的方式:使用 ptrace 在目標程序中調用 mmap,分配出需要的內存,然后將二進制程序寫入新分配出的內存區域中,將 rip 指向它。在運行結束之后調用 munmap 就能保持內存區域的干凈。

在實踐中,我們使用 process_vm_writev 代替了使用 ptrace POKE_TEXT 寫入,在寫入大量內容的時候它更加穩定高效一些。

在擁有以上手段之后,如果一個進程自己有辦法替換自己的 fd,那么通過 ptrace,就能讓它運行同樣的一段程序來替換 fd。這樣一來問題就簡單了:我們只需要找到一個進程自己替換自己的 fd 的方法。如果對 Linux 的系統調用較為熟悉的話,馬上就能找到答案:dup2。

使用 dup2 替換 fd

dup2 的函數簽名是 int dup2(int oldfd, int newfd);,它的作用是創建一份 oldfd 的拷貝,并且這個拷貝的 fd 號是 newfd。如果 newfd 原本就有打開著的 fd ,它會被自動地 close。

假定現在進程正打開著 /var/run/__chaosfs__test__/a ,fd 為 1 ,希望替換成 /var/run/test/a,那么它需要做的事情有:

  1. 使用通過 fcntl 系統調用獲取 /var/run/__chaosfs__test__/a 的 OFlags(即 open 調用時的參數,比如 O_WRONLY );

  2. 使用 lseek 系統調用獲取當前的 seek 位置;

  3. 使用 open 系統調用,以相同的 OFlags 打開 /var/run/test/a,假設 fd 為 2;

  4. 使用 lseek 改變新打開的 fd 2 的 seek 位置;

  5. 使用 dup2(2, 1) 用新打開的 fd 2 來替換 /var/run/__chaosfs__test__/a 的 fd 1;

  6. 將 fd 2 關掉。

這樣之后,當前進程的 fd 1 就會指向 /var/run/test/a,任何對于它的操作都會通過 FUSE,能夠被注入錯誤了。

使用 ptrace 讓目標進程運行替換 fd 的程序

那么只要結合“使用 ptrace 能夠讓 tracee 運行任意二進制程序”的知識和“使用dup2替換自己已經打開的fd”的方法,就能夠讓 tracee 自己把已經打開的 fd 給替換掉啦!

對照前文描述的步驟,結合 syscall 指令的用法,寫出對應的匯編代碼是容易的,你可以在這里看到對應的源碼,使用匯編器可以將它輸出為可供使用的二進制程序(我們使用的是 dynasm-rs)。然后用 ptrace 讓目標進程運行這段程序,就完成了在運行時對 fd 的替換。

讀者可以稍稍思考如何使用類似的方式來改換 cwd,替換 mmap 呢?它們的流程完全是類似的。

注:實現中假定了目標程序依照 Posix Thread,目標進程與它的線程之間共享打開的文件,即 clone 創建線程時指定了 CLONE_FILES。所以將只會對一個線程組的第一個線程進行 fd 替換。

流程總覽

在了解了這一切技術之后,實現運行時文件系統的思路應當已經逐漸清晰了起來。在這一節我將直接展示出整個注入實現的流程圖:

怎么注入I/O故障

平行的數條線表示不同的線程,從左至右依照時間先后順序。可以看到對 “掛載/卸載文件系統 ”和 “進行 fd 等資源的替換” 這兩個任務進行了較為精細的時間順序的安排,這是有必要的。為什么呢?如果讀者對整個過程的了解已經足夠清晰,不妨試著自己思考它的答案。

細枝末節的問題

mnt namespace 可能引發的 mmap 失效

在 mnt namespace 切換之后,已經創建完成的 mmap 是否還有效呢?比如一個 mmap 指向 /a/b,而在切換 mnt namespace 之后 /a/b 消失了,再訪問這個 mmap 時是否會造成意料之外的崩潰呢?值得注意的是,動態鏈接庫全是通過 mmap 載入進內存的,訪問它們是否會有問題呢?

事實上,是不會有問題的。這涉及到 mnt namespace 的方式和目的。mnt namespace 只涉及到對線程可見性的控制,具體的做法,則是在調用 setns 時修改內核中某一線程 task_struct 內 vfsmount 指針的修改,從而當線程使用任何傳入路徑的系統調用的時候(比如 open、rename 等)的時候,Linux 內核內通過 vfsmount 從路徑名查詢到文件(作為 file 結構體),會受到 namespace 的影響。而對于已經打開的 fd(指向一個 file 結構體),它的 open、write、read 等操作直接指向對應文件系統的函數指針,不會受到 namespace 的影響;對于一個已經打開的 mmap (指向一個 address_space 結構體),它的 writepage, readpage 等操作也直接指向對應文件系統的函數指針,也不受到 namespace 的影響。

注入的范圍

由于在注入過程中,不可能將機器上運行的所有進程暫停并檢查它們已經打開的 fd 和 mmap 等資源,這樣做的開銷不可接受。在實踐中,我們選擇預先進入目標容器的 pid namespace,并對這個 namespace 中能看見的所有進程進行暫停和檢查。

所以注入和恢復的范圍是全部 pid namespace 中的進程。而切換 pid namespace 意味著需要預先設定子進程的 pid namespace 再 clone(因為 Linux 并不允許切換當前進程的 pid namespace ),這又將帶來諸多問題。

切換 namespace 對 clone flag 有些限制

切換 mnt namespace 將不允許 clone 時攜帶參數 CLONE_FS。而預先設定好子進程 pid namespace 的情況下,將不允許 clone 時攜帶參數 CLONE_THREAD。為了應對這個問題,我們選擇修改 glibc 的源碼,能夠在 chaos-mesh/toda-glibc 中找到修改后的 glibc 的源碼。修改的只有 pthread 部分 clone 時傳入的參數。

在去掉 CLONE_THREADCLONE_FS 之后,pthread 的表現與原先有較大差異。其中最大的差異便是新建的 pthread 線程不再是原有進程的 tasks,而是一個新的進程,它們的 tgid 是不同的。這樣 pthread 線程之間的關系從進程與tasks變成了進程與子進程。這又會帶來一些麻煩,比如在退出時可能需要對子進程進行額外的清理。

在更低版本的內核中,也不允許不同 pid namespace 的進程共享 SIGHAND,所以還需要把 CLONE_SIGHAND 去掉。

為什么不使用nsenter

在 chaos-daemon 中,很多需要在目標命名空間中的操作都是通過 nsenter 完成的,比如 nsenter iptables 這樣聯合使用。而 nsenter 卻無法應對 IOChaos 的場景,因為如果在進程啟動時就已進入目標 mnt namespace,那將找不到合適的動態鏈接庫(比如 libfuse.so 和自制的 glibc)。

構造 /dev/fuse

由于目標容器中不一定有 /dev/fuse (事實上更可能沒有),所以在進入目標容器的 mnt namespace 后掛載 FUSE 時會遇到錯誤。所以在進入目標的 mnt namespace 后需要構造 /dev/fuse。這個構造的過程還是很容易的,因為 fuse 的 major number 和 minor number 是固定的 10 和 229。所以只要使用 makedev 函數和 mknod 系統調用,就能夠創造出 /dev/fuse 。

去掉 CLONE_THREAD 之后等待子進程死亡的問題

在子進程死亡時,會向父進程發送 SIGCHLD 信號通知自己的死亡。如果父進程沒有妥善的處理這個信號(顯式地忽略或是在信號處理中 wait ),那么子進程就會持續處于 defunct 狀態。

而在我們的場景下,這個問題變得更加復雜了:因為當一個進程的父進程死亡之后,它的父進程會被重新置為它所在的 pid namespace 的 1 號進程。通常來說一個好的 init 進程(比如 systemd )會負責清理這些 defunct 進程,但在容器的場景下,作為 pid 1 的應用通常并沒有被設計為一個好的 init 進程,不會負責處理掉這些 defunct 進程。

為了解決這個問題,我們使用 subreaper 的機制來讓一個進程的父進程死亡時并不是直接將父進程置為 1,而是進程樹上離得最近的 subreaper。然后使用 wait 來等待所有子進程死亡再退出。

waitpid 在不同內核版本下表現不一致

waitpid 在不同版本內核下表現不一致,在較低版本的內核中,對一個作為子線程(指并非主線程的線程)的 tracee 使用 waitpid 會返回 ECHILD ,還沒有確定這樣的原因是什么,也沒有找到相關的文檔。

以上就是關于“怎么注入I/O故障”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

i/o
AI

屏南县| 乐业县| 铁岭县| 祁东县| 信丰县| 鄂尔多斯市| 象山县| 富锦市| 麦盖提县| 扎赉特旗| 江达县| 滁州市| 旬阳县| 临湘市| 巴青县| 琼中| 渑池县| 富川| 盖州市| 龙州县| 化隆| 五指山市| 霸州市| 大连市| 广灵县| 常熟市| 西乌珠穆沁旗| 诏安县| 泰和县| 澎湖县| 汉阴县| 长乐市| 葵青区| 茶陵县| 清徐县| 太原市| 邵阳市| 西乡县| 多伦县| 大冶市| 日土县|