前言
本文通過兩個簡單的服務之間的訪問,結合tcpdump抓包,詳細分析下在IPVS模式下,kubernetes實作通過服務名稱訪問NodePort、ClusterIp型別的service的原理,
當然kubernetes網路實作牽扯到很多知識,特別是對Linux低層的模塊的各種呼叫,如果對Linux中的網路命名空間、eth設備對、網橋等模塊不熟悉的話,可以先參考下另一篇文章[Docker 網路](Docker 網路.md),之后也可以看下另一篇檔案[Kubernetes kube-proxy](Kubernetes kube-proxy詳解.md)來了解下kube-proxy的IPVS模式
本文環境基于flannel網路插件,具體搭建參考kubernetes安裝-二進制
集群環境
| 角色 | 系統 | CPU Core | 記憶體 | 主機名稱 | ip | 安裝組件 |
|---|---|---|---|---|---|---|
| master | 18.04.1-Ubuntu | 4 | 8G | master | 192.168.0.107 | kubectl,kube-apiserver,kube-controller-manager,kube-scheduler,etcd,flannald,kubelet,kube-proxy |
| slave | 18.04.1-Ubuntu | 4 | 4G | slave | 192.168.0.114 | docker,flannald,kubelet,kube-proxy,coredns |
拓撲圖

節點路由資訊
-
master節點
$ route -n -v 內核 IP 路由表 目標 網關 子網掩碼 標志 躍點 參考 使用 介面 0.0.0.0 192.168.0.1 0.0.0.0 UG 600 0 0 wlp3s0 169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 wlp3s0 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-471858815e83 172.30.22.0 0.0.0.0 255.255.255.0 U 0 0 0 docker0 172.30.78.0 172.30.78.0 255.255.255.0 UG 0 0 0 flannel.1 192.168.0.0 0.0.0.0 255.255.255.0 U 600 0 0 wlp3s0 -
slave節點
route -v -n 內核 IP 路由表 目標 網關 子網掩碼 標志 躍點 參考 使用 介面 0.0.0.0 192.168.0.1 0.0.0.0 UG 600 0 0 wlo1 169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 wlo1 172.30.22.0 172.30.22.0 255.255.255.0 UG 0 0 0 flannel.1 172.30.78.0 0.0.0.0 255.255.255.0 U 0 0 0 docker0 192.168.0.0 0.0.0.0 255.255.255.0 U 600 0 0 wlo1
鏡像準備
-
web鏡像
用spring boot啟動了一個web服務,監聽8080埠,里面提供一個方法 /header/list,呼叫這個方法后,會把呼叫者地址相關資訊輸出出來
@RequestMapping("/header/list") public String listHeader(HttpServletRequest request) { log.info("host is" + request.getHeader("host")); log.info("remoteAddr is " + request.getRemoteHost()); log.info("remotePort is " + request.getRemotePort()); return "OK"; } -
curl鏡像
基于 alpine鏡像,只安裝了一個curl命令,使我們可以通過這個命令訪問web服務
FROM alpine:latest RUN apk update RUN apk add --upgrade curl
為節點添加label
為了控制pod啟動到指定的節點完成下面的分析,給兩個節點分別添加不同的label
$ kubectl label nodes master sample=master
node/master labeled
$ kubectl label nodes slave sample=slave
node/slave labeled
例子1
web服務的curl服務對應的pod都在master節點上,由拓撲圖可知,此次訪問通信只用經過master 節點上的docker0網橋即可實作
-
撰寫web服務啟動檔案
$ cat > web.yml <<EOF apiVersion: v1 kind: Service metadata: name: clientip spec: #type: NodePort selector: app: clientip ports: - name: http port: 8080 targetPort: 8080 #nodePort: 8086 --- apiVersion: apps/v1 kind: Deployment metadata: name: clientip-deployment spec: selector: matchLabels: app: clientip replicas: 1 template: metadata: labels: app: clientip spec: nodeSelector: sample: master containers: - name: clientip image: 192.168.0.107/k8s/client-ip-test:0.0.2 ports: - containerPort: 8080 EOF -
撰寫啟動 curl pod的檔案
$ cat > pod_curl.yml <<EOF apiVersion: v1 kind: Pod metadata: name: curl spec: containers: - name: curl image: 192.168.0.107/k8s/curl:1.0 command: - sleep - "3600" nodeSelector: sample: master EOF -
啟動服務
$ kubectl create -f web.yml -f pod_curl.yml $ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES clientip-deployment-5d8b5dcb46-qprps 1/1 Running 0 4s 172.30.22.4 master <none> <none> curl 1/1 Running 0 9s 172.30.22.3 master <none> <none> $ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE clientip ClusterIP 10.254.0.30 <none> 8080/TCP 51s kubernetes ClusterIP 10.254.0.1 <none> 443/TCP 25d可以看到,兩個服務服務都正常啟動起來,并啟動在master節點上
-
啟動監聽master節點上docker0、flannel.1設備
$ tcpdump -n -vv -i docker0 $ tcpdump -n -vv -i flannel.1- 在curl 容器中訪問clientip 這個web服務
$ kubectl exec -it curl curl http://clientip:8080/header/list OK -
監控日志分析
-
web 服務日志
2020-03-06 08:29:05.447 INFO 6 --- [nio-8080-exec-1] c.falcon.clientip.ClientIpController : host isclientip:8080 2020-03-06 08:29:05.447 INFO 6 --- [nio-8080-exec-1] c.falcon.clientip.ClientIpController : remoteAddr is 172.30.22.3 2020-03-06 08:29:05.447 INFO 6 --- [nio-8080-exec-1] c.falcon.clientip.ClientIpController : remotePort is 42000- 請求remoteAddr IP 172.30.22.3對應curl pod的IP地址
-
docker0網路監控(只摘錄了主要流程的日志)
172.30.22.3.47980 > 10.254.0.2.53: [bad udp cksum 0xcd6e -> 0xdae6!] 22093+ A? clientip.default.svc.cluster.local. (52) ... 10.254.0.2.53 > 172.30.22.3.47980: [udp sum ok] 22093*- q: A? clientip.default.svc.cluster.local. 1/0/0 clientip.default.svc.cluster.local. A 10.254.0.30 (102) ... 172.30.22.3.42000 > 10.254.0.30.8080: Flags [P.], cksum 0xcdbb (incorrect -> 0x95b1), seq 0:88, ack 1, win 507, options [nop,nop,TS val 3200284558 ecr 1892112994], length 88: HTTP, length: 88 GET /header/list HTTP/1.1 Host: clientip:8080 User-Agent: curl/7.67.0 Accept: */* ... 172.30.22.3.42000 > 172.30.22.4.8080: Flags [P.], cksum 0x84c2 (incorrect -> 0xdeaa), seq 1:89, ack 1, win 507, options [nop,nop,TS val 3200284558 ecr 1892112994], length 88: HTTP, length: 88 GET /header/list HTTP/1.1 Host: clientip:8080 User-Agent: curl/7.67.0 Accept: */* ... 172.30.22.4.8080 > 172.30.22.3.42000: Flags [P.], cksum 0x84dd (incorrect -> 0xe64b), seq 1:116, ack 89, win 502, options [nop,nop,TS val 1892113104 ecr 3200284558], length 115: HTTP, length: 115 HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 2 Date: Fri, 06 Mar 2020 08:29:05 GMT OK[!http]- 第一條 通過47980埠向DNS服務器發起決議域名clientip.default.svc.cluster.local的請求
- 第二條 DNS服務決議出clientip.default.svc.cluster.local對應的IP是10.254.0.30
- 第三條通過42000埠 向10.254.0.30:8080 發出請求
- 請求10.254.0.30:8080在input鏈上被IPVS匹配,因為10.254.0.30是service的ClusterIp,IPVS匹配成功,采用NAT機制將目的地址轉換成172.30.22.4.8080,進入postrouting,master節點上的路由資訊發現發往172.30.22.4的請求還是通過docker0網路設備發送,所以在docker0上又收到了第四條記錄,即向真實服務172.30.22.4.8080發起的請求
- 第五條記錄 真實的web服務172.30.22.4在完成處理后直接將結果回傳到了172.30.22.3中,沒有經過IPVS的mssq
-
觀察flannel.1設備的輸出,此時是不會出現和請求172.30.22.4相關的資訊,此處略去
-
例子2
curl服務對應的pod在master節點上,web服務對應的pod在slave節點上,由拓撲圖可知,這時要完成從curl的pod內部訪問到web服務依次要經過 master.docker0->master.flannel.1->master.wlp3s0->slave.wlo1->slave.flannel.1->slave.docker0
-
修改web的啟動檔案,將nodeSelector的值修改成sample=slave,重新啟動web應用
$ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES clientip-deployment-68c57b7965-pmwp2 1/1 Running 0 33s 172.30.78.3 slave <none> <none> curl 1/1 Running 0 48m 172.30.22.3 master <none> <none> $ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE clientip ClusterIP 10.254.167.63 <none> 8080/TCP 94s kubernetes ClusterIP 10.254.0.1 <none> 443/TCP 25d -
日志監控
-
監控web服務的日志
$ kubectl logs -f clientip-deployment-68c57b7965-pmwp2 -
監控master各個網路設備的日志
$ tcpdump -n -vv -i docker0 $ tcpdump -n -vv -i flannel.1 $ tcpdump -n -vv -i wlp3s0 -
監控slave節點各個網路設備日志
$ tcpdump -n -vv -i docker0 $ tcpdump -n -vv -i flannel.1 $ tcpdump -n -vv -i wlo1
-
-
監控日志分析
-
web日志
2020-03-07 11:13:22.384 INFO 6 --- [nio-8080-exec-3] c.falcon.clientip.ClientIpController : host isclientip:8080 2020-03-07 11:13:22.384 INFO 6 --- [nio-8080-exec-3] c.falcon.clientip.ClientIpController : remoteAddr is 172.30.22.3 2020-03-07 11:13:22.384 INFO 6 --- [nio-8080-exec-3] c.falcon.clientip.ClientIpController : remotePort is 51596- 對應的遠端IP是172.30.22.3,就是我們發起請求的curl pod對應的IP
-
master網路設備的日志分析(只展示主要流程,tcp握手程序略去)
-
docker0設備
... 11:13:22.346481 IP (tos 0x0, ttl 64, id 28047, offset 0, flags [DF], proto UDP (17), length 80) 172.30.22.3.35482 > 10.254.0.2.53: [bad udp cksum 0xcd6e -> 0x55df!] 3111+ A? clientip.default.svc.cluster.local. (52) ... 11:13:22.355447 IP (tos 0x0, ttl 62, id 34179, offset 0, flags [DF], proto UDP (17), length 130) 10.254.0.2.53 > 172.30.22.3.35482: [udp sum ok] 3111*- q: A? clientip.default.svc.cluster.local. 1/0/0 clientip.default.svc.cluster.local. A 10.254.167.63 (102) ... 11:13:22.359009 IP (tos 0x0, ttl 64, id 23895, offset 0, flags [DF], proto TCP (6), length 140) 172.30.22.3.51596 > 10.254.167.63.8080: Flags [P.], cksum 0x74dd (incorrect -> 0x0f66), seq 1:89, ack 1, win 507, options [nop,nop,TS val 56684247 ecr 2651496809], length 88: HTTP, length: 88 GET /header/list HTTP/1.1 Host: clientip:8080 User-Agent: curl/7.67.0 Accept: */* ... 11:13:22.372907 IP (tos 0x0, ttl 62, id 63303, offset 0, flags [DF], proto TCP (6), length 167) 10.254.167.63.8080 > 172.30.22.3.51596: Flags [P.], cksum 0x077c (correct), seq 1:116, ack 89, win 502, options [nop,nop,TS val 2651496823 ecr 56684247], length 115: HTTP, length: 115 HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 2 Date: Sat, 07 Mar 2020 03:13:22 GMT OK[!http]- 第一條 向DNS服務器發起決議域名clientip.default.svc.cluster.local的請求
- 第二條 DNS服務決議出clientip.default.svc.cluster.local對應的IP是10.254.167.63
- 第三條 向10.254.167.63:8080 發出請求
- 第四條記錄 從10.254.167.63:8080回傳的資訊傳遞給了172.30.22.3.51596
回傳資訊是從10.254.167.63:8080發回來的,和發出去的路徑是一致的,在回傳時IPVS的masq(SNAT),將真實服務器地址轉換成了虛擬地址
-
flannel.1網路設備
11:13:22.359020 IP (tos 0x0, ttl 63, id 23895, offset 0, flags [DF], proto TCP (6), length 140) 172.30.22.3.51596 > 172.30.78.3.8080: Flags [P.], cksum 0xbcc1 (incorrect -> 0xc781), seq 1:89, ack 1, win 507, options [nop,nop,TS val 56684247 ecr 2651496809], length 88: HTTP, length: 88 GET /header/list HTTP/1.1 Host: clientip:8080 User-Agent: curl/7.67.0 Accept: */* ... 11:13:22.372887 IP (tos 0x0, ttl 63, id 63303, offset 0, flags [DF], proto TCP (6), length 167) 172.30.78.3.8080 > 172.30.22.3.51596: Flags [P.], cksum 0xbf97 (correct), seq 1:116, ack 89, win 502, options [nop,nop,TS val 2651496823 ecr 56684247], length 115: HTTP, length: 115 HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 2 Date: Sat, 07 Mar 2020 03:13:22 GMT OK[!http]- 第一條172.30.22.3發出的請求,通過51596埠,向真實的服務器172.30.78.3.8080發起請求,
- 第二條,真實的服務器回傳回應資訊給172.30.22.3.51596
-
wlp3s0網卡(物理網卡)
... 11:13:22.359026 IP (tos 0x0, ttl 64, id 22491, offset 0, flags [none], proto UDP (17), length 190) 192.168.0.107.33404 > 192.168.0.114.8472: [udp sum ok] OTV, flags [I] (0x08), overlay 0, instance 1 IP (tos 0x0, ttl 63, id 23895, offset 0, flags [DF], proto TCP (6), length 140) 172.30.22.3.51596 > 172.30.78.3.8080: Flags [P.], cksum 0xc781 (correct), seq 1:89, ack 1, win 507, options [nop,nop,TS val 56684247 ecr 2651496809], length 88: HTTP, length: 88 GET /header/list HTTP/1.1 Host: clientip:8080 User-Agent: curl/7.67.0 Accept: */* ... 11:13:22.372815 IP (tos 0x0, ttl 64, id 57065, offset 0, flags [none], proto UDP (17), length 217) 192.168.0.114.43021 > 192.168.0.107.8472: [udp sum ok] OTV, flags [I] (0x08), overlay 0, instance 1 IP (tos 0x0, ttl 63, id 63303, offset 0, flags [DF], proto TCP (6), length 167) 172.30.78.3.8080 > 172.30.22.3.51596: Flags [P.], cksum 0xbf97 (correct), seq 1:116, ack 89, win 502, options [nop,nop,TS val 2651496823 ecr 56684247], length 115: HTTP, length: 115 HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 2 Date: Sat, 07 Mar 2020 03:13:22 GMT OK[!http]- 第一條 172.30.22.3.51596向172.30.78.3.8080發出的請求,封裝到了udp資料包的內部,通過物理網卡通道192.168.0.107.33404 > 192.168.0.114.8472進行傳輸
- 第二條 從172.30.78.3.8080向172.30.22.3.51596的回傳資訊,封裝到了udp資料包的內部,通過物理網卡通道192.168.0.114.43021 > 192.168.0.107.8472進行傳輸
-
-
master節點上資料傳輸總結(通過抓包中的時間分析出資料到達各個設備的先后順序,紅色方塊curl開始)

- 發送流程
- 發起請求的pod向DNS服務器發出請求,查找clientip對應的IP地址
- 找到IP后向對應的地址發送真實的請求
- 因為這個IP地址是一個service的ClusterIP,會系結到kubernetes為每一臺節點機器創建的dummy設備kube-ipvs0上,所以宿主機會認為這是一個本機IP,進入內核的input鏈
- IPVS在input鏈上對這個ClusterIP進行判斷,發現是一個集群服務,會執行DNAT,找到一個真實的后端服務(172.30.78.3.8080),將請求目的地址轉換成這個真實服務,之后將請求跳轉到內核的POSTROUTING鏈上
- 在路由選擇階段,根據master節點的路由規則,發現發往172.30.78.0/24的請求要經過flannel.1網路設備,所以flannel.1網路設備中有了172.30.22.3.51596 > 172.30.78.3.8080的請求資訊
- flannel1.1網路設備,通過flanneld,查找到172.30.78.3.8080所在的物理節點,將資料包重新包裝成,追加outerIp、port資訊(192.168.0.114.8472)
- 此時再根據路由規則,發往192.168.0.114的請求需經過wlp3s0網卡,所以wlp3s0上收到了192.168.0.107.33404 > 192.168.0.114.8472的請求包,請求正式發送出去
- 接收流程
- wlp3s0網卡上接收到192.168.0.114.43021回傳的資訊
- 因為是一個vxlan格式的資料包,所以會丟個flanneld處理,將outerIp、Port資訊去除,得到內部的tcp請求資訊172.30.78.3.8080 > 172.30.22.3.51596
- 之后flanneld發請求資訊轉發送給flannel1.1網路設備,所以flannel1.1網路設備上我們能監聽到172.30.78.3.8080 > 172.30.22.3.51596的資料包,當資料到達INPUT時,IPVS開始作業,此時IPVS判斷出此報文是之前發出請求的回應,繼而進行SNAT(在IPVS原始碼塊的handle_response,對于tcp協議是tcp_snat_handler函式中處理),將回傳請求的源地址轉換成真實服務對應的虛擬服務地址,即10.254.167.63.8080,之后使用函式ip_vs_route_me_harder進行重新路由,
- 根據路由規則,發往172.30.22.3.51596的資料包,要經過docker0網路設備,所以我們在docker0設備上看到了10.254.167.63.8080 > 172.30.22.3.51596的資料包
- 發送流程
-
-
slave節點上網路設備的日志分析(只展示主要流程,tcp握手程序略去)
-
docker0設備
... 11:13:22.379401 IP (tos 0x0, ttl 62, id 23895, offset 0, flags [DF], proto TCP (6), length 140) 172.30.22.3.51596 > 172.30.78.3.8080: Flags [P.], cksum 0xc781 (correct), seq 1:89, ack 1, win 507, options [nop,nop,TS val 56684247 ecr 2651496809], length 88: HTTP, length: 88 GET /header/list HTTP/1.1 ... 11:13:22.389173 IP (tos 0x0, ttl 64, id 63303, offset 0, flags [DF], proto TCP (6), length 167) 172.30.78.3.8080 > 172.30.22.3.51596: Flags [P.], cksum 0xbcdc (incorrect -> 0xbf97), seq 1:116, ack 89, win 502, options [nop,nop,TS val 2651496823 ecr 56684247], length 115: HTTP, length: 115 HTTP/1.1 200- 第一條172.30.22.3發出的請求,通過51596埠,向真實的服務器172.30.78.3.8080發起請求,
- 第二條,真實的服務器回傳回應資訊給172.30.22.3.51596
-
flannel.1設備
11:13:22.379392 IP (tos 0x0, ttl 63, id 23895, offset 0, flags [DF], proto TCP (6), length 140) 172.30.22.3.51596 > 172.30.78.3.8080: Flags [P.], cksum 0xc781 (correct), seq 1:89, ack 1, win 507, options [nop,nop,TS val 56684247 ecr 2651496809], length 88: HTTP, length: 88 GET /header/list HTTP/1.1 Host: clientip:8080 User-Agent: curl/7.67.0 Accept: */* ... 11:13:22.389192 IP (tos 0x0, ttl 63, id 63303, offset 0, flags [DF], proto TCP (6), length 167) 172.30.78.3.8080 > 172.30.22.3.51596: Flags [P.], cksum 0xbcdc (incorrect -> 0xbf97), seq 1:116, ack 89, win 502, options [nop,nop,TS val 2651496823 ecr 56684247], length 115: HTTP, length: 115 HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 2 Date: Sat, 07 Mar 2020 03:13:22 GMT OK[!http]- 第一條172.30.22.3發出的請求,通過51596埠,向真實的服務器172.30.78.3.8080發起請求,
- 第二條,真實的服務器回傳回應資訊給172.30.22.3.51596
-
wlo1網卡
11:13:22.379300 IP (tos 0x0, ttl 64, id 22491, offset 0, flags [none], proto UDP (17), length 190) 192.168.0.107.33404 > 192.168.0.114.8472: [udp sum ok] OTV, flags [I] (0x08), overlay 0, instance 1 IP (tos 0x0, ttl 63, id 23895, offset 0, flags [DF], proto TCP (6), length 140) 172.30.22.3.51596 > 172.30.78.3.8080: Flags [P.], cksum 0xc781 (correct), seq 1:89, ack 1, win 507, options [nop,nop,TS val 56684247 ecr 2651496809], length 88: HTTP, length: 88 GET /header/list HTTP/1.1 Host: clientip:8080 User-Agent: curl/7.67.0 Accept: */* ... 11:13:22.389223 IP (tos 0x0, ttl 64, id 57065, offset 0, flags [none], proto UDP (17), length 217) 192.168.0.114.43021 > 192.168.0.107.8472: [udp sum ok] OTV, flags [I] (0x08), overlay 0, instance 1 IP (tos 0x0, ttl 63, id 63303, offset 0, flags [DF], proto TCP (6), length 167) 172.30.78.3.8080 > 172.30.22.3.51596: Flags [P.], cksum 0xbf97 (correct), seq 1:116, ack 89, win 502, options [nop,nop,TS val 2651496823 ecr 56684247], length 115: HTTP, length: 115 HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 2 Date: Sat, 07 Mar 2020 03:13:22 GMT OK[!http] ...- 第一條 172.30.22.3.51596向172.30.78.3.8080發出的請求,封裝到了udp資料包的內部,通過物理網卡通道192.168.0.107.33404 > 192.168.0.114.8472進行傳輸
- 第二條 從172.30.78.3.8080向172.30.22.3.51596的回傳資訊,封裝到了udp資料包的內部,通過物理網卡通道192.168.0.114.43021 > 192.168.0.107.8472進行傳輸
-
slave節點上回應請求程序(通過抓包中的時間分析出資料到達各個設備的先后順序,紅色方塊請求進入為起始點)

- wlo1網卡上接收到192.168.0.107.33404的請求資訊
- 因為是一個vxlan格式的資料包,所以會丟個flanneld處理,將outerIp、Port資訊去除,得到內部的tcp請求資訊172.30.22.3.51596 > 172.30.78.3.8080
- 之后flanneld發請求資訊轉發送給flannel1.1網路設備,所以flannel1.1網路設備上我們能監聽到172.30.22.3.51596 > 172.30.78.3.8080的資料包,
- flannel1.1進行路由選擇,根據路由規則,發送給172.30.78.3.8080的資料包要從docker0設備進入,將請求資料包轉發到docker0設備,所以在docker0設備上監聽到了172.30.22.3.51596 > 172.30.78.3.8080的請求資料包
- 172.30.78.3對應的pod回應請求,并構造response回傳172.30.22.3.51596,在docker0設備上有了172.30.78.3.8080 > 172.30.22.3.51596的回應資訊
- 根據slave上的路由規則,發往172.30.22.3.51596的資料包,要經過flannel1.1網路設備,所以flannel.1網路設備中有了172.30.78.3.8080 > 172.30.22.3.51596的回應資訊
- flannel1.1網路設備,將資料發送給flanneld,flanneld查找到172.30.22.3.51596所在的物理節點,將資料包重新包裝成,追加outerIp、port資訊(192.168.0.107.8472),之后通過路由規則,發往192.168.0.107.8472的資料包從wlo1走,所以在wlo1網卡上出現192.168.0.114.43021 > 192.168.0.107.8472的資料包
-
例子3
只在slave上啟動一個web服務,type設定成NodePort,對應的nodePort設定成8086,從master宿主機上使用curl http://slaveIp:8088/header/list 訪問web服務(直接從slave上訪問,資料不需要傳輸,無法看到slave機器上物理網卡上的資料包,所以為了分析,我們從master上訪問)
原理
當創建NodePort型別的service時,Kubernetes會從API Server指定的引數--service-node-port-range中選擇一個port分配給service,也可以自己通過.spec.ports[*].nodePort自己指定,之后kubernetes會在集群的每個node上監聽對應的port,
除了在所有節點節點上監聽port外,kubernetes會自動給我們創建一個ClusterIP型別的service,所以創建NodePort的service后,也可以像上個例子一樣在集群內部通過 service Name+ service Port的形式訪問
此時資料包不需要在集群內pod中跨主機流轉,所以資料包不會經過flannel.1,資料包處理流程: master.wlp3s0->.slave.wlo1->slave.docker0->slave.docker0->slave.wlo1-> master.wlp3s0
啟動服務
-
修改web啟動檔案
$cat > web.yml <<EOF apiVersion: v1 kind: Service metadata: name: clientip spec: type: NodePort selector: app: clientip ports: - name: http port: 8080 targetPort: 8080 nodePort: 8086 --- apiVersion: apps/v1 kind: Deployment metadata: name: clientip-deployment spec: selector: matchLabels: app: clientip replicas: 1 template: metadata: labels: app: clientip spec: nodeSelector: sample: slave containers: - name: clientip image: 192.168.0.107/k8s/client-ip-test:0.0.2 ports: - containerPort: 8080 EOF -
啟動服務
$ kubectl create -f web.yml service/clientip created deployment.apps/clientip-deployment created $ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES clientip-deployment-68c57b7965-28w4t 1/1 Running 0 10s 172.30.78.3 slave <none> <none> $ kubectl get svc -o wide NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR clientip NodePort 10.254.85.24 <none> 8080:8086/TCP 17s app=clientip kubernetes ClusterIP 10.254.0.1 <none> 443/TCP 27d <none>
日志監控
-
監控web服務的日志
$ kubectl logs -f clientip-deployment-68c57b7965-28w4t -
監控master wlp3s0網卡的日志
$ tcpdump -n -vv -i wlp3s0 -
監控slave節點各個網路設備日志
$ tcpdump -n -vv -i docker0 $ tcpdump -n -vv -i flannel.1 $ tcpdump -n -vv -i wlo1
日志分析(只展示主要流程,tcp握手程序略去)
-
web日志
2020-03-08 10:15:01.498 INFO 6 --- [nio-8080-exec-2] c.falcon.clientip.ClientIpController : host is192.168.0.114:8086 2020-03-08 10:15:01.499 INFO 6 --- [nio-8080-exec-2] c.falcon.clientip.ClientIpController : remoteAddr is 172.30.78.1 2020-03-08 10:15:01.499 INFO 6 --- [nio-8080-exec-2] c.falcon.clientip.ClientIpController : remotePort is 38362- 主意remoteAddr對應的值是172.30.78.1,并不是我們的宿主機IP,原因參考請求程序
-
slave 日志
-
docker0設備日志
... 10:15:01.494019 IP (tos 0x0, ttl 63, id 41431, offset 0, flags [DF], proto TCP (6), length 145) 172.30.78.1.38362 > 172.30.78.3.8080: Flags [P.], cksum 0x171b (correct), seq 0:93, ack 1, win 502, options [nop,nop,TS val 670876057 ecr 1109116899], length 93: HTTP, length: 93 GET /header/list HTTP/1.1 Host: 192.168.0.114:8086 User-Agent: curl/7.58.0 Accept: */* ... 10:15:01.503806 IP (tos 0x0, ttl 64, id 34492, offset 0, flags [DF], proto TCP (6), length 167) 172.30.78.3.8080 > 192.168.0.107.38362: Flags [P.], cksum 0xbbce (incorrect -> 0x0f9e), seq 1:116, ack 94, win 502, options [nop,nop,TS val 1109116911 ecr 670876057], length 115: HTTP, length: 115 HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 2 Date: Sun, 08 Mar 2020 02:15:01 GMT OK[!http] ...-
第一條,請求從docker0進入172.30.78.3.8080,注意此時的請求是從172.30.78.1.38362過來的,就是我們在web容器中看到的remoteAddr,原因參考請求程序
-
第二條,請求處理后從docker0回傳,這時對應的回應回傳的地址又變成了實際發出訪問的地址192.168.0.107.38362
-
-
flnanel.1設備日志
tcpdump -n -vv -i flannel.1 tcpdump: listening on flannel.1, link-type EN10MB (Ethernet), capture size 262144 bytes- 說明沒有相關資料包經過
-
wlo1物理網卡日志
... 10:15:01.493998 IP (tos 0x0, ttl 64, id 41431, offset 0, flags [DF], proto TCP (6), length 145) 192.168.0.107.38362 > 192.168.0.114.8086: Flags [P.], cksum 0x8928 (correct), seq 1:94, ack 1, win 502, options [nop,nop,TS val 670876057 ecr 1109116899], length 93 ... 10:15:01.503827 IP (tos 0x0, ttl 63, id 34492, offset 0, flags [DF], proto TCP (6), length 167) 192.168.0.114.8086 > 192.168.0.107.38362: Flags [P.], cksum 0x489f (correct), seq 1:116, ack 94, win 502, options [nop,nop,TS val 1109116911 ecr 670876057], length 115 ...- 第一條,slave主機收到192.168.0.107.38362發過來的請求
- 第二條,slave將web容器回應的內容回傳給192.168.0.107.38362
-
-
master wlp3s0網卡日志
... 10:15:01.447172 IP (tos 0x0, ttl 64, id 41431, offset 0, flags [DF], proto TCP (6), length 145) 192.168.0.107.38362 > 192.168.0.114.8086: Flags [P.], cksum 0x82b1 (incorrect -> 0x8928), seq 1:94, ack 1, win 502, options [nop,nop,TS val 670876057 ecr 1109116899], length 93 ... 10:15:01.460324 IP (tos 0x0, ttl 63, id 34492, offset 0, flags [DF], proto TCP (6), length 167) 192.168.0.114.8086 > 192.168.0.107.38362: Flags [P.], cksum 0x489f (correct), seq 1:116, ack 94, win 502, options [nop,nop,TS val 1109116911 ecr 670876057], length 115 ...- 第一條向192.168.0.114.8086發送請求
- 第二條從192.168.0.114.8086接收到回應
-
slave上回應請求程序總結(紅色方塊請求進入為起始點)
從master到slave的請求程序和普通請求一樣,此處不在描述

-
wlo1物理網卡收到請求,發現是訪問自己機器的IP,進入Netfilter的INPUT鏈
-
IPVS在input鏈上判斷訪問的地址192.168.0.114.8086是一個集群服務(為什么能判斷出來,參考ipvs判斷原理),從自己的hash表中選擇一個真實的服務172.30.78.3.8080,并做DNAT,將請求的目的地址換成這個真實的服務器地址,進入POSTROUTING階段
-
在POSTROUTING階段,按照IPTABLES的規則會進行masquerade(為什么執行,參考執行masquerade的原因),之后進行路由選擇,根據slave的路由規則表,發往172.30.78.3.8080的資料需要經過docker0,根據masquerade的原理,在發送時將源地址變成了docker0網路設備的地址
$ ip addr 6: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default link/ether 02:42:0d:ab:b0:60 brd ff:ff:ff:ff:ff:ff inet 172.30.78.1/24 brd 172.30.78.255 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::42:dff:feab:b060/64 scope link valid_lft forever preferred_lft forever對應的地址是172.30.78.1,這就是為什么我們在web日志,以及在docker0網路上看到請求是172.30.78.1的原因
-
之后請求轉發到docker0網路,從docker0網路進入到web容器內
-
web容器處理完請求構成回應體,在回傳時發現這個請求是經過masquerade進來的,回傳時查找masquerade前的真實請求發起者,將資料回傳地址設定為192.168.0.107.38362,之后根據路由規則,發送給192.168.0.107.38362的資料包,需要從物理網卡wlo1發送,所以資料轉發給了wlo1網卡,在進入之前,會執行IPVS的masquerade,將源地址修改成192.168.0.114,并通過wlo1網卡發送給master
-
Slave上資料流轉原理
-
IPVS判斷出192.168.0.114.8086是集群服務原理
我們知道IPVS根據自己的hash表中的內容進行判斷,所以kubernetes只需要把集群服務相關的資訊存入到IPVS的hash表中就能實作了,利用ipvsadm工具查看當啟動一個NodePort的service后,kubernetes會在這個hash表中存入哪些內容(下面命令輸出中略去了不相干的記錄)
$ ipvsadm --list IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Scheduler Flags -> RemoteAddress:Port Forward Weight ActiveConn InActConn TCP localhost:8086 rr -> 172.30.78.3:http-alt Masq 1 0 0 TCP slave:8086 rr -> 172.30.78.3:http-alt Masq 1 0 0 TCP promote.cache-dns.local:http rr -> 172.30.78.3:http-alt Masq 1 0 0 ...- 可以看到kubernetes不僅將自動創建的ClusterIP對應的記錄插入到hash表中,針對NodePort服務,還會多插入兩條記錄localhost和hostname對應的規則,這樣當我們訪問192.168.0.114.8086時,能匹配到slave:8086,所以IPVS判斷出這訪問的是一個集群服務,會進行DNAT
-
請求在POSTROUTING階段執行masquerade的原理
-
首先看下采用IPVS模式時,kubernetes給我們創建的ipset,及其作用
name members usage -CLUSTER-IP All service IP + port Mark-Masq for cases that masquerade-all=true or clusterCIDR specified -LOOP-BACK All service IP + port + IP masquerade for solving hairpin purpose -EXTERNAL-IP service external IP + port masquerade for packages to external IPs -LOAD-BALANCER load balancer ingress IP + port masquerade for packages to load balancer type service -LOAD-BALANCER-LOCAL LB ingress IP + port with externalTrafficPolicy=local accept packages to load balancer with externalTrafficPolicy=local -LOAD-BALANCER-FW load balancer ingress IP + port with loadBalancerSourceRanges package filter for load balancer with loadBalancerSourceRanges specified -LOAD-BALANCER-SOURCE-CIDR load balancer ingress IP + port + source CIDR package filter for load balancer with loadBalancerSourceRanges specified -NODE-PORT-TCP nodeport type service TCP port masquerade for packets to nodePort(TCP) -NODE-PORT-LOCAL-TCP nodeport type service TCP port with externalTrafficPolicy=local accept packages to nodeport service with externalTrafficPolicy=local -NODE-PORT-UDP nodeport type service UDP port masquerade for packets to nodePort(UDP) -NODE-PORT-LOCAL-UDP nodeport type service UDP port with externalTrafficPolicy=local accept packages to nodeport service with externalTrafficPolicy=local - 其中KUBE-NODE-PORT-TCP 里面存盤的是需要進行masquerade的本機埠號
-
其次,需要知道kubernetes是如何利用這些ipset的,再看下kubernetes為我們在iptables中追加的規則
下面的輸出內容是在kube-proxy啟動引數:iptables.masqueradeAll=false;clusterCIDR=172.30.0.0/16時的結果,配置成其他的KUBE-SERVICES的規則鏈會稍有不同,輸出進行了精簡只包含了和kubernetes相關的規則
``` $ iptables -n -L -t nat Chain PREROUTING (policy ACCEPT) target prot opt source destination KUBE-SERVICES all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */ Chain OUTPUT (policy ACCEPT) target prot opt source destination KUBE-SERVICES all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */ Chain POSTROUTING (policy ACCEPT) target prot opt source destination KUBE-POSTROUTING all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes postrouting rules */ Chain KUBE-FIREWALL (0 references) target prot opt source destination KUBE-MARK-DROP all -- 0.0.0.0/0 0.0.0.0/0 Chain KUBE-KUBELET-CANARY (0 references) target prot opt source destination Chain KUBE-LOAD-BALANCER (0 references) target prot opt source destination KUBE-MARK-MASQ all -- 0.0.0.0/0 0.0.0.0/0 Chain KUBE-MARK-DROP (1 references) target prot opt source destination Chain KUBE-MARK-MASQ (2 references) target prot opt source destination MARK all -- 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000 Chain KUBE-NODE-PORT (1 references) target prot opt source destination KUBE-MARK-MASQ tcp -- 0.0.0.0/0 0.0.0.0/0 /* Kubernetes nodeport TCP port for masquerade purpose */ match-set KUBE-NODE-PORT-TCP dst Chain KUBE-POSTROUTING (1 references) target prot opt source destination MASQUERADE all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service traffic requiring SNAT */ mark match 0x4000/0x4000 MASQUERADE all -- 0.0.0.0/0 0.0.0.0/0 match-set KUBE-LOOP-BACK dst,dst,src Chain KUBE-SERVICES (2 references) target prot opt source destination KUBE-MARK-MASQ all -- !172.30.0.0/16 0.0.0.0/0 /* Kubernetes service cluster ip + port for masquerade purpose */ match-set KUBE-CLUSTER-IP dst,dst KUBE-NODE-PORT all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 match-set KUBE-CLUSTER-IP dst,dst ``` 1. PREROUTING階段 * 在PREROUTING階段,所有請求會jump到KUBE-SERVICES規則鏈 * 在KUBE-SERVICES規則鏈里,根據我們訪問的地址salveIp:8086,第一條不滿足,會匹配到第二條即訪問的地址是本機,所以jump到KUBE-NODE-PORT規則鏈 * 在KUBE-NODE-PORT規則鏈中會判斷請求目的埠號是否在KUBE-NODE-PORT-TCP這個ipset中,是的話跳轉到KUBE-MARK-MASQ,看下我們啟動NodePort service后這個ipset中的值 ``` $ ipset --list KUBE-NODE-PORT-TCP Name: KUBE-NODE-PORT-TCP Type: bitmap:port Revision: 3 Header: range 0-65535 Size in memory: 8268 References: 1 Number of entries: 1 Members: 8086 ``` kubernetes的確把我們創建的服務對應的node port值存入這個里面了 * KUBE-MARK-MASQ規則鏈對進入這個規則鏈的所有請求都打上一個標簽0x4000 1. POSTROUTING階段 * 在POSTROUTING階段,所有的請求都jump到KUBE-POSTROUTING規則鏈中 * 在KUBE-POSTROUTING規則鏈中,根據第一條規則,當進來的資料包有0x4000標記時進行MASQUERADE,根據在PREROUTING階段中的處理,訪問salveIp:8086的請求會滿足條規則,所以會對我們的請求進行MASQUERADE -
總結
這樣,本文用三個例子,通過用tcpdump對各個網路設備上資料包的分析,闡述了不同情況下kubernetes的網路請求程序,最后一個例子結合kubernetes給我創建的ipset、iptables規則講述了kubernetes實作服務訪問的原理,前面兩個例子讀者也可以采用這樣的方式結合下iptables中的規則鏈,來驗證下資料的流轉流程,
另外最后一個例子,還可以通過集群中master節點的8086來訪問web服務,這時資料包還會經過兩個節點的flannel.1網路設備,但不會經過master.docker0設備,并且web中收到請求的remoteAddr也會不一樣,下面只給出請求程序不再給出具體的tcpdump日志資訊
請求:
master.wlp3s0->master.flannel.1->master.wlp3s0->slave.wlo1->slave.flannel.1->slave.docker0
回應:
slave.docker0->slave.flannel.1->slave.wlo1->master.wlp3s0->master.flannel.1-> master.wlp3s0
讀者朋友可以自行試下,結合tcpdump工具和iptables中的規則對資料包流轉程序進行分析,
題外話
額外的一點思考,為啥kubernetes要設計的這么復雜對通過node port的請求進行masquerade呢,這是因為當創建一個NodePort服務后,kubernetes不只是讓服務對應的endpoint所在的節點上能夠提供服務,而是讓集群中所有的節點都可以在對應的port上提供服務,這樣我們從外部通過node port訪問集群服務時,有可能訪問的服務對應的pod不在我們訪問的節點上,這樣要是不經過masquerade,真實的endpoint處理完請求后在回應時看到的也是真實的clientIP,資料就不會先回傳到client一開始請求的node上,而是直接回傳給了client,這樣client收到結果發現是和請求的地址不一樣的服務器給了回應,會認為這是不合法的的回應體,所以為了讓client能從請求的節點上拿到回應體,所以需要對外部訪問node port的請求統一做masquerade,這樣資料回傳時,會首先回傳到client請求的節點上,再由此節點回傳給client,如果因為業務需求,如一些審計什么的,必須要獲取到client的真實IP,可以考慮下面三種方式:
- 在集群外層再加一個代理(ingress方式),在代理里面獲取client IP,存入約定好的header 頭中,在集群內的服務通過這個header資訊來獲取
- POD直接設定成hostNetwork
- 通過將服務設定{"externalTrafficPolicy":"Local"}},這時如果pod不在對應的節點上時,是無法提供服務的
kubernetes的官網對此的探討:
Using Source IP
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/38927.html
標籤:其他
上一篇:Kubernetes(十八) kube-proxy 詳解
下一篇:http協議
