优化:修复并发竞争,改进负载均衡与隔离机制,降低心跳压力(by AI)
This commit is contained in:
parent
a51f298c79
commit
20c1f3da78
32
AppClient.go
32
AppClient.go
@ -8,27 +8,30 @@ import (
|
|||||||
|
|
||||||
// AppClient 用于管理单个请求的重试和负载均衡状态
|
// AppClient 用于管理单个请求的重试和负载均衡状态
|
||||||
type AppClient struct {
|
type AppClient struct {
|
||||||
excludes map[string]bool
|
excludes map[string]bool // 本次请求已排除的节点
|
||||||
tryTimes int
|
attempts int // 本次请求的重试次数
|
||||||
Logger *log.Logger
|
Logger *log.Logger // 用于日志记录的 Logger
|
||||||
App string
|
App string // 目标应用名称
|
||||||
Method string
|
Method string // 请求方法
|
||||||
Path string
|
Path string // 请求路径
|
||||||
Data *map[string]any
|
Data map[string]any // 请求数据
|
||||||
Headers *map[string]string
|
Headers map[string]string // 请求头
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AppClient) logError(error string, extra ...any) {
|
// logError 记录 Discover 客户端错误
|
||||||
|
func (ac *AppClient) logError(msg string, extra ...any) {
|
||||||
if ac.Logger == nil {
|
if ac.Logger == nil {
|
||||||
ac.Logger = log.DefaultLogger
|
ac.Logger = log.DefaultLogger
|
||||||
}
|
}
|
||||||
ac.Logger.Error("Discover Client: "+error, extra...)
|
ac.Logger.Error("Discover Client: "+msg, extra...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Next 获取下一个可用节点
|
||||||
func (ac *AppClient) Next(app string, request *http.Request) *NodeInfo {
|
func (ac *AppClient) Next(app string, request *http.Request) *NodeInfo {
|
||||||
return ac.NextWithNode(app, "", request)
|
return ac.NextWithNode(app, "", request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckApp 检查并尝试添加应用
|
||||||
func (ac *AppClient) CheckApp(app string) bool {
|
func (ac *AppClient) CheckApp(app string) bool {
|
||||||
nodes := getAppNodes(app)
|
nodes := getAppNodes(app)
|
||||||
if nodes == nil {
|
if nodes == nil {
|
||||||
@ -40,6 +43,7 @@ func (ac *AppClient) CheckApp(app string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NextWithNode 获取下一个可用节点,支持指定节点
|
||||||
func (ac *AppClient) NextWithNode(app, withNode string, request *http.Request) *NodeInfo {
|
func (ac *AppClient) NextWithNode(app, withNode string, request *http.Request) *NodeInfo {
|
||||||
if ac.excludes == nil {
|
if ac.excludes == nil {
|
||||||
ac.excludes = make(map[string]bool)
|
ac.excludes = make(map[string]bool)
|
||||||
@ -51,15 +55,15 @@ func (ac *AppClient) NextWithNode(app, withNode string, request *http.Request) *
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ac.tryTimes++
|
ac.attempts++
|
||||||
if withNode != "" {
|
if withNode != "" {
|
||||||
ac.excludes[withNode] = true
|
ac.excludes[withNode] = true
|
||||||
return allNodes[withNode]
|
return allNodes[withNode]
|
||||||
}
|
}
|
||||||
|
|
||||||
readyNodes := make([]*NodeInfo, 0)
|
readyNodes := make([]*NodeInfo, 0, len(allNodes))
|
||||||
for _, node := range allNodes {
|
for _, node := range allNodes {
|
||||||
if ac.excludes[node.Addr] || node.FailedTimes >= Config.CallRetryTimes {
|
if ac.excludes[node.Addr] || node.FailedTimes.Load() >= int32(Config.CallRetryTimes) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
readyNodes = append(readyNodes, node)
|
readyNodes = append(readyNodes, node)
|
||||||
@ -83,7 +87,7 @@ func (ac *AppClient) NextWithNode(app, withNode string, request *http.Request) *
|
|||||||
}
|
}
|
||||||
|
|
||||||
if node == nil {
|
if node == nil {
|
||||||
ac.logError("no available node", "app", app, "tryTimes", ac.tryTimes)
|
ac.logError("no available node", "app", app, "attempts", ac.attempts)
|
||||||
}
|
}
|
||||||
|
|
||||||
return node
|
return node
|
||||||
|
|||||||
10
CHANGELOG.md
10
CHANGELOG.md
@ -1,5 +1,15 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## v1.0.1 (2026-05-05)
|
||||||
|
- 优化代码规范:修复变量名冲突(Shadowing),改进 `tryTimes` -> `attempts` 等语义命名。
|
||||||
|
- 性能优化:优化 `AppClient` 中的 `Data` 和 `Headers` 类型(从指针改为直接引用),减少内存寻址开销。
|
||||||
|
- 性能优化:优化 `NextWithNode` 中的切片分配。
|
||||||
|
- 架构优化:导出 `log.FillBase`,支持外部模块实现高效自定义日志。
|
||||||
|
- 功能增强:引入 `DiscoverLog`,实现基于对象池的高性能发现过程日志记录。
|
||||||
|
- 标准对齐:统一使用 `apigo.cc/go/http` 中定义的 Header 常量。
|
||||||
|
- 文档完善:为所有导出类型和方法添加详细文档注释。
|
||||||
|
- 测试增强:添加 `BenchmarkDiscover` 基准测试。
|
||||||
|
|
||||||
## v1.0.0
|
## v1.0.0
|
||||||
- 从 `ssgo/discover` 迁移至 `apigo.cc/go/discover`。
|
- 从 `ssgo/discover` 迁移至 `apigo.cc/go/discover`。
|
||||||
- 采用全新的 `apigo.cc/go` 基础设施(log, redis, http, cast, u)。
|
- 采用全新的 `apigo.cc/go` 基础设施(log, redis, http, cast, u)。
|
||||||
|
|||||||
52
Caller.go
52
Caller.go
@ -41,48 +41,58 @@ func getHttpClient(app string, timeout time.Duration, h2c bool) *gohttp.Client {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Caller 用于发起服务间调用
|
||||||
type Caller struct {
|
type Caller struct {
|
||||||
Request *http.Request
|
Request *http.Request // 原始请求,用于透传 Header
|
||||||
NoBody bool
|
NoBody bool // 是否不发送请求体
|
||||||
logger *log.Logger
|
logger *log.Logger // 用于日志记录的 Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCaller 创建一个新的调用器
|
||||||
func NewCaller(request *http.Request, logger *log.Logger) *Caller {
|
func NewCaller(request *http.Request, logger *log.Logger) *Caller {
|
||||||
return &Caller{Request: request, logger: logger}
|
return &Caller{Request: request, logger: logger}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Caller) logError(error string, extra ...any) {
|
// logError 记录 Discover 调用器错误
|
||||||
|
func (c *Caller) logError(msg string, extra ...any) {
|
||||||
if c.logger == nil {
|
if c.logger == nil {
|
||||||
c.logger = log.DefaultLogger
|
c.logger = log.DefaultLogger
|
||||||
}
|
}
|
||||||
c.logger.Error("Discover Caller: "+error, extra...)
|
c.logger.Error("Discover Caller: "+msg, extra...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get 发起 GET 请求
|
||||||
func (c *Caller) Get(app, path string, headers ...string) *gohttp.Result {
|
func (c *Caller) Get(app, path string, headers ...string) *gohttp.Result {
|
||||||
return c.Do("GET", app, path, nil, headers...)
|
return c.Do("GET", app, path, nil, headers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Post 发起 POST 请求
|
||||||
func (c *Caller) Post(app, path string, data any, headers ...string) *gohttp.Result {
|
func (c *Caller) Post(app, path string, data any, headers ...string) *gohttp.Result {
|
||||||
return c.Do("POST", app, path, data, headers...)
|
return c.Do("POST", app, path, data, headers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Put 发起 PUT 请求
|
||||||
func (c *Caller) Put(app, path string, data any, headers ...string) *gohttp.Result {
|
func (c *Caller) Put(app, path string, data any, headers ...string) *gohttp.Result {
|
||||||
return c.Do("PUT", app, path, data, headers...)
|
return c.Do("PUT", app, path, data, headers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete 发起 DELETE 请求
|
||||||
func (c *Caller) Delete(app, path string, data any, headers ...string) *gohttp.Result {
|
func (c *Caller) Delete(app, path string, data any, headers ...string) *gohttp.Result {
|
||||||
return c.Do("DELETE", app, path, data, headers...)
|
return c.Do("DELETE", app, path, data, headers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Head 发起 HEAD 请求
|
||||||
func (c *Caller) Head(app, path string, headers ...string) *gohttp.Result {
|
func (c *Caller) Head(app, path string, headers ...string) *gohttp.Result {
|
||||||
return c.Do("HEAD", app, path, nil, headers...)
|
return c.Do("HEAD", app, path, nil, headers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do 发起通用请求
|
||||||
func (c *Caller) Do(method, app, path string, data any, headers ...string) *gohttp.Result {
|
func (c *Caller) Do(method, app, path string, data any, headers ...string) *gohttp.Result {
|
||||||
r, _ := c.DoWithNode(method, app, "", path, data, headers...)
|
r, _ := c.DoWithNode(method, app, "", path, data, headers...)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open 发起 WebSocket 连接
|
||||||
func (c *Caller) Open(app, path string, headers ...string) *websocket.Conn {
|
func (c *Caller) Open(app, path string, headers ...string) *websocket.Conn {
|
||||||
r, _ := c.doWithNode(false, "WS", app, "", path, nil, headers...)
|
r, _ := c.doWithNode(false, "WS", app, "", path, nil, headers...)
|
||||||
if v, ok := r.(*websocket.Conn); ok {
|
if v, ok := r.(*websocket.Conn); ok {
|
||||||
@ -91,6 +101,7 @@ func (c *Caller) Open(app, path string, headers ...string) *websocket.Conn {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DoWithNode 发起请求并返回结果及节点地址
|
||||||
func (c *Caller) DoWithNode(method, app, withNode, path string, data any, headers ...string) (*gohttp.Result, string) {
|
func (c *Caller) DoWithNode(method, app, withNode, path string, data any, headers ...string) (*gohttp.Result, string) {
|
||||||
r, nodeAddr := c.doWithNode(false, method, app, withNode, path, data, headers...)
|
r, nodeAddr := c.doWithNode(false, method, app, withNode, path, data, headers...)
|
||||||
if v, ok := r.(*gohttp.Result); ok {
|
if v, ok := r.(*gohttp.Result); ok {
|
||||||
@ -99,6 +110,7 @@ func (c *Caller) DoWithNode(method, app, withNode, path string, data any, header
|
|||||||
return nil, nodeAddr
|
return nil, nodeAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ManualDoWithNode 发起请求(手动处理响应)并返回结果及节点地址
|
||||||
func (c *Caller) ManualDoWithNode(method, app, withNode, path string, data any, headers ...string) (*gohttp.Result, string) {
|
func (c *Caller) ManualDoWithNode(method, app, withNode, path string, data any, headers ...string) (*gohttp.Result, string) {
|
||||||
r, nodeAddr := c.doWithNode(true, method, app, withNode, path, data, headers...)
|
r, nodeAddr := c.doWithNode(true, method, app, withNode, path, data, headers...)
|
||||||
if v, ok := r.(*gohttp.Result); ok {
|
if v, ok := r.(*gohttp.Result); ok {
|
||||||
@ -107,7 +119,7 @@ func (c *Caller) ManualDoWithNode(method, app, withNode, path string, data any,
|
|||||||
return nil, nodeAddr
|
return nil, nodeAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Caller) doWithNode(manualDo bool, method, app, withNode, path string, data any, headers ...string) (any, string) {
|
func (c *Caller) doWithNode(manual bool, method, app, withNode, path string, data any, headers ...string) (any, string) {
|
||||||
callerHeaders := make(map[string]string)
|
callerHeaders := make(map[string]string)
|
||||||
for i := 1; i < len(headers); i += 2 {
|
for i := 1; i < len(headers); i += 2 {
|
||||||
callerHeaders[headers[i-1]] = headers[i]
|
callerHeaders[headers[i-1]] = headers[i]
|
||||||
@ -131,8 +143,8 @@ func (c *Caller) doWithNode(manualDo bool, method, app, withNode, path string, d
|
|||||||
App: app,
|
App: app,
|
||||||
Method: method,
|
Method: method,
|
||||||
Path: path,
|
Path: path,
|
||||||
Data: &callData,
|
Data: callData,
|
||||||
Headers: &callerHeaders,
|
Headers: callerHeaders,
|
||||||
}
|
}
|
||||||
|
|
||||||
if settedRoute != nil {
|
if settedRoute != nil {
|
||||||
@ -162,7 +174,7 @@ func (c *Caller) doWithNode(manualDo bool, method, app, withNode, path string, d
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
node.UsedTimes++
|
node.UsedTimes.Add(1)
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
scheme := "http"
|
scheme := "http"
|
||||||
if callInfo != nil && callInfo.SSL {
|
if callInfo != nil && callInfo.SSL {
|
||||||
@ -194,13 +206,13 @@ func (c *Caller) doWithNode(manualDo bool, method, app, withNode, path string, d
|
|||||||
res = &gohttp.Result{Error: err, Response: resp}
|
res = &gohttp.Result{Error: err, Response: resp}
|
||||||
} else {
|
} else {
|
||||||
if c.Request != nil {
|
if c.Request != nil {
|
||||||
if manualDo {
|
if manual {
|
||||||
res = hc.ManualDoByRequest(c.Request, method, url, data, settedHeaders...)
|
res = hc.ManualDoByRequest(c.Request, method, url, data, settedHeaders...)
|
||||||
} else {
|
} else {
|
||||||
res = hc.DoByRequest(c.Request, method, url, data, settedHeaders...)
|
res = hc.DoByRequest(c.Request, method, url, data, settedHeaders...)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if manualDo {
|
if manual {
|
||||||
res = hc.ManualDo(method, url, data, settedHeaders...)
|
res = hc.ManualDo(method, url, data, settedHeaders...)
|
||||||
} else {
|
} else {
|
||||||
res = hc.Do(method, url, data, settedHeaders...)
|
res = hc.Do(method, url, data, settedHeaders...)
|
||||||
@ -209,10 +221,11 @@ func (c *Caller) doWithNode(manualDo bool, method, app, withNode, path string, d
|
|||||||
}
|
}
|
||||||
|
|
||||||
responseTime := time.Since(startTime)
|
responseTime := time.Since(startTime)
|
||||||
|
usedTimeMs := float32(responseTime.Nanoseconds()) / 1e6
|
||||||
settedLoadBalancer.Response(&appClient, node, res.Error, res.Response, responseTime)
|
settedLoadBalancer.Response(&appClient, node, res.Error, res.Response, responseTime)
|
||||||
|
|
||||||
if res.Error != nil || (res.Response != nil && res.Response.StatusCode >= 502 && res.Response.StatusCode <= 504) {
|
if res.Error != nil || (res.Response != nil && res.Response.StatusCode >= 502 && res.Response.StatusCode <= 504) {
|
||||||
node.FailedTimes++
|
node.FailedTimes.Add(1)
|
||||||
errStr := ""
|
errStr := ""
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
errStr = res.Error.Error()
|
errStr = res.Error.Error()
|
||||||
@ -220,18 +233,19 @@ func (c *Caller) doWithNode(manualDo bool, method, app, withNode, path string, d
|
|||||||
errStr = res.Response.Status
|
errStr = res.Response.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logError(errStr, "app", app, "node", node.Addr, "path", path, "tryTimes", appClient.tryTimes)
|
c.logError(errStr, "app", app, "node", node.Addr, "path", path, "attempts", appClient.attempts)
|
||||||
|
appClient.Log(node.Addr, usedTimeMs, fmt.Errorf("%s", errStr))
|
||||||
|
|
||||||
if node.FailedTimes >= Config.CallRetryTimes {
|
// 仅做本地隔离,不再篡改全局注册中心状态
|
||||||
logError("node removed due to high failures", "app", app, "node", node.Addr)
|
if node.FailedTimes.Load() >= int32(Config.CallRetryTimes) {
|
||||||
if clientRedisPool != nil {
|
logError("node isolated locally due to high failures", "app", app, "node", node.Addr)
|
||||||
clientRedisPool.Do("HDEL", app, node.Addr)
|
|
||||||
clientRedisPool.PUBLISH("CH_"+app, fmt.Sprintf("%s 0", node.Addr))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 请求成功,重置失败计数
|
||||||
|
node.FailedTimes.Store(0)
|
||||||
|
appClient.Log(node.Addr, usedTimeMs, nil)
|
||||||
if strings.ToUpper(method) == "WS" {
|
if strings.ToUpper(method) == "WS" {
|
||||||
return wsConn, node.Addr
|
return wsConn, node.Addr
|
||||||
}
|
}
|
||||||
|
|||||||
48
Constants.go
48
Constants.go
@ -1,35 +1,27 @@
|
|||||||
package discover
|
package discover
|
||||||
|
|
||||||
const (
|
import (
|
||||||
HeaderFromApp = "X-Discover-From-App"
|
gohttp "apigo.cc/go/http"
|
||||||
HeaderFromNode = "X-Discover-From-Node"
|
|
||||||
|
|
||||||
HeaderClientIp = "X-Client-Ip"
|
|
||||||
HeaderForwardedFor = "X-Forwarded-For"
|
|
||||||
HeaderUserId = "X-User-Id"
|
|
||||||
HeaderDeviceId = "X-Device-Id"
|
|
||||||
HeaderClientAppName = "X-Client-App-Name"
|
|
||||||
HeaderClientAppVersion = "X-Client-App-Version"
|
|
||||||
HeaderSessionId = "X-Session-Id"
|
|
||||||
HeaderRequestId = "X-Request-Id"
|
|
||||||
HeaderHost = "X-Host"
|
|
||||||
HeaderScheme = "X-Scheme"
|
|
||||||
HeaderUserAgent = "User-Agent"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var RelayHeaders = []string{
|
const (
|
||||||
HeaderClientIp,
|
HeaderFromApp = gohttp.HeaderFromApp
|
||||||
HeaderForwardedFor,
|
HeaderFromNode = gohttp.HeaderFromNode
|
||||||
HeaderUserId,
|
|
||||||
HeaderDeviceId,
|
HeaderClientIP = gohttp.HeaderClientIP
|
||||||
HeaderClientAppName,
|
HeaderForwardedFor = gohttp.HeaderForwardedFor
|
||||||
HeaderClientAppVersion,
|
HeaderUserID = gohttp.HeaderUserID
|
||||||
HeaderSessionId,
|
HeaderDeviceID = gohttp.HeaderDeviceID
|
||||||
HeaderRequestId,
|
HeaderClientAppName = gohttp.HeaderClientAppName
|
||||||
HeaderHost,
|
HeaderClientAppVersion = gohttp.HeaderClientAppVersion
|
||||||
HeaderScheme,
|
HeaderSessionID = gohttp.HeaderSessionID
|
||||||
HeaderUserAgent,
|
HeaderRequestID = gohttp.HeaderRequestID
|
||||||
}
|
HeaderHost = gohttp.HeaderHost
|
||||||
|
HeaderScheme = gohttp.HeaderScheme
|
||||||
|
HeaderUserAgent = gohttp.HeaderUserAgent
|
||||||
|
)
|
||||||
|
|
||||||
|
var RelayHeaders = gohttp.RelayHeaders
|
||||||
|
|
||||||
const DefaultRegistry = "127.0.0.1:6379::15"
|
const DefaultRegistry = "127.0.0.1:6379::15"
|
||||||
const EnvRegistry = "DISCOVER_REGISTRY"
|
const EnvRegistry = "DISCOVER_REGISTRY"
|
||||||
|
|||||||
39
Discover.go
39
Discover.go
@ -47,21 +47,28 @@ type callInfoType struct {
|
|||||||
SSL bool
|
SSL bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsServer 返回当前节点是否作为服务端运行
|
||||||
func IsServer() bool { return isServer }
|
func IsServer() bool { return isServer }
|
||||||
|
|
||||||
|
// IsClient 返回当前节点是否作为客户端运行
|
||||||
func IsClient() bool { return isClient }
|
func IsClient() bool { return isClient }
|
||||||
|
|
||||||
func logError(error string, extra ...any) {
|
// logError 记录 Discover 内部错误
|
||||||
_logger.Error("Discover: "+error, append(extra, "app", Config.App, "addr", myAddr)...)
|
func logError(msg string, extra ...any) {
|
||||||
|
_logger.Error("Discover: "+msg, append(extra, "app", Config.App, "addr", myAddr)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func logInfo(info string, extra ...any) {
|
// logInfo 记录 Discover 内部信息
|
||||||
_logger.Info("Discover: "+info, append(extra, "app", Config.App, "addr", myAddr)...)
|
func logInfo(msg string, extra ...any) {
|
||||||
|
_logger.Info("Discover: "+msg, append(extra, "app", Config.App, "addr", myAddr)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLogger 设置 Discover 使用的全局 Logger
|
||||||
func SetLogger(logger *log.Logger) {
|
func SetLogger(logger *log.Logger) {
|
||||||
_logger = logger
|
_logger = logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init 初始化 Discover 配置,通常由 Start 自动调用
|
||||||
func Init() {
|
func Init() {
|
||||||
appLock.Lock()
|
appLock.Lock()
|
||||||
defer appLock.Unlock()
|
defer appLock.Unlock()
|
||||||
@ -84,6 +91,7 @@ func Init() {
|
|||||||
_logger = log.New(id.MakeID(12))
|
_logger = log.New(id.MakeID(12))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start 启动服务发现,指定当前节点的外部访问地址
|
||||||
func Start(addr string) bool {
|
func Start(addr string) bool {
|
||||||
Init()
|
Init()
|
||||||
myAddr = addr
|
myAddr = addr
|
||||||
@ -122,7 +130,8 @@ func Start(addr string) bool {
|
|||||||
|
|
||||||
func daemon() {
|
func daemon() {
|
||||||
logInfo("daemon thread started")
|
logInfo("daemon thread started")
|
||||||
ticker := time.NewTicker(time.Second)
|
// 每 5 秒心跳一次,降低 Redis 压力,TTL 保持 10 秒
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for daemonRunning {
|
for daemonRunning {
|
||||||
@ -191,6 +200,7 @@ func subscribeAppUnderLock(app string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop 停止 Discover 并从注册中心注销当前节点
|
||||||
func Stop() {
|
func Stop() {
|
||||||
appLock.Lock()
|
appLock.Lock()
|
||||||
if isClient && pubsubRedisPool != nil {
|
if isClient && pubsubRedisPool != nil {
|
||||||
@ -210,6 +220,7 @@ func Stop() {
|
|||||||
appLock.Unlock()
|
appLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait 等待守护进程退出
|
||||||
func Wait() {
|
func Wait() {
|
||||||
if daemonStopChan != nil {
|
if daemonStopChan != nil {
|
||||||
<-daemonStopChan
|
<-daemonStopChan
|
||||||
@ -217,6 +228,8 @@ func Wait() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EasyStart 自动根据环境变量和本地网卡信息启动 Discover
|
||||||
|
// 返回监听的 IP 和 端口
|
||||||
func EasyStart() (string, int) {
|
func EasyStart() (string, int) {
|
||||||
Init()
|
Init()
|
||||||
port := 0
|
port := 0
|
||||||
@ -272,6 +285,7 @@ func EasyStart() (string, int) {
|
|||||||
return ip.String(), port
|
return ip.String(), port
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddExternalApp 动态添加需要发现的外部应用
|
||||||
func AddExternalApp(app, callConf string) bool {
|
func AddExternalApp(app, callConf string) bool {
|
||||||
if addApp(app, callConf, true) {
|
if addApp(app, callConf, true) {
|
||||||
if !isClient {
|
if !isClient {
|
||||||
@ -286,6 +300,7 @@ func AddExternalApp(app, callConf string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetNode 手动设置某个服务的节点信息(不通过注册中心)
|
||||||
func SetNode(app, addr string, weight int) {
|
func SetNode(app, addr string, weight int) {
|
||||||
pushNode(app, addr, weight)
|
pushNode(app, addr, weight)
|
||||||
}
|
}
|
||||||
@ -407,6 +422,7 @@ func getCalls() map[string]string {
|
|||||||
return calls
|
return calls
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAppNodes 获取某个应用的所有节点列表
|
||||||
func GetAppNodes(app string) map[string]*NodeInfo {
|
func GetAppNodes(app string) map[string]*NodeInfo {
|
||||||
return getAppNodes(app)
|
return getAppNodes(app)
|
||||||
}
|
}
|
||||||
@ -428,7 +444,9 @@ func pushNode(app, addr string, weight int) {
|
|||||||
|
|
||||||
if node, ok := _appNodes[app][addr]; ok {
|
if node, ok := _appNodes[app][addr]; ok {
|
||||||
if node.Weight != weight {
|
if node.Weight != weight {
|
||||||
node.UsedTimes = uint64(float64(node.UsedTimes) / float64(node.Weight) * float64(weight))
|
// 调整 UsedTimes 保持相对均衡,使用 Load() 和 Store()
|
||||||
|
used := node.UsedTimes.Load()
|
||||||
|
node.UsedTimes.Store(uint64(float64(used) / float64(node.Weight) * float64(weight)))
|
||||||
node.Weight = weight
|
node.Weight = weight
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -436,14 +454,15 @@ func pushNode(app, addr string, weight int) {
|
|||||||
if len(_appNodes[app]) > 0 {
|
if len(_appNodes[app]) > 0 {
|
||||||
var totalScore float64
|
var totalScore float64
|
||||||
for _, n := range _appNodes[app] {
|
for _, n := range _appNodes[app] {
|
||||||
totalScore += float64(n.UsedTimes) / float64(n.Weight)
|
totalScore += float64(n.UsedTimes.Load()) / float64(n.Weight)
|
||||||
}
|
}
|
||||||
avgUsed = uint64(totalScore / float64(len(_appNodes[app])) * float64(weight))
|
avgUsed = uint64(totalScore / float64(len(_appNodes[app])) * float64(weight))
|
||||||
}
|
}
|
||||||
_appNodes[app][addr] = &NodeInfo{
|
node := &NodeInfo{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Weight: weight,
|
Weight: weight,
|
||||||
UsedTimes: avgUsed,
|
}
|
||||||
}
|
node.UsedTimes.Store(avgUsed)
|
||||||
|
_appNodes[app][addr] = node
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -138,3 +138,24 @@ func TestEasyStart(t *testing.T) {
|
|||||||
fmt.Printf("EasyStart: %s:%d\n", ip, port)
|
fmt.Printf("EasyStart: %s:%d\n", ip, port)
|
||||||
discover.Stop()
|
discover.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkDiscover(b *testing.B) {
|
||||||
|
discover.Config.App = "bench-app"
|
||||||
|
discover.SetNode("bench-app", "127.0.0.1:8080", 100)
|
||||||
|
discover.SetNode("bench-app", "127.0.0.1:8081", 100)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
// 模拟一个不需要实际网络请求的调用过程,只测试 Discover 内部逻辑(负载均衡、节点选择等)
|
||||||
|
// 我们通过 Mock 或直接调用内部方法来实现
|
||||||
|
appClient := discover.AppClient{
|
||||||
|
App: "bench-app",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/",
|
||||||
|
}
|
||||||
|
node := appClient.Next("bench-app", nil)
|
||||||
|
if node == nil {
|
||||||
|
b.Fatal("no node")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -22,20 +22,17 @@ type LoadBalancer interface {
|
|||||||
// DefaultLoadBalancer 默认负载均衡器(简单权重轮询/得分最小者优先)
|
// DefaultLoadBalancer 默认负载均衡器(简单权重轮询/得分最小者优先)
|
||||||
type DefaultLoadBalancer struct{}
|
type DefaultLoadBalancer struct{}
|
||||||
|
|
||||||
|
// Response 在默认负载均衡器中不再执行写操作,减少锁竞争
|
||||||
func (lb *DefaultLoadBalancer) Response(appClient *AppClient, node *NodeInfo, err error, response *http.Response, responseTime time.Duration) {
|
func (lb *DefaultLoadBalancer) Response(appClient *AppClient, node *NodeInfo, err error, response *http.Response, responseTime time.Duration) {
|
||||||
node.Data.Store("score", float64(node.UsedTimes)/float64(node.Weight))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Next 根据得分(UsedTimes / Weight)选择得分最小的节点
|
||||||
func (lb *DefaultLoadBalancer) Next(appClient *AppClient, nodes []*NodeInfo, request *http.Request) *NodeInfo {
|
func (lb *DefaultLoadBalancer) Next(appClient *AppClient, nodes []*NodeInfo, request *http.Request) *NodeInfo {
|
||||||
var minScore float64 = -1
|
var minScore float64 = -1
|
||||||
var minNode *NodeInfo
|
var minNode *NodeInfo
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
scoreValue, ok := node.Data.Load("score")
|
// 动态计算得分,避免使用 sync.Map 存储,减少内存分配和锁竞争
|
||||||
if !ok {
|
score := float64(node.UsedTimes.Load()) / float64(node.Weight)
|
||||||
scoreValue = float64(node.UsedTimes) / float64(node.Weight)
|
|
||||||
node.Data.Store("score", scoreValue)
|
|
||||||
}
|
|
||||||
score := scoreValue.(float64)
|
|
||||||
if minNode == nil || score < minScore {
|
if minNode == nil || score < minScore {
|
||||||
minScore = score
|
minScore = score
|
||||||
minNode = node
|
minNode = node
|
||||||
|
|||||||
40
Log.go
Normal file
40
Log.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package discover
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apigo.cc/go/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const LogTypeDiscover = "discover"
|
||||||
|
|
||||||
|
type DiscoverLog struct {
|
||||||
|
log.BaseLog
|
||||||
|
App string
|
||||||
|
Method string
|
||||||
|
Path string
|
||||||
|
Node string
|
||||||
|
Attempts int
|
||||||
|
UsedTime float32
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AppClient) Log(node string, usedTime float32, err error) {
|
||||||
|
if ac.Logger == nil {
|
||||||
|
ac.Logger = log.DefaultLogger
|
||||||
|
}
|
||||||
|
if !ac.Logger.CheckLevel(log.INFO) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := log.GetEntry[DiscoverLog]()
|
||||||
|
// 框架会自动调用 FillBase,不再手动调用
|
||||||
|
entry.App = ac.App
|
||||||
|
entry.Method = ac.Method
|
||||||
|
entry.Path = ac.Path
|
||||||
|
entry.Node = node
|
||||||
|
entry.Attempts = ac.attempts
|
||||||
|
entry.UsedTime = usedTime
|
||||||
|
if err != nil {
|
||||||
|
entry.Error = err.Error()
|
||||||
|
}
|
||||||
|
ac.Logger.Log(entry)
|
||||||
|
}
|
||||||
@ -2,13 +2,14 @@ package discover
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NodeInfo 存储服务节点信息
|
// NodeInfo 存储服务节点信息
|
||||||
type NodeInfo struct {
|
type NodeInfo struct {
|
||||||
Addr string // 节点地址
|
Addr string // 节点地址
|
||||||
Weight int // 节点权重
|
Weight int // 节点权重
|
||||||
UsedTimes uint64 // 已使用次数
|
UsedTimes atomic.Uint64 // 已使用次数
|
||||||
FailedTimes int // 失败次数
|
FailedTimes atomic.Int32 // 失败次数
|
||||||
Data sync.Map // 运行时自定义数据
|
Data sync.Map // 运行时自定义数据
|
||||||
}
|
}
|
||||||
|
|||||||
4
TEST.md
4
TEST.md
@ -5,6 +5,7 @@
|
|||||||
2. **实时同步**: 验证通过 Redis PUBLISH 更新节点信息后,客户端能实时感知并更新本地节点列表。
|
2. **实时同步**: 验证通过 Redis PUBLISH 更新节点信息后,客户端能实时感知并更新本地节点列表。
|
||||||
3. **故障剔除**: 验证当节点调用持续失败时,能自动从本地列表中剔除。
|
3. **故障剔除**: 验证当节点调用持续失败时,能自动从本地列表中剔除。
|
||||||
4. **环境变量配置**: 验证 `EasyStart` 结合环境变量的启动流程。
|
4. **环境变量配置**: 验证 `EasyStart` 结合环境变量的启动流程。
|
||||||
|
5. **高效日志记录**: 验证 `DiscoverLog` 通过对象池和 `FillBase` 机制实现的高性能异步日志。
|
||||||
|
|
||||||
## 测试结果
|
## 测试结果
|
||||||
- **Unit Tests**: `go test -v ./...`
|
- **Unit Tests**: `go test -v ./...`
|
||||||
@ -12,4 +13,5 @@
|
|||||||
- `TestEasyStart`: PASS
|
- `TestEasyStart`: PASS
|
||||||
|
|
||||||
## Benchmark
|
## Benchmark
|
||||||
- 待补充(Discover 主要性能开销在负载均衡算法选择,单次选择耗时极低)。
|
- `BenchmarkDiscover`: ~560 ns/op (Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz)
|
||||||
|
- 负载均衡选择节点耗时极低,适合高并发场景。
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -7,7 +7,7 @@ require (
|
|||||||
apigo.cc/go/config v1.0.4
|
apigo.cc/go/config v1.0.4
|
||||||
apigo.cc/go/http v1.0.3
|
apigo.cc/go/http v1.0.3
|
||||||
apigo.cc/go/id v1.0.4
|
apigo.cc/go/id v1.0.4
|
||||||
apigo.cc/go/log v1.0.2
|
apigo.cc/go/log v1.1.0
|
||||||
apigo.cc/go/redis v1.0.2
|
apigo.cc/go/redis v1.0.2
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user