package healthcheck import ( "context" "errors" "fmt" "strings" "sync/atomic" "time" "google.golang.org/grpc" "google.golang.org/grpc/connectivity" ) const ( serviceName = "gateway-service" readyBudget = 2 * time.Second grpcCheckBudget = 500 * time.Millisecond componentStatus = "ok" failStatus = "fail" ) // Check 描述健康检查子项,失败信息会暴露给部署系统和本地排障命令。 type Check struct { Name string `json:"name"` Status string `json:"status"` Message string `json:"message,omitempty"` } // GRPCDependency describes one internal service connection required before the // gateway should receive new external traffic. type GRPCDependency struct { Name string Conn *grpc.ClientConn } // State 记录 gateway 接新流量所需的进程状态和 room-service 连接状态。 type State struct { nodeID string jwtSecret string dependencies []GRPCDependency httpServing atomic.Bool draining atomic.Bool } // NewState 创建 gateway 健康状态所有者。 func NewState(nodeID string, jwtSecret string, room *grpc.ClientConn, dependencies ...GRPCDependency) *State { all := make([]GRPCDependency, 0, 1+len(dependencies)) all = append(all, GRPCDependency{Name: "room_grpc", Conn: room}) all = append(all, dependencies...) return &State{ nodeID: nodeID, jwtSecret: jwtSecret, dependencies: all, } } // ServiceName 返回健康检查对外声明的服务边界名。 func (s *State) ServiceName() string { return serviceName } // NodeID 返回当前实例标识,用于定位具体 gateway 进程。 func (s *State) NodeID() string { return s.nodeID } // MarkHTTPServing 标记 gateway HTTP listener 已进入 Serve 循环。 func (s *State) MarkHTTPServing() { s.httpServing.Store(true) } // MarkHTTPStopped 标记 gateway HTTP listener 已退出 Serve 循环。 func (s *State) MarkHTTPStopped() { s.httpServing.Store(false) } // MarkDraining 让 ready 立即失败,滚动下线时负载均衡不再打新流量。 func (s *State) MarkDraining() { s.draining.Store(true) } // Live 只判断 gateway 进程 HTTP runtime 是否仍在服务,不探测 room-service。 func (s *State) Live() []Check { return []Check{ boolCheck("http_server", s.httpServing.Load(), "HTTP listener is not serving"), } } // Ready 检查 gateway 接新请求所需硬依赖:JWT 配置、HTTP listener 和 room-service。 func (s *State) Ready(ctx context.Context) []Check { ctx, cancel := context.WithTimeout(ctx, readyBudget) defer cancel() checks := []Check{ boolCheck("jwt_config", strings.TrimSpace(s.jwtSecret) != "", "JWT secret is empty"), boolCheck("http_listener", s.httpServing.Load(), "HTTP listener is not serving"), boolCheck("draining", !s.draining.Load(), "node is draining"), } checks = append(checks, s.grpcChecks(ctx)...) return checks } // Healthy 汇总检查项状态,HTTP status code 只看这个结果。 func Healthy(checks []Check) bool { for _, check := range checks { if check.Status != componentStatus { return false } } return true } func (s *State) grpcChecks(ctx context.Context) []Check { results := make([]Check, len(s.dependencies)) done := make(chan struct{}) for index, dependency := range s.dependencies { index := index dependency := dependency go func() { name := strings.TrimSpace(dependency.Name) if name == "" { name = "grpc_dependency" } checkCtx, cancel := context.WithTimeout(ctx, grpcCheckBudget) defer cancel() if err := waitGRPCReady(checkCtx, dependency.Conn); err != nil { results[index] = failCheck(name, err.Error()) } else { results[index] = okCheck(name) } done <- struct{}{} }() } for range s.dependencies { <-done } return results } func waitGRPCReady(ctx context.Context, conn *grpc.ClientConn) error { if conn == nil { return errors.New("room gRPC connection is not initialized") } for { state := conn.GetState() if state == connectivity.Ready { return nil } if state == connectivity.Shutdown { return errors.New("room gRPC connection is shutdown") } // grpc.Dial 是惰性连接;ready 探测负责触发连接并在预算内等待。 conn.Connect() if !conn.WaitForStateChange(ctx, state) { return fmt.Errorf("room gRPC not ready before deadline: %w", ctx.Err()) } } } func okCheck(name string) Check { return Check{Name: name, Status: componentStatus} } func failCheck(name string, message string) Check { return Check{Name: name, Status: failStatus, Message: message} } func boolCheck(name string, ok bool, message string) Check { if ok { return okCheck(name) } return failCheck(name, message) }