Kubernetes 发布服务的时候发生 5xx?Pod 如何优雅退出

494 阅读5分钟

Pod优雅退出时要中断长任务的执行和拒绝所有连接。

因为组件有可能在做其他的事情,所以无法确定要多久的时间才能将IP从可用列表中移除,这个问题就是我们这篇文章所要讨论的优雅退出的问题。

为什么会出现5xx?

创建Pod的时候是会完成创建后才将IP上报给控制平面,然后外部才可以进行访问。

但是删除Pod的时候删除endpoint 和 kubelet 销毁Pod会同时进行,这种情况会出现竞争。

如果在传播endpoint已经被删除以前,Pod就已经被退出了,这种情况就会产生错误,因为 Ingress 控制器、kube-proxy、CoreDNS 等没有足够的时间从其内部状态中删除 对应Pod 的 IP 地址。

我们理想的状态是k8s等待集群所有组件的状态都被更新了再删除Pod,但是k8s并没有这样做。

k8s提供了强大的语义来更新endpoint的信息,当收到 Informer 需要将 endpoints 删除的通知时,kube-proxy就会立即执行删除的动作,但是k8s并不会验证endpoint的信息是否是最新的。这个时候如果IP访问的信息没有被删除,而程序已经退出的话,就产生了我们所看到的访问服务出现 5xx 的情况。

image.png 既然知道了产生5xx的原因,那我们就可以在程序退出的时候等待一段时间再结束。

可以看下我们下面的代码例子:

package main

import (
  "fmt"
  "os"
  "os/signal"
  "syscall"
)

func main() {
  // 创建信号通道
  sigs := make(chan os.Signal, 1)
  done := make(chan bool, 1)
  // 注册通道
  signal.Notify(sigs, syscall.SIGTERM)

  go func() {
    // 等待信号
    sig := <-sigs
    fmt.Println("捕获到 SIGTERM 信号,正在关闭")
    // 完成所有未完成的请求,然后...
    done <- true
  }()

  fmt.Println("启动应用程序")
  // 主逻辑写在这里
  <-done
  fmt.Println("退出")
}

至于这个时间应该是多长呢?

默认情况下k8s 发送了 SIGTERM 后30s就会强制退出程序。可以在15s内继续进行操作就跟什么事情都没发生一样,这个时间间隔足够让kube-proxy 、ingress-controller 和 CoreDNS等组件去传播 endpoint 删除信息。

通过 preStop hook 进行优雅退出

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
    - name: web
      image: nginx
      ports:
        - name: web
          containerPort: 80
      **lifecycle:
        preStop:
          exec:
            command: ["sleep", "15"]**

这个和在程序中接收到信号等待15s是不一样的,在 preStop hook执行的时候,我们的应用程序并不知道它要退出了,这个时候程序也不会收到退出的信号。

而且preStop的退出时长是计算在k8s强制退出的时长里面的,比如 preStop hook 执行时间超过 30s,那么k8s会直接发送 SIGKILL 信号而不会发送 SIGTERM 信号。

image.png

优雅退出需要较长的时间

如果要依赖 p8s抓取监控数据的话,较长的退出时间并不好。

因为 p8s 抓取的数据是需要依赖 endpoints 暴露的 /metrics 接口,但是Pod删除的过程中 endpoints 已经被移除了,这个时候我们也无法拉取到应用程序的任何指标。

这样对于我们来说这段时间程序的执行监控数据处于黑盒的状态,这也是我们不希望看到的。

滚动升级的局限

我们有些应用程序可能会保存着客户端的长连接,这个时候我们不希望由服务端主动去断开,因为这样会导致客户端有明显的服务不可用。

如果我们的应用程序里面有长时间在处理的任务,比如正在运行批统计数据的话,我们也没办法确定什么时候可以将Pod进行删除。

所以使用滚动升级,并不能很好的解决上面的两个问题。

蓝绿部署

蓝绿部署解决了滚动升级的局限,能够准确控制流量何时进入新的Pod中。

但是蓝绿环境也有存在局限,比如在一个长任务执行的程序中,现在蓝环境中部署了正式版本,由于要升级,则在绿环境部署新的版本,完成后进行切换。

但是如果还要部署一个新的在蓝环境中,这个时候由于任务执行的时间比较上,上次蓝环境的Pod尚未结束,则这个时候则会对程序的执行有影响。

Rainbow Deployment 用于解决蓝绿环境存在问题,其实相当于拓展了环境的个数,比如原先只有蓝绿环境,现在还多了个黄的环境,这样的话如果使用了类似websocket的内容,也不会出现丢数据或者session关闭的情况,因为旧的版本可以等到所有连接都断开后再去回收环境。

apiVersion: apps/v1
kind: Deployment
metadata:
  **name: nginx-deployment-[color]**
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
        **color: [color]**
    spec:
      containers:
      - name: nginx
        image: your_application:0.2
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginxservice
spec:
  selector:
    app: nginx
    **environment: [color]**
  ports:
    - protocol: TCP
      port: 80
      name: nginxs
      targetPort: 80

这个yaml文件的 [color] 可以通过 CI/CD 进行字符串替换,生成新的颜色,部署一个新的Deployment。

而且这里也不一定要使用颜色,也可以通过使用 git commit 的hash值进行部署,这样可以有无限多个环境。

更多相关内容:docs.release.com/reference-d…

写在最后

感谢你读到这里,如果想要看更多 云原生及Kubernetes 的文章可以订阅我的专栏: juejin.cn/column/7321… 。