记一次 k8s 集群单点故障引发的血案

写在前面

公司使用了 k8s 集群来管理一些比较基础的有状态集群,基于 k8s 进行了简单的二次开发,使之可以支持有状态的集群(并没有使用自带的petset,现在改名为statefulset了好像)。运行了了挺长时间,一直比较正常。但由于一些历史原因及侥幸心理,k8s 集群中的 apisever 一直都是单点,是的,只有一台机器。世上很多事情就是这样,你做了防范,事故却从不出现,显得防范毫无用处,当你没做防范的时候,事故总是如约而至,打你个措手不及。

血案经过

  • 那是一个安静的晚上,楼主在安静的撸码,突然发现一台机器 down 掉了,就是那台跑了 apiserver controller-manager scheduler 的机器(一挂全挂。不作就不会死啊真的是)
  • 但是楼主并没有太在意,在楼主的认知里,这些挂了,对正在运行的容器应该不会有影响。但为了保险起见,楼主又在别的机器上搭建了新的 k8s 控制组件,并用 ha 作了高可用。共用的都是同一套 etcd 集群。这个是不可避免的。做完后看了下业务容器,都是正常的。
  • 但是,过了五分钟,同事向我反映,他的业务的 pod 的状态全部变成 terminating 了。我赶紧看了一下,果然如此。当时直接吓尿,赶紧查看实际的业务容器,发现都是正常的。这是怎么回事?
  • 想了一下,突然想起重新搭建的 k8s apiserver 对现在集群的 kubelet 来说,是根本访问不到的,因为现在 kubelet 启动连接的 apiserver 地址还是之前的地址。另外,依稀记得 controller-manager 超过 5min 检测不到来自 node 的心跳,就会认为这个 node 已经挂掉,然后就会把该 node 上的 pod 全部删除。所以,目前的情况符合 pod eviction 条件,所有 node 上的 pod 已经被标记为删除,但由于 kubelet 现在与 apiserver 无法通信,所以容器并没有被实际干掉。
  • (事后来看,这时候正确的处理办法是停掉所有 node 上的 kubelet,保证业务容器的安全,然后再想其它解决办法,跳过 apiserver 直接修改 etcd 是最有效的办法;或者之前不启动新的 apiserver 及其它组件,当时有别的业务需要访问 apiserver,所以需要搭建一套, 但 controller-manager 是用不到的,如果没有启动 controller-manager 则 node-controller 就不会进行 pod eviction, 后面的悲剧就不会出现了)
  • 之前挂掉的 apiserver 机器我们无法直接控制,后面网络突然又恢复了,所有 kubelet 开始同步状态,杀掉正在运行的容器,血案爆发。

复盘

这次故障最大的教训就是不要留有侥幸心理,系统中不要留下单点,做好高可用。但如果处理得当,故障是可以止于机器故障的,不会引发后来的血案,究其原因,还是自己对 k8s 理解和实践的不够深入,对一些并不常用但其实很重要的特点没有深入研究。比如这次故障主要涉及到的 node controller 和 pod eviction,之前只是简单了解过,真正遇到问题,完全无法进行完善的处理。对故障涉及到的 k8s 特性进行了一些了解,下面主要记录一下这些内容。

  • 首先是 node controller 和 pod eviction,node controller 是 k8s 众多 controller 的一种。主要作用是检测集群中 node 的状态,并进行相应的处理。下面是 controller manager 在运行时与 node controller 相关的一些选项。
    首先是 1.3 版本,处理的比较粗暴简单,血案用的版本,后升级到 1.5 版本,对比之下可以看到其中的改进。

    --deleting-pods-burst=1: Number of nodes on which pods are bursty deleted in case of node failure. For more details look into RateLimiter
    --deleting-pods-qps=0.1: Number of nodes per second on which pods are deleted in case of node failure
    上面的两个选项主要是用来作限速的,比如第二个选项,当多个 node failure 时,不用立即将所有 node 上的 pod 删除,而是每 10s 中删除一个 node 上的 pod 。这样处理一方面是减轻了 apiserver 的压力,同时也防止出现 node 一挂上面的 pod 就被立即删除的情况,毕竟 node 有可能迅速恢复的。
    --node-monitor-grace-period=40s: Amount of time which we allow running Node to be unresponsive before marking it unhealty. Must be N times more than kubelet's nodeStatusUpdateFrequency, where N means number of retries allowed for kubelet to post node status.
    --node-monitor-period=5s: The period for syncing NodeStatus in NodeController.
    --node-startup-grace-period=1m0s: Amount of time which we allow starting Node to be unresponsive before marking it unhealty.
    --pod-eviction-timeout=5m0s: The grace period for deleting pods on failed nodes.
    node 在 node controller 中主要有两个时间,一个是 lastprobetime, 一个是 lasttransitiontime。lastprobetime 是 node controller 设置的,在 nc 对 node 状态进行例行检查时,如果发现保存的 node lasttransitiontime 发生了变化,就将 lastprobetime 设置为 now。如果没有发生变化,则保持之前的内容。
    lasttransitiontime 为 kubelet 上报自己状态时所带的时间。
    如果 time.now after lastprobetime + pod-eviction-timeout,则 node 标记为下线,将 node 加入 pod eviction 队列。

    以上就是 1.3 版本的 node controller,可以看到只要 node 被标记下线了,就会执行 pod eviction。下面是 1.5 版本的相关配置选项。

    --large-cluster-size-threshold int32                                Number of nodes from which NodeController treats the cluster as large for the eviction logic purposes. --secondary-node-eviction-rate is implicitly overridden to 0 for clusters this size or smaller. (default 50)
    --node-eviction-rate float32                                        Number of nodes per second on which pods are deleted in case of node failure when a zone is healthy (see --unhealthy-zone-threshold for definition of healthy/unhealthy). Zone refers to entire cluster in non-multizone clusters. (default 0.1)
      --node-monitor-grace-period duration                                Amount of time which we allow running Node to be unresponsive before marking it unhealthy. Must be N times more than kubelet's nodeStatusUpdateFrequency, where N means number of retries allowed for kubelet to post node status. (default 40s)
      --node-monitor-period duration                                      The period for syncing NodeStatus in NodeController. (default 5s)
      --node-startup-grace-period duration                                Amount of time which we allow starting Node to be unresponsive before marking it unhealthy. (default 1m0s)
      --pod-eviction-timeout duration                                     The grace period for deleting pods on failed nodes. (default 5m0s)
      --secondary-node-eviction-rate float32                              Number of nodes per second on which pods are deleted in case of node failure when a zone is unhealthy (see --unhealthy-zone-threshold for definition of healthy/unhealthy). Zone refers to entire cluster in non-multizone clusters. This value is implicitly overridden to 0 if the cluster size is smaller than --large-cluster-size-threshold. (default 0.01)
      可以看到,pod eviction 的策略细致了很多。加入了 big cluster 和 zone 的概念。默认情况下是一个 zone kubernetes。分为几种情况来考虑:
      1、集群是 healthy 的,则按 pod-eviction-rate 删除。
      2、集群是 unhealthy 的,且是小集群,则不进行删除。
      3、集群是 unhealthy 的大集群,则按 secondary-node-eviction-rate 进行删除。
  • 第二部分是关于绕过 apiserver 直接修改 etcd 状态的。理论上这个肯定是可行的,但当时尝试采用这个方法修改 pod 状态的时候,一直不奏效。以为是 kubelet 有地方没搞明白,看了半天 kubelet 代码,没有发现问题。后来进行了一下测试,发现按之前的方法修改 etcd ,watch apiserver pods 的时,获取不到修改的信息。查看了 apiserver 相关代码,发现了如下的一个选项:

    --watch-cache                                             Enable watch caching in the apiserver (default true)

    这个 watch-cache 很明显是缓存 etcd watch 的结果的。但是当修改 etcd 的时候,watch-cache 会判断之前缓存的资源的 resourceversion 和从 etcd watch 到的对应资源的 resourceversion 作比较,如果后者不比前者大,则不进行更新。在修改 etcd 的时候,一定要注意把 resourceversion 修改掉。或者在 apiserver 中把 watch-cache 这个选项关掉也可以。

    结束语

    kubernetes 的发展很快,一定要注意新版本的变化。对一些特性要多加实验。并深入到源码层次。

你可能感兴趣的:(容器和容器云)