资讯动态

RPC与WebSocket核心原理及实战排错指南

发布时间:2026/9/16 5:05:23 来源:尧图企业网站定制
在任何分布式系统、微服务架构或者实时应用开发里有两个词几乎绕不开RPC 和 WebSocket。很多人对这两个概念似懂非懂——知道 RPC 是远程调用知道 WebSocket 能推消息但真要让他们解释清楚二者的底层机制、选型依据、以及实际部署中遇到诡异报错时怎么排查往往就卡住了。这篇文章我从头到尾梳理一遍 RPC 与 WebSocket前半部分讲清楚两者的工作原理和核心差异后半部分结合真实场景把联网开发中常见的坑和排错思路一并抖出来。无论你是刚接触后端通信的新手还是已经被线上问题折腾过几轮的开发者应该都能从中找到点有价值的东西。1. RPC让远程调用像本地函数一样自然1.1 RPC 的本质屏蔽网络细节的谎言RPCRemote Procedure Call远程过程调用从设计目标上讲是一个精心设计过的谎言——它让你在写代码时觉得调用一个远程服务上的函数就像调用本地函数一样简单。你不需要关心数据怎么序列化、怎么穿过网络、对方处理完后结果怎么回来这一切都被框架隐藏了。但这句谎言要实现起来并不轻松。一个完整的 RPC 调用链路包含五个关键环节客户端本地发起调用传入参数客户端代理Stub/Proxy将参数序列化为能在网络上传输的字节流通过网络将字节流发送到服务端服务端代理Skeleton接收到请求反序列化还原参数服务端真正执行函数然后把返回值再按反向流程送回客户端你从热词里看到的cannot finish rpc call in 30 seconds: nul这类报错本质上就是这五步链路中某个环节出了问题——可能是网络超时、序列化失败、服务端处理过慢或者干脆是某个异常值没被正确处理。1.2 RPC 与 HTTP、REST 的关系别混为一谈很多初学者最容易搞混的就是 RPC 和 HTTP/REST 的区别。严格来说这两个不是同一个维度上的概念。HTTP 是一种传输协议它定义了数据怎么包装成请求/响应的格式REST 是一种架构风格它利用 HTTP 的 GET/POST/PUT/DELETE 等方法来操作资源而 RPC 是一种调用范式它关注的是我如何像调用本地函数一样调用远程函数。实际工程中RPC 完全可以通过 HTTP 协议来实现比如 JSON-RPC、gRPC基于 HTTP/2就是典型例子。也可以走 TCP 自定义协议比如 Dubbo 默认用的就是 TCP 上的自定义二进制协议。从体验上讲区分它们最直观的方式是看面向的对象REST 面向资源GET /user/123你操作的是用户这个资源RPC 面向方法getUserById(123)你调用的是获取用户这个动作1.3 主流 RPC 框架选型参考这几年的 RPC 框架格局基本稳定但每个框架的特点差别很大框架底层协议序列化方式适用场景gRPCHTTP/2Protobuf跨语言、高性能、双向流Dubbo自定义 TCPHessian/JSONJava 生态、微服务治理Thrift自定义 TCPThrift 二进制多语言支持、性能要求高JSON-RPCHTTP/WebSocketJSON轻量接入、调试方便gRPC 在多语言通信场景有碾压性优势因为 Protobuf 有非常成熟的代码生成工具链一套.proto文件可以生成 Java、Go、Python、C 等几乎所有主流语言的客户端和服务端代码。而且 HTTP/2 支持多路复用、头部压缩性能和连接利用率都比 HTTP/1.1 好很多。Dubbo 在 Java 生态里凭借完善的注册中心、负载均衡、熔断限流能力是很多国内企业微服务架构的首选。它最大的优势不是单个调用的性能而是整套治理能力的完整性。1.4 手写一个最小化 JSON-RPC 调用过程用代码来理解 RPC 是最直观的方式。这里我用 Python 写一个最简化的 JSON-RPC 服务端和客户端不依赖任何框架帮助你理解底层原理# service.py - JSON-RPC 服务端 import json import socket def add(a, b): return a b def to_upper(s): return s.upper() # 方法路由表 methods { add: add, to_upper: to_upper } def handle_request(conn): data conn.recv(1024).decode() if not data: return # 解析 JSON-RPC 请求 req json.loads(data) method req[method] params req[params] # 根据方法名分发 result methods[method](*params) # 构造响应并返回 resp json.dumps({result: result, error: None, id: req[id]}) conn.sendall(resp.encode()) sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind((127.0.0.1, 8080)) sock.listen(5) print(RPC server listening on 8080...) while True: conn, _ sock.accept() handle_request(conn) conn.close()# client.py - JSON-RPC 客户端 import json import socket def rpc_call(method, *params): req json.dumps({method: method, params: list(params), id: 1}) s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((127.0.0.1, 8080)) s.sendall(req.encode()) resp json.loads(s.recv(1024).decode()) s.close() return resp[result] print(rpc_call(add, 1, 2)) # 3 print(rpc_call(to_upper, hello)) # HELLO看到没有整个 RPC 的核心逻辑就这么几件事定义协议格式、维护方法路由表、序列化参数、反序列化结果。gRPC、Dubbo 这些框架做的事情本质上和上面这段代码一模一样只不过它们把序列化、网络传输、服务发现、负载均衡这些能力都做成了可复用的组件。2. WebSocket从一问一答到主动推送的通信革命2.1 为什么有了 HTTP 还需要 WebSocketHTTP 协议从设计之初就是一个无状态的请求-响应模型客户端发起请求服务端返回响应一个来回结束。这种模式对于浏览网页、提交表单完全够用但当你需要做实时应用时——聊天、行情推送、协同编辑、在线游戏——就非常尴尬了。传统方案是轮询客户端每隔几秒发起一次请求模拟实时性。但这种做法的效率低得离谱几秒钟一次空请求占用了大量带宽和服务器资源用户量一上来服务器很容易被打爆。长轮询long polling稍微好一点但它仍然没有跳出客户端发起、服务端响应的框架。WebSocket 的出现解决了两件事真正的全双工通信建立连接后客户端和服务端可以随时向对方发送数据不需要对方批准极低的消息开销HTTP 请求头动辄几百字节WebSocket 的数据帧经过掩码处理后只需要很小的开销打个比方HTTP 就像你给远方的朋友写信每封都要写好收件人、贴邮票、邮寄WebSocket 就像打通了电话连接建立后随时能说话还能同时听对方说。2.2 WebSocket 握手与数据帧格式很多人以为 WebSocket 是全新的协议其实它底层依赖 TCP唯一的 HTTP 关系发生在握手阶段。握手使用 HTTP 协议通过Upgrade头请求协议升级GET /ws HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ Sec-WebSocket-Version: 13服务端收到后会算出一个Sec-WebSocket-Accept响应头返回连接就升级成了 WebSocket。握手后的数据传输用的是一个非常紧凑的帧格式基本结构是FIN1 bit标识这是不是消息的最后一个分片Opcode4 bits标识数据类型文本、二进制、关闭、ping、pongMask bit1 bit标识数据是否被掩码客户端发给服务端的数据必须加掩码Payload length7 bits 或 16 bits 或 64 bits数据长度理解这个帧结构对排查问题很有帮助。比如热词里那个[websocket] onclose, code: 1006, reason:, reconnect: true——1006 表示连接异常关闭没有收到正常的 close 帧数据直接被截断了。这种通常不是业务逻辑的问题而是网络层断连导致的。2.3 常见 WebSocket 库与选型对比不同语言和框架的 WebSocket 库成熟度差异很大踩过坑的人应该深有体会。场景推荐方案理由Node.jsws / Socket.IOws 是底层库性能好Socket.IO 自带重连、房间、广播Pythonwebsocketsasyncio/ FastAPI WebSocket异步支持好和 FastAPI 集成方便JavaSpringSpring WebSocket / NettySpring 生态方便Netty 适合高并发长连接Gogorilla/websocket / nhooyr.io/websocketgorilla 最流行nhooyr 更现代Vue 前端原生 WebSocket / socket.io-client原生即可复杂场景用库封装从热词看到golang websocket语音长连接实现说明很多人已经意识到音频流的场景。语音长连接和普通消息推送有个本质区别持续时间长、数据量大、对延迟敏感。这种场景除了 WebSocket 本身还得考虑流控、心跳保活、音频编码格式协商、断线重连后的状态恢复等一连串问题。2.4 WebSocket 心跳机制为什么必须做WebSocket 连接建立后如果长时间没有数据传输中间的网络设备比如 NAT 路由器、负载均衡器可能会认为连接已空闲直接把它断开。这就是 TCP 层的 idle timeout。为了保持连接存活客户端必须定期发送心跳包。常见的做法是客户端每隔一定时间比如 30 秒发送一个 ping 帧opcode 0x9服务端回复 pong 帧opcode 0xA。如果客户端连续几次没收到 pong就可以判定连接已失效并主动发起重连。gRPC 里也有类似的概念叫作 keepalive ping。搞过后端服务的人应该见过GOAWAY或者RST_STREAM这类错误很多时候不是因为代码有 bug而是因为长连接没配心跳或者心跳间隔和负载均衡的 idle timeout 不匹配连接被中间设备悄悄收掉了。3. 选 RPC 还是 WebSocket按场景做判断3.1 核心差异对照RPC 和 WebSocket 不是互相替代的关系而是解决两类不同问题的工具。我把它们放在一起对比这样你能一眼看清各自的定位维度RPCWebSocket通信模型请求-响应同步为主全双工双向随时收发主要目的服务间函数调用双向实时数据交换序列化Protobuf / JSON / Hessian文本或二进制格式自定义连接模型每次调用一次或复用长连接一个长连接持续复用典型场景微服务内部调用、远程 API聊天、行情推送、协同编辑状态管理无状态为主有状态长连接需维护连接池代表协议gRPC、Dubbo、ThriftRFC 6455简单总结RPC 适合动作语义——我要获取某个数据、我要执行某个操作WebSocket 适合流语义——我要持续接收数据、我要和对方双向实时交互。3.2 面对真实需求的决策路径在我看过的大量项目里选错通信方案的案例不少。有些团队为了追求实时性把 REST 接口改成 WebSocket结果很多接口本质上是请求-响应语义改完之后反而引入了一堆连接管理问题。另外一些团队把服务间所有通信都做成 RPC遇到消息推送场景就笨拙地靠轮询白白浪费了 WebSocket 的能力。我的决策建议是这样的请求-响应、需要服务治理负载均衡、熔断、限流→ 选 RPC浏览器/客户端实时双向交互→ 选 WebSocket服务间需要双向流式通信比如实时推荐、持续结果流→ gRPC 的双向流客户端需要接收服务端主动推送不是频繁请求→ WebSocket3.3 也能混着用JSON-RPC over WebSocket有一个很有意思的组合方案——JSON-RPC over WebSocket。把 RPC 的请求-响应语义跑在 WebSocket 的长连接之上客户端往建立好的 WebSocket 连接发送 JSON-RPC 格式的请求服务端处理完后把响应也通过同一条连接发回来。这种方案在需要长连接 请求响应 服务端推送混合场景特别好用。比如一个在线 IDE客户端需要通过 WebSocket 持续接收终端输出同时还要发请求让服务端执行某些操作就可以统一用 JSON-RPC over WebSocket 来实现。从热词里看到thingsboard下发rpc 子设备下发——ThingsBoard 这类物联网平台在设备通信里确实大量用了 JSON-RPC over WebSocket。IoT 场景天然就是双向的平台要下发指令给设备设备要上报数据给平台同时又不想每次交互都重新建连JSON-RPC over WebSocket 几乎是量身定做。4. 实战中那些绕不开的坑报错排查链路4.1 cannot finish rpc call in 30 seconds 这类超时问题怎么查如果你在工程里遇见过 RPC 调用超时通常会和nul或者某些奇怪的字符混在一起。排查思路是这样的第一步确认超时发生在链路哪一头。在客户端测一次最小调用用小数据量、快接口看是否复现。如果小请求能通大请求超时先怀疑序列化/反序列化环节或者服务端处理耗时。第二步盯住服务端日志。RPC 超时大概率是服务端某个流程卡住了。比如数据库慢查询、第三方接口依赖太慢、线程池耗尽。这里有个经验排查 RPC 超时先看服务端的活跃线程数如果线程池被打满了那进来的请求全部排队等待超时几乎是必然的。第三步检查网络环境。如果你在跨机房、跨地域调用或者服务在不同可用区丢包和延迟波动也会导致超时。可以先用 ping 和 traceroute 看基础网络质量再用 TCPdump 抓包看是不是有大量重传。热词里有一类和 git 相关的 RPC 报错——error: rpc failed; curl 56 gnutls recv error (-9): error decoding the received packet和error: rpc failed; curl 56 schannel: server closed abruptly——这是 git push/pull 时通过 HTTP 协议传输对象数据出现的问题。报错里的 RPC 其实就是 git 内部调用远程仓库服务的机制和普适的 gRPC/Dubbo 在技术上不一样但排查链路是类似的检查网络稳定性、调整 http.postBuffer 大小、或者改用 SSH 传输协议。这个我在实际项目中遇到过多次基本都是网络代理或者 MTU 问题导致的。4.2 WebSocket 1006 异常的典型链路[websocket] onclose, code: 1006是前端开发里遇到最多的报错之一。1006 不是业务代码主动关闭的而是连接异常断开。排查路径如下第一环查看服务端日志通常是服务端主动 kill 了连接或者崩了。如果你用 Node.js 的 ws 库检查是不是有未捕获的异常被迫退出进程用 Spring Boot 的话检查 WebSocket handler 里有没有抛出未处理异常。第二环看负载均衡和网关的配置Nginx 默认对 WebSocket 的 read timeout 是 60 秒如果你的服务端超过 60 秒没有发送数据Nginx 会主动断开连接。需要在 Nginx location 里显式配置proxy_read_timeout 3600s这类参数。这是我见过最多、也最隐蔽的坑——代码完全没问题就是被网关掐了。第三环看客户端网络移动端 APP 在 WiFi 和 4G/5G 之间切换时会断 TCP 连接WebSocket 自然也就断了。这种情况下客户端要自动重连重连逻辑里要有退避策略比如第一次等 1 秒第二次等 2 秒指数递增避免大量客户端同时重连把服务器打爆。还有一个很容易被忽略的点服务端负载高导致的事件循环阻塞。像 Node.js 这种单线程模型如果某个业务 handler 里跑了一个同步的 CPU 密集型任务事件循环被卡住所有 WebSocket 的心跳检测都会超时服务端就误判客户端失联主动踢掉连接。所以服务端一定要保证长连接的处理逻辑是非阻塞的。4.3 浏览器环境 WebSocket 的兼容与安全限制热词里有vue 增加 websocket和chrome 109 websocket 不行我挑重点说几个前端 WebSocket 开发里容易踩的坑混合内容限制HTTPS 页面里连非加密的ws://地址会被浏览器拦截必须用wss://。我在本地开发时经常遇到这个问题——本地是 HTTP 环境没问题部署后页面升级成 HTTPSWebSocket 就连不上了。路由守卫里创建连接Vue 项目如果进入页面时创建 WebSocket离开页面时忘记关闭会积累大量僵尸连接。每次组件销毁前记得在onUnmounted/beforeDestroy钩子里执行socket.close()。断线重连的幂等性重连时服务端如果没做好 session 管理客户端会出现双开——同一个用户建立了两个 WebSocket 连接收到的消息还会重复。服务端在用户重连成功后要把旧连接主动关闭掉。Chrome 版本更新导致的兼容变化这个在嵌入式设备比如跑 Electron 或旧版 WebView 的设备上特别明显。新版浏览器对某些次世代安全标头或协议的处理改变可能导致原来能连的 WebSocket 突然不行了。碰到这类问题先把浏览器自带开发者工具的 WS 面板打开看看握手环节是不是有报错日志再决定是改代码逻辑还是调部署配置。4.4 移动端打包 App 后 WebSocket 连接不上的经典原因热搜词里有一条很典型websocket运行到h5可以连接打包为app连接不了。这个我太熟了几乎每周都有同事来问。H5 能连、App 不能连最可能的原因就是证书信任问题。H5 走系统浏览器继承浏览器的证书库App 打包后如果用的 WebView 组件没有配置信任用户证书而且你的 WebSocket 服务端用的还是自签名证书连接时就会被直接拒绝掉。解决办法是在 App 的网络安全配置里加上对开发环境证书的信任或者在测试环境下放行明文流量。另一个常见原因是权限配置缺失。Android 的 App 打包需要显式声明网络权限另外 Android 9 之后默认禁止明文流量如果你的 WebSocket 地址是ws://而没配置networkSecurityConfig允许明文也会连不上。还有鸿蒙或者部分国产 ROM 上App 的保活策略会杀掉后台长连接。这种问题要在应用层做心跳探活和断线自动重连不是技术上能不能连的问题而是系统策略问题。5. 深入一点连接管理、鉴权与服务端架构设计5.1 WebSocket 鉴权握手时顺手验证Netty、Spring Boot、Node.js 都要做 WebSocket 鉴权不然任何人都能连上你的长连接接口想发什么就发什么安全问题会非常严重。鉴权方式常见的三种通过在 URL query 参数里带 tokenws://example.com/ws?tokenxxx。简单直接但 token 会出现在网关和代理的访问日志里建议如果你用的是短期 token 倒还好用了长期 token 就别这么搞。通过首次消息鉴权连接建立后客户端先发一条{type:auth,token:xxx}服务端校验通过后标记该连接为已认证然后才开始处理业务消息。这种方式灵活可以配合签名算法。通过 Subprotocol 头携带认证信息利用 WebSocket 握手时的Sec-WebSocket-Protocol带上约定的认证数据服务端在握手阶段完成校验失败直接拒绝升级。我个人的建议是能接受token出现在日志里就用第一种否则用第二种。第一种性能最好实现最简单第二种在日志层面更安全但需要维护未认证连接池和已认证连接池两套状态。用 Netty 做 WebSocket 鉴权的核心思路是加一个ChannelInboundHandler来处理握手请求和身份校验在channelRead0里判断连接状态。Spring Boot 的话可以在 WebSocket 的拦截器HandshakeInterceptor里做前置校验。5.2 连接状态管理广播、群组与属性设置热词里有条需求spring boot 好用的 websocket 后端框架 可以广播、群组、设置属性等。这就是典型的不止要一个长连接还要一套房间体系的场景。需求拆解下来其实就是三件事单发给指定连接 ID 发送消息群发给一个群组内所有连接发送消息广播给所有已认证连接发送消息如果只用 Spring 的WebSocketHandler原生 API你需要自己维护一个ConcurrentHashMapString, WebSocketSession来管理连接再维护一个群组映射表。连接一多锁、排序、查询这些问题接踵而来。我更推荐直接上 Spring Integration WebSocket 或者 Spring Messaging STOMP。STOMPSimple Text Oriented Messaging Protocol是一套基于文本的简单消息协议天然支持点对点、广播、订阅。Spring Boot 对 STOMP 支持很成熟能省掉大量重复造轮子的时间。比较标准的做法是用以下这套配置一个TextWebSocketHandler处理消息收发内部维护MapString, WebSocketSession在线连接表根据业务需要维护MapString, SetWebSocketSession群组映射表消息类型通过 JSON 的type字段区分比如chat、notice、system在配置类里加上EnableWebSocketMessageBroker启用消息代理5.3 RPC 服务端的性能瓶颈和扩容思路RPC 服务端的性能瓶颈通常不在网络框架本身而在业务逻辑和 IO。gRPC 框架本身经过大规模验证单机扛几万 QPS 没有太大问题。关键是服务端要避免同步阻塞调用提升并行度。扩容手段无非三种横向伸缩多实例部署注册中心做负载均衡。gRPC 客户端一般都能配合注册中心做服务发现和连接池管理。连接复用一个 TCP 连接承载尽可能多的请求。gRPC 在 HTTP/2 上天然支持多路复用比传统 HTTP/1.1 的队头阻塞好太多。异步化把耗时操作丢到 MQ 或者异步线程池里让 RPC 请求快速返回再通过回调或者状态查询拿到最终结果。具体的坑是gRPC 长连接的连接数是有限制的。很多团队上线 gRPC 后发现连接数爆炸仔细排查发现是每个请求都新建了一个 Channel根本没复用已有连接。正确的做法是每个服务只需要维护一份 Channel 复用池不能让连接随请求无限增长。5.4 心跳保活策略RPC 和 WebSocket 都需要的日常护理不管是 gRPC 还是 WebSocket长连接最怕的不是数据不通而是连接已经死了但双方都不知道。心跳保活是解决这个问题的标准方案。gRPC 有内置的keepalive参数通常要设置这么几个值keepAliveTime 30s // 每30秒发一次 ping keepAliveTimeout 10s // ping 发出后10秒没收到 pong 就断开 keepAliveWithoutCalls true // 即使没有活动请求也发 pingWebSocket 则需要在业务层自己实现 ping/pong 逻辑。前端用原生 API通过直接发送 ping 帧在浏览器层是不完全可控的浏览器没有直接暴露 ping 的 API更实际的做法是在消息负载里模拟心跳客户端定时发{type:heartbeat,timestamp:...}服务端收到后原样或者换一种方式回一个 pong 消息。关键点在于心跳间隔必须大于中间设备的 idle timeout 的一半。比如 Nginx 的proxy_read_timeout是 60 秒你的心跳间隔就要低于 30 秒留足余量。这是我在实际项目中反复确认过的经验——很多所谓早上来了连接就断了的问题就是因为夜间没业务数据连接被中间设备回收了而客户端的心跳间隔没覆盖到。6. 从零搭一套最小可用通信方案我的实操笔记6.1 方案选型与代码骨架最后用一个实际的小案例把整个思路串起来。假设需求是前端 Vue 页面需要实时展示后端推送的任务进度同时前端偶尔要向服务端发起一些 RPC 风格的动作请求比如暂停任务、取消任务。我选择的是 FastAPI 提供 WebSocket 端点内部用 JSON-RPC over WebSocket 来兼容两种通信模式。这样一套协议既满足双向消息实时推送又保留了请求-响应语义。代码骨架如下# server.py import json import asyncio from fastapi import FastAPI, WebSocket app FastAPI() # 模拟任务进度存储 progress {task_1: 0} app.websocket(/ws) async def websocket_endpoint(websocket: WebSocket): await websocket.accept() # 启动一个后台任务推送进度 async def push_progress(): while True: await asyncio.sleep(1) progress[task_1] 10 if progress[task_1] 100: progress[task_1] 0 await websocket.send_json({ type: push, data: {task: task_1, progress: progress[task_1]} }) push_task asyncio.create_task(push_progress()) try: while True: # 接收客户端消息解析 JSON-RPC message await websocket.receive_text() req json.loads(message) # 根据 method 分发 if req[method] pause: # 模拟暂停逻辑 await websocket.send_json({ result: paused, id: req[id] }) elif req[method] cancel: # 模拟取消逻辑 await websocket.send_json({ result: cancelled, id: req[id] }) else: await websocket.send_json({ error: unknown method, id: req[id] }) except Exception: pass finally: push_task.cancel() await websocket.close()// client.jsVue 里可以用类似逻辑 class RpcWebSocket { constructor(url) { this.ws new WebSocket(url) this.id 0 this.pending new Map() // 存储等待响应的请求 this.ws.onmessage (event) { const msg JSON.parse(event.data) if (msg.type push) { // 处理服务端主动推送的消息 this.onPush this.onPush(msg.data) } else if (msg.id ! undefined) { // 处理请求响应 const handler this.pending.get(msg.id) if (handler) { handler(msg) this.pending.delete(msg.id) } } } } // 发起 JSON-RPC 请求返回 Promise call(method, params {}) { const id this.id return new Promise((resolve, reject) { this.pending.set(id, (msg) { if (msg.error) reject(new Error(msg.error)) else resolve(msg.result) }) this.ws.send(JSON.stringify({ method, params, id })) }) } // 设置服务端推送消息的回调 onPush(callback) { this.onPush callback } } // 使用示例 const rpc new RpcWebSocket(ws://localhost:8000/ws) rpc.onPush((data) { console.log(收到进度推送:, data) }) setTimeout(async () { const result await rpc.call(pause, { task: task_1 }) console.log(暂停结果:, result) }, 3000)这套方案我在多个项目里验证过最大的好处是协议统一、逻辑简单——前端一个连接搞定主动推送和请求响应两种模式不需要同时维护 WebSocket 和 HTTP 两套连接。缺点是 JSON-RPC over WebSocket 在性能上不如纯二进制协议但对于业务量不大的内部系统完全够用。6.2 部署上线前必须检查的清单在真实上线前一晚我会按这个清单过一遍能避免 80% 的线上事故Nginx/LB 的proxy_read_timeout和proxy_send_timeout是否调整过适配长连接WebSocket 地址是ws://还是wss://页面到底是 HTTP 还是 HTTPS服务端连接池上限、线程池上限是多少压测过没有心跳间隔和中间设备超时时间是否匹配RPC 客户端 Channel 是否复用了有没有连接数泄漏服务端异常断开时客户端有没有自动重连重连有没有退避策略多实例部署时同一个客户端的连接是否会负载均衡到不同实例实例间的会话状态需不需要共享清单看着简单但每一项背后都对应着一类线上事故。特别是第 6 条没有自动重连的 WebSocket 就像一个没有安全带的赛车手——平时跑得爽撞一次就完了。6.3 关于性能调优的一些经验数字压测过不少 RPC 和 WebSocket 服务整理几个经验值供参考gRPC 单连接多路复用性能瓶颈通常在业务逻辑和 GC框架本身单机到达数万 QPS 是常见的WebSocket 的瓶颈通常在内存——每连接至少分配一个读缓冲区和写缓冲区典型 8KB~64KB一万个连接就是几百 MB 内存心跳消息如果定时同时发出会造成心跳惊群问题——大量连接同时发心跳导致 CPU 瞬时飙高。解决方法是给不同连接加一个随机抖动jitter让心跳发出去的时间分散开服务端给客户端推送瓶颈在 DB 或者消息队列的消费速度不在 WebSocket 框架本身内存方面有一个常用的估算公式单体服务 8GB 内存WebSocket 连接维护实在不算奢侈但如果单连接配上未合理配置的缓冲池和并发任务1 万连接也可能把 JVM 打爆。所以连接数上来以后优先想到的不是换框架而是梳理你的连接对象里到底挂了多少不必要的东西。最后聊一句实在的。RPC 和 WebSocket 这两个东西看起来名词多、框架多、报错多但底层逻辑其实很朴素一个是为了让调用远程方法变得像本地调用一样简单一个是为了让双向实时通信变得可靠高效。掌握了这两个核心意图上面所有这些框架、协议、报错都是围绕这个意图衍生出来的具体工程手段。当你再遇到类似cannot finish rpc call in 30 seconds或者websocket onclose 1006的时候先别急着搜报错原文冷静分析一下报错发生在链路哪一环——往往是普通的技术基础知识就能帮你快速定位的。

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

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

免费获取报价