Files

362 lines
12 KiB
Markdown
Raw Permalink Normal View History

2026-05-24 11:42:38 +08:00
---
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]]