feat: simplify Redis discovery (use KEYS), align Status() interface, and overhaul documentation (Quick Start + examples) (by AI)
This commit is contained in:
parent
0984b688be
commit
90da7ff607
147
README.md
147
README.md
@ -1,79 +1,92 @@
|
|||||||
# @go/gateway
|
# @go/gateway (高性能动态网关)
|
||||||
|
|
||||||
基于 `@go/service` 和 Redis 的高性能、事件驱动的动态 API 网关。
|
基于 `@go/service` 实现的微服务 API 网关。专注于作为“配置下发搬运工”,将繁重的路由转发交由底层 `service` 核心无锁处理。
|
||||||
|
|
||||||
`gateway` 专注于充当配置下发的搬运工,将路由匹配、转发等所有繁重工作剥离给底层的 `service` 核心。支持通过 Redis 进行 0 延迟、0 丢包的路由级全量/局部热更新。
|
---
|
||||||
|
|
||||||
## 功能特性
|
## 🚀 快速开始 (1分钟)
|
||||||
|
|
||||||
- **极致性能**:基于 `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. 安装
|
||||||
```bash
|
```bash
|
||||||
# 以后台进程启动
|
go get apigo.cc/go/gateway
|
||||||
./gateway start
|
|
||||||
|
|
||||||
# 查看服务状态 (通过 IPC)
|
|
||||||
./gateway status
|
|
||||||
|
|
||||||
# 平滑重启 / 重载兜底
|
|
||||||
./gateway restart
|
|
||||||
./gateway reload # 触发底层 service 的 OnReload,全量同步 Redis
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 环境变量配置
|
### 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
|
```yaml
|
||||||
gateway:
|
gateway:
|
||||||
Redis: "127.0.0.1:6379"
|
Redis: "127.0.0.1:6379" # (可选) 动态配置中心
|
||||||
Prefix: "gateway"
|
Prefix: "gateway" # 键名前缀
|
||||||
Channel: "gateway:channel"
|
|
||||||
|
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 匹配顺序:`精确域名:端口` > `纯域名` > `纯端口` > `*`。*
|
||||||
|
|||||||
24
app.go
24
app.go
@ -6,6 +6,7 @@ import (
|
|||||||
"apigo.cc/go/redis"
|
"apigo.cc/go/redis"
|
||||||
"apigo.cc/go/service"
|
"apigo.cc/go/service"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -89,6 +90,20 @@ func (g *GatewayApp) Reload() error {
|
|||||||
return err
|
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 扫描所有网关配置并加载
|
// loadAll 初始化或 Reload 阶段从 Redis 扫描所有网关配置并加载
|
||||||
func (g *GatewayApp) loadAll(logger *log.Logger) {
|
func (g *GatewayApp) loadAll(logger *log.Logger) {
|
||||||
if g.rd == nil {
|
if g.rd == nil {
|
||||||
@ -97,11 +112,16 @@ func (g *GatewayApp) loadAll(logger *log.Logger) {
|
|||||||
|
|
||||||
logger.Info("gateway loading full configuration")
|
logger.Info("gateway loading full configuration")
|
||||||
|
|
||||||
hosts := g.rd.Do("SMEMBERS", GatewayConf.Prefix+":hosts").Strings()
|
// 直接从 Redis 扫描所有符合前缀的 Key (无需手动维护 hosts Set)
|
||||||
for _, host := range hosts {
|
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)
|
g.loadHost(host, logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// loadHost 加载指定 Host 下的所有类型配置
|
// loadHost 加载指定 Host 下的所有类型配置
|
||||||
func (g *GatewayApp) loadHost(host string, logger *log.Logger) {
|
func (g *GatewayApp) loadHost(host string, logger *log.Logger) {
|
||||||
|
|||||||
21
app_test.go
21
app_test.go
@ -35,6 +35,9 @@ func TestGateway(t *testing.T) {
|
|||||||
service.Host("*").GET("/hello", func(req *service.Request) string {
|
service.Host("*").GET("/hello", func(req *service.Request) string {
|
||||||
return "hello from backend, path: " + req.RequestURI
|
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()
|
asBackend := service.AsyncStart()
|
||||||
defer asBackend.Stop()
|
defer asBackend.Stop()
|
||||||
@ -48,6 +51,7 @@ func TestGateway(t *testing.T) {
|
|||||||
proxyRules := []service.ProxyRule{
|
proxyRules := []service.ProxyRule{
|
||||||
{Path: "^/api/(.*)$", ToApp: "test-backend", ToPath: "/$1"},
|
{Path: "^/api/(.*)$", ToApp: "test-backend", ToPath: "/$1"},
|
||||||
{Path: "/direct", ToApp: "test-backend", ToPath: "/hello"},
|
{Path: "/direct", ToApp: "test-backend", ToPath: "/hello"},
|
||||||
|
{Path: "/v2/*", ToApp: "test-backend", ToPath: "/hello/*"},
|
||||||
}
|
}
|
||||||
rewriteRules := []service.RewriteRule{
|
rewriteRules := []service.RewriteRule{
|
||||||
{Path: "^/old-api/(.*)$", ToPath: "/api/$1"},
|
{Path: "^/old-api/(.*)$", ToPath: "/api/$1"},
|
||||||
@ -56,7 +60,6 @@ func TestGateway(t *testing.T) {
|
|||||||
proxyJson, _ := json.Marshal(proxyRules)
|
proxyJson, _ := json.Marshal(proxyRules)
|
||||||
rewriteJson, _ := json.Marshal(rewriteRules)
|
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", "proxies", string(proxyJson))
|
||||||
rd.Do("HSET", "gateway:host:gw.test", "rewrites", string(rewriteJson))
|
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)
|
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
|
// 测试 3: Rewrite 后再 Proxy
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
18
main.go
18
main.go
@ -12,7 +12,23 @@ func main() {
|
|||||||
_ = config.Load(&GatewayConf, "gateway")
|
_ = config.Load(&GatewayConf, "gateway")
|
||||||
|
|
||||||
// 2. 初始化 Starter 信息
|
// 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 服务
|
// 3. 注册 Gateway 服务
|
||||||
// GatewayApp 自身实现了 starter.Service 和 starter.Reloader 接口
|
// GatewayApp 自身实现了 starter.Service 和 starter.Reloader 接口
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user