当前位置: 首页 > 科技观察

记得一个由K8SHostPort引起的服务排查攻略

时间:2023-03-22 11:04:30 科技观察

最近查了一个kubernetes使用hostport后的问题,奇怪的知识又增加了。问题背景集群环境为K8sv1.15.9,cni指定flannel-vxlan和portmap,kube-proxy使用ipvs模式,集群有3个master,也都是节点。在这里,node-1、node-2和node-3使用express。集群中有2个mysql,分别部署在两个ns下。mysql本身不是问题的重点,这里就不赘述了。这里用mysql-A和mysql-B表示。mysql-A在node-1上,mysql-B在node-2上。两个数据库的svc名称和user和password完全不同。用一张图来说明这个奇怪的现象会更清楚:绿线表示访问没有问题,红线表示连接Mysql-A时用户名密码错误。特别奇怪的是,在Node-2上通过svc访问Mysql-A时,输入Mysql-A的用户名和密码,密码不正确,确认了密码,但是当输入Mysql的用户名和密码时-B都进去了,就可以Connected了,看了数据,连上了Mysql-B的数据库。给人的感觉是请求转到了Mysql-A,最后又转到了Mysql-B,这在当时是很意外的。如果你遇到一个奇怪的问题,让我们调查一下。调查过程很简单。最重要的是通过这次踩坑的机会,挖掘出一些奇怪的知识。排查过程由于在Node-1上连接Mysql-A/Mysql-B没有问题,所以基本可以排查Mysql-A的问题了。经过实验,Node-2上的所有服务在想连接Mysql-A时都有这个问题,但是访问其他服务没有问题,说明mysql-A的3306端口有问题,通过前面的step应该检查mysql-A的问题,只能在Node-2上找到问题。在k8s中,这样的请求转发有一个奇怪的现象。排除一些常见的原因,最大的嫌疑就是iptables了。笔者遇到过很多次。这次也不例外。虽然现在的集群使用的是ipvs,但是我们还是照例看iptables的规则。检查Node-2上的iptables和Node-1上的iptables之间的比较。结果很奇怪。在Node-2上,我们发现以下内容:其他节点上不存在规则。-ACNI-DN-xxxx-ptcp-mtcp--dport3306-jDNAT--to-destination10.224.0.222:3306-ACNI-HOSTPORT-DNAT-mcomment--注释“dnatname”:\"cni0\"id:\"xxxxxxxxxxxxxx\""-jCNI-DN-xxx-ACNI-HOSTPORT-SNAT-mcomment--comment"snatname":\"cni0\"id:\"xxxxxxxxxxxx\""-jCNI-SN-xxx-ACNI-SN-xxx-s127.0.0.1/32-d10.224.0.222/32-ptcp-mtcp--dport80-jMASQUERADE其中10.224.0.222为Mysql-B的podip,xxxxxxxxxxxx经验证为暂停的idMysql-B对应的容器。由以上规则总结,3306端口的请求会被转发到地址10.224.0.222,也就是Mysql-B。看到这里,笔者明白了为什么在Node-2上访问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很少用到,所以我没有太注意它。在网上搜了一会儿,描述的不多。好像大家用的不多。是因为主机端口吗?话不多说,给我看看代码是通过实验验证的。这里简单用三个nginx来说明问题,其中两个使用hostPort。这里专门指定了不同的端口,其他的完全一样,发布到集群,yaml文件如下:apiVersion:apps/v1kind:Deploymentmetadata:name:nginx-hostport2labels:k8s-app:nginx-hostport2spec:replicas:1selector:matchLabels:k8s-app:nginx-hostport2template:metadata:labels:k8s-app:nginx-hostport2spec:nodeName:spring-38containers:-name:nginximage:nginx:latestports:-containerPort:80hostPort:31123最后,问题重现:肯定是因为使用了hostPort而写了这些规则,但是谁写的问题还是没有解决?罪魁祸首的作者认为这些iptables规则是kube-proxy写的,但是查看kubelet的源码并没有找到上述规则的关键字。再次实验,结合网上探索,可以得出以下结论:首先,从kubernetes官方找到如下描述:CNI网络插件支持hostPort。您可以使用CNI插件团队提供的官方portmap[1]插件,或者使用您自己的具有portMapping功能的插件。如果您想要启用hostPort支持,您必须在您的cni-conf-dir中指定portMappings功能。例如:{"name":"k8s-pod-network","cniVersion":"0.3.0","plugins":[{#...otherplugins}{"type":"portmap","capabilities":{"portMappings":true}}]}也就是说,如果你usehostPort是portmapcni提供的,提供portMapping能力,同时如果要使用这个能力,必须在配置文件中开启portmap,这个在笔者的集群中也是开启的,这个对应另一个重要结论是的:用于为CNI设置HostPorts的CNI“portmap”插件在iptablesnat链的前端插入规则;优先于KUBE-SERVICES链。因此,HostPort/portmap规则可以匹配incomingtraffic即使在链后面有更好的拟合,更具体的服务定义规则,如NodePorts参考:https://ubuntu.com/security/CVE-2019-9946翻译是,在使用hostPort后,它将被插入natchain的iptables对应的规则,并且这些规则被插入到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,会发生什么情况呢?apiVersion:apps/v1kind:Deploymentmetadata:name:nginx-nodeport2labels:k8s-app:nginx-nodeport2spec:replicas:1selector:matchLabels:k8s-app:nginx-nodeport2template:metadata:labels:k8s-app:nginx-nodeport2spec:nodeName:spring-38containers:-name:nginximage:nginx:latestports:-containerPort:80---apiVersion:v1kind:Servicemetadata:name:nginx-nodeport2spec:type:NodePortports:-port:80targetPort:80nodePort:31123selector:k8s-app:nginx-nodeport2可以发现NodePort可以创建成功,监听端口也出现了。由此也可以说明hostpostrt指定的端口没有监听主机的端口,否则会提示端口重复。那么问题又来了。同一台机器上同时存在hostPort和nodePort端口。如果此时curl是31123,访问的是哪一个?多次curl请求后,使用hostport的nginxpod接收请求。原因是KUBE-NODE-PORT规则在KUBE-SERVICE链的最后一个位置,hostPort通过portmap写的规则排在它之前。因此会优先匹配hostport规则,将请求转发到hostport所在的pod。两者的顺序是不能改变的,所以hostport的申请先发布还是后发布都不会影响请求转发。另外在ipvsadm中无法查询到hostport规则,而在ipvsadm中可以查询到nodePort规则。问题解决如果要删除这些规则,可以直接删除hostport,规则也会随之删除。比如下图中删除了一个nginxhostport。此外,还可以使用moreport-forward进行端口转发。现在是什么状况?它实际上使用socat和netenter工具。我在网上看到一篇文章。原理写的很好。如果你有兴趣,你可以看看。参考:https://vflong.github.io/sre/k8s/2020/03/15/how-the-kubectl-port-forward-command-works.html生产建议总之,除非生产环境是必须的并且没有别的了,否则一定不能用hostport。除了影响调度结果外,还会出现上述问题,可能造成的后果非常严重。