Init
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
---
|
||||
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]]
|
||||
Reference in New Issue
Block a user