- 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.
3.1 KiB
3.1 KiB
为什么要做 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 天然是“有状态协议”:同一会话必须持续命中同一实例才能复用上下文。
粘性路由的目标就是保证这一点。
网关当前策略:
- 请求到达后先在 Redis
claim会话归属 - 如果归属自己:本地处理
- 如果归属其他实例:返回
409 + routeTo(HTTP 头X-LSP-Route-To) - 上游(前端或 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