如果我们没有在 k8s 上运行的应用程序考虑正常关闭,它可能会在滚动更新期间立即返回 502 错误(Bad Gateway)。
首先,我将简要说明滚动更新开始后旧 pod 将如何终止。然后,我将展示帮助一个 Go 应用程序实现零停机时间的简单的正常关机实现。
根据官方文档[1],以下两个步骤将异步运行;
步骤 1。如果清单文件中定义了 preStop 挂钩,则运行它。之后,发送 SIGTERM 终止 pod 的每个容器中的进程。
第 2 步。从服务中摘除关闭的 pod。该服务不再将请求路由到这些 pod。
如果我们不通过 preStop 钩子让应用程序休眠几秒钟或适当地处理 SIGTERM,则 Step1 可以比 Step2 更早完成。如果在 Step2 结束之前有一些请求,该服务可能会将这些请求路由到终止的 pod 并返回 502 错误。因此,滚动更新可能会导致短暂的停机时间,直到所有到来的请求都被路由到新的 pod。
让我们通过两个实验进一步了解这一点。
实际上,将 sleep 命令作为 preStop 挂钩运行是实现优雅关机的最简单方法。但是,如果我们的应用程序运行在轻量级容器(如 alpine)上,则无法设置该命令,因为 shell 在此类容器上不可用。
B 计划是在应用程序代码级别处理 SIGTERM。
这是我在 Go 中的应用程序代码。
import (
"context"
"flag"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
var t time.Duration
flag.DurationVar(&t, "shutdown.delay", 0, "duration until shutdown starts")
flag.Parse()
srv := http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world"))
}),
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
log.Println("Server is running")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
for {
select {
case <-ctx.Done():
time.Sleep(t)
srv.Shutdown(ctx)
return
}
}
}
服务器在 shutdown.delay 标志指定的秒数内开始关闭。我通过 minikube 创建了一个本地 k8s 集群,并使用vegeta[2] 向我的应用程序发送 HTTP 请求。您可以在Gist[3] 上查看 k8s 清单文件和 Dockerfile 。
在没有正常关机的情况下进行实验
让我们从第一个没有正常关机的实验开始。 在这种情况下,我们可以将 0s 设置为 shutdown.delay。
# deployment.yaml
template:
spec:
containers:
- name: graceful-shudown
args:
- --shutdown.delay=0s
# rolling update starts in 30 seconds
$ sleep 30; kubectl rollout restart deployment graceful-shutdown
# execute vegeta command on a different tab
$ echo "GET http://graceful.shutdown.test" | vegeta attack -duration=60s -rate=1000 | tee results.bin | vegeta report
Requests [total, rate, throughput] 60000, 1000.02, 996.32
Duration [total, attack, wait] 59.999783354s, 59.999059582s, 723.772µs
Latencies [mean, 50, 95, 99, max] 136.958326ms, 553.588µs, 10.9967ms, 5.001062432s, 5.089183568s
Bytes In [total, mean] 690719, 11.51
Bytes Out [total, mean] 0, 0.00
Success [ratio] 99.63%
Status Codes [code:count] 200:59779 502:221
Error Set:
502 Bad Gateway
我发送了 60 秒的请求,并在 30 秒内开始滚动更新。如我们所见,返回了一些 502 响应。
对于这个实验,我将 shutdown.delay 设置为 5s,其他设置与上一个实验相同。
# deployment.yaml
template:
spec:
containers:
- name: graceful-shudown
args:
- --shutdown.delay=5s
# rolling update starts in 30 seconds
$ sleep 30; kubectl rollout restart deployment graceful-shutdown
# execute vegeta command on a different tab
$ echo "GET http://graceful.shutdown.test" | vegeta attack -duration=60s -rate=1000 | tee results.bin | vegeta report
Requests [total, rate, throughput] 60000, 1000.02, 1000.00
Duration [total, attack, wait] 59.999790006s, 59.999058824s, 731.182µs
Latencies [mean, 50, 95, 99, max] 1.662431ms, 512.264µs, 3.372343ms, 26.208994ms, 178.154272ms
Bytes In [total, mean] 660000, 11.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:60000
Error Set:
这一次,所有响应的状态都是 200。
为了避免在滚动更新期间停机,我们必须在服务器开始关闭之前通过一些方法(例如 preStop 或信号处理)实现优雅关闭。
作者:山中裕太郎 出处:https://dev.to/yutaroyamanaka/understand-how-graceful-shutdown-can-achieve-zero-downtime-during-k8s-rolling-update-15eh
[1]
官方文档: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-lifetime
[2]
vegeta: https://github.com/tsenart/vegeta
[3]
Gist: https://gist.github.com/yutaroyamanaka/3b77f028bfec131b86f2207714d7306c