210 lines
7.0 KiB
Go
210 lines
7.0 KiB
Go
// 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 返回当前进程节点 ID,HTTP 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)
|
||
}
|