一次由 Kubernetes HostPort 引发的服务故障排错记实

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

 

1.问题背景

  集群环境为 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名跟用户、密码完全不相同

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

一次由 Kubernetes HostPort 引发的服务故障排错记实

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

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

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

 

2.排查过程

  既然在 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 上发现有以下的规则在其它节点上没有

-A CNI-DN-xxxx -p tcp -m tcp --dport 3306 -j DNAT --to-destination 10.224.0.222:3306
-A CNI-HOSTPORT-DNAT -m comment --comment "dnat name": \"cni0\" id: \"xxxxxxxxxxxxx\"" -j CNI-DN-xxx
-A CNI-HOSTPORT-SNAT -m comment --comment "snat name": \"cni0\" id: \"xxxxxxxxxxxxx\"" -j CNI-SN-xxx
-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 后,使用正确的用户名密码自然可以登录成功

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

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

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

 

3.问题复现

  同样是 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 文件如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-hostport2
  labels:
    k8s-app: nginx-hostport2
spec:
  replicas: 1
  selector:
    matchLabels:
      k8s-app: nginx-hostport2
  template:
    metadata:
      labels:
        k8s-app: nginx-hostport2
    spec:
      nodeName: spring-38
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
              hostPort: 31123

  Finally,问题复现:

一次由 Kubernetes HostPort 引发的服务故障排错记实

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

 

4.罪魁祸首

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

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

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

  The CNI networking plugin supports hostPort. You can use the official portmap 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:

{
  "name": "k8s-pod-network",
  "cniVersion": "0.3.0",
  "plugins": [
    {
        # ...其它的plugin
    }
    {
      "type": "portmap",
      "capabilities": {"portMappings": true}
    }
  ]
}

  参考: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/

  也就是如果使用了 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 的源码中果然是可以看到相应的代码

一次由 Kubernetes HostPort 引发的服务故障排错记实

  感兴趣的可以的 plugins 项目的 meta/portmap/portmap.go 中查看完整的源码

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

 

5.端口占用

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

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

一次由 Kubernetes HostPort 引发的服务故障排错记实

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

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

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

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

一次由 Kubernetes HostPort 引发的服务故障排错记实

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

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-nodeport2
  labels:
    k8s-app: nginx-nodeport2
spec:
  replicas: 1
  selector:
    matchLabels:
      k8s-app: nginx-nodeport2
  template:
    metadata:
      labels:
        k8s-app: nginx-nodeport2
    spec:
      nodeName: spring-38
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-nodeport2
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 80
    nodePort: 31123
  selector:
    k8s-app: nginx-nodeport2

  一次由 Kubernetes HostPort 引发的服务故障排错记实

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

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

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

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

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

一次由 Kubernetes HostPort 引发的服务故障排错记实

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

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

 

6.问题解决

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

一次由 Kubernetes HostPort 引发的服务故障排错记实

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

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

 

7.生产建议

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

 

8.参考文章

https://www.qikqiak.com/post/how-to-use-ipvs-in-kubernetes/

https://serenafeng.github.io/2020/03/26/kube-proxy-in-iptables-mode/

https://zhuanlan.zhihu.com/p/94418251

https://ronaknathani.com/blog/2020/07/kubernetes-nodeport-and-iptables-rules/

https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/

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

 

转载: https://izsk.me/2021/08/01/Kubernetes-hostport/

上一篇:四表五链概念(IPTABLES)


下一篇:iptables理解