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

362 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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]]