容器网络7:从外部访问Service

在前面的文章中,我们讨论了Kubernetes内部的Pod之间如何互相访问。接下来,我们重点讨论如何从外部访问Kubernetes内部的Pod。

一、NodePort

通过前面的讲解,我们应该能够明白这样一个事实:Service 的访问信息在 Kubernetes 集群之外,其实是无效的。这其实也容易理解:所谓 Service 的访问入口,其实就是每台宿主机上由 kube-proxy 生成的 iptables 规则,以及 kube-dns 生成的 DNS 记录。而一旦离开了这个集群,这些信息对用户来说,也就没有作用了。

所以,在使用 Kubernetes 的 Service 时,一个必须要面对和解决的问题就是:如何从外部(Kubernetes 集群之外),访问到 Kubernetes 里创建的 Service?

这里最常用的一种方式就是:NodePort。我们先来看这个例子。

1.1、创建NodePort Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Service
metadata:
name: my-nginx
labels:
run: my-nginx
spec:
type: NodePort
ports:
- nodePort: 8080
targetPort: 80
protocol: TCP
name: http
- nodePort: 443
protocol: TCP
name: https
selector:
run: my-nginx

在这个 Service 的定义里,我们声明它的类型是,type=NodePort。然后,在 ports 字段里声明了 Service 的 8080 端口代理 Pod 的 80 端口,Service 的 443 端口代理 Pod 的 443 端口。

当然,如果我们不显式地声明 nodePort 字段,Kubernetes 就会为我们分配随机的可用端口来设置代理。这个端口的范围默认是 30000-32767,我们可以通过 kube-apiserver 的–service-node-port-range 参数来修改它。

1.2、访问Service

要访问这个 Service,我们只需要访问:

1
<任何一台宿主机的IP地址>:8080

就可以访问到某一个被代理的 Pod 的 80 端口了。

1.3、理解NodePort模式

在理解了Service 的工作原理之后,NodePort 模式也就非常容易理解了。显然,kube-proxy 要做的,就是在每台宿主机上生成这样一条 iptables 规则:

1
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/my-nginx: nodePort" -m tcp --dport 8080 -j KUBE-SVC-67RL4FN6JRUPOJYM

而我们在前面已经讲到,KUBE-SVC-67RL4FN6JRUPOJYM 其实就是一组随机模式的 iptables 规则。所以接下来的流程,就跟 ClusterIP 模式完全一样了。

1.4、理解SNAT操作的作用

需要注意的是,在 NodePort 方式下,Kubernetes 会在 IP 包离开宿主机发往目的 Pod 时,对这个 IP 包做一次 SNAT 操作,如下所示:

1
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE

可以看到,这条规则设置在 POSTROUTING 检查点,也就是说,它给即将离开这台主机的 IP 包,进行了一次 SNAT 操作,将这个 IP 包的源地址替换成了这台宿主机上的 CNI 网桥地址,或者宿主机本身的 IP 地址(如果 CNI 网桥不存在的话)。

当然,这个 SNAT 操作只需要对 Service 转发出来的 IP 包进行(否则普通的 IP 包就被影响了)。而 iptables 做这个判断的依据,就是查看该 IP 包是否有一个“0x4000”的“标志”。这个标志正是在 IP 包被执行 DNAT 操作之前被打上去的。

可是,为什么一定要对流出的包做 SNAT操作呢?

这里的原理其实很简单,如下所示:

1
2
3
4
5
6
7
8
9
          client
\ ^
\ \
v \
node 1 <--- node 2
| ^ SNAT
| | --->
v |
endpoint

当一个外部的 client 通过 node 2 的地址访问一个 Service 的时候,node 2 上的负载均衡规则,有可能把这个 IP 包转发给一个在 node 1 上的 Pod。这里没有任何问题。

而当 node 1 上的这个 Pod 处理完请求之后,它就会按照这个 IP 包的源地址发出回复。

可是,如果没有做 SNAT 操作的话,这时候,被转发来的 IP 包的源地址就是 client 的 IP 地址。所以此时,Pod 就会直接将回复发给client。对于 client 来说,它的请求明明发给了 node 2,收到的回复却来自 node 1,这个 client 很可能会报错。

所以,在上图中,当 IP 包离开 node 2 之后,它的源 IP 地址就会被 SNAT 改成 node 2 的 CNI 网桥地址或者 node 2 自己的地址。这样,Pod 在处理完成之后就会先回复给 node 2(而不是 client),然后再由 node 2 发送给 client。

当然,这也就意味着这个 Pod 只知道该 IP 包来自于 node 2,而不是外部的 client。对于 Pod 需要明确知道所有请求来源的场景来说,这是不可以的。

1.5、externalTrafficPolicy字段的作用

我们可以将 Service 的 spec.externalTrafficPolicy 字段设置为 local,这就保证了所有 Pod 通过 Service 收到请求之后,一定可以看到真正的、外部 client 的源地址。

而这个机制的实现原理也非常简单:这时候,一台宿主机上的 iptables 规则,会设置为只将 IP 包转发给运行在这台宿主机上的 Pod。所以这时候,Pod 就可以直接使用源地址将回复包发出,不需要事先进行 SNAT 了。这个流程,如下所示:

1
2
3
4
5
6
7
8
9
      client
^ / \
/ / \
/ v X
node 1 node 2
^ |
| |
| v
endpoint

当然,这也就意味着如果在一台宿主机上,没有任何一个被代理的 Pod 存在,比如上图中的 node 2,那么我们使用 node 2 的 IP 地址访问这个 Service,就是无效的。此时,请求会直接被 DROP 掉。

二、LoadBalancer

从外部访问 Service 的第二种方式,适用于公有云上的 Kubernetes 服务。这时候,我们可以指定一个 LoadBalancer 类型的 Service,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
---
kind: Service
apiVersion: v1
metadata:
name: example-service
spec:
ports:
- port: 8765
targetPort: 9376
selector:
app: example
type: LoadBalancer

在公有云提供的 Kubernetes 服务里,都使用了一个叫作 CloudProvider 的转接层,来跟公有云本身的 API 进行对接。所以,在上述 LoadBalancer 类型的 Service 被提交后,Kubernetes 就会调用 CloudProvider 在公有云上为我们创建一个负载均衡服务,并且把被代理的 Pod 的 IP 地址配置给负载均衡服务做后端。

三、ExternalName

第三种方式,是 Kubernetes 在 1.7 之后支持的一个新特性,叫作 ExternalName。举个例子:

1
2
3
4
5
6
7
kind: Service
apiVersion: v1
metadata:
name: my-service
spec:
type: ExternalName
externalName: my.database.example.com

在上述 Service 的 YAML 文件中,指定了一个 externalName=my.database.example.com 的字段。而且我们应该会注意到,这个 YAML 文件里不需要指定 selector。

这时候,当我们通过 Service 的 DNS 名字访问它的时候,比如访问:my-service.default.svc.cluster.local。那么,Kubernetes 返回的就是my.database.example.com。所以说,ExternalName 类型的 Service,其实是在 kube-dns 里添加了一条 CNAME 记录。这时,访问 my-service.default.svc.cluster.local 就和访问 my.database.example.com 这个域名是一个效果了。

四、ExternalIP

此外,Kubernetes 的 Service 还允许我们为 Service 分配公有 IP 地址,比如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kind: Service
apiVersion: v1
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376
externalIPs:
- 80.11.12.10

在上述 Service 中,我们为它指定的 externalIPs=80.11.12.10,那么此时,就可以通过访问 80.11.12.10:80 访问到被代理的 Pod 了。不过,在这里 Kubernetes 要求 externalIPs 必须是至少能够路由到一个 Kubernetes 的节点。

五、Service故障定位

在理解了 Kubernetes Service 机制的工作原理之后,很多与 Service 相关的问题,其实都可以通过分析 Service 在宿主机上对应的 iptables 规则(或者 IPVS 配置)得到解决。

5.1、确认 kube-dns运行正常

比如,当我们的 Service 没办法通过 DNS 访问到的时候。我们就需要区分到底是 Service 本身的配置问题,还是集群的 DNS 出了问题。一个行之有效的方法,就是检查 Kubernetes 自己的 Master 节点的 Service DNS 是否正常:

1
2
3
4
5
6
7
# 在一个Pod里执行
$ nslookup kubernetes.default
Server: 10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name: kubernetes.default
Address 1: 10.0.0.1 kubernetes.default.svc.cluster.local

如果上面访问 kubernetes.default 返回的值都有问题,那就需要检查 kube-dns 的运行状态和日志了。否则的话,应该去检查自己的 Service 定义是不是有问题。

5.2、检查Service的Endpoints

而如果 Service 没办法通过 ClusterIP 访问到的时候,首先应该检查的是这个 Service 是否有 Endpoints:

1
2
3
$ kubectl get endpoints hostnames
NAME ENDPOINTS
hostnames 10.244.0.5:9376,10.244.0.6:9376,10.244.0.7:9376

需要注意的是,如果Pod 的 readniessProbe 没通过,它也不会出现在 Endpoints 列表里。

5.3、确认kube-proxy正常运行

而如果 Endpoints 正常,那么就需要确认 kube-proxy 是否在正确运行。在通过 kubeadm 部署的集群里,应该看到 kube-proxy 输出的日志如下所示:

1
2
3
4
5
6
7
8
9
10
I1027 22:14:53.995134    5063 server.go:200] Running in resource-only container "/kube-proxy"
I1027 22:14:53.998163 5063 server.go:247] Using iptables Proxier.
I1027 22:14:53.999055 5063 server.go:255] Tearing down userspace rules. Errors here are acceptable.
I1027 22:14:54.038140 5063 proxier.go:352] Setting endpoints for "kube-system/kube-dns:dns-tcp" to [10.244.1.3:53]
I1027 22:14:54.038164 5063 proxier.go:352] Setting endpoints for "kube-system/kube-dns:dns" to [10.244.1.3:53]
I1027 22:14:54.038209 5063 proxier.go:352] Setting endpoints for "default/kubernetes:https" to [10.240.0.2:443]
I1027 22:14:54.038238 5063 proxier.go:429] Not syncing iptables until Services and Endpoints have been received from master
I1027 22:14:54.040048 5063 proxier.go:294] Adding new service "default/kubernetes:https" at 10.0.0.1:443/TCP
I1027 22:14:54.040154 5063 proxier.go:294] Adding new service "kube-system/kube-dns:dns" at 10.0.0.10:53/UDP
I1027 22:14:54.040223 5063 proxier.go:294] Adding new service "kube-system/kube-dns:dns-tcp" at 10.0.0.10:53/TCP

5.4、检查宿主机iptables规则

如果 kube-proxy 一切正常,就应该仔细查看宿主机上的 iptables 了。而一个 iptables 模式的 Service 对应的规则包括:

  • KUBE-SERVICES 或者 KUBE-NODEPORTS 规则对应的 Service 的入口链,这个规则应该与 VIP 和 Service 端口一一对应;
  • KUBE-SEP-(hash) 规则对应的是 DNAT 链,这些规则应该与 Endpoints 一一对应;
  • KUBE-SVC-(hash) 规则对应的是负载均衡链,这些规则的数目应该与 Endpoints 数目一致;
  • 如果是 NodePort 模式的话,还有 POSTROUTING 处的 SNAT 链。

通过查看这些链的数量、转发目的地址、端口、过滤条件等信息,就能很容易发现一些异常的蛛丝马迹。

5.5、检查hairpin-mode

当然,还有一种典型问题,就是 Pod 没办法通过 Service 访问到自己。这往往就是因为 kubelet 的 hairpin-mode 没有被正确设置。此时,只需要确保将 kubelet 的 hairpin-mode 设置为 hairpin-veth 或者 promiscuous-bridge 即可。

在 hairpin-veth 模式下,我们应该能看到 CNI 网桥对应的各个 VETH 设备,都将 Hairpin 模式设置为了 1,如下所示:

1
2
3
$ for d in /sys/devices/virtual/net/cni0/brif/veth*/hairpin_mode; do echo "$d = $(cat $d)"; done
/sys/devices/virtual/net/cni0/brif/veth4bfbfe74/hairpin_mode = 1
/sys/devices/virtual/net/cni0/brif/vethfc2a18c5/hairpin_mode = 1

而如果是 promiscuous-bridge 模式的话,我们应该看到 CNI 网桥的混杂模式(PROMISC)被开启,如下所示:

1
2
$ ifconfig cni0 |grep PROMISC
UP BROADCAST RUNNING PROMISC MULTICAST MTU:1460 Metric:1

六、小结

在这篇文章中,我们主要讨论了如下内容:

  • Kubernetes提供了多种供集群外部访问的方式,包括NodePort模式、LoadBalancer模式、ExternalName模式以及ExternalIP模式。

  • Service NodePort模式的工作原理与clusterIP模式类似,不同之处在于,nodePort模式会在宿主机上为Service对象分配一个端口。集群外部的用户通过访问宿主机上的这个端口就能访问到Pod。

  • Service LoadBalancer模式适用于公有云提供的Kubernetes服务。当使用该类型的Service对象时,kubernetes会调用公有云的API,创建出相应的负载均衡服务。

  • Service ExternalName模式会在kube-dns中为Service的域名添加一条CNAME记录。

  • Service ExternalIP模式可以让我们为Service对象指定一个公有IP地址。

  • Service是由kube-dns、kube-proxy和iptables共同实现的。当Service无法被访问时,应该依次对他们进行检查。


容器网络7:从外部访问Service
https://kuberxy.github.io/2020/10/07/容器网络7:从外部访问Service/
作者
Mr.x
发布于
2020年10月7日
许可协议