本节摘要:应用层要扛高并发,第一件事是把应用做成无状态的——任何实例都能处理任何请求,实例之间没有差异。本节讲有状态应用为什么没法水平扩展,怎么通过状态外置(Session 进 Redis、文件进对象存储)实现无状态化,以及弹性伸缩怎么根据负载自动增减实例。核心是理解:无状态是水平扩展的前提,没有无状态化,加再多机器也扩不动。
阅读完本节,你应当能够:
假设你的应用部署了 3 个实例做负载均衡。如果应用是有状态的——比如用户的登录信息存在某个实例的内存里——那用户第一次请求落在实例 A,登录信息存在 A;第二次请求如果负载均衡分给了实例 B,B 里没有这个用户的登录信息,用户就得重新登录。这种情况下,你没法随意加实例分担压力,因为状态散落在各个实例里。
这就是有状态应用阻碍水平扩展的本质。水平扩展(加机器分担负载)的前提是:任何实例都能处理任何请求。但如果有状态,请求必须落在存了对应状态的那个实例上,加再多别的实例也帮不上忙。
要让应用能水平扩展,必须把它做成无状态的——实例本身不存任何业务状态,状态全部外置到专门的存储(Redis、数据库、对象存储)。这样任何实例处理任何请求都一样,加实例就能线性扩容,减实例也不丢数据。这就是为什么无状态化是应用层高并发的第一步。
先把“状态”讲清楚。应用里的状态,指的是那些在不同请求之间需要保持、共享的数据。常见的有这么几类。
会话状态(Session):用户的登录信息、购物车内容。传统做法存在应用内存里,用户下次请求要访问同一个实例。
缓存数据:应用为了加速,把一些计算结果或数据库查询结果缓存在本地内存。这些缓存是局部于某个实例的。
文件和上传内容:用户上传的文件存在应用服务器的本地磁盘。其他实例访问不到。
单例对象:应用里用单例模式管理的计数器、锁等。它们是进程内的,跨实例不共享。
这些状态只要存在实例本地,应用就是有状态的,就没法自由水平扩展。
无状态化的思路很简单:把所有状态从实例本地搬到专门的外部存储。每个状态类型有对应的外置方案。
Session 外置到分布式缓存:用 Redis 这样的分布式缓存存 Session,所有实例共享。用户请求落在任何实例,都从 Redis 取登录信息。这是最常见的做法。
本地缓存改为分布式缓存:不用实例本地内存做缓存,改用 Redis 或 Memcached。所有实例共享同一份缓存。
文件存到对象存储:用户上传的文件不存本地磁盘,存到对象存储(如腾讯云 COS、AWS S3)。所有实例通过对象存储访问。
计数器和锁用分布式组件:跨实例的计数用 Redis,跨实例的锁用 Redis 或 ZooKeeper,不用进程内的单例。
| 状态类型 | 本地存(有状态) | 外置方案(无状态) |
|---|---|---|
| 会话 Session | 应用内存 | Redis 分布式缓存 |
| 缓存数据 | 本地内存 | Redis / Memcached |
| 上传文件 | 本地磁盘 | 对象存储 |
| 计数器/锁 | 进程内单例 | Redis / ZooKeeper |
状态全部外置后,应用实例本身不存任何业务数据,变成纯粹的计算节点——接收请求、处理逻辑、读写外部存储、返回结果。这种实例是无状态的,可以随意增减。
应用无状态化后,就能弹性伸缩——根据负载自动增减实例。容器编排平台(如 Kubernetes)的 HPA(Horizontal Pod Autoscaler)是典型实现。
HPA 的工作原理:它持续监控应用的负载指标(CPU 利用率、内存、自定义的 QPS 等),当指标超过设定的上限,自动增加实例数;当指标低于下限,自动减少实例数。流量大时扩容抗住,流量小时缩容省钱。
弹性伸缩的核心好处是成本效率——不再需要按峰值固定预留资源(峰值也许一年只有几小时),而是按实际需求动态调整。对一个流量波动大的业务(白天忙晚上闲、大促暴增),能省大量闲置资源成本。
无状态化没什么高深技术,就是把状态从本地搬到外部存储。但它难在是个“意识问题”——开发时随手把数据存内存、存本地磁盘太方便了,要养成“任何状态都外置”的习惯。
审查一个应用是不是真无状态,有个简单测试:随机杀掉任何一个实例,用户感知不到中断(请求被分到其他实例,状态从外部存储取回)。如果杀实例会导致用户掉登录、丢数据,说明还有状态藏在本地。
⚠️ 常见坑:开发时为图快,把缓存或临时数据存在实例内存,想着“先这样后面再改”。结果上线后忘了改,系统一直在有状态运行,扩容时才发现问题。无状态化要从架构设计阶段就立规矩,别留技术债。
弹性伸缩不是瞬时完成的。从指标超过阈值到新实例启动、就绪、接流量,需要几十秒到几分钟(拉镜像、启动应用、健康检查通过)。这段时间里,旧实例还在扛压力,可能已经过载。
所以弹性伸缩不能应对突发尖峰(秒级的流量暴增),它适合应对分钟级到小时级的负载变化。对于秒级尖峰,要靠限流和缓存顶住那几十秒,等扩容生效。容量规划时,要预留余量,别把基础实例数设得正好够用——那样一有波动就来不及扩。
弹性伸缩的扩容通常比较安全(加实例只会更好),但缩容要小心。缩容过快,可能把还在处理请求的实例杀掉,导致请求失败。Kubernetes 的优雅下线(先标记不接新流量,等处理完在途请求再杀)能缓解,但配置不当还是可能出问题。
缩容的阈值要保守一点,触发要慢一点。宁可多留一两个实例,也别为了省那点钱频繁缩扩抖动。
💡 关键直觉:无状态化是水平扩展的地基,弹性伸缩是无状态化的红利。先把无状态化做扎实,弹性伸缩才能真的省心。没有无状态化这个地基,弹性伸缩扩出来的实例各有各的状态,反而更乱。
下一节讲应用层抗压的另一个手段——异步化和消息削峰,把耗时操作卸到后台。
伸缩策略的选型逻辑值得单独说透。定时伸缩最简单也最廉价,适合流量规律性强的业务——电商的晚高峰、内容平台的午休时段,提前十分钟扩容到位,成本可控。指标驱动伸缩更聪明,响应真实负载,但存在固有滞后:指标上升触发扩容、实例拉起、预热、注册发现、负载均衡生效,整条链路两到五分钟,秒级突刺根本等不了。预测式伸缩是第三条路:用历史流量训练简单模型提前预判,大厂的活动场景常用,但维护成本高,流量模式改变时预测会失效。实践中的组合是"定时打底、指标兜底、突发场景叠预测",三层各司其职。
伸缩还有一个常被忽略的维度:扩容易、缩容难。缩容时要处理存量连接的排空、正在执行的任务迁移、以及"刚缩完又来流量"的抖动。建议缩容策略保守——分批进行、批间观察、且设置缩容冷却时间;对有长连接的服务用连接数而非 CPU 作为缩容依据。另外冷启动的优化空间值得投入:镜像瘦身、启动并行化、依赖预热,把实例从拉起到可服务的时间压缩一半,等于把弹性响应能力翻倍,这比调灵敏度阈值有效得多。
关于无状态化改造还有一条渐进路径值得参考:不必一次性把所有状态外置,可以按"新写的功能严格无状态、存量功能按故障复盘驱动逐个改造"的节奏推进。每次因本地状态导致的扩容事故或数据不一致工单,都是一次精准的改造需求。这样推进两年下来,系统的状态面会自然收缩到最小,且每次改造都有真实事故做依据,说服力远超"架构规范要求"。
还有伸缩组配置的一个对比结论:固定实例数加手动扩容、最小最大区间的自动伸缩、以及定时加指标的混合策略,三者的适用场景截然不同。固定实例最省心也最贵,适合流量平稳的内部系统;纯自动伸缩看起来先进,但对规律性流量其实是浪费——每晚定时到来的高峰,用定时策略提前扩容比等指标触发更稳;混合策略是多数在线业务的正解。配置之外,伸缩组的健康检查类型要选对:应用级健康检查(探活业务接口)比实例级(只看进程存活)更能保证伸缩出来的实例真的可用。
伸缩策略的评审还有一个时间维度要交代:任何策略调整后需要观察完整的流量周期(至少一周,含周末和工作日)才能下结论,单日数据会被当天的特殊事件污染。给每次策略变更建立观察档案,记录变更前后各一周的核心指标,这份档案既是后续调参的依据,也是向上汇报时展示弹性建设收益的素材。