多个RPC请求同时涌进来,服务器并不是“一次只办一件事”,而是靠“连接接收层分流 + 线程池或协程池并行 + 队列兜底 + 超时熔断保护”这一整套机制把请求切成小块并发推进;真正决定上限的往往不是CPU,而是线程模型和队列策略的选择。
请求从网卡到业务代码,中间被谁接住了
很多人以为并发是业务代码的事,其实请求还没进你的Service,已经被切过好几刀了。
一次RPC调用在服务端的完整路径大致是这样的:
- 网卡与内核:数据包到达网卡,内核协议栈完成TCP握手与数据缓存,唤醒监听在该端口上的进程。
- 接入层:NIO框架(Netty、Java NIO、Go netpoll)通过epoll/kqueue拿到可读事件,把字节流读进堆外缓冲区。
- 解码与分发:按协议(Dubbo、gRPC、HTTP/2)拆帧、反序列化,得到方法名、参数、traceId等元信息。
- 调度:把这次调用投递到业务线程池或协程调度器,这一步是并发的分水岭。
- 业务执行:真正的Service方法跑起来,可能查库、调下游、算逻辑。
- 编码回写:结果序列化后原路写回,释放连接或归还连接池。
可以看到,第2步到第3步通常是单线程或少量IO线程在跑,第4步之后才真正并行,所以业内一个共识是:RPC服务端的并发能力,约等于“IO线程不阻塞 + 业务线程池够用 + 队列不炸”三者相乘的结果,任何一环堵住,前面收得再快也没用。
RPC请求并发处理有哪些常见方案?
不同语言、不同框架的实现细节不一样,但底层无非四种模型,理解了这四种,看任何框架的文档都不会懵。
每请求一线程:最直白,也最容易翻车
来一个请求就new一个线程,处理完销毁,优点是编程模型简单,同步代码写起来顺手;缺点是线程创建销毁开销大,一台机器撑到几千线程时,上下文切换会把CPU吃光,现代框架基本不会把它作为默认选项,只在小工具或测试环境里还能见到。
线程池复用:Java生态的主流答案
Tomcat的maxThreads、Dubbo的dubbo.protocol.threads、gRPC-Java的executor,本质都是固定大小线程池,请求进来排队,空闲线程领任务。
关键参数只有三个:
- 核心线程数与最大线程数:决定并行度上限。
- 队列容量:决定能扛住多少瞬时堆积。
- 拒绝策略:队列满了怎么办,抛异常还是丢给调用方排队。
实操上,用jstack -l <pid> > /tmp/thread.txt能直接看到线程池里有多少线程卡在WAITING,多少在跑业务;配合top -Hp <pid>找出CPU占用最高的线程号,再转成十六进制去dump里搜,能定位到具体哪行代码在拖后腿。
IO多路复用加事件循环:Netty和Node的路子
一个或几个EventLoop线程管成千上万连接,业务逻辑要么在IO线程里快速跑完,要么甩给业务线程池,这种模型的并发连接数极高,但绝不能在IO线程里做阻塞调用,否则一个慢查询能把整条EventLoop堵死。
协程与虚拟线程:把并发成本压到最低
Go的goroutine、Java 21之后的虚拟线程、Kotlin协程,思路一致:用极轻量的执行单元替代OS线程,让“一个请求一个执行流”重新变得便宜,近年来不少公司在网关和RPC框架层做迁移,收益主要体现在高并发下的内存占用和切换开销上,代价是老代码里的synchronized、ThreadLocal要重新审视。
高并发场景下RPC框架线程模型怎么选?
选型没有绝对优劣,看业务形态,下面这张表是常见组合的横向对照,可以直接对着自己的场景找位置。
| 模型 | 典型代表 | 适用场景 | 主要风险 |
|---|---|---|---|
| 线程池 + 阻塞IO | Dubbo默认、Tomcat | 业务逻辑重、依赖多、需要同步语义 | 线程数膨胀、队列积压 |
| IO多路复用 + 业务线程池 | Netty自定义Server | 网关、长连接、高连接数低计算 | IO线程被阻塞 |
| 纯事件循环 | Node.js、部分Go实现 | IO密集、逻辑轻 | CPU密集任务拖垮全局 |
| 协程/虚拟线程 | Go、Java 21+ | 高并发、每请求独立上下文 | 生态兼容、调试复杂 |
判断口径很简单:
- 单次业务耗时主要在等下游(DB、HTTP),线程池模型够用,把线程数调到“QPS × 平均耗时”附近,再留30%余量。
- 连接数上万但每连接流量小,选多路复用。
- 请求量级大、希望代码写得像同步,协程或虚拟线程更舒服。
微服务RPC并发调优实战场景:三个最容易踩的坑
线程池开大就完事?队列才是隐形炸弹
把threads从200调到500,压测QPS没涨,延迟反而抖动,原因往往是队列没设上限,请求全堆在队列里,客户端超时了服务端还在慢慢消费,形成“假忙碌”,建议给队列设明确容量,比如dubbo.protocol.queues=0配合合适的拒绝策略,让压力尽早暴露,而不是憋在后面。
超时配置不匹配,线程被白白占住
客户端超时3秒,服务端下游超时5秒,结果客户端已经放弃,服务端线程还在等DB返回,这类线程泄漏在dump里表现为大量线程卡在socket read,对齐原则:下游超时 < 服务端总超时 < 客户端超时,逐层递减。
连接数与文件描述符,系统层面的天花板
高并发下先看两个数:ss -s看当前连接总数,ulimit -n看进程能打开的文件句柄上限,默认1024在几千QPS下很快就顶到,表现为Too many open files,调整路径是改/etc/security/limits.conf里的nofile,再确认systemd服务单元没有覆盖。
单机RPC并发量上不去怎么办?按这条链路排查
遇到压测上不去、线上毛刺多的情况,按顺序走一遍,基本能找到瓶颈点:
- 看CPU:
top里us高说明计算密集,sy高说明系统调用或切换多,wa高说明IO卡住。 - 看线程:jstack采样三次,对比哪些线程一直停在同一个栈帧。
- 看连接:
ss -s的TIME_WAIT数量是否异常,是否该开tcp_tw_reuse。 - 看下游:RPC耗时拆解里,业务自身耗时占比多少,下游占比多少。
- 看GC:年轻代回收频繁会让延迟毛刺明显,用
jstat -gcutil <pid> 1000观察。
多数情况下,前三步就能锁定问题,真正需要改架构的比例并不高。
写在最后
多个RPC请求的并发处理,本质是把“接收、解码、调度、执行、回写”这条流水线的每一段都做成不堵的样子,线程模型只是其中一环,选对模型、设好队列、对齐超时,比盲目堆线程数有效得多。
关于多个RPC请求并发处理的常见问题
一个RPC请求在服务端会占用几个线程?
在典型的线程池模型里,稳定期占用1个IO线程的时间片加1个业务线程的完整执行周期,如果业务里有异步编排,可能同一请求会跨越多个线程,但任一时刻活跃线程数不超过编排层数。
线程池大小到底怎么估算?
常用口径是“目标QPS × 单请求平均耗时(秒)”得到理论并发数,再乘1.3左右作为线程数,同时用压测验证,若单请求耗时里等下游占比很高,可考虑异步化而不是继续加线程。
RPC并发高的时候,服务端最先撑不住的是哪里?
从大量线上案例看,最先触顶的通常是连接数相关的文件描述符限制和下游依赖的连接池,而不是CPU本身;线程池队列满和下游超时往往是同时出现的信号。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/783150.html


