eBPF在大厂的应用
本文翻譯自 Facebook 在 LPC 2021 大會(huì)上的一篇分享:From XDP to Socket: Routing of packets beyond XDP with BPF。
因?yàn)?XDP 運(yùn)行在網(wǎng)卡上,而且在邊界和流量入口,再往后的路徑(尤其是到了內(nèi)核協(xié)議棧)它就管不到了,所以引入了其他一些BPF技術(shù)來“接力”這個(gè)路由過程。另外, 這里的“路由”并非狹義的路由器三層路由,而是泛指 L3-L7 流量轉(zhuǎn)發(fā)。
翻譯時(shí)加了一些鏈接和代碼片段,以更方便理解。
由于譯者水平有限,本文不免存在遺漏或錯(cuò)誤之處。如有疑問,請(qǐng)查閱原文。
以下是譯文。
譯者序
1 引言
1.1 前期工作
1.2 Facebook 流量基礎(chǔ)設(shè)施
1.3 面臨的挑戰(zhàn)
2 選擇后端主機(jī):數(shù)據(jù)中心內(nèi)流量的一致性與無狀態(tài)路由(四層負(fù)載均衡)
2.3.1 原理和流程
2.3.2 開銷
2.3.3 實(shí)現(xiàn)細(xì)節(jié)
2.3.4 效果
2.3.5 限制
數(shù)據(jù)開銷:TCP header 增加 6 個(gè)字節(jié)
運(yùn)行時(shí)開銷:不明顯
監(jiān)聽的 socket 事件
維護(hù) TCP flow -> server_id 的映射
server_id 的分配和同步
2.2.1 容錯(cuò)性:后端故障對(duì)非相關(guān)連接的擾動(dòng)
2.2.2 TCP 長連接面臨的問題
2.2.3 QUIC 協(xié)議為什么不受影響
connection_id
完全無狀態(tài)四層路由
2.1 Katran (L4LB) 負(fù)載均衡機(jī)制
2.2 一致性哈希的局限性
2.3 TCP 連接解決方案:利用 BPF 將 backend server 信息嵌入 TCP Header
2.4 小結(jié)
3 選擇 socket:服務(wù)的真正優(yōu)雅發(fā)布(七層負(fù)載均衡)
3.3.1 方案設(shè)計(jì)
3.3.2 好處
3.3.3 發(fā)布過程中的流量切換詳解
3.3.4 新老方案效果對(duì)比
3.3.5 小結(jié)
3.2.1 早期方案:socket takeover (or zero downtime restart)
3.2.2 其他方案調(diào)研:SO_REUSEPORT
3.2.3 思考
發(fā)布流程
存在的問題
3.1.1 發(fā)布流程
3.1.2 存在的問題
3.1 當(dāng)前發(fā)布方式及存在的問題
3.2 不損失容量、快速且用戶無感的發(fā)布
3.3 新方案:bpf_sk_reuseport
4 討論
4.1 遇到的問題:CPU 毛刺(CPU spikes)甚至卡頓
4.2 Listening socket hashtable
4.3?bpf_sk_select_reuseport?vs?bpf_sk_lookup
1 引言
用戶請(qǐng)求從公網(wǎng)到達(dá) Facebook 的邊界 L4LB 節(jié)點(diǎn)之后,往下會(huì)涉及到兩個(gè)階段(每個(gè)階 段都包括了 L4/L7)的流量轉(zhuǎn)發(fā):
從 LB 節(jié)點(diǎn)負(fù)載均衡到特定主機(jī)
主機(jī)內(nèi):將流量負(fù)載均衡到不同socket
以上兩個(gè)階段都涉及到流量的一致性路由(consistent routing of packets)問題。本文介紹這一過程中面臨的挑戰(zhàn),以及我們?nèi)绾位谧钚碌?BPF/XDP 特性來應(yīng)對(duì)這些挑戰(zhàn)。
1.1 前期工作
幾年前也是在 LPC 大會(huì),我們分享了 Facebook 基于 XDP 開發(fā)的幾種服務(wù),例如
基于 XDP 的四層負(fù)載均衡器(L4LB)katran[2], 從 2017 年開始,每個(gè)進(jìn)入 facebook.com 的包都是經(jīng)過 XDP 處理的;
基于 XDP 的防火墻(擋在 katran 前面)。
左:第一代,基于 IPVS,L4LB 需獨(dú)占節(jié)點(diǎn);右:第二代,基于 XDP,不需獨(dú)占節(jié)點(diǎn),與業(yè)務(wù)后端混布。
1.2 Facebook 流量基礎(chǔ)設(shè)施
從層次上來說,如下圖所示,Facebook 的流量基礎(chǔ)設(shè)施分為兩層:
邊界層(edge tiers),位于 PoP 點(diǎn)
數(shù)據(jù)中心層,我們稱為 Origin DC
每層都有一套全功能 LB(L4+L7)
Edge PoP 和 Origin DC 之間的 LB 通常是長鏈接
從功能上來說,如下圖所示:
用戶連接(user connections)在邊界終結(jié),
Edge PoP LB 將 L7 流量路由到終端主機(jī),
Origin DC LB 再將 L7 流量路由到最終的應(yīng)用,例如 HHVM 服務(wù)。
1.3 面臨的挑戰(zhàn)
總結(jié)一下前面的內(nèi)容:公網(wǎng)流量到達(dá)邊界節(jié)點(diǎn)后,接下來會(huì)涉及 兩個(gè)階段的流量負(fù)載均衡(每個(gè)階段都是 L4+L7),
宏觀層面:LB 節(jié)點(diǎn) -> 后端主機(jī)
微觀層面(主機(jī)內(nèi)):主機(jī)內(nèi)核 -> 主機(jī)內(nèi)的不同 socket
這兩個(gè)階段都涉及到流量的高效、一致性路由(consistent routing)問題。
本文介紹這一過程中面臨的挑戰(zhàn),以及我們是如何基于最新的 BPF/XDP 特性 來解決這些挑戰(zhàn)的。具體來說,我們用到了兩種類型的 BPF 程序:
BPF TCP header options[3]:解決主機(jī)外(宏觀)負(fù)載均衡問題;
BPF_PROG_TYPE_SK_REUSEPORT[4](及相關(guān) map 類型?BPF_MAP_TYPE_REUSEPORT_SOCKARRAY):解決主機(jī)內(nèi)(微觀)負(fù)載均衡問題。
2 選擇后端主機(jī):數(shù)據(jù)中心內(nèi)流量的一致性與無狀態(tài)路由(四層負(fù)載均衡)
先看第一部分,從 LB 節(jié)點(diǎn)轉(zhuǎn)發(fā)到 backend 機(jī)器時(shí),如何來選擇主機(jī)。這是四層負(fù)載均衡問題。
2.1 Katran (L4LB) 負(fù)載均衡機(jī)制
回到流量基礎(chǔ)設(shè)施圖,這里主要關(guān)注 Origin DC 內(nèi)部 L4-L7 的負(fù)載均衡,
katran 是基于 XDP 實(shí)現(xiàn)的四層負(fù)載均衡器,它的內(nèi)部機(jī)制:
實(shí)現(xiàn)了一個(gè) Maglev Hash 變種,通過一致性哈希選擇后端;
在一致性哈希之上,還維護(hù)了自己的一個(gè)本地緩存來跟蹤連接。這個(gè)設(shè)計(jì)是為了在某些后端維護(hù)或故障時(shí),避免其他后端的哈希發(fā)生變化,后面會(huì)詳細(xì)討論。
用偽代碼來表示 Katran 選擇后端主機(jī)的邏輯:
int?pick_host(packet*?pkt)?{if?(is_in_local_cache(pkt))return?local_cache[pkt]return?consistent_hash(pkt)?%?server_ring }這種機(jī)制非常有效,也非常高效(highly effective and efficient)。
2.2 一致性哈希的局限性
2.2.1 容錯(cuò)性:后端故障對(duì)非相關(guān)連接的擾動(dòng)
一致性哈希的一個(gè)核心特性是具備對(duì)后端變化的容錯(cuò)性(resilience to backend changes)。當(dāng)一部分后端發(fā)生故障時(shí),其他后端的哈希表項(xiàng)不受影響(因此對(duì)應(yīng)的連接及主機(jī)也不受影響)。Maglev 論文中已經(jīng)給出了評(píng)估這種容錯(cuò)性的指標(biāo),如下圖,
Resilience of Maglev hashing to backend changesMaglev: A fast and reliable software network load balancer. OSDI 2016
橫軸表示 backend 掛掉的百分比
縱軸是哈希表項(xiàng)(entries)變化的百分比,對(duì)應(yīng)受影響連接的百分比
Google 放這張圖是想說明:一部分后端發(fā)生變化時(shí),其他后端受影響的概率非常小;但從我們的角度來說,以上這張圖說明:即使后端掛掉的比例非常小, 整個(gè)哈希表還是會(huì)受影響,并不是完全無感知 —— 這就會(huì) 導(dǎo)致一部分流量被錯(cuò)誤路由(misrouting):
對(duì)于短連接來說,例如典型的 HTTP 應(yīng)用,這個(gè)問題可能影響不大;
但對(duì)于 tcp 長連接,例如持續(xù)幾個(gè)小時(shí)的視頻流,這種擾動(dòng)就不能忍了。
2.2.2 TCP 長連接面臨的問題
首先要說明,高效 != 100% 有效。對(duì)于 TCP 長連接來說(例如視頻),有兩種場景會(huì)它們被 reset:
int?pick_host(packet*?pkt)?{if?(is_in_local_cache(pkt))???????????????//?場景一:ECMP shuffle 時(shí)(例如 LB 節(jié)點(diǎn)維護(hù)或故障),這里會(huì) missreturn?local_cache[pkt]return?consistent_hash(pkt)?%?server_ring?//?場景二:后端維護(hù)或故障時(shí),這里的好像有(較小)概率發(fā)生變化 }解釋一下:
如果 LB 升級(jí)、維護(hù)或發(fā)生故障,會(huì)導(dǎo)致路由器 ECMP shuffle,那原來路由到某個(gè) LB 節(jié)點(diǎn)的 flow,可能會(huì)被重新路由到另一臺(tái) LB 上;雖然我們維護(hù)了 cache,但它是 LB node local 的,因此會(huì)發(fā)生 cache miss;
如果后端節(jié)點(diǎn)升級(jí)、維護(hù)或發(fā)生故障,那么根據(jù)前面 maglev 容錯(cuò)性的實(shí)驗(yàn)結(jié)果,會(huì)有一 部分(雖然比例不是很大)的 flow 受到影響,導(dǎo)致路由錯(cuò)誤。
以上分析可以看出,“持續(xù)發(fā)布” L4 和 L7 服務(wù)會(huì)導(dǎo)致連接不穩(wěn)定,降低整體可靠性。除了發(fā)布之外,我們隨時(shí)都有大量服務(wù)器要維護(hù),因此哈希 ring 發(fā)生變化(一致性哈希 發(fā)生擾動(dòng))是日常而非例外。任何時(shí)候發(fā)生 ECMP shuffle 和服務(wù)發(fā)布/主機(jī)維護(hù),都會(huì)導(dǎo) 致一部分 active 連接受損,雖然量很小,但會(huì)降低整體的可靠性指標(biāo)。
解決這個(gè)問題的一種方式是在所有 LB 節(jié)點(diǎn)間共享這個(gè) local cache (類似于 L4LB 中的 session replication),但這是個(gè)很糟糕的主意 ,因?yàn)檫@就需要去解決另外一大堆分布式系統(tǒng)相關(guān)的問題,尤其我們不希望引入任何 會(huì)降低這個(gè)極快數(shù)據(jù)路徑性能的東西。
2.2.3 QUIC 協(xié)議為什么不受影響
但對(duì)于 QUIC 來說,這都不是問題。
connection_id
QUIC 規(guī)范(RFC 9000)中允許 server 將任意信息嵌入到包的?connection_id?字段。
Facebook 已經(jīng)廣泛使用 QUIC 協(xié)議,因此在 Facebook 內(nèi)部,我們可以
在 server 端將路由信息(routing information)嵌入到?connection_id?字段,并
要求客戶端必須將這個(gè)信息帶回來。
完全無狀態(tài)四層路由
這樣整條鏈路上都可以從包中提取這個(gè) id,無需任何哈希或 cache 查找,最終實(shí)現(xiàn)的是一個(gè) 完全無狀態(tài)的四層路由(completely stateless routing in L4)。
那能不能為 TCP 做類似的事情呢?答案是可以。這就要用到 BPF-TCP header option 了。
2.3 TCP 連接解決方案:利用 BPF 將 backend server 信息嵌入 TCP Header
2.3.1 原理和流程
基本思想:
編寫一段?BPF_PROG_TYPE_SOCK_OPS?類型的 BPF 程序,attach 到 cgroup:
在 LISTEN, CONNECT, CONN_ESTD 等事件時(shí)會(huì)觸發(fā) BPF 程序的執(zhí)行
BPF 程序可以獲取包的 TCP Header,然后往其中寫入路由信息(這里是 server_id),或者從中讀取路由信息
在 L4LB 側(cè)維護(hù)一個(gè) server_id 緩存,記錄仍然存活的 backend 主機(jī)
以下圖為例,我們來看下 LB 節(jié)點(diǎn)和 backend 故障時(shí),其他 backend 上的原有連接如何做到不受影響:
客戶端發(fā)起一個(gè) SYN;
L4LB 第一次見這條 flow,因此通過一致性哈希為它選擇一臺(tái) backend 主機(jī),然后將包轉(zhuǎn)發(fā)過去;
圖中這臺(tái)主機(jī)獲取到自己的 server_id 是 42,然后將這個(gè)值寫到 TCP header;
客戶端主機(jī)收到包后,會(huì)解析這個(gè) id 并存下來,后面發(fā)包時(shí)都會(huì)帶上這個(gè) server_id;
服務(wù)端應(yīng)答 SYN+ACK,其中 服務(wù)端 BPF 程序?qū)?server_id 嵌入到 TCP 頭中;
假設(shè)過了一會(huì)發(fā)生故障,前面那臺(tái) L4LB 掛了(這會(huì)導(dǎo)致 ECMP 發(fā)生變化);另外,某些 backend hosts 也掛了(這會(huì) 影響一致性哈希,原有連接接下來有小概率會(huì)受到影響),那么接下來,
客戶端流量將被(數(shù)據(jù)中心基礎(chǔ)設(shè)施)轉(zhuǎn)發(fā)到另一臺(tái) L4LB;
這臺(tái)新的 L4LB 解析客戶端包的 TCP header,提取 server_id,查詢 server_id 緩存( 注意不是 Katran 的 node-local 連接緩存)之后發(fā)現(xiàn) 這臺(tái)機(jī)器還是 active 的,因此直接轉(zhuǎn)發(fā)給這臺(tái)機(jī)器。
可以看到在 TCP Header 中引入了路由信息后,未發(fā)生故障的主機(jī)上的長連接就能夠避免 因 L4LB 和主機(jī)掛掉而導(dǎo)致的 misrouting(會(huì)被直接 reset)。
2.3.2 開銷
數(shù)據(jù)開銷:TCP header 增加 6 個(gè)字節(jié)
struct?tcp_opt?{uint8_t??kind;uint8_t??len;uint32_t?server_id; };?//?6-bytes?total運(yùn)行時(shí)開銷:不明顯
需要在 L4LB 中解析 TCP header 中的 server_id 字段,理論上來說,這個(gè)開銷跟代碼實(shí) 現(xiàn)的好壞相關(guān)。我們測量了自己的實(shí)現(xiàn),這個(gè)開銷非常不明顯。
2.3.3 實(shí)現(xiàn)細(xì)節(jié)
監(jiān)聽的 socket 事件
switch?(skops->op)?{case?BPF_SOCK_OPS_TCP_LISTEN_CB:case?BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB:case?BPF_SOCK_OPS_TCP_CONNECT_CB:case?BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB:case?BPF_SOCK_OPS_PARSE_HDR_OPT_CB:case?BPF_SOCK_OPS_HDR_OPT_LEN_CB:case?BPF_SOCK_OPS_WRITE_HDR_OPT_CB:.?.?. }維護(hù) TCP flow -> server_id 的映射
在每個(gè) LB 節(jié)點(diǎn)上用 bpf_sk_storage 來存儲(chǔ) per-flow server_id。也就是說,
對(duì)于建連包特殊處理,
建連之后會(huì)維護(hù)有 flow 信息(例如連接跟蹤),
對(duì)于建連成功后的普通流量,從 flow 信息就能直接映射到 server_id, 不需要針對(duì)每個(gè)包去解析 TCP header。
server_id 的分配和同步
前面還沒有提到如何分配 server_id,以及如何保證這些后端信息在負(fù) 載均衡器側(cè)的時(shí)效性和有效性。
我們有一個(gè) offline 工作流,會(huì)給那些有業(yè)務(wù)在運(yùn)行的主機(jī)隨機(jī)分配 一個(gè) id,然后將這個(gè)信息同步給 L4 和 L7 負(fù)載均衡器(Katran and Proxygen), 后者拿到這些信息后會(huì)將其加載到自己的控制平面。因此這個(gè)系統(tǒng)不會(huì)有額外開銷,只要 保證 LB 的元信息同步就行了。
由于這個(gè)機(jī)制同時(shí)適用于 QUIC 和 TCP,因此 pipeline 是同一個(gè)。
2.3.4 效果
下面是一次發(fā)布,可以看到發(fā)布期間 connection reset 并沒有明顯的升高:
2.3.5 限制
這種方式要求 TCP 客戶端和服務(wù)端都在自己的控制之內(nèi),因此
對(duì)典型的數(shù)據(jù)中心內(nèi)部訪問比較有用;
要用于數(shù)據(jù)中心外的 TCP 客戶端,就要讓后者將帶給它們的 server_id 再帶回來,但這個(gè)基本做不到;
即使它們帶上了,網(wǎng)絡(luò)中間處理節(jié)點(diǎn)(middleboxes)和防火墻(firewalls)也可能會(huì)將這些信息丟棄。
2.4 小結(jié)
通過將 server_id 嵌入 TCP 頭中,我們實(shí)現(xiàn)了一種 stateless routing 機(jī)制,
這是一個(gè)完全無狀態(tài)的方案
額外開銷(CPU / memory)非常小,基本感知不到
其他競品方案都非常復(fù)雜,例如在 hosts 之間共享狀態(tài),或者將 server_id 嵌入到 ECR (Echo Reply) 時(shí)間戳字段。
3 選擇 socket:服務(wù)的真正優(yōu)雅發(fā)布(七層負(fù)載均衡)
前面介紹了流量如何從公網(wǎng)經(jīng)過內(nèi)網(wǎng) LB 到達(dá) backend 主機(jī)。再來看在主機(jī)內(nèi),如何路由流量來保證七層服務(wù)(L7 service)發(fā)布或重啟時(shí)不損失任何流量。
這部分內(nèi)容在 SIGCOMM 2020 論文中有詳細(xì)介紹。想了解細(xì)節(jié)的可參考:
Facebook,Zero Downtime Release: Disruption-free Load Balancing of a Multi-Billion User Website[5]. SIGCOMM 2020
3.1 當(dāng)前發(fā)布方式及存在的問題
L7LB Proxygen 自身也是一個(gè)七層服務(wù),我們以它的升級(jí)為例來看一下當(dāng)前發(fā)布流程。
3.1.1 發(fā)布流程
發(fā)布前狀態(tài):Proxygen 實(shí)例上有一些老連接,也在不斷接受新連接,
拉出:拉出之后的實(shí)例不再接受新連接,但在一定時(shí)間窗口內(nèi),繼續(xù)為老連接提供服務(wù);
這個(gè)窗口稱為 graceful shutdown(也叫 draining) period,例如設(shè)置為 5 或 10 分鐘;
拉出一般是通過將 downstream service 的健康監(jiān)測置為 false 來實(shí)現(xiàn)的,例如在這個(gè)例子中,就是讓 Proxygen 返回給 katran 的健康監(jiān)測是失敗的。
發(fā)布新代碼:graceful 窗口段過了之后,不管它上面還有沒有老連接,直接開始升級(jí)。
一般來說,只要 graceful 時(shí)間段設(shè)置比較合適,一部分甚至全部老連接能夠在這個(gè) 窗口內(nèi)正常退出,從而不會(huì)引起用戶可見的 spike;但另一方面,如果此時(shí)仍然有老 連接,那這些客戶端就會(huì)收到 tcp reset。
部署新代碼,
關(guān)閉現(xiàn)有進(jìn)程,創(chuàng)建一個(gè)新進(jìn)程運(yùn)行新代碼。
監(jiān)聽并接受新連接:升級(jí)之后的 Proxygen 開始正常工作, 最終達(dá)到和升級(jí)之前同等水平的一個(gè)連接狀態(tài)。
3.1.2 存在的問題
很多公司都是用的以上那種發(fā)布方式,它的實(shí)現(xiàn)成本比較低,但也存在幾個(gè)問題:
發(fā)布過程中,系統(tǒng)容量會(huì)降低。
從 graceful shutdown 開始,到新代碼已經(jīng)接入了正常量級(jí)的流量,這段時(shí)間內(nèi) 系統(tǒng)容量并沒有達(dá)到系統(tǒng)資源所能支撐的最大值, 例如三個(gè) backend 本來最大能支撐 3N 個(gè)連接,那在升級(jí)其中一臺(tái)的時(shí)間段內(nèi),系統(tǒng)能支撐的最大連接數(shù)就會(huì)小于 3N,在 2N~3N 之間。這也是為什么很多公司都避免在業(yè)務(wù)高峰(而是選擇類似周日凌晨五點(diǎn)這樣的時(shí)間點(diǎn))做這種變更的原因之一。
發(fā)布周期太長
假設(shè)有 100 臺(tái)機(jī)器,分成 100 個(gè)批次(phase),每次發(fā)布一臺(tái), 如果 graceful time 是 10 分鐘,一次發(fā)布就需要 1000 分鐘,顯然是不可接受的。
本質(zhì)上來說,這種方式擴(kuò)展性太差,主機(jī)或?qū)嵗龜?shù)量一多效率就非常低了。
3.2 不損失容量、快速且用戶無感的發(fā)布
以上分析引出的核心問題是:如何在用戶無感知的前提下,不損失容量(without losing capacity)且非常快速(very high velocity)地完成發(fā)布。
3.2.1 早期方案:socket takeover (or zero downtime restart)
我們?cè)谠缙谧约簩?shí)現(xiàn)了一個(gè)所謂的 zero downtime restart 或稱 socket takeover 方案。具體細(xì)節(jié)見前面提到的 LPC 論文,這里只描述下大概原理:相比于等待老進(jìn)程的連接完全退出再開始發(fā)布,我們的做法是直接創(chuàng)建一個(gè)新進(jìn)程,然后通過一個(gè)唯 一的 local socket 將老進(jìn)程中 TCP listen socket 和 UDP sockets 的文件描述符 (以及 SCM rights)轉(zhuǎn)移到新進(jìn)程。
發(fā)布流程
如下圖所示,發(fā)布前,實(shí)例正常運(yùn)行,同時(shí)提供 TCP 和 UDP 服務(wù),其中,
TCP socket 分為兩部分:已接受的連接(編號(hào) 1~N)和監(jiān)聽新連接的 listening socket
UDP socket,bind 在 VIP 上
接下來開始發(fā)布:
創(chuàng)建一個(gè)新實(shí)例
將 TCP listening socket 和 UDP VIP 遷移到新實(shí)例;老實(shí)例仍然 serving 現(xiàn)有 TCP 連接(1 ~ N),
新實(shí)例開始接受新連接(N+1 ~ +∞),包括新的 TCP 連接和新的 UDP 連接
老實(shí)例等待 drain
可以看到,這種方式:
在發(fā)布期間不會(huì)導(dǎo)致系統(tǒng)容器降低,因?yàn)槲覀兺耆A袅死蠈?shí)例,另外創(chuàng)建了一個(gè)新實(shí)例
發(fā)布速度可以顯著加快,因?yàn)榇藭r(shí)可以并發(fā)發(fā)布多個(gè)實(shí)例
老連接被 reset 的概率可以大大降低,只要允許老實(shí)例有足夠的 drain 窗口
那么,這種方式有什么缺點(diǎn)嗎?
存在的問題
一個(gè)顯而易見的缺點(diǎn)是:這種發(fā)布方式需要更多的系統(tǒng)資源,因?yàn)閷?duì)于每個(gè)要升級(jí)的實(shí)例 ,它的新老實(shí)例需要并行運(yùn)行一段時(shí)間;而在之前發(fā)布模型是干掉老實(shí)例再創(chuàng)建新實(shí)例, 不會(huì)同時(shí)運(yùn)行。
但我們今天要討論的是另一個(gè)問題:UDP 流量的分發(fā)或稱解復(fù)用(de-multiplex)。
TCP 的狀態(tài)維護(hù)在內(nèi)核。
UDP 協(xié)議 —— 尤其是維護(hù)連接狀態(tài)的 UDP 協(xié)議,具體來說就是 QUIC —— 所有 狀態(tài)維護(hù)在應(yīng)用層而非內(nèi)核,因此內(nèi)核完全沒有 QUIC 的上下文。
由于 socket 遷移是在內(nèi)核做的,而內(nèi)核沒有 QUIC 上下文(在應(yīng)用層維護(hù)),因此 當(dāng)新老進(jìn)程同時(shí)運(yùn)行時(shí),內(nèi)核無法知道對(duì)于一個(gè)現(xiàn)有 UDP 連接的包,應(yīng)該送給哪個(gè)進(jìn)程 (因?yàn)閷?duì)于 QUIC 沒有 listening socket 或 accepted socket 的概念),因此有些包會(huì)到老進(jìn)程,有些到新進(jìn)程,如下圖左邊所示;
為解決這個(gè)問題,我們引入了用戶空間解決方案。例如在 QUIC 場景下,會(huì)查看 ConnectionID 等 QUIC 規(guī)范中允許攜帶的元信息,然后根據(jù)這些信息,通過另一個(gè) local socket 轉(zhuǎn)發(fā)給相應(yīng)的老進(jìn)程,如以上右圖所示。
雖然能解決 QUIC 的問題,但可以看出,這種方式非常復(fù)雜和脆弱,涉及到大量進(jìn)程間通信,需要維護(hù)許多狀態(tài)。有沒有簡單的方式呢?
3.2.2 其他方案調(diào)研:SO_REUSEPORT
Socket takeover 方案復(fù)雜性和脆弱性的根源在于:為了做到客戶端無感,我們?cè)趦蓚€(gè)進(jìn)程間共享了同一個(gè) socket。因此要解決這個(gè)問題,就要避免在多個(gè)進(jìn)程之間共享 socket。
這自然使我們想到了?SO_REUSEPORT[6]: ?它允許 多個(gè) socket bind 到同一個(gè) port。但這里仍然有一個(gè)問題:UDP 包的路由過程是非一致的(no consistent routing for UDP packets),如下圖所示:
如果新老實(shí)例的 UDP socket bind 到相同端口,那一個(gè)實(shí)例重啟時(shí),哈希結(jié)果就會(huì)發(fā)生變化,導(dǎo)致這個(gè)端口上的包發(fā)生 misrouting。
另一方面,SO_REUSEPORT 還有性能問題,
TCP 是有一個(gè)獨(dú)立線程負(fù)責(zé)接受連接,然后將新連接的文件描述符轉(zhuǎn)給其他線程 ,這種機(jī)制在負(fù)載均衡器中非常典型,可以認(rèn)為是在 socket 層做分發(fā);
UDP 狀態(tài)在應(yīng)用層,因此內(nèi)核只能在 packet 層做分發(fā), 負(fù)責(zé)監(jiān)聽 UDP 新連接的單個(gè)線性不但要處理新連接,還負(fù)責(zé)包的分發(fā),顯然會(huì)存在瓶頸和擴(kuò)展性問題。
因此直接使用 SO_REUSEPORT 是不行的。
3.2.3 思考
我們后退一步,重新思考一下我們的核心需求是什么。有兩點(diǎn):
在內(nèi)核中實(shí)現(xiàn)流量的無損切換,以便客戶端完全無感知;
過程能做到快速和可擴(kuò)展,不存在明顯性能瓶頸;
內(nèi)核提供了很多功能,但并沒有哪個(gè)功能是為專門這個(gè)場景設(shè)計(jì)的。因此要徹底解決問題,我們必須引入某種創(chuàng)新。
理論上:只要我們能控制主機(jī)內(nèi)包的路由過程(routing of the packets within a host),那以上需求就很容易滿足了。
實(shí)現(xiàn)上:仍然基于 SO_REUSEPORT 思想,但同時(shí)解決 UDP 的一致性路由和瓶頸問題。
最終我們引入了一個(gè) socket 層負(fù)載均衡器 bpf_sk_reuseport。
3.3 新方案:bpf_sk_reuseport
3.3.1 方案設(shè)計(jì)
簡單來說,
在 socket 層 attach 一段 BPF 程序,控制 TCP/UDP 流量的轉(zhuǎn)發(fā)(負(fù)載均衡):
通過一個(gè) BPF map 維護(hù)配置信息,業(yè)務(wù)進(jìn)程 ready 之后自己配置流量切換。
3.3.2 好處
這種設(shè)計(jì)的好處:
通用,能處理多種類型的協(xié)議。
在 VIP 層面,能更好地控制新進(jìn)程(新實(shí)例)啟動(dòng)后的流量接入過程,例如
Proxygen 在啟動(dòng)時(shí)經(jīng)常要做一些初始化操作,啟動(dòng)后做一些健康檢測工作, 因此在真正開始干活之前還有一段并未 ready 接收請(qǐng)求/流量的窗口 —— 即使它此時(shí)已經(jīng) bind 到端口了。
在新方案中,我們無需關(guān)心這些,應(yīng)用層自己會(huì)判斷新進(jìn)程什么時(shí)候可以接受流量 并通知 BPF 程序做流量切換;
性能方面,也解決了前面提到的 UDP 單線程瓶頸;
在包的路由(packet-level routing)方面,還支持根據(jù) CPU 調(diào)整路由權(quán)重(adjust weight of traffic per-cpu)。例如在多租戶環(huán)境中,CPU 的利用率可能并不均勻,可以根據(jù)自己的需要實(shí)現(xiàn)特定算法來調(diào)度,例如選擇空閑的 CPU。
最后,未來迭代非常靈活,能支持多種新場景的實(shí)驗(yàn),例如讓每個(gè)收到包從 CPU 負(fù)責(zé)處理該包,或者 NUMA 相關(guān)的調(diào)度。
3.3.3 發(fā)布過程中的流量切換詳解
用一個(gè)?BPF_MAP_TYPE_REUSEPORT_SOCKARRAY?類型的 BPF map 來配置轉(zhuǎn)發(fā)規(guī)則,其中,
key:<VIP>:<Port>
value:socket 的文件描述符,與業(yè)務(wù)進(jìn)程一一對(duì)應(yīng)
如下圖所示,即使新進(jìn)程已經(jīng)起來,但只要還沒 ready(BPF map 中仍然指向老進(jìn)程),
BPF 就繼續(xù)將所有流量轉(zhuǎn)給老進(jìn)程,
新進(jìn)程 ready 后,更新 BPF map,告訴 BPF 程序它可以接收流量了:
BPF 程序就開始將流量轉(zhuǎn)發(fā)給新進(jìn)程了:
前面沒提的一點(diǎn)是:我們?nèi)匀幌M麑?UDP 包轉(zhuǎn)發(fā)到老進(jìn)程上,這里實(shí)現(xiàn)起來其實(shí)就非常簡單了:
已經(jīng)維護(hù)了 flow -> socket 映射
如果 flow 存在,就就轉(zhuǎn)發(fā)到對(duì)應(yīng)的 socket;不存在在創(chuàng)建一個(gè)新映射,轉(zhuǎn)發(fā)給新實(shí)例的 socket。
這也解決了擴(kuò)展性問題,現(xiàn)在可以并發(fā)接收包(one-thread-per-socket),不用擔(dān)心新進(jìn)程啟動(dòng)時(shí)的 disruptions 或 misrouting 了:
3.3.4 新老方案效果對(duì)比
先來看發(fā)布過程對(duì)業(yè)務(wù)流量的擾動(dòng)程度。下圖是我們的生產(chǎn)數(shù)據(jù)中心某次發(fā)布的統(tǒng)計(jì),圖中有兩條線:
一條是已發(fā)布的 server 百分比,
另一個(gè)條是同一時(shí)間的丟包數(shù)量,
可以看到在整個(gè)升級(jí)期間,丟包數(shù)量沒有明顯變化。
再來看流量分發(fā)性能,分別對(duì) socket takeover 和 bpf_sk_reuseport 兩種方式加壓:
控制組/對(duì)照組(左邊):3x 流量時(shí)開始丟包,
實(shí)驗(yàn)組(右邊):30x,因此還沒有到分發(fā)瓶頸但 CPU 已經(jīng)用滿了,但即使這樣丟包仍然很少。
3.3.5 遇到的坑
生產(chǎn)環(huán)境遇到過一個(gè)嚴(yán)重問題:新老進(jìn)程同時(shí)運(yùn)行期間,觀察到 CPU spike 甚至 host locking;但測試環(huán)境從來沒出現(xiàn)過,而且在實(shí)現(xiàn)上我們也沒有特別消耗 CPU 的邏輯。
排查之后發(fā)現(xiàn),這個(gè)問題跟 BPF 程序沒關(guān)系,直接原因是
在同一個(gè) netns 內(nèi)有大量 socket,
新老實(shí)例同時(shí)以支持和不支持 bpf_sk_reuseport 的方式 bind 到了同一端口,
bind("[::1]:443");?/*?without?SO_REUSEPORT.?Succeed.?*/ bind("[::2]:443");?/*?with????SO_REUSEPORT.?Succeed.?*/ bind("[::]:443");??/*?with????SO_REUSEPORT.?Still?Succeed?*/bind() 實(shí)現(xiàn)中有一個(gè) spin lock 會(huì)遍歷一個(gè)很長的 hashtable bucket,
如果有大量 http endpoints,那 key 很可能就是 ?443 和 80;這會(huì)導(dǎo)致 CPU 毛刺甚至機(jī)器卡住。
這個(gè)問題花了很長時(shí)間排查,因此有人在類型場景下遇到類似問題,很可能跟這個(gè)有關(guān)。相關(guān)內(nèi)核代碼[7], 修復(fù)見?patch[8]。
3.3.6?bpf_sk_select_reuseport?vs?bpf_sk_lookup
Cloudflare 引入了?`bpf_sk_lookup`[9],
This?series?proposes?a?new?BPF?program?type?named?BPF_PROG_TYPE_SK_LOOKUP, or?BPF?sk_lookup?for?short.BPF?sk_lookup?program?runs?when?transport?layer?is?looking?up?a?listening socket?for?a?new?connection?request?(TCP),?or?when?looking?up?an unconnected?socket?for?a?packet?(UDP).This?serves?as?a?mechanism?to?overcome?the?limits?of?what?bind()?API?allows to?express.?Two?use-cases?driving?this?work?are:(1)?steer?packets?destined?to?an?IP?range,?fixed?port?to?a?single?socket192.0.2.0/24,?port?80?->?NGINX?socket(2)?steer?packets?destined?to?an?IP?address,?any?port?to?a?single?socket198.51.100.1,?any?port?->?L7?proxy?socket更多信息,可參考他們的論文:
The ties that un-bind: decoupling IP from web services and sockets for robust addressing agility at CDN-scale, SIGCOMM 2021
可以看到,它也允許多個(gè) socket bind 到同一個(gè) port,因此與?bpf_sk_select_reuseport功能有些重疊,因?yàn)槎叨荚从谶@樣一種限制:在收包時(shí),缺少從應(yīng)用層直接命令內(nèi)核選擇哪個(gè) socket 的控制能力。
但二者也是有區(qū)別的:
sk_select_reuseport?與 IP 地址所屬的 socket family 是緊耦合的
sk_lookup?則將 IP 與 socket 解耦 —— lets it pick any / netns
3.3.7 小結(jié)
本節(jié)介紹了我們的基于 BPF_PROG_TYPE_SK_REUSEPORT 和 BPF_MAP_TYPE_REUSEPORT_SOCKARRAY 實(shí)現(xiàn)的新一代發(fā)布技術(shù),它能實(shí)現(xiàn)主機(jī)內(nèi)新老實(shí)例流量的無損切換,優(yōu)點(diǎn):
簡化了運(yùn)維流程,去掉脆弱和復(fù)雜的進(jìn)程間通信(IPC),減少了故障;
效率大幅提升,例如 UDP 性能 10x;
可靠性提升,例如避免了 UDP misrouting 問題和 TCP 三次握手時(shí)的競爭問題。
引用鏈接
[1]
From XDP to Socket: Routing of packets beyond XDP with BPF:?https://linuxplumbersconf.org/event/11/contributions/950/
[2]katran:?https://engineering.fb.com/2018/05/22/open-source/open-sourcing-katran-a-scalable-network-load-balancer/
[3]BPF TCP header options:?https://lwn.net/Articles/827672/
[4]BPF_PROG_TYPE_SK_REUSEPORT:?http://archive.lwn.net:8080/netdev/20180808080131.3014367-1-kafai@fb.com/t/
[5]Zero Downtime Release: Disruption-free Load Balancing of a Multi-Billion User Website:?https://dl.acm.org/doi/pdf/10.1145/3387514.3405885
[6]SO_REUSEPORT:?https://lwn.net/Articles/542629/
[7]代碼:?https://github.com/torvalds/linux/blob/v5.10/net/ipv4/inet_connection_sock.c#L376
[8]patch:?https://lore.kernel.org/lkml/20200601174049.377204943@linuxfoundation.org/
[9]bpf_sk_lookup:?https://lwn.net/Articles/825103/
原文:https://arthurchiao.art/blog/facebook-from-xdp-to-socket-zh/
- END -
看完一鍵三連在看,轉(zhuǎn)發(fā),點(diǎn)贊
是對(duì)文章最大的贊賞,極客重生感謝你
推薦閱讀
定個(gè)目標(biāo)|建立自己的技術(shù)知識(shí)體系
大廠后臺(tái)開發(fā)基本功修煉路線和經(jīng)典資料
Linux網(wǎng)絡(luò)新技術(shù)基石 |eBPF and XDP
性能優(yōu)化實(shí)戰(zhàn)|使用eBPF代替iptables優(yōu)化服務(wù)網(wǎng)格數(shù)據(jù)面性能
萬字長文|深入理解XDP全景指南
你好,這里是極客重生,我是阿榮,大家都叫我榮哥,從華為->外企->到互聯(lián)網(wǎng)大廠,目前是大廠資深工程師,多次獲得五星員工,多年職場經(jīng)驗(yàn),技術(shù)扎實(shí),專業(yè)后端開發(fā)和后臺(tái)架構(gòu)設(shè)計(jì),熱愛底層技術(shù),豐富的實(shí)戰(zhàn)經(jīng)驗(yàn),分享技術(shù)的本質(zhì)原理,希望幫助更多人蛻變重生,拿BAT大廠offer,培養(yǎng)高級(jí)工程師能力,成為技術(shù)專家,實(shí)現(xiàn)高薪夢(mèng)想,期待你的關(guān)注!點(diǎn)擊藍(lán)字查看我的成長之路。
校招/社招/簡歷/面試技巧/大廠技術(shù)棧分析/后端開發(fā)進(jìn)階/優(yōu)秀開源項(xiàng)目/直播分享/技術(shù)視野/實(shí)戰(zhàn)高手等,?極客星球希望成為最有技術(shù)價(jià)值星球,盡最大努力為星球的同學(xué)提供面試,跳槽,技術(shù)成長幫助!詳情查看->極客星球
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 求點(diǎn)贊,在看,分享三連
總結(jié)
以上是生活随笔為你收集整理的eBPF在大厂的应用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大厂提供什么样的软硬件来吸引人才?
- 下一篇: 技术面试中,遇到不会回答的问题怎么破?来