362 lines
12 KiB
Markdown
362 lines
12 KiB
Markdown
---
|
||
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]]
|