谈高并发时,话题经常一上来就跳到缓存、分库分表、消息队列。这些手段都对,但都属于”单机能力之外的补救”。真正决定一个系统能不能横向长大的,是更基础的一件事:服务本身能不能无状态地复制。这一层没打通,加多少台机器都是在跟自己较劲。
有状态服务的扩展障碍
先看反例。一个典型的有状态服务,会话数据存在进程内存里:用户登录后,session 对象挂在当前 JVM 或当前进程的哈希表上。此时来了第二台机器,问题立刻出现——用户第一次请求落在 A 机器,第二次落在 B 机器,B 上根本没有这个 session,用户被莫名登出。
状态存在本地文件、本地定时任务、本地队列、本地缓存里,都是同样的问题。它们让”这台机器”变成不可替代的个体,任何复制、迁移、回滚操作都会牵动状态的归属。更麻烦的是故障恢复:机器挂了,状态跟着丢,重启后旧状态无处可寻。有状态的服务不是不能扩展,而是每一步扩展都要回答”状态怎么办”,成本随规模非线性上升。
无状态改造:会话外置与共享存储
无状态的核心定义是:任意一次请求落在任何一台机器上,处理结果都一样,机器之间没有”我这边才知道”的信息。改造的抓手是把状态挪出进程。
会话外置是最先要做的。把 session 放进共享存储(内存数据库或数据库),应用进程只认一个 session id。这一步做完,服务实例就成了可互换的副本,加机器、摘机器、滚动重启都不影响用户。
本地缓存是第二个要处理的点。进程内缓存让单机很快,但每台机器缓存内容不同,命中率随机器数量稀释。要么把缓存挪到共享的分布式缓存层,要么配合一致性哈希路由让同一 key 稳定落同一机器:前者简单通用,后者性能更高但运维复杂,按业务对命中率的敏感度选。
本地定时任务也要改造。单机上跑的定时清理、统计任务,多机部署会重复执行。要么用分布式调度器保证同一任务全局只有一个执行者,要么把任务本身设计成幂等的、重复执行也无害。
改造完成后,服务才真正具备”加机器就涨容量”的资格。这是一切水平扩展的起点。
负载均衡:把流量摊平
无状态服务解决了”能不能复制”,负载均衡解决”怎么把流量分给副本”。常见的分发策略各有权衡。
轮询最简单,机器性能一致时很均衡;加权轮询适合异构机器,按性能配权重;最少连接数适合处理时长差异大的请求,把新请求派给当前最闲的机器;一致性哈希则按请求 key 定向,让同一用户的请求稳定落同一台机器,配合本地缓存的场景常用。
健康检查不可少:机器挂了要及时摘除,恢复后要能挂回。检查频率太低,故障机器继续接流量;太频繁,检查本身成为负担。实践中常配”连续失败 N 次才摘除”,避免网络抖动误摘。
负载均衡的层级也值得留意。四层转发快、七层能按内容路由(按 URL、按 header 分流),网关层常做七层,内网服务间调用用四层或客户端负载均衡。层级选错,要么功能不够,要么性能白白损失。
限流与降级:保护在哪里发生
扩容能增加容量,但总有扩不动或突发超预期的时候。限流和降级是给系统装保险丝,关键问题是在哪一层做。
限流放在入口(网关层)最有效,请求还没打到下游就被挡掉,保护的是整个体系。放得太深(每个服务各限各的),上游放开、下游多服务同时被限,用户体验是”随机失败”而非”整体降级”。令牌桶允许一定突发、漏桶严格匀速,按业务对突发的容忍度选。
降级的核心是”放弃次要功能,保住主要功能”:列表页砍推荐位、详情页砍评论、下单链路砍积分计算。降级开关要提前埋好、可动态配置,而不是故障时改代码上线。没有预案的降级,临场只能硬扛。
熔断是限流降级的近亲:下游持续失败就暂时切断调用,快速失败而不是拖垮自己,过一段时间再试探恢复。它保护调用方,让故障不沿调用链蔓延。
连接池与线程模型:微观上的瓶颈
宏观架构理顺后,微观的资源模型决定单机能撑多少并发。数据库连接、HTTP 客户端、线程池,本质都是”用池子管理稀缺资源”。
连接池太小,请求排队等连接,延迟上升;太大,数据库连接数被压垮,反而更慢。经验上的起点是按数据库承受能力倒推:数据库能稳定支撑的连接数,除以应用实例数再留余量,就是单实例连接池的上限,再用压测微调。
线程模型的选择取决于阻塞还是非阻塞。阻塞模型(一线程一请求)直观易懂,但高并发下线程数暴涨、上下文切换吃掉大量 CPU;非阻塞模型(事件驱动、协程)用少量线程支撑大量并发连接,代价是代码复杂度上升、调试困难。IO 密集型服务用非阻塞模型收益大,CPU 密集型的收益有限,且要注意别把阻塞调用放进非阻塞线程里,那等于自废武功。
队列长度也易忽略。等待队列设得过长,请求堆积、内存上涨、超时才失败,白白消耗资源;设得过短,稍有波动就拒绝请求。队列是有界缓冲不是无限仓库,容量要按内存预算和可接受延迟倒推。
容量评估:QPS、延迟与并发
“要多少机器”这个问题,可以用一个朴素的关系式框住:并发数 ≈ QPS × 平均延迟。一个接口平均耗时 100 毫秒,要支撑每秒 1000 次调用,在途请求约 100 个,这决定了线程池、连接池、内存的基线需求。
反过来推也成立:单机能稳定处理的并发数(受线程数、连接数、CPU 限制)除以平均延迟,就是单机的 QPS 上限。总目标 QPS 除以单机上限,就是机器数,再按峰值系数(比如日常的若干倍)和冗余系数(故障一台不塌)放大,得到实际部署量。
容量评估的价值不在于算得多准,而在于把”感觉不够用”变成”差在哪一项”。是延迟太长导致并发堆积?还是单机 QPS 上限太低?不同的答案对应不同的优化方向,盲目加机器有时治标不治本。
压测的意义与误区
压测是验证容量评估的唯一手段。没有压测的容量数字是假设,压测过的是事实。
压测要做在生产同构环境(同配置、同依赖、同数据量级),数据量差异会让缓存命中率和索引选择完全不同,结论直接失真。目标不只是”最大 QPS 多少”,更是观察拐点在哪、延迟在哪开始恶化、瓶颈在 CPU、内存、IO 还是连接池。
常见误区是只压单接口:真实流量是多接口混合、有前置依赖,单接口压测往往高估系统能力。另一个误区是把压测结果当承诺:压测环境比生产干净,实际容量通常要打折扣,规划时留缓冲。
无状态设计把服务变成可复制的单元,负载均衡把流量摊给副本,限流降级兜住超预期,容量评估和压测给出决策依据。系统才有资格谈”加机器就涨容量”——这是高并发架构真正的起点,比任何单项技术都更值得先做对。
评论(0)
还没有评论,来抢沙发。
请 登录 后参与评论。