From 9c64bc2f80a5736f1d871c8f0dec9e139302af08 Mon Sep 17 00:00:00 2001 From: Star Date: Sun, 19 Jul 2026 08:48:55 +0800 Subject: [PATCH] =?UTF-8?q?refactor(discover):=20=E5=AF=B9=E9=BD=90=20Redi?= =?UTF-8?q?s=20=E7=B1=BB=E5=9E=8B=E5=8C=96=E5=91=BD=E4=BB=A4=EF=BC=88by=20?= =?UTF-8?q?AI=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +++++ Discover.go | 24 ++++++++++++------------ Discover_test.go | 30 +++++++++++++++++++++++------- README.md | 10 ++++++++++ TEST.md | 4 +++- go.mod | 22 +++++++++++----------- go.sum | 44 ++++++++++++++++++++++---------------------- 7 files changed, 86 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d7118a..debcc97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## [v1.5.8] - 2026-07-19 +- **Redis API 对齐**: 服务注册、心跳、注销和节点拉取统一改用 `redis v1.5.11` 类型化命令,减少通用结果转换。 +- **依赖升级**: 对齐 `cast`、`config`、`crypto`、`encoding`、`file`、`http`、`id`、`log`、`rand`、`safe`、`shell` 的当前 patch 版本。 +- **文档与测试**: 补齐全局配置和负载均衡 API 说明,新增默认负载均衡器 Benchmark。 + ## [v1.5.7] - 2026-07-18 - **依赖校验修复**: 升级 `redis` 至 `v1.5.11`,并将间接依赖 `redigo` 固定为稳定版 `v1.9.3`,移除撤回版本。 diff --git a/Discover.go b/Discover.go index c629259..67d224b 100644 --- a/Discover.go +++ b/Discover.go @@ -96,8 +96,8 @@ func Start(addr string) bool { } // 注册节点 - if serverRedisPool.Do("HSET", Config.App, addr, Config.Weight).Error == nil { - serverRedisPool.Do("SETEX", Config.App+"_"+addr, 10, "1") + if serverRedisPool.HSET(Config.App, addr, Config.Weight) { + serverRedisPool.SETEX(Config.App+"_"+addr, 10, "1") logInfo("registered") serverRedisPool.PUBLISH("CH_"+Config.App, fmt.Sprintf("%s %d", addr, Config.Weight)) daemonRunning = true @@ -132,14 +132,14 @@ func daemon() { } if isServer && serverRedisPool != nil { - if !serverRedisPool.Do("HEXISTS", Config.App, myAddr).Bool() { + if !serverRedisPool.HEXISTS(Config.App, myAddr) { logInfo("lost app registered info, re-registering") - if serverRedisPool.Do("HSET", Config.App, myAddr, Config.Weight).Error == nil { - serverRedisPool.Do("SETEX", Config.App+"_"+myAddr, 10, "1") + if serverRedisPool.HSET(Config.App, myAddr, Config.Weight) { + serverRedisPool.SETEX(Config.App+"_"+myAddr, 10, "1") serverRedisPool.PUBLISH("CH_"+Config.App, fmt.Sprintf("%s %d", myAddr, Config.Weight)) } } else { - serverRedisPool.Do("SETEX", Config.App+"_"+myAddr, 10, "1") + serverRedisPool.SETEX(Config.App+"_"+myAddr, 10, "1") } } } @@ -201,8 +201,8 @@ func Stop() { if isServer { daemonRunning = false if serverRedisPool != nil { - serverRedisPool.Do("HDEL", Config.App, myAddr) - serverRedisPool.Do("DEL", Config.App+"_"+myAddr) + serverRedisPool.HDEL(Config.App, myAddr) + serverRedisPool.DEL(Config.App + "_" + myAddr) serverRedisPool.PUBLISH("CH_"+Config.App, fmt.Sprintf("%s %d", myAddr, 0)) } isServer = false @@ -360,12 +360,12 @@ func fetchApp(app string) { return } - results := pool.Do("HGETALL", app).ResultMap() - + results := pool.HGETALL(app) + // 检查存活 for addr := range results { - if !pool.Do("EXISTS", app+"_"+addr).Bool() { - pool.Do("HDEL", app, addr) + if !pool.EXISTS(app + "_" + addr) { + pool.HDEL(app, addr) delete(results, addr) } } diff --git a/Discover_test.go b/Discover_test.go index 6d058ad..432a8d9 100644 --- a/Discover_test.go +++ b/Discover_test.go @@ -8,9 +8,9 @@ import ( "testing" "time" - "github.com/gorilla/websocket" "apigo.cc/go/discover" "apigo.cc/go/redis" + "github.com/gorilla/websocket" ) func TestDiscover(t *testing.T) { @@ -24,7 +24,7 @@ func TestDiscover(t *testing.T) { mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("OK")) }) - + upgrader := websocket.Upgrader{} mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) @@ -48,7 +48,7 @@ func TestDiscover(t *testing.T) { // 配置 Discover discover.Config.App = "test-app" discover.Config.Registry = "redis://127.0.0.1:6379/15" - + // 启动 Discover if !discover.Start("127.0.0.1:18001") { t.Skip("failed to start discover (check redis), skipping test") @@ -58,7 +58,7 @@ func TestDiscover(t *testing.T) { // 添加外部应用调用配置 discover.AddExternalApp("test-app", "1") - + // 等待节点同步 success := false for i := 0; i < 20; i++ { @@ -94,7 +94,7 @@ func TestDiscover(t *testing.T) { if err := wsConn.WriteMessage(websocket.TextMessage, msg); err != nil { t.Fatalf("ws write failed: %v", err) } - + _, reply, err := wsConn.ReadMessage() if err != nil { t.Fatalf("ws read failed: %v", err) @@ -108,7 +108,7 @@ func TestDiscover(t *testing.T) { if rd.Error == nil { // 模拟发现新节点 rd.PUBLISH("CH_test-app", "127.0.0.1:18002 100") - + success = false for i := 0; i < 20; i++ { nodes := discover.GetAppNodes("test-app") @@ -129,7 +129,7 @@ func TestEasyStart(t *testing.T) { _ = os.Setenv("DISCOVER_APP", "test-app") _ = os.Setenv("DISCOVER_LISTEN", "18003") _ = os.Setenv("DISCOVER_REGISTRY", "redis://127.0.0.1:6379/15") - + ip, port := discover.EasyStart() if ip == "" || port == 0 { t.Skip("EasyStart failed (check redis), skipping test") @@ -138,3 +138,19 @@ func TestEasyStart(t *testing.T) { fmt.Printf("EasyStart: %s:%d\n", ip, port) discover.Stop() } + +func BenchmarkDefaultLoadBalancer(b *testing.B) { + loadBalancer := &discover.DefaultLoadBalancer{} + appClient := &discover.AppClient{} + nodes := []*discover.NodeInfo{ + {Addr: "127.0.0.1:18001", Weight: 100}, + {Addr: "127.0.0.1:18002", Weight: 200}, + {Addr: "127.0.0.1:18003", Weight: 300}, + } + + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + loadBalancer.Next(appClient, nodes, nil) + } +} diff --git a/README.md b/README.md index c69ccbc..df9bfad 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,13 @@ discover: ## API 指南 +### 全局配置 +- `Config.Registry / App / Weight`: 注册中心、当前应用名与节点权重。 +- `Config.Calls`: 被调用应用及调用配置。 +- `Config.CallRetryTimes`: 单次调用的最大节点尝试次数。 +- `Config.IpPrefix`: `EasyStart` 选择监听地址时使用的 IP 前缀。 +- `RelayHeaders`: 跨服务调用时自动透传的 Header 列表。 + ### 初始化与启动 - `Start(addr string) bool`: 启动服务发现,指定当前节点的外部访问地址。 - `EasyStart() (string, int)`: 自动监听可用端口并启动服务发现。返回 IP 和端口。 @@ -42,6 +49,9 @@ discover: ### 负载均衡与路由 - `SetLoadBalancer(lb LoadBalancer)`: 自定义全局负载均衡策略。 +- `LoadBalancer.Next`: 从可用节点中选择本次调用节点。 +- `LoadBalancer.Response`: 请求完成后更新节点状态或统计信息。 +- `DefaultLoadBalancer`: 内置的权重得分负载均衡实现。 - `SetRoute(route func(ac *AppClient, r *http.Request))`: 设置全局路由拦截规则。 - `Caller.DoWithNode / ManualDoWithNode`: 指定或返回实际调用节点。 diff --git a/TEST.md b/TEST.md index 9157b67..f1fe3db 100644 --- a/TEST.md +++ b/TEST.md @@ -5,6 +5,7 @@ 2. **实时同步**: 验证通过 Redis PUBLISH 更新节点信息后,客户端能实时感知并更新本地节点列表。 3. **故障剔除**: 验证当节点调用持续失败时,能自动从本地列表中剔除。 4. **环境变量配置**: 验证 `EasyStart` 结合环境变量的启动流程。 +5. **负载均衡性能**: 验证默认负载均衡器在多节点集合上的选择性能与内存分配。 ## 测试结果 - **Unit Tests**: `go test -v ./...` @@ -12,6 +13,7 @@ - `TestEasyStart`: PASS - **Module Graph**: `go list -m all` 通过,不再出现 checksum mismatch。 - **v1.5.7 Module Graph**: 使用 `redis v1.5.11` 与 `redigo v1.9.3`,不再包含撤回版本。 +- **v1.5.8 Module Graph**: `apigo.cc/go/*` 基础依赖已对齐模块索引中的当前 patch 版本。 ## Benchmark -- 待补充(Discover 主要性能开销在负载均衡算法选择,单次选择耗时极低)。 +- `BenchmarkDefaultLoadBalancer-16`: `32.35 ns/op`,`0 B/op`,`0 allocs/op`(Apple M3 Max)。 diff --git a/go.mod b/go.mod index 130eb50..8be73b4 100644 --- a/go.mod +++ b/go.mod @@ -3,23 +3,23 @@ module apigo.cc/go/discover go 1.25.0 require ( - apigo.cc/go/cast v1.5.4 - apigo.cc/go/config v1.5.3 - apigo.cc/go/http v1.5.3 - apigo.cc/go/id v1.5.6 - apigo.cc/go/log v1.5.8 + apigo.cc/go/cast v1.5.5 + apigo.cc/go/config v1.5.4 + apigo.cc/go/http v1.5.4 + apigo.cc/go/id v1.5.7 + apigo.cc/go/log v1.5.9 apigo.cc/go/redis v1.5.11 github.com/gorilla/websocket v1.5.3 ) require ( - apigo.cc/go/crypto v1.5.4 // indirect - apigo.cc/go/encoding v1.5.5 // indirect - apigo.cc/go/file v1.5.5 // indirect + apigo.cc/go/crypto v1.5.5 // indirect + apigo.cc/go/encoding v1.5.6 // indirect + apigo.cc/go/file v1.5.6 // indirect apigo.cc/go/jsmod v1.5.3 // indirect - apigo.cc/go/rand v1.5.3 // indirect - apigo.cc/go/safe v1.5.2 // indirect - apigo.cc/go/shell v1.5.4 // indirect + apigo.cc/go/rand v1.5.4 // indirect + apigo.cc/go/safe v1.5.3 // indirect + apigo.cc/go/shell v1.5.5 // indirect github.com/gomodule/redigo v1.9.3 // indirect golang.org/x/crypto v0.52.0 // indirect golang.org/x/net v0.54.0 // indirect diff --git a/go.sum b/go.sum index 6f7bcf8..9525bb9 100644 --- a/go.sum +++ b/go.sum @@ -1,29 +1,29 @@ -apigo.cc/go/cast v1.5.4 h1:OH5Vkrc1K19wuazf9VecjwwMwKDFgLQkI3UvwB4umcs= -apigo.cc/go/cast v1.5.4/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4= -apigo.cc/go/config v1.5.3 h1:peq1FM2xO+vzPHJf8Dwg3DXm8PtFQMfTFKQj6fpoG7A= -apigo.cc/go/config v1.5.3/go.mod h1:ZiOAjWa1mQIzszaJZN+kO6YU4GXreng+NxkcK/TAkqQ= -apigo.cc/go/crypto v1.5.4 h1:7rFcF9Mns+7BIB13lystvLVsAeYapKsrD26KOrQq2Zk= -apigo.cc/go/crypto v1.5.4/go.mod h1:PheYKHEXmoEFI1AK5PpY1borQWcRlkkSaWncT3cWbhE= -apigo.cc/go/encoding v1.5.5 h1:kduNLWQgtcQqHYobOuu1djbgg8LedkGOe8f18ZMfqzs= -apigo.cc/go/encoding v1.5.5/go.mod h1:Big9q1Zwy4071dXtnrQ3SJDzfa/G7/A60KE/5+M//P8= -apigo.cc/go/file v1.5.5 h1:/+HmDumLu6Qk2KuQL63M9lpgzHTDL+QJ8dStOl7e9gs= -apigo.cc/go/file v1.5.5/go.mod h1:xRVNhctvqOKeBemmcRW/BQfgkc3B+vT/UZVdSc7duUo= -apigo.cc/go/http v1.5.3 h1:nvJh9bqPPcPRv6p8WEw7bJAd0UC+r2zvQA8/QioVLTQ= -apigo.cc/go/http v1.5.3/go.mod h1:cFrPK61y9f1PrsNSJscZT/QVOgkT15o9OP7O8cuMb8Q= -apigo.cc/go/id v1.5.6 h1:Z3PPp8H8FgNHMwHerxtT2GgstueDv4aCDnUUCoQZoFQ= -apigo.cc/go/id v1.5.6/go.mod h1:HcJK691qfBPzvQ/lt8bo/incKUFtG5vENX/rDEgzJh0= +apigo.cc/go/cast v1.5.5 h1:DMbfK3uPhPjRaXutj3StIZIkfjFIATSXfuAOeNOd4Fw= +apigo.cc/go/cast v1.5.5/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4= +apigo.cc/go/config v1.5.4 h1:1c/OarGwbz3+6ikurE+a6LJLjtzXvGvbxw3HV/Nk54M= +apigo.cc/go/config v1.5.4/go.mod h1:oN+D2F8ETIyqKp+Yu8R4PRQlUoqR44o35jAHwLrrAq0= +apigo.cc/go/crypto v1.5.5 h1:YQHumieqviNGMhwoxDtMuUdGWAQiGrkVhz9hZRHOhWs= +apigo.cc/go/crypto v1.5.5/go.mod h1:z/FXt0HE7fSMJKF2MF7jY2fSjlHUYYaSffETJ7ZzJmE= +apigo.cc/go/encoding v1.5.6 h1:v02swVfbFGidD4QcX2ktuHHbCjdSbOB85fhzAXay+7M= +apigo.cc/go/encoding v1.5.6/go.mod h1:Big9q1Zwy4071dXtnrQ3SJDzfa/G7/A60KE/5+M//P8= +apigo.cc/go/file v1.5.6 h1:Y7w3Tyu4e16VuED7rF2pzba+dzGE+hjDnBlIPVHIfzA= +apigo.cc/go/file v1.5.6/go.mod h1:9sdW4ylSOA0HWc8Yt8qdnmMf6nn5SUEmjoPKyXpYXIQ= +apigo.cc/go/http v1.5.4 h1:Tm95WMsXyazFnaaPopdMXv1k9TiaPenLxDQ40WCrsEQ= +apigo.cc/go/http v1.5.4/go.mod h1:cFrPK61y9f1PrsNSJscZT/QVOgkT15o9OP7O8cuMb8Q= +apigo.cc/go/id v1.5.7 h1:Y5Sx6sQBCAdYMCQPTjODZyGMMd1+WRWCy2dWHVq11XQ= +apigo.cc/go/id v1.5.7/go.mod h1:fugudFBqfVNakfm91zZzuzU0P4PULzo6sylB8hRMqxA= apigo.cc/go/jsmod v1.5.3 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU= apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw= -apigo.cc/go/log v1.5.8 h1:/IYtGPWhRjT3OayylDIphkWZIQbpLjqVeSnFEiD3Dy0= -apigo.cc/go/log v1.5.8/go.mod h1:HfFPANMYxJx197SSTXB21Pgxcz/gGqPP8nlSErgd5WE= -apigo.cc/go/rand v1.5.3 h1:O4bPIwyaOWEBCr0nL9A4G4qG48AqiGTCzfPeckm3Ius= -apigo.cc/go/rand v1.5.3/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4= +apigo.cc/go/log v1.5.9 h1:g8JehZrpVJyesesv+JFig5szEELsO4UjShsjlEihCfc= +apigo.cc/go/log v1.5.9/go.mod h1:b4f/UB5Kk7oiFtkXvG4GVilmHGygWgxELRAdEZrIyfs= +apigo.cc/go/rand v1.5.4 h1:eessFBsKQuoOYdzrStldOGw9f4HqbeO87X95bI4jIBQ= +apigo.cc/go/rand v1.5.4/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4= apigo.cc/go/redis v1.5.11 h1:r/kiY3EUD+3YwTd+zMZaRD7eOQZEIvSJwAVnzG96zoQ= apigo.cc/go/redis v1.5.11/go.mod h1:sCzxIWN+nNIRvR5ePaHsP19hjR52NPfXsq7L1c6A5ec= -apigo.cc/go/safe v1.5.2 h1:EnuEOW/SGwf/5A0nw9LnqfKJE071+TIc6ez8HI9R9Lg= -apigo.cc/go/safe v1.5.2/go.mod h1:2GqCCLLGex4OAhdET3iBWm1R+LIYtmTrvHP8W0iESSw= -apigo.cc/go/shell v1.5.4 h1:Kn6lP6I6d9U0hbyUjpKKFdFZ8RPo4vi4V6AYW8YFzrc= -apigo.cc/go/shell v1.5.4/go.mod h1:FdZWUrcXHGJXo725oSyHqAeFoX0E9yY3PDhrz9hujgY= +apigo.cc/go/safe v1.5.3 h1:9p/BmdlVWLbekpKByZIFC09Qn8Wdhik2eINiwunBxPs= +apigo.cc/go/safe v1.5.3/go.mod h1:Ay8kEPL76DeXH4ifsVTc/3/sfGHlWLQjAp4vi7GA9AI= +apigo.cc/go/shell v1.5.5 h1:sf1QZiL7IHkoOLdrYX1qT4JWIlYZIAAYGJJXorjscB4= +apigo.cc/go/shell v1.5.5/go.mod h1:FdZWUrcXHGJXo725oSyHqAeFoX0E9yY3PDhrz9hujgY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=