Pod 与 Service

对于 Kubernetes (K8S) 的初学者来说最早接触到的一个概念可能就是 Pod 与 Service. Pod 简单来说就是一个或者一组 container 的集合. 但是在 K8S 的世界里, Pod 的一生可能非常短暂, 稍纵即逝. 而且 Pod 在每次启动时都会分配一个新的 IP 地址. 这样的话集群中互相依赖的 Pod 之间的访问就成了一个新的问题!!

K8S 为了解决这个问题引入了 Service 资源. Service 资源的本质是为一组 Pod 提供一个相对固定的 IP 地址. 无论 Pod 的 IP 地址如何变化, Service 的 IP 地址都不会改变. K8S 集群中每个 Node 上运行的 kube-proxy 组件负责转发 Service 与 Pod 之间的流量. kube-proxy 通过 iptables DNAT 或者 ipvs 规则将一个数据包的目的IP 从 Service 的 Cluster IP 修改为其背后的 Pod 的 IP 地址并进行转发. 比如在我们的 K8S 集群中有如下 pod:

1
2
3
# kubectl get pod dvwa-6bcb999d58-hpg2j -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
dvwa-6bcb999d58-hpg2j 1/1 Running 0 29d 10.42.3.125 node2 <none> <none>

我们看到这个 Pod 的 IP 地址为 10.42.3.125, 并且运行在 node2 这个节点上. 这个 Pod 对应的 Service 为:

1
2
3
# kubectl get service dvwa -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
dvwa NodePort 10.43.18.128 <none> 80:30237/TCP 29d app=dvwa

我们看到这个 Service 的 CLUSTER-IP 为 10.43.18.128. 我们尝试访问下这个服务, 可以看到请求可以正常转发到实际通过服务的 Pod 上.

1
2
3
4
5
6
7
8
9
10
11
# curl -I http://10.43.18.128/login.php
HTTP/1.1 200 OK
Date: Fri, 03 Sep 2021 13:22:12 GMT
Server: Apache/2.4.25 (Debian)
Set-Cookie: PHPSESSID=reqgk2qhhcmf77m9l6fhokftb5; path=/
Expires: Tue, 23 Jun 2009 12:00:00 GMT
Cache-Control: no-cache, must-revalidate
Pragma: no-cache
Set-Cookie: PHPSESSID=reqgk2qhhcmf77m9l6fhokftb5; path=/
Set-Cookie: security=low
Content-Type: text/html;charset=utf-8

此时我们重启 node2 再对 Pod 进行观察. 由于 node2 无法提供服务, pod 漂移到了 node1 并且获得了新的 IP 10.42.2.140. 于此同时, K8S 删除了之前的 Pod dvwa-6bcb999d58-hpg2j. 与此同时 Service dvwa 的 IP 地址并没有改变, 访问此IP 仍然可以访问服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
dvwa-6bcb999d58-7n88s 1/1 Running 0 2m58s 10.42.2.140 node1 <none> <none>
dvwa-6bcb999d58-hpg2j 1/1 Terminating 1 29d 10.42.3.131 node2 <none> <none>

# kubectl get service dvwa -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
dvwa NodePort 10.43.18.128 <none> 80:30237/TCP 29d app=dvwa

# curl -I http://10.43.18.128/login.php
HTTP/1.1 200 OK
Date: Fri, 03 Sep 2021 14:09:26 GMT
Server: Apache/2.4.25 (Debian)
Set-Cookie: PHPSESSID=gvhvtmasg3imm7oe4gqov30pm6; path=/
Expires: Tue, 23 Jun 2009 12:00:00 GMT
Cache-Control: no-cache, must-revalidate
Pragma: no-cache
Set-Cookie: PHPSESSID=gvhvtmasg3imm7oe4gqov30pm6; path=/
Set-Cookie: security=low
Content-Type: text/html;charset=utf-8

通过 iptables 我们还可以来查看下 kube-proxy 定义的数据包转发规则. 首先我们可以找到名为 KUBE-SERVICES 的 chain:

1
2
# iptables -t nat -nvL PREROUTING | grep KUBE
39M 8368M KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */

在 KUBE-SERVICES 这条 chain 里, 我们可以找到与我们 Service DVWA 相关的两条规则:

  • KUBE-MARK-MASQ 会对所有发往 Service 10.43.18.128 80 端口的流量进行标记. 所有被标记的数据包都会在 POSTROUTING 规则中 SNAT 源地址为 node 的 IP 地址.
  • KUBE-SVC-DVCPEHURWBFNLMQF 会匹配到所有发往 10.43.18.128 这个 IP 地址 80 端口的流量.
1
2
3
# iptables -t nat -nvL KUBE-SERVICES | grep dvwa
0 0 KUBE-MARK-MASQ tcp -- * * !10.42.0.0/16 10.43.18.128 /* default/dvwa:http cluster IP */ tcp dpt:80
0 0 KUBE-SVC-DVCPEHURWBFNLMQF tcp -- * * 0.0.0.0/0 10.43.18.128 /* default/dvwa:http cluster IP */ tcp dpt:80

最终我们顺着 chain 可以找到 KUBE-SEP-7NAXZ4TOZS5VAIRS. 里面的DNAT 规则将所有流量都转发到 10.42.2.140 的80端口.

1
2
3
4
5
6
7
8
9
# iptables -t nat -nvL KUBE-SVC-DVCPEHURWBFNLMQF
Chain KUBE-SVC-DVCPEHURWBFNLMQF (2 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-SEP-7NAXZ4TOZS5VAIRS all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/dvwa:http */
# iptables -t nat -nvL KUBE-SEP-7NAXZ4TOZS5VAIRS
Chain KUBE-SEP-7NAXZ4TOZS5VAIRS (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 10.42.2.140 0.0.0.0/0 /* default/dvwa:http */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/dvwa:http */ tcp to:10.42.2.140:80

还记得 10.42.2.140 是谁的 IP 地址吗?

1
2
3
4
# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
dvwa-6bcb999d58-7n88s 1/1 Running 0 2m58s 10.42.2.140 node1 <none> <none>
dvwa-6bcb999d58-hpg2j 1/1 Terminating 1 29d 10.42.3.131 node2 <none> <none>

所以通过上面的实验我们可以大致理解下一个数据包如何通过 iptables 的转发最终到达 Pod.