发布于2026-07-17 阅读(0)
扫一扫,手机访问
## 1. 三种探针的区别
Kubernetes 提供了三种探针,职责各不相同。三者的区别,一句话总结:
- **存活探针(Liveness)**:问的是“进程还活着吗?”—— 如果死了,K8s 就重启容器。适用于检测死锁、内存泄漏导致的僵死状态。
- **就绪探针(Readiness)**:问的是“准备好接流量了吗?”—— 如果没准备好,就从 Service 的负载均衡池里移除。适用于初始化未完成、依赖服务不可用等场景。
- **启动探针(Startup)**:给慢启动应用兜底用的。比如 JVM 应用或需要加载大量数据的服务,启动耗时很长,此时用启动探针暂时禁用其他探针,避免误判。
这三者各司其职,千万别搞混。
| 探针类型 | 职责 | 失败后果 | 适用场景 |
|---------|------|---------|---------|
| Liveness(存活) | 进程是否活着 | 重启容器 | 检测死锁、内存泄漏导致的僵死 |
| Readiness(就绪) | 是否准备好接收流量 | 从负载均衡移除 | 初始化未完成、依赖服务不可用 |
| Startup(启动) | 慢启动应用是否已启动 | 禁用其他探针 | 启动耗时长的应用(JVM/加载大量数据) |
## 2. Go 健康检查接口实现
### 基础版本(最小可用)
先看一个最简实现。存活探针和就绪探针分别用不同的路径,避免混淆。
```go
package main
import (
"encoding/json"
"log"
"net/http"
"sync/atomic"
"time"
)
var ready atomic.Bool
func main() {
go initialize()
http.HandleFunc("/healthz", livenessHandler) // 存活探针
http.HandleFunc("/readyz", readinessHandler) // 就绪探针
http.HandleFunc("/startupz", startupHandler) // 启动探针(可选)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func initialize() {
log.Println("Initializing...")
time.Sleep(5 * time.Second) // 模拟启动耗时
ready.Store(true)
log.Println("Ready")
}
// 存活探针:只检查进程是否活着,不要检查外部依赖
func livenessHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "alive"})
}
// 就绪探针:检查应用是否准备好接收流量
func readinessHandler(w http.ResponseWriter, r *http.Request) {
if !ready.Load() {
w.WriteHeader(http.StatusServiceUna vailable)
json.NewEncoder(w).Encode(map[string]string{"status": "not ready"})
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ready"})
}
// 启动探针:用于慢启动应用
func startupHandler(w http.ResponseWriter, r *http.Request) {
if !ready.Load() {
w.WriteHeader(http.StatusServiceUna vailable)
return
}
w.WriteHeader(http.StatusOK)
}
```
基础版本实现很简单,用 `atomic.Bool` 管理就绪状态。存活探针直接返回 200,就绪探针检查原子变量。这种设计在生产环境也能用,但只适合那些依赖很少、启动很快的服务。
### 生产级版本(带依赖检查)
如果服务依赖数据库、Redis、外部 API 等,就需要更精细的控制。生产级设计的关键在于:**存活探针保持简单,就绪探针真正检查依赖**。这背后的逻辑是:如果数据库挂了,存活探针还去检查,那所有 Pod 都会因为检测失败而被反复重启,这就是典型的“级联故障”。
```go
package health
import (
"context"
"database/sql"
"encoding/json"
"errors"
"net/http"
"sync"
"time"
)
// Status 健康状态
type Status string
const (
StatusHealthy Status = "healthy"
StatusUnhealthy Status = "unhealthy"
StatusDegraded Status = "degraded"
)
// CheckResult 单个检查项结果
type CheckResult struct {
Name string `json:"name"`
Status Status `json:"status"`
Message string `json:"message,omitempty"`
Duration time.Duration `json:"duration_ms,omitempty"`
}
// HealthResponse 整体健康响应
type HealthResponse struct {
Status Status `json:"status"`
Timestamp time.Time `json:"timestamp"`
Checks []CheckResult `json:"checks,omitempty"`
}
// Checker 依赖检查器接口
type Checker interface {
Name() string
Check(ctx context.Context) CheckResult
}
// Service 健康检查服务
type Service struct {
checkers []Checker
timeout time.Duration
mu sync.RWMutex
ready bool
}
func NewService(timeout time.Duration) *Service {
return &Service{timeout: timeout}
}
func (s *Service) Register(checkers ...Checker) {
s.checkers = append(s.checkers, checkers...)
}
func (s *Service) SetReady(ready bool) {
s.mu.Lock()
defer s.mu.Unlock()
s.ready = ready
}
func (s *Service) IsReady() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.ready
}
// LivenessHandler 存活探针 - 只检查进程是否活着
func (s *Service) LivenessHandler(w http.ResponseWriter, r *http.Request) {
// 存活探针必须保持简单,不要检查外部依赖
// 否则数据库故障会导致所有 Pod 被重启,引发级联故障
response := HealthResponse{
Status: StatusHealthy,
Timestamp: time.Now(),
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}
// ReadinessHandler 就绪探针 - 检查依赖是否就绪
func (s *Service) ReadinessHandler(w http.ResponseWriter, r *http.Request) {
if !s.IsReady() {
w.WriteHeader(http.StatusServiceUna vailable)
json.NewEncoder(w).Encode(HealthResponse{
Status: StatusUnhealthy,
Timestamp: time.Now(),
Message: "service not initialized",
})
return
}
ctx, cancel := context.WithTimeout(r.Context(), s.timeout)
defer cancel()
results := make([]CheckResult, 0, len(s.checkers))
overallStatus := StatusHealthy
for _, checker := range s.checkers {
start := time.Now()
result := checker.Check(ctx)
result.Duration = time.Since(start)
results = append(results, result)
if result.Status == StatusUnhealthy {
overallStatus = StatusUnhealthy
} else if result.Status == StatusDegraded && overallStatus == StatusHealthy {
overallStatus = StatusDegraded
}
}
response := HealthResponse{
Status: overallStatus,
Timestamp: time.Now(),
Checks: results,
}
if overallStatus == StatusUnhealthy {
w.WriteHeader(http.StatusServiceUna vailable)
} else {
w.WriteHeader(http.StatusOK)
}
json.NewEncoder(w).Encode(response)
}
// ========== 具体检查器实现 ==========
// DatabaseChecker 数据库连接检查
type DatabaseChecker struct {
DB *sql.DB
}
func (d *DatabaseChecker) Name() string { return "database" }
func (d *DatabaseChecker) Check(ctx context.Context) CheckResult {
// 使用 PingContext 而非复杂查询,避免影响性能
err := d.DB.PingContext(ctx)
if err != nil {
return CheckResult{
Name: d.Name(),
Status: StatusUnhealthy,
Message: err.Error(),
}
}
return CheckResult{
Name: d.Name(),
Status: StatusHealthy,
}
}
// RedisChecker Redis 连接检查
type RedisChecker struct {
// redis.Client
}
func (r *RedisChecker) Name() string { return "redis" }
func (r *RedisChecker) Check(ctx context.Context) CheckResult {
// 实现类似 DatabaseChecker
return CheckResult{Name: r.Name(), Status: StatusHealthy}
}
// ExternalAPIChecker 外部 API 检查
type ExternalAPIChecker struct {
URL string
Client *http.Client
}
func (e *ExternalAPIChecker) Name() string { return "external-api" }
func (e *ExternalAPIChecker) Check(ctx context.Context) CheckResult {
req, _ := http.NewRequestWithContext(ctx, "GET", e.URL, nil)
resp, err := e.Client.Do(req)
if err != nil {
return CheckResult{
Name: e.Name(),
Status: StatusDegraded, // 外部服务不可用,标记为降级而非不可用
Message: err.Error(),
}
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return CheckResult{Name: e.Name(), Status: StatusHealthy}
}
return CheckResult{
Name: e.Name(),
Status: StatusDegraded,
Message: "status: " + resp.Status,
}
}
```
这个版本提供了可扩展的检查器接口,每个依赖都可以独立实现检查逻辑。特别值得注意的是,**外部 API 不可用被标记为 `Degraded` 而非 `Unhealthy`**,这是一个重要的设计理念:外部服务故障不应该导致整个 Pod 被从负载均衡中移除,而是应降级运行。
## 3. 优雅关闭集成
健康检查与优雅关闭结合,是生产环境必须考虑的场景。当 Pod 收到 SIGTERM 信号准备退出时,应该先标记为“未就绪”,让 K8s 将其从 Service 流量池中移除,等待一段时间确保负载均衡器更新,然后再真正关闭服务。
```go
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
healthSvc := health.NewService(3 * time.Second)
healthSvc.Register(
&health.DatabaseChecker{DB: db},
&health.RedisChecker{},
)
mux := http.NewServeMux()
mux.HandleFunc("/healthz", healthSvc.LivenessHandler)
mux.HandleFunc("/readyz", healthSvc.ReadinessHandler)
server := &http.Server{Addr: ":8080", Handler: mux}
// 启动服务
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
// 等待信号
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
<-sigChan
// 优雅关闭:先标记为未就绪,停止接收新流量
log.Println("Shutting down gracefully...")
healthSvc.SetReady(false)
// 等待负载均衡器更新(匹配 K8s readiness probe 周期)
time.Sleep(10 * time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("Server forced to shutdown: %v", err)
}
log.Println("Server exited")
}
```
关键点在于:**先 SetReady(false),再 sleep 等待**。这个等待时间需要匹配 K8s 的就绪探针周期(`periodSeconds`),确保所有节点的负载均衡器都已经感知到 Pod 不再就绪。
## 4. Kubernetes 配置
在 Kubernetes 中配置探针时,参数设置同样关键。以下是推荐配置:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-go-app
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: my-go-app:latest
ports:
- containerPort: 8080
# 存活探针:只检查进程是否活着
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10 # 启动后等待时间
periodSeconds: 10 # 检查频率
timeoutSeconds: 3 # 超时时间(必须 < 代码中的 timeout)
failureThreshold: 3 # 连续失败次数才重启
# 就绪探针:检查依赖是否就绪
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5 # 检查频率更高,快速切换流量
timeoutSeconds: 3
failureThreshold: 3
# 启动探针:保护慢启动应用(可选)
startupProbe:
httpGet:
path: /readyz
port: 8080
failureThreshold: 30 # 最多允许 30 次失败
periodSeconds: 10 # 总启动时间 = 30 * 10 = 300s
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"] # 给优雅关闭留时间
```
这里有个细节值得注意:**就绪探针的 `periodSeconds` 设为 5 秒,比存活探针的 10 秒更频繁**。因为就绪探针切换流量的需求更紧迫,需要更快响应状态变化。而存活探针的检查频率低一些,避免不必要的开销。
## 5. 关键最佳实践
### ✅ 应该做的
- **存活探针保持简单**:只检查进程是否响应,不要检查数据库/Redis,避免级联重启
- **就绪探针检查依赖**:数据库、缓存、配置是否加载完成
- **超时设置合理**:代码中的 timeout 要小于 K8s 的 `timeoutSeconds`,通常 2-3 秒
- **使用原子操作**:`atomic.Bool` 或 `sync.RWMutex` 管理就绪状态
- **记录状态变更**:健康状态变化时打印日志,便于排查
### ❌ 不应该做的
- **不要在存活探针里做 DB Ping**:数据库故障会导致所有 Pod 无限重启
- **不要做全表扫描或复杂查询**:健康检查频率高,避免影响性能
- **不要返回过多信息**:探针失败时 K8s 只关心状态码,详细日志用于调试即可
- **不要把外部服务失败标记为 Unhealthy**:建议用 `Degraded`,避免流量全部切走
## 6. gRPC 健康检查
如果服务使用 gRPC 协议,需要实现标准健康检查协议。Kubernetes v1.25+ 已经原生支持 gRPC 探针,配置起来很方便。
```go
import (
"google.golang.org/grpc/health/grpc_health_v1"
)
type HealthService struct{}
func (h *HealthService) Check(ctx context.Context, req *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) {
return &grpc_health_v1.HealthCheckResponse{
Status: grpc_health_v1.HealthCheckResponse_SERVING,
}, nil
}
func (h *HealthService) Watch(req *grpc_health_v1.HealthCheckRequest, stream grpc_health_v1.Health_WatchServer) error {
return nil
}
// 注册
grpc_health_v1.RegisterHealthServer(grpcServer, &HealthService{})
```
对应的 Kubernetes 配置:
```yaml
livenessProbe:
grpc:
port: 50051
service: "my-service"
```
简洁明了,省去了自己写 HTTP 接口的麻烦。当然,前提是集群版本不低于 v1.25。
---
总的来说,健康检查接口的设计原则可以概括为:**各司其职,层级分明**。存活探针只管活不活着,就绪探针才管能不能干活,启动探针给慢应用兜底。按照这个思路来,无论在 Go 还是其他语言里,都能写出靠谱的探针实现。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8