NETGEAR拒绝连接请求_破案:Kubernetes/Docker 上无法解释的连接超时
編譯:bot(才云)
技術(shù)校對(duì):星空下的文仔(才云)
編者按:將應(yīng)用遷移到 Kubernetes 時(shí),有時(shí)候工程師們會(huì)發(fā)現(xiàn)一些令人費(fèi)解的連接超時(shí),無論怎么排查都找不到原因。在這篇文章中,軟件架構(gòu)師 Maxime Lagresle分享了自己團(tuán)隊(duì)的親身經(jīng)驗(yàn)。
Linux 內(nèi)核有一個(gè)眾所周知的問題,就是在做 SNAT(修改數(shù)據(jù)包的源地址)時(shí)容易出現(xiàn) SYN 丟包。
默認(rèn)情況下,SNAT 一般使用 iptables 偽裝規(guī)則在 Docker 和 Flannel 的傳出連接上執(zhí)行,當(dāng)多個(gè)容器同時(shí)嘗試和同一外部地址建立新連接時(shí),丟包情況就會(huì)發(fā)生。在一些情景下,可能兩個(gè)連接會(huì)被分配到一個(gè)端口用做地址轉(zhuǎn)換,導(dǎo)致一個(gè)或多個(gè)丟包,以及至少 1 秒的連接延遲。
Linux 的源代碼中提到了這個(gè)現(xiàn)象,內(nèi)核也已經(jīng)支持一個(gè)緩解此問題的 flag,但目前社區(qū)還沒有太多相關(guān)文檔。為了讓更多開發(fā)者,尤其是不熟悉 DNAT 的 Kubernetes 新手提前做好心理準(zhǔn)備,在這篇文章中,我們將嘗試去調(diào)查這個(gè)問題,并尋找可靠的解決方案。
注:DNAT 上也存在相同的問題,這意味著在 Kubernetes 上,你訪問 ClusterIP 時(shí)也可能丟包。當(dāng)你從 Pod 發(fā)送請(qǐng)求到 ClusterIP,默認(rèn)情況下,kube-proxy(通過 iptables)會(huì)把 ClusterIP 改成你想要訪問的 Service 的某個(gè) Pod IP,而該問題的存在會(huì)導(dǎo)致 DNS 解析域名時(shí)出現(xiàn)間歇性延遲,詳參 Issue 56903。背景簡(jiǎn)介
Maxime Lagresle 是某公司后端架構(gòu)團(tuán)隊(duì)的軟件架構(gòu)師。他的團(tuán)隊(duì)花了一年時(shí)間構(gòu)建了一個(gè) PaaS,經(jīng)過謹(jǐn)慎評(píng)估,他們決定把基于 Capistrano/Marathon/Bash 的部署都遷移到 Kubernetes 上。
那時(shí)候,他們的設(shè)置依賴 Ubuntu Xenial 虛擬機(jī)(Docker v17.06)、Kubernetes v1.8,以及在主機(jī)網(wǎng)關(guān)模式下運(yùn)行的 Flannel v1.9.0 。
在遷移時(shí),Maxime Lagresle 團(tuán)隊(duì)注意到,當(dāng)把這些部署放到 Kubernetes 上運(yùn)行時(shí),應(yīng)用程序中的連接超時(shí)會(huì)明顯增加。而在他們把第一個(gè)基于 Scala 的應(yīng)用程序遷移上去后,超時(shí)的情況就更明顯了——相比平時(shí)的幾百毫秒,幾乎每一秒都會(huì)有一個(gè)響應(yīng)非常慢的請(qǐng)求。但這個(gè)應(yīng)用程序非常普通,只負(fù)責(zé)公開 REST 端點(diǎn)并查詢平臺(tái)上的其他 Service,收集、處理數(shù)據(jù)并將數(shù)據(jù)返回給客戶端。
經(jīng)過觀察,他們發(fā)現(xiàn)請(qǐng)求的響應(yīng)時(shí)間很奇怪,幾乎都被延遲了 1-3 秒。于是他們決定找出原因。
縮小問題范圍
在他們的待辦列表中,有一項(xiàng)任務(wù)是監(jiān)控 KubeDNS 的表現(xiàn)。由于依賴 HTTP 客戶端,名稱解析時(shí)間可能是連接時(shí)間的一部分,他們決定先處理該任務(wù),并確保該組件運(yùn)行良好。
為了判斷這一猜想,Maxime Lagresle 團(tuán)隊(duì)寫了一個(gè)小的 DaemonSet,它可以直接查詢 KubeDNS 和數(shù)據(jù)中心名稱服務(wù)器,并將響應(yīng)時(shí)間發(fā)送到 InfluxDB。很快,圖表顯示響應(yīng)時(shí)間非???#xff0c;名稱解析并不是延遲的罪魁禍?zhǔn)住?/p>
之后,他們又將重點(diǎn)轉(zhuǎn)移到探索這些延遲背后的含義上。負(fù)責(zé)該 Scala 應(yīng)用的團(tuán)隊(duì)先做了一些修改,使響應(yīng)慢的請(qǐng)求在后臺(tái)繼續(xù)發(fā)送,并在向客戶端拋出超時(shí)錯(cuò)誤后記錄持續(xù)時(shí)間。Maxime Lagresle 團(tuán)隊(duì)則在運(yùn)行應(yīng)用程序的 Kubernetes 節(jié)點(diǎn)上做網(wǎng)絡(luò)跟蹤,嘗試將響應(yīng)慢的請(qǐng)求與網(wǎng)絡(luò)轉(zhuǎn)儲(chǔ)的內(nèi)容進(jìn)行匹配。
10.244.38.20 嘗試連接到端口 80 上的 10.16.46.24結(jié)果顯示,延遲是由第一個(gè)網(wǎng)絡(luò)數(shù)據(jù)包的重傳引起的,該數(shù)據(jù)包的作用是啟動(dòng)連接(具有 SYN 標(biāo)志的數(shù)據(jù)包)。這解釋了請(qǐng)求響應(yīng)時(shí)間的延遲量為什么是 1-3 秒,因?yàn)檫@種數(shù)據(jù)包的重傳延遲是 1 秒(第二次)、3 秒(第三次)、6 秒、12 秒、24 秒……以此類推。
這是一個(gè)有趣的發(fā)現(xiàn),因?yàn)閬G失 SYN 數(shù)據(jù)包不是由隨機(jī)網(wǎng)絡(luò)故障導(dǎo)致的,而可能是網(wǎng)絡(luò)設(shè)備或 SYN 泛洪保護(hù)算法主動(dòng)丟棄了新連接。
在默認(rèn)的 Docker 安裝中,每個(gè)容器都有一個(gè)虛擬網(wǎng)絡(luò)接口(veth)上的 IP,它和主接口(如 eth0、docker0)一樣連接著 Docker 主機(jī)上的 Linux 網(wǎng)橋。容器通過網(wǎng)橋互相通信,如果容器想連接 Docker 主機(jī)外部地址,那么數(shù)據(jù)包會(huì)先進(jìn)入網(wǎng)橋,再通過 eth0 路由到服務(wù)器外部。
下圖的示例已經(jīng)對(duì)默認(rèn) Docker 設(shè)置進(jìn)行了調(diào)整,以匹配網(wǎng)絡(luò)捕獲中看到的網(wǎng)絡(luò)配置:
實(shí)際上 veth 端口是成對(duì)出現(xiàn)的,但這里不需要關(guān)注這點(diǎn)Maxime Lagresle 團(tuán)隊(duì)隨機(jī)查看了網(wǎng)橋上的數(shù)據(jù)包,之后繼續(xù)查看虛擬機(jī)的主接口 eth0,并根據(jù)結(jié)果集中查看網(wǎng)絡(luò)基礎(chǔ)架構(gòu)和虛擬機(jī)。
10.244.38.20 嘗試連接到端口 80 上的 10.16.46.24網(wǎng)絡(luò)捕獲結(jié)果顯示,第一個(gè) SYN 包在 13:42:23.828339 從容器網(wǎng)絡(luò)接口(veth)離開,經(jīng)過網(wǎng)橋(cni0)。在 13:42:24.826211 待了 1 秒后,容器沒有從 10.16.46.24 得到響應(yīng)就重傳了這個(gè)包。所以再一次,這個(gè)包會(huì)先出現(xiàn)在容器的網(wǎng)絡(luò)接口,然后是網(wǎng)橋。
在下一行,我們可以看到這個(gè)包在 IP 地址和端口從 10.244.38.20:38050 轉(zhuǎn)換成 10.16.34.2:10011 之后,在 13:42:24.826263 離開了 eth0。下一行顯示遠(yuǎn)程服務(wù)是如何響應(yīng)的。
很明顯,問題很可能出在虛擬機(jī)上,與其他基礎(chǔ)架構(gòu)無關(guān)。為了配置更靈活的解決方案,他們編寫了一個(gè)非常簡(jiǎn)單的 Go 程序,可以通過一些可配置的設(shè)置對(duì)端點(diǎn)發(fā)出請(qǐng)求:
- 兩個(gè)請(qǐng)求之間的延遲;
- 并發(fā)請(qǐng)求的數(shù)量;
- 超時(shí);
- 要調(diào)用的端點(diǎn)。
要連接的遠(yuǎn)程端點(diǎn)是具有 Nginx 的虛擬機(jī),測(cè)試程序?qū)⑨槍?duì)此端點(diǎn)發(fā)出請(qǐng)求,并記錄任何高于一秒的響應(yīng)時(shí)間。
Go 程序:https://github.com/maxlaverse/snat-race-conn-test
解決問題
已知數(shù)據(jù)包是在網(wǎng)橋之間丟包的,eth0 正是執(zhí)行 SNAT 操作的地方。所以,如果因?yàn)槟承┰?#xff0c;導(dǎo)致 Linux 內(nèi)核無法分配一個(gè)空閑的源端口來做地址轉(zhuǎn)換,他們就看不到這個(gè)包出 eth0。
這里可以設(shè)一個(gè)簡(jiǎn)單的測(cè)試來做驗(yàn)證——嘗試 pod-to-pod 通信并記錄響應(yīng)延遲的請(qǐng)求的數(shù)量。Maxime Lagresle 團(tuán)隊(duì)做了測(cè)試,發(fā)現(xiàn)沒有丟包。接下來,他們準(zhǔn)備深入看一下 conntrack。
注:為了更好地理解后續(xù)內(nèi)容,建議讀者先了解一些關(guān)于源網(wǎng)絡(luò)地址轉(zhuǎn)換的知識(shí)。用戶空間中的 conntrack
在整個(gè)過程中,他們提出了一系列猜想:
猜想一:大部分連接都被轉(zhuǎn)成相同的 host:port。
這一點(diǎn)已經(jīng)被否定了。
猜想二:這個(gè)現(xiàn)象很可能是一些配置錯(cuò)誤的 SYN 泛洪保護(hù)引起的。
他們檢查了網(wǎng)絡(luò)內(nèi)核參數(shù),并沒有找到自己不知道的機(jī)制;增加了 conntrack 表的大小,內(nèi)核日志也沒有報(bào)錯(cuò)。所以這個(gè)猜想也不正確。
猜想三:端口復(fù)用。如果端口耗盡并且沒有可用于 SNAT 操作的端口,那么數(shù)據(jù)包是可能會(huì)被丟棄或拒絕的。
他們查看了 conntrack 表,發(fā)現(xiàn) conntrack 包有一個(gè)命令,可以顯示一些統(tǒng)計(jì)信息(conntrack -S)。在運(yùn)行該命令時(shí),有一個(gè)字段非常有趣:“insert_failed”具有非零值。
他們?cè)俅芜\(yùn)行測(cè)試程序,并密切關(guān)注字段的統(tǒng)計(jì)信息,發(fā)現(xiàn)如果按一個(gè)丟包導(dǎo)致 1 秒請(qǐng)求延遲、兩個(gè)丟包導(dǎo)致 3 秒請(qǐng)求延遲來算,統(tǒng)計(jì)信息里的數(shù)據(jù)和丟包的數(shù)量是完全一致的!
man page 上有對(duì)那個(gè)字段的清楚描述:嘗試列表插入但失敗的條目數(shù)(如果已存在相同的條目,則會(huì)發(fā)生)。但這個(gè)描述沒有解答在什么情況下插入會(huì)失敗?無論如何,在低負(fù)載服務(wù)器發(fā)生丟包怎么看都不像是正常現(xiàn)象。
Netfilter NAT & Conntrack 內(nèi)核模塊
在閱讀了 Netfilter 內(nèi)核代碼之后,他們決定重新編譯它并添加一些跟蹤。
NAT 代碼在 POSTROUTING 鏈上被 hook 了兩次。首先,通過修改源 IP 和端口來修改包的結(jié)構(gòu);其次,如果期間沒有丟包,在 conntrack 表中記錄轉(zhuǎn)換。這意味著 SNAT 端口分配與表中的插入之間存在延遲,如果存在沖突和數(shù)據(jù)包丟失,則可能會(huì)導(dǎo)致插入失敗。
在 tcp 連接上執(zhí)行 SNAT 時(shí),NAT 模塊會(huì)做以下嘗試:
- 第一步:如果數(shù)據(jù)包的源 IP 在目標(biāo) NAT 池中,并且元組可用,則返回(數(shù)據(jù)包保持不變);
- 第二步:找到 NAT 池中使用最少的 IP,并用它替換數(shù)據(jù)包中的源 IP;
- 第三步:檢查端口是否在允許的端口范圍內(nèi)(默認(rèn)為 1024-64512),以及具有該端口的元組是否可用。如果是,請(qǐng)返回(源 IP 已更改,端口保留)。注:SNAT 端口范圍不受內(nèi)核參數(shù)值net.ipv4.ip_local_port_rangekernel 的影響;
- 第四步:端口不可用,通過調(diào)用 nf_nat_l4proto_unique_tuple() 請(qǐng)求 tcp 層為 SNAT 找到一個(gè)唯一的端口。
按照上述步驟,當(dāng)主機(jī)僅運(yùn)行一個(gè)容器時(shí),NAT 模塊很可能在第三步之后返回,容器內(nèi)部進(jìn)程使用的本地端口也將被保留并用于傳出連接。當(dāng) Docker 主機(jī)運(yùn)行多個(gè)容器時(shí),連接的源端口更可能已被另一個(gè)容器的連接使用。在這種情況下,調(diào)用 nf_nat_l4proto_unique_tuple () 以查找 NAT 操作的可用端口。
Netfilter 還支持另外兩種算法來查找 SNAT 空閑端口:
- 部分隨機(jī)端口搜索初始位置分配。在 SNAT 規(guī)則有NF_NAT_RANGE_PROTO_RANDOM flag 的時(shí)候被使用;
- 完全隨機(jī)端口搜索初始位置分配。在規(guī)則有 NF_NAT_RANGE_PROTO_RANDOM_FULLY flag 的時(shí)候被使用。
NF_NAT_RANGE_PROTO_RANDOM 雖然降低了兩個(gè)線程以相同初始端口啟動(dòng)的次數(shù),但仍有很多錯(cuò)誤。Maxime Lagresle 團(tuán)隊(duì)在使用 NF_NAT_RANGE_PROTO_RANDOM_FULLY 時(shí),才發(fā)現(xiàn) conntrack 表插入錯(cuò)誤的次數(shù)明顯變少:在 Docker 測(cè)試虛擬機(jī)上,使用默認(rèn)偽裝規(guī)則,且有 10-80 個(gè)線程連接到同一主機(jī)時(shí),conntrack 表的插入失敗率大約在 2% 到 4%;如果在內(nèi)核強(qiáng)制使用完全隨機(jī),錯(cuò)誤就降到了 0(在真實(shí)集群中確實(shí)也接近 0)。
激活 K8s 上的完全隨機(jī)端口分配
NF_NAT_RANGE_PROTO_RANDOM_FULLY flag 需要在偽裝規(guī)則中設(shè)置。在Maxime Lagresle 團(tuán)隊(duì)的 Kubernetes 設(shè)置中,Flannel 負(fù)責(zé)添加這些規(guī)則。它在 Docker 鏡像構(gòu)建期間從源代碼構(gòu)建 iptables。iptables 工具不支持設(shè)置此 flag,但 Maxime Lagresle 團(tuán)隊(duì)已經(jīng)提交了一個(gè)合并的小補(bǔ)丁并添加了此功能。
Flannel 補(bǔ)丁:
https://gist.github.com/maxlaverse/1fb3bfdd2509e317194280f530158c98
他們現(xiàn)在使用的是應(yīng)用了這個(gè)補(bǔ)丁的 Flannel 修改版本,并在偽裝規(guī)則上增加了 --random-fullyflag(4 行更改)。通過 DaemonSet,他們可以在每個(gè)節(jié)點(diǎn)上獲取 conntrack 統(tǒng)計(jì)信息,并將指標(biāo)發(fā)送到 InfluxDB 以密切關(guān)注插入錯(cuò)誤。
通過這個(gè)補(bǔ)丁,他們的錯(cuò)誤數(shù)量已經(jīng)從每個(gè)節(jié)點(diǎn)幾秒一次下降到整個(gè)集群幾小時(shí)一次,效果顯著。
小結(jié)
Kubernetes 正值快速發(fā)展,但上述問題卻到現(xiàn)在還存在著,討論它的人也不多,這一點(diǎn)是令人驚訝的。隨著越來越多應(yīng)用程序連接到相同的端點(diǎn),這一問題的嚴(yán)重性將被很快暴露出來。
我們需要采取一些額外的解決措施,因?yàn)槊總€(gè)人都在使用這種 DNS 循環(huán),或者將 IP 添加到每個(gè)主機(jī)的 NAT 池。如果你在工作中曾遇到過這個(gè)問題,希望這篇文章對(duì)你有所幫助!
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的NETGEAR拒绝连接请求_破案:Kubernetes/Docker 上无法解释的连接超时的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 汇编程序格式
- 下一篇: tensorflow切换到gpu_使用免