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 的情况。
既然知道了产生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
信号。
优雅退出需要较长的时间
如果要依赖 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值进行部署,这样可以有无限多个环境。
写在最后
感谢你读到这里,如果想要看更多 云原生及Kubernetes 的文章可以订阅我的专栏: juejin.cn/column/7321… 。