记一次 K8S HostPort 引发的服务故障排错指南

时间:2021-12-05 14:10:19

 记一次 K8S HostPort 引发的服务故障排错指南

最近排查了一个 kubernetes 中使用了 hostport 后遇到比较坑的问题,奇怪的知识又增加了。

问题背景

集群环境为 K8s v1.15.9,cni 指定了 flannel-vxlan 跟 portmap, kube-proxy 使用 mode 为 ipvs,集群 3 台 master,同时也是 node,这里以 node-1,node-2,node-3 来表示。

集群中有 2 个 mysql, 部署在两个 ns 下,mysql 本身不是问题重点,这里就不细说,这里以 mysql-A,mysql-B 来表示。

mysql-A 落在 node-1 上,mysql-B 落在 node-2 上, 两个数据库 svc 名跟用户、密码完全不相同。

出现诡异的现象这里以一张图来说明会比较清楚一些:

记一次 K8S HostPort 引发的服务故障排错指南

其中绿线的表示访问没有问题,红线表示连接 Mysql-A 提示用户名密码错误。

特别诡异的是,当在 Node-2 上通过 svc 访问 Mysql-A 时,输入 Mysql-A 的用户名跟密码提示密码错误,密码确认无疑,但当输入 Mysql-B 的用户名跟密码,居然能够连接上,看了下数据,连上的是 Mysql-B 的数据库,给人的感觉就是请求转到了 Mysql-A, 最后又转到了 Mysql-B,当时让人大跌眼镜。

碰到诡异的问题那就排查吧,排查的过程倒是不费什么事,最主要的是要通过这次踩坑机会挖掘一些奇怪的知识出来。

排查过程

既然在 Node-1 上连接 Mysql-A/Mysql-B 都没有问题,那基本可以排查是 Mysql-A 的问题。

经实验,在 Node-2 上所有的服务想要连 Mysql-A 时,都有这个问题,但是访问其它的服务又都没有问题,说明要么是 mysql-A 的 3306 这个端口有问题,通过上一步应该排查了 mysql-A 的问题,那问题只能出在 Node-2 上。

在 k8s 中像这样的请求转发出现诡异现象,当排除了一些常见的原因之外,最大的嫌疑就是 iptables 了,作者遇到过多次。

这次也不例外,虽然当前集群使用的 ipvs, 但还是照例看下 iptables 规则,查看 Node-2 上的 iptables 与 Node-1 的 iptables 比对,结果有蹊跷, 在 Node-2 上发现有以下的规则在其它节点上没有。

  1. -A CNI-DN-xxxx -p tcp -m tcp --dport 3306 -j DNAT --to-destination 10.224.0.222:3306 
  2.  
  3. -A CNI-HOSTPORT-DNAT -m comment --comment "dnat name": \"cni0\" id: \"xxxxxxxxxxxxx\"" -j CNI-DN-xxx 
  4.  
  5. -A CNI-HOSTPORT-SNAT -m comment --comment "snat name": \"cni0\" id: \"xxxxxxxxxxxxx\"" -j CNI-SN-xxx 
  6.  
  7. -A CNI-SN-xxx -s 127.0.0.1/32 -d 10.224.0.222/32 -p tcp -m tcp --dport 80 -j MASQUERADE 

其中 10.224.0.222 为 Mysql-B 的 pod ip, xxxxxxxxxxxxx 经查实为 Mysql-B 对应的 pause 容器的 id。

从上面的规则总结一下就是目的为 3306 端口的请求都会转发到 10.224.0.222 这个地址,即 Mysql-B。

看到这里,作者明白了为什么在 Node-2 上去访问 Node-1 上 Mysql-A 的 3306 会提示密码错误而输入 Mysql-B 的密码却可以正常访问。

虽然两个 mysql 的 svc 名不一样,但上面的 iptables 只要目的端口是 3306 就转发到 Mysql-B 了,当请求到达 mysql 后,使用正确的用户名密码自然可以登录成功。

原因是找到了,但是又引出来了更多的问题?

这几条规则是谁入到 iptables 中的?

怎么解决呢,是不是删掉就可以?

问题复现

同样是 Mysql,为何 Mysql-A 没有呢? 那么比对一下这两个 Mysql 的部署差异。

比对发现, 除了用户名密码,ns 不一样外,Mysql-B 部署时使用了 hostPort=3306, 其它的并无异常。

难道是因为 hostPort?

作者日常会使用 NodePort,倒却是没怎么在意 hostPort,也就停留在 hostPort 跟 NodePort 的差别在于 NodePort 是所有 Node 上都会开启端口,而 hostPort 只会在运行机器上开启端口,由于 hostPort 使用的也少,也就没太多关注,网上短暂搜了一番,描述的也不是很多,看起来大家也用的不多。

那到底是不是因为 hostPort 呢?

Talk is cheap, show me the code

通过实验来验证,这里简单使用了三个 nginx 来说明问题, 其中两个使用了 hostPort,这里特意指定了不同的端口,其它的都完全一样,发布到集群中,yaml 文件如下:

  1. apiVersion: apps/v1 
  2.  
  3. kind: Deployment 
  4.  
  5. metadata: 
  6.  
  7. name: nginx-hostport2 
  8.  
  9. labels: 
  10.  
  11. k8s-app: nginx-hostport2 
  12.  
  13. spec: 
  14.  
  15. replicas: 1 
  16.  
  17. selector: 
  18.  
  19. matchLabels: 
  20.  
  21. k8s-app: nginx-hostport2 
  22.  
  23. template: 
  24.  
  25. metadata: 
  26.  
  27. labels: 
  28.  
  29. k8s-app: nginx-hostport2 
  30.  
  31. spec: 
  32.  
  33. nodeName: spring-38 
  34.  
  35. containers: 
  36.  
  37. - name: nginx 
  38.  
  39. image: nginx:latest 
  40.  
  41. ports: 
  42.  
  43. - containerPort: 80 
  44.  
  45. hostPort: 31123 

Finally,问题复现:

记一次 K8S HostPort 引发的服务故障排错指南

可以肯定,这些规则就是因为使用了 hostPort 而写入的,但是由谁写入的这个问题还是没有解决?

罪魁祸首

作者开始以为这些 iptables 规则是由 kube-proxy 写入的, 但是查看 kubelet 的源码并未发现上述规则的关键字

再次实验及结合网上的探索,可以得到以下结论:

首先从 kubernetes 的官方发现以下描述:

The CNI networking plugin supports hostPort. You can use the official portmap[1] plugin offered by the CNI plugin team or use your own plugin with portMapping functionality.

If you want to enable hostPort support, you must specify portMappings capability in your cni-conf-dir. For example:

  1.  
  2. "name""k8s-pod-network"
  3.  
  4. "cniVersion""0.3.0"
  5.  
  6. "plugins": [ 
  7.  
  8.  
  9. # ...其它的plugin 
  10.  
  11.  
  12.  
  13. "type""portmap"
  14.  
  15. "capabilities": {"portMappings"true
  16.  
  17.  
  18.  

也就是如果使用了 hostPort, 是由 portmap 这个 cni 提供 portMapping 能力,同时,如果想使用这个能力,在配置文件中一定需要开启 portmap,这个在作者的集群中也开启了,这点对应上了

另外一个比较重要的结论是:

The CNI ‘portmap’ plugin, used to setup HostPorts for CNI, inserts rules at the front of the iptables nat chains; which take precedence over the KUBE- SERVICES chain. Because of this, the HostPort/portmap rule could match incoming traffic even if there were better fitting, more specific service definition rules like NodePorts later in the chain

参考: https://ubuntu.com/security/CVE-2019-9946

翻译过来就是使用 hostPort 后,会在 iptables 的 nat 链中插入相应的规则,而且这些规则是在 KUBE- SERVICES 规则之前插入的,也就是说会优先匹配 hostPort 的规则,我们常用的 NodePort 规则其实是在 KUBE- SERVICES 之中,也排在其后

从 portmap 的源码中果然是可以看到相应的代码:

记一次 K8S HostPort 引发的服务故障排错指南

所以,最终是调用 portmap 写入的这些规则。

端口占用

进一步实验发现,hostport 可以通过 iptables 命令查看到, 但是无法在 ipvsadm 中查看到。

使用 lsof/netstat 也查看不到这个端口,这是因为 hostport 是通过 iptables 对请求中的目的端口进行转发的,并不是在主机上通过端口监听。

记一次 K8S HostPort 引发的服务故障排错指南

既然 lsof 跟 netstat 都查不到端口信息,那这个端口相当于没有处于 listen 状态?

如果这时再部署一个 hostport 提定相同端口的应用会怎么样呢?

结论是: 使用 hostPort 的应用在调度时无法调度在已经使用过相同 hostPort 的主机上,也就是说,在调度时会考虑 hostport

如果强行让其调度在同一台机器上,那么就会出现以下错误,如果不删除的话,这样的错误会越来越多,吓的作者赶紧删了。

记一次 K8S HostPort 引发的服务故障排错指南

如果这个时候创建一个 nodePort 类型的 svc, 端口也为 31123,结果会怎么样呢?

  1. apiVersion: apps/v1 
  2.  
  3. kind: Deployment 
  4.  
  5. metadata: 
  6.  
  7. name: nginx-nodeport2 
  8.  
  9. labels: 
  10.  
  11. k8s-app: nginx-nodeport2 
  12.  
  13. spec: 
  14.  
  15. replicas: 1 
  16.  
  17. selector: 
  18.  
  19. matchLabels: 
  20.  
  21. k8s-app: nginx-nodeport2 
  22.  
  23. template: 
  24.  
  25. metadata: 
  26.  
  27. labels: 
  28.  
  29. k8s-app: nginx-nodeport2 
  30.  
  31. spec: 
  32.  
  33. nodeName: spring-38 
  34.  
  35. containers: 
  36.  
  37. - name: nginx 
  38.  
  39. image: nginx:latest 
  40.  
  41. ports: 
  42.  
  43. - containerPort: 80 
  44.  
  45. --- 
  46.  
  47. apiVersion: v1 
  48.  
  49. kind: Service 
  50.  
  51. metadata: 
  52.  
  53. name: nginx-nodeport2 
  54.  
  55. spec: 
  56.  
  57. type: NodePort 
  58.  
  59. ports: 
  60.  
  61. - port: 80 
  62.  
  63. targetPort: 80 
  64.  
  65. nodePort: 31123 
  66.  
  67. selector: 
  68.  
  69. k8s-app: nginx-nodeport2 

记一次 K8S HostPort 引发的服务故障排错指南

可以发现,NodePort 是可以成功创建的,同时监听的端口也出现了。

从这也可以说明使用 hostposrt 指定的端口并没有 listen 主机的端口,要不然这里就会提示端口重复之类。

那么问题又来了,同一台机器上同时存在有 hostPort 跟 nodePort 的端口,这个时候如果 curl 31123 时, 访问的是哪一个呢?

经多次使用 curl 请求后,均是使用了 hostport 那个 nginx pod 收到请求。

原因还是因为 KUBE-NODE-PORT 规则在 KUBE-SERVICE 的链中是处于最后位置,而 hostPort 通过 portmap 写入的规则排在其之前。

记一次 K8S HostPort 引发的服务故障排错指南

因此会先匹配到 hostport 的规则,自然请求就被转到 hostport 所在的 pod 中,这两者的顺序是没办法改变的,因此无论是 hostport 的应用发布在前还是在后都无法影响请求转发。

另外再提一下,hostport 的规则在 ipvsadm 中是查询不到的,而 nodePort 的规则则是可以使用 ipvsadm 查询得到。

问题解决

要想把这些规则删除,可以直接将 hostport 去掉,那么规则就会随着删除,比如下图中去掉了一个 nginx 的 hostport。

记一次 K8S HostPort 引发的服务故障排错指南

另外使用较多的 port-forward 也是可以进行端口转发的,它又是个什么情况呢? 它其实使用的是 socat 及 netenter 工具,网上看到一篇文章,原理写的挺好的,感兴趣的可以看一看。

参考: https://vflong.github.io/sre/k8s/2020/03/15/how-the-kubectl-port-forward-command-works.html

生产建议

一句话,生产环境除非是必要且无他法,不然一定不要使用 hostport,除了会影响调度结果之外,还会出现上述问题,可能造成的后果是非常严重的。

原文链接:https://mp.weixin.qq.com/s?__biz=MzA4Nzg5Nzc5OA==&mid=2651702450&idx=1&sn=3c4c5c10126a9ee92159fc1b3478fe13&chksm=8bcb071bbcbc8e0dc1eacb2ec2e94b9c3769330d9649d798e0fe6e6dcc100f00aeb5e2df470c&mpshare=1&