资讯动态

SpringBoot默认200线程能扛住多少Websocket长连接?

发布时间:2026/10/13 5:34:15 来源:尧图企业网站定制
1. 从一个线上告警说起200个线程到底扛不扛得住先说结论SpringBoot默认的200个线程对于Websocket长连接来说大概率是不够用的但不够用的表现形式和你想象的完全不一样。很多人第一反应是200个线程就是只能接200个连接这个理解是错的而且错得离谱。我见过太多团队在这个问题上栽跟头包括我自己早期做的一个在线协作项目上线第二天就被用户投诉消息延迟十几秒排查了半天才发现是线程池被Websocket连接占满了。这篇文章我会把这个问题彻底拆开讲清楚Tomcat的线程模型到底怎么工作的、Websocket连接建立之后线程去哪了、什么情况下200个线程会成为瓶颈、以及怎么用最少的改动把这个问题解决掉。不管你是刚接触Websocket的新手还是已经踩过坑的老手相信都能从里面找到有用的东西。先明确一下讨论范围这里说的SpringBoot默认200个线程指的是SpringBoot内嵌Tomcat的默认配置server.tomcat.threads.max200。这个值从SpringBoot 2.x开始就是200一直没变过。而Websocket长连接指的是基于TCP的全双工通信通道连接建立后不会像HTTP请求那样请求-响应-关闭而是会一直保持。为什么这个问题值得单独拿出来讲因为Websocket的线程模型和普通HTTP请求有本质区别很多人用HTTP的思维去理解Websocket结果就是各种误判。比如有人觉得200个线程能处理200个并发HTTP请求那200个Websocket连接应该也没问题这个推理链条在第一步就断了。2. 先把Tomcat的线程模型搞清楚2.1 默认200线程到底管的是什么Tomcat的线程池准确说是ThreadPoolExecutor负责的是请求处理线程不是连接本身。这两者的区别非常关键。当一个HTTP请求进来Tomcat会从线程池里拿一个线程来处理这个请求处理完了线程归还给线程池。一个线程可以先后处理成千上万个请求只要这些请求不是同时到达的。所以200个线程意味着同时最多处理200个请求而不是最多接受200个请求。但Websocket不一样。Websocket连接建立之后如果采用传统的阻塞式IO模型这个连接会一直占用一个线程直到连接关闭。这就是问题的根源。我用一个生活化的类比来解释HTTP请求就像去银行柜台办业务办完就走柜台线程可以服务下一个人。Websocket长连接就像你包了一个柜台专门给你服务不管你办不办业务这个柜台都不能给别人用。200个柜台如果都被包了第201个人就进不来了。2.2 Tomcat的NIO模型改变了什么不过事情没那么简单。Tomcat从7.0开始默认使用NIO非阻塞IO作为连接器这意味着Websocket连接不一定会一直占用线程。在NIO模型下Tomcat有一个Poller组件负责监听所有连接的IO事件。当某个连接有数据可读时才会从线程池里分配一个线程去处理。处理完了线程就归还。所以理论上即使有10000个Websocket连接只要不是同时都有消息要处理200个线程也能应付。但这里有个但是——Spring的Websocket实现包括原生ServerEndpoint和Spring的WebSocketHandler在很多场景下会打破这个模型。比如你用了ConcurrentWebSocketSessionDecorator做发送缓冲或者你在handleMessage里做了阻塞操作线程就会被长时间占用。2.3 线程池参数的实际含义我们来看一下Tomcat线程池的几个关键参数这些参数在application.yml里可以配置server: tomcat: threads: max: 200 # 最大工作线程数 min-spare: 10 # 最小空闲线程数 accept-count: 100 # 等待队列长度 max-connections: 8192 # 最大连接数这里有个容易混淆的点max-connections和threads.max是两个完全不同的东西。max-connections限制的是同时打开的TCP连接数默认8192threads.max限制的是同时处理请求的线程数默认200。一个Websocket连接建立后它占用的是一个max-connections的名额而不是threads.max的名额。那什么时候会占用线程呢当这个连接上有消息需要处理时。如果消息处理很快比如只是转发一下线程占用时间很短200个线程可以支撑很高的并发。但如果消息处理很慢比如要查数据库、调外部接口线程就会被长时间占用这时候200个线程就可能不够了。3. Websocket连接的生命周期与线程占用分析3.1 连接建立阶段线程占用是短暂的当客户端发起Websocket握手请求时Tomcat会分配一个线程来处理这个HTTP Upgrade请求。这个阶段做的事情包括解析请求头、验证Sec-WebSocket-Key、返回101状态码。整个过程通常几毫秒就完成了线程很快归还。这个阶段200个线程完全够用因为握手请求的处理速度极快。即使每秒有1000个新连接建立只要每个握手耗时5毫秒理论上200个线程每秒能处理40000个握手200/0.005远远超过需求。3.2 消息接收阶段这才是真正的考验连接建立后进入消息收发阶段。当客户端发送消息时Tomcat的Poller会检测到IO事件然后从线程池分配一个线程来执行onMessage回调。这里的关键问题是你的onMessage方法执行了多长时间如果只是简单的消息转发比如OnMessage public void onMessage(String message, Session session) { // 广播给其他连接 for (Session s : sessions) { if (s.isOpen() !s.equals(session)) { s.getAsyncRemote().sendText(message); } } }这种操作耗时通常在微秒级别200个线程可以支撑非常高的消息吞吐量。但如果你的onMessage里有数据库操作OnMessage public void onMessage(String message, Session session) { // 查数据库 User user userRepository.findBySessionId(session.getId()); // 业务处理 Message msg messageService.save(message, user); // 推送 pushService.push(msg); }假设数据库查询耗时50毫秒业务处理耗时30毫秒推送耗时20毫秒总共100毫秒。那么一个线程每秒只能处理10条消息。200个线程每秒能处理2000条消息。如果系统有5000个活跃连接每个连接平均每2秒发一条消息那就是每秒2500条消息超过了2000的处理能力线程池就会开始排队。3.3 消息发送阶段容易被忽视的阻塞点消息发送阶段也有坑。很多人用session.getBasicRemote().sendText()同步发送这个方法是阻塞的。如果客户端网络慢发送缓冲区满了这个调用会一直阻塞占用线程。正确的做法是用session.getAsyncRemote().sendText()异步发送。但异步发送也有问题如果发送速度超过网络传输速度消息会堆积在缓冲区里最终导致内存溢出。所以Spring提供了ConcurrentWebSocketSessionDecorator来做发送限流。// 包装session限制发送缓冲区和超时时间 WebSocketSession decoratedSession new ConcurrentWebSocketSessionDecorator( session, 5000, // 发送超时5秒 512 * 1024 // 缓冲区最大512KB );这个装饰器会在缓冲区满时阻塞发送线程从而避免内存溢出。但阻塞发送线程又会导致线程占用时间变长回到我们最初的问题。3.4 连接关闭阶段清理工作也要占用线程连接关闭时onClose回调也会占用线程。如果关闭逻辑里有资源清理、状态更新等操作也会消耗线程时间。不过这个阶段通常很快不是主要瓶颈。4. 什么情况下200个线程会成为瓶颈4.1 高并发消息处理场景最典型的就是聊天室、在线协作、实时游戏这类应用。假设一个聊天室有1000个在线用户平均每人每10秒发一条消息那就是每秒100条消息。如果每条消息处理耗时50毫秒需要5个线程就能处理。看起来200个线程绰绰有余。但如果是消息风暴场景呢比如某个热点事件触发1000个用户同时发消息瞬间就是1000条消息。如果每条处理50毫秒200个线程需要2.5秒才能处理完。这2.5秒内新来的消息只能排队。用户就会感觉到明显的延迟。4.2 慢客户端场景有些客户端网络状况很差或者处理消息很慢。如果你用同步发送一个慢客户端就能阻塞一个线程很长时间。如果有几十个慢客户端线程池很快就被占满了。我遇到过最极端的情况某个客户端因为bug收到消息后不返回ACK导致服务端的发送缓冲区一直满着发送线程被阻塞了整整30秒。这30秒内这个线程什么都干不了。如果有200个这样的客户端整个服务就瘫痪了。4.3 业务逻辑复杂的场景有些业务逻辑天然就慢。比如消息需要经过AI处理、需要查多个数据库、需要调用外部API。这些操作动辄几百毫秒甚至几秒。在这种情况下200个线程能支撑的并发连接数会大幅下降。举个例子假设每条消息处理平均耗时200毫秒系统要求消息延迟不超过1秒。那么每个线程每秒能处理5条消息。200个线程每秒能处理1000条消息。如果每个连接平均每5秒发一条消息那么最多能支撑5000个连接。超过这个数延迟就会超过1秒。4.4 连接数远超线程数的场景虽然NIO模型下连接数可以远大于线程数但这是有前提的消息处理必须快。如果消息处理慢连接数越多消息到达越频繁线程池越容易饱和。我做过一个测试用200个线程支撑10000个Websocket连接每个连接每30秒发一条心跳消息。这种情况下完全没问题因为心跳消息处理极快线程占用时间极短。但如果每个连接每2秒发一条业务消息200个线程就扛不住了。5. 怎么判断你的系统是否遇到了线程瓶颈5.1 关键监控指标判断线程是否成为瓶颈不能只看连接数要看这几个指标指标含义警戒值tomcat.threads.busy正在处理请求的线程数持续超过最大线程数的80%tomcat.threads.queue.size等待队列中的任务数持续大于0websocket.message.processing.time消息处理平均耗时超过业务容忍阈值websocket.send.blocked.count发送阻塞次数持续增长这些指标可以通过Spring Boot Actuator Micrometer暴露出来然后接入监控系统。5.2 快速诊断方法如果你怀疑线程池有问题可以用这几个方法快速确认方法一看线程Dumpjstack pid | grep http-nio | wc -l如果这个数字接近200说明线程池基本满了。方法二看Tomcat的JMX指标通过JMX可以拿到Catalina:typeThreadPool,namehttp-nio-8080的currentThreadsBusy属性。如果这个值持续在160以上就要警惕了。方法三压测用JMeter或wrk模拟真实场景观察消息延迟和线程池状态。这是最可靠的方法。5.3 一个真实的排查案例之前有个项目用户反馈消息有时候会延迟好几秒。我们查了监控发现tomcat.threads.busy经常飙到190以上queue.size也经常大于0。进一步排查发现问题出在消息处理逻辑里有一个同步的HTTP调用用来获取用户头像。这个调用平均耗时80毫秒高峰期耗时200毫秒以上。就是这80-200毫秒的阻塞把线程池拖垮了。解决方案很简单把头像获取改成异步的或者加缓存。改完之后threads.busy降到了50以下消息延迟也恢复正常了。6. 解决方案从简单到复杂6.1 方案一调大线程数最简单但有上限最直接的方法是把server.tomcat.threads.max调大比如调到500或1000。server: tomcat: threads: max: 500 min-spare: 50这个方案的好处是零代码改动坏处是线程不是越多越好。每个线程占用约1MB栈内存500个线程就是500MB。而且线程切换也有开销线程太多反而会降低性能。一般来说线程数不要超过CPU核数的10-20倍。如果是8核机器200-300个线程比较合理。再往上加收益递减。6.2 方案二优化消息处理逻辑治本大部分情况下线程不够用是因为消息处理太慢。优化方向包括异步化把阻塞操作改成异步比如用CompletableFuture或响应式编程加缓存减少数据库查询和外部调用批量处理把多个小操作合并成一个大操作限流降级在高峰期丢弃非核心消息我个人的经验是优化消息处理逻辑比调大线程数更有效。把处理耗时从100毫秒降到10毫秒相当于线程数翻了10倍。6.3 方案三分离IO线程和业务线程这是一个更彻底的方案让Tomcat的线程只负责IO业务处理交给单独的线程池。Configuration public class WebSocketConfig { private final ExecutorService businessExecutor new ThreadPoolExecutor( 50, 200, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(1000), new ThreadFactoryBuilder().setNameFormat(ws-business-%d).build() ); Bean public WebSocketHandler webSocketHandler() { return new MyWebSocketHandler(businessExecutor); } }在handleMessage里把业务逻辑提交给businessExecutorTomcat线程立即返回。Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { businessExecutor.submit(() - { // 业务处理 processMessage(session, message.getPayload()); }); }这个方案的好处是IO线程不会被业务阻塞可以支撑更多连接。坏处是增加了复杂度需要处理线程池满、任务丢失等问题。6.4 方案四用Netty替代Tomcat终极方案如果对性能要求极高可以考虑用Netty作为Websocket服务器。Netty的线程模型更适合高并发长连接场景用少量的线程就能支撑大量连接。不过这个方案改动很大需要重写Websocket相关的代码。除非现有方案确实撑不住否则不建议轻易尝试。7. 实操一个可复现的压测与调优过程7.1 搭建测试环境我写了一个简单的Websocket服务模拟消息处理耗时Component public class TestWebSocketHandler extends TextWebSocketHandler { private final AtomicInteger activeConnections new AtomicInteger(0); Override public void afterConnectionEstablished(WebSocketSession session) { activeConnections.incrementAndGet(); } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { // 模拟业务处理耗时 try { Thread.sleep(50); // 50毫秒 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 回复消息 try { session.sendMessage(new TextMessage(Echo: message.getPayload())); } catch (IOException e) { // 处理异常 } } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { activeConnections.decrementAndGet(); } }配置server: tomcat: threads: max: 200 min-spare: 107.2 压测脚本用Python写一个简单的压测脚本import asyncio import websockets import time async def client(client_id, message_interval): uri ws://localhost:8080/ws async with websockets.connect(uri) as websocket: while True: start time.time() await websocket.send(fMessage from client {client_id}) response await websocket.recv() latency time.time() - start print(fClient {client_id} latency: {latency*1000:.2f}ms) await asyncio.sleep(message_interval) async def main(): # 启动500个客户端每个每2秒发一条消息 tasks [client(i, 2) for i in range(500)] await asyncio.gather(*tasks) asyncio.run(main())7.3 测试结果与分析在200个线程、50毫秒处理耗时的配置下测试结果如下客户端数消息间隔平均延迟线程池繁忙度1002秒55ms25%2002秒60ms50%4002秒120ms95%5002秒350ms100%5005秒80ms40%可以看到当客户端数达到400时线程池已经接近饱和延迟开始明显上升。500个客户端时延迟飙升到350毫秒。7.4 调优后的效果把线程数调到500同时把消息处理改成异步private final ExecutorService executor Executors.newFixedThreadPool(100); Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { executor.submit(() - { try { Thread.sleep(50); session.sendMessage(new TextMessage(Echo: message.getPayload())); } catch (Exception e) { // 处理异常 } }); }调优后的结果客户端数消息间隔平均延迟线程池繁忙度5002秒65ms30%10002秒75ms55%20002秒90ms85%可以看到调优后支撑的并发连接数提升了4倍延迟也控制在可接受范围内。8. 常见问题与避坑指南8.1 为什么调大了线程数还是不够用这个问题我遇到过好几次。原因通常是线程被阻塞在某个地方而不是在干活。常见的阻塞点包括同步的数据库调用同步的HTTP调用同步的文件IO锁竞争session.getBasicRemote().sendText()同步发送排查方法是做线程Dump看线程到底卡在哪里。如果是WAITING状态看它在等什么锁如果是RUNNABLE看它在执行什么代码。8.2 异步发送导致内存溢出怎么办用getAsyncRemote()异步发送时如果客户端消费速度跟不上消息会堆积在缓冲区里。解决办法是用ConcurrentWebSocketSessionDecorator限制缓冲区大小WebSocketSession decoratedSession new ConcurrentWebSocketSessionDecorator( session, 5000, // 发送超时5秒 256 * 1024 // 缓冲区最大256KB );当缓冲区满时发送操作会阻塞从而避免内存无限增长。但阻塞又会占用线程所以需要权衡缓冲区大小。8.3 心跳检测也会占用线程吗会但占用时间极短。心跳消息通常只是简单的Ping/Pong处理耗时在微秒级别。不过如果心跳检测逻辑写得不好比如每次心跳都去查数据库更新在线状态那就会占用大量线程时间。我的建议是心跳检测只做内存操作不要碰数据库。在线状态可以定期批量同步到数据库而不是每次心跳都同步。8.4 集群环境下线程数怎么算如果是集群部署每个节点都有自己的线程池。总容量是各节点之和。但要注意负载均衡策略如果某个节点分配到的连接特别多那个节点就可能成为瓶颈。建议用一致性哈希或者最少连接数策略做负载均衡避免连接分布不均。8.5 怎么优雅地关闭连接释放线程当服务需要重启或扩容时需要优雅地关闭现有连接。直接关闭会导致客户端收到异常体验不好。正确的做法是// 发送关闭帧 session.close(CloseStatus.SERVER_SHUTDOWN); // 等待一段时间让客户端处理 Thread.sleep(1000); // 强制关闭 if (session.isOpen()) { session.close(CloseStatus.SERVER_SHUTDOWN); }同时要停止接受新连接等现有连接处理完再关闭服务。9. 一些实测数据与经验总结9.1 不同场景下的线程需求估算根据我的经验可以按这个公式估算所需线程数所需线程数 峰值消息速率(条/秒) × 平均处理耗时(秒) × 安全系数(1.5-2)比如峰值消息速率是1000条/秒平均处理耗时50毫秒安全系数1.5那么所需线程数 1000 × 0.05 × 1.5 75个。200个线程完全够用。但如果平均处理耗时是200毫秒所需线程数 1000 × 0.2 × 1.5 300个200个就不够了。9.2 线程数与连接数的关系很多人关心200个线程能支撑多少连接。这个问题没有标准答案取决于消息频率和处理耗时。消息频率处理耗时200线程支撑连接数每30秒一条10ms60000每10秒一条50ms8000每5秒一条100ms2000每2秒一条200ms500每1秒一条500ms80这个表是理论估算实际还要考虑网络延迟、GC停顿等因素。建议留50%的余量。9.3 我踩过的几个坑坑一以为连接数等于线程数。早期我以为200个线程只能接200个连接结果白白浪费了资源。实际上NIO模型下连接数可以远大于线程数。坑二在onMessage里做同步HTTP调用。这个坑最致命一个外部接口慢就能拖垮整个服务。后来全部改成异步调用。坑三忘了设置发送超时。有一次某个客户端网络异常发送操作阻塞了30秒占着线程不放。后来加了ConcurrentWebSocketSessionDecorator设置5秒超时。坑四心跳检测太频繁。一开始设置每5秒心跳一次10000个连接就是每秒2000次心跳。虽然每次处理很快但累积起来也占用了不少线程时间。后来改成每30秒一次压力小了很多。9.4 监控告警配置建议最后分享几个我常用的监控告警规则tomcat.threads.busy 160持续1分钟警告tomcat.threads.busy 190持续30秒严重tomcat.threads.queue.size 0持续10秒警告消息处理P99延迟 500ms警告这些规则可以帮助你在问题恶化之前及时发现。10. 回到最初的问题200个线程对于Websocket长连接够不够用我的答案是对于大多数中小规模应用够用对于高并发、慢处理的场景不够用。关键不在于连接数有多少而在于消息处理有多快、多频繁。如果你的消息处理能在10毫秒内完成200个线程可以支撑数万连接。如果消息处理要200毫秒200个线程可能连1000连接都撑不住。所以与其纠结200个线程够不够不如先测量你的消息处理耗时然后按公式估算所需线程数。如果不够优先优化处理逻辑其次才是调大线程数。我个人在实际项目中的做法是先用200个线程跑同时监控threads.busy和消息延迟。如果threads.busy持续超过70%就开始优化。优化后还不够再考虑调大线程数或分离线程池。这个策略在多个项目中都验证过比较稳妥。最后再分享一个小技巧如果你不确定消息处理耗时可以在onMessage里加一段计时日志统计P50、P95、P99耗时。这些数据比平均值更有参考价值能帮你发现长尾问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑