k8s網絡診斷之我的流量去哪了
背景資訊:
某客戶回報,生産業務的應用在做滾動更新時大量502報錯,同時生産業務中pod設定了prestop來優雅下線(延遲關閉),但是在滾動更新時依然會有502的問題,由于生産環境流量比較高,決定搭建測試環境模拟複現該問題,實際複現過程中發現,pod切換完成後,用戶端通路svc關聯的SLB,應用會逾時一段時間或者qps下降為0<br />
如圖4.0.1所示,qps=0以及timeout都是連接配接異常

圖4.0.1
deplyment裡面關于prestop的設定節選(具有 readiness健康檢查)
spec:
containers:
- image: nginx:1.18
imagePullPolicy: IfNotPresent
lifecycle:
preStop:
exec:
command:
- /bin/bash
- -c
- sleep 20
readinessProbe:
failureThreshold: 5
httpGet:
path: /
port: 80
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 1
複現架構(圖4.0.2):

圖4.0.2
這種問題我們在了解架構之後,看看抓包在哪個環節比較簡便,先抓包看下是建聯異常,還是傳回資料異常呢?
抓eth0的對應svc的nodeport,看起來有重傳,但是不知道重傳給哪個pod,圖4.1.1 圖4.1.2
tcpdump -i eth0 port 31199 -s 0 -w nodeport.pcap
圖4.1.1

圖4.1.2
4.2 抓所有端口接着看,發現有異常的syn重傳在包裡有端口複用,如圖4.2.1所示

圖4.2.1
但是将端口過濾出來看,上一次複用該端口已經正常的fin掉了(證明不是長連接配接的問題),且間隔了一分鐘左右才複用如圖4.2.2,新pod沒有重傳封包,為什麼還是将封包轉發給了老的pod呢?

圖4.2.2
4.3 是否會是ipvs規則更新比較慢導緻轉發給了老的pod?接着收集一下endpoint,pod ,ipvs等元件的變化看看是否存在未更新的情況,收集腳本如下(注意替換ip)
$ cat pod-ep-ipvs.sh
for i in {1..200}
do
echo $(date) >> pod.txt
kubectl get pods -n ratel -o wide |grep v***s-test >> pod.txt
kubectl describe ep -n ratel v***s-test-service >>pod.txt
sudo ipvsadm -Ln |egrep "10.27.255.42:80|10.20.81.251:80" -A 5 >>pod.txt(svc,slb的ip)
sudo ipvsadm -Ln -c |egrep "10.27.1.245|10.27.1.246"|egrep -vi "close|time_wait" >>pod.txt(pod的ip)
sudo egrep "10.27.1.245|10.27.1.246" /proc/net/nf_conntrack|egrep -vi "close|time_wait" >>pod.txt(pod的ip)
sleep 1s
done
從抓取的記錄可以看到相關資源的ipvs規則,pod狀态以及endpoint的設定都是新的podip,說明問題可能出在轉發層面,而負載轉發流量的恰恰就是ipvs本身
2020年 06月 08日 星期一 15:14:28 CST
====pod的采集====
v****s-test-85f56494bd-5swfv 1/1 Running 0 50s 10.27.1.249 cn-beijing.10.20.79.226 <none>
v***s-test-85f56494bd-lx59h 1/1 Running 0 57s 10.27.1.248 cn-beijing.10.20.79.226 <none>
====endpoint的采集====
Name: v***s-test-service
Namespace: r***l
Labels: app=v***s-test
team=default
Annotations: <none>
Subsets:
Addresses: 10.27.1.248,10.27.1.249
NotReadyAddresses: <none>
Ports:
Name Port Protocol
---- ---- --------
crontab 50091 TCP
main 80 TCP
Events: <none>
====ipvs rule的采集====
TCP 10.20.81.251:80 rr
-> 10.27.1.248:80 Masq 1 0 0
-> 10.27.1.249:80 Masq 1 0 0
TCP 10.20.81.252:80 rr
-> 10.27.1.114:8080 Masq 1 0 0
-> 10.27.4.110:8080 Masq 1 0 0
--
TCP 10.27.255.42:80 rr
-> 10.27.1.248:80 Masq 1 0 0
-> 10.27.1.249:80 Masq 1 0 0
TCP 10.27.255.42:50091 rr
-> 10.27.1.248:50091 Masq 1 0 0
-> 10.27.1.249:50091 Masq 1 0 0
前面腳本抓取的日志中是有提取ipvs 的session以及nf_conn表的記錄的,從日志裡面可以看到表項裡面都有syn建聯的封包了,
====ipvs的session采集====
IPVS connection entries
pro expire state source virtual destination
TCP 00:09 SYN_RECV 100.97.130.6:27307 10.20.79.226:31199 10.27.1.245:80
TCP 00:59 SYN_RECV 10.23.31.87:59462 10.20.79.226:31199 10.27.1.246:80
TCP 00:09 SYN_RECV 100.97.130.130:26633 10.20.79.226:31199 10.27.1.245:80
====nf_conntrack表内session的采集====
ipv4 2 tcp 6 107 SYN_SENT src=10.27.2.92 dst=10.27.1.245 sport=37912 dport=18080 [UNREPLIED] src=10.27.1.245 dst=10.27.2.92 sport=18080 dport=37912 mark=0 zone=0 use=2
ipv4 2 tcp 6 119 SYN_SENT src=10.23.31.87 dst=10.20.79.226 sport=59462 dport=31199 [UNREPLIED] src=10.27.1.246 dst=10.23.31.87 sport=80 dport=59462 mark=0 zone=0 use=2
ipv4 2 tcp 6 103 SYN_SENT src=10.27.1.241 dst=10.27.1.246 sport=38702 dport=80 [UNREPLIED] src=10.27.1.246 dst=10.27.1.241 sport=80 dport=38702 mark=0 zone=0 use=2
ipv4 2 tcp 6 104 SYN_SENT src=10.27.4.30 dst=10.27.1.245 sport=36178 dport=18080 [UNREPLIED] src=10.27.1.245 dst=10.27.4.30 sport=18080 dport=36178 mark=0 zone=0 use=2
如下圖所示圖4.3.1,用戶端的syn重傳無法建聯,我們知道tcp按次握手要 syn--synack--ack才能完成建聯,為什麼server端不傳回synack呢?

圖4.3.1
而在server端的抓包如圖4.3.2,雖然有端口複用,但是上一次複用這個端口的時候,對應的連接配接已經被fin掉了,四次揮手fin已經完成的狀态下,對應的連接配接進入time_Wait狀态

圖4.3.2
4.4 在k8s 叢集裡選擇ipvs元件,那麼負責轉發流量給對應的endpoint就是ipvs,這種情況就要抓全量的session去看session狀态變化,懷疑跟五元組相同複用有關系,通過抓ipvs以及/proc/net/nf_conntrack全量session對比發現上一秒time_Wait還在過期倒計時,下一秒複用端口變成了syn_recv,并轉發給了老的ip
2020年 06月 08日 星期一 17:58:22 CST
TCP 01:00 TIME_WAIT 10.23.31.87:58578 10.20.79.226:31199 10.27.1.249:80
ipv4 2 tcp 6 60 TIME_WAIT src=10.23.31.87 dst=10.20.79.226 sport=58578 dport=31199 src=10.27.1.249 dst=10.23.31.87 sport=80 dport=58578 [ASSURED] mark=0 zone=0 use=2
2020年 06月 08日 星期一 17:58:24 CST
TCP 00:58 TIME_WAIT 10.23.31.87:58578 10.20.79.226:31199 10.27.1.249:80
ipv4 2 tcp 6 58 TIME_WAIT src=10.23.31.87 dst=10.20.79.226 sport=58578 dport=31199 src=10.27.1.249 dst=10.23.31.87 sport=80 dport=58578 [ASSURED] mark=0 zone=0 use=2
2020年 06月 08日 星期一 17:58:26 CST
TCP 00:59 SYN_RECV 10.23.31.87:58578 10.20.79.226:31199 10.27.1.249:80
ipv4 2 tcp 6 119 SYN_SENT src=10.23.31.87 dst=10.20.79.226 sport=58578 dport=31199 [UNREPLIED] src=10.27.1.249 dst=10.23.31.87 sport=80 dport=58578 mark=0 zone=0 use=2
2020年 06月 08日 星期一 17:58:27 CST
TCP 00:58 SYN_RECV 10.23.31.87:58578 10.20.79.226:31199 10.27.1.249:80
ipv4 2 tcp 6 117 SYN_SENT src=10.23.31.87 dst=10.20.79.226 sport=58578 dport=31199 [UNREPLIED] src=10.27.1.249 dst=10.23.31.87 sport=80 dport=58578 mark=0 zone=0 use=2
而後确認為bug
https://github.com/kubernetes/kubernetes/issues/81775======start 這一段引用自網絡資料========
原因
K8S 實作了 pod graceful termination,也就是在顯式縮容或者部署新服務進而舊的 replica set 隐式縮容時,請求這個 pod 對應 cluster IP 的用戶端無感。 其實作機制是這樣的:
首先, kube-proxy 把 LVS 裡某個 cluster IP 要删除的 pod IP 的 weight 設定成 0,但并不删除這個 pod IP,目的是讓新的連接配接走剩下的活着的 pod IP,而老的沒斷開的連接配接繼續傳輸資料;
然後, kube-proxy 不斷檢查 LVS 那個要删除的 pod IP 上的活躍連接配接和非活躍連接配接數,隻有當兩者都為零時,才會真的從 LVS 裡删除這個 pod IP。
一般來說,K8S 裡服務之間往往請求很短,會使用短連接配接或者 keepalive time 比較短的長連接配接,而 pod graceful termination 預設期限 30s,也就是 30s 内 pod 還沒退出就會強行殺掉,是以「已有連接配接」有30s 的時間自然斷開,不影響服務,而「新連接配接」有 weight 設定的緣故,不會走到這個要被删除的 pod IP,是以新連接配接暢通無阻, 總結來說,非常完美的 graceful termination。
然而細節是魔鬼,K8S 1.13 裡為了提升性能,把 /proc/sys/net/ipv4/vs/conn_reuse_mode 從預設值1 改成 0 了, 這個值為 1 時,會導緻某種情況下建立連接配接有 1s 的額外延遲,而改成 0 的效果是, 如果請求 cluster IP 的源端口被重用了,也就是在 conntrack table 裡已經有了 條目了,那麼 IPVS 會直接選擇之前服務這個 的 pod,并不會真的按權重走負載均衡邏輯,導緻新的連接配接去了那個要被删除的 pod,當那個 pod 30s 後被删除後,這些重用了源端口的流量就會連接配接失敗了。在這個四元組裡 src_ip 是請求服務的用戶端 pod ip,對于這一個用戶端 pod 來說是固定的,cluster 的 ip 和 port 也都是固定的,隻有 src_port 可以在 net.ipv4.ip_local_port_range 規定的不到三萬個端口裡變化,在高并發短連結情況下非常容易發生源端口複用的情況。
======end 這一段引用自網絡資料========
優化方案:
1,擴大用戶端local_portrange
2,關閉ipvs所在的執行個體的net.ipv4.vs.conn_reuse_mode 設定為1,會導緻性能下降,偶發1s的延遲(0複用會導緻五元組相同轉發給老的pod)
3,使用iptables的模式
總結:
在IPVS網絡代理模式下的kubernetes叢集中進行滾動更新,期間如果用戶端在短時間(兩分鐘)内發送大量請求(短連結,需建聯),且用戶端端口觸發複用(端口随機配置設定不是一直遞增,但是本地端口依然是用的越少越不容易複現),這個時候由于用戶端的請求封包網絡五元組相同,且ipvs的複用連接配接參數net.ipv4.vs.conn_reuse_mode為0時,就會觸發IPVS複用Connection(time_wait變SYN_RECV/syn_sent),會導緻封包被轉發到了一個已經“銷毀”的Pod上,進而導緻建聯失敗,引發業務異常
擴充:
為何ipvs是syn_recv,而nf_conntrack表是syn_sent?
我們依據tcp三次握手的狀态變化來推斷,client跟ipvs建連是以ipvs記錄的是syn_recv,而ipvs實際将流量導向真實pod的時候,ipvs作為一個“中間商”,它發送syn給pod,是以nf_conntrack裡面填充的是syn_sent

-------------------------專家服務 值得信賴------------------------------
-------------------------專家服務 不負所托------------------------------
======再擴充======
案例更新2:
後來遇到一個類似的case,如下這個截圖,為什麼tcp三次握手建聯不成功呢?(端口複用,synack緣何沒有,ack裡面的seq是否正确?對端tcp連接配接的狀态?)

案例更新3:
源pod通路目标位址為nodeip+nodeport,nodeip是terway-eniip的模式,eni網卡直通,但是會時通時不通
client重傳封包如下:

server端接收封包如下:

server端的封包沒有收到一開始的syn?那麼可能在哪個環節被拒絕了呢?
conntrack表的采集日志如下:
