2 系统分析与大型互联网架构设计

felix.shao2025-02-16

2 系统分析与大型互联网架构设计

1 概述

 主要介绍大型系统在设计时需要重点考虑的一些原则和设计要点。

2 系统分析原则:如何从全局掌控一个大型系统

2.1 高并发原则

 保证项目在高并发环境下的正常运行有两种方式。

  • 垂直扩展:通过软件技术或升级硬件来提高单机的性能。这种比较快,只需要购买性能更强大的硬件设备就能迅速提升性能,但是有极限。
  • 水平扩展:通过增加服务器的节点个数来横向扩展系统的性能。这个是大型互联网系统对高并发的最终解决方案。

 具体来讲,在技术层面,可以使用缓存减少对数据库的访问,用熔断或降级提高响应的速度,通过流量削峰等手段在项目的入口限流。
 衡量高并发的常见指标包括响应时间、吞吐量或 QPS、并发用户数等。

2.2 容错原则

 高并发常见会造成各种逻辑混乱的情景,我们需要对各种潜在的问题做好预案,如下一部分问题。

  • 使用 Spring Boot + Redis 实现分布式缓存。
  • 使用 MQ 实现分布式场景下的事务一致性。
  • 使用 MQ、RPG 模式、Token 等解决重复提交问题。
  • 使用 “去重表” 实现操作的幂等性。
  • 使用集群或 Zookeeper 解决失败迁移问题。
  • 隔离秒杀成独立的服务,防止秒杀带来的流量问题影响到系统中的其他服务。

2.3 CAP 原则

 CAP 原则是理解及设计分布式系统的基础,包含了 C(Consistency,一致性)、A(Availability,可用性)、P(Partition tolerance,分区容错性)三个部分。

  • 一致性 C:在同一时刻,所有节点中的数据都是相同的。
  • 可用性 A:在合理的时间范围内,系统能够提供正常的服务。
  • 分区容错性 P:当分布式系统中的一个或多个节点发生网络故障(网络分区),从而脱离整个系统的网络环境时,系统仍然能够提供可靠的服务。也就是说,当部分节点故障时,系统还能够正常运行。

 CAP 原则:在任何一个分布式系统中,C、A、P 三者不可兼得,最多只能同时满足两个。一般而言,分布式必然会遇到网络问题,分区容错性是最基本的要求。因此,在实际设计时,往往是在一致性和可用性之间根据业务来权衡。
 对于不同业务也会需要考虑到不同的 CAP 选择,以电商网站为例,会员登录、个人设置、个人订单、购物车、搜索用 AP,因为这些数据短时间内不一致不影响使用;后台的商品管理就需要 CP,避免商品数量的不一致;支付功能需要 CA,保证支付功能的安全稳定。

2.4 幂等性原则

 幂等性原则是对调用服务次数的一种限制,即无论对某个服务提供的接口调用多次或是一次,其结果都是相同的。
 以下是幂等性的两种解决方案思路。

  • 1 写操作之前先通过执行读操作来实现。
    • 1.1 读操作:查询支付服务中的支付状态(已支付或未支付)。
    • 1.2 写操作:若已支付,直接返回结果;若未支付,先执行支付操作,再返回支付结果。
  • 2 使用 “去重表” 方式来实现。
    • 2.1 每个操作在第一次执行时,会生成一个全局性唯一 ID,如订单 ID。
    • 2.2 在 “去重表” 中查询 “1” 中的 ID 是否存在。
    • 2.3 如果存在,直接返回结果;如果不存在,则再执行核心操作(如支付),并将 “1” 中的 ID 存入 “去重表” 中,最后返回结果。

 除了这两种方法,还可以通过 CAS 算法、分布式锁、悲观锁等方式实现幂等性。
 特殊的是,查询和删除操操作是不会出现幂等性问题的。
 幂等性和表单重复提交的主要区别是用户的操作意图不同。幂等性是由于网络等故障,用户不知道第一次操作是否成功,因此发送了多次重复操作,意图在于确保第一次的操作成功;而表单重复提交是指用户已经看到了第一次操作成功的结果,但是由于误操作或其他原因再次点击了 “刷新页面” 等功能按钮,导致多次发起相同的请求。可以通过 Token 令牌机制、RPG 模式、数据库唯一约束等方法避免表单的重复提交问题。

2.5 可扩展性原则

 项目的规模会随着用户数量的增加而增大,因此大型系统务必要再设计时就考虑到项目扩展的解决方案。
 可扩展原则要从项目架构、数据库设计、技术选型和编码规范等多方面考量。
 以下是实现可拓展原则的一些具体措施。

  • 定义项目插件的统一顶级接口,在扩展功能时使用方便扩展的集成自定义插件(如 MyBatis 插件开发的流程)。
  • 使用无状态的应用服务,避免开发后期遇到 Session 共享等数据同步问题。
  • 使用 HDFS 等分布式文件系统,在存储容量不足时迅速通过增加设备来扩容。
  • 合理地设计了数据库的分库分表策略及数据异构方式,就能快速进行数据库扩容。
  • 使用分布式或微服务架构,快速开发并增加新的功能模块。

2.6 可维护原则与可监控原则

 可维护原则是指系统在开发完毕后,维护人员能够方便地改进或修复系统中存在的问题。它包含了可理解性、可修改性和可移植性等多方面因素,可拓展性原则也可以归纳为可维护原则中的一个细分领域。通常可以从以下方面来实现项目的可维护原则。

  • 项目的日志记录功能完善,易于追溯问题、统计操作情况。
  • 有 BugFree 等 BUG 管理工具。
  • 有丰富的项目文档和注释。
  • 统一的开发规范。
  • 使用模块化的编程模式。

 可监控原则是指对系统中的流量、性能、服务、异常等情况进行实时监控。此外,还需要对项目中的一些关键技术做性能的监控,确保新技术的引入的确能带来性能的提升。

3 系统设计要点:在设计阶段提前规避问题

 有缺陷的架构设计可能会导致后期的开发工作十分艰难,甚至会造成 “推到重来” 的情形。因此,我们在设计阶段应尽可能地规避可能会遇到的各种问题。下面是几个经典的问题。

3.1 Session 共享问题

 概念略。
 共享 Session 对象有以下几种方式。

  • Session Replication。同步 session 到其他节点。这种方案会引起广播风暴(同步到其他所有节点,增大了网络开销)和严重的冗余。
  • Session Sticky。通过 Nginx 等负载均衡对各个用户进行标记(如对 Cookie 标记),使每个用户都请求固定的服务节点。当某个节点沓机,那么该节点上的所有 Session 对象都会丢失。
  • 独立 Session 服务器。如使用数据库、各种分布式或集群存储系统存储 Session。

3.2 优先考虑无状态服务

 在使用了 “独立 Session 服务器” 后,应用服务就是一种 “无状态服务”,换句话说,此时的应用服务与用户的状态是无关的。
 这里的 “状态” 不仅仅是 Session,也可以是任意类型的数据(结构化数据、非结构化数据)、文件等。
 “无状态服务” 有很多优势,如下。

  • 数据同步。不需要数据同步。
  • 快速部署。因为是 “无状态” 的,所以很容易对应用服务进行横向扩展。

 另一方面,将带有数据的服务设置为 “有状态”,并进行集群的 “集中部署”(如 MySQL 集群),从而降低集群内部数据同步带来的延迟。“集中部署” 是指尽可能地将相同或相关的数据、业务部署在同一机房中,利用内网提高数据的传输速度,尽量避免跨机房调用。

3.3 技术选型原则与数据库设计

 在做技术选型时,要综合考量待选技术的性能、安全性、并预估这些技术是否有足够长的生命力、能否快速上手等。
 一种数据库选型的思路如下。

  • 搭建高可用的 Redis 集群,并通过主从同步进行数据备份、通过读写分离降低并发写操作的冲突、通过哨兵模式在 Master 挂掉后选举新的 Master。
  • 搭建双 Master 的 MySQL 集群,并通过主从同步做数据备份。
  • 通过 MyCat 对大容量的数据进行分库 / 分表,并控制 MySQL 的读写分离。
  • 通过 Haproxy 搭建 MyCat 集群。
  • 通过 Keepalived 搭建 Haproxy 集群,通过心跳检测机制防止单节点故障;并且 Keepalived 可以生成一个 VIP,并用此 VIP 与 Redis 建立连接。

 在实际进行数据库开发时,还需要合理使用索引技术及适当设置数据库的各项性能参数,从而最大限度地优化数据访问操作。

3.4 缓存穿透与缓存雪崩

 缓存可以在一定程度上缓解高并发造成的性能问题,但在一些特定场景下缓存自身也会带来一些问题,比较典型的就是缓存穿透与缓存雪崩问题。

3.4.1 缓存穿透

 是指大量查询一些数据库中不存在的数据,从而影响数据库的性能。
 解决思路举例如下。

  • 拦截非法的查询请求,仅将合理的请求发送给 MySQL。例如:可以使用验证码、IP 限制等手段限制恶意攻击,并用敏感词过滤器等拦截不合法的非法查询。
  • 缓存空对象。
  • 建立数据标识仓库。类似布隆过滤器除了逻辑,当数据一定存在时,才去 MySQL 中查询,不过可能因为 hash 值在概率上可能相同,可能会漏掉对个别数据的拦截。

3.4.2 缓存雪崩

 缓存雪崩是指由于某种原因造成 Redis 突然失效,造成 MySQL 瞬间压力剧增,进而严重影响 MySQL 性能,甚至造成 MySQL 服务沓机。
 缓存雪崩的两个常见原因是:Redis 重启和 Redis 中的大量缓存对象都设置了相同的过期时间。具体的解决方案可参考如下。

  • 搭建 Redis 集群,保证高可用。
  • 避免大量缓存对象的 Key 集中失效,尽量让过期时间分配均匀一些。例如,可以将各个缓存的过期时间乘以一个随机数。
  • 通过队列、锁机制等控制并发访问 MySQL 的线程数。

3.5 综合因素

 还需要考量项目各个功能模块是否都符合相关法律法规,项目组员之间是否由足够的沟通,项目的迭代周期是否合适,各种技术中的性能参数如何优化,如何根据项目情况决定测试与实施的方式,如何在不同的技术之间做平滑迁移,客户在使用时是否有方便的反馈渠道,项目的开发成本是否符合预期等问题。

4 大型系统的演进

 主要如下。

  1. 不同类型的服务器。大型系统一般需要 3 台服务器:应用服务器、数据库服务器和文件服务器。并且不同类型的服务器对硬件的需求也各不相同。
  2. 集群服务与动静分离。如静态资源用 CDN,动态资源用应用服务器,之后如果并发数继续增大等,就可以使用分布式对项目进行拆分。
  3. 分布式系统。将一个系统拆分为多个模块并部署到不同的计算机上。有分布式应用、分布式文件系统、分布式数据库系统。
  4. 提高数据的访问性能。如使用缓存,缓存有本地缓存和远程缓存,远程缓存需要进行远程 IO 操作,因此缓存的速度比本地缓存慢。缓存没命中,查询数据库时,可以使用主从同步、搜索引擎、大数据技术进行处理。
  5. 跨语言 RPC 整合。每个语言擅长的领域不同,不同部门开发的服务可能是基于不同的编程语言,此时可以使用 Thrift、gRPC 等 RPC 技术将这些服务进行整合。注意 RPC 会给整个系统增加一层跨语言的中转结构,因此必然会带来一定程序的性能损耗。

5 大型系统架构设计

 在设计大型系统的架构时,要特别注意对流量的控制,可以采取降级、限流和缓存等策略。

5.1 服务预处理:限流与多层负载

 以下是几种处理客户端海量请求的思路,仅供参考,详细内容见原书,还有配套的图片。

  • 1 拦截非法请求,从而进行一定程度的限流。
    • 1.1 加入验证码,防止机器人恶意攻击。
    • 1.2 隐藏秒杀入口地址,确保用户在进行了合理的操作后才能进入。
    • 1.3 限制 IP 操作:对于秒杀等限量服务,限制某一 IP 能够发起请求的次数。
    • 1.4 延长用户操作时间:为避免用户刷单,可以对具体业务在操作时间上进行限制,如同一用户 5s 内只能进行一次操作。
  • 2 通过 LVS (TCP 层) 对客户请求进行分流(负载均衡),可以对具体业务在操作时间上进行限制,如同一用户 5s 内只能操作一次。
  • 3 通过 Nginx 将请求进行动静分离:将静态请求部署到 CDN 上进行加速,将动态请求发送给 MQ 进行流量削峰。
  • 4 可以将同一个服务部署到多个节点时尚,形成集群服务,并用 Nginx 进行整合,用来实现并发请求的负载均衡和失败迁移。
  • 5 使用 Maven 或 Gradle 进行依赖管理,并使用 Git/Github 进行版本控制和团队协作开发。
  • 6 进一步扩容。上面架构扛得住千万级流量了,想支持亿级流量可以再进行扩充。
    • 6.1 通过 DNS 绑定多个 LVS 组成的集群,进一步实现负载均衡,即横向拓展集群。
    • 6.2 通过 Nginx 将动态请求的路径转发到特定的服务地址上。即不同路由分流到不同服务器。

TIP

 关于 Nginx 还需要注意以下两点。

  1. Nginx 处于 OSI 网络模型的第七层(应用层),除了实现动静分离外,还可以用于各种策略的负载均衡(如轮询、指定访问的 URL 规则);而 LVS 处于 OSI 的第四层(传输层),是利用 Linux 内核直接对流量进行转发,可靠性高并且对 CPU 及内存的消耗低。
  2. Nginx 还可以实现定时器、封禁特定 IP/UA 等功能,并且可以通过缓存、Lua 插件等实现一些简单的功能,直接处理一部分用户请求,从而减轻服务端的压力。

 如果系统不是很大,就没有必要使用以上所有组件。实际上,根据 OSI 七层网络模型,我们可以在第二层、第三层、第四层和第七层等多层分别进行负载,然后再把它们穿起来,形成级联负载。

5.2 各组件的技术选型建议

 海量请求在经过多级限流和多层负载后,就会抵达应用服务进行实际的处理。
 以下是一些建议。

  1. 通过 Zuul 再次屏蔽某些不合理请求,或生成各个微服务的虚拟地址。
  2. 发来的请求经过 SpringMVC 等控制器进行调整,并通过 Ribbon 或 Feign 进行客户端负载均衡,发送给一个压力较轻的服务端处理。
  3. 控制器根据具体情况,可以请求 Eureka 中已注册的微服务,也可以通过 gRPC、Thrift、Netty + Protobuf 等 RPC 方式整合其他语言提高的服务。
  4. 微服务可以使用 Spring Boot 进行快速构建,并用 Spring Cloud 组装各个微服务。
  5. 对于不同的服务,适当选取相应的技术。
  6. 各模块、各服务之间使用 Restful 风格相互调用,并通过 JSON 格式返回处理结果。

6 分布式 ID 生成器

 保证 ID 不会冲突,一般有 3 种解决方案。

  1. 使用数据库的自增特性。
  2. 使用 UUID 算法产生 ID 值。
  3. 使用 SnowFlake 算法产生 ID 值。

 我们主要是使用 SnowFlake 算法,其也被称为雪花算法。该算法回生成一个 64bit 的整数,但实际只使用 63bit,共可以表示 2^63 个 ID 值。
 Java 中可以用 Long 表示 SnowFlake 生成的 ID,其结构如下。

0 0............0 0000000000 000000000000
  41 位时间戳    10 位机器码 12 位序列号(递增)
  • 41 位时间戳。某一时刻的时间戳(毫秒)。
  • 10 位机器码。
  • 12 位序列号(递增)。

参考文献

  • [亿级流量 Java 高并发与编程实战]
Last Updated 2/16/2025, 4:13:06 PM