极客时间已完结课程限时免费阅读

38 | 从外界连通Service与Service调试“三板斧”

38 | 从外界连通Service与Service调试“三板斧”-极客时间

38 | 从外界连通Service与Service调试“三板斧”

讲述:张磊

时长11:04大小5.07M

你好,我是张磊。今天我和你分享的主题是:从外界连通 Service 与 Service 调试“三板斧”。
在上一篇文章中,我为你介绍了 Service 机制的工作原理。通过这些讲解,你应该能够明白这样一个事实:Service 的访问信息在 Kubernetes 集群之外,其实是无效的。
这其实也容易理解:所谓 Service 的访问入口,其实就是每台宿主机上由 kube-proxy 生成的 iptables 规则,以及 kube-dns 生成的 DNS 记录。而一旦离开了这个集群,这些信息对用户来说,也就自然没有作用了。
所以,在使用 Kubernetes 的 Service 时,一个必须要面对和解决的问题就是:如何从外部(Kubernetes 集群之外),访问到 Kubernetes 里创建的 Service?
这里最常用的一种方式就是:NodePort。我来为你举个例子。
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 参数来修改它。
那么这时候,要访问这个 Service,你只需要访问:
<任何一台宿主机的IP地址>:8080
就可以访问到某一个被代理的 Pod 的 80 端口了。
而在理解了我在上一篇文章中讲解的 Service 的工作原理之后,NodePort 模式也就非常容易理解了。显然,kube-proxy 要做的,就是在每台宿主机上生成这样一条 iptables 规则:
-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 模式完全一样了。
需要注意的是,在 NodePort 方式下,Kubernetes 会在 IP 包离开宿主机发往目的 Pod 时,对这个 IP 包做一次 SNAT 操作,如下所示:
-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操作呢?
这里的原理其实很简单,如下所示:
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 需要明确知道所有请求来源的场景来说,这是不可以的。
所以这时候,你就可以将 Service 的 spec.externalTrafficPolicy 字段设置为 local,这就保证了所有 Pod 通过 Service 收到请求之后,一定可以看到真正的、外部 client 的源地址。
而这个机制的实现原理也非常简单:这时候,一台宿主机上的 iptables 规则,会设置为只将 IP 包转发给运行在这台宿主机上的 Pod。所以这时候,Pod 就可以直接使用源地址将回复包发出,不需要事先进行 SNAT 了。这个流程,如下所示:
client
^ / \
/ / \
/ v X
node 1 node 2
^ |
| |
| v
endpoint
当然,这也就意味着如果在一台宿主机上,没有任何一个被代理的 Pod 存在,比如上图中的 node 2,那么你使用 node 2 的 IP 地址访问这个 Service,就是无效的。此时,你的请求会直接被 DROP 掉。
从外部访问 Service 的第二种方式,适用于公有云上的 Kubernetes 服务。这时候,你可以指定一个 LoadBalancer 类型的 Service,如下所示:
---
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 地址配置给负载均衡服务做后端。
而第三种方式,是 Kubernetes 在 1.7 之后支持的一个新特性,叫作 ExternalName。举个例子:
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 这个域名是一个效果了。
此外,Kubernetes 的 Service 还允许你为 Service 分配公有 IP 地址,比如下面这个例子:
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 的节点。你可以想一想这是为什么。
实际上,在理解了 Kubernetes Service 机制的工作原理之后,很多与 Service 相关的问题,其实都可以通过分析 Service 在宿主机上对应的 iptables 规则(或者 IPVS 配置)得到解决。
比如,当你的 Service 没办法通过 DNS 访问到的时候。你就需要区分到底是 Service 本身的配置问题,还是集群的 DNS 出了问题。一个行之有效的方法,就是检查 Kubernetes 自己的 Master 节点的 Service DNS 是否正常:
# 在一个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 定义是不是有问题。
而如果你的 Service 没办法通过 ClusterIP 访问到的时候,你首先应该检查的是这个 Service 是否有 Endpoints:
$ 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 列表里。
而如果 Endpoints 正常,那么你就需要确认 kube-proxy 是否在正确运行。在我们通过 kubeadm 部署的集群里,你应该看到 kube-proxy 输出的日志如下所示:
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
如果 kube-proxy 一切正常,你就应该仔细查看宿主机上的 iptables 了。而一个 iptables 模式的 Service 对应的规则,我在上一篇以及这一篇文章里已经全部介绍到了,它们包括:
KUBE-SERVICES 或者 KUBE-NODEPORTS 规则对应的 Service 的入口链,这个规则应该与 VIP 和 Service 端口一一对应;
KUBE-SEP-(hash) 规则对应的 DNAT 链,这些规则应该与 Endpoints 一一对应;
KUBE-SVC-(hash) 规则对应的负载均衡链,这些规则的数目应该与 Endpoints 数目一致;
如果是 NodePort 模式的话,还有 POSTROUTING 处的 SNAT 链。
通过查看这些链的数量、转发目的地址、端口、过滤条件等信息,你就能很容易发现一些异常的蛛丝马迹。
当然,还有一种典型问题,就是 Pod 没办法通过 Service 访问到自己。这往往就是因为 kubelet 的 hairpin-mode 没有被正确设置。关于 Hairpin 的原理我在前面已经介绍过,这里就不再赘述了。你只需要确保将 kubelet 的 hairpin-mode 设置为 hairpin-veth 或者 promiscuous-bridge 即可。
这里,你可以再回顾下第 34 篇文章《Kubernetes 网络模型与 CNI 网络插件》中的相关内容。
其中,在 hairpin-veth 模式下,你应该能看到 CNI 网桥对应的各个 VETH 设备,都将 Hairpin 模式设置为了 1,如下所示:
$ 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)被开启,如下所示:
$ ifconfig cni0 |grep PROMISC
UP BROADCAST RUNNING PROMISC MULTICAST MTU:1460 Metric:1

总结

在本篇文章中,我为你详细讲解了从外部访问 Service 的三种方式(NodePort、LoadBalancer 和 External Name)和具体的工作原理。然后,我还为你讲述了当 Service 出现故障的时候,如何根据它的工作原理,按照一定的思路去定位问题的可行之道。
通过上述讲解不难看出,所谓 Service,其实就是 Kubernetes 为 Pod 分配的、固定的、基于 iptables(或者 IPVS)的访问入口。而这些访问入口代理的 Pod 信息,则来自于 Etcd,由 kube-proxy 通过控制循环来维护。
并且,你可以看到,Kubernetes 里面的 Service 和 DNS 机制,也都不具备强多租户能力。比如,在多租户情况下,每个租户应该拥有一套独立的 Service 规则(Service 只应该看到和代理同一个租户下的 Pod)。再比如 DNS,在多租户情况下,每个租户应该拥有自己的 kube-dns(kube-dns 只应该为同一个租户下的 Service 和 Pod 创建 DNS Entry)。
当然,在 Kubernetes 中,kube-proxy 和 kube-dns 其实也是普通的插件而已。你完全可以根据自己的需求,实现符合自己预期的 Service。

思考题

为什么 Kubernetes 要求 externalIPs 必须是至少能够路由到一个 Kubernetes 的节点?
感谢你的收听,欢迎你给我留言,也欢迎分享给更多的朋友一起阅读。
分享给需要的人,Ta购买本课程,你将得20
生成海报并分享

赞 24

提建议

上一篇
37 | 找到容器不容易:Service、DNS与服务发现
下一篇
39 | 谈谈Service与Ingress
unpreview
 写留言

精选留言(43)

  • 我来也
    2020-04-11
    课后思考题: 为什么 Kubernetes 要求 externalIPs 必须是至少能够路由到一个 Kubernetes 的节点? 因为k8s只是在集群中的每个节点上创建了一个 externalIPs 与kube-ipvs0网卡的绑定关系. 若流量都无法路由到任意的一个k8s节点,那自然无法将流量转给具体的service. -------------------------------- 最近在这个externalIPs上面踩了一个坑. 在阿里云上提交了工单.第二天,应该是换了第3个售后工程师,才解决的. 给你们分享一下,哈哈! 事情是这样的: 我的k8s集群中有3个节点,假设分别是: A: 192.168.1.1 B: 192.168.1.2 C: 192.168.1.3 症状是: 我在节点B/C上,ssh 192.168.1.1, 最终都是登录到了本机,并未成功登录节点A. 但是节点A登录节点B/C, 节点B和节点C互相登录都正常. 只有节点B和节点C上,登录节点A不成功. 就是因为我的某一个service服务就配置了 externalIPs, 配置的IP就是 192.168.1.1(A) -------------------------------- 那个售后工程师发现在节点B和C上有这么一条记录 $ ip addr inet 192.168.1.1/32 brd 192.168.1.1 scope global kube-ipvs0 那么在节点B和节点C上,访问192.168.1.1的流量,全都转到了kube-ipvs0. 最终这个流量都转到本机了,并未真的发送到远端真实的IP:192.168.1.1 我立马就意识到,我的service绑定了节点的IP.可能是这个导致的. 后来把externalIPs移除掉节点的IP后,该规则就不见了,节点B和节点C也可以正常访问节点A了. -------------------------------- 这个 externalIPs 其实可以填多个,且不要求本机就有对应的IP. 比如你可以填集群中根本就不存在的IP 172.17.0.1, 那么在k8s所有节点上,你都可以通过netstat -ant | grep 172.17.0.1 查看到,有监听该IP的端口.
    展开
    共 7 条评论
    91
  • swordholder
    2018-11-19
    对ExternalName的作用没太理解。 访问my-service.default.svc.cluster.local被替换为my.database.example.com,这和我从外部访问到 Kubernetes 里的服务有什么关系? 感觉这更像是从Kubernetes内访问外部资源的方法。
    共 7 条评论
    27
  • 王天明
    2018-11-24
    终于算是清楚了,在nodePort模式下,关于端口的有三个参数,port, nodePort, targetPort。 如果如下定义,(请张老师指出是否有理解偏差) spec: type: NodePort ports: - name: http port: 8080 nodePort: 30080 targetPort: 80 protocol: TCP 则svc信息如下,(注意下面无targetPort信息,只是ClustorPort与NodePort,注意后者为Nodeport) ingress-nginx ingress-nginx NodePort 10.105.192.34 <none> 8080:30080/TCP,8443:30443/TCP 28m 则可访问的组合为 1. clusterIP + port: 10.105.192.34:8080 2. 内网IP/公网ip + nodePort: 172.31.14.16:30080。(172.31.14.16我的aws局域网ip.) 30080为nodePort也可以在iptables-save中映证。 还有,就是port参数是必要指定的,nodePort即使是显式指定,也必须是在指定范围内。 (在三板斧中也问过张老师如何使用nodePort打通外网的问题,一并谢谢了)
    展开

    作者回复: 对

    共 6 条评论
    23
  • 虎虎❤️
    2018-11-21
    思考题 pod的external ip 应该是通过iptables进行配置。可能是一个虚拟ip,在网络中没有对应的设备。所以,必须有路由规则支持。否则客户端可能没办法找到该ip的路径,得到目的网络不可达的回复。
    共 1 条评论
    8
  • 2019-11-06
    nodePort下必须要指定Service的port,我用的v1.14,可能检查的更严格了。
    5
  • LEON
    2018-11-19
    在我们通过 kubeadm 部署的集群里,你应该看到 kube-proxy 输出的日志如下所示:—输出日志的命令是什么?
    共 1 条评论
    4
  • Len
    2018-11-21
    因为 kubernetes 维护着 externalIPs IP + PORT 到具体服务的 endpoints 路由规则。
    共 1 条评论
    3
  • Geek_5baa01
    2021-07-02
    我一直再想 LoadBalancer 的问题,如果他是负载到 k8s node 上,那么我应该选择那些 node 做它的负载端点呢?这里被负载的 node 需要额外承担更多的网络流程,这对这些 node 带宽容量评估是个问题,另外还需要考虑 HA 问题,肯定要选择多个 node 作为负载端点,要怎么选择呢?
    2
  • 我来也
    2020-10-11
    关于源IP的问题,简易看看这篇官方文档: 文档 教程 Services 使用 Source IP https://kubernetes.io/zh/docs/tutorials/services/source-ip/
    2
  • 勤劳的小胖子-libo
    2018-11-22
    写了一个简单的nodeport,可以访问上一节的hostname deployment. apiVersion: v1 kind: Service metadata: name: hostnames labels: app: hostnames spec: type: NodePort selector: app: hostnames ports: - nodePort: 30225 port: 80 targetPort: 9376 protocol: TCP name: http 通过curl <任意Node>:30225可以访问。 vagrant@kubeadm1:~/37ServiceDns$ curl 192.168.0.24:30225 hostnames-84985c9fdd-sgwpp 另外感觉我这边的输出关于kube-proxy: vagrant@kubeadm1:~/37ServiceDns$ kubectl logs -n kube-system kube-proxy-pscvh W1122 10:13:18.794590 1 server_others.go:295] Flag proxy-mode="" unknown, assuming iptables proxy I1122 10:13:18.817014 1 server_others.go:148] Using iptables Proxier. I1122 10:13:18.817394 1 server_others.go:178] Tearing down inactive rules. E1122 10:13:18.874465 1 proxier.go:532] Error removing iptables rules in ipvs proxier: error deleting chain "KUBE-MARK-MASQ": exit status 1: iptables: Too many links. I1122 10:13:18.894045 1 server.go:447] Version: v1.12.2 后面信息都是正常的。不知道上面这些信息可以ignore吗?
    展开
    2
  • LEON
    2018-11-19
    而如果你的 Service 没办法通过 ClusterIP 访问到的时候,你首先应该检查的是这个 Service 是否有 Endpoints:———请问老师service ip与cluster ip有什么区别?为什么这块不直接是service ip?
    共 1 条评论
    2
  • Andy
    2020-05-26
    应该可以通过这章学的内容,访问 Dashboard 可视化插件了吧!试试
    1
  • 随欲而安
    2019-06-17
    对ExternalName的作用没太理解,老师能解释一下么
    共 1 条评论
    1
  • Flying
    2019-04-13
    老师,你好,使用NodePort类型的Service,会创建 -A KUBE-NODEPORTS 类的 iptables规则及 SNAT规则,如果kube-proxy使用的是 ipvs模式,也同样会创建这两个规则吗

    作者回复: 对的

    1
  • Chenl07
    2019-01-13
    老师,我创建一个externalip的service后,对应的service端口为30100,externalip是一个VIP,实际指向集群中某两个节点。创建后,集群中任意节点上都没有侦听这个端口(netstat grep),那通过我的VIP,怎么能访问到30100这个端口了?
    共 1 条评论
    1
  • 勤劳的小胖子-libo
    2018-11-22
    请问一下什么应用场景下:“还有一种典型问题,就是 Pod 没办法通过 Service 访问自己?” 为什么要访问自己?
    共 1 条评论
    1
  • Geek_5ada4a
    2022-11-29 来自广东
    “而这个机制的实现原理也非常简单:这时候,一台宿主机上的 iptables 规则,会设置为只将 IP 包转发给运行在这台宿主机(如node2)上的 Pod。所以这时候,Pod 就可以直接使用源地址将回复包发出,不需要事先进行 SNAT 了。” 这段话不太理解,即使Pod就运行在宿主机node2上,Pod ip 和宿主机node2的ip也不同吧,没有解决回消息给client时,source ip != node2 ip 的问题啊
  • yuling朱
    2022-09-14 来自广东
    "在 NodePort 方式下,Kubernetes 会在 IP 包离开宿主机发往目的 Pod 时,对这个 IP 包做一次 SNAT 操作",有个疑问,这里表述是不是有点不准确,在ClusterIP下就不会做SNAT了吗? 只要转发到其他节点就会做SNAT。
  • 国少
    2022-06-16
    有同学了解 会话保持 这部分吗?
  • 窝窝头
    2022-06-09
    如果都不能路由的kubernetes的节点的话就没什么用了,请求都无法路由进去