K8S 生态周报| 集群中应用正常但探针失败,如何解决?

「K8S 生态周报」内容主要包含我所接触到的 K8S 生态相关的每周值得推荐的一些信息。欢迎订阅知乎专栏「k8s生态」

大家好,我是张晋涛。

这是春节后的第一篇「K8S 生态周报」,主要集中在上游进展。

上游进展

这个 PR 对有大量 workload 并且 开启了 Liveness/Readiness Probes 的集群 可以说是非常有用了。

在这个场景下, 有时可能会出现应用程序是正常运行的,但是配置的 Liveness/Readiness Probes 结果是失败的,导致 Unhealthy 状态的出现,进而影响应用的稳定性。

这是因为自从 Kubernetes v1.2 版本开始,kubelet probe 就使用短链接的方式了,主要是考虑到 Kubernetes 中 Pod 和 port 都很多,每次检查都连接不同的地址,并且用短链接的方式就不再需要处理连接关闭的事情了。

kubelet using short connections

对于每次的探针,都会建立新的连接,而且每个连接都会消耗对应的资源,直到 TIME-WAIT 状态超时后释放。

这对于小规模或者压力不太大的集群而言是可接受的,但是对于有大量 workload 和 probes 的场景就无法接受了,因为对于每次 探针的检查,实际都会有 conntrack 记录,并且是一直在进行消耗。

ipv4     2 tcp      6 51 TIME_WAIT src=127.0.0.1 dst=127.0.0.1 sport=39458 dport=6666 src=127.0.0.1 dst=127.0.0.1 sport=6666 dport=39458 [ASSURED] mark=0 secctx=system_u:object_r:unlabeled_t:s0 zone=0 u
se=2

每个机器上 conntrack 记录的容量是有限的,比如在我机器上是 262144:

(MoeLove) ➜ sudo sysctl -a | grep conntrack_max
net.netfilter.nf_conntrack_max = 262144
net.nf_conntrack_max = 262144

如果连接消耗的资源持续未释放,就会导致新连接失败,进而导致 Unhealthy 的状态。

看到这里,想必你应该已经能得出解决的办法了。

  • 调整机器配置: 如果遇到类似情况,你可以查看下系统日志,看看是否有 nf_conntrack: table full 类似这样的日志,如果有可以调大 conntrack ,但是这需要对每台机器操作,而且这需要集群管理员按照实际情况进行操作。当然还有一些其他的配置也需要对应调整,比如可用端口之类的,这种方式对于解决自己环境中遇到问题而言可行,但是对于 Kubernetes 而言,其实就会增加它的运维成本;
  • 连接复用:这会是一种有效的解决办法,但是这需要涉及到所有 probe 相关代码的重构,成本较高;
  • 减少 TIME_WAIT 超时时间:这可以通过在建立 socket 连接的时候,设置 SO_LINGER 来完成。但这里有个点需要注意,之所以在此处能通过设置 SO_LINGER 来完成,是由于这种场景下,通常都是短连接探针发送数据较少,缓冲区内没有其他额外数据,所以即使设置 SO_LINGER 丢弃也没关系。这也是这个 PR 中所采用的办法。

核心逻辑是:

func ProbeDialer() *net.Dialer {
	dialer := &net.Dialer{
		Control: func(network, address string, c syscall.RawConn) error {
			return c.Control(func(fd uintptr) {
				syscall.SetsockoptLinger(int(fd), syscall.SOL_SOCKET, syscall.SO_LINGER, &syscall.Linger{Onoff: 1, Linger: 1})
			})
		},
	}
	return dialer
}

可以看到这里提供了两个选项,它们的含义如下;

  • Onoff: 它表示是否忽略 Linger 的配置,0 表示关闭,非 0 表示打开;
  • Linger: 这就是超时时间了,配置为 0 就表示立即关闭,发送 RST 直接丢弃缓冲区数据,但是考虑这种方式不太温和,所以设置成了 1 ,给一个尽可能短的时间,并且 1s 内也可以将缓冲区内的数据继续发送。 这里也有另一个影响需要注意 如果你部署在 Kubernetes 集群中的服务,配置健康检查探针时,用的不是专门的健康检查接口(只是返回健康状态),而是复用了其他的业务接口,并且其中传输数据量大的话,将可能导致异常

后续还有另一个 PR 是补充了对 gPRC probes 的处理:Kubelet GRPC probes: improve network resources utilization by rphillips · Pull Request #115321 · kubernetes/kubernetes

在 Kubernetes 环境下排查问题是个挺有趣的事情,但正如我之前和这个 PR 的作者聊的那样,每个角色都有各自关注的点,conntrack 多数情况下在排查问题的时候才会有人注意到。

2023-01-30 06-03-21屏幕截图.png

在这个 PR 中移除了对 IPVS scheduler 的校验,原先在代码中指定了如下 10 种 IPVS 可用的 scheduler 。

const (
	RoundRobin IPVSSchedulerMethod = "rr"
	WeightedRoundRobin IPVSSchedulerMethod = "wrr"
	LeastConnection IPVSSchedulerMethod = "lc"
	WeightedLeastConnection IPVSSchedulerMethod = "wlc"
	LocalityBasedLeastConnection IPVSSchedulerMethod = "lblc"
	LocalityBasedLeastConnectionWithReplication IPVSSchedulerMethod = "lblcr"
	SourceHashing IPVSSchedulerMethod = "sh"
	DestinationHashing IPVSSchedulerMethod = "dh"
	ShortestExpectedDelay IPVSSchedulerMethod = "sed"
	NeverQueue IPVSSchedulerMethod = "nq"
)

但是 IPVS 也在发展,比如 Linux 内核种还增加了 fo, ovf, mh 等算法, 而且也有一些团队会选择自己来扩展 IPVS scheduler,此处的限制就导致无法进行灵活的扩展了。

移除掉这里校验之后,集群管理员可以配置任意可用的 IPVS scheduler 了,但如果配置错了,那么也会有错误信息的。

在这个 PR 之前,如果创建了 ExternalName 类型的 service,会自动的创建出来 endpoints 和 endpointslice,虽然会加上 headless 的 label ,但它们没有什么真正的作用。

(MoeLove) ➜ kubectl create service externalname my-ns --external-name moelove.info
service/my-ns created
(MoeLove) ➜ kubectl get svc
NAME         TYPE           CLUSTER-IP   EXTERNAL-IP    PORT(S)   AGE
kubernetes   ClusterIP      10.43.0.1    <none>         443/TCP   10d
my-ns        ExternalName   <none>       moelove.info   <none>    5s
(MoeLove) ➜ kubectl get ep 
NAME         ENDPOINTS            AGE
kubernetes   166.66.116.66:6443   10d
my-ns        <none>               9s
(MoeLove) ➜ kubectl get endpointslice
NAME          ADDRESSTYPE   PORTS     ENDPOINTS       AGE
kubernetes    IPv4          6443      166.66.116.66   10d
my-ns-8r2vj   IPv6          <unset>   <unset>         15s
my-ns-vcw59   IPv4          <unset>   <unset>         15s

而且有可能会产生一些异常情况,比如如下配置

apiVersion: v1
kind: Service
metadata:
  name: new-blog
  namespace: default
spec:
  externalName: blog.moelove.info
  type: ExternalName
  selector:
    app: myapp
---
apiVersion: v1
kind: Pod
metadata:
  name: testpod
  labels:
    app: myapp
spec:
  containers:
    - name: web
      image: nginx
      ports:
        - name: web
          containerPort: 80
          protocol: TCP

将上述配置应用到集群后,看下效果:

(MoeLove) ➜ kubectl get svc,ep,endpointslices --show-labels
NAME                 TYPE           CLUSTER-IP   EXTERNAL-IP         PORT(S)   AGE   LABELS
service/kubernetes   ClusterIP      10.43.0.1    <none>              443/TCP   10d   component=apiserver,provider=kubernetes
service/new-blog     ExternalName   <none>       blog.moelove.info   <none>    63s   <none>

NAME                   ENDPOINTS            AGE   LABELS
endpoints/kubernetes   166.66.116.66:6443   10d   endpointslice.kubernetes.io/skip-mirror=true
endpoints/new-blog     <none>               64s   service.kubernetes.io/headless=

NAME                                            ADDRESSTYPE   PORTS     ENDPOINTS       AGE   LABELS
endpointslice.discovery.k8s.io/kubernetes       IPv4          6443      166.66.116.66   10d   kubernetes.io/service-name=kubernetes
endpointslice.discovery.k8s.io/new-blog-n22t7   IPv4          <unset>   10.0.0.129      64s   endpointslice.kubernetes.io/managed-by=endpointslice-controller.k8s.io,kubernetes.io/service-name=new-blog,service.kubernetes.io/headless=
endpointslice.discovery.k8s.io/new-blog-r479n   IPv6          <unset>   <unset>         64s   endpointslice.kubernetes.io/managed-by=endpointslice-controller.k8s.io,kubernetes.io/service-name=new-blog,service.kubernetes.io/headless=

(MoeLove) ➜ kubectl get pods -owide
NAME      READY   STATUS    RESTARTS   AGE     IP           NODE                                              NOMINATED NODE   READINESS GATES
testpod   1/1     Running   0          72s     10.0.0.129   moelove-kubernetes-cluster-node-pool-6666-ud11o   <none>           <none>

会发现 endpointslice 中有一个是指向了正在运行的 Pod (这是由于 label selector 导致的) ,但如果将该 service 转换成 ClusterIP 类型时候,kube-proxy 也不会再为它创建新的规则,无法进行正常的流量代理。

所以在这个 PR 中移除了对 ExternalName 类型 service 的 endpoints 和 endpointslice 的创建。

HorizontalPodAutoscaler autoscaling/v2beta2 在 v1.23+ 废弃,v1.26+ 不可用,所以将 kubectl 使用的版本切换到了 autoscaling/v2

其他

以上就是本次的全部内容,我们下期再见!


欢迎订阅我的文章公众号【MoeLove】

TheMoeLove

加载评论