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:
2026-02-15 17:46:34 +08:00
parent 57afb90bc0
commit 3284ce07c7
22 changed files with 1863 additions and 87 deletions

View 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 AlibabaNacos示例
## 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 通道可建立,断线重连后会话仍可恢复。

View File

@@ -0,0 +1,94 @@
# 为什么要做 Redis 会话外置 + 粘性路由
## 背景
当前 LSP 网关的核心能力是:
- 把编辑器请求转成 LSPJSON-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`