探討 Go 應用程序在 Kubernetes 中的生命周期
本文深入探討了在 Kubernetes 中開發 Go 應用程序的最佳實踐,重點分析了 Pod 生命周期的不同階段以及 Kubernetes 終止信號的作用,以確保應用程序的平穩關閉,避免數據丟失或用戶體驗中斷。通過對生命周期的詳細管理,可以有效地進行更新和負載調整。
應用程序生命周期的三個階段
- 應用程序啟動
- 應用程序運行
- 應用程序結束
在每個階段,都需要確保一切按計劃進行。通常我們只關注應用程序運行階段,但在 Kubernetes 中管理所有三個階段同樣重要。
1. 應用程序啟動
在 Kubernetes 中,Pod 被認為是準備就緒的,當所有容器都準備好接受請求時。這個過程通過 readiness 探針和 readinessGates 來實現。
readiness 探針的作用
readiness 探針用于指示服務是否準備好接受請求。它通過定期檢查應用程序的狀態來確保服務的可用性。以下是一個 readiness 探針的定義示例:
readinessProbe:
httpGet:
scheme: HTTP
path: /ready
port: service
timeoutSeconds: 2
periodSeconds: 60
關鍵參數:
? timeoutSeconds: 探針響應的時間限制。
? periodSeconds: 探針調用的頻率。
2. 應用程序運行
在應用程序運行階段,Liveness 和 Readiness 探針用于監控服務的健康狀態。Liveness 探針特別用于檢測應用程序是否進入死鎖狀態,并決定是否需要重啟容器。
liveness 探針的定義示例
livenessProbe:
httpGet:
scheme: HTTP
path: /health
port: service
timeoutSeconds: 1
periodSeconds: 30
initialDelaySeconds: 60
重要注意事項:
? 確保探針快速響應,避免因服務繁忙而導致不必要的重啟。
? 避免將探針與外部服務健康檢查結合使用,以免單一故障導致所有容器重啟。
3. 應用程序結束
應用程序結束階段涉及 Pod 的優雅關閉。Kubernetes 通過發送 SIGTERM 和 SIGKILL 信號來管理 Pod 的終止過程。
Go 應用程序的優雅關閉示例
func main() {
// Step 1: start
startService := time.Now()
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
zerolog.DurationFieldUnit = time.Second
logger := zerolog.New(os.Stdout).With().Str("service", ServiceName).Timestamp().Logger()
config, err := NewConfig()
if err != nil {
logger.Fatal().Err(err).Msg("Unable to setup config")
}
mux := http.NewServeMux()
mux.HandleFunc("/health", healthHandler(logger))
mux.HandleFunc("/ready", readyHandler(logger))
mux.HandleFunc("/task", taskHandler(logger))
server := &http.Server{
Addr: ":" + config.ServicePort,
Handler: recovery(
http.TimeoutHandler(mux, config.HandlerTimeout, fmt.Sprintf("server timed out, request exceeded %s\n", config.HandlerTimeout)),
logger,
),
ErrorLog: log.New(logger, "", log.LstdFlags),
ReadTimeout: config.ReadTimeout,
WriteTimeout: config.WriteTimeout,
}
waitGroup := &sync.WaitGroup{}
// Run HTTP server
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
errListen := server.ListenAndServe()
if err != nil && !errors.Is(errListen, http.ErrServerClosed) {
logger.Fatal().Err(errListen).Msg("server.ListenAndServe error")
}
}()
go func() {
<-ctx.Done()
logger.Info().Msg("HTTP server cancelled")
timeoutCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
errShutdown := server.Shutdown(timeoutCtx)
if errShutdown != nil {
logger.Error().Err(errShutdown).Msg("server.Shutdown error")
}
}()
waitGroup.Add(1)
go performTask(ctx, waitGroup, logger)
logger.Info().Dur("duration", time.Since(startService)).Msg("Service started successfully")
runningService := time.Now()
// Step 2: running
<-ctx.Done()
stop()
// Step 3: shutdown
logger.Info().Dur("duration", time.Since(runningService)).Msg("Gracefully shutting down service...")
startGracefullyShuttingDown := time.Now()
waitGroup.Wait()
logger.Info().Dur("duration", time.Since(startGracefullyShuttingDown)).Msg("Shutdown service complete")
}
完整示例可以在 GitHub 上找到。
結論
本文強調了在設計階段考慮失敗設計的重要性。計算機應用程序必須能夠承受任何底層軟件或硬件的故障。通過優雅降級和最終一致性等概念,可以提高應用程序的可靠性和可用性。通過合理使用探針和優雅關閉機制,開發者可以確保 Go 應用程序在 Kubernetes 中的穩定運行和高效管理。