Files
cs-note/hhs/gRPC/3. 服务端实现/10-健康检查与反射.md
T
2026-05-24 11:42:38 +08:00

12 KiB
Raw Blame History

tags, create time
tags create time
gRPC
Go
Health Check
Debug
Reflection
2026-05-11 16:00

健康检查与反射

概述

生产级 gRPC 服务需要具备两大能力:可观测性(知道服务是否存活)和 可调试性(了解服务暴露了哪些 API)。gRPC 生态提供了两个独立的内置模块来解决这些问题:

  • Health Check v1 API — 标准协议层健康探针,供 K8s / LB / 运维工具查询服务状态
  • Server Reflection — 开发阶段的内省机制,允许客户端动态枚举所有 service、method 和 message 定义

这两个功能完全独立、可分别启用。核心原则是:生产环境必须开 Health Check、关闭 Reflection。

[!question] 为什么不用 HTTP health endpoint? 你可以同时暴露 HTTP 和 gRPC health,但用 gRPC 统一的好处是:不需要维护两套协议、负载均衡器可以直接用 gRPC probe、而且 gRPC Health Check 支持 Watch 模式实现实时监控。如果你只有 gRPC 服务而没有 HTTP sidecar,那 gRPC health 就是唯一选择。

Health Check Protocol Buffer

gRPC 官方定义了标准 Health Check proto,位于 grpc-go 源码树中(Go 包路径 google.golang.org/grpc/health/grpc_health_v1),无需单独下载 .proto 文件:

[!tip] 其他语言如何引用? Health Check proto 也开源在 grpc/grpc-proto 仓库。protobuf / Java / Python 等语言的客户端可以直接克隆该仓库并使用 protoc 编译,或通过 maven / pip 等包管理器引入对应 stub。

syntax = "proto3";
package grpc.health.v1;

message HealthCheckRequest {
  string service = 1;
}

message HealthCheckResponse {
  enum ServingStatus {
    UNKNOWN = 0;
    SERVING = 1;
    NOT_SERVING = 2;
    SERVICE_UNKNOWN = 3;
    UNAVAILABLE = 4;
  }
  ServingStatus status = 1;
}

service Health {
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
  rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}

[!tip] ServingStatus 的含义

  • SERVING: 服务正常对外提供 RPC
  • NOT_SERVING: 服务拒绝新请求(正在关机、依赖断裂等)
  • SERVICE_UNKNOWN: 查询的 service name 未注册
  • UNAVAILABLE: 内部错误导致无法判断状态

两个端点的区别:

端点 类型 用途
Check Unary RPC 瞬时查询,适合 liveness/readiness probe
Watch Server Streaming 持续监听状态变化,适合 dashboard / alerting

在 Go 中实现 Health Service

import (
    "google.golang.org/grpc/health"
    "google.golang.org/grpc/health/grpc_health_v1"
)

func main() {
    s := grpc.NewServer()

    // 创建 health server 实例
    healthServer := health.NewServer()

    // 按 service name 注册健康状态
    healthServer.SetServingStatus("user.v1.UserService", grpc_health_v1.HealthCheckResponse_SERVING)
    healthServer.SetServingStatus("order.v1.OrderService", grpc_health_v1.HealthCheckResponse_SERVING)

    // root service "" 表示整个进程级别的健康状态
    healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)

    // 挂载到 gRPC server —— 这一步自动启用了 Check 和 Watch
    grpc_health_v1.RegisterHealthServer(s, healthServer)
}

[!note] 关于 RegisterHealthServer 调用此方法后,Check 和 Watch 两个 RPC 都会自动注册到你的 server 上。不需要手动编写任何 Watch handler —— health.NewServer() 内部使用 watcher map + channel 实现了完整的 Watch 逻辑。你只需要调用 SetServingStatus(key, status) 来更新状态即可。

关键细节:

  • "" (空字符串)表示整个进程级别的健康状态,是 K8s 最常用的探测目标
  • "user.v1.UserService" 这种带 package 的路径可以精确到某个具体 service
  • SetServingStatus 可随时调用,不需要重启 server

[!warning] 常见误区:不要手写 Watch handler 有些教程展示了手写的 Watch stream loop,但那是不必要的——官方库已实现完毕。除非你有极其特殊的需求(比如自定义状态变更事件源),否则直接用 health.NewServer() 提供的默认行为即可。

Health Check 使用场景

Kubernetes Probe

apiVersion: v1
kind: Pod
spec:
  containers:
  - name: my-service
    readinessProbe:
      grpc:
        port: 50051
        service: ""       # 对应 SetServingStatus("", SERVING)
      initialDelaySeconds: 5
      periodSeconds: 10
    livenessProbe:
      grpc:
        port: 50051
        service: ""
      failureThreshold: 3

Kubernetes 原生支持 gRPC probe,直接调用 grpc_health_v1.Health.Check 即可,无需额外的 HTTP endpoint。

[!tip] Readiness vs Liveness

  • Readiness probe: 决定是否将流量接入 Pod。建议设置合理的 initialDelaySeconds,避免启动期间被误杀。
  • Liveness probe: 决定是否需要重启 Pod。不要用它检测临时性故障,否则会导致不必要的重启循环。

Load Balancer 摘机判断

负载均衡器(如 Envoy)支持 gRPC health checking protocol。当服务返回 NOT_SERVING 时,LB 会自动将该实例从后端池中摘除。

# Envoy lb_config —— 仅示意,完整配置参考 Envoy docs
common_lb_config:
  health_check:
    grpc: {}           # 使用 gRPC health check protocol
    timeout: 5s
    interval: 10s
    unhealthy_threshold: 3
    healthy_threshold: 2

蓝绿部署流程

sequenceDiagram
    participant K8s as "K8s Scheduler"
    participant HP as "Health Probe"
    participant HS as "Health Server"
    participant SS as "Serving Status"
    participant LB as "Load Balancer"

    K8s->>HP: Every 10s Call Check("")
    HP->>HS: rpc Check(service="")
    HS->>SS: query status
    SS-->>HS: SERVING
    HS-->>HP: HealthCheckResponse{SERVING}
    HP-->>K8s: OK

    alt service shutdown begins
        K8s->>SS: SetServingStatus("", NOT_SERVING)
        Note over K8s,SS: draining pods or rolling update
        HP->>HS: rpc Check(service="")
        HS->>SS: query status
        SS-->>HS: NOT_SERVING
        HS-->>HP: HealthCheckResponse{NOT_SERVING}
        HP-->>K8s: FAIL x3 -> restart/drain
    end

    style OK fill:#00D866,color:#fff
    style FAIL fill:#EE5A24,color:#fff

Server Reflection

启用 reflection 后,外部工具可以通过 gRPC 协议枚举所有 service、method、message 定义。这在开发和测试阶段非常有用。

// 方式 A:显式注册(推荐,可读性好)
import "google.golang.org/grpc/reflection"
reflection.Register(s)

// 方式 B:blank import(利用 init() 自动注册)
import _ "google.golang.org/grpc/reflection"

启用 reflection 后可以做这些操作:

  • 枚举所有已注册的 services 和 methods
  • 查看 message 的字段类型和编号
  • 构造请求进行交互式测试
  • IDE 自动补全 gRPC call

[!warning] 生产环境务必关闭 Reflection Reflection 开启后存在多重安全隐患:

  1. API 枚举:攻击者可以获取全部 service、method 名称,了解业务逻辑结构
  2. Message Schema 泄露:暴露所有 message 的字段类型和编号,辅助构造恶意请求
  3. 无需认证:Reflection RPC 本身没有鉴权机制,任何能连接 gRPC port 的请求都能使用

如果需要在生产环境调试,考虑使用专门的 tracing / metrics / audit logging 方案替代。

调试命令示例

安装 grpcurl 后可直接使用:

# 列出所有已注册的服务
grpcurl -plaintext localhost:50051 list

# 输出:
# grpc.health.v1.Health
# user.v1.UserService
# order.v1.OrderService

# 查看某个服务的完整定义
grpcurl -plaintext localhost:50051 describe user.v1.UserService

# 查看 message 结构
grpcurl -plaintext localhost:50051 describe user.v1.CreateUserRequest

# 调用 RPC(注入 request body)
grpcurl -plaintext \
    -d '{"name":"test","email":"test@example.com"}' \
    localhost:50051 user.v1.UserService/CreateUser

# 调用 Health Check
grpcurl -plaintext -d '{"service":""}' \
    localhost:50051 grpc.health.v1.Health/Check

# 输出:
# {
#   "status": "SERVING"
# }

Production Setup

生产环境的正确姿势:开启 health check,关闭 reflection。

func NewProdServer() *grpc.Server {
    s := grpc.NewServer(
        grpc.Creds(credentials.NewTLS(tlsConfig)),
        grpc.MaxRecvMsgSize(16*1024*1024),
        grpc.ChainUnaryInterceptor(logging.Unary(), auth.Unary()),
    )

    // 注册业务 service
    registerServices(s)

    // Health check always on in production
    hs := health.NewServer()
    hs.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
    grpc_health_v1.RegisterHealthServer(s, hs)

    // NO reflection in production!
    // reflection.Register(s) // <-- commented out

    return s
}

对比表格:

环境 Reflection Health Check Reason
Local Dev On Optional 方便调试
Staging Optional On 接近生产行为
Production Off On 安全 + 运维需求

Watch 端点的高级用法

Watch 是一个 server streaming 端点,比 unary Check 强大得多:客户端建立连接后,服务器会在状态变化时主动推送新值,而无需客户端反复轮询。

在 Go 中获取 Watch 流非常简单——只需调用 grpc_health_v1.NewHealthClient 然后执行 Watch RPC:

// Client 端:订阅 root service 的健康状态变化
client := grpc_health_v1.NewHealthClient(conn)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

watchStream, err := client.Watch(ctx, &grpc_health_v1.HealthCheckRequest{
    Service: "",
})
if err != nil {
    log.Fatal(err)
}

for {
    resp, err := watchStream.Recv()
    if err != nil {
        log.Printf("watch error: %v", err)
        return
    }
    log.Printf("status changed to: %s\n", resp.Status)
    // → SERVING → NOT_SERVING → SERVING ...
}

应用场景:

  • Metrics 采集器:实时消费状态变化,更新 prometheus histogram
  • Dashboard:前端 WebSocket 推送背后可以用 gRPC Watch 替代
  • Alerting System:检测到 NOT_SERVING 时立即触发告警,比轮询更高效
sequenceDiagram
    participant Obs as "Observer (Prometheus / Dashboard)"
    participant HS as "Health Server"
    participant Store as "Serving Status Map"

    Obs->>HS: Watch(service="") → stream created
    HS->>Store: Register watcher with channel
    Store-->>HS: Immediate: SERVING
    HS-->>Obs: Send(SERVING)

    Note over Store: Later... admin triggers shutdown

    Store->>HS: Notify all watchers: NOT_SERVING
    HS-->>Obs: Send(NOT_SERVING)

    Note over Obs: Alert fires within one notification cycle

    Obs->>HS: Close stream (context cancelled)
    HS->>Store: Unregister watcher

生产环境故障排查

部署后遇到问题?以下是高频场景的速查表:

[!faq] Probe 一直失败 (NOT_SERVING),但服务实际正常运行 原因:可能是启动速度太快,probe 在初始化完成之前就发起探测。解决:增大 initialDelaySeconds,或在初始化完成后才调用 SetServingStatus("", SERVING)。

[!faq] 为什么 Check 返回 SERVING 但接口实际不可用? 原因:SetServingStatus 只在内存中标记状态,不会检查依赖服务(DB、Redis 等)是否可用。如果需要深度健康检查,应该在 Check handler 中主动校验依赖项后再返回 NOT_SERVING。

[!faq] 切换服务版本时,老版本还在收流量 原因:K8s 删除 Pod 需要时间。正确做法是先调 SetServingStatus("", NOT_SERVING) 通知 LB 摘机,等业务请求归零后,再执行 Pod 删除。这叫 graceful drain。

// graceful drain 示例:Shutdown hook
func GracefulStop(server *grpc.Server, healthServer *health.Server) {
    // Step 1: 告诉 health check 我们不再 serving
    healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_NOT_SERVING)

    // Step 2: 停止接收新连接(但不中断已有请求)
    server.GracefulStop()
}

关联笔记

  • hhs/gRPC/3. 服务端实现/08-Server 搭建与注册
  • hhs/gRPC/3. 服务端实现/09-Streaming Handler