2026-05-12 21:51:39 +08:00

186 lines
4.6 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
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)
}