文章

高并发系统设计与演进实战指南

高并发系统设计与演进实战指南

极客时间《高并发系统40讲》总结

第一章:高并发系统的设计理念与目标

在互联网业务飞速发展的今天,高并发系统设计已成为架构设计的核心挑战。设计一个优秀的系统,必须围绕三个核心目标展开:高性能、高可用、可扩展

1.1 系统设计的三大目标

  1. 高性能 (High Performance) 性能是系统设计的基石。它是指系统能够以更短的响应时间处理更多的并发请求。
    • 度量指标:主要包括吞吐量(Throughput)*和*响应时间(Response Time)。这两个指标呈倒数关系。通常使用平均值、最大值以及分位值(如 99 分位值,即 99% 的请求响应时间在多少毫秒以内)来衡量。
    • 优化原则:性能优化应遵循“问题导向”(不盲目优化)、“八二原则”(20% 的优化解决 80% 的瓶颈)、“数据支撑”以及“持续优化”。
    • 阿姆达尔定律:该定律描述了通过增加处理核心数来提升性能的极限,提示我们在并行计算中,串行部分将决定系统的加速上限。
  2. 高可用 (High Availability, HA) 高可用指系统具备较高的无故障运行能力,通常用“N 个 9”来衡量。
    • 度量:Availability = MTBF / (MTBF + MTTR)。MTBF 为平均故障间隔时间,MTTR 为平均恢复时间。核心系统通常要求达到四个 9(99.99%),即年故障时间不超过 1 小时。
    • 设计原则:Design for failure(为失败而设计)。必须预设故障必然发生,从而设计自动发现和恢复机制。
  3. 可扩展性 (Scalability) 指系统可以通过增加机器资源的方式线性提高处理能力。
    • 瓶颈:无状态服务(如 Web 层)易于扩展,而有状态组件(如数据库)扩展难度大。
    • 拆分思路:通过业务维度(垂直拆分)和数据维度(水平拆分)将复杂系统解耦。

1.2 高并发设计的通用方法

应对高并发大流量的冲击,业界通用的三种方法论包括:

  1. Scale-out(横向扩展):分而治之,通过分布式部署将流量分流到多台服务器上。
  2. 缓存:利用内存等高速介质提升读取性能,降低数据库压力,遵循“空间换时间”思想。
  3. 异步:在未处理完成前先返回,后续通过回调或消息通知,减少调用方阻塞,削峰填谷。

第二章:数据库架构演进与优化

随着流量增加,数据库往往是最先遇到的瓶颈。演进过程通常从单机优化开始,逐步走向分布式架构。

2.1 数据库连接池化

在系统初期,频繁创建和销毁数据库连接会消耗大量资源(TCP 握手、认证)。

  • 解决方案:引入数据库连接池,预先建立连接并复用。
  • 关键配置:需合理设置最小连接数和最大连接数。如果连接长时间空闲,数据库可能会主动关闭,因此连接池需具备定期检测连接可用性(如发送 select 1)的机制。

2.2 主从读写分离

大部分互联网业务呈现“读多写少”的特征。

  • 架构:将数据库分为主库(Master,负责写)和从库(Slave,负责读)。主库将 Binlog 同步给从库,从库重放实现数据复制。
  • 挑战主从延迟。数据写入主库后,从库可能尚未同步,导致业务读不到最新数据。
  • 应对策略
    1. 数据冗余:关键数据在消息队列中携带,避免回查数据库。
    2. 缓存:写主库同时写缓存,读请求优先读缓存。
    3. 强制读主:对于一致性要求极高的场景(如注册后立即登录),强制查询主库,但需谨慎使用以免压垮主库。

2.3 分库分表(Sharding)

当单表数据量达到千万级或亿级,或者单库写入成为瓶颈时,必须进行拆分。

  • 垂直拆分:按业务模块将表拆分到不同的数据库(如用户库、订单库),实现专库专用和故障隔离。
  • 水平拆分:将单表按规则拆分到多个库表中。
    • 哈希拆分:按 ID 哈希取模,数据分布均匀,但扩容迁移复杂。
    • 区间拆分:按时间或 ID 区间拆分,扩容简单,但容易产生热点数据。
  • 引入的问题
    • 分区键限制:查询必须带上分区键,否则会导致全库扫描。
    • Join 与聚合限制:跨库 Join 性能极差,Count() 需要单独计数表支持。
    • ID 全局唯一性:自增 ID 失效,需引入分布式 ID 生成器。

2.4 分布式 ID 生成:Snowflake 算法

为了解决分库分表后的 ID 问题,推荐使用 Snowflake(雪花)算法。

  • 结构:1 位符号位 + 41 位时间戳 + 10 位机器 ID + 12 位序列号。
  • 优势:全局唯一、单调递增(有利于 B+ 树索引性能)、包含业务含义(时间、机房信息)。
  • 注意事项:严重依赖系统时间,需处理时钟回拨问题。低并发下可能导致 ID 末位总是偶数,造成分片不均,可随机起始序列号解决。

2.5 NoSQL 的互补应用

关系型数据库在某些场景下存在短板,需引入 NoSQL 作为补充。

  • 写入性能:利用 LSM 树结构(如 HBase)将随机写转换为顺序写,提升写入吞吐。
  • 全文搜索:使用 Elasticsearch 利用倒排索引解决模糊查询(如 LIKE %keyword%)无法走索引的问题。
  • 扩展性:MongoDB 等原生支持 Replica Set 和 Sharding,扩容更加自动化。

2.6 数据库迁移方案

在架构升级(如上云、分库分表)时,数据迁移必须保证平滑和数据一致性。

  • 双写方案
    1. 新库配置为旧库从库同步历史数据。
    2. 业务改造,开启双写(异步写新库),并记录写入新库失败的日志用于补偿。
    3. 数据校验,抽取数据比对。
    4. 灰度切换读流量,最终切断旧库写入。
  • 级联同步:适合全量迁移上云。旧库 -> 新库 -> 备库,通过主从复制层层同步,切换时只需短暂亦可。

第三章:缓存架构设计

缓存是提升系统性能的“银弹”,通过将热点数据存储在内存中,极大降低数据库压力。

3.1 缓存分类

  • 静态缓存:CDN、Nginx 静态页面,用于加速静态资源(图片、视频、JS/CSS)访问。
  • 分布式缓存:Redis、Memcached,用于加速动态请求,通过集群突破单机内存限制。
  • 本地缓存:Guava Cache 等,部署在应用进程内,无需跨网络,用于阻挡极端热点查询(如秒杀前几秒的配置读取)。

3.2 缓存读写策略

  • Cache Aside(旁路缓存):业界最常用策略。
    • :先读缓存,命中返回;未命中读 DB,然后回种缓存。
    • :先更新 DB,后删除缓存
    • 注意:先删缓存后写库会导致并发下的脏数据;先更新 DB 后更新缓存也可能因并发导致不一致。推荐配合分布式锁或较短的过期时间使用。
  • Read/Write Through:应用只与缓存交互,缓存负责同步 DB。
  • Write Back:只写缓存,异步刷新到 DB(如 Linux Page Cache)。性能最好,但在掉电等故障下会丢失数据。

3.3 缓存高可用

  • 客户端分片:一致性 Hash 算法(带虚拟节点),减少节点变动时的数据漂移。
  • 中间代理:Codis、Twemproxy,代理层负责路由和分片。
  • 服务端方案:Redis Sentinel(哨兵)实现主从自动切换;Redis Cluster 实现去中心化的分片和高可用。

3.4 常见痛点与解决方案

  1. 缓存穿透:查询不存在的数据,请求直击数据库。
    • 解决:回种空值(设短过期)或使用布隆过滤器(Bloom Filter)拦截。
  2. 缓存击穿 (Dog-pile effect):热点 Key 失效,大量并发请求穿透。
    • 解决:分布式锁(只允许一个线程回源加载),或后台线程定时更新。
  3. 缓存雪崩:大量缓存同时失效。
    • 解决:过期时间加随机值,多级缓存架构。

3.5 CDN 加速

CDN 利用 DNS 的 CNAME 解析和 GSLB(全局负载均衡),将用户请求调度到最近的边缘节点,解决跨地域网络延迟问题。对于静态资源(图片、视频),CDN 是必选项。


第四章:消息队列 (MQ)

消息队列是分布式系统中的“缓冲剂”和“解耦剂”。

4.1 核心作用

  • 削峰填谷:在秒杀等场景下,暂存瞬时流量,避免数据库崩溃。通过控制消费速度保护后端。
  • 异步处理:将非核心流程(如发货后发送通知、积分处理)移出主流程,降低响应时间。
  • 解耦合:生产者和消费者仅依赖队列,互不感知,便于独立扩展。

4.2 可靠性保证

  • 防止消息丢失
    • 生产端:失败重试。
    • 存储端:配置同步刷盘或 ISR 机制(如 Kafka acks=all)保证多副本同步。
    • 消费端:确保业务逻辑处理完成后再提交 Offset(消费进度)。
  • 防止重复消费(幂等性)
    • 网络抖动会导致 ACK 丢失从而重发消息。
    • 解决:业务层必须实现幂等。例如使用唯一业务 ID 在数据库检查是否已处理,或利用数据库乐观锁(版本号)更新。

4.3 性能与延迟监控

  • 零拷贝:利用 Sendfile 技术,直接从内核缓冲区发送到网卡,减少 CPU 上下文切换和拷贝次数。
  • 监控:利用 JMX 或生成“监控消息”定期写入并检测消费延迟,对消息堆积进行报警。
  • 优化:增加 Partition(分区)以增加并发度,或在 Consumer 内部使用线程池并行处理。

第五章:分布式服务架构 (微服务)

当单体架构面临研发效率低、部署困难、连接数瓶颈时,微服务化是必然选择。

5.1 服务拆分原则

  • 高内聚低耦合:每个服务职责单一,关注点分离。
  • 先粗后细:逐步拆分,优先剥离独立性强的边界服务(如短信服务)。
  • 接口兼容:参数封装,避免破坏性变更。

5.2 核心组件

  1. RPC 框架:解决跨进程通信。
    • IO 模型:推荐同步多路 IO 复用(如 Netty, Epoll),避免阻塞。
    • 序列化:Protobuf/Thrift(性能好、跨语言、字节数小),JSON(通用但性能稍差)。
  2. 注册中心:服务自动注册与发现(Service Discovery)。
    • 机制:心跳检测服务健康状态。需防范“通知风暴”和网络抖动导致的节点过度摘除(设置自我保护阈值)。
  3. API 网关:系统的统一入口。
    • 功能:路由转发、协议转换、认证鉴权、限流熔断、黑白名单。
    • 实现:Zuul, Kong。采用责任链模式扩展功能,利用 IO 多路复用提升性能。

5.3 Service Mesh

为了解决跨语言服务治理复用难的问题,Service Mesh 将治理逻辑下沉到 Sidecar 代理(如 Envoy),业务代码只需与 Localhost 通信。

  • 实现方式:iptables 流量劫持或轻量级客户端。
  • 代表:Istio, Linkerd。

第六章:系统治理与维护

高可用系统不仅在于设计,更在于运行期的治理。

6.1 服务保护机制

  • 熔断 (Circuit Breaker):当调用下游服务失败率达到阈值时(如 50%),暂时断开调用,快速返回错误,防止级联故障(雪崩)。状态机:关闭 -> 打开 -> 半打开。
  • 降级:主动关闭非核心服务(如评论、推荐),释放资源保护核心业务(如下单)。可通过配置中心动态开关实现。
  • 限流:限制并发请求数,防止系统过载。
    • 算法:令牌桶(Token Bucket,允许突发流量)、漏桶(Leaky Bucket,平滑流量)、滑动窗口。
    • 部署:网关层(全局限流)或 RPC 客户端层。

6.2 负载均衡

  • 分类
    • 四层负载:LVS,性能高,工作在传输层。
    • 七层负载:Nginx,灵活,工作在应用层。
    • 客户端负载:Ribbon/Dubbo 内置。
  • 策略:轮询、加权轮询、最小活跃数、一致性 Hash。推荐使用动态策略(如最小活跃数)以感知后端负载。

6.3 监控与配置

  • 监控体系
    • 指标:Google 四个黄金信号(延迟、通信量、错误、饱和度)。
    • APM:端到端监控,采集客户端网络、卡顿数据。
    • 分布式追踪:利用 TraceIdSpanId 串联跨服务调用,解决微服务下故障定位难的问题。
  • 配置中心:如 Apollo、Nacos。实现配置的动态热更新,无需重启服务。需设计多级缓存(内存+文件)保证配置中心宕机不影响应用启动。

6.4 全链路压测

  • 目的:在双 11 等大促前发现瓶颈。
  • 核心技术
    • 流量隔离:使用“影子库”(Shadow DB),压测数据带标记,路由到影子库,不污染线上数据。
    • 数据构造:基于线上真实流量回放或脱敏拷贝。

第七章:高可用架构演进

7.1 多机房部署

  • 同城双活:机房延迟低(<3ms)。DB 主库在一个机房,备机房读从库。容灾级别:机房级。
  • 异地多活:机房延迟大(>30ms)。
    • 核心原则:数据闭环。用户按 ID 分片到不同“单元”,所有读写在单元内完成。
    • 数据同步:跨机房异步复制,容忍数据短时间不一致。

第八章:典型案例实战

8.1 计数系统 (微博场景)

  • 挑战:万亿级数据,高并发写。
  • 演进
    1. MySQL:性能差,分库分表也难抗。
    2. RedisIncr 原子操作。优化存储结构,减少指针开销;使用 Hash 结构存储多条计数。
    3. 冷热分离:SSD + 内存。热数据在内存,冷数据 Dump 到磁盘(如 Pika 方案),降低成本。

8.2 信息流系统 (Feed)

  • 推模式 (Push/写扩散):发微博时写入所有粉丝的“收件箱”。读性能好,但大 V 发博写压力巨大,不仅延迟高且存储成本高。
  • 拉模式 (Pull/读扩散):只写“发件箱”。用户读时聚合关注人的发件箱。写性能好,但读聚合成本高,缓存带宽压力大。
  • 推拉结合
    • 大 V 发微博:只推送给在线/活跃粉丝,或不推送。
    • 普通用户发微博:推给所有粉丝。
    • 读取时:合并拉取的大 V 内容和推送的普通内容。

8.3 未读数系统

  • 全量通知(如系统公告):不给每个人存未读数。只记录用户最后读取的全局消息 ID,未读数 = 最新全局 ID - 用户 Last Read ID。
  • 信息流未读:记录用户关注关系中所有人的博文总数快照。未读数 = (当前关注人总博文数) - (快照中的总数)。

总结

高并发系统的演进是一个持续的过程,遵循“从简单到复杂”、“问题驱动”的原则。

  1. 初期:通过垂直扩展 (Scale-up) 和简单的缓存连接池解决问题。
  2. 中期:引入读写分离分库分表解决存储瓶颈,利用消息队列削峰解耦,将单体应用拆分为微服务
  3. 后期:建设完善的监控治理(限流熔断)、压测体系,并向Service Mesh异地多活等高级架构演进。

每一个技术组件(Redis、Kafka、MySQL、Nginx)都有其适用场景和副作用(如一致性问题),架构师的核心职责就是在业务需求、成本、性能和复杂度之间做权衡(Trade-off)。

本文由作者按照 CC BY 4.0 进行授权