ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

百亿级高并发系统架构设计实录:核心难点与踩坑复盘

百亿级高并发系统架构设计实录:核心难点与踩坑复盘 百亿级这个词在外行眼里是数字游戏在内行眼里就是三件事流量洪峰怎么挡、数据一致性怎么保、出故障怎么快速止血。我这些年参与过好几个大体量系统的架构设计与重构最近又有机会完整复盘了一套每日请求量达到百亿级别的系统架构实录这里把整个设计过程、关键决策、踩坑经历一次性整理出来。无论你是正在设计新系统还是打算重构老系统这篇内容应该能帮你少走不少弯路。写这篇文章之前我先说一句百亿级系统的难点从来不在某个单点技术上而是在于所有环节的耦合。你以为瓶颈在数据库结果发现是网关线程池被打满你以为加缓存能解决结果缓存集群的带宽先爆了你以为做了限流就安全结果降级策略又拖垮了下游。这套实录就是把这些问题挨个拆开讲讲每一个决策背后的推导逻辑。1. 业务规模与技术挑战拆解1.1 百亿级流量到底意味着什么先做一个简单的数学题。日请求量百亿级意味着每秒的平均QPS在12万左右这还只是平均值。业务流量向来不是均匀的早高峰、晚高峰、大促秒杀、突发热点峰值往往是均值的5到10倍。按10倍算峰值QPS在百万级别。这是第一个门槛你的整个技术链路从用户端到最后一个存储节点任何一环都不能在百万QPS的冲击下崩溃。第二个门槛是数据量。请求量百亿级产生的日志、业务数据、埋点数据一天就是PB级别。存储成本、查询性能、数据冷热分离、归档策略每一样都让人头疼。我之前帮一个客户做容量评估发现光是日志系统的存储开销就占了整个IT预算的三成这就是前期没做好数据分层设计的代价。第三个门槛是依赖复杂度。别以为百亿级系统就是一个巨型单体实际拆开来看核心链路可能涉及几百个微服务、几十个缓存集群、上百组数据库实例、消息队列、搜索引擎、大数据计算引擎……任何一个环节抖动都可能被流量放大成雪崩。所以这套架构实录里我最想讲的不是某个组件的用法而是整套治理体系是怎么建起来的。1.2 核心矛盾可用性、成本与研发效率的三方博弈做架构设计本质上是在可用性、成本、研发效率三个维度之间找平衡点。先看可用性。百亿级系统的可用性目标通常是99.99%以上一年停机时间不能超过53分钟。算一笔账每天百亿请求假设每秒能处理10万请求一秒钟的停机就意味着至少10万请求失败如果发生在高峰期影响面是千万级用户。这就是为什么所有高并发系统都把冗余和容灾放在第一优先级。再看成本。机器不是免费的。支撑百万QPS的流量假设单机扛5000 QPS就需要200台应用服务器加上缓存、数据库、消息队列、监控系统整个集群规模轻松超过千台。每台服务器按几万元计算基础设施投入就是千万级别这还只是硬件电费、带宽、运维人力都是持续的支出。所以我一直强调好的架构不光是扛得住流量还要控制得住成本。举个例子数据冷热分离做得好的系统存储成本能省一半以上。最后是研发效率。这个往往被技术人忽略但实际业务方最在意。架构过于复杂每次上线都要全链路回归发一次版本要协调几十个团队业务迭代速度会被拖垮。所以我们既要保证系统稳定又要让团队能快速开发、快速上线这需要极强的工程基建能力。1.3 架构选型的核心判断依据这套系统为什么最终采用了混合架构而不是全部微服务化或者全部单体化这得从业务形态说起。核心交易链路追求的是稳定和低延迟所以采用相对集中的服务化架构控制调用深度限制依赖广度。非核心业务追求的是快速迭代和独立扩展所以拆成独立的微服务互不干扰。数据层则根据访问特征做了拆分强一致性的数据走关系型数据库高吞吐的缓存数据走分布式缓存集群非结构化的日志和埋点数据直接进消息队列和列式存储。这个选型过程背后有一个很重要的判断依据单点组件能不用就不用关键链路绝不出现单点故障。例如注册中心至少要部署三个节点的集群配置中心必须做多机房容灾网关要支持多活部署。任何组件在设计时都要回答一个问题如果这台机器挂了流量还能不能走通2. 整体架构设计与分层模型2.1 四层架构模型接入层、应用层、数据层、基础设施层这套架构最大的特点是将整个系统划分为四个清晰层次每层都有独立的容灾和扩展策略。接入层是系统的第一道防线负责所有的入口流量。这一层主要部署负载均衡集群和API网关核心职责是域名解析、SSL卸载、路由转发、限流降级、黑白名单访问控制。接入层要做到无状态这样才能水平扩展应对流量突增时只需要加机器。应用层承载全部的业务逻辑由若干个核心服务集群和众多微服务组成。核心服务集群处理交易、账户、订单等关键链路的请求部署上采用多机房多活的方式。支撑服务则按照业务域划分每个服务独立部署、独立扩缩容。这一层最关键的是服务之间的通信方式我们统一采用接口化调用超时和重试策略都做了严格限制防止雪崩效应。数据层是最难治理的一层。百亿级系统的数据层一定是混合存储架构关系型数据库负责事务性强的数据缓存集群负责热点数据的读取消息队列负责削峰填谷和异步解耦大数据组件负责离线和近线计算。数据层内部还有一条清晰的数据流转管道业务数据产生后先写数据库再同步到缓存再异步进入消息队列最终汇聚到大数据平台做分析。基础设施层包含注册中心、配置中心、分布式链路追踪、日志系统、监控告警系统和统一认证鉴权体系。这一层不直接处理业务请求但所有业务请求都依赖它们。基础设施层如果出问题整个系统都会受影响所以它自己的容灾级别反而是最高的。2.2 服务拆分粒度与领域划分策略服务拆分是所有微服务架构里最容易翻车的环节。拆粗了一个大服务内部耦合严重团队协作效率低拆细了一次请求要跨十几个服务延迟和失败率都上升。这套系统的拆分逻辑核心是围绕业务域走。先梳理清楚业务边界将整个系统划分为用户域、交易域、商品域、支付域、营销域等核心领域每个领域对应一个或多个核心服务。服务之间通过标准接口通信不直接共享数据库。举个例子交易域和库存域之间存在频繁的数据交互但它们不共享同一张表而是通过消息队列和RPC接口完成数据一致性协作。服务拆分还有一条重要原则依赖方向要清晰。不允许出现服务之间循环依赖一旦出现就会导致发布顺序难以管理故障排查困难。我们用工具扫描了全链路依赖关系把循环依赖全部拆干净。宁可在设计阶段多花时间建模也不要在运行阶段被问题追着跑。2.3 混合架构中的注册中心与配置治理百亿级系统里服务数量几百个实例数量上千个服务发现和配置管理是基础设施的核心。这里我们选择了一套开源的注册中心体系做了一些深度定制。注册中心要解决的问题非常明确服务提供方启动后把自己的地址注册上去服务消费方启动后订阅自己关心的服务列表一旦服务列表发生变化消费方需要实时感知并调整。这个机制看似简单但做到高并发下的实时一致非常困难。我们遇到的第一个坑是注册中心集群脑裂问题网络分区导致部分节点无法同步服务列表边缘流量出现间歇性调用失败。后来通过调整集群选主策略、增加节点间同步频率、客户端本地缓存兜底才稳定下来。配置中心的治理也值得一提。几百个服务的配置分散管理的话灾难随时会发生。我们把所有配置收口到一个统一的配置中心支持配置的版本管理、灰度发布、变更审计和回滚。环境隔离是硬性要求开发环境、测试环境、预发环境、生产环境的配置必须严格分离。这点做不好早晚会发生用测试配置刷新生产集群的事故。3. 核心技术与组件选型实录3.1 网关层设计限流、熔断与全链路压测网关是整个系统的交通警察每天的流量都要经过它来调度和管控。这套系统的网关层核心职责主要有三个路由转发、流量控制和协议适配。路由转发是基础能力根据请求的URL和Header信息将流量转发到对应的后端服务。这里要做的是高性能和低延迟网关采用异步非阻塞模型单机可以支撑数万QPS同时网关的内存占用控制在很低的水平。流量控制是网关最核心的功能。限流算法选用的是令牌桶和滑动窗口的组合常规情况用令牌桶突发流量用滑动窗口做精准控制。为什么不用漏桶漏桶处理匀速流效果好但面对互联网典型的突发流量会切掉大量请求用户体验太差。令牌桶允许一定程度的突发配合滑动窗口预设阈值既能放行合理的流量尖峰又能挡住恶意攻击。熔断是流量控制的下半场。当后端服务连续出现超时或异常网关要自动切断对该服务的调用快速失败并返回兜底结果。熔断器有三个状态关闭、开启、半开。关闭时正常放行连续失败率达到阈值就切换为开启状态此时所有请求快速失败经过一个时间窗口后进入半开状态放行少量试探请求如果成功则恢复关闭状态。这个机制不是阻止故障发生而是防止故障蔓延。全链路压测必须单独提一下。百亿级系统上线前一定要做全链路压测而且生产环境要常态化压测。我们有一套压测平台能够模拟几十万QPS的真实流量同时对全链路施加压力观察各个节点的表现。压测的指标不只是吞吐量和耗时还包括错误率、内存和CPU水位、GC频率、数据库连接数等。有一次压测中我们发现集群整体QPS还没到峰值的一半数据库的活跃会话数已经逼近上限原来是一个慢SQL导致的连接耗尽。这类问题不在生产环境压测根本发现不了。3.2 缓存层的架构设计与热点治理缓存是百亿级系统的第二道大坝。数据库能抗住的流量有限日常读请求绝大部分靠缓存扛。这套系统的缓存架构分成两层本地缓存进程内缓存用于扛住高度热点、集中访问的数据。例如某个爆款商品的信息同一时刻可能有几十万请求在访问如果全部穿透到分布式缓存缓存集群也会成为瓶颈所以先在应用进程内做一级缓存。本地缓存的容量有限因此只缓存少量极热门的数据并设置较短的过期时间。分布式缓存集群扛剩下的读流量。我们选用的是Redis Cluster模式数据分片存储在多台机器上客户端通过一致性哈希算法定位分片。扩容时采用渐进式迁移避免大量key同时搬迁造成集群抖动。热点key问题是最让人头疼的。想象一个新商品刚上架或者某个新闻事件突然爆发一个key的访问量可能在几秒内从每秒几千飙升到每秒百万。这个单一key所在的缓存节点会先被打满接着说好的缓存命中率瞬间失效大量请求穿透到数据库数据库连接池被打光整个服务雪崩。解决方案我们试了很多种最后落地是一套组合拳热点探测用异步任务实时统计每个key的访问频次一旦发现访问频率超过阈值立刻把这个key连同value推送到所有应用节点的本地缓存中去同时将这个key在分布式缓存中的过期时间延长。这样热点key的请求会命中本地缓存分布式缓存节点压力大减。再配合一个应急预案系统可以在10秒内完成热点key的自动发现与多级缓存切换运维人员不需要任何手工操作。3.3 存储层的分库分表与读写分离实践数据层永远是大系统的命门。百亿级系统的数据库单库单表绝对扛不住所以分库分表是必修课。先看分库分表的分片策略。我们用的是业务维度加时间维度的双重分片。例如订单数据最核心的查询维度是用户ID和订单ID所以主分片键选择为用户ID将同一个用户的订单数据落到同一个分片里这样按用户查询订单不需要跨分片聚合。订单表本身按照时间做二级分区保留最近三个月的数据在热分区超过三个月的自动归档到冷存储。这套策略的收益非常明显单表数据量从几亿降到了几千万SQL查询响应时间从秒钟级降到了毫秒级。读写分离同样关键。核心服务的数据库采用一主多从的架构读流量走从库写流量走主库。主库和从库之间通过binlog同步延迟一般在毫秒级别。这里要特别注意业务容忍度某些强一致性的场景支付结果、库存扣减必须走主库读业务上需要明确标注读写路由策略不能一刀切。分页查询和跨分片聚合是分库分表后最麻烦的问题。原来一条SQL就能完成的全局排序分页现在要并行发到全部分片执行再汇总后重新排序性能开销很大。我们的方案是尽量避免跨分片查询把多数查询收敛到单分片内完成。实在无法避免的用异步任务预计算结果再在应用层做二次聚合避免查询时实时计算。这是一种用空间换时间、用异步换同步的典型思路。数据库连接池的参数也要精细化设置。连接池上限设置过大空闲连接会浪费内存太小又扛不住高峰。我们根据压测数据按单实例QPS、单个查询耗时、平均并发数反复调整最终确定了一个动态水位平时维持并发数的1.5倍高峰时最大扩到3倍超出即排队等待。这套参数在故障演练时表现比较稳。4. 核心链路与关键流程详解4.1 下单链路全流程拆解下单是电商系统最核心的一条链路考察整个架构的承压能力。用户点击下单按钮后请求经过网关到达交易服务交易服务要同时完成多项操作创建订单、锁定库存、生成支付单、发送消息通知。串行执行的话延迟很高而且每多一步失败概率就高一分。这套系统最后采用了异步化加最终一致性的方案。核心流程分成两个阶段第一阶段是预下单只做订单草稿的创建和预占库存。库存预占的操作非常轻量使用缓存里的原子操作完成不直接扣减数据库库存。这个阶段回应给用户的响应一般在100毫秒以内用户可以立即收到“订单创建中”的反馈。第二阶段是异步确认交易服务通过消息队列发送订单确认消息下游的库存服务、支付服务、积分服务各自订阅消息异步执行自己的逻辑。消息队列在这里起了关键作用削峰填谷将高峰期骤增的订单处理请求转化为相对平稳的异步消费流量解耦交易服务无需同步等待下游所有服务的处理结果。那怎么保证最终一致性呢我们用一张分布式事务消息表记录每笔订单的实时状态。每个服务处理完自己的部分后回调更新状态。状态在超时未完成时会触发可靠性补偿任务进行对账和重试。这套方案牺牲了强一致性但换来了系统整体的高吞吐和高可用这在百亿级流量下是必须的取舍。4.2 库存扣减与防超卖库存是电商系统里最敏感的东西扣多了叫超卖扣少了叫少卖都是事故。百亿级系统的库存扣减我们经过几轮迭代才稳定下来讲一下最终采用的方案。第一版方案是数据库乐观锁update库存表 set stock stock - n where product_id ? and stock n。这种方式最简单能保证不会超卖但在大流量下数据库的写压力会直接被打满高峰期库存表的行锁竞争极其严重。第二版方案把库存预热到Redis中用Redis的原子减操作。Redis单线程模型天然保证原子性通过Lua脚本将“检查库存足够、扣减库存、更新已售数量”三个操作合并为一个原子操作。实测下来单机Redis可以扛住数万次的扣减请求性能不再是瓶颈。但Redis扣库存和数据库库存之间怎么保持一致总不能靠猜。最终方案是引入异步对账机制Redis扣减成功只表示预占成功真正的库存扣减通过消息队列异步落库。落库时使用数据库乐观锁再做一次校验防止Redis集群故障时出现数据不一致。生产环境还跑了一个定时任务每五分钟比对Redis库存和数据库库存出现差异就自动告警由人工介入修正。这套组合在多次大促实战中经受住了考验。4.3 分布式事务与最终一致性方案选型分布式事务是百亿级系统的永恒话题。没有银弹每种方案都有代价关键是选一个适合业务场景的。这套系统绝大多数场景采用的方案是本地消息表加消息队列。本地消息表的核心思想在业务数据库里建一张消息表业务操作和消息写入放在同一个本地事务中保证业务操作成功对应的消息一定也能写入。后台任务扫描本地消息表把消息投递到消息队列下游消费者处理成功后回调确认。这套方案的好处是落地简单、可靠度高适合对实时性要求不高、允许一定延迟的业务。部分核心场景比如支付用的是TCCTry-Confirm-Cancel模式。每个服务需要实现三个方法Try阶段资源预留Confirm阶段确认执行Cancel阶段回滚补偿。它能做到业务逻辑层面上的最终一致但是开发工作量比较大对服务的要求也高。我们只在支付链路和跨仓调拨等少数高价值场景使用了TCC其余一概走消息队列。这里有一个我特别想提醒的点分布式事务的补偿逻辑一定要可观测、可重放。所有补偿任务运行结束后必须记录完整的执行日志方便复盘和校验。任何补偿逻辑靠人工盯是不行的要把补偿做成一个独立调度的任务平台具备重试、告警、人工触发能力。5. 高可用建设与故障演练实录5.1 多机房多活部署方案单机房部署不管你的机器有多贵、运维有多强一场机房级别故障光纤被挖断、电力系统异常就能让整个系统瘫痪。这套架构的多活部署核心思路是双机房互备加就近接入。两个机房之间通过专线互联数据层做双向同步应用层做多活部署。正常情况下用户的请求由就近机房处理流量自然分流。当一个机房出现故障时DNS切换和网关路由规则联动切换把流量全部导向另一个机房切换时间控制在1分钟以内。这个方案的难点在于数据冲突处理。双机房同时写同一份数据如果都使用自增主键就会出现主键冲突。我们的方案是全局使用分布式ID生成器保证全局唯一避免冲突。数据同步层面使用消息队列进行跨机房同步同步延迟控制在秒级以内。要注意的是跨机房同步不能丢消息必须记录消息位点同步任务支持断点续传。很多人问过一个问题多活到底该做到什么程度我认为至少要做到核心链路的无感知切换。用户交易、订单查询、支付这些核心服务必须能在机房故障时自动切换这些场景对用户的影响是直接的经济损失。非核心服务像消息推送、内容推荐可以降级为单机房运行切换慢一点没关系。5.2 容量评估与弹性扩缩容容量评估这个活儿平时没人重视出事就抓瞎。百亿级系统的容量规划必须是数据驱动的拍脑袋估算早晚出问题。第一步明确核心指标日请求量、峰值QPS、平均响应时间、可用性目标。这些指标要和业务方对齐不能技术团队自嗨。第二步做容量计算。假设日请求量百亿峰值QPS按均值五倍估算为50万。单机应用层能扛5000 QPS需要部署100台。每个请求平均耗时200毫秒数据库活跃连接数大约是QPS和耗时的乘积即10000个活跃连接。单库最大连接数2000的话需要拆分5个库。这里其实可以进一步压缩数字但思路就是这样把每个环节的能力都算清楚才能知道瓶颈在哪。第三步在关键节点保留一定的水位冗余。我们一般按峰值流量的1.5倍做容量水位确保大促等极端场景既能扛得住又不至于过度浪费资源。弹性扩缩容是基于水位自动触发的应用服务器采用容器化部署当整体CPU超过70%自动扩容策略就启动了新实例从启动到接入流量在三分钟以内。缩容也一样持续低水位超过半小时自动回收多余实例。5.3 故障演练与红蓝对抗故障不发生的地方不需要高可用架构高可用是演练出来的。我们有一个原则每个月至少一次故障演练每年至少一次全链路红蓝对抗。故障演练是小规模、定向的比如挑一个缓存集群手动杀掉一台机器观察系统是否会自动剔除故障节点业务是否受影响。这类演练安全性高做起来快适合高频次执行。红蓝对抗是规模最大、最真实的。红队是专门负责搞破坏的团队他们会模拟各种极端情况同时杀掉几个核心服务、把数据库连接池耗尽、灌入异常流量、制造机房网络分区……蓝队是真实的运维和研发团队他们在毫不知情的情况下要靠自己的监控告警、定位排查和应急预案去扛住这波攻击。这个演练过程非常残酷但也最能暴露系统设计里的问题。我们曾经在一次演练中发现某个依赖服务的超时时间设置成了30秒而网关的熔断阈值是10秒这两个参数就不匹配导致网关迟迟不能熔断用户请求全部进入等待状态。这类问题只有在演练中才会现出原形。每次演练后都要输出报告列出发现的问题清单、整改时间表和责任人形成闭环。6. 常见故障与排查实用手册6.1 缓存穿透、击穿与雪崩速查表这三个概念经常被混为一谈但成因和解决方案完全不同整理成一张速查表。故障类型现象典型原因解决手段缓存穿透缓存和数据库都没有该数据请求全部打到数据库恶意攻击或大量无效查询缓存空值布隆过滤器前置拦截缓存击穿某一个热点key过期瞬间大量并发请求穿透到数据库热点key过期时间设置不当互斥锁重建缓存逻辑过期热点key永不过期加异步刷新缓存雪崩大量key同时过期数据库压力瞬间暴涨过期时间集中缓存集群故障过期时间加随机性多级缓存由消息队列异步重建缓存排查这类问题核心是先看监控。Redis的命中率曲线如果出现断崖式下跌大概率是击穿或雪崩。此时数据库的慢查询监控应该能看到大量同类型SQL集中出现。确认原因后应急手段可以很快先加限流挡住过量请求再启动异步重建任务强制刷新缓存最后修复触发的根因。6.2 慢SQL引发数据库连接耗尽这里讲一个真实的事故。某天中午业务高峰期监控系统大面积告警下单接口超时率急剧上升。第一批排查的人先看应用日志发现数据库连接池报错无法获取连接等待超时。这说明连接池被耗尽。进一步查数据库侧活跃会话数已经拉满大量会话都卡在同一条SQL上。这条SQL是订单查询语句按创建时间做了大范围扫描由于订单表数据量已经过亿索引选择失误导致走了全表扫描单个查询耗时达到十几秒。几十个这样的慢SQL同时执行数据库连接就被耗尽其他正常请求都排队等待。处理过程分了三步第一步先将这条SQL强制路由到只读从库减轻主库压力第二步杀掉已经超时的会话释放连接第三步优化SQL改写为按分片键用户ID查询并在时间字段增加二级索引。整个恢复过程用了不到五分钟但教训很深所有SQL上线前必须做执行计划审查数据量过亿的表不能允许出现无分片键的查询这个规则后来直接做进了代码审核规范。6.3 消息积压的排查与快速恢复消息队列是削峰填谷的利器但也会带来消息积压问题。某次大促期间订单确认消息出现了积压堆积量最高达到几千万条。排查思路先从生产端开始确认生产者是否还在正常发送发送速率有没有突增。再查消费端消费者是否存活、消费者处理速率是否下降、消费者有没有报错。我们那次的情况是消费者的数据库操作出现瓶颈批量入库的SQL耗时增加导致消费速率跟不上生产速率。快速恢复有两个方向横向扩容消费者实例是最直接的手段但前提是下游存储的写入能力跟得上不然扩容再多实例也是挤在存储层排队另一个方向是调整消费者的批量处理参数每次拉取更多的消息减少网络往返和事务开销。实际操作中两个方向同时做了积压在半小时内恢复到正常水位。随后长期措施也跟上给消息队列配置了积压监控告警积压超过阈值自动触发消费者扩容让问题在业务影响前就被处理。7. 架构实录之外的经验复盘写完这套实录再复盘整个架构设计过程有几个体会值得单独说一下。第一点是架构设计要带着约束条件去做。没有资源约束的架构方案本质上都是耍流氓。你设计的方案能不能在现有预算内落地团队能不能驾驭运维能不能看得懂这些比技术先进性重要得多。我见过很多团队一上来就追求新技术最终项目烂尾就是因为没有把团队能力算进设计方案里。第二点是高并发系统最后拼的都是治理能力。光有框架和组件远远不够完整的监控告警体系、日志追踪体系、全链路压测平台、故障演练机制、应急预案的完备程度这些才是百亿级系统能在风暴中屹立不倒的真正原因。不少系统的技术选型并不差但出了故障才发现监控缺失、日志不全、预案没有整个团队只能干瞪眼。第三点是架构演进是螺旋上升的不存在一步到位的完美方案。这套系统从最初的几十台服务器发展到现在的几千台规模经历过单体应用拆分的阵痛也付出过数据不一致的代价。每一次优化都是带着业务压力往前走的不是纯技术的理想化演进。所以如果你正在做架构改造我的建议很朴素小步快跑每走一步都要有明确的度量指标和回滚方案。最后分享一个具体的小技巧给核心链路的每一个接口定义好降级预案并且让预案可以一键执行。平时这些预案可能永远用不上但一旦发生故障提前准备好的降级脚本能让你在几分钟内恢复核心业务而不是让几十个研发在会议室里现场讨论怎么改代码。这个准备工作性价比极高强烈建议每个团队都做一遍。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进