前言
本文通过两个简单的服务之间的访问,结合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