论高并发系统的设计与实践
论高并发系统的设计与实践
试题:
(1)概要叙述你参与管理和开发的软件项目以及你在其中所承担的主要工作。
(2)从缓存设计,异步处理,限流降级,数据库优化,服务拆分,水平扩容等方面进行论述。
(3)阐述遇到了哪些性能问题,以及如何综合运用上述策略解决。
本文为考后复盘,非考场答卷原文
考后基于回忆重新整理的完整技术复盘,篇幅和理论深度均经过了大幅度扩展,与考场 3000 字限时作答有本质区别。请勿作为考场写作的参考范本,备考建议参考软考备考经验。
本人自参加工作以来,一直在某互联网办公软件公司从事搜索与知识问答相关业务的研发工作。随着大语言模型的逐渐成熟,我所在部门推出了一款面向企业客户的知识问答产品——通过 RAG(检索增强生成)架构,将用户的自然语言问题转化为对海量企业文档的语义检索,再由大语言模型基于检索结果生成回答。我在该项目中负责问答架构的设计与优化,以及其与传统搜索系统的集成。
知识问答的请求链路在一次用户请求中会产生显著的内部放大效应:Embedding 向量化、多路文档召回、重排序、Prompt 拼接、LLM 推理,一个用户请求在系统内部平均产生 12 次下游调用,峰值可达 20 次以上。在日均百万级问答请求的规模下,系统峰值 QPS 达到搜索底层日常负载的 5-8 倍。上线初期 DAU 约 2 万,端到端首 Token 延迟(TTFT)约为 5 秒;随着 DAU 增长至 15 万,峰值时段 TTFT 恶化至 13 秒,GPU 利用率仅 45%。业务同时面向付费租户(SLA 保障)和非付费租户(共享免费额度、允许适度降级),进一步增加了并发控制的复杂度。
高并发系统的优化并非无本之木,计算机科学在半个多世纪的发展中沉淀出了一套行之有效的方法论。下面我将从缓存设计、异步处理、限流降级、数据库优化、服务拆分和水平扩容六个维度,先探讨其学科渊源与核心原理,再逐一结合业务场景展开具体的架构设计。
其一,缓存设计,是计算机存储层次结构优化的核心方法,是经济学比较优势原理基于局部性理论在计算机领域的伟大发明。缓存通过对两种不同性价比的存储介质——容量小速度快单价高(如 SRAM、DRAM)与容量大速度慢单价低(如 SSD、HDD)——进行比较优势的均衡,借助程序中的时间局部性和空间局部性获得高命中率,从而实现「接近快存储的速度、接近慢存储的成本」。从 CPU 的 L1/L2/L3 Cache 到操作系统的 Page Cache,再到应用层的 Redis/Memcached,缓存思想贯穿了计算机系统的每一个抽象层级。高并发系统中,缓存的策略选择——旁路、穿透、击穿、雪崩——本质上是在一致性与性能之间寻找纳什均衡。
其二,异步处理,源于操作系统领域的中断机制与任务调度理论,是离散数学中排队论思想在系统架构中的发扬。同步模型下,调用方必须阻塞等待被调用方执行完毕——这是冯·诺依曼体系结构串行执行语义的自然延伸,但也是并发场景下 CPU 资源的最大浪费源。异步处理通过消息队列将生产者和消费者解耦,将瞬时的高峰流量在时间轴上摊平,本质上是将排队论中的 M/M/c 模型引入服务调用链路——以适度的端到端延迟换取吞吐量的数量级提升,是典型的以空间(队列积压)换时间(请求响应)的运筹学决策。
其三,限流降级,源于计算机网络中 IP 协议尽力而为(Best Effort)的设计哲学,是一种「自知其短,故而有所不为」的系统工程智慧。IP 协议从不承诺交付,路由器在拥塞时直接丢弃数据包——这种看似粗暴的行为恰恰是互联网能在数十年间稳定运行的基础:局部放弃保障了全局存活。限流降级继承了这一思想——当系统负载接近阈值时,主动感知下游状态并在必要时拒绝或降级部分请求,是系统自身的一种负反馈调节机制。与其让整个系统因过载而雪崩,不如在入口处就有所取舍。
其四,数据库优化,是数据结构与算法领域「空间换时间、预处理换查询」思想在最底层存储系统上的投影。从 B+ 树的层级索引到倒排索引的 posting list,再到列式存储的压缩与向量化扫描,数据库的每一次根本性进步都源于对数据访问模式更深刻的洞察。搜索引擎的底层优化,核心不在于购买更快的硬件,而在于识别数据的价值差异——冷热分离、低价值数据隔离、索引裁剪——以更少的计算完成同样有效的检索。这是信息检索理论中「精度与效率权衡」原则的工程实践。
其五,服务拆分,是软件工程中单一职责原则在分布式系统中的自然推演,是系统工程中流水线思维方法的智慧结晶。福特将汽车制造从单工位装配拆解为流水线工序,每一道工序只做一件事,整条线的吞吐量因此飙升——微服务拆分遵循完全相同的逻辑。通过对不同资源瓶颈的服务进行解耦——计算密集型与 IO 密集型分离、延迟敏感与吞吐敏感分离——可以实现按需扩缩容和异构部署,避免不同资源争抢者互相拖累。而拆分的粒度判断——拆到哪一层为止——本质上是在模块内聚与网络开销之间求解一个含约束的最优化问题。
其六,水平扩容,是无状态服务弹性伸缩的理想形态,是对分布式系统八大透明性的极致追求。访问透明性、位置透明性、迁移透明性、重定位透明性、复制透明性、并发透明性、故障透明性和伸缩透明性——这八项目标定义了分布式系统设计的最高标准。无状态架构将全部会话状态、缓存状态、令牌桶状态外置到中间件集群中,使服务节点本身成为可随时替换或新增的无差别计算单元。在此基础上配合服务网格的流量治理能力,扩容不再是运维事件,而是系统的一项自治功能——负载上升时自动扩展,下降时自动收缩,对调用方完全透明。
以上六种方法共同构成了一个从「减少不必要计算」到「平滑必要负载」再到「弹性应对增长」的三层高并发治理框架。它们并非彼此独立的银弹——缓存的命中直接降低了后端链路请求量,异步解耦为服务拆分提供了通信基础,无状态化是水平扩容的前提,限流降级则是整个系统在面对不可控外部冲击时的最后一道防线。下面逐一展开这六个维度在本项目中的具体应用。
缓存设计的具体应用。 知识问答系统存在两类高价值缓存场景。第一是用户上下文缓存:每个问答请求需要用户所属租户、权限范围、可访问文档库列表等上下文信息,它们在单次会话中不变,体现了典型的时间局部性——刚刚查过的数据马上又会用到。我们在 API 网关层引入 Redis 缓存,以 tenantId:userId 为键,TTL 设置为会话超时时间,用户服务 QPS 降低了约 70%。第二是文档召回缓存:企业场景中高频问题(如「公司年假政策」「报销流程」)对应的文档集合相对稳定,这是空间局部性在业务语义层上的映射——相似问题对应相似的文档结果。我们将 Embedding 向量和召回结果以 queryHash → rankedDocIds 的形式缓存,命中时直接跳过 Embedding 和检索阶段。该缓存将热门问题的 TTFT 从 5 秒降至约 2 秒,命中率稳定在 38%。两类缓存均采用旁路缓存模式——读时先查 Redis,未命中则查询并将结果回填;写时直接失效对应缓存键而非更新缓存值,避免了缓存与源数据的不一致窗口。
异步处理的具体应用。 大语言模型的推理过程在学术上可分为 Prefill 和 Decode 两个阶段:Prefill 计算密集但延迟可预测,Decode 内存密集且受 KV-Cache 大小影响显著。两者在同一 GPU 上形成了典型的排队论困境——两类任务在同一个服务队列中等待,计算密集的长任务(Prefill)阻塞了 IO 密集的短任务(Decode),平均等待时间远超理论最优值。我们将 LLM 推理拆分为 Prefill 服务和 Decode 服务,通过 RocketMQ 实现两者的异步解耦:Prefill 完成计算后将 KV-Cache 的存储地址投递给消息队列,Decode 从共享存储加载 KV-Cache 后执行自回归生成,以 SSE 流式输出。消息队列在此扮演了拥塞控制器的角色——利用 RocketMQ 的 Tag 过滤特性按 tenantId 设置消息 Tag,付费租户配置独立 Consumer Group 和更高优先级的消费线程池,非付费租户共享默认 Consumer Group,实现了业务层面的资源隔离。KV-Cache 存储在基于 NVMe 的分布式共享存储中,传递延迟稳定在 30 毫秒以内,避免了每个 Decode 节点重复计算 Prefill 的开销。拆分后 Prefill 节点配置高算力 GPU(如 A100),Decode 节点使用成本更低的推理卡(如 T4/L4),两类任务不再相互阻塞——Prefill 队列积压时 Decode 仍可继续生成已有 KV-Cache 的请求,GPU 成本降低约 25%,系统整体吞吐量提升约 40%,TTFT 从 13 秒回落至 8 秒。
限流降级的具体应用。 我改造了经典的令牌桶算法,实现了一个基于 Redis 的分布式限流器。令牌桶的参数设计——补充速率决定了长期限流阈值,桶容量决定了突发容忍度——直接体现了排队论中服务速率与队列容量的 trade-off。每个租户在 Redis 中维护独立的令牌桶,付费租户的补充速率为其 SLA 承诺的 1.2 倍留有冗余,非付费租户共享一个全局免费令牌池,补充速率等于系统空闲资源。原子操作通过 Redis Lua 脚本实现,限流判定在 API 网关层完成,被限流的请求不进入后续链路——这与 IP 协议中路由器丢包而不告警的行为如出一辙:在最外层做出取舍,最大程度保护内部资源。非付费租户的降级体验设计尤为关键:降级模式下返回基于缓存文档片段的快速摘要,响应延迟约 200 毫秒,显著快于完整问答。用户主观感知上并未感到「被降级」——他们仍然得到了一个可用的回答,只是详细程度有所降低。线上数据表明,非付费用户的问题解决率在降级模式下约为 78%,与完整模式的 85% 差距在可接受范围内,高峰时段降级率控制在 5% 以内。
数据库优化的具体应用。 底层搜索引擎基于 Elasticsearch,ES 本身已实现了索引层面的读写分离,但在高并发场景下两类特殊消息带来了不合理负载。处理的思路是识别数据的价值差异——这正是信息检索中「并非每个文档都值得完整索引」原则的体现。一是机器人消息:企业协作平台中存在大量 CI 通知、监控告警、日报推送等自动化消息,量极大但检索价值很低。我们将机器人消息从主索引中剥离,单独建立索引并配置独立的低规格节点,在文档召回阶段设置 50 毫秒硬超时——超时即丢弃该索引的召回结果。二是历史消息索引:即时通讯消息随时间呈典型的冷热分明特征,近 30 天的消息贡献了 95% 以上的搜索请求,这是一个高度倾斜的帕累托分布。我们按时间范围做了分表——热索引部署在高性能 SSD 节点上,温索引和冷索引部署在 HDD 节点上并降低副本数。知识问答默认只检索热索引,仅在用户显式指定时间范围时才触发温冷索引查询,以最小的集群负载覆盖了绝大多数的用户需求。
服务拆分的具体应用。 Prefill-Decode 的拆分本质上是服务粒度的一次权衡,需要在模块内聚与网络开销之间求解最优边界。我们的判断依据有三条。第一,是否具备独立的扩缩容需求——Prefill 瓶颈在 GPU 算力(FLOPS),Decode 瓶颈在显存带宽和 KV-Cache 容量,两类资源的紧缺节奏完全不同,不拆分则流水线中最慢的工位决定了整条线的产出速率。第二,是否存在显著的资源争抢——GPU 显存是两者共享资源,Prefill 需要大量临时缓冲区而 Decode 需要持久 KV-Cache,合在一起时频繁触发显存碎片和 OOM,拆分后各自独立管理,故障域隔离。第三,网络通信开销是否可接受——KV-Cache 在 NVMe 共享存储下的传递延迟稳定在 30 毫秒以内,而整体推理延迟在秒级,额外开销在可接受范围内。基于同样三个条件,我们也评估了 Embedding 和重排序服务是否应该拆分——它们的计算延迟相对稳定(约 50-100 毫秒),不构成核心瓶颈,且可被多个业务线复用,拆分的独立扩缩容收益有限,反而引入额外的 RPC 和序列化开销。因此将其保留在搜索引擎单体内部,仅对 LLM 推理做了 Prefill-Decode 拆分。这是基于「瓶颈优先」原则的务实选择——只拆分那些不拆就无法独立优化的模块,而非追求极致的微服务化。
水平扩容的具体应用。 上述所有服务——Embedding、重排序、Prefill、Decode、API 网关——均设计为无状态,会话上下文、缓存、令牌桶和消息队列全部外置到 Redis 和 RocketMQ 中。这是分布式系统八大透明性中伸缩透明性的基础:只有节点本身不携带任何状态,扩容和缩容才可能对调用方完全透明。在此基础上,每个服务 Pod 旁路部署一个 Sidecar 代理,负责采集请求延迟、队列深度、CPU 和内存使用率等实时指标,控制面聚合为全局服务健康视图后按预设策略驱动自动扩缩容——Decode 服务在 P99 延迟超过 2 秒或请求队列深度超过 500 时触发扩容。服务网格的流量管理能力进一步提升了扩容的平滑性:新 Pod 经健康检查确认就绪后,通过加权轮询逐步引入流量,避免了冷启动期间的请求超时;单实例异常时流量自动转移,实现了故障透明性——对调用方而言,实例级别的故障被网格完全屏蔽,无需等待 Kubernetes 层面的 Pod 重建。付费租户的 Pod 通过节点亲和性固定在高配 GPU 节点上,两类租户的 Sidecar 指标携带租户标签,控制面据此分池管理,确保付费租户的扩容阈值优先触发。
经过上述六个维度的优化,系统在 DAU 从 2 万增长至 15 万的背景下,峰值 TTFT 从 13 秒恢复至 8 秒,缓存命中率从零提升至 38%,GPU 利用率从 45% 提升至 72%,非付费用户在高峰时段的降级率控制在 5% 以内,系统峰值 QPS 从 500 提升至 2500。回顾整个过程,六个优化维度之间存在着清晰的逻辑递进——缓存减少重复计算以降低总量,异步与拆分将剩余负载在时间与空间上摊平,数据库优化在存储层削减无效工作,无状态架构为水平扩容铺路,限流降级则是所有策略都失效时的最后防线。高并发系统的架构设计,与其说是解决一个性能问题,不如说是在资源约束下持续做出一系列有据可依的权衡——每一次「为什么这样而不是那样」的追问,推动着架构从能用到好用、从单点到体系的演进。
实践中也遇到了几个值得记录的问题。首先是运维复杂度的上升。Prefill-Decode 拆分以 30 毫秒的网络传递延迟换来了 40% 的吞吐量提升,但也引入了一条新的故障链路——KV-Cache 共享存储的稳定性成了整个推理链路的单点。一次 NVMe 存储节点的间歇性抖动,导致正在生成的数百个请求同时中断,用户端表现为「回答生成到一半突然停住」。事后我们为 KV-Cache 增加了本地 SSD 的冗余副本——写入共享存储的同时在 Prefill 节点本地保留一份,Decode 侧优先读共享存储、失败时通过消息队列向 Prefill 节点请求重传。这个补丁有效但不够优雅,本质上是拆分的代价在运维侧滞后显现。
其次是缓存一致性的边界情况。文档召回缓存的失效策略是「文档更新时直接失效对应缓存键」,但这个逻辑依赖文档管理系统的更新事件能准确、及时地触发。实际运行中,文档的批量导入和权限变更有时绕过了事件通知通道,导致缓存中残留了已过期的文档 ID 列表。用户偶尔会收到「点击结果后显示文档已删除」的体验中断——频率不高(约每周 2-3 次反馈),但对企业用户的信任度有直接影响。后来在缓存层增加了定时全量校验——每 30 分钟对热缓存键做一次来源文档的存活检查——以微小的性能开销换取了缓存与源数据的一致性保障。
第三个问题是 Prefill-Decode 分离后的 GPU 资源调度在业界的标准化仍在进行中,我们的方案带有较强的定制色彩。随着大模型推理框架(vLLM、SGLang 等)对 Disaggregated Prefill 的原生支持逐渐成熟,我们自行实现的拆分逻辑和消息队列调度层在未来可能被框架内置能力替代。意识到这一点后,我们将拆分调度层设计为与具体推理框架解耦的独立模块——未来切换到底层框架的 native 实现时,对上游的 API 网关和下游的 Decode 节点都是透明的。架构设计不仅要为当前问题负责,也要为自己的退场留好通路。
歌未竟,东方白。高并发系统的架构设计是一场永不停歇的博弈——业务规模在增长,硬件代际在更迭,大模型的推理范式仍在快速演进。今天看似最优的 Prefill-Decode 拆分策略,或许在下一个推理架构变革中就需要重新审视。但正是在这种持续的自我追问中,架构师的价值得以显现:不是交付一个完美的系统,而是在每一个技术代际中,以扎实的学科基础和清醒的权衡判断,做出当下最好的工程决策。能够在这样一个技术浪潮奔涌的时代从事系统架构工作,亲历大语言模型从实验品走向规模化服务的过程,将教科书中的理论化为生产环境中真实承载百万用户的系统,是莫大的幸运。不负这个伟大的时代。