|
|
пре 4 дана | |
|---|---|---|
| .. | ||
| src | пре 4 дана | |
| README.md | пре 4 дана | |
| pom.xml | пре 4 дана | |
设计文档:
docs/design/ai-gateway-user-sticky-routing.md(本文与其冲突时以本文为准, 实施时对设计做了若干修正,见文末「与设计文档的差异」)。
案件数据源(DuckDB)、上传附件、Agent 记忆/技能、Python 产物都落在各 ai-server 节点的本地磁盘;
/case/open 还把案件数据源注册进节点 JVM 内存。因此不能做普通的无状态负载均衡 ——
请求落到非所属机器上,即使 token 和 caseId 都能从共享 Redis 恢复,本机也没有对应数据源,
会直接报「当前案件数据源未打开或已释放」。
网关只做一件事:同一用户的请求永远转发到同一台 ai-server。
x-token → userId → 绑定节点(Redis 缓存 + PostgreSQL 兜底);Electron / Web
│ http://gateway:8080/js/a/**
▼
┌──────────────── ai-gateway :8080 ────────────────┐
│ 登录:username → 已绑定节点;未绑定 → 轮询候选节点 │
│ (登录成功后 bindIfAbsent 落库,失败不绑定) │
│ 普通请求:x-token → userId → 绑定节点 │
│ 无 token / token 失效:健康节点轮询(透传后端 401)│
└──────────────┬───────────────────┬───────────────┘
ai-server node-1 ai-server node-2 (:8980,各自本地数据)
└──── 共享 PostgreSQL(gateway_* 配置与路由表)
└──── 共享 Redis(sa-token 登录态 + 网关缓存)
技术栈:Spring Boot 3.5 + Spring Cloud Gateway 2025.0.x(WebFlux/Netty), sa-token 1.46(只读共享 Redis 解析 token,不做拦截)、PostgreSQL、Redis。
关键类速查:
| 类 | 职责 |
|---|---|
route/StickyRouteFilter(order 10001) |
粘性决策、节点健康校验、重写目标 URL、X-Gateway-Node 响应头、503 |
route/LoginRequestFilter(9999) |
登录体缓存(≤64KB)解析 username;Accept-Encoding: identity |
route/LoginResponseFilter(-2) |
登录响应捕获(≤256KB)→ 绑定 + token 缓存;响应原样透传 |
route/TokenResolver |
gateway:token:{svc}:{token} 缓存 → StpUtil.getLoginIdByToken 兜底 |
route/UserRouteService |
userId/username → 节点 两级缓存读写、bindIfAbsent、缓存清理 |
registry/GatewayRegistry |
服务/节点快照(DB 为事实来源),变化发布 RefreshRoutesEvent |
registry/HealthChecker |
每 10s 探活,连续 3 次失败 DOWN,1 次成功恢复 UP |
admin/* |
/gateway/admin/** 管理接口(管理员 token + IP 白名单) |
support/GatewayErrorHandler |
503/502/404/500 统一 {code,message,data} JSON |
LoginResponseFilter的 order 是NettyWriteResponseFilter - 1(-2): 写响应体的 NettyWriteResponseFilter(-1)持有它自己拿到的 exchange, 只有比它更外层 mutate response,装饰器才会被写路径穿过(SCG 官方 ModifyResponseBody 同款排法)。 登录路径判定在该过滤器内独立完成,绑定用的节点属性在 post 阶段从共享 attributes 读取。
# 开发(连 192.168.0.106 的 PG/Redis,与 ai-server dev 同一套)
mvn -pl ai-gateway package -DskipTests
java -jar ai-gateway/target/ai-gateway-1.0.jar --spring.profiles.active=dev
# 生产:用环境变量注入凭证
PG_URL=... PG_USER=... PG_PASSWORD=... REDIS_HOST=... \
java -jar ai-gateway-1.0.jar --spring.profiles.active=pro
sql/gateway.sql(幂等建表,源文件在仓库根 sql/,打包时复制进 classpath)。gateway_service 表为空时按 gateway.services YAML 播种;之后以数据库为准,YAML 不再覆盖。token-name、同一 database),
否则 token 键位对不上、全部解析失败。网关改 sa-token 配置前先对照后端。gateway:token:{svc}:{token}(TTL 24h)、
gateway:route:user:{svc}:{userId}、gateway:route:name:{svc}:{lower(username)}(无 TTL,DB 为准)。/gateway/admin/**)需要管理员 x-token(sys_user.is_admin=1 且 status=1),可选 gateway.admin.ip-whitelist
(单 IP / CIDR)。gateway.admin.enabled=false 时一律 404。
GET /gateway/admin/status 服务/节点/健康/绑定数总览
GET /gateway/admin/services
POST /gateway/admin/services {id,name,pathPrefix,loginPath,healthPath,defaultNodeId,enabled}
PUT /gateway/admin/services/{serviceId}
DELETE /gateway/admin/services/{serviceId} (有节点/绑定时 409)
POST /gateway/admin/services/{serviceId}/nodes {id,name,baseUrl,enabled,remark}
PUT /gateway/admin/nodes/{nodeId}
DELETE /gateway/admin/nodes/{nodeId} (有绑定时 409)
POST /gateway/admin/nodes/{nodeId}/check 手动探活
GET /gateway/admin/routes?serviceId=&keyword=&page=&size=
POST /gateway/admin/routes/bind {serviceId,userId|username,nodeId} 预绑定/重绑
DELETE /gateway/admin/routes/{id} 解绑(同步清 Redis 缓存)
POST /gateway/admin/routes/import-existing {serviceId,nodeId} 存量用户批量绑定
POST /gateway/admin/reload 强制重载配置
多网关实例:写库实例立即刷新;其余实例每 10s 对账(gateway.registry-poll-interval)。
QingJian/ 数据目录就绪。node-1 指向现有后端,default-node-id=node-1。POST /gateway/admin/routes/import-existing 把存量 sys_user 绑到 node-1
(未导入的用户也会走 default-node 兜底)。VITE_GLOB_API_URL(或打包产物 _app.config.js)改为 http://<gateway>:8080。X-Gateway-Node 一致。| 现象 | 排查 |
|---|---|
| 503 用户未绑定节点 | 走了 default-node 兜底仍无绑定?检查 gateway_service.default_node_id 与路由表 |
| 503 绑定节点不可用 | GET /gateway/admin/status 看节点 state;节点恢复后自动放行,映射不变 |
| 502 连接失败 | 节点 base_url / 防火墙 / 后端进程 |
| 登录后仍不粘 | 看日志「登录成功:用户 …绑定候选节点」;检查响应结构是否仍是 Result<LoginVO>(code=200 + data.userId/username) |
| token 全部解析失败 | 网关与后端 sa-token 配置 / Redis database 不一致 |
| 改了管理接口没生效 | 本实例立即生效;其他实例等 10s 对账,或 POST /gateway/admin/reload |
对照设计稿逐条核实后修正的点,均已实现并验证:
app.cors.*),网关再配一层会输出重复
Access-Control-Allow-Origin 被浏览器拒绝。默认透传后端响应;gateway.cors.enabled=true
可打开网关层 CORS,常备的 DedupeResponseHeader 默认过滤器兜底去重。is-concurrent: true,后端实际是 false(单端登录);
网关已逐项复制后端配置,避免键位/行为差异。code=401 引导重新登录;而不是设计稿笼统落入的 503。LoginResponseFilter order 为 -2 而非 10002:NettyWriteResponseFilter(-1)持有自己
的 exchange,order 10002 的装饰器会被整个绕过(响应捕获不到、绑定永不发生)。RefreshRoutesEvent:@PostConstruct 阶段发事件会在启动期拉起
路由基础设施(→ 路由定义仓库 → registry),形成循环依赖导致启动失败。URI 构造器会二次编码已编码的 query(%E4 → %25E4)。用 mock 后端 + 直改库完成端到端验证:
X-Gateway-Node/X-Request-Id 响应头 ✔gateway_user_route 绑定落库 ✔x-token 请求按缓存/路由表粘到绑定节点 ✔单测 23 个(IP 白名单 / 节点轮询 / 登录体解析 / 目标 URI 构造):mvn -pl ai-gateway test。
冒烟工具(不入部署包):src/test/python/mock_backend.py(模拟后端)、
UpdateNode.java / QueryRoutes.java / RestoreNodes.java(直改库的辅助脚本)。