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