天天看點

eBay流量管理之LB上下遊封包追蹤

作者:存儲矩陣

一.背景

網絡抓包技術在定位現代複雜的網絡問題中有着舉足輕重的作用。tcpdump,wireshark這些抓包工具成為運維,研發人員定位網絡問題的瑞士軍刀。不管你是什麼樣的應用,采用什麼樣的網絡協定,最終都是一個個符合某種協定規範的封包在網絡上傳輸,隻要我們抓住這些封包加以分析,一般都能找到網絡問題的根源。

但是,随着7層網絡負載均衡器(後文簡稱LB)的引入,從用戶端到伺服器的請求從一個TCP連接配接變成了兩個獨立的TCP連接配接:從用戶端到LB的連接配接從LB到伺服器的連接配接

兩個連接配接共同完成從用戶端到伺服器端的請求和應答。如果要想知道從用戶端到伺服器整個鍊路上的網絡傳輸情況,我們勢必要把這兩條連接配接上的封包都給抓出來。不僅如此,我們還要知道這兩條不同連接配接上的封包的對應關系。這裡有兩個主要的問題需要解決:

eBay流量管理之LB上下遊封包追蹤

(點選可檢視大圖)

對于用戶端到LB的一個HTTP請求,LB在把這個請求轉給後端伺服器的時候有多個伺服器可以選擇(如上圖所示有server1,server2)。當LB選好某一個伺服器之後,還需要從同一個伺服器中的多個TCP連接配接中選擇一個連接配接來傳輸封包。以上圖為例,我們已經知道client和VIP的位址和端口資訊,根據這些過濾條件,我們很容易抓取到用戶端到LB到封包。但是,我們并不知道LB會選擇哪一個後端伺服器的哪一個TCP連接配接來轉發這個請求,✦ ✦ ✦ ✦ ✦ ✦ ✦ ✦

對于多個用戶端到LB的HTTP請求,如果是先後發生的,那麼LB一般會複用同一個從LB到伺服器的TCP連接配接來處理這些請求。這意味着我們從LB到伺服器的抓包中,并沒有辦法确定這些封包是來自于哪一個用戶端到LB的哪一個連接配接。這使得我們在排查問題的時候✦ ✦ ✦ ✦ ✦ ✦ ✦ ✦

從上面的圖中可以看到,client1和client2同時都去通路同一個VIP,此時LB一般情況下并不會和後端同一個伺服器建立兩條TCP連接配接來分别處理client1和client2的請求。LB會複用同一條到後端伺服器的連接配接來處理client1和client2的請求。這樣我們就算抓到從LB到後端伺服器的封包,我們也無法判斷這個封包是在處理client1的請求還是client2的請求。

二.問題分析

在eBay之前的網絡架構中,主要使用廠商提供的硬體負載均衡器(後文簡稱HLB)。廠商都提供了豐富的網絡排查工具幫助我們定位問題,比如netscaler的nstrace。

這是因為,廠商的LB接管了封包的整個生命周期。也就是從接收到封包開始,經過處理,到最後發出去封包,整個過程都是在LB的控制範圍内。是以HLB有能力知道封包轉發的時候的兩個關鍵資訊:封包發給了哪一個後端伺服器選用了跟後端伺服器之間的哪一條TCP連接配接來發包

同時,nstrace抓包的介入點也是廠商的軟體可以自己完全控制的,可以在封包處理的任何一個時刻去抓包。從下圖中可以看到,HLB可以在剛接收到封包的時候抓包,也可以在LB處理完用戶端連接配接并建立和伺服器端連接配接的時候抓包,也可以在封包完全組裝好,準備發給伺服器的時候抓包,是以它要解決前面兩個問題相對來說是容易的。

eBay流量管理之LB上下遊封包追蹤

(點選可檢視大圖)

目前eBay内部的網絡架構中,以軟體實作的負載均衡器(後文簡稱SLB)已經開始大規模替換HLB。。Envoy是一個運作于作業系統(本文中講述的内容隻針對Linux)之上的應用軟體,它基于作業系統提供的socket API來完成封包的接收和轉發。作為應用軟體,envoy通過socket收發的是應用層協定的資料,而完整的TCP/IP封包卻是Linux核心的網絡協定棧來負責的。這跟HLB有着很大的不同。我們通常的抓包工具抓包的時間點是在核心協定棧裡,而且這個時間點也是固定的。從下面的圖中我們可以看到在收包(ingress packet capture)和發包(egress packet capture)的時候抓包的位置。抓包是發生在網絡驅動程式和以太封包處理之間。

eBay流量管理之LB上下遊封包追蹤

(點選可檢視大圖)

對于本文的情況,我們隻知道用戶端位址端口和VIP的位址端口,這可以幫助我們抓到用戶端到LB的封包。然而,對于LB到伺服器這條連接配接,我們并不知道封包的目的位址,抓包的時候沒有足夠的資訊可以幫助我們過濾得到我們想要的封包。

三.解決方案

本文設計并實作了一套工具,能夠根據用戶端IP位址和要通路的虛拟IP位址和端口(VIP),抓取從客戶和LB之間以及對應的從LB和伺服器之間互動的所有封包,進而形成完整的鍊路來分析網絡問題。本文的讨論都是基于TCP協定上運作HTTP/HTTPS的場景。

我們先來看一下envoy和核心各自持有哪些資訊:Envoy:唯一知道封包應該發送給哪個伺服器的哪一條連接配接的元件。核心:隻是負責按照envoy的訓示把應用層的資料封裝各層網絡協定報頭之後發送到網絡上。是以我們首先需要改造的就是envoy本身的源代碼,讓它能夠在把應用層資料傳遞給核心的時候(向後端伺服器發送封包時)攜帶更多的資訊,讓核心能夠清楚知道這個封包是來自于哪個用戶端的哪個TCP連接配接。好在envoy來自于開源社群,我們能夠比較友善地去修改源代碼滿足我們的需求。核心接收到envoy傳遞過來的有關用戶端的資訊之後,首先需要将該資訊儲存到封包的核心資料結構skb裡面。現在核心有了這些資訊,有以下幾種方法可以選擇:

通過eBPF方式加入自己的代碼邏輯插入到核心中。但是對于我們需要修改的TCP發包函數并沒有hook點可以插入,對于eBPF trace/kprobe的方式雖然可以找到TCP發包函數,但是eBPF trace/kprobe 對于核心資料都是隻讀的,無法進行資料的改寫。是以此方法其實是不可行的。直接修改核心,重新編譯。出于對eBay目前實際情況的考量,此方法成本較高,不易推行。以動态加載子產品的方式去改變核心行為。理論上這是大多數時候增加核心功能的推薦方式。這種方法其實又可以分為兩種情況:核心已經提供了動态加載的Hook點。本文的情況是需要修改TCP發包函數,這個并沒有直接相關的Hook點,除非我們替換整個TCP子產品。但是我們需要的代碼修改量其實非常少,完全沒有必要替換整個TCP子產品。利用livepatch的方式去替換核心的一些函數。

最終我們選擇了livepatch的方式去更改核心代碼,将資料儲存到skb中。通過livepatch的方式還可以達到按需加載的目的,因為絕大多數時候是不需要這個patch的,隻有我們需要排查問題的時候才加載,任務完成之後就可以解除安裝patch了。這樣對系統的影響也是最小的。目前核心基于傳統cBPF抓包的時候,核心裡的cBPF程式隻能根據封包本身的内容做過濾,也就是根據資料鍊路層、IP層、傳輸層、應用層(基于封包資料偏移量來實作)報頭來過濾,。如果我們希望的過濾條件不是封包上固定的偏移量或者這個過濾條件本身就不屬于封包的某個字段,cBPF就無能為力了。比如我們用tcpdump做抓包的時候,常用的過濾條件,比如IP位址、端口、TCP flag等等,都屬于RFC規定的在封包固定位置出現的字段。另外一些屬性,比如這個封包是屬于哪個應用程序,這種資訊是無法在封包上面展現的,也就不能作為抓包過濾條件。為了達到根據envoy傳遞給核心的資訊進行過濾的目的,我們有幾種可選辦法:

不改動封包長度,将資訊寫入某層協定報頭的某個已知字段。此方法需要找到一些RFC定義的保留字段或者更改已知字段的值,具有一定風險。

eBay流量管理之LB上下遊封包追蹤

(點選可檢視大圖)

改動封包長度,将資訊以字段選項的形式插入到目前封包,比如IP option或者TCP option。此方法需要增加封包長度,會帶來額外附載。

eBay流量管理之LB上下遊封包追蹤

(點選可檢視大圖)

改造目前cBPF的實作,讓cBPF能夠通過skb的一些字段對封包進行過濾,而不是僅僅通過封包自身的字段來進行過濾。此方法要求對于cBPF有足夠的了解,但是cBPF本身代碼晦澀難懂,不易操作。而且對于核心的影響也是難以估計。

eBay流量管理之LB上下遊封包追蹤

(點選可檢視大圖)

以eBPF的方式,插入一段代碼并設定過濾條件,将封包從核心直接發送到使用者空間。因為可以加入自定義代碼,是以過濾條件可以十分靈活,除了可以根據封包本身任意字段外,還可以根據封包對應的skb上所有的中繼資料以及狀态資訊進行過濾。

eBay流量管理之LB上下遊封包追蹤

(點選可檢視大圖)

上面的方法中,第4種看上去是最完美的方式,也是筆者一開始去實際操作的方法。在Linux kernel 5.5 開始引入了新的helper 函數 bpf_skb_output, 可以将封包以BPF perf event的方式從核心态發送到使用者态空間。

int bpf_skb_output(void *ctx, struct bpf_map *map, u64 flags, void *data, u64 size)+ * Description+ * Write raw *data* blob into a special BPF perf event held by+ * *map* of type **BPF_MAP_TYPE_PERF_EVENT_ARRAY**.

在實際編寫代碼測試的時候發現, 通過檢視這個helper函數的實作,可以發現是通過記憶體拷貝的方式将整個封包複制一份。雖然封包拷貝确實是性能的大敵,但是在5M/s這麼低的流量就開始丢失event是不能用記憶體拷貝來解釋的。我們發現,最大的問題還是在于BPF的perf event本身的機制。簡單的說就是使用者程式通過定期輪詢的方式去從核心擷取event。不管我們把這個輪詢的時間間隔設定多短,最後設定成沒有間隔的不斷輪詢,性能上并沒有太大的提升,不超過10MB就開始丢event了。除了性能的原因,目前eBay生産環境上使用的kernel版本還是5.4,就算采用這個方法,還需要更新核心版本或者把eBPF相關的patch引入到目前版本,這些工作都不在控制範圍之内。是以最終沒有進一步去研究如何提升BPF perf event的性能來達到抓包的目的,但是我覺得這是eBPF可以去改進的地方。

最終我們在項目中選擇了第二種方法。該方法是在Linux預設提供的netfilter架構裡加入我們自己的代碼去插入新的TCP Option報頭選項。後面會較長的描述如何去實作的。

通過這種方法,我們成功地将用戶端到LB的連接配接資訊傳遞到了從LB發送給伺服器的封包之中,cBPF根據這些資訊來進行過濾就成為了可能!

不過我們也不能高興的太早。現在雖然解決了從LB到伺服器的請求封包中攜帶用戶端相關資訊的問題,這個用戶端資訊被儲存在TCP option裡,但是從後端伺服器傳回給LB的封包中是沒有這個TCP option的。是以我們還需要一種機制能抓取從伺服器傳回給LB的封包。

這裡要強調的是:我們在抓取封包的時候已知條件隻有用戶端位址和VIP位址,對于後端伺服器的IP端口資訊,我們是一無所知的。是以在通過TCP option作為filter抓到LB到伺服器的封包的時候,還得把這個包含TCP option的封包的源目的IP位址和源目的端口四元組記錄下來,用來過濾從伺服器傳回給LB的封包。這裡我們選擇了eBPF trace的方式,

eBay流量管理之LB上下遊封包追蹤

(點選可檢視大圖)

從上面的架構圖中,我把整個工具鍊分成三部分:

fetrace tool這是一個使用者真正操作的指令行工具集。它主要完成下面的功能:根據需求動态加載/解除安裝 livepatch 子產品和netfilter hook子產品。擷取eBPF trace程式的結果(伺服器IP和端口),動态啟動一個線程去過濾并捕獲LB和這個伺服器之間的我們期望的封包。把使用者輸入的過濾條件(基于tcpdump文法)轉化成核心可識别的cBPF代碼,然後調用libpcap去過濾并捕獲封包。

envoy

加入新的network filter來配置你想要抓取封包的的用戶端IP/端口和VIP資訊。通過envoy的dynamic metadata機制将用戶端資訊傳遞給LB發給後端伺服器的連接配接。替換envoy的發包函數,把用戶端資訊傳遞到核心中。

核心

livepatch:從envoy傳遞過來的資料中解析出用戶端資訊并儲存到skb。eBPF trace (ip_output): 将伺服器IP和端口資訊暴露給使用者态的fetrace tool。etfilter hook:将用戶端資訊插入到TCP報頭選項。

Envoy

Packet Trace Filterenvoy提供了filter的插件編寫,本文加入了一個新的filter作為packet trace的過濾條件。

eBay流量管理之LB上下遊封包追蹤

(點選可檢視大圖)

通過上面這個filter,envoy就會抓取滿足以下條件的封包:

  • 目的IP是10.18.144.73
  • 目的端口是8080
  • 源IP是10.110.112.181

在實際情況中, 用戶端端口一般是系統随機配置設定的,是以一般無法也不需要指定這個值。通過上面的配置,envoy隻對滿足這三個過濾條件的用戶端到LB的TCP連接配接做抓包,同時抓取對應的從LB到後端伺服器的TCP連接配接封包。用戶端資訊的傳遞Envoy提供了filter dynamic metadata機制。本文增加了dynamic metadata從下遊連接配接(用戶端到LB)傳遞到上遊連接配接(LB到伺服器)的功能。這樣用戶端資訊被當成一種dynamic metadata, 傳遞到LB和伺服器端的連接配接裡。關于這部分的實作,起初筆者是直接将envoy的上遊和下遊連接配接加入指針互指來完成,後來在envoy核心開發維護者的建議下采用了目前的方案。因為從envoy的設計來說,上下遊的連接配接是解耦的,不應該直接把上下遊的連接配接直接關聯,否則就違背了envoy的設計初衷。發包

eBay流量管理之LB上下遊封包追蹤

(點選可檢視大圖)

通過上面的圖,我們可以看到對于envoy發包的改動就是在調用socket API的時候,把用戶端端口資訊加入到TCP socket的ancillary data裡去。因為有了前面packet trace filter的過濾,這裡envoy隻需要針對滿足過濾條件的封包加入額外的資訊。因為packet trace filter 過濾條件裡已經包含了用戶端IP資訊,

Kernel

Livepatch

結合前面envoy發包的圖中,核心部分需要修改tcp_sendmsg_locked函數,将從socket ancillary data取出用戶端的端口資訊并儲存到skb某個字段。我們選用了一個eBay内部沒有使用到的一個字段來儲存這個值。

Netfilter Hook

我們選擇用netfilter hook插入skb裡儲存的用戶端的端口資訊,是應該把這個值插入到IP option ,還是TCP option呢?考慮到我們的netfilter hook是在IP層的hook,一開始本文選擇将這個值插入到IP option。但是在做性能測試的時候卻發現,該方法對網絡延時有非常大的影響。我們先來看一下封包的完整流程:

用戶端發起請求 -> LB處理 -> 伺服器處理 -> 伺服器回複請求 -> LB處理 -> 用戶端

整個過程在我的測試環境中,不插入IP option的時候隻需要幾毫秒,插入IP option并且不引起IP分片的情況下,。從我們netfilter hook加入的代碼分析,雖然有記憶體拷貝來插入IP option的操作(通常情況下,該操作是影響性能的關鍵因素之一),但是在本文的情況下,記憶體拷貝的長度是很小的,就是IP報頭的移動,隻有二十個位元組,理論上不應該對延遲有這麼大的影響。但是經過多次測試,仍然是同樣的結果。于是我們通過打時間戳的辦法将封包的整個過程分成 netfilter hook之前、之後、核心驅動發送封包、伺服器收到封包、伺服器發出封包、LB收到回複封包這幾個過程。最後對比發現,。由此也可看出,插入IP option引起的時延并不來自于核心的操作,而是來自于從LB到伺服器中間的網絡裝置(交換機/路由器/防火牆)。由此推測網絡裝置對于自定義的IP option處理是比較低效的。但是我們又沒法改變中間裝置,于是選擇TCP option的方法再次嘗試,好在這次的結果是令人欣慰的:在使用自定義TCP option的情況下,延遲沒有發生明顯改變。

eBPF

由于本文隻使用eBPF的trace功能,是以選擇了目前比較成熟的BCC架構,從核心ip_output函數中導出伺服器IP和端口資訊。fetrace tool 用golang進行簡單封裝之後,通過eBPF event擷取到伺服器IP和端口。

CLI (fetrace tool)

包括了:基于kpatch的shell腳本,負責加載/解除安裝 livepatch和netfilter hook。基于golang的CLI,負責讀取使用者希望的過濾參數并根據eBPF導出的伺服器IP和端口動态啟動goroutine過濾抓取LB和某個伺服器之間的封包。。最終結束抓包後,CLI負責将所有單個抓封包件合并成一個檔案。

四.示範

網絡拓撲

eBay流量管理之LB上下遊封包追蹤

(點選可檢視大圖)

步驟

在本次示範過程中,envoy和fetrace以docker容器方式,以host network模式運作于核心5.4-59版本的Ubuntu作業系統的同一台虛拟機中。client1/2 server1/2 分别以docker容器方式,運作于不同的虛拟機之中。在fetrace 容器之中加載livepatch和netfilter子產品

root@xxxx:/# ./fetrace/scripts/pre_start.sh loadStarting load live patchloading patch module: /fetrace/modules/livepatch-mark_patch_1-5.4.0-59.ko waiting (up to 15 seconds) for patch transition to complete...transition complete (1 seconds)

  • 在envoy容器之中配置packet trace filter指定client IP,VIP IP和端口

在client1/2, server1/2 分别啟動Envoy nighthawk測試工具收發http流量

client http://10.18.144.73:8080 --duration 30 --concurrency 10 --rps 100

  • 在fetrace容器之中,啟動CLI抓包封包,大約30秒之後,“Ctrl-c” 終止CLI

root@xxxx:/# ./fetrace/bin/fetrace --cif eth0 --vip 10.18.144.73 --vport 8080 --cip 10.110.112.181I0806 04:01:50.267638 37 main.go:140] Notify sub goroutine quitI0806 04:01:50.267683 37 pcap.go:190] V2S receives quitI0806 04:01:50.267694 37 pcap.go:190] V2S receives quitI0806 04:01:50.267733 37 pcap.go:188] C2V receives quitI0806 04:01:50.267755 37 main.go:129] C2V goroutine exitI0806 04:01:50.267640 37 capture.go:104] Perf stopI0806 04:01:50.586269 37 main.go:135] eBPF goroutine exitI0806 04:01:50.586288 37 main.go:143] Merge all pcapI0806 04:01:50.595461 37 main.go:145] Main thread exit

然後可以在fetrace容器裡看到如下抓封包件

root@xxxx:/# ls *.pcapc2v.pcap final.pcap v2s_10.18.144.73_47842_10.75.119.32_80.pcap v2s_10.18.144.73_49990_10.75.119.32_80.pcap

c2v.pcap: clientvip

v2s_xxxx.pcap: 對應每一個伺服器一個抓封包件

final.pcap: 所有抓封包件合并成一個

  • 用wireshark打開final.pcap
eBay流量管理之LB上下遊封包追蹤

(點選可檢視大圖)

可以看到從LB到伺服器的封包的TCP字段多了一個自定義的TCP option。這裡的Magic Number 0x9a6a 轉換成10進制(44350)之後就是用戶端發起連接配接的端口号。通過這個端口号,我們就可以把從用戶端發送給LB和LB發送到後端伺服器的封包給對應起來了。

10.110.112.181:44350>10.18.144.73:8080->10.18.179.53:80

eBay流量管理之LB上下遊封包追蹤

(點選可檢視大圖)

  • 在fetrace容器中,抓包完成之後解除安裝子產品

root@xxxx:/# ./fetrace/scripts/pre_start.sh unloadStarting unload live patchdisabling patch module: livepatch_mark_patchwaiting (up to 15 seconds) for patch transition to complete...transition complete (2 seconds)unloading patch module: livepatch_mark_patchUnload fetrace_hook module

五.總結和展望

在基于socket的網絡應用軟體中,網絡中傳輸的封包經過核心協定棧處理之後,使用者态程式接收到的資訊并不能完全包含傳輸過程中的所有資訊。對于大部分應用來說,使用者态程式的确不需要知道底層的傳輸資訊,這也正是socket設計之初衷,希望屏蔽和應用無關的底層資訊,讓應用開發者專注于業務層面的邏輯開發。但是,随着網絡基礎架構和應用架構的不斷演化,比如7層負載均衡的代理模式就需要終止目前傳輸,然後建立連接配接;比如微服務架構更是要求一個簡單的請求需要經過多個伺服器之間的中轉才能最終完成一次請求和答複。是以,如何把所有這些中間過程的處理形成一條完整的、可追蹤的全鍊路成了一個巨大的挑戰。目前關于全鍊路追蹤的解決方案都是從應用的層面在應用層去添加額外的追蹤資訊來實作,比如HTTP協定的X-Forwarded-For頭部或者trace ID頭部等機制。這些機制對于關心具體應用的開發者來說确實是有效的。但是從網絡基礎架構的層面來看,我們并不關心應用層的具體協定,那麼就需要把追蹤資訊下沉到更低的網絡協定層。

本文基于傳輸層的追蹤資訊做了相關研究并給出了可行性方案和實作。該解決方案從應用軟體到核心都進行了相應的改進,從中我們可以看出目前基于socket的應用軟體的一些弊端,即缺乏對于全鍊路需求的支援。為了讓基于socket的軟體能夠更好的支援全鍊路追蹤,我認為可以:

從核心層面提供更加豐富的socket選項,讓應用軟體能夠通過socket擷取到任何可以在網絡上傳輸的資訊,比如各個網絡協定層級的選項(ip選項,tcp選項,tls選項等等),同時也能讓socket把更多的使用者自定義屬性從使用者态傳入到核心态,讓協定棧能夠自由地添加各個網絡協定層級的選項來擴充網絡傳輸的表達能力,進而獲得到全鍊路的追蹤能力。對應上面核心對socket的增強,提供增強的socket API(glibc)來适配。應用軟體使用增強的socket API來進行程式設計。但是這對已有的軟體有侵入性。對于源碼不能修改或者源碼修改的代價較大的情況,可以考慮采用使用者态的ebpf hook來實作(需要進一步探索可行性和實際解決方案)。

繼續閱讀