资讯动态

SpringBoot WebSocket 200线程能支撑多少连接?线程模型与优化实践

发布时间:2026/10/14 22:45:53 来源:尧图企业网站定制
1. 从一个线上告警说起200个线程到底扛不扛得住那天凌晨两点多手机突然震个不停运维群里弹出一连串告警某业务系统的WebSocket推送延迟飙升部分客户端开始批量掉线重连。我爬起来连上服务器一看Tomcat的线程池已经打满日志里全是Thread pool exhausted的报错。当时第一反应是——SpringBoot默认的200个线程对于WebSocket长连接来说到底够不够用这个问题其实困扰过很多人。你随便搜一下答案五花八门有人说“200个线程支持几千个连接没问题”也有人说“我们线上500个连接就开始卡了”。为什么会有这么大的差异因为这个问题本身就不能一概而论它取决于你的连接模型、线程使用方式、业务处理逻辑以及心跳机制的设计。这篇文章我打算把这个问题彻底拆开讲清楚。不管你是刚接触WebSocket的新手还是已经踩过坑的老手都能从中找到对自己有用的东西。我会从SpringBoot的默认线程模型讲起分析WebSocket长连接到底是怎么消耗线程的然后给出不同场景下的容量估算方法最后分享几个我实际项目中用过的优化方案。整篇内容基于我这些年做即时通讯和实时推送系统的经验尽量说人话少堆术语。注意本文讨论的是基于SpringBoot内嵌Tomcat的默认配置场景其他容器如Undertow、Jetty的线程模型有差异后续会另开一篇单独讲。2. 先搞明白SpringBoot默认的200个线程是什么2.1 Tomcat线程池的默认参数从哪来SpringBoot内嵌Tomcat的时候如果你没有手动配置server.tomcat.threads.max那么它用的就是Tomcat的默认值。这个默认值在org.apache.tomcat.util.net.AbstractEndpoint里定义具体是最大线程数maxThreads200最小空闲线程数minSpareThreads10最大连接数maxConnections8192NIO模式下接受队列长度acceptCount100很多人看到200就慌了觉得“才200个线程我1000个WebSocket连接怎么办”。但这里有个关键点被忽略了maxThreads限制的是同时处理请求的线程数不是能建立的连接数。在NIO模式下一个线程可以管理多个连接连接本身不占线程只有需要执行读写操作的时候才会占用线程。这就好比一个餐厅有200个服务员但餐厅里可以坐2000个客人。客人坐着聊天连接空闲的时候不需要服务员盯着只有当客人要下单或者上菜有数据读写的时候服务员才过去服务。所以200个线程能不能撑住取决于你的“客人”有多频繁地“叫服务员”。2.2 WebSocket连接在Tomcat中的线程生命周期WebSocket和普通HTTP请求最大的区别在于HTTP请求是短连接请求处理完线程就释放了WebSocket是长连接连接建立后一直保持着但保持连接本身不占用线程。具体来说一个WebSocket连接的生命周期中线程的占用情况是这样的握手阶段客户端发起HTTP Upgrade请求Tomcat分配一个线程处理握手完成后线程归还池子。这个阶段很快通常几毫秒。空闲阶段连接建立后如果没有数据传输这个连接就静静地待着不占用任何工作线程。NIO的Selector会负责监听事件。消息接收阶段当客户端发来消息Selector检测到读事件从线程池拿一个线程来执行你的onMessage回调。执行完线程归还。消息推送阶段服务端主动推送消息时需要从线程池拿线程来执行写操作。如果推送频繁线程竞争就激烈。心跳阶段如果开启了心跳检测每次心跳的收发都会短暂占用线程。所以你看线程是被“事件”消耗的不是被“连接”消耗的。200个线程能支持多少连接完全取决于你的事件频率和处理耗时。2.3 一个容易混淆的概念maxConnections和maxThreads这两个参数经常被搞混。我用一个表格来对比参数含义默认值作用范围maxConnections最大连接数8192NIO限制同时保持的连接数量maxThreads最大工作线程数200限制同时处理请求的线程数量acceptCount等待队列长度100当线程满了之后新连接排队等待打个比方maxConnections是停车场能停多少辆车maxThreads是收费站有几个收费窗口acceptCount是收费站前面能排多长的队。车停进来不代表要一直占着收费窗口只有进出的时候才需要。实操心得很多人一看到连接数上不去就调maxThreads其实先看看是不是maxConnections或者操作系统的文件描述符限制卡住了。我遇到过好几次折腾半天线程参数最后发现是ulimit -n没改。3. 核心问题拆解什么在消耗你的线程3.1 消息频率是决定性因素假设你有1000个WebSocket连接每个连接每秒收发1条消息每条消息处理耗时5毫秒。那么每秒需要处理的事件数是1000连接 × 2收发× 1条/秒 2000个事件/秒 每个事件占用线程5ms 总线程需求量 2000 × 5ms 10000ms 10秒的线程工作量也就是说每秒需要10个线程满负荷工作才能处理完。200个线程绰绰有余。但如果每个连接每秒收发10条消息呢1000连接 × 2 × 10条/秒 20000个事件/秒 总线程需求量 20000 × 5ms 100000ms 100秒的线程工作量每秒需要100个线程满负荷工作。200个线程开始吃紧因为线程不可能100%利用率还要考虑上下文切换、GC停顿等因素。如果每个连接每秒收发50条消息那200个线程就完全不够了大量事件会在队列里堆积表现为消息延迟越来越大。3.2 消息处理逻辑的耗时上面假设每条消息处理5毫秒这已经算比较快的了。如果你的onMessage回调里有数据库查询、远程调用、复杂计算耗时可能飙升到50毫秒甚至几百毫秒。我见过一个反面案例某开发者在onMessage里直接查数据库每次查询平均20毫秒。500个连接每个每秒5条消息算一下500 × 2 × 5 5000事件/秒 5000 × 20ms 100000ms 100秒线程工作量每秒需要100个线程200个线程的池子看起来还能撑但实际上数据库连接池可能先扛不住了。而且一旦数据库响应变慢线程全部阻塞在IO上整个系统雪崩。注意事项WebSocket的onMessage回调里绝对不要做耗时操作。正确的做法是把消息丢到业务线程池或者消息队列里异步处理onMessage只负责接收和转发。3.3 心跳机制的开销心跳是另一个容易被忽视的线程消耗大户。假设你设置每30秒发一次心跳1000个连接1000连接 / 30秒 33.3个心跳事件/秒 每个心跳处理1ms 线程需求量 33.3 × 1ms 0.033秒这个开销很小可以忽略。但如果心跳间隔设置成1秒呢1000连接 / 1秒 1000个心跳事件/秒 1000 × 1ms 1秒线程工作量每秒需要1个线程专门处理心跳虽然不多但如果心跳处理逻辑复杂比如要查在线状态、更新Redis耗时会增加。更麻烦的是心跳超时检测。如果你用定时任务扫描所有连接判断是否超时1000个连接扫描一遍可能就要几十毫秒如果扫描频率高这个开销也不小。3.4 广播和群发场景的线程爆炸广播是最容易把线程打满的场景。假设一个群有500人你给这个群广播一条消息for (Session session : groupSessions) { session.getBasicRemote().sendText(message); }这段代码在同一个线程里串行发送500条消息。如果每条发送耗时1毫秒总共500毫秒。这500毫秒里这个线程被完全占用。如果同时有10个群在广播就需要10个线程同时工作500毫秒。如果广播频率高比如每秒5次广播每次涉及500人5次/秒 × 500条 2500条消息/秒 2500 × 1ms 2.5秒线程工作量每秒需要2.5个线程专门做广播。看起来不多但广播往往是突发的比如某个热点事件触发全员推送瞬间几万条消息涌出来线程池瞬间被打满。4. 不同场景下的容量估算与实测数据4.1 场景一低频消息的IoT设备监控场景描述5000个设备通过WebSocket上报状态每个设备每10秒上报一次服务端偶尔下发指令。线程消耗计算上报事件5000 / 10秒 500事件/秒 下发事件假设每秒10条 总事件510事件/秒 每条处理耗时2ms纯内存操作 线程需求量510 × 2ms 1.02秒每秒只需要约1个线程200个线程绰绰有余。实测下来这种场景下5000连接CPU占用不到5%线程池活跃线程数长期在5以下。结论200线程完全够用甚至可以把maxThreads调小到50节省资源。4.2 场景二中等频率的IM聊天场景描述2000个在线用户平均每人每5秒发一条消息消息需要持久化到数据库。线程消耗计算消息事件2000 / 5秒 400事件/秒 每条处理耗时15ms含数据库写入 线程需求量400 × 15ms 6秒每秒需要6个线程。200个线程看起来够但要注意数据库连接池的瓶颈。如果数据库连接池只有20个连接那么同时最多20个线程能执行数据库操作其他线程会阻塞等待连接。实测数据我在一个类似场景下压测过2000连接、每秒400消息Tomcat活跃线程数稳定在30-50之间响应延迟P99在50ms以内。但当消息频率翻倍到每秒800条时活跃线程数飙升到120延迟P99突破500ms。结论200线程在中等频率下够用但余量不大建议根据业务增长提前扩容。4.3 场景三高频推送的实时行情场景描述1000个客户端订阅实时行情服务端每秒推送10次更新每次更新涉及100个订阅者。线程消耗计算推送事件10次/秒 × 100订阅者 1000事件/秒 每条处理耗时1ms纯内存序列化发送 线程需求量1000 × 1ms 1秒看起来每秒只需要1个线程但问题在于突发性。行情更新往往是瞬间的10次更新可能集中在100毫秒内完成1000事件 / 0.1秒 10000事件/秒 线程需求量10000 × 1ms 10秒瞬间需要10个线程同时工作。如果行情特别剧烈瞬间推送量翻10倍就需要100个线程。200个线程的池子在这种情况下会非常紧张。实测数据某行情推送系统1000连接、每秒10次推送Tomcat活跃线程数峰值达到80-100CPU占用40%左右。当连接数增加到3000时线程池频繁打满消息开始堆积。结论高频推送场景下200线程只能支持约1000-1500连接需要优化推送模型或增加线程数。4.4 场景四大规模广播的通知系统场景描述10000个在线连接运营人员不定期发送全员广播。线程消耗计算单次广播10000条消息 每条发送耗时0.5ms 单次广播总耗时5000ms 5秒如果用一个线程串行发送需要5秒才能发完。这5秒内这个线程被完全占用。如果同时有多个广播任务线程消耗成倍增加。更严重的是如果广播时使用getBasicRemote()同步发送发送速度受网络影响可能更慢。改用getAsyncRemote()异步发送可以缓解但异步发送底层仍然需要线程来处理写事件。实测数据10000连接的全员广播使用同步发送时单线程耗时约8-10秒期间活跃线程数增加1。如果同时有5个广播任务活跃线程数增加5看起来不多但广播期间其他消息的处理会受到影响因为线程池被广播任务占用了。结论广播场景下200线程本身不是瓶颈但广播任务会挤占正常消息处理的线程资源。建议为广播单独设置线程池或者使用消息队列削峰。5. 线程池打满的典型症状与排查方法5.1 如何判断线程池是否成为瓶颈线程池打满不是一瞬间发生的通常有一些前兆。我整理了一个症状对照表症状可能原因排查方法消息延迟逐渐增大线程池队列堆积监控活跃线程数和队列长度客户端频繁重连心跳超时检查心跳发送是否及时CPU占用高但吞吐低线程上下文切换频繁查看线程dump统计线程状态部分连接无响应线程被慢操作阻塞检查onMessage中是否有耗时操作内存持续增长消息堆积在队列检查线程池队列大小和消息积压量最直接的排查方法是线程dump。用jstack命令导出线程栈然后统计有多少线程处于RUNNABLE、BLOCKED、WAITING状态。如果大量线程处于BLOCKED状态说明有锁竞争如果大量线程处于WAITING状态说明在等待IO或连接池。5.2 一个真实的排查案例某次线上问题WebSocket推送延迟从平均50ms飙升到5秒。我按以下步骤排查查看Tomcat线程池状态通过Actuator端点发现活跃线程数200队列长度100已经满了。导出线程dump发现180个线程阻塞在数据库查询上只有20个线程在处理WebSocket消息。定位代码发现某个新上线的功能在onMessage里同步调用了数据库而且查询没有走索引单次查询耗时200ms。临时解决回滚该功能线程池立即恢复正常。长期解决将该功能改为异步处理消息先入队列由独立线程池消费。这个案例说明线程池打满往往是业务代码问题而不是线程数不够。盲目调大maxThreads只会掩盖问题最终导致更严重的雪崩。实操心得遇到线程池打满先别急着调参数。第一步永远是导出线程dump看看线程到底卡在哪里。十次有八次是代码里有慢操作。5.3 监控指标应该看哪些光靠感觉判断不够需要建立监控。我建议至少监控以下指标tomcat.threads.busy活跃线程数持续接近maxThreads就是危险信号tomcat.threads.config.max最大线程数确认配置生效tomcat.global.request请求总数和耗时分布自定义指标WebSocket连接数、消息收发速率、消息处理耗时P99这些指标可以通过SpringBoot Actuator Micrometer暴露然后接入监控面板。设置告警阈值比如活跃线程数超过150持续1分钟就告警。6. 优化方案让200个线程支撑更多连接6.1 异步化把耗时操作从onMessage中剥离这是最有效的优化手段。onMessage只负责接收消息然后丢到业务线程池或消息队列立即返回。这样WebSocket的工作线程不会被阻塞可以处理更多事件。OnMessage public void onMessage(String message, Session session) { // 只做入队不做业务处理 businessExecutor.submit(() - { try { processMessage(message, session); } catch (Exception e) { log.error(处理消息失败, e); } }); }业务线程池的大小根据业务类型设置。如果是IO密集型可以设置大一些比如CPU核数×2如果是CPU密集型设置为CPU核数1。注意事项异步化之后消息的处理顺序可能发生变化。如果业务对顺序有要求需要按会话ID做哈希保证同一会话的消息由同一个线程处理。6.2 批量发送减少线程占用时间广播场景下逐条发送效率低。可以攒一批消息一次性发送。比如每100毫秒收集一次待发送消息然后批量写出。// 使用ConcurrentLinkedQueue缓存待发送消息 private QueueString sendQueue new ConcurrentLinkedQueue(); // 定时批量发送 Scheduled(fixedDelay 100) public void flushMessages() { ListString batch new ArrayList(); String msg; while ((msg sendQueue.poll()) ! null batch.size() 1000) { batch.add(msg); } if (!batch.isEmpty()) { // 批量发送逻辑 } }批量发送的代价是增加了延迟最多100毫秒但对于行情推送、通知广播这类场景100毫秒的延迟通常可以接受。6.3 调整线程池参数不是越大越好很多人第一反应是把maxThreads从200调到500甚至1000。但线程不是越多越好线程切换有开销每个线程占用约1MB栈内存。1000个线程就是1GB内存而且上下文切换会消耗大量CPU。我的建议是先优化代码异步化、批量发送、减少锁竞争再调参数根据压测结果逐步调整每次增加50观察吞吐量和延迟变化设置合理的队列acceptCount不要太大否则请求排队时间过长客户端早就超时了一个参考配置server: tomcat: threads: max: 300 min-spare: 20 accept-count: 50 max-connections: 100006.4 使用Netty替代Tomcat处理WebSocket如果WebSocket连接数真的很大比如几万、几十万Tomcat的线程模型就不太适合了。Netty基于Reactor模型用少量线程就能管理大量连接更适合长连接场景。不过这个方案改动较大需要把WebSocket服务独立出来SpringBoot只负责HTTP接口。我一般建议连接数超过5000再考虑这个方案否则Tomcat足够用。7. 常见问题速查与避坑指南7.1 为什么连接数上不去问题配置了maxConnections10000但实际只能建立几百个连接。排查思路检查操作系统文件描述符限制ulimit -n默认可能是1024检查Tomcat的maxConnections是否生效检查是否有防火墙或负载均衡器的连接数限制检查客户端是否有连接数限制解决修改/etc/security/limits.conf增加nofile限制。同时检查SpringBoot配置是否正确加载。7.2 心跳超时导致频繁重连问题客户端每隔几分钟就重连一次日志显示心跳超时。原因心跳发送被线程池阻塞没有及时发出。解决心跳发送使用独立的线程池不与业务消息共用心跳间隔不要设置太短建议30-60秒服务端心跳超时时间设置为心跳间隔的2-3倍7.3 消息丢失或乱序问题异步化之后消息处理顺序乱了或者部分消息丢失。原因异步线程池队列满了之后默认策略是抛异常消息被丢弃。解决根据会话ID哈希到固定线程保证同一会话顺序线程池队列使用有界队列拒绝策略改为CallerRunsPolicy由调用线程执行自然降级关键消息增加确认重传机制7.4 内存泄漏Session没有正确关闭问题运行几天后内存持续增长Full GC频繁。原因WebSocket连接关闭后Session对象没有被移除导致内存泄漏。解决在onClose回调中确保移除Session使用ConcurrentHashMap存储Session关闭时及时清理定期扫描超时连接并关闭OnClose public void onClose(Session session) { String sessionId session.getId(); sessionMap.remove(sessionId); // 其他清理逻辑 }实操心得我习惯在Session的userProperties里存一个最后活跃时间然后用一个定时任务每30秒扫描一次关闭超过2分钟没有活跃的连接。这样即使客户端异常断开没有触发onClose也能及时清理。7.5 压测时线程数上不去问题用JMeter压测WebSocket发现服务端活跃线程数很低但客户端已经卡了。原因JMeter本身的WebSocket插件性能有限或者压测机网络带宽不够。解决使用多台压测机分布式压测或者用Gatling等更适合长连接压测的工具检查压测机的文件描述符限制8. 我的实际项目配置参考最后分享一个我在实际项目中用的配置场景是5000个IoT设备连接每台设备每5秒上报一次数据服务端偶尔下发指令。server: tomcat: threads: max: 150 min-spare: 20 accept-count: 100 max-connections: 10000 connection-timeout: 20000业务线程池配置Configuration public class ExecutorConfig { Bean(websocketBusinessExecutor) public ThreadPoolTaskExecutor websocketBusinessExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(20); executor.setMaxPoolSize(50); executor.setQueueCapacity(2000); executor.setThreadNamePrefix(ws-biz-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }这个配置下5000连接稳定运行Tomcat活跃线程数长期在10-30之间业务线程池活跃线程数在5-15之间CPU占用不到10%。偶尔的广播任务会短暂推高线程数但因为有独立的业务线程池不影响WebSocket的收发。关键点在于WebSocket的onMessage只做入队所有业务逻辑在独立线程池处理。这样Tomcat的200个线程实际配置150只需要处理网络事件不会被业务逻辑阻塞。回到最初的问题SpringBoot默认200个线程对于WebSocket长连接够用吗答案是——取决于你的消息频率和处理逻辑。低频场景下200线程能支撑上万连接高频场景下可能1000连接就吃紧。关键不是调大线程数而是理解线程被什么消耗然后针对性地优化。

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

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

免费获取报价 →
↑