feat: enhance API and session management with Nacos and Redis integration
- Add Nacos registry for service registration and deregistration. - Implement Redis registry for session management with heartbeat and session claiming. - Improve completion service with session handling and request validation. - Enhance WebSocket handling for completion requests with JSON-RPC support. - Add tests for new registry implementations and completion manager functionalities. - Refactor existing code for better readability and maintainability.
This commit is contained in:
186
backend/docs/java-nacos-integration-guide.md
Normal file
186
backend/docs/java-nacos-integration-guide.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# Go LSP Gateway 接入 Java 微服务(Nacos)指南
|
||||
|
||||
## 1. 结论先说
|
||||
|
||||
可以。
|
||||
你完全可以把当前 Go LSP Gateway 注册到 Nacos,然后由 Java 微服务通过服务名发现并转发请求。
|
||||
|
||||
但要注意:
|
||||
- Nacos 解决的是「服务发现」问题。
|
||||
- LSP 场景还需要「会话粘性」问题(同 `sessionId` 要持续命中同一实例)。
|
||||
- 粘性在你现在的实现里由 Redis 会话目录 + `routeTo` 重试机制承担,Nacos 本身不替代这部分。
|
||||
|
||||
## 2. 当前 Go 服务能力(已具备)
|
||||
|
||||
对外接口:
|
||||
- HTTP: `POST /api/v1/completions/{language}`
|
||||
- WS: `GET /ws/completions`、`GET /ws/completions/{language}`
|
||||
- 健康检查:`/health`、`/health/live`、`/health/ready`
|
||||
|
||||
多副本会话能力:
|
||||
- Redis 会话外置(默认 `10.0.0.10:6379`, DB `1`)
|
||||
- 会话归属冲突返回 `409`,并带 `routeTo` + `X-LSP-Route-To`
|
||||
|
||||
## 3. 推荐架构
|
||||
|
||||
1. 前端 -> Java Gateway/BFF -> Go LSP Gateway
|
||||
2. Go LSP Gateway 多副本部署(Nacos 注册)
|
||||
3. Redis 作为会话目录(`language + sessionId -> owner instance`)
|
||||
|
||||
## 4. Nacos 接入方式
|
||||
|
||||
你有两种落地方式:
|
||||
|
||||
1. 由部署系统注册(推荐)
|
||||
指 K8s/运维平台在发布时自动调用 Nacos OpenAPI 注册实例,Go 程序本身不依赖 Nacos SDK。
|
||||
2. Go 进程内注册
|
||||
在 Go 服务启动时使用 `nacos-sdk-go` 注册/心跳/下线。
|
||||
|
||||
你当前明确要求走 SDK,本项目已支持方式 2。
|
||||
|
||||
## 5. Java 侧必须做的事
|
||||
|
||||
## 5.1 统一转发入口
|
||||
|
||||
Java 提供统一接口给前端,例如:
|
||||
- `POST /editor/completions/{language}`
|
||||
|
||||
内部转发到:
|
||||
- `http://{lsp-service}/api/v1/completions/{language}`
|
||||
|
||||
## 5.2 强制透传稳定 sessionId
|
||||
|
||||
请求体必须带稳定 `sessionId`,建议格式:
|
||||
- `tenant:user:project:tab`
|
||||
|
||||
如果 `sessionId` 不稳定,会导致会话频繁重建、补全抖动。
|
||||
|
||||
## 5.3 处理 409 routeTo(关键)
|
||||
|
||||
当 Go 返回 `409` 时:
|
||||
- 读取响应体 `routeTo`(或 header `X-LSP-Route-To`)
|
||||
- Java 侧自动重试一次到 `routeTo`
|
||||
|
||||
这样可以跨实例正确命中会话拥有者。
|
||||
|
||||
## 5.4 透传请求头
|
||||
|
||||
- `X-Request-Id`(链路追踪)
|
||||
- `X-API-Key`(若 Go 配置了 `LSP_API_TOKEN`)
|
||||
|
||||
## 6. Spring Cloud Alibaba(Nacos)示例
|
||||
|
||||
## 6.1 application.yml(服务发现)
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
application:
|
||||
name: editor-bff
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: 10.0.0.20:8848
|
||||
```
|
||||
|
||||
Go 服务在 Nacos 中注册名假设为:`lsp-gateway`
|
||||
|
||||
## 6.2 Go 侧与 Spring 配置映射(SDK 注册)
|
||||
|
||||
你的 Java 配置:
|
||||
|
||||
```yaml
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
enabled: true
|
||||
register-enabled: true
|
||||
server-addr: 10.0.0.10:8848
|
||||
username: nacos
|
||||
password: nacos
|
||||
```
|
||||
|
||||
对应 Go 环境变量:
|
||||
|
||||
- `ENABLE_NACOS_REGISTER=true`
|
||||
- `NACOS_SERVER_ADDR=10.0.0.10:8848`
|
||||
- `NACOS_USERNAME=nacos`
|
||||
- `NACOS_PASSWORD=nacos`
|
||||
- `NACOS_SERVICE_NAME=lsp-gateway`
|
||||
- `NACOS_GROUP=DEFAULT_GROUP`
|
||||
- `NACOS_IP=<当前实例可达IP>`
|
||||
- `NACOS_PORT=8080`(或你的服务端口)
|
||||
|
||||
## 6.3 Gateway 路由示例(HTTP + WS)
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: lsp-http
|
||||
uri: lb://lsp-gateway
|
||||
predicates:
|
||||
- Path=/lsp/api/**
|
||||
filters:
|
||||
- RewritePath=/lsp/api/(?<segment>.*), /api/$\{segment}
|
||||
|
||||
- id: lsp-ws
|
||||
uri: lb:ws://lsp-gateway
|
||||
predicates:
|
||||
- Path=/lsp/ws/**
|
||||
filters:
|
||||
- RewritePath=/lsp/ws/(?<segment>.*), /ws/$\{segment}
|
||||
```
|
||||
|
||||
## 6.4 BFF 转发逻辑(WebClient 伪代码)
|
||||
|
||||
```java
|
||||
Mono<ResponseEntity<String>> proxyCompletion(String language, String body, HttpHeaders headers) {
|
||||
return call("lb://lsp-gateway/api/v1/completions/" + language, body, headers)
|
||||
.flatMap(resp -> {
|
||||
if (resp.getStatusCodeValue() != 409) return Mono.just(resp);
|
||||
|
||||
String routeTo = extractRouteTo(resp); // from body.routeTo or X-LSP-Route-To
|
||||
if (routeTo == null || routeTo.isBlank()) return Mono.just(resp);
|
||||
|
||||
String direct = routeTo + "/api/v1/completions/" + language;
|
||||
return call(direct, body, headers); // retry once
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Go 服务部署参数建议(生产)
|
||||
|
||||
- `ENABLE_REDIS_STICKY=true`
|
||||
- `REDIS_ADDR=10.0.0.10:6379`
|
||||
- `REDIS_DB=1`
|
||||
- `REDIS_PASSWORD=`(空)
|
||||
- `INSTANCE_ID`:每个实例唯一(建议 Pod 名)
|
||||
- `INSTANCE_URL`:实例可回源地址(供 routeTo 使用)
|
||||
- `ENABLE_NACOS_REGISTER=true`
|
||||
- `NACOS_SERVER_ADDR=10.0.0.10:8848`
|
||||
- `NACOS_USERNAME=nacos`
|
||||
- `NACOS_PASSWORD=nacos`
|
||||
- `NACOS_IP`:实例可达内网 IP(不要填 127.0.0.1)
|
||||
- `NACOS_PORT`:实例监听端口
|
||||
- `MAX_SESSIONS`:按机器资源评估(如 200~500)
|
||||
- `SESSION_TTL`:建议 10~30 分钟
|
||||
|
||||
## 8. 常见误区
|
||||
|
||||
1. “用了 Nacos 就不需要 Redis 会话目录”
|
||||
错。Nacos 只告诉你“有哪些实例”,不维护“某会话属于哪台实例”。
|
||||
|
||||
2. “随机 LB 也能跑”
|
||||
能跑但体验会抖,LSP 上下文会丢。
|
||||
|
||||
3. “不传 sessionId 也没关系”
|
||||
错。会话粘性依赖稳定 sessionId。
|
||||
|
||||
## 9. 联调 checklist
|
||||
|
||||
1. Nacos 中能看到 `lsp-gateway` 实例。
|
||||
2. Java 转发 HTTP 可通。
|
||||
3. 同一 `sessionId` 连续请求响应稳定。
|
||||
4. 人为让请求打到非 owner 实例时,Java 能按 `routeTo` 自动重试成功。
|
||||
5. WS 通道可建立,断线重连后会话仍可恢复。
|
||||
94
backend/docs/why-redis-sticky-routing.md
Normal file
94
backend/docs/why-redis-sticky-routing.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# 为什么要做 Redis 会话外置 + 粘性路由
|
||||
|
||||
## 背景
|
||||
|
||||
当前 LSP 网关的核心能力是:
|
||||
- 把编辑器请求转成 LSP(JSON-RPC over stdio)
|
||||
- 为每个 `language + sessionId` 维护一个长生命周期会话(对应语言服务器进程与文档状态)
|
||||
|
||||
单实例时,这套方案天然稳定。
|
||||
一旦进入微服务部署(多副本 + 负载均衡),如果没有额外机制,会出现严重一致性问题。
|
||||
|
||||
## 不做会发生什么
|
||||
|
||||
### 1. 会话状态丢失
|
||||
|
||||
同一个用户会话的请求可能先到实例 A、再到实例 B。
|
||||
但 B 没有 A 的内存态(didOpen/didChange 版本、LSP 上下文),会导致:
|
||||
- 补全质量波动
|
||||
- 诊断/跳转不一致
|
||||
- 重复初始化语言服务器
|
||||
|
||||
### 2. 成本放大
|
||||
|
||||
会话不稳定会触发频繁建进程,带来:
|
||||
- 更高 CPU/内存
|
||||
- 更高请求尾延迟(P95/P99)
|
||||
- 更多瞬时失败
|
||||
|
||||
### 3. 故障不可控
|
||||
|
||||
无统一会话归属时,问题难定位(到底是哪台实例持有上下文、何时丢失)。
|
||||
|
||||
## 为什么要 Redis 会话外置
|
||||
|
||||
Redis 不是替代 LSP 进程,而是做“会话目录(Session Directory)”:
|
||||
- 记录某个 `language + sessionId` 当前归属哪个实例
|
||||
- 记录实例心跳与可回源地址
|
||||
- 给会话绑定 TTL,支持自动过期与回收
|
||||
|
||||
它带来的价值:
|
||||
- 多副本下会话归属可见、可控
|
||||
- 实例重启/扩容/缩容时路由行为可预测
|
||||
- 可以做统一治理(观测、告警、自动修复)
|
||||
|
||||
## 为什么要粘性路由
|
||||
|
||||
LSP 天然是“有状态协议”:同一会话必须持续命中同一实例才能复用上下文。
|
||||
粘性路由的目标就是保证这一点。
|
||||
|
||||
网关当前策略:
|
||||
1. 请求到达后先在 Redis `claim` 会话归属
|
||||
2. 如果归属自己:本地处理
|
||||
3. 如果归属其他实例:返回 `409 + routeTo`(HTTP 头 `X-LSP-Route-To`)
|
||||
4. 上游(前端或 Java 网关)据此重试到目标实例
|
||||
|
||||
这比纯随机 LB 更符合 LSP 工作方式。
|
||||
|
||||
## 设计取舍
|
||||
|
||||
### 收益
|
||||
- 会话一致性显著提升
|
||||
- 进程复用率提高,资源更稳
|
||||
- 故障域清晰,便于排障
|
||||
|
||||
### 代价
|
||||
- 引入 Redis 依赖(网络与可用性要保障)
|
||||
- 路由逻辑更复杂(冲突重试、TTL 管理)
|
||||
- 需要对上游调用方约束:必须稳定传 `sessionId`
|
||||
|
||||
## 什么时候可以不做
|
||||
|
||||
可以暂不启用 Redis/粘性路由的场景:
|
||||
- 仅单实例部署
|
||||
- 开发/演示环境
|
||||
- 对补全一致性不敏感
|
||||
|
||||
但只要进入生产多副本,建议启用。
|
||||
|
||||
## 适配 Java 微服务体系的意义
|
||||
|
||||
将 LSP 网关作为独立微服务后:
|
||||
- Java 业务服务无需关心各语言 LSP 细节
|
||||
- 可以统一接入鉴权、限流、链路追踪
|
||||
- LSP 能力扩展(Go/JS/TS/Java/Python)不会反复侵入业务服务
|
||||
|
||||
这就是把它做成独立 LSP 服务的核心原因:**把状态复杂性收敛在一个可治理的边界里**。
|
||||
|
||||
## 当前实现对应点
|
||||
|
||||
- Redis 默认配置:`10.0.0.10:6379`, `DB=1`, 无密码
|
||||
- 会话注册与认领:`internal/cluster/redis_registry.go`
|
||||
- 会话管理与归属检查:`internal/completion/manager.go`
|
||||
- 路由冲突返回:`internal/api/handler.go`, `internal/api/ws_handler.go`
|
||||
- 启动配置入口:`cmd/server/main.go`
|
||||
Reference in New Issue
Block a user