Spring Boot 2.x WebSocket性能瓶颈主要在连接管理、序列化和容器线程模型三处:默认Tomcat的max-connections(8192)和max-threads(200)构成硬性天花板,需显式调优;Jackson反射序列化占延迟40%以上,应预热ObjectMapper并禁用运行时反射;Tomcat实现下sendMessage为同步阻塞,须改用异步重载并配Netty心跳机制。

Spring Boot 2.x 的 WebSocket 性能瓶颈,90% 出现在连接管理、序列化和容器线程模型三处,不是代码写得不够“优雅”,而是默认配置在高并发下根本扛不住。
WebSocket 连接数上不去?先查 Tomcat max-connections 和 max-threads
Spring Boot 2.x 默认内嵌 Tomcat,其 max-connections(默认 8192)和 max-threads(默认 200)是硬性天花板。当并发连接数接近或超过 max-connections,新连接会被直接拒绝,客户端报 ERR_CONNECTION_REFUSED 或握手超时;若连接数未满但消息处理慢,max-threads 耗尽会导致后续 HTTP 管理接口(如 Actuator)也卡死。
- 必须显式配置:在
application.yml中设server.tomcat.max-connections: 20000和server.tomcat.max-threads: 500(按 8 核服务器推荐) -
server.tomcat.accept-count建议设为0:不排队,立刻拒绝,避免请求在队列里堆积放大延迟 - 别只改 Spring 配置——Linux 内核参数也要同步调:
net.core.somaxconn至少设为 65535,否则 Tomcat 配再大也无效
消息吞吐卡在 300 QPS?检查 JSON 序列化与 Session 存储方式
实测表明,在 1KB 消息体、500 并发连接下,Jackson 默认 ObjectMapper 序列化耗时占端到端延迟的 40% 以上;同时用 ConcurrentHashMap 直接存 WebSocketSession 对象,会在高并发 afterConnectionEstablished 时引发锁争用,CPU 花费在 Unsafe.park 上。
- 序列化层:禁用 Jackson 的运行时反射,启用
ObjectMapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL),并预热ObjectWriter - 会话管理:不要把
WebSocketSession放进全局 Map 后裸写 —— 改用带分段锁的Striped<lock></lock>(Guava)或直接切到CopyOnWriteArrayList+ ID 查表(适合读多写少) - 避免在
handleMessage中做阻塞操作(如同步 DB 查询),否则一个慢连接拖垮整个 EventLoop 线程
心跳超时频繁断连?Netty 层 idleStateHandler 配置被 Spring 封装掩盖
Spring Boot 2.x 的 spring-boot-starter-websocket 默认使用 Tomcat WebSocket 实现,不暴露 Netty 的 IdleStateHandler。一旦网络中间件(如 Nginx、SLB)设置了 60 秒空闲超时,而服务端没主动发 ping,连接就会被静默关闭,现象是客户端反复触发 onclose 但无错误码。
- 必须切换到底层 Netty:引入
netty-websocket-spring-boot-starter(如org.yeauty:netty-websocket-spring-boot-starter:0.9.5),它支持在@OnOpen方法里拿到Channel - 手动添加心跳:在连接建立后执行
channel.pipeline().addFirst(new IdleStateHandler(30, 0, 0)),再注册HeartbeatHandler发送PingWebSocketFrame - 注意:Tomcat 实现无法控制底层 socket 的 SO_KEEPALIVE 行为,Netty 可设
channel.config().setOption(ChannelOption.SO_KEEPALIVE, true)
升级后 CPU 占用飙升?检查 WebSocketHandler 的线程安全边界
很多升级者把原有 TextWebSocketHandler 直接复用,却忽略了 Spring Boot 2.x 中 WebSocketSession 的 sendMessage 方法在 Tomcat 实现下是**同步阻塞**的——如果客户端网络差或接收缓冲区满,该线程会一直卡在 write() 系统调用上,导致线程池迅速耗尽。
- 务必用
session.sendMessage(new TextMessage(payload), callback)的异步重载,提供SendHandler处理成功/失败 - 不要在
handleMessage里调session.getPrincipal()或session.getAttributes():这些方法在某些容器下会触发同步 session 加载,加锁开销极大 - 日志打点避开
session.getId()在高频路径中调用——它内部有 volatile 读+字符串拼接,压测时可观测到 5% 的 CPU 消耗来自此处
真正卡住 WebSocket 升级的,从来不是“怎么写”,而是“默认配置在哪一层失效”。Tomcat 的连接数、Jackson 的反射、Spring 封装对 Netty 的屏蔽、以及 sendMessage 的同步陷阱——这四点漏掉任意一个,性能都稳稳卡在 300 QPS 上下,且问题现象高度相似:连接数上不去、延迟忽高忽低、CPU 使用率和 GC 频率不成比例地跳升。



















