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