实时通信五方案实战指南:WebSocket/SSE/MQTT/短长轮询选型与落地
2026/9/23 13:50:26 网站建设 项目流程

1. 这不是“选哪个好”的选择题,而是“在什么场景下必须用哪个”的生存指南

后端开发里聊实时通信,很多人一上来就问:“WebSocket 和 SSE 到底谁更强?”“MQTT 是不是比轮询高级?”——这种问题本身就已经掉坑里了。我带过七届校招后端新人,也给二十多家中小厂做过架构咨询,见过太多团队把 WebSocket 当万能胶水:登录通知用它、订单状态推用它、AI流式响应也硬塞进去,结果上线三天 CPU 暴涨 40%,运维半夜打电话让我删代码。真实世界里,没有“最优解”,只有“最不踩坑的解”。短轮询不是古董,它今天还在银行核心系统的对账模块里跑着;SSE 不是 WebSocket 的简化版,它是浏览器原生支持的、零客户端依赖的流式通道,大模型回答逐字渲染就靠它稳稳撑住;MQTT 更不是 IoT 专属,我们去年在某政务审批平台用它做跨省厅局的异步事件广播,吞吐量比 HTTP+Redis 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 直接返回 501,MQTT 网关防火墙策略禁止非 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 默认maxThreads=200,若不做调整,长轮询会直接触发线程池拒绝。我们在线上将maxThreads提至 2000,并启用NIO模式,但更根本的解法是异步化挂起。Spring Boot 5.x 后推荐用DeferredResultResponseBodyEmitter,它们不绑定 Servlet 线程,而是将请求注册到AsyncContext,由业务线程池回调。Python Flask 用stream_with_context,FastAPI 用StreamingResponse配合async def。实测对比:Tomcat 同配置下,DeferredResult方案支撑 3000 并发无压力,而传统Thread.sleep()挂起在 800 并发时就开始排队。另一个隐形陷阱是超时级联:客户端设 timeout=35s,服务端设readTimeout=30s,Nginx 设proxy_read_timeout=25s,三者不一致会导致连接在中间层被静默断开。我们的规范是:所有超时值取最小公约数,且服务端超时必须比 Nginx 小 3 秒以上,留出 TCP FIN 包传输时间。

2.3 SSE:浏览器端“开箱即用”的单向流,专治大模型流式输出

SSE(Server-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 流,关键在StreamingResponsemedia_type="text/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.comstream2.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_topic="device/{id}/status", will_payload="offline", will_qos=1, will_retain=True。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 { ResponseEntity<OrderStatus> 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 ResponseEntity<OrderStatus> 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 142ms,95% 延迟 < 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(), timeout=timeout ) # 检查是否为当前用户 if notification["user_id"] == user_id: yield f"data: {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_type="text/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 会提前关闭连接;前端XMLHttpRequesttimeout必须大于服务端超时,留出网络传输余量。

3.3 SSE:用 SSE 实现大模型回答的逐字流式渲染

这是当前最热的场景。我们基于 Ollama 的llama3模型,用 FastAPI 封装流式 API:

from 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(f'ollama run llama3 "{prompt}"') async def generate(): try: # 启动子进程 process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, limit=1024*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 f"data: {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_type="text/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 MQTT:EMQX 服务器搭建与 Spring Boot 客户端集成

MQTT 服务端我们选用 EMQX(开源版),因其企业级特性与文档完善度。Windows 下手动部署步骤:

  1. 下载emqx-5.7.2-windows-amd64.zip,解压到C:\emqx
  2. 修改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
  3. 创建 Windows 服务:
    sc create emqx binPath= "C:\emqx\bin\emqx.exe start" start= auto sc start emqx
  4. 访问http://localhost:18083,默认账号admin/admin

Spring Boot 客户端集成(使用spring-integration-mqtt):

<dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-mqtt</artifactId> </dependency>
@Configuration @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) { Message<String> 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); } }

关键配置:cleanSession=false确保服务端保存订阅关系;qos=1保证指令不丢失;retained=true用于状态主题,新订阅者立即获取最新值。

3.5 WebSocket:Spring Boot 实现带心跳与重连的生产级连接

Spring Boot 2.6+ 推荐使用spring-websocket+SockJS(兼容 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.js):

let 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,后端服务日志无异常。
排查过程:

  1. curl -v http://backend/api/order/123/status正常,排除后端问题
  2. curl -v http://nginx/api/order/123/status返回 502
  3. 查看 Nginx error.log:upstream prematurely closed connection while reading response header from upstream
    根源:Nginx 默认proxy_read_timeout为 60 秒,但后端 Tomcat 的connectionTimeout为 20000ms(20秒)。当后端处理稍慢(如数据库锁),Nginx 在 60 秒后主动关闭连接,而 Tomcat 仍在写响应,导致“prematurely closed”。
    解决方案:
  • 统一超时:Nginxproxy_read_timeout 25;,TomcatconnectionTimeout="20000"
  • 增加缓冲区: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 prio=5 os_prio=0 tid=0x00007f8b4c0a1000 nid=0x7a3 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()仍需线程等待回调。默认maxThreads=200不足。
修复:

  • Tomcatserver.xml<Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="2000" minSpareThreads="100"/>
  • Spring Bootapplication.ymlserver.tomcat.threads.max=2000
  • 更优解:改用WebFlux+Mono.delayElement(),彻底脱离 Servlet 容器线程模型

4.3 SSE:Chrome 控制台显示“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 路径的 gzip:
    location /ai/stream { gzip off; # 关键! proxy_buffering off; ... }
  • 或更精细控制:gzip_types text/event-stream;改为gzip_types text/plain;,排除text/event-stream

4.4 MQTT:EMQX 连接数突增 10 倍的“幽灵设备”

现象:EMQX Dashboard 显示连接数达 50000+,但实际设备只有 5000 台。
排查:

  • emqx_ctl clients list | wc -l确认连接数
  • emqx_ctl clients show --clientid "device_123"查看单个设备连接详情
    发现:同一设备 ID 出现多个连接,created_at时间戳相差几

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询