2026-04-25 01:18:30 +08:00

210 lines
7.0 KiB
Go
Raw 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.

// Package healthcheck 提供 im-service 的 HTTP 和 gRPC 健康检查统一状态模型。
package healthcheck
import (
"context"
"errors"
"fmt"
"sync/atomic"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
)
const (
// serviceName 是 gRPC health 和 HTTP health 对外稳定暴露的服务名。
serviceName = "im-service"
// readyBudget 限制 ready 总耗时,避免探针积压拖垮进程。
readyBudget = 500 * time.Millisecond
// roomGRPCBudget 限制 room-service 连接探测时间ready 失败应快速反馈给调度层。
roomGRPCBudget = 300 * time.Millisecond
// componentStatus 是所有健康检查项成功时使用的稳定字符串。
componentStatus = "ok"
// failStatus 是任一健康检查项失败时使用的稳定字符串。
failStatus = "fail"
)
// RuntimeProbe 覆盖 im-service 进程内、无 IO 的核心运行时状态。
type RuntimeProbe interface {
// HealthCheck 检查进程内依赖是否初始化完整,不应执行慢 IO。
HealthCheck() error
}
// Check 描述单个健康检查项,失败时会直接暴露给部署系统和排障工具。
type Check struct {
// Name 是检查项稳定名称,便于监控系统聚合。
Name string `json:"name"`
// Status 只能是 ok 或 fail。
Status string `json:"status"`
// Message 只在失败时返回,避免健康响应泄露过多运行细节。
Message string `json:"message,omitempty"`
}
// State 记录 listener、drain 和 room-service 连接这些健康检查硬边界。
type State struct {
// nodeID 用于定位具体 im-service 实例。
nodeID string
// runtime 检查 session manager、store 等进程内对象是否可用。
runtime RuntimeProbe
// room 是到 room-service 的 gRPC 连接ready 需要它可连接。
room *grpc.ClientConn
// httpServing 表示 HTTP/WebSocket listener 已进入 Serve 循环。
httpServing atomic.Bool
// grpcServing 表示内部 gRPC listener 已进入 Serve 循环。
grpcServing atomic.Bool
// draining 表示实例正在下线ready 应立即失败。
draining atomic.Bool
}
// NewState 创建 im-service 健康状态所有者;业务代码只通过标记方法更新状态。
func NewState(nodeID string, runtime RuntimeProbe, room *grpc.ClientConn) *State {
return &State{
nodeID: nodeID,
runtime: runtime,
room: room,
}
}
// ServiceName 返回健康检查对外声明的服务边界名。
func (s *State) ServiceName() string {
return serviceName
}
// NodeID 返回当前进程节点 IDHTTP health response 会透出它用于定位实例。
func (s *State) NodeID() string {
return s.nodeID
}
// MarkHTTPServing 标记 HTTP/WebSocket listener 已进入 Serve 循环。
func (s *State) MarkHTTPServing() {
s.httpServing.Store(true)
}
// MarkHTTPStopped 标记 HTTP/WebSocket listener 已退出 Serve 循环。
func (s *State) MarkHTTPStopped() {
s.httpServing.Store(false)
}
// MarkGRPCServing 标记内部 gRPC listener 已进入 Serve 循环。
func (s *State) MarkGRPCServing() {
s.grpcServing.Store(true)
}
// MarkGRPCStopped 标记内部 gRPC listener 已退出 Serve 循环。
func (s *State) MarkGRPCStopped() {
s.grpcServing.Store(false)
}
// MarkDraining 让 ready 立即失败,保证下线时不会继续接新连接或新 RPC。
func (s *State) MarkDraining() {
s.draining.Store(true)
}
// Live 检查本进程入口和内存运行时是否仍可工作,不探测下游依赖。
func (s *State) Live() []Check {
// live 只看本进程是否还应该存活,不把 room-service 抖动作为重启本进程的理由。
checks := []Check{
boolCheck("http_server", s.httpServing.Load(), "HTTP/WebSocket listener is not serving"),
boolCheck("grpc_bridge", s.grpcServing.Load(), "gRPC bridge listener is not serving"),
}
if s.runtime == nil {
// runtime 缺失代表 App 装配异常live 失败可以让进程被 supervisor 重启。
checks = append(checks, failCheck("session_manager", "runtime probe is not initialized"))
return checks
}
if err := s.runtime.HealthCheck(); err != nil {
// 内存态依赖不可用时继续服务会导致连接和投递路径异常。
checks = append(checks, failCheck("session_manager", err.Error()))
return checks
}
// 当前 session manager 是同步内存索引,没有后台 loop这里确认其依赖已初始化。
checks = append(checks, okCheck("session_manager"))
return checks
}
// Ready 检查 im-service 接新流量所需的硬条件,包含 room-service gRPC 可连接性。
func (s *State) Ready(ctx context.Context) []Check {
// ready 包含下游依赖和 drain 状态,用于决定是否继续接新 WS 和内部 RPC。
ctx, cancel := context.WithTimeout(ctx, readyBudget)
defer cancel()
checks := []Check{
boolCheck("http_listener", s.httpServing.Load(), "HTTP/WebSocket listener is not serving"),
boolCheck("grpc_listener", s.grpcServing.Load(), "gRPC listener is not serving"),
boolCheck("draining", !s.draining.Load(), "node is draining"),
}
roomCtx, roomCancel := context.WithTimeout(ctx, roomGRPCBudget)
defer roomCancel()
if err := waitGRPCReady(roomCtx, s.room); err != nil {
// room-service 不可达时新订阅和公屏发言无法做守卫校验,因此不应接新流量。
checks = append(checks, failCheck("room_grpc", err.Error()))
} else {
checks = append(checks, okCheck("room_grpc"))
}
return checks
}
// Healthy 汇总检查项状态HTTP status code 和 gRPC serving status 都依赖它。
func Healthy(checks []Check) bool {
for _, check := range checks {
if check.Status != componentStatus {
// 任一组件失败即整体失败,避免 ready/liveness 聚合时掩盖部分不可用。
return false
}
}
return true
}
func waitGRPCReady(ctx context.Context, conn *grpc.ClientConn) error {
if conn == nil {
// 没有连接对象说明 App.New 装配失败,不应报告 ready。
return errors.New("room gRPC connection is not initialized")
}
for {
state := conn.GetState()
if state == connectivity.Ready {
// Ready 表示至少已经完成一次可用连接建立。
return nil
}
if state == connectivity.Shutdown {
// Shutdown 连接不可恢复,必须让 ready 失败等待进程重建。
return errors.New("room gRPC connection is shutdown")
}
// grpc.Dial 是惰性连接;健康检查显式触发一次连接,再等待状态变化。
conn.Connect()
if !conn.WaitForStateChange(ctx, state) {
return fmt.Errorf("room gRPC not ready before deadline: %w", ctx.Err())
}
}
}
func okCheck(name string) Check {
// 成功检查项不附带 message保持健康响应短小稳定。
return Check{Name: name, Status: componentStatus}
}
func failCheck(name string, message string) Check {
// 失败检查项必须携带 message方便直接从探针响应定位失败边界。
return Check{Name: name, Status: failStatus, Message: message}
}
func boolCheck(name string, ok bool, message string) Check {
if ok {
return okCheck(name)
}
// bool 型检查用于 listener/drain 等本地状态,失败原因由调用方传入稳定文本。
return failCheck(name, message)
}