并发控制原理与设计

接入与开发

讲清并发控制的核心原理与常见设计算法,给后端和接口开发者一套可落地的判断标准与排查顺序。

服务一上量,最先出问题的往往不是模型而是并发:队列堆积、超时雪崩、限流误伤,全靠拍脑袋加机器解决不了。这篇不聊协议也不聊客户端,只把并发控制这件事拆开——控制什么、用哪些算法、边界在哪、出问题按什么顺序查,让你在设计接口和排队层时知道自己在做哪种取舍。数字口径一律以站内页面为准。

服务一上量,最先炸的往往不是模型本身,而是并发:队列堆成山、超时连锁、限流误伤正常用户。加机器能顶一阵,但顶不住设计上的漏洞。这篇不聊协议、不聊客户端选型,只把并发控制拆开看:你到底在控什么、几种算法各自适合什么流量形状、排队和拒绝的分界线在哪、重试怎么才不反噬自己、最后怎么用指标验证设计生效。你在设计排队层或接口层时,照着这份清单对一遍,基本能知道自己在做哪种取舍。

需要说明的是,本文不写死任何具体阈值。不同业务的承压能力差得远,凡是涉及延迟基线、配额上限、错误率拐点这类数字,都到站内对应页面看当时的真实口径再定。照抄别人的数字,通常会在你这条链路上翻车。

先分清你到底在控制什么

很多人一上来就说「我要做限流」,但并发控制至少有四层对象,混着谈必然打架:连接数、在飞请求数、下游处理槽位、后端账号配额。这四层各自的上限来源完全不同,有的是你自己进程扛不住,有的是下游数据库只能吃这么多,有的是上游服务商给你的硬配额。先定位瓶颈在哪一层,再决定限哪一层。

控制对象上限通常来自限它的目的限错了会怎样
连接数本机 fd、内存、握手成本防止资源被连接吃光限太死会让健康请求排队握手
在飞请求数下游并发处理能力保护脆弱下游不被打穿下游其实能扩时压低了吞吐
处理槽位工作线程、协程池容量让队列和延迟可控槽位和队列都不设上限等于没控
账号配额上游硬性限制避免整批请求被整体拒绝只控这一层会被瞬时突发击穿
  • 动作一:画一张「调用链—瓶颈点—可限点」的表,把每次外部调用的上下游标清楚
  • 动作二:给每个可限点标注上限来源,是进程资源、下游容量还是外部配额
  • 动作三:只对瓶颈层设硬限制,其余层保留观测位但先不拦截
  • 什么时候不适用:下游是弹性扩容且没有硬配额时,早限流反而压低吞吐,这时该控的是成本和超时

几种核心算法适合什么形状的流量

计数窗口
按固定时间片统计请求数,实现最省事,但窗口边界容易放过瞬时峰值,适合量级粗放的内部任务
漏桶
无论来多少都以恒定速率放行,削峰最干净,适合保护一次只能吃固定量的脆弱下游
令牌桶
按速率发令牌,桶里攒的量决定能突发多少,适合用户侧交互这种允许短时突发的场景
信号量槽位
控制的是「同时有多少个在跑」,和速率限制不是一回事,必须另外给队列长度设上限

最容易搞混的是速率限制和并发槽位。速率限制管的是单位时间放进来多少,槽位管的是此刻有多少个在跑。一个下单接口可以每秒放进来很多请求,但如果它同时只能有固定数量的请求在跑,那就得靠槽位。只做速率限制不给槽位,长任务会把并发全占住,短任务只能干等。

  • 突发可容忍且下游有余量:选令牌桶
  • 下游一次只能吃固定量:选槽位加队列,队列长度单独设上限
  • 只用内存实现、精度要求低:用计数窗口近似即可
  • 要求严格匀速:选漏桶,但注意它会牺牲峰值吞吐
  • 判断标准:先问下游能接受多大突发,再问你能接受多长排队,两个答案基本决定算法选型

排队、拒绝与背压:排队不是免费的

队列只能削峰,不能扩容。队列越长,尾延迟越高,很多超时事故的根因就是把排队时间排除在超时预算之外:请求在队列里已经躺了很久,一出队还没执行就超时了。正确的做法是在入队那一刻就把预计排队时长计入整体超时预算,超了就当场拒绝,别让它白占一个槽位。

  1. 把超时预算拆开分成排队、执行、重试三段,各自留出上限,加起来不能超过调用方愿意等的时间
  2. 设置两个水位一个水位开始告警和降级,另一个水位开始直接拒绝,两个值分开调,别用一个阈值兼顾两件事
  3. 拒绝要显式满了就返回可重试的明确信号并带上建议等待时长,比让请求在队列里静默过期对调用方友好得多
  4. 背压往上游传并发到顶时把「忙」的状态透出,让上游降速或分批,而不是让它无限重试再打进来一轮

重试、优先级和公平性怎么不掉坑

重试会放大并发,这一点在设计阶段最容易被忽略。下游只是打了个嗝,客户端集体重试,直接把下游打穿,这就是自己打自己的雪崩。所以重试必须带预算:整体失败率高到一定程度就停止重试,把节奏交给上层降级;重试间隔要加随机抖动,避免所有客户端同一时刻再冲一次。

共享槽位必须分池。交互请求和批量任务混在一个池里,批量的长任务会把交互的延迟拖垮,用户侧感受最直接。分池的粒度按限什么来定:按用户限是防单个用户刷爆,按 Key 限是保护后端配额,按租户限是防止大客户挤占小客户。哪种更合适,取决于超限的那一刻你更怕误伤谁。

  • 给重试设置总预算和最大次数,两者取先到者
  • 每次重试间隔加随机抖动,别用固定间隔
  • 交互池和批量池分开,配额互不挤占
  • 内部单一调用方、任务无优先级差异时,分池带来的复杂度大于收益,可以不分
  • 公平性判断标准:哪个群体被误伤时业务损失最大,就把配额优先分给它的对手方

怎么观测和验证你的设计真的生效

没有观测的限流等于蒙眼开车。至少要同时看四个指标:在飞请求数、队列等待时长、拒绝率、重试次数。只看拒绝率会误判,拒绝率低可能是限得太松根本没拦住,也可能是因为上游自己先崩了;四个指标放一起看,才能分清是限太死还是太松。

现象大概率原因下一步动作
队列等待时长涨、在飞数不高槽位不够或下游变慢先查下游延迟,再谈要不要加槽位
拒绝率突然抬高、重试次数同步涨阈值偏紧或下游抖动先看下游健康状况,别急着放宽阈值
在飞数顶住上限、延迟平稳限流正常生效这个位置就是当前安全水位
重试次数远高于拒绝率客户端没做预算和抖动从调用方侧收重试,而不是改服务端

验证方法很简单:压测时逐档抬高并发,记录尾延迟和错误率的拐点,拐点之前的位置才是安全水位。上线顺序建议先只观测不拦截,拿到真实分布再收紧阈值,避免第一天就误伤正常流量。延迟基线不要凭感觉设,到站内测速页面 /tools/speed-test 量一遍你这条链路的真实数字,再据此定阈值。

限流的目标不是把请求挡住,而是让系统在压力下依然给出可预期的行为。
下一步把你现在这条接口链路的瓶颈层和可限点列成表,再到站内页面核对上游配额和实时价格口径,把阈值建立在真实数字上,而不是拍脑袋。

常见问题

并发控制和限流是一回事吗?

不完全是。限流通常指按速率放行,属于并发控制的一种手段;并发控制还包括在飞请求数、槽位占用、队列深度和重试预算。只做速率限制而不控在飞数,下游变慢时依然会堆积。先把瓶颈分层,再决定用哪一类手段。

队列设多长合适?

没有通用答案,取决于你能接受的最大等待时长和下游处理速度。做法是先定端到端超时预算,把网络、排队、处理三段分开,排队能分到多少时间就对应多长队列。队列长了尾延迟必然上升,需要用真实流量校准。

被限流后重试该怎么做?

重试要带抖动和次数上限,并且整体受一个重试预算约束,避免重试把并发再推高一倍。优先按服务端返回的可重试信号退避,不要固定间隔硬撞。同时把重试次数记进日志,否则你看到的流量永远比真实请求少。

本文依据各平台公开文档与官方公告整理,只做中文归纳;具体做法、参数与限制以官方文档为准。