kubernetes故障排查记录

本文整理了在工作过程中经历的部分kubernetes故障排查记录。

1. k8s网络直接路由模式下网卡流量异常

起因:node 1到node 2的通信延迟比其他集群高,经过vnstat监听node 2 网卡发现eth0几乎没流量,eth1流量高达几百Mbps,几乎打满带宽。

确认问题:k8s节点node 1 eth0到node 2的流量预期经过node 2 eth0,但是却在node 2的eth1网卡监听到,流量链路异常。

分析:在和运维同学沟通确认了接线和路由配置没问题后,画出简易网络拓扑:

image

确认三层网络正常,问题发生在二层数据链路层,而二层使用MAC地址定位出口网卡,查看node 1记录的目标ip 192.168.4.112对应的MAC地址发现是192.168.3.112的MAC地址,说明node 1 arp洪泛学习了错误的MAC地址。

解决方案: ip neighbour删除arp记录让node 1重新学习。学习错误MAC地址的原因待观察。

2. internal ip在集群安装和投产时不一致导致API Server向kubelet的请求返回x509 ip非法错误。

组建k3s/k8s集群时,kubelet初始化会经历两个步骤:

  • 选择默认路由的网卡ip作为节点internal ip
  • 主节点将kubelet设置的当前节点internal ip,加入kubelet服务端证书的ip白名单, 签发并下发证书,供节点的kubelet作为服务端tls证书使用

背景: 我们的k3s/k8s集群主要用于边缘计算,使用物联网卡接入公网,流量费用较贵,因此为了在投产前能更经济更快地把基础服务的镜像拉下来,我们上游运维人员会在初始化集群时给gateway节点/主节点接入网线,加入办公室网络,其他从节点将主节点作为默认网关拉镜像。因此在初始化集群时,gateway节点会多出来个网卡,作为访问默认网关的网卡使用。

问题:集群的基础服务的容器全部running后,运维人员给集群断开办公室网络,交付使用。发现在gateway节点使用kubectl工具无法访问pod的日志,返回报错:

Error from server: Get "https://192.168.3.123:10250/containerLogs/<NAMESPACE>/<POD_NAME>/<CONTAINER_NAME>": x509 certificate is valid for 127.0.0.1, 172.29.1.100, not 192.168.3.123

分析: 日志请求的链路上遇到了TLS双向验证失败的问题,具体原因是某个服务在TLS握手阶段拒绝了请求方,因为请求方的ip未在该服务ip白名单内,而白名单是写进x509证书中的,进一步根据10250端口定位该服务是kubelet,也就是说,使用serving-kubelet.crt作为服务端证书的kubelet没有放行API Server的访问容器日志的请求。

询问运维同学,了解了初始化集群到投入使用的过程:

image

定位到问题后,找到机器上serving-kubelet.crt证书的位置,openssl x509命令查看证书内容:

$ sudo openssl x509 -in  serving-kube-apiserver.crt -text

Certificate:
...
            X509v3 Subject Alternative Name:
                DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:localhost, DNS:master, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, IP Address:172.29.10.100, IP Address:10.43.0.1
...

可以看到证书SAN配置的宿主机网卡的ip白名单有127.0.0.1,172.29.10.100,并没有请求的ip 192.168.3.123

解决方案 问题确认后,需要找到合适的修复方案,达到两个目的:

1 为已投入使用的存量集群更新ip白名单

2 为后续初始化的增量的集群固定internal ip,防止将临时ip作为internal ip。

因此技术调研的方向确定为…

存量集群:

  • 固定internal ip
  • 更新kubelet服务端证书

增量集群:

  • 固定internal ip

固定 internal ip

google后得知kubelet可以指定node-ip作为节点internal-ip, K3S可以通过/etc/rancher/k3s/config.yaml中透传kubelet配置参数,设置node-ip

更新kubelet服务端证书

未google到相关文档,需要查阅K3S源码,分析kubelet证书签署的流程和函数逻辑。

k3s 签署kubelet证书的函数调用关系:

image

router内配置了一条路由,向该路由发起请求会触发调用servingKubeletCert签署kubelet证书

authed.Path(prefix + "/serving-kubelet.crt").Handler(servingKubeletCert(serverConfig, serverConfig.Runtime.ServingKubeletKey, nodeAuth))

k3s 从节点加入集群获取ca,或者主节点启动后,k3s服务自身会访问该路由调用servingKubeletCert函数签署serving-kubelet.crt证书 servingKubeletCert实现:

func servingKubeletCert(server *config.Control, keyFile string, auth nodePassBootstrapper) http.Handler {
	return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
		if req.TLS == nil {
			resp.WriteHeader(http.StatusNotFound)
			return
		}

		nodeName, errCode, err := auth(req)
		if err != nil {
			sendError(err, resp, req, errCode)
			return
		}

		caCerts, caKey, key, err := getCACertAndKeys(server.Runtime.ServerCA, server.Runtime.ServerCAKey, server.Runtime.ServingKubeletKey)
		if err != nil {
			sendError(err, resp, req)
			return
		}

		ips := []net.IP{net.ParseIP("127.0.0.1")}  // 默认配置的IP Address

    // append 额外ip, 从请求头中的K3S-Node-IP中获取
		if nodeIP := req.Header.Get(version.Program + "-Node-IP"); nodeIP != "" {
			for _, v := range strings.Split(nodeIP, ",") {
				ip := net.ParseIP(v)
				if ip == nil {
					sendError(fmt.Errorf("invalid node IP address %s", ip), resp, req)
					return
				}
				ips = append(ips, ip)
			}
		}

    // 签署/更新 证书
		cert, err := certutil.NewSignedCert(certutil.Config{
			CommonName: nodeName,
			Usages:     []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
			AltNames: certutil.AltNames{
				DNSNames: []string{nodeName, "localhost"},
				IPs:      ips,
			},
		}, key, caCerts[0], caKey)
		if err != nil {
			sendError(err, resp, req)
			return
		}

		keyBytes, err := os.ReadFile(keyFile)
		if err != nil {
			http.Error(resp, err.Error(), http.StatusInternalServerError)
			return
		}

    //返回证书内容
		resp.Write(util.EncodeCertsPEM(cert, caCerts))
		resp.Write(keyBytes)
	})
}

得知证书额外的ip需要从请求头K3S-Node-IP的值中获取,继续了解如何设置Node-IP

在pkg/cli/cmds/agent.go 定义的Agent结构体中,最终确认了Node-IP的源头

type Agent struct {
  ...
	DisableServiceLB         bool
	ETCDAgent                bool
	LBServerPort             int
	ResolvConf               string
	DataDir                  string
	NodeIP                   cli.StringSlice // node-ip list 通过cli命令行参数传递
	NodeExternalIP           cli.StringSlice
	NodeName                 string
  ...
}

也就是说,为k3s启动命令添加–node-ip,再重启k3s服务即可刷新证书。经过测试,符合预期。

3. K3S使用自签k3s ca证书

2023-04, k3s certificate相关文档未发布,自签证书预置方案是通过issue得知的:https://github.com/k3s-io/k3s/issues/1868

一共三个ca client-ca、server-ca、request-header.ca

flow:

  • Stop K3s service
  • Remove all certs from /var/lib/rancher/k3s/server/tls
  • Recreate all three CAs.
  • Start k3s service.

4. 集群gateway节点无法转发ip报文

我们边缘计算业务会部署一套一主多从的k3s集群,只有主节点能与外网通信,称为gateway节点,其他从节点将主节点作为网关,请求公网的流量由公网转发

主节点配置源地址伪装:

iptables -A -t nat -s xxxx -j MASQUERADE

从节点配置默认网关:

ip route add default via <主节点ip>

在主节点系统从ubuntu16.04升级到20.04后,发现主节点无法转发从节点报文了,了解了linux ip转发的相关配置发现,linux系统在网卡间转发ip报文需要确认两项:

  • 系统参数 net.ipv4.ip_forward=1
  • iptables -A FORWARD -j ACCEPT

经过排查,发现iptables FORWARD链的流量默认DROP,是ubuntu20.04的默认配置,需要手动添加-j ACCEPT命令才能开启,可见ubuntu 20.04对网络流量的管控更严格。

5. flannel ip泄露

环境

  • ubuntu16.04
  • docker 19.02
  • k3s 1.25.11
  • flannel

起因 有pod未正常运行,kubectl get node 发现该pod未分配ip, 用kubectl describe pod 发现kubelet返回如下报错

Error adding network, no ip address available in network "cbr0"

分析fannel使用cni插件host-local作为IPAM工具管理ip,google出古早issue讨论并在kubelet添加pod ip垃圾回收机制,kubelet重启会触发,但是现在问题仍存在,原因待分析

临时处理方案

rm -rf /var/lib/cni/flannel/  #清理ip
systemctl restart docker
systemctl restart k3s