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

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

 [[418642]]

最近排查了一个 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 名跟用户、密码完全不相同。

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

其中绿线的表示访问没有问题,红线表示连接 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,问题复现:

可以肯定,这些规则就是因为使用了 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 的源码中果然是可以看到相应的代码:

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

端口占用

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

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

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

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

结论是: 使用 hostPort 的应用在调度时无法调度在已经使用过相同 hostPort 的主机上,也就是说,在调度时会考虑 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 

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

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

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

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

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

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

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

问题解决

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

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

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

生产建议

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

 

 

责任编辑:张燕妮 来源: 高效运维
相关推荐

2021-11-11 16:14:04

Kubernetes

2023-12-05 08:33:44

滴滴故障k8s

2023-04-30 00:02:40

K8Skubelet数据

2022-12-17 19:49:37

GCJVM故障

2020-06-12 13:26:03

线程池故障日志

2022-11-29 21:26:26

跨域配置

2024-02-20 16:55:14

K8S云计算

2024-03-18 15:44:48

K8S故障运维

2021-12-02 07:50:30

NFS故障内存

2021-01-08 13:52:15

Consul微服务服务注册中心

2020-11-02 13:44:56

CentOSK8SLinux

2021-04-23 08:35:16

k8s故障检测

2019-08-12 08:36:33

K8S网络Pod

2021-05-26 11:06:06

Kubernetes网络故障集群节点

2022-02-23 08:01:04

KubernetesK8sPod

2021-11-01 17:29:02

Windows系统Fork

2021-12-06 17:21:05

异常报错故障

2017-09-01 09:17:51

DNS缓存惨案

2023-01-04 18:32:31

线上服务代码

2024-01-07 19:43:50

K8S节点
点赞
收藏

51CTO技术栈公众号