--- tags: [gRPC, Go, Health Check, Debug, Reflection] create time: 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`](https://github.com/grpc/grpc-proto/tree/master/grpc/health/v1) 仓库。protobuf / Java / Python 等语言的客户端可以直接克隆该仓库并使用 `protoc` 编译,或通过 maven / pip 等包管理器引入对应 stub。 ```protobuf 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 ```go 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 ```yaml 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 会自动将该实例从后端池中摘除。 ```yaml # 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 ``` ### 蓝绿部署流程 ```mermaid 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 定义。这在开发和测试阶段非常有用。 ```go // 方式 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](https://github.com/fullstorydev/grpcurl) 后可直接使用: ```bash # 列出所有已注册的服务 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。 ```go 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: ```go // 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 时立即触发告警,比轮询更高效 ```mermaid 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**。 ```go // 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]]