packetdrill 简介
本文內容是 2013 年 Google 對 packetdrill 的論文翻譯。
網絡協議測試很麻煩,線上的網絡問題往往都是偶發的,難以捕捉。
packetdrill 是一個跨平臺的腳本工具,可以用來測試整個 TCP/UDP/IP 網絡棧實現的正確性和性能,從系統調用一直到硬件網絡接口,從 IPv4 到 IPv6。
該工具對 Google 工程師研發 Linux TCP 中的 Early Retransmit,Fast Open,Loss Probes 這些新功能也起到了很重要的作用,并幫助工程師找到了 10 個 Linux 自身的 bug。該工具在 Google 內部進行內核研發的各個階段都發揮了價值。
簡介
網絡協議在現代計算機系統中非常重要,但是在實際開發工作中,這些協議只是在部署前做一些臨時的測試,并在上線后經常出各種各樣的問題。宏觀來說這是因為網絡開發本身確實很復雜。比如 TCP 的 roadmap RFC 包含了 32 個其它的 RFC 文檔。Linux 實現了其中的大多數特性。但是現在依然有新的算法涌現,并且會和既有的網絡特性進行交互,在這個前提下 TCP 越來越復雜,測試起來也越來越麻煩。Google 給 Linux TCP 開發了很多特性,同時也在測試這些特性的時候面臨著很大的挑戰。主要是因為彼此關聯的組件實在太多了:應用層,內核,驅動,網絡接口和網絡。基于以下原因,不得不搞一個專門的測試工具了:
新特性開發:給 TCP 開發新特性經常依賴在生產環境上打測試 patch,或者在模擬的網絡情境下工作。要造出這些場景都非常費時間。給生產環境打 patch 風險高并且完全沒法自動化,不可重復。搞虛擬的環境又非常的不現實,不一定能有真實環境的效果。
回歸測試:雖然測試整體性能比較有用,但是基于 netperf, 或者應用壓測或者生產環境的負載模擬出來的 TCP 回歸測試仍然可能沒辦法發現一些擁塞控制、loss recovery,流控,安全,DoS 或者協議狀態機方面的復雜 bug。這些過程還會受到測試環境或內容所產生的噪音干擾,并且并不準確和獨立;在這種環境下也可能很難發現一些潛在的 bug。
問題定位:復現 TCP bug 非常有挑戰,并且需要開發者去修改內核來收集相關的之間。但是生產環境修改風險過高,且需要經過多次高昂的迭代成本。需要一個專門的工具來在非生產環境的機器重現問題的 trace 流程。
packetdrill 就是基于這些原因產生的工具,可以用精確、可復現、自動化的腳本來測試整個網絡協議棧。使用起來也滿足設計目標:
方便:開發者可以快速學習 packetdrill 的語法,不需要理解 packetdrill 或者協議的內部實現。packetdrill 的語法對于腳本作者來說,可以很方便地將 packet traces 轉成測試腳本。工具是實時運行,所以測試一般在一秒內也就能跑完,可以快速迭代。
真實環境:packetdrill 是和 packet 和 syscall 打交道的,是使用真實、精確的事件序列來測試精確的內核鏡像,在物理機上是實時運行。并且和真實是物理網卡、真實的驅動、真實的線纜、真實的交換機等設備一起運行。不需要依賴虛擬機,或者用戶態的虛擬機,或者模擬網絡或者 TCP 的近似模型。
可復現:可以穩定地產生和測試腳本同樣的時間序列,有較高的成功率,盡管 2500 次可能會產生一次失敗。
通用:可以跑 IPv4,IPv6 的腳本,并且支持 IPv4-mapped IPv6 模式。可以在 Linux,FreeBSD,OpenBSD,NetBSD 上跑,跨一切 POSIX 類平臺,只要平臺支持 libpcap 抓包和注入庫就可以。同時可以由協議的實現者用新的算法來進行擴展,因為這個庫本身是開源的。
這個庫本身在開發環境和生產環境中都能產生作用。開發 feature 的時候,用他來寫 unit test,并使我們可以實踐 TDD,增量地測試復雜的 TCP 新特性非常重要。用他來做回歸測試也很簡單。代碼跑在生產環境以后,我們用它來做隔離和復現 bugs 也可以。packetdrill 提供了簡明但準確的語言來討論 TCP 的各種場景,可以用在 bug report 和 email 討論時。
設計
腳本語言
packetdrill 是完全腳本驅動的,這樣使其交互非常方便。packetdrill 腳本使用了我們設計的一種語言,這種語言對用習慣了 tcpdump 和 strace 的網絡工程師來說應該看起來非常面熟。語言有四種語句:
? Packets, 使用了類似 tcpdump 的語法,包括 TCP, UDP, ICMP packets, 以及常見的 TCP options: SACK, Timestamp, MSS, window scale, Fast Open
? System calls, 使用類似 strace 的語法
? 用反引號包住的 shell 命令,這樣可以對系統進行配置或者用 ss 之類的工具對網絡棧的狀態進行斷言 ? 用Python scripts enclosed in %{}% 包住的 Python 腳本,使我們可以進行輸出或者進行 Linux 和 FreeBSD 操作系統為 TCP sockets 暴露的 tcp_info 狀態斷言
執行模型
packetdrill 解析整個 test 腳本,并按照腳本里的時間戳步驟來回放所有帶時間戳的行,并對場景進行驗證。對于每一行系統調用,packetdrill 會執行這個系統調用,并驗證其是否返回了期望的結果。對于每個命令行命令,packetdrill 執行這個 shell 命令。對于每個 incoming 包(在行首用 < 來標記),packetdrill 構造一個包并把它注入到內核。對于每一個 outgoing 的包(在行首用 > 來標記),packetdrill 會嗅探下一個 outgoing 的包并驗證這個包的時機和內容和腳本的內容相符。
考慮圖 1 的腳本樣例,這個例子的 packetdrill 腳本測試 TCP fast retransmit。這個測試在 Linux,FreeBSD,OpenBSD 和 NetBSD 上用真實的網卡都應該是能通過的。腳本以一個典型的打開一個 socket(1-4行)為例并建立一條連接(5-8行)。在把數據寫入到 socket(9 行)后,腳本期望測試的網絡棧發送一個數據包(10 行),然后腳本讓 packetdrill 注入一個 ACK 包(11 行) 讓網絡棧去處理。腳本會驗證 fast retransmit 在三次重復的 ack 到達后會被觸發。
本地和遠程測試
packetdrill 有兩種測試模式:本地模式使用虛擬的網絡設備通道,真實模式使用物理網卡。本地模式 packetdrill 使用一臺機器和虛擬的網絡設備同時作為包的 source 和 sink。這樣可以測試系統調用,sockets,TCP 和 IP 層,這種模式驗證起來也比較簡單,因為沒有多臺機器的交互,沒有網絡延遲。遠程模式,用戶需要運行兩個 packetdrill 進程,其中一個在遠程機器上運行并通過 LAN 與其它節點交互。這種流程能夠驗證整個網絡系統:系統調用,sockets,TCP,IP 軟件和硬件的 offload 策略,物理網卡驅動,網卡硬件,線纜,路由器。然而,因為要走網絡交互,所以實際的時間誤差會比較大,可能會導致一些隨機的測試失敗。
實現
packetdrill 是用 C 寫的完全用戶態的應用,完全遵循 Linux 內核的代碼風格來方便在內核的測試環境中使用。本節深入探討這個工具的實現細節。
組件
Lexer and Parser
為了通用性和擴展性,我們分別用 flex 和 bison 來生成 packetdrill 的 lexer 和 parser。腳本語言的結構很簡單,并且包含有 c/c++ 風格的注釋。
解析器
packetdrill 解釋器開啟一個單獨的線程來處理事件事件的主流程,和另外一個線程來執行那些會阻塞的系統調用(比如 poll)。
Packet 事件?為了方便,腳本用一種抽象符號來標記數據包。在 packetdrill 內部會對 TCP 和 UDP 行為進行建模,維護從腳本中的值到真實數據包的映射。這個翻譯過程包括 IP,UDP 和 TCP header 字段,TCP 的選項(比如 SACK 和時間戳)。因此我們會跟蹤每一個 socket 和它的 IP 地址,端口號,TCP 序列號,TCP 時間戳。
對于 outbound 的 packet 事件我們會馬上開始嗅探,以檢測到腳本指定的包之前的任意 packet。當嗅探一個 outbound 的包時,我們會找到那個發出這個包的 socket,并驗證這個包是在期望的時間被發送。然后將這個包翻譯為一段等價的腳本,并用翻譯后的腳本與腳本中的 bits 做等價驗證。
對于 inbound 的 packet 事件,我們會暫停指定的時間,然后將腳本的值構造為一個等價的 packet,并把這個包注入到 kernel,這樣我們測試的網絡棧就可以處理這個 packet 了
為了嗅探流出的 packets,我們使用了 packet socket(在 linux 平臺) 或者 libpcap(在 BSD 類的操作系統中)。本地模式注入 packets,我們使用 TUN 設備,遠程模式注入 packet,我們用 libpcap。本地模式時,為了消費測試 packets 我們使用了 TUN 設備;遠程模式 packet 會流向物理網絡,并被遠端的 kernel drop 掉 ,因為沒有和遠端 IP 地址對應的網卡(interfae)。
在 packetdrill 腳本中,一些向外流出的 TCP 包是可選的。這樣可以讓我們簡化測試,只聚焦在單一的行為領域就行了,也簡化了腳本的維護,通過避免那些協議棧的差別(與當前正在編寫的測試沒關系的那些網絡協議棧差別),使跨平臺成為可能。舉個例子,寫測試腳本的時候,可以把 TCP receive window 給省略掉 ,或者用一個 <...> 的記號表示 TCP options。這里如果指定了的話,測試過程會檢查;但沒指定的話,測試就直接忽略這些細節了。比如在圖 1 中的 <...> 用在 SYN/ACK packet 上,在各種不同的操作系統,就忽略了這里的一些細節區別。
系統調用?對于非阻塞的系統調用事件,我們會直接在主線程中調用系統調用。對于阻塞調用,我們會把事件推進事件隊列,并向單獨的系統調用線程發信號。主線程之后等待系統調用線程被阻塞或者完成這次調用。
在執行系統調用的時候,腳本里的那些表達式會被翻譯成等價的參數,并傳遞給該調用。當調用返回時,會對輸出進行校驗,內容包括 errno 和腳本的期望輸出。
Shell 命令?packetdrill 使用 system 命令來執行 shell 命令。
Python 腳本?packetdrill 執行 Python 的程序片段來記錄 socket 的 tcp_info 結構體,并生成 Python 代碼來導出這些數據,在測試結束后會用 Python 解析器來做結果校驗。
Handling Variation
網絡協議特性
packetdrill 支持很多協議特性。開發者可以在不修改腳本的情況下直接測試 IPv4,IPv6,IPv4-mapped IPv6 模式,只要用命令行 flag 指定地址模式和 MTU 大小就可以了。除了 IPv4,IPv6,TCP 和 UDP 之外,還支持 ECN 和 inbound ICMP(主要是為了 path MTU discovery)。給 packetdrill 增加那些基于 IP 的其它協議也很直接,比如 DCCP 或者 SCTP。
機器設置
我們發現很多腳本都可以共享機器的配置,因此大多腳本啟動時都會調用默認的 shell 命令來配置機器參數。同時,因為腳本中的系統調用不會指定測試機器的配置,解析器會在測試期間把這些相應的值都替換成合適的值。比如,在 IPv4,IPv6,IPv4-mapped IPv6 這些協議中,我們需要選擇不同的默認 IP 地址。
時間模型
很多協議對時間都很敏感,我們在腳本中支持了重要的靈活時間功能。packetdrill 強制每條語句必須帶一個時間戳:如果事件沒有在這個指定的時間發生,packetdrill 會觸發一個 error 并報告實際事件發生的時間。表格 1 展示了 packetdrill 的時間模型。
避免隨機失敗
我們用 --tolerance_usecs 參數設置了 4ms 的容忍值,并持續使用了該參數長達一年,這樣設置使得事件只要在我們期望時間的 4ms 范圍內發生就認為測試是成功的。這也使得 1-ms 的 RTT 和 3-ms 的 RTO 能夠被覆蓋在內。我們認為這是基于精度和維護成本的一種折衷。已經能夠幫我們找到大多數重要的時間方面的 bug,并且能夠將 packetdrill 在大多數場景下不觸發任何一次隨機失敗。
packetdrill 在內部也有一些措施來盡量減少這種時間方面的隨機失敗,比如讓測試執行開始和內核的調度 tick 盡量對齊。控制 sleep wakeup 事件,以在一些沒有常規的調度 tick 并使用實時調度優先級的 Linux kernel 環境獲取到原始的 tick 值。使用 mlockall() 來嘗試把內存頁 pin 到 RAM,在力所能及的前提下對數據進行預計算,并在 test 結束后自動發送 TCP RST 幀,避免連接上的自動重傳行為。
經驗和成果
我們在 Google 生產環境機器上,使用 packetdrill 測試 Linux 內核已經有 18 個月的時間。下面我們討論我們怎么發現這個工具很有用的。
使用 packetdrill 開發的特性
我們的團隊使用 packetdrill 來測試我們在 Linux 中實現并發布的功能。成功地避免了將不計其數的 bug 推向生產環境。這其中包括 TCP Early Retransmit,TCP Fast Open,TCP Loss Probe 以及對 Linux F-RTO 實現的完全重寫,我們也用它來測試 TCP 的前向糾錯功能。在 packetdrill 出現之前的功能我們也進行了測試,包括 TCP 初始的窗口協商,限制 TCP 重傳超時到 1 秒以及 Proportional Rate Reduction。
使用 packetdrill 找到的 Linux bug
Google 的工程師用 packetdrill 發現了很多 Linux 的 bug,感興趣的可以去看原論文。
捕獲網絡協議處理的外部行為變化
Catching external behavior changes packetdrill 腳本還使我們的團隊注意到 linux kernel 升級中的一些變化,雖然這學變化不是 bug,但仍然會對我們的生產環境產生一些影響,比如 timer slack,和最近修復的 packet size accounting。對于這學變化,我們也及時地對生產環境的 kernel 進行了一些適配。
測試套件
覆蓋率?我們組的 9 個開發者總共寫了 266 個 packetdrill 腳本來測試 Google 的生產環境的 Linux kernel 和 92 個腳本來測試 packetdrill 工具自己本身。因為 packetdrill 使開發者能夠在 IPv4,IPv6,IPv4-mapped IPv6 模式下都能跑測試腳本,我們實際的測試 case 多達 657 個。表格 2 總結了我們的 packetdrill 腳本覆蓋到的所有 TCP 功能。
可重復性?為了量化我們測試結果的可重復性,我們檢查了過去兩天在 2.2GHz 64bit 多核 PC 上跑過的所有 Google 生產環境的測試的隨機失敗情況。最近的 54 次 657 個測試都跑完的情況下,packetdrill 的所有測試用例中只有 14 個測試用例失敗,這些都是意外的隨機失敗,不是程序的 bug。這說明我們的誤失敗率 < 0.0004,1/2500。對于我們內核組來說這是可以接受的成本。盡管如此,我們希望通過腳本的迭代進一步降低這種 test case 的誤失敗率。
執行時間?packetdrill 腳本執行起來非常快,所以我們在代碼 review 之前會執行 packetdrill 腳本相關的測試,每次修改 Google 生產環境的 TCP 代碼的 commit 都會先過一次 packetdrill 上面提到的 54 個測試,總共執行 657 個測試用例的時間是 25-26 分鐘左右,平均每個 case 2.4 秒完成。
相關工具
調試和測試協議有很多工具實現如 RFC2398 categorizes late-90s 工具. Packet Shell 看起來在設計上和 packetdrill 最接近,允許腳本發包和收包來測試 TCP 節點的響應,但是這個工具是給 Solaris 系統設計的,并且已經不再公開,這個工具的設計讓使用者也比較苦逼(比如你需要寫 8 行 Tcl 命令來注入一個簡單的 TCP SYN 包),并且不支持 socket API,不支持指定包的到達時間,不支持處理 timers。Orchestra 是一個錯誤注入庫,能夠檢查 TCP 實現是否遵循了 TCP 的 RFC。這個工具是在 X-kernel 的 TCP 協議棧下又實現了一層,以執行用戶指定的行為,包括 delay,drop,reorder 以及編譯包的行為,結果需要人肉驗證,并且測試也沒法自動化,對于新的 TCP 協議棧來說比較難用。并且本身也不是為了測試目的開發的,TCPanaly 這個工具是通過分析 TCP 的 traces 來驗證 TCP 的實現,診斷是否違反了 RFC 或者是否有性能問題。在 packetdrill,這種領域知識是通過腳本來建立的;但是在 TCPanaly,這些知識是通過對軟件本身的理解來達成的,這種知識難以進行評審和擴展。
上面提到的這些工具都是在 1990 年代后期完成的,就我們了解到的情況,每一個工具都沒有被用來測試現代的 TCP 協議棧。相比之下 IxANVL 是一個現代的商業化協議測試工具,覆蓋了 TCP 的 RFS 以及一些其它的網絡協議,但是和 packetdrill 不一樣,這個工具擴展或者腳本化都不太容易,測試新功能也不容易且不開源。
另外一些研究怎么測試協議的結果則是用一些比較正式的語言來寫一個工具,然后再用這個工具來集成到自動化測試流程中。但是這些模型為了學術化,過于嚴謹,維護成本很高,且不可持續,其本身和快速進化的代碼也很難有效配合。還有一些工具能夠自動地找到 bug,但是只覆蓋了非常窄的領域,并且只能測試用戶領域的代碼。這些工具可以算是我們工作的一些補充。
結論
packetdrill 使得快速,準確可重現的對整個 TCP/UDP/IP 網絡棧進行測試成為了可能。我們發現 packetdrill 在開發過程,回歸測試以及問題定位中驗證協議正確性、性能,安全方面都不可或缺。我們將 packetdrill 開源并希望和社區來分享這個優秀的工具并希望能夠使互聯網協議的改進更加方便。源代碼和腳本可以在 http://code.google.com/p/packetdrill/ 找到。
總結
以上是生活随笔為你收集整理的packetdrill 简介的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 极端情况下收缩 Go 进程的线程数
- 下一篇: 喜提 redir contributor