9、健康检查与心跳模式:健康检查端点设计、心跳机制、探活策略(TCP、HTTP、gRPC)、Kubernetes探针

各位同学,咱们今天聊一个看似简单、实则坑特别多的模式——健康检查与心跳。你想想看,一个服务部署上去,你怎么知道它还活着?是不是还在干活?还是说它已经“脑死亡”了,只是进程没挂?

我见过太多线上事故了。服务进程明明在,但线程池全堵死了,请求进来就超时。这时候如果你只看进程是否存活,那监控面板上一片绿,用户那边已经骂翻天了。所以,健康检查不是“有没有在运行”,而是“能不能正常服务”。

核心观点:健康检查是分布式系统的“生命线”。没有它,你的系统就是盲人骑瞎马。

9.1 健康检查端点设计

先说最简单的做法——暴露一个 HTTP 端点,比如 /health。但这里有个细节:这个端点该检查什么?

我个人习惯把健康检查分成三个层次:

  • Liveness(存活):进程是否活着。说白了就是“有没有挂”。
  • Readiness(就绪):服务是否准备好接收流量。比如刚启动时还在加载缓存,这时候不应该放流量进来。
  • Dependency(依赖):下游依赖是否正常。比如数据库、Redis、消息队列。

我在项目中遇到过一个问题:某个服务依赖的 Redis 集群挂了,但健康检查只检查了进程本身。结果网关还在往这个服务发请求,每个请求都超时,最终导致雪崩。从那以后,我所有的健康检查都会带上依赖检查。

小技巧:依赖检查不要搞成“强依赖”。如果 Redis 挂了但服务还能降级运行,那健康检查应该返回 200,只是 body 里标记一下 Redis 异常。否则你会把整个服务搞下线。

一个典型的健康检查端点实现(Go 语言示例):

// 健康检查处理器
func HealthHandler(w http.ResponseWriter, r *http.Request) {
    status := map[string]string{
        "status": "ok",
        "version": "1.2.3",
    }

    // 检查数据库
    if err := db.Ping(); err != nil {
        status["database"] = "unhealthy"
        status["status"] = "degraded"
    }

    // 检查 Redis
    if err := redis.Ping(); err != nil {
        status["redis"] = "unhealthy"
        // 不改变整体状态,因为可以降级
    }

    w.Header().Set("Content-Type", "application/json")
    if status["status"] != "ok" {
        w.WriteHeader(http.StatusServiceUnavailable)
    }
    json.NewEncoder(w).Encode(status)
}

9.2 心跳机制

健康检查是“别人来问我”,心跳是“我主动告诉别人我还活着”。这两种方式各有适用场景。

心跳机制通常用在集群管理、选主场景。比如 etcd、ZooKeeper 里的 session 保活,或者你自定义的分布式系统中,每个节点定期向协调者发送心跳。

这里有个关键参数:心跳间隔超时时间。我见过有人把心跳间隔设成 1 秒,超时设成 2 秒。结果网络稍微抖动一下,整个集群就开始震荡——节点被频繁踢出再加入。嗯,这其实是个经典的反模式。

避坑指南:我曾经把心跳间隔设得太短,导致协调者 CPU 被打满。建议心跳间隔至少是超时时间的 1/3 到 1/2。比如超时 9 秒,心跳间隔 3 秒。这样能容忍 2-3 次心跳丢失。

心跳消息的设计也很重要。不要只发一个“我还活着”的空包。我习惯在心跳里带上:

  • 节点 ID
  • 当前时间戳
  • 负载指标(CPU、内存、连接数)
  • 处理中的请求数

这样协调者不仅能知道节点是否存活,还能做负载均衡决策。

9.3 探活策略:TCP、HTTP、gRPC

说到探活,其实就是“怎么检查”。三种主流方式,各有优劣。

方式 优点 缺点 适用场景
TCP 简单、轻量、无侵入 只能检查端口是否监听,无法判断业务状态 四层负载均衡、基础存活检查
HTTP 能检查业务逻辑,返回状态码和 body 需要实现端点,有额外开销 七层负载均衡、Kubernetes 探针
gRPC 高效、支持流式、强类型 需要实现 gRPC 健康检查协议,调试稍麻烦 微服务间通信、高性能场景

TCP 探活是最基础的。说白了就是“三次握手能不能成功”。但你要注意,很多服务有“连接 backlog”限制。如果 backlog 满了,TCP 连接会被拒绝,但服务本身可能还在正常运行。这时候 TCP 探活会误报。

HTTP 探活是最常用的。Kubernetes 的 livenessProbe 和 readinessProbe 就是基于 HTTP 的。我建议你为每个服务实现两个端点:/healthz(存活)和 /readyz(就绪)。

gRPC 探活稍微复杂一点。gRPC 官方提供了一个健康检查协议(grpc.health.v1.Health),你只需要实现 CheckWatch 两个 RPC 方法。我个人觉得 gRPC 探活的好处是:它和你的业务 RPC 走同一个连接,能真实反映服务的处理能力。

// gRPC 健康检查实现(Go 语言)
type HealthServer struct {
    mu     sync.RWMutex
    status map[string]healthpb.HealthCheckResponse_ServingStatus
}

func (s *HealthServer) Check(ctx context.Context, req *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    status := s.status[req.Service]
    if status == healthpb.HealthCheckResponse_UNKNOWN {
        return &healthpb.HealthCheckResponse{
            Status: healthpb.HealthCheckResponse_NOT_SERVING,
        }, nil
    }
    return &healthpb.HealthCheckResponse{Status: status}, nil
}

9.4 Kubernetes 探针

Kubernetes 把健康检查玩出了花。它提供了三种探针:

  • livenessProbe:检查容器是否存活。如果失败,Kubelet 会重启容器。
  • readinessProbe:检查容器是否就绪。如果失败,Service 不会把流量转发过来。
  • startupProbe:检查容器是否启动完成。用于启动慢的应用,防止被 livenessProbe 误杀。

这里有个常见的坑:很多人把 livenessProbe 和 readinessProbe 配置成一样的。你想想看,如果数据库挂了,livenessProbe 失败,Kubernetes 会重启容器。但重启能解决数据库问题吗?不能。结果就是容器反复重启,陷入 CrashLoopBackOff。

避坑指南:我曾经见过一个团队,把数据库连接检查放在 livenessProbe 里。数据库维护期间,所有 Pod 被不断重启,整个集群瘫痪了半小时。记住:livenessProbe 只检查“进程是否卡死”,不要检查外部依赖。

一个合理的 Kubernetes 探针配置示例:

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
  - name: my-app
    image: my-app:1.0.0
    ports:
    - containerPort: 8080
    startupProbe:
      httpGet:
        path: /startupz
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 5
      failureThreshold: 30  # 给足启动时间
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
      initialDelaySeconds: 10
      periodSeconds: 10
      failureThreshold: 3
    readinessProbe:
      httpGet:
        path: /readyz
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 5
      failureThreshold: 2

注意这里的 failureThreshold。我习惯把 livenessProbe 的阈值设大一点(3-5次),避免网络抖动导致误杀。readinessProbe 的阈值可以小一点(1-2次),因为就绪状态变化频繁一点没关系。

总结一下:健康检查不是“有没有在运行”,而是“能不能正常服务”。TCP 探活最基础,HTTP 探活最常用,gRPC 探活最高效。Kubernetes 的三种探针各司其职,别搞混了。

最后说一句:健康检查的日志一定要控制好。我见过有人每秒钟打印一次健康检查日志,一天下来几个 GB。建议健康检查的日志级别设为 DEBUG,或者干脆不打印。

健康检查与心跳模式知识体系 健康检查与心跳 健康检查端点设计 Liveness(存活) Readiness(就绪) Dependency(依赖) 心跳机制 主动上报存活状态 心跳间隔 vs 超时时间 携带负载指标 探活策略 TCP:端口监听检查 HTTP:业务状态检查 gRPC:高效健康检查 Kubernetes 探针 livenessProbe 容器是否存活 readinessProbe 容器是否就绪 startupProbe 启动是否完成

这张图把今天的内容串起来了。从健康检查端点设计,到心跳机制,再到三种探活策略,最后落到 Kubernetes 探针。你把这个图记在脑子里,面试的时候也能讲得清清楚楚。

一句话总结:健康检查是系统的“体检报告”,心跳是系统的“打卡记录”,探针是系统的“监控摄像头”。三者配合,才能保证你的服务高可用。

公众号:蓝海资料掘金营,微信deep3321