ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WebSocket生产实战:前端实时通信的四大支柱与避坑指南

WebSocket生产实战:前端实时通信的四大支柱与避坑指南 1. 为什么今天还必须啃透 WebSocket——一个前端老手的真实项目复盘我带过的三个团队里每年都有至少两个项目卡在实时通信上一个是物流调度系统司机端位置更新延迟超过3秒客户就投诉一个是在线教育平台老师拖拽PPT时学生端白屏2秒当堂课差评率直接翻倍还有一个是工业设备监控大屏传感器数据每5秒轮询一次后端CPU常年92%以上运维半夜打电话让我“看看是不是前端又在疯狂请求”。最后全靠 WebSocket 一锤定音。这不是炫技是真实业务里活生生的“保命线”。你可能已经背过“WebSocket 是全双工、长连接、低开销”的定义但真正上线时你会发现浏览器兼容性不是文档里写的“支持IE10”那么简单心跳包间隔设成30秒和45秒线上故障率能差出3倍Vue3 的 reactive 对象在 socket 消息回调里直接赋值会丢失响应式甚至 Chrome 109 升级后某个 header 字段的大小写变化会让握手直接失败。这篇内容不讲教科书定义只拆解我过去三年在6个生产环境项目里踩过的坑、验证过的参数、压测过的阈值、以及那些面试官不会问但上线必栽的细节。核心关键词就两个前端、WebSocket。如果你正在做聊天、协作编辑、实时报表、IoT 设备监控、或者任何需要“秒级响应”的功能这篇文章里的配置、代码片段、排查命令你可以直接复制粘贴进项目里跑起来。新手能看懂原理老手能拿到即战力面试前通读一遍高频考点“WebSocket 与 HTTP 区别”“如何处理断线重连”“心跳机制设计”答案全在实操细节里。2. 从协议本质到选型决策为什么不用轮询、SSE非得用 WebSocket2.1 协议层的硬伤HTTP 轮询和 SSE 在真实场景中怎么崩的先说结论轮询Polling是慢性自杀SSE 是单向输血WebSocket 才是双向血管。这不是技术洁癖是业务压力逼出来的选择。去年我们给某银行做的风控大屏要求每200毫秒刷新一次交易流水状态。最初用 AJAX 短轮询后端接口平均响应时间80ms加上网络抖动实际间隔在150-300ms之间浮动。结果呢页面刚加载1分钟Chrome 开发者工具 Network 面板里密密麻麻全是 pending 请求内存占用飙升到1.2GB用户反馈“点按钮没反应”。换成长轮询Long Polling问题更糟——每个请求都要维持 TCP 连接后端 Tomcat 默认最大连接数20020个并发用户就把线程池打满运维报警邮件刷屏。我们做过压测1000并发用户下短轮询 QPS 峰值卡在1200而 WebSocket 轻松扛住8000 QPS连接内存开销只有轮询的1/17。SSEServer-Sent Events看起来优雅EventSourceAPI 简单服务端用text/event-stream头就能推数据。但它有致命缺陷只能服务器推客户端没法主动发消息。风控大屏里有个“手动标记高危交易”功能用户点一下按钮后端必须立刻收到指令并同步更新所有在线终端的状态。SSE 做不到只能额外配一套 WebSocket 或者再开一个 POST 接口架构瞬间变复杂。更隐蔽的坑是缓存SSE 响应头里Cache-Control: no-cache必须严格设置否则 Nginx 或 CDN 可能缓存 event stream导致数据永远不更新。我们曾在线上遇到过 CDN 缓存了3分钟的 SSE 流用户看到的全是旧数据排查了6小时才发现是 CDN 配置漏了proxy_cache_bypass $http_upgrade。2.2 WebSocket 握手过程的魔鬼细节从 HTTP 到 ws:// 的三步生死劫WebSocket 不是凭空建立的它始于一次 HTTP 升级Upgrade请求。这个握手过程藏着最多线上故障。我画个真实抓包流程客户端发起 GET 请求GET /ws/chat HTTP/1.1Host: api.example.comUpgrade: websocket← 关键必须小写websocket大写WebSocket有些旧版 Nginx 直接拒绝Connection: Upgrade← 必须存在且值为UpgradeSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ← Base64 编码的随机字符串服务端要用它生成Sec-WebSocket-AcceptSec-WebSocket-Version: 13← 当前唯一标准版本写成12或14服务端会返回 400服务端校验并响应HTTP/1.1 101 Switching Protocols← 状态码必须是 101不是 200Upgrade: websocket← 必须和客户端一致Connection: Upgrade← 同上Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbKxOo← 服务端把客户端的Sec-WebSocket-Key加上固定字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11SHA-1 哈希后再 Base64 编码。这个计算必须精确少一个字符握手就失败。连接建立后续所有通信走二进制帧不再走 HTTP。线上最常出问题的是第2步。我们有个项目部署在阿里云 SLB 后面SLB 默认会修改Sec-WebSocket-Accept头导致客户端校验失败。解决方案不是改服务端而是在 SLB 控制台开启“WebSocket 支持”开关并确保健康检查用 TCP 而非 HTTP。另一个坑是Origin头如果前端页面域名是https://admin.example.com而 WebSocket 地址写成ws://api.example.com注意是ws不是wss现代浏览器会因跨域拒绝连接。必须用wss://api.example.com且服务端要校验Origin是否在白名单里否则恶意网站能伪造连接。2.3 为什么 Vue/React 项目里不能裸用原生 WebSocket框架层的响应式陷阱原生WebSocketAPI 看似简单const ws new WebSocket(wss://api.example.com/ws); ws.onmessage (event) { const data JSON.parse(event.data); // 更新 state... };但在 Vue3 里如果你直接state.messages.push(data)会发现视图不更新。为什么因为push方法触发不了Proxy的settrapVue 的响应式系统监听不到数组长度变化。必须用state.messages [...state.messages, data]或state.messages state.messages.concat(data)。React 也类似useState的 setter 函数必须传新引用setState(prev [...prev, data])才行。更大的坑在错误处理。ws.onerror事件里你无法获取具体的错误信息event对象是空的。我们曾经在线上遇到WebSocket is closed before the connection is established错误查了两天才发现是 Nginx 的proxy_read_timeout设置成了5秒而服务端启动慢握手超时了。真正的错误日志藏在ws.onclose的event.code和event.reason里。比如code1006表示异常关闭通常是网络问题code4500是我们自定义的“鉴权失败”码。所以健壮的初始化必须这样写class SocketManager { constructor(url) { this.url url; this.reconnectDelay 1000; // 初始重连间隔 this.maxReconnectDelay 30000; // 最大重连间隔 this.reconnectAttempts 0; } connect() { this.ws new WebSocket(this.url); this.ws.onopen () { console.log(WebSocket connected); this.reconnectAttempts 0; // 连接成功重置计数 }; this.ws.onmessage (event) { try { const data JSON.parse(event.data); // 这里分发消息避免在 onmessage 里直接操作 DOM 或 state this.handleMessage(data); } catch (e) { console.error(Invalid JSON in WebSocket message, e); } }; this.ws.onclose (event) { console.warn(WebSocket closed: code${event.code}, reason${event.reason}); if (event.code ! 1000) { // 1000 是正常关闭 this.reconnect(); } }; this.ws.onerror (error) { // 注意这里 error 对象信息极少主要靠 onclose 提供线索 console.error(WebSocket error:, error); }; } }3. 生产环境必配的四大支柱心跳、重连、鉴权、消息队列3.1 心跳机制不是可选项是生存必需品没有心跳你的 WebSocket 连接会在 30-60 秒内被中间代理Nginx、CDN、防火墙静默断开。这不是理论是血泪教训。我们第一个项目没加心跳上线三天后凌晨2点开始陆续有用户报告“消息收不到”监控显示连接数缓慢下降直到早上9点全部掉线。查日志发现 Nginx 的proxy_read_timeout默认是60秒而我们的业务消息间隔可能长达2分钟。心跳方案有两种服务端主动 Ping服务端每30秒发一个{type:ping}消息客户端收到后立即回{type:pong}。优点是服务端可控缺点是增加服务端负担。客户端主动 Ping客户端每25秒发{type:ping}服务端收到后回{type:pong}。强烈推荐此方案因为客户端能感知网络状态且避免服务端被恶意 Ping 淹没。关键参数怎么定我们压测过不同组合客户端 Ping 间隔服务端超时阈值3000 并发连接内存占用断线检测延迟20s30s1.8GB≤30s25s45s1.2GB≤45s30s60s0.9GB≤60s最终选择25s Ping / 45s 超时。理由内存节省33%且45秒内断线能被及时发现用户无感比30秒方案更稳妥。代码实现// 客户端心跳 startHeartbeat() { this.heartbeatTimer setInterval(() { if (this.ws this.ws.readyState WebSocket.OPEN) { try { this.ws.send(JSON.stringify({ type: ping })); this.lastPingTime Date.now(); } catch (e) { console.error(Failed to send ping, e); this.ws.close(); // 主动关闭触发重连 } } }, 25000); // 25秒 // 检查是否超时 this.heartbeatCheckTimer setInterval(() { if (this.ws this.ws.readyState WebSocket.OPEN Date.now() - this.lastPingTime 45000) { // 45秒未收到 pong console.warn(Heartbeat timeout, closing connection); this.ws.close(4500); // 自定义关闭码 } }, 10000); // 每10秒检查一次 } // 处理服务端 pong handleMessage(data) { if (data.type pong) { this.lastPingTime Date.now(); // 更新最后心跳时间 } }3.2 断线重连指数退避不是玄学是数学最优解简单粗暴的setTimeout(connect, 1000)会导致雪崩。1000个用户同时断线第一秒涌进1000个连接请求后端直接503。我们用指数退避Exponential Backoffreconnect() { if (this.reconnectAttempts 5) { console.error(Max reconnect attempts reached); return; } const delay Math.min( Math.pow(2, this.reconnectAttempts) * 1000, // 1s, 2s, 4s, 8s, 16s this.maxReconnectDelay // 上限30秒 ); this.reconnectTimeout setTimeout(() { this.reconnectAttempts; console.log(Reconnecting... attempt ${this.reconnectAttempts}, delay ${delay}ms); this.connect(); }, delay); }为什么是 2 的 n 次方因为网络抖动具有随机性指数增长能让请求在时间上自然分散。我们对比过线性退避每次1秒和指数退避在 500 并发重连测试中指数退避的后端峰值 QPS 是线性的 1/3且连接成功率高 12%。3.3 鉴权Token 不是万能的JWT 过期怎么办WebSocket 握手是 HTTP 请求所以可以在 URL 里传 Tokenwss://api.example.com/ws?tokenxxx。但这是明文传输且 Token 可能泄露在浏览器历史或代理日志里。更安全的做法是在Sec-WebSocket-Protocol头里传加密信息或者用 Cookie需withCredentials: true。我们最终采用Cookie HttpOnly Token方案用户登录后后端 Set-CookieHttpOnly且Secure仅 HTTPSWebSocket 连接时浏览器自动携带 Cookie服务端校验 Cookie 中的 Token 有效性但 JWT 过期怎么办用户开着页面2小时Token 过期了WebSocket 连接会因鉴权失败被关闭。我们设计了一个Token 刷新管道客户端监听onclose若event.code 4001自定义“Token 过期”码则先调用/auth/refresh接口获取新 Token刷新成功后重新初始化 WebSocket 连接刷新失败如 Refresh Token 也过期跳转登录页这个流程必须原子化避免用户看到“连接断开”提示。我们在 Vue 组件onBeforeUnmount里清理定时器onMounted里启动连接确保生命周期干净。3.4 消息队列解决“消息洪峰”与“UI 渲染阻塞”想象一个股票行情页面每秒推送100条价格变动。如果每条消息都触发一次 Vue 的ref更新浏览器渲染线程会被占满页面卡死。解决方案是消息缓冲 批量更新// 消息队列 messageQueue []; queueTimer null; handleMessage(data) { this.messageQueue.push(data); // 启动或重置批量处理定时器 if (!this.queueTimer) { this.queueTimer setTimeout(() { // 批量处理例如合并相同股票的最新价 const grouped this.groupMessages(this.messageQueue); this.updateUI(grouped); // 一次性更新 UI this.messageQueue []; // 清空队列 this.queueTimer null; }, 100); // 100ms 内积攒消息 } } groupMessages(messages) { const map new Map(); messages.forEach(msg { const key ${msg.symbol}_${msg.type}; map.set(key, msg); // 只保留最新一条 }); return Array.from(map.values()); }100ms 是经验值。太短如10ms队列积攒不够效果不明显太长如500ms用户会觉得延迟。我们用 Lighthouse 测试过100ms 批处理后FPS 从 23 稳定到 58完全流畅。4. 实战全流程从零搭建一个 Vue3 WebSocket 聊天室含完整代码4.1 服务端选型与最小可行代码Node.js ws 库不用搞复杂的 Spring Boot一个轻量级 Node.js 服务足够验证。ws库是事实标准比socket.io更贴近协议本质且无额外开销。npm init -y npm install wsserver.jsconst WebSocket require(ws); const http require(http); const url require(url); // 创建 HTTP 服务器用于提供静态文件 const server http.createServer((req, res) { if (req.url /) { res.writeHead(200, { Content-Type: text/html }); res.end( !DOCTYPE html html headtitleWebSocket Chat/title/head body div idmessages/div input idmessageInput placeholderType a message / button onclicksendMessage()Send/button script src/client.js/script /body /html ); } else if (req.url /client.js) { res.writeHead(200, { Content-Type: application/javascript }); res.end(clientCode); } else { res.writeHead(404); res.end(Not Found); } }); // WebSocket 服务器 const wss new WebSocket.Server({ server, path: /ws }); // 存储所有连接 const clients new Map(); wss.on(connection, (ws, req) { const clientId Date.now().toString(36) Math.random().toString(36).substr(2, 5); clients.set(clientId, ws); console.log(Client ${clientId} connected); // 发送欢迎消息 ws.send(JSON.stringify({ type: system, message: Welcome! You are client ${clientId}. Total online: ${clients.size} })); // 广播上线消息 broadcast({ type: system, message: Client ${clientId} joined }, ws); ws.on(message, (data) { try { const msg JSON.parse(data.toString()); if (msg.type chat) { const broadcastMsg { type: chat, from: clientId, content: msg.content, timestamp: new Date().toISOString() }; broadcast(broadcastMsg, ws); } } catch (e) { ws.send(JSON.stringify({ type: error, message: Invalid JSON })); } }); ws.on(close, () { clients.delete(clientId); console.log(Client ${clientId} disconnected); broadcast({ type: system, message: Client ${clientId} left }); }); ws.on(error, (err) { console.error(WebSocket error:, err); }); }); function broadcast(data, excludeWs null) { const payload JSON.stringify(data); clients.forEach((client) { if (client.readyState WebSocket.OPEN client ! excludeWs) { client.send(payload); } }); } // 心跳检查服务端 setInterval(() { clients.forEach((ws, id) { if (ws.isAlive false) { console.log(Client ${id} heartbeat failed, closing); ws.terminate(); clients.delete(id); return; } ws.isAlive false; // 重置标志 ws.ping(); // 发送 ping }); }, 30000); // 每30秒检查 // 处理 ping wss.on(ping, (ws) { ws.isAlive true; }); const PORT process.env.PORT || 3000; server.listen(PORT, () { console.log(Server running on http://localhost:${PORT}); });4.2 前端 Vue3 组合式 API 封装可直接复用创建composables/useWebSocket.jsimport { ref, onMounted, onUnmounted, watch } from vue; export function useWebSocket(url, options {}) { const { onOpen () {}, onMessage () {}, onClose () {}, onError () {}, autoConnect true, protocols [] } options; const ws ref(null); const status ref(connecting); // connecting, open, closing, closed const lastError ref(null); // 心跳相关 const heartbeatTimer ref(null); const heartbeatCheckTimer ref(null); const lastPingTime ref(Date.now()); const connect () { if (ws.value ws.value.readyState WebSocket.OPEN) return; try { ws.value new WebSocket(url, protocols); ws.value.onopen () { status.value open; lastError.value null; onOpen(); startHeartbeat(); }; ws.value.onmessage (event) { try { const data JSON.parse(event.data); onMessage(data); } catch (e) { console.error(Failed to parse WebSocket message, e); } }; ws.value.onclose (event) { status.value closed; onClose(event); if (event.code ! 1000) { // 非正常关闭尝试重连 setTimeout(connect, 3000); } }; ws.value.onerror (error) { status.value closed; lastError.value error; onError(error); }; } catch (e) { lastError.value e; status.value closed; onError(e); } }; const startHeartbeat () { // 清除旧定时器 if (heartbeatTimer.value) clearInterval(heartbeatTimer.value); if (heartbeatCheckTimer.value) clearInterval(heartbeatCheckTimer.value); heartbeatTimer.value setInterval(() { if (ws.value ws.value.readyState WebSocket.OPEN) { try { ws.value.send(JSON.stringify({ type: ping })); lastPingTime.value Date.now(); } catch (e) { console.error(Failed to send ping, e); ws.value?.close(); } } }, 25000); heartbeatCheckTimer.value setInterval(() { if (ws.value ws.value.readyState WebSocket.OPEN Date.now() - lastPingTime.value 45000) { console.warn(Heartbeat timeout); ws.value.close(4500); } }, 10000); }; const sendMessage (data) { if (ws.value ws.value.readyState WebSocket.OPEN) { try { ws.value.send(JSON.stringify(data)); } catch (e) { console.error(Failed to send message, e); } } else { console.warn(WebSocket not ready, message queued); // 可以加消息队列 } }; const close () { if (ws.value) { ws.value.close(); clearInterval(heartbeatTimer.value); clearInterval(heartbeatCheckTimer.value); heartbeatTimer.value null; heartbeatCheckTimer.value null; } }; // 生命周期钩子 onMounted(() { if (autoConnect) { connect(); } }); onUnmounted(() { close(); }); // 暴露 API return { ws, status, lastError, connect, close, sendMessage }; }4.3 Vue3 组件实战聊天室界面与状态管理ChatRoom.vuetemplate div classchat-container div classmessages refmessagesContainer div v-formsg in messages :keymsg.id :class[message, msg.type chat ? (msg.from myId ? own : other) : system] span classtimestamp{{ formatTime(msg.timestamp) }}/span span v-ifmsg.type chat classsender{{ msg.from }}:/span span classcontent{{ msg.content }}/span /div /div div classinput-area input v-modelnewMessage keyup.entersend placeholderType a message... refinputRef / button clicksendSend/button /div /div /template script setup import { ref, onMounted, onUnmounted, nextTick } from vue; import { useWebSocket } from /composables/useWebSocket; const props defineProps({ url: { type: String, required: true } }); const { ws, status, sendMessage } useWebSocket(props.url, { onOpen: () { console.log(WebSocket connected); }, onMessage: (data) { if (data.type chat) { messages.value.push({ id: Date.now(), ...data }); } else if (data.type system) { messages.value.push({ id: Date.now(), type: system, message: data.message }); } }, onClose: (event) { console.log(WebSocket closed, event.code, event.reason); } }); const messages ref([]); const newMessage ref(); const myId ref(user_ Math.random().toString(36).substr(2, 9)); const inputRef ref(null); const messagesContainer ref(null); // 自动滚动到底部 const scrollToBottom () { nextTick(() { if (messagesContainer.value) { messagesContainer.value.scrollTop messagesContainer.value.scrollHeight; } }); }; // 发送消息 const send () { if (newMessage.value.trim()) { sendMessage({ type: chat, content: newMessage.value.trim() }); newMessage.value ; } }; // 输入框聚焦 onMounted(() { if (inputRef.value) { inputRef.value.focus(); } }); // 滚动监听 onMounted(() { scrollToBottom(); }); // 消息变化时滚动 watch(messages, scrollToBottom, { deep: true }); // 格式化时间 const formatTime (isoString) { const date new Date(isoString); return date.toLocaleTimeString([], { hour: 2-digit, minute: 2-digit }); }; /script style scoped .chat-container { display: flex; flex-direction: column; height: 100vh; max-width: 800px; margin: 0 auto; padding: 20px; border: 1px solid #eee; } .messages { flex: 1; overflow-y: auto; padding: 10px; background-color: #f9f9f9; border-radius: 4px; margin-bottom: 10px; } .message { margin-bottom: 8px; padding: 6px 10px; border-radius: 4px; font-size: 14px; line-height: 1.4; } .message.own { background-color: #007bff; color: white; align-self: flex-end; margin-left: auto; } .message.other { background-color: #e9ecef; color: #333; } .message.system { background-color: #6c757d; color: white; text-align: center; font-style: italic; margin: 5px 0; } .timestamp { font-size: 11px; opacity: 0.7; margin-right: 5px; } .sender { font-weight: bold; margin-right: 5px; } .input-area { display: flex; gap: 10px; } .input-area input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } .input-area button { padding: 10px 20px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } /style4.4 部署与 Nginx 配置绕不开的生产环境关卡本地跑通不等于线上可用。Nginx 是 WebSocket 的守门人配置错一个字母就全军覆没。这是经过验证的最小可行配置upstream websocket_backend { server 127.0.0.1:3000; # 你的 Node.js 服务 } server { listen 443 ssl; server_name api.example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; location /ws { proxy_pass http://websocket_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; # 关键必须是小写 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; # 心跳超时必须大于客户端 Ping 间隔 proxy_read_timeout 60; proxy_send_timeout 60; # 缓存关闭 proxy_cache off; } # 其他 API 路由 location /api { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }重点解释proxy_set_header Connection upgrade值必须是小写upgrade大写Upgrade会导致握手失败。proxy_read_timeout 60Nginx 等待后端响应的最大时间必须大于客户端心跳间隔我们用25s Ping所以设60s。proxy_cache off绝对禁止缓存 WebSocket 流。部署后用wscat工具快速验证# 安装 npm install -g wscat # 连接测试 wscat -c wss://api.example.com/ws # 如果看到 Connected说明 Nginx 配置正确 # 然后在另一终端发送消息测试 echo {type:chat,content:hello} | wscat -c wss://api.example.com/ws5. 线上排障黄金法则从 Chrome DevTools 到 Wireshark 的四级诊断5.1 第一级浏览器开发者工具90% 问题在此定位打开 Chrome DevTools → Network → Filter 选WSWebSocket查看连接状态Status列显示101表示握手成功200或400表示失败。检查请求头确认Upgrade: websocket和Connection: Upgrade存在且值正确。查看响应头确认Upgrade: websocket和Connection: Upgrade存在Sec-WebSocket-Accept有值。消息面板点击连接切换到Messages标签能看到所有收发的帧。这是最直观的调试入口。如果消息发不出去看Frames里有没有send记录如果收不到看receive里有没有数据。常见错误码含义CodeMeaning常见原因1000正常关闭ws.close()被调用1001服务端关闭服务端进程退出或崩溃1002协议错误客户端发送了非法帧如非 UTF-8 字符串1003不支持的数据类型发送了 ArrayBuffer 但服务端只接受字符串1006异常关闭网络中断、代理断开、Nginx 超时4001自定义Token 过期我们约定的业务码5.2 第二级服务端日志与连接数监控在 Node.js 服务里加详细日志wss.on(connection, (ws, req) { const ip req.socket.remoteAddress; const userAgent req.headers[user-agent]; console.log([CONNECT] IP: ${ip}, UA: ${userAgent}, Path: ${req.url}); ws.on(close, (code, reason) { console.log([CLOSE] Code: ${code}, Reason: ${reason}, IP: ${ip}); }); ws.on(error, (err) { console.error([ERROR] IP: ${ip}, Error:, err); }); });用netstat查看连接数# 查看 ESTABLISHED 连接数 netstat -an | grep :3000 | grep ESTABLISHED | wc -l # 查看 TIME_WAIT 连接过多说明连接释放慢 netstat -an | grep :3000 | grep TIME_WAIT | wc -l如果TIME_WAIT过多1000需要调优内核参数# /etc/sysctl.conf net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_fin_timeout 305.3 第三级Nginx 日志分析定位代理层问题Nginx 错误日志是宝藏# nginx.conf error_log /var/log/nginx/websocket_error.log warn;常见错误upstream
返回列表