ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

实时通信五方案实战指南:WebSocket/SSE/MQTT/短长轮询选型与落地

实时通信五方案实战指南:WebSocket/SSE/MQTT/短长轮询选型与落地 1. 这不是“选哪个好”的选择题而是“在什么场景下必须用哪个”的生存指南后端开发里聊实时通信很多人一上来就问“WebSocket 和 SSE 到底谁更强”“MQTT 是不是比轮询高级”——这种问题本身就已经掉坑里了。我带过七届校招后端新人也给二十多家中小厂做过架构咨询见过太多团队把 WebSocket 当万能胶水登录通知用它、订单状态推用它、AI流式响应也硬塞进去结果上线三天 CPU 暴涨 40%运维半夜打电话让我删代码。真实世界里没有“最优解”只有“最不踩坑的解”。短轮询不是古董它今天还在银行核心系统的对账模块里跑着SSE 不是 WebSocket 的简化版它是浏览器原生支持的、零客户端依赖的流式通道大模型回答逐字渲染就靠它稳稳撑住MQTT 更不是 IoT 专属我们去年在某政务审批平台用它做跨省厅局的异步事件广播吞吐量比 HTTPRedis Pub/Sub 高出 3.2 倍。这五种方案本质是五把不同齿距的扳手短轮询是梅花扳手拧标准螺栓快准稳长轮询是可调扳手适配非标接口但费力SSE 是内六角专攻浏览器端单向流MQTT 是液压扭矩扳手扛得住万台设备并发订阅WebSocket 是套筒组全双工、低延迟但得自己搭好润滑系统心跳、重连、分片。你不会拿液压扳手去拧手机螺丝也不会用梅花扳手去紧风电塔螺栓。本文不讲理论对比只讲我在生产环境里亲手调参、压测、回滚、重写的五次实战——从 Java Spring Boot 到 Python FastAPI从 Windows 本地调试到 Linux 容器集群每个方案都附带可直接粘贴进项目的配置片段、压测数据截图文字还原、以及那个让所有人拍大腿的“原来这里要这样写”的细节。如果你正卡在 AI 流式输出卡顿、IoT 设备掉线率高、或者管理后台实时告警延迟超标这篇就是你的止血绷带。2. 方案设计逻辑为什么这五种方案根本不在同一维度上竞争2.1 短轮询不是技术落后而是“确定性”压倒一切时的理性选择短轮询常被嘲讽为“原始人敲石头”但它的核心价值从来不是性能而是确定性与兼容性。想象一个场景某省社保系统需要每 5 秒检查一次医保结算状态下游对接的是 2008 年上线的 COBOL 主机系统只提供标准 HTTP GET 接口且明确拒绝任何长连接请求。此时 WebSocket 连接建立失败率 92%SSE 因主机不支持 chunked encoding 直接返回 501MQTT 网关防火墙策略禁止非 1883 端口通信。短轮询成了唯一合法路径。它的设计哲学是“用时间换空间用重复换可靠”每次请求都是独立事务超时即放弃不依赖前序状态。我实测过 Spring Boot RestTemplate 实现的短轮询服务在 1000 并发下平均 RT 127ms含 DNS 解析、TCP 握手、TLS 握手错误率 0.03%——这个数字比任何长连接方案在弱网环境下的重连成功率都高。关键参数不是间隔时间而是退避策略。简单设成固定 5 秒是灾难当 1000 个客户端同时发起请求瞬间打满后端线程池。我们采用指数退避 随机抖动基础间隔 3s失败后按 2^n × 3s 递增n 为失败次数并在每次计算后乘以 0.8~1.2 的随机因子。这样既避免雪崩又保证最终一致性。工具链上Java 侧用ScheduledExecutorService而非Scheduled因为后者无法动态调整间隔Python 侧用asyncio.create_task()配合asyncio.sleep()避免阻塞事件循环。注意短轮询的“短”是相对概念30 秒间隔在某些金融清算场景下也算“短”。2.2 长轮询在 HTTP 协议框架内争取“伪长连接”的妥协艺术长轮询的本质是 HTTP 协议的“障眼法”客户端发请求服务端不急着返回而是挂起连接直到有数据或超时才响应。它解决的是“如何在不改协议的前提下降低轮询频率”。但很多人忽略了一个致命细节连接挂起期间服务端资源消耗模式完全不同。短轮询每次请求占用一个线程/协程约 200ms长轮询则可能持续 30 秒以上。这意味着同样 1000 并发短轮询需 1000 个瞬时线程长轮询需 1000 个长期存活的线程/协程。Java Tomcat 默认maxThreads200若不做调整长轮询会直接触发线程池拒绝。我们在线上将maxThreads提至 2000并启用NIO模式但更根本的解法是异步化挂起。Spring Boot 5.x 后推荐用DeferredResult或ResponseBodyEmitter它们不绑定 Servlet 线程而是将请求注册到AsyncContext由业务线程池回调。Python Flask 用stream_with_contextFastAPI 用StreamingResponse配合async def。实测对比Tomcat 同配置下DeferredResult方案支撑 3000 并发无压力而传统Thread.sleep()挂起在 800 并发时就开始排队。另一个隐形陷阱是超时级联客户端设 timeout35s服务端设readTimeout30sNginx 设proxy_read_timeout25s三者不一致会导致连接在中间层被静默断开。我们的规范是所有超时值取最小公约数且服务端超时必须比 Nginx 小 3 秒以上留出 TCP FIN 包传输时间。2.3 SSE浏览器端“开箱即用”的单向流专治大模型流式输出SSEServer-Sent Events常被误认为 WebSocket 的阉割版但它在特定场景下是降维打击。核心优势在于浏览器原生支持、自动重连、文本流解析简单。当你用大模型 API 做流式回答时后端收到data: {token:hello}\n\n这样的 chunk前端只需const eventSource new EventSource(/ai/stream); eventSource.onmessage (e) { console.log(e.data); }——没有握手、没有帧解析、没有心跳维护。我们用 FastAPI 实现 SSE 流关键在StreamingResponse的media_typetext/event-stream和响应体格式每条消息必须以data:开头空行分隔可选id:和event:字段。实测发现Chrome 对 SSE 的默认重连间隔是 3 秒但若服务端返回retry: 5000则下次重连延至 5 秒。这个机制救了我们一命某次模型服务波动连续 3 次返回空流SSE 自动重试用户无感知而 WebSocket 在同样情况下需前端手动ws.close()再new WebSocket()中间出现 1.2 秒空白期。注意事项SSE 仅支持服务端→客户端单向推送且HTTP/1.1 下存在连接数限制Chrome 同域最多 6 个。解决方案是域名分片stream1.example.com、stream2.example.com…或升级到 HTTP/2需 Nginx 1.19 配置http2 on;。安全方面SSE 天然携带 Cookie无需额外鉴权但务必在响应头加Cache-Control: no-cache否则代理服务器可能缓存流式响应。2.4 MQTT为海量设备和事件解耦而生的发布-订阅协议MQTT 不是“另一个 WebSocket”它是为物联网场景深度定制的轻量级发布-订阅协议。其核心设计哲学是极简报文头、QoS 分级、主题树路由。一个 MQTT CONNECT 报文仅 2-5 字节不含 ClientID而 WebSocket 握手需 1KB HTTP 头。我们部署的 AEP 平台接入 12 万台工业传感器若全用 WebSocket单节点需 24GB 内存改用 EMQX MQTT 服务器后同配置下内存占用降至 3.8GB。关键在 QoS 级别选择QoS 0最多一次适合温湿度上报丢一包无妨QoS 1至少一次用于设备指令下发需服务端存储未确认消息QoS 2恰好一次极少用因三次握手机制带来 300ms 延迟。主题设计是灵魂factory/{factoryId}/machine/{machineId}/sensor/{sensorType}这样的层级结构让factory//machine//sensor/temperature可精准匹配所有温度传感器。实操中最大坑是遗嘱消息Will Message配置设备异常断开时MQTT 服务端自动发布预设消息到device/{deviceId}/status主题值为offline。但很多客户端库默认不启用 Will或设置错误的 QoS。我们强制要求所有设备 SDK 初始化时设置will_topicdevice/{id}/status, will_payloadoffline, will_qos1, will_retainTrue。Retain 标志让新订阅者立即收到最新状态避免“先订阅后上线”的状态盲区。2.5 WebSocket全双工低延迟通道但“连接稳定”才是真正的技术难点WebSocket 的价值被严重低估——它不只是“比 HTTP 快”而是提供了真正的全双工字节流通道。这意味着你可以像操作 TCP Socket 一样发送二进制帧、自定义协议、甚至实现私有 RPC。但 90% 的失败案例源于对“连接生命周期”的无知。WebSocket 连接不是“建好就完事”它经历四个阶段建立Handshake、活跃Data Transfer、半关闭Close Initiated、完全关闭Closed。其中半关闭阶段最易出错客户端发FIN包后服务端仍可发送数据但若服务端此时也发FIN就会触发CLOSE_WAIT状态堆积。我们在 Spring Boot 整合 WebSocket 时发现OnClose方法执行缓慢如同步写数据库导致连接卡在半关闭态。解决方案是OnClose中只做轻量操作记录日志、更新内存状态重任务扔进线程池异步处理。另一个致命点是心跳保活。浏览器 WebSocket 默认无心跳Nginx 默认 60 秒断开空闲连接。我们采用双心跳服务端每 30 秒发PING帧客户端收到后立即回PONG同时 Nginx 配置proxy_read_timeout 75;确保比心跳周期长。测试发现单纯依赖WebSocketSession.isOpen()判断连接状态不可靠——网络闪断时该方法仍返回true。必须结合try-catch发送操作try { session.sendMessage(...); } catch (IOException e) { // 连接已断 }。最后WebSocket Subprotocol子协议不是可选项Sec-WebSocket-Protocol: json-rpc能让前后端约定消息格式避免每次解析都做类型判断。3. 实操细节从零搭建可落地的五种方案含完整代码与配置3.1 短轮询Spring Boot 实现带退避策略的健壮轮询服务我们以“订单支付状态轮询”为例后端提供/api/order/{orderId}/status接口。关键不是接口本身而是客户端轮询逻辑的鲁棒性。Java 客户端代码如下public class OrderStatusPoller { private final RestTemplate restTemplate; private final ScheduledExecutorService scheduler; public OrderStatusPoller() { this.restTemplate new RestTemplate(); // 使用有界队列避免任务堆积 this.scheduler Executors.newScheduledThreadPool(5, new ThreadPoolExecutor.CallerRunsPolicy()); } public void startPolling(String orderId) { // 初始间隔 3 秒失败次数计数器 AtomicInteger failCount new AtomicInteger(0); Runnable pollingTask () - { try { ResponseEntityOrderStatus response restTemplate.exchange( http://backend/api/order/{orderId}/status, HttpMethod.GET, null, OrderStatus.class, orderId ); if (response.getBody().getStatus().equals(PAID)) { System.out.println(Order paid!); scheduler.shutdown(); // 成功则停止 return; } // 重置失败计数 failCount.set(0); } catch (Exception e) { int currentFail failCount.incrementAndGet(); // 计算退避时间2^fail * 3s 随机抖动 long baseDelay (long) Math.pow(2, currentFail) * 3000; long jitter (long) (baseDelay * 0.2 * (Math.random() - 0.5)); long delay Math.min(baseDelay jitter, 30000); // 上限 30s System.out.printf(Poll failed %d times, retry in %d ms%n, currentFail, delay); // 下次重试 scheduler.schedule(this::startPolling, delay, TimeUnit.MILLISECONDS); } }; // 首次执行 scheduler.schedule(pollingTask, 0, TimeUnit.MILLISECONDS); } }服务端接口需注意禁用 HTTP 缓存。Spring Boot Controller 添加GetMapping(/api/order/{orderId}/status) public ResponseEntityOrderStatus getStatus(PathVariable String orderId) { HttpHeaders headers new HttpHeaders(); headers.setCacheControl(CacheControl.noCache()); // 关键 return ResponseEntity.ok() .headers(headers) .body(orderService.getStatus(orderId)); }Nginx 配置需显式关闭缓存location /api/order/ { proxy_pass http://backend; proxy_cache_bypass $http_upgrade; proxy_no_cache $http_upgrade; add_header Cache-Control no-store, no-cache, must-revalidate; }压测数据JMeter 2000 线程模拟客户端平均 RT 142ms95% 延迟 200ms错误率 0.017%。对比固定间隔 5s 方案峰值 QPS 降低 63%线程池压力下降 89%。3.2 长轮询FastAPI 异步挂起实现毫秒级响应FastAPI 的StreamingResponse是实现长轮询的利器。我们构建一个“实时通知中心”客户端订阅/notify/{userId}服务端挂起直到新通知到达。from fastapi import FastAPI, Request, Depends from starlette.responses import StreamingResponse import asyncio import json from typing import AsyncGenerator app FastAPI() # 模拟通知队列实际用 Redis Stream 或 Kafka notification_queue asyncio.Queue() app.post(/notify/push) async def push_notification(user_id: str, content: str): await notification_queue.put({user_id: user_id, content: content}) return {status: ok} app.get(/notify/{user_id}) async def long_polling(user_id: str, request: Request): # 设置超时为 30 秒 timeout 30.0 async def event_generator() - AsyncGenerator[str, None]: try: # 等待通知或超时 notification await asyncio.wait_for( notification_queue.get(), timeouttimeout ) # 检查是否为当前用户 if notification[user_id] user_id: yield fdata: {json.dumps(notification)}\n\n else: # 放回队列供其他用户消费 await notification_queue.put(notification) except asyncio.TimeoutError: # 超时返回空消息触发客户端重连 yield data: {\type\:\timeout\}\n\n return StreamingResponse( event_generator(), media_typetext/plain, headers{Cache-Control: no-cache, Connection: keep-alive} )前端 JavaScript 调用let eventId 0; function startLongPolling(userId) { const url /notify/${userId}?t${Date.now()}; const xhr new XMLHttpRequest(); xhr.open(GET, url, true); xhr.timeout 35000; // 客户端超时需比服务端长 xhr.onreadystatechange function() { if (xhr.readyState 4) { if (xhr.status 200) { const data JSON.parse(xhr.responseText.trim()); if (data.type timeout) { // 超时立即重连 startLongPolling(userId); } else { console.log(New notification:, data); // 处理通知 startLongPolling(userId); // 成功后立即发起下一次 } } else { // 错误指数退避重连 setTimeout(() startLongPolling(userId), Math.min(Math.pow(2, eventId) * 1000, 30000)); } } }; xhr.send(); }关键点服务端StreamingResponse必须设置Connection: keep-alive否则 Nginx 会提前关闭连接前端XMLHttpRequest的timeout必须大于服务端超时留出网络传输余量。3.3 SSE用 SSE 实现大模型回答的逐字流式渲染这是当前最热的场景。我们基于 Ollama 的llama3模型用 FastAPI 封装流式 APIfrom fastapi import FastAPI, Request, HTTPException from fastapi.responses import StreamingResponse import asyncio import json import subprocess import shlex app FastAPI() app.get(/ai/stream) async def stream_ai_response(prompt: str, request: Request): # 构建 Ollama 命令生产环境建议用 HTTP API 替代 CLI cmd shlex.split(follama run llama3 {prompt}) async def generate(): try: # 启动子进程 process await asyncio.create_subprocess_exec( *cmd, stdoutasyncio.subprocess.PIPE, stderrasyncio.subprocess.PIPE, limit1024*1024 # 1MB 缓冲区 ) # 逐行读取 stdout while True: line await process.stdout.readline() if not line: break # 解析 Ollama 的 JSONL 输出 try: data json.loads(line.decode(utf-8).strip()) if response in data: # SSE 格式data: {json}\n\n yield fdata: {json.dumps({token: data[response]})}\n\n except json.JSONDecodeError: continue # 等待进程结束 await process.wait() except asyncio.CancelledError: # 客户端取消请求如用户关闭页面 print(Client cancelled connection) # 发送终止信号给子进程 if process and process.returncode is None: process.terminate() await process.wait() raise return StreamingResponse( generate(), media_typetext/event-stream, headers{ Cache-Control: no-cache, Connection: keep-alive, X-Accel-Buffering: no # 关键禁用 Nginx 缓冲 } )Nginx 配置必须添加location /ai/stream { proxy_pass http://fastapi; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_cache_bypass $http_upgrade; # 禁用缓冲确保流式数据实时传递 proxy_buffering off; proxy_buffer_size 4k; proxy_buffers 8 4k; # 关键告诉 Nginx 不要缓冲响应 proxy_max_temp_file_size 0; }前端渲染逻辑const eventSource new EventSource(/ai/stream?prompt encodeURIComponent(prompt)); eventSource.onmessage (e) { const data JSON.parse(e.data); document.getElementById(output).textContent data.token; }; // 处理连接关闭如模型结束 eventSource.addEventListener(end, () { console.log(Stream ended); eventSource.close(); }); // 错误重连SSE 自动重试但需监听 error 事件 eventSource.onerror (e) { console.error(SSE error:, e); // 可在此添加自定义重连逻辑 };实测效果输入 “写一首关于春天的诗”首 token 延迟 1.2s后续 token 间隔 80~120ms全程无卡顿。对比 WebSocket 方案代码量减少 60%且无需处理连接管理。3.4 MQTTEMQX 服务器搭建与 Spring Boot 客户端集成MQTT 服务端我们选用 EMQX开源版因其企业级特性与文档完善度。Windows 下手动部署步骤下载emqx-5.7.2-windows-amd64.zip解压到C:\emqx修改etc\emqx.conf# 启用 Dashboard dashboard.enable true dashboard.listener.http 18083 # 配置 MQTT 监听端口 listener.tcp.external 0.0.0.0:1883 listener.tcp.external.acceptors 64 listener.tcp.external.max_connections 10000 # 启用 WebSocket 监听供浏览器客户端 listener.ws.external 0.0.0.0:8083 listener.ws.external.mqtt_path /mqtt # 认证配置使用 JWT authentication.1.type jwt authentication.1.jwt.secret your-secret-key创建 Windows 服务sc create emqx binPath C:\emqx\bin\emqx.exe start start auto sc start emqx访问http://localhost:18083默认账号admin/adminSpring Boot 客户端集成使用spring-integration-mqttdependency groupIdorg.springframework.integration/groupId artifactIdspring-integration-mqtt/artifactId /dependencyConfiguration EnableIntegration public class MqttConfig { Value(${mqtt.broker-url:tcp://localhost:1883}) private String brokerUrl; Value(${mqtt.client-id:backend-service}) private String clientId; Bean public MqttConnectOptions mqttConnectOptions() { MqttConnectOptions options new MqttConnectOptions(); options.setServerURIs(new String[]{brokerUrl}); options.setClientId(clientId); options.setCleanSession(false); // 保留离线消息 options.setAutomaticReconnect(true); options.setKeepAliveInterval(60); options.setConnectionTimeout(30); // JWT 认证 options.setPassword(your-jwt-token.toCharArray()); return options; } Bean public MqttPahoClientFactory mqttClientFactory() { DefaultMqttPahoClientFactory factory new DefaultMqttPahoClientFactory(); factory.setConnectionOptions(mqttConnectOptions()); return factory; } Bean ServiceActivator(inputChannel mqttOutboundChannel) public MessageHandler mqttOutbound() { MqttMessageHandler handler new MqttMessageHandler(); handler.setAsync(true); handler.setTopicExpression(new LiteralExpression(device/{deviceId}/command)); handler.setQos(1); // 至少一次 handler.setRetained(false); return handler; } }发布消息示例Service public class MqttPublisher { Autowired private MessageChannel mqttOutboundChannel; public void sendCommand(String deviceId, String command) { MessageString message MessageBuilder .withPayload(command) .setHeader(MqttHeaders.TOPIC, device/ deviceId /command) .setHeader(MqttHeaders.QOS, 1) .build(); mqttOutboundChannel.send(message); } }订阅示例监听设备状态Component public class DeviceStatusListener { ServiceActivator(inputChannel mqttInputChannel) public void handleDeviceStatus(Message? message) { String payload new String((byte[]) message.getPayload()); String topic (String) message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC); System.out.println(Received on topic : payload); } }关键配置cleanSessionfalse确保服务端保存订阅关系qos1保证指令不丢失retainedtrue用于状态主题新订阅者立即获取最新值。3.5 WebSocketSpring Boot 实现带心跳与重连的生产级连接Spring Boot 2.6 推荐使用spring-websocketSockJS兼容 IE。核心配置Configuration EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void configureMessageBroker(MessageBrokerRegistry config) { // 启用简单消息代理内存级生产环境建议用 Redis config.enableSimpleBroker(/topic, /queue); config.setApplicationDestinationPrefixes(/app); config.setUserDestinationPrefix(/user); } Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 注册 WebSocket 端点 registry.addEndpoint(/ws) .setAllowedOrigins(*) // 生产环境需指定域名 .withSockJS(); // 启用 SockJS 回退 } }后端控制器Controller public class WebSocketController { MessageMapping(/chat) SendTo(/topic/chat) public ChatMessage handleMessage(ChatMessage message) { // 处理消息 return message; } EventListener public void handleWebSocketConnectEvent(SessionConnectedEvent event) { System.out.println(WebSocket connected: event.getSessionId()); } EventListener public void handleWebSocketDisconnectEvent(SessionDisconnectEvent event) { System.out.println(WebSocket disconnected: event.getSessionId()); // 清理资源 cleanupSession(event.getSessionId()); } private void cleanupSession(String sessionId) { // 异步清理避免阻塞事件线程 CompletableFuture.runAsync(() - { // 删除内存中的会话状态 sessionStore.remove(sessionId); }); } }前端 JavaScript使用 Stomp.jslet stompClient null; function connect() { const socket new SockJS(/ws); stompClient Stomp.over(socket); // 配置心跳 stompClient.heartbeat.outgoing 10000; // 10秒发一次心跳 stompClient.heartbeat.incoming 10000; // 10秒等待一次心跳 const onConnect (frame) { console.log(Connected: frame); stompClient.subscribe(/topic/chat, (message) { console.log(Received: message.body); }); }; const onError (error) { console.error(STOMP error: error); // 触发重连 setTimeout(connect, 5000); }; stompClient.connect({}, onConnect, onError); } // 手动发送心跳备用 function sendHeartbeat() { if (stompClient stompClient.connected) { stompClient.send(/app/heartbeat, {}, JSON.stringify({})); } } // 页面卸载时优雅关闭 window.addEventListener(beforeunload, () { if (stompClient) { stompClient.disconnect(); } }); connect();Nginx WebSocket 代理配置location /ws { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # WebSocket 超时设置 proxy_read_timeout 60; proxy_send_timeout 60; }压测结果使用 Gatling 模拟 5000 WebSocket 连接CPU 使用率 42%内存占用 1.8GB消息延迟 P95 50ms。关键指标连接建立成功率 99.98%断线重连平均耗时 1.2s。4. 实战问题排查那些让你凌晨三点爬起来的典型故障4.1 短轮询Nginx 502 Bad Gateway 的隐藏元凶现象短轮询请求在高并发下大量返回 502后端服务日志无异常。排查过程curl -v http://backend/api/order/123/status正常排除后端问题curl -v http://nginx/api/order/123/status返回 502查看 Nginx error.logupstream prematurely closed connection while reading response header from upstream根源Nginx 默认proxy_read_timeout为 60 秒但后端 Tomcat 的connectionTimeout为 20000ms20秒。当后端处理稍慢如数据库锁Nginx 在 60 秒后主动关闭连接而 Tomcat 仍在写响应导致“prematurely closed”。解决方案统一超时Nginxproxy_read_timeout 25;TomcatconnectionTimeout20000增加缓冲区proxy_buffer_size 128k; proxy_buffers 4 256k;关键添加proxy_ignore_client_abort on;允许客户端断开时后端继续执行4.2 长轮询Tomcat 线程池耗尽的无声杀手现象长轮询接口在 800 并发时响应变慢jstack显示大量WAITING线程。线程堆栈http-nio-8080-exec-123 #123 daemon prio5 os_prio0 tid0x00007f8b4c0a1000 nid0x7a3 waiting on condition [0x00007f8b2d7f9000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836) at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:957) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1281) at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:231) at org.apache.catalina.connector.Request.waitForAsyncTimeout(Request.java:4020)原因DeferredResult虽不占 Servlet 线程但CountDownLatch.await()仍需线程等待回调。默认maxThreads200不足。修复Tomcatserver.xmlExecutor nametomcatThreadPool namePrefixcatalina-exec- maxThreads2000 minSpareThreads100/Spring Bootapplication.ymlserver.tomcat.threads.max2000更优解改用WebFluxMono.delayElement()彻底脱离 Servlet 容器线程模型4.3 SSEChrome 控制台显示“net::ERR_INCOMPLETE_CHUNKED_ENCODING”现象SSE 连接频繁断开控制台报错但服务端无异常日志。根源Nginx 默认启用gzip压缩而 SSE 流式响应被 gzip 缓冲导致 chunked encoding 不完整。验证curl -H Accept-Encoding: gzip http://nginx/ai/stream返回乱码curl -H Accept-Encoding: identity http://nginx/ai/stream正常。解决方案Nginx 配置中禁用 SSE 路径的 gziplocation /ai/stream { gzip off; # 关键 proxy_buffering off; ... }或更精细控制gzip_types text/event-stream;改为gzip_types text/plain;排除text/event-stream4.4 MQTTEMQX 连接数突增 10 倍的“幽灵设备”现象EMQX Dashboard 显示连接数达 50000但实际设备只有 5000 台。排查emqx_ctl clients list | wc -l确认连接数emqx_ctl clients show --clientid device_123查看单个设备连接详情发现同一设备 ID 出现多个连接created_at时间戳相差几
返回列表