From 90da7ff6077651ed4b8343c55a95001bf8cd4811 Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Wed, 13 May 2026 00:49:29 +0800 Subject: [PATCH] feat: simplify Redis discovery (use KEYS), align Status() interface, and overhaul documentation (Quick Start + examples) (by AI) --- README.md | 147 ++++++++++++++++++++++++++++------------------------ app.go | 26 ++++++++-- app_test.go | 21 +++++++- main.go | 18 ++++++- 4 files changed, 140 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index db4a868..1320bdd 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,92 @@ -# @go/gateway +# @go/gateway (高性能动态网关) -基于 `@go/service` 和 Redis 的高性能、事件驱动的动态 API 网关。 +基于 `@go/service` 实现的微服务 API 网关。专注于作为“配置下发搬运工”,将繁重的路由转发交由底层 `service` 核心无锁处理。 -`gateway` 专注于充当配置下发的搬运工,将路由匹配、转发等所有繁重工作剥离给底层的 `service` 核心。支持通过 Redis 进行 0 延迟、0 丢包的路由级全量/局部热更新。 +--- -## 功能特性 - -- **极致性能**:基于 `service` 包的 Copy-on-Write (写时复制) 原语,无锁构建,转发阶段无锁等待。 -- **事件驱动热更新**:摒弃轮询,监听 Redis Pub/Sub,精准下发变更配置,旧连接不受影响。 -- **服务发现原生集成**:与 `@go/discover` 无缝集成,支持基于服务名的直连与负载均衡。 -- **生命周期接管**:由 `@go/starter` 接管,支持通过 `kill -SIGHUP` 触发本地全量重新同步。 - -## 配置模型 (Redis Hash) - -Gateway 将配置存储在 Redis 的 Hash 结构中,并按照 **Host (域名)** 进行字段隔离,提升拉取效率和管理粒度。 - -- **`gateway:proxies`** 代理规则 -- **`gateway:rewrites`** 重写规则 -- **`gateway:statics`** 静态目录服务 - -### 结构示例 - -假设配置的 Host 为 `api.example.com`: - -```json -// HSET gateway:proxies "api.example.com" -[ - { "Path": "^/user/(.*)$", "AuthLevel": 0, "ToApp": "user-service", "ToPath": "/$1" }, - { "Path": "/direct", "AuthLevel": 0, "ToApp": "http://10.0.0.1:8080", "ToPath": "/hello" } -] - -// HSET gateway:rewrites "api.example.com" -[ - { "Path": "^/old-api/(.*)$", "ToPath": "/api/$1" } -] - -// HSET gateway:statics "www.example.com" -{ - "/ui": "/var/www/html" -} -``` - -## 动态热更新 API (Pub/Sub) - -无需重启进程,向 Redis 的 `gateway:channel` 频道发布 JSON 消息,网关会在收到消息后仅拉取变化的那一部分(局部全量更新): - -```json -{ - "action": "update", - "type": "proxy", // 可选: "proxy", "rewrite", "static" - "host": "api.example.com" -} -``` - -## 启动与管理 +## 🚀 快速开始 (1分钟) +### 1. 安装 ```bash -# 以后台进程启动 -./gateway start - -# 查看服务状态 (通过 IPC) -./gateway status - -# 平滑重启 / 重载兜底 -./gateway restart -./gateway reload # 触发底层 service 的 OnReload,全量同步 Redis +go get apigo.cc/go/gateway ``` -## 环境变量配置 +### 2. 运行 (本地配置模式) +创建 `env.yml`: +```yaml +service: + Listen: ":80,http" + Proxies: + "*": + - Path: "/api/*" + ToApp: "my-backend-service" # 转发至服务发现 + ToPath: "/v1/*" +``` +启动: +```bash +go run . start +``` -支持通过环境变量或 `env.yml` 覆盖。 +--- + +## 📂 核心配置 (env.yml) + +网关完全继承了 `service` 的配置能力,详细配置请参考:[https://apigo.cc/go/service](https://apigo.cc/go/service) ```yaml gateway: - Redis: "127.0.0.1:6379" - Prefix: "gateway" - Channel: "gateway:channel" + Redis: "127.0.0.1:6379" # (可选) 动态配置中心 + Prefix: "gateway" # 键名前缀 + +service: + App: "gateway" + Listen: ":80,http|:443" # 支持多端口、SSL + Register: "127.0.0.1:6379" # 服务发现中心 ``` + +--- + +## 🔄 动态路由管理 (Redis) + +网关自动扫描 `gateway:host:*` 格式的键,无需手动维护索引。 + +### A. 设置路由 (HSET) +```bash +# 配置域名 gw.test 的代理规则 +HSET gateway:host:gw.test proxies '[{"Path":"/api/*","ToApp":"user-svc","ToPath":"/v1/*"}]' + +# 配置域名 gw.test 的重写规则 +HSET gateway:host:gw.test rewrites '[{"Path":"^/old/(.*)$","ToPath":"/new/$1"}]' + +# 配置域名 gw.test 的静态目录 +HSET gateway:host:gw.test statics '{"/ui":"/var/www/html"}' +``` + +### B. 秒级推送到全集群 (PUBLISH) +```bash +# 发布域名字符串即可触发该域名的局部热更新 +PUBLISH gateway:channel "gw.test" +``` + +--- + +## 🛠 命令行操作 (CLI) + +```bash +./gateway start # 后台启动 +./gateway status # 查看状态(包含:监听地址、动态域名数、日志队列等) +./gateway reload # 重载本地 YAML 并全量刷入 Redis 配置 +./gateway stop # 优雅停止 +``` + +--- + +## 📝 路由匹配速查 + +| 模式 | 示例 (Path) | 说明 | +| :--- | :--- | :--- | +| **精确** | `/login` | 字符串完全一致 | +| **通配** | `/api/*` | **必须带 `/*` 结尾**,高性能前缀匹配 | +| **正则** | `^/user/(.*)$`| 包含 `(` 即视为正则,支持捕获组 | + +*Host 匹配顺序:`精确域名:端口` > `纯域名` > `纯端口` > `*`。* diff --git a/app.go b/app.go index 6b52d8c..a458c8f 100644 --- a/app.go +++ b/app.go @@ -6,6 +6,7 @@ import ( "apigo.cc/go/redis" "apigo.cc/go/service" "context" + "fmt" "strings" ) @@ -89,6 +90,20 @@ func (g *GatewayApp) Reload() error { return err } +// Status 返回网关运行状态 +func (g *GatewayApp) Status() (string, error) { + addr, err := g.WebServer.Status() + if err != nil { + return "", err + } + + hostCount := 0 + if g.rd != nil { + hostCount = len(g.rd.Do("KEYS", GatewayConf.Prefix+":host:*").Strings()) + } + return fmt.Sprintf("%s, dynamic_hosts: %d", addr, hostCount), nil +} + // loadAll 初始化或 Reload 阶段从 Redis 扫描所有网关配置并加载 func (g *GatewayApp) loadAll(logger *log.Logger) { if g.rd == nil { @@ -97,9 +112,14 @@ func (g *GatewayApp) loadAll(logger *log.Logger) { logger.Info("gateway loading full configuration") - hosts := g.rd.Do("SMEMBERS", GatewayConf.Prefix+":hosts").Strings() - for _, host := range hosts { - g.loadHost(host, logger) + // 直接从 Redis 扫描所有符合前缀的 Key (无需手动维护 hosts Set) + keys := g.rd.Do("KEYS", GatewayConf.Prefix+":host:*").Strings() + prefixLen := len(GatewayConf.Prefix + ":host:") + for _, key := range keys { + if len(key) > prefixLen { + host := key[prefixLen:] + g.loadHost(host, logger) + } } } diff --git a/app_test.go b/app_test.go index 8d6bce2..4b09898 100644 --- a/app_test.go +++ b/app_test.go @@ -35,6 +35,9 @@ func TestGateway(t *testing.T) { service.Host("*").GET("/hello", func(req *service.Request) string { return "hello from backend, path: " + req.RequestURI }) + service.Host("*").GET("/hello/world", func(req *service.Request) string { + return "hello from backend, path: " + req.RequestURI + }) asBackend := service.AsyncStart() defer asBackend.Stop() @@ -48,6 +51,7 @@ func TestGateway(t *testing.T) { proxyRules := []service.ProxyRule{ {Path: "^/api/(.*)$", ToApp: "test-backend", ToPath: "/$1"}, {Path: "/direct", ToApp: "test-backend", ToPath: "/hello"}, + {Path: "/v2/*", ToApp: "test-backend", ToPath: "/hello/*"}, } rewriteRules := []service.RewriteRule{ {Path: "^/old-api/(.*)$", ToPath: "/api/$1"}, @@ -56,7 +60,6 @@ func TestGateway(t *testing.T) { proxyJson, _ := json.Marshal(proxyRules) rewriteJson, _ := json.Marshal(rewriteRules) - rd.Do("SADD", "gateway:hosts", "gw.test") rd.Do("HSET", "gateway:host:gw.test", "proxies", string(proxyJson)) rd.Do("HSET", "gateway:host:gw.test", "rewrites", string(rewriteJson)) @@ -130,6 +133,22 @@ func TestGateway(t *testing.T) { t.Fatalf("Proxy regexp failed, got: %s", body) } + // ============================================ + // 测试 2.1: 极速通配符前缀匹配 (Discover) + // ============================================ + req, _ = gohttp.NewRequest("GET", "http://127.0.0.1:"+gwPort+"/v2/world?x=2", nil) + req.Host = "gw.test" + res, err = client.Do(req) + body = "" + if err == nil && res != nil { + b, _ := io.ReadAll(res.Body) + res.Body.Close() + body = string(b) + } + if err != nil || !strings.Contains(body, "hello from backend, path: /hello/world?x=2") { + t.Fatalf("Proxy wildcard failed, got: %s", body) + } + // ============================================ // 测试 3: Rewrite 后再 Proxy // ============================================ diff --git a/main.go b/main.go index 5a5e35c..39af498 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,23 @@ func main() { _ = config.Load(&GatewayConf, "gateway") // 2. 初始化 Starter 信息 - starter.SetAppInfo("gateway", "2.0.0") + starter.SetAppInfo("gateway", "1.0.0") + starter.SetUsage(`High-performance API gateway based on apigo.cc/go/service. +Full Guide: https://apigo.cc/go/service + +Example Config (env.yml): + gateway: + Redis: "127.0.0.1:6379" # Configuration Center + service: + Listen: ":80,http" # HTTP Port + +Dynamic Route Update (via redis-cli): + PUBLISH gateway:channel "api.example.com" + +Matching Logic: + - Exact: "/login" (Literal string) + - Prefix: "/api/*" (Matches start, requires suffix "/*") + - Regex: "^/v1/(.*)$" (Matches pattern if "(" is present)`) // 3. 注册 Gateway 服务 // GatewayApp 自身实现了 starter.Service 和 starter.Reloader 接口