系统设计 System Design -3-1-系统设计权衡-一致性/可用性@延迟/吞吐量@架构的复杂性/组件的职责

92 阅读20分钟

强一致性与最终一致性

image.png 对数据准确性有严格要求的应用程序(例如金融系统)通常会选择强一致性

而为了获得更好的性能和可用性而能够容忍一些暂时不一致的应用程序(例如社交媒体信息流)则可能会选择最终一致性

  • 一致性保证:强一致性确保所有用户同时看到相同的数据,而最终一致性允许数据在一段时间内不一致,但最终变得统一。
  • 性能与一致性:强一致性优先考虑一致性,这会影响性能和可扩展性。最终一致性优先考虑性能和可用性,但会牺牲即时数据一致性。

延迟与吞吐量

🆚低延迟对于需要快速响应时间的应用程序至关重要,而高吞吐量对于处理大量数据的系统至关重要。

延迟与延迟或时间有关,侧重于速度。吞吐量与工作量或数据量有关,侧重于容量

如何改善延迟

  1. 优化网络路由:使用内容分发网络 (CDN) 从地理位置更靠近用户的位置提供内容。这可以减少数据传输距离,从而降低延迟。
  2. 缓存频繁访问的数据:将频繁访问的数据缓存在内存中,从而无需反复从原始源获取数据。
  3. 升级硬件:更快的处理器、更大的内存和更快的存储(如 SSD)可以减少处理时间。
  4. 使用更快的通信协议:HTTP/2 等协议可以通过多路复用和标头压缩等功能减少延迟。
  5. 数据库优化:使用索引、优化查询和内存数据库来减少数据访问和处理时间。
  6. 负载平衡:在服务器之间有效地分配传入请求,以防止任何单个服务器成为瓶颈。
  7. 代码优化:优化算法并删除不必要的计算以加快执行速度。
  8. 最小化外部调用:减少应用程序中的 API 调用或外部依赖的数量。

如何提高吞吐量

  1. 水平扩展:添加更多服务器来处理增加的负载。这通常比垂直扩展(升级单台服务器的容量)更有效。
  2. 实现缓存:将经常访问的数据缓存在内存中,以减少重复处理数据的需要。
  3. 并行处理:使用并行计算技术,同时划分和处理任务。
  4. 批处理:对于非实时数据,批量处理比单独处理每个项目更有效率。
  5. 优化数据库性能:确保高效的数据存储和检索。这可能包括分区和分片等技术。
  6. 异步处理:对于不需要立即完成的任务使用异步流程。
  7. 网络带宽:增加网络带宽以适应更高的数据传输速率。

数据库中的 ACID 与 BASE 属性

  • 一致性和可用性:ACID 优先考虑每个事务的一致性和可靠性,而 BASE 优先考虑系统可用性和分区容忍度,允许一定程度的数据不一致
  • 系统设计:ACID一般用于传统的关系型数据库,而BASE常与NoSQL、分布式数据库相关。
  • 用例对齐:ACID 非常适合需要强数据完整性的应用程序(银行或金融系统),而 BASE 更适合需要高可用性和可扩展性的大规模应用程序(社交网络或电子商务产品目录)。

直读缓存与直写缓存

两种用于管理缓存和主存储系统之间数据同步的缓存策略

  • 对于读取性能至关重要且可以在首次读取请求时将数据加载到缓存中的场景,直读缓存是最佳选择。
  • 对于写入操作中数据完整性和一致性至关重要的应用,直写缓存则更为适用。

这两种策略都能提升性能,但在数据处理方面有所不同——直读注重读取效率,而直写则注重写入的可靠性。

批处理与流处理

  • 批处理适用于不需要立即采取行动的大规模数据处理任务,例如财务报告。
  • 流处理对于实时应用至关重要,例如监控系统或实时分析,因为即时数据处理和快速决策至关重要。

数据处理:批处理是随着时间的推移积累大量数据后进行处理,而流处理是连续实时地处理数据。

及时性:批处理适用于不需要立即处理数据的场景,而流处理则适用于需要根据传入数据立即采取行动的情况。

复杂性和资源:与批处理的更直接和有计划的性质相比,流处理通常更复杂且资源密集,可满足实时数据的需求。

负载均衡器与 API 网关

在复杂的 Web 架构中:

  • 负载均衡器将用于在多个服务器或服务之间分配传入流量,从而提高性能和可靠性。
  • API网关将成为客户端与后端 API 或微服务交互的入口点,提供统一的接口,处理各种跨切关注点,并降低客户端应用程序的复杂性。

API 网关之前的负载均衡器

  • 最常见的设置:负载均衡器位于 API 网关之前。这是许多架构中的典型配置。

  • 功能:负载均衡器在 API 网关的多个实例之间分配传入流量,确保没有单个网关实例成为瓶颈。

  • 好处

    • 高可用性:此设置增强了 API 网关的可用性和可靠性。
    • 可扩展性:促进 API 网关实例的水平扩展。
  • 示例:在基于云的微服务架构中,外部流量首先到达负载均衡器,负载均衡器随后将请求路由到多个 API 网关实例之一。之后,选定的 API 网关实例会处理请求,与相应的微服务通信,并返回响应。

API 网关后的负载均衡器

  • 替代配置:在某些情况下,API 网关可以放在负载均衡器前面,尤其是当负载均衡器用于将流量分发到各个微服务或后端服务时。
  • 功能:API 网关首先处理并将请求路由到内部负载均衡器,然后将请求分发到适当的服务实例。
  • 用例:当 API 网关后面的不同服务需要自己的负载平衡逻辑时很有用。

混合方法:某些架构可能在 API 网关之前和之后的两端都有负载均衡器。

API 网关与直接服务公开

  • API 网关有利于统一对分布式系统的访问并简化客户端交互,因此非常适合复杂的大规模微服务架构。
  • 直接公开服务在延迟方面更高效,架构也更简单,但会给客户端带来更大的负担,需要管理与多个服务的交互。

接触点:API 网关为访问多个服务提供了单一接触点,而直接服务暴露则需要客户端与多个端点进行交互。

跨切关注点:API 网关集中了安全性和速率限制等常见功能,而在直接服务公开中,这些问题由每个服务处理。

代理与反向代理

  • 代理面向客户端,管理传出流量和用户访问
  • 反向代理面向服务器,管理传入服务器基础架构的流量

image.png

API 网关与反向代理

  • API 网关更侧重于在微服务架构中管理、路由和编排 API 调用
  • 而反向代理则侧重于常规服务器效率、安全性和网络流量管理

API 网关: 在其核心,就是一个功能极其丰富的、专门为 API 和微服务优化的反向代理,自己就可以实现负载均衡

Nginx :作为“边缘代理” 永远在 API 网关的前面,作为第一道防线。

SQL 与 NoSQL

  • 当您需要强 ACID 合规性并且数据结构清晰一致时,请使用 SQL 。
  • 当您处理大量数据或需要数据模型的灵活性时,请使用 NoSQL 。

SQL:MySQL、PostgreSQL、Oracle、Microsoft SQL Server

NoSQL:MongoDB(文档)、Redis(键值)、Cassandra(宽列)、Neo4j(图形)

  1. 数据结构:SQL 需要预定义的模式;NoSQL 更灵活。
  2. 扩展:SQL 垂直扩展(需要更强大的硬件),而 NoSQL 水平扩展(跨多台服务器)。
  3. 事务:SQL 提供强大的事务功能,非常适合复杂查询。NoSQL 提供有限的事务支持,但在速度和可扩展性方面更胜一筹。
  4. 复杂性:SQL 可以处理复杂的查询,而 NoSQL 针对查询的速度和简单性进行了优化。

主副本与对等复制

主副本和对等复制是分布式系统中两种不同的数据复制方法。

主副本提供了简单性和读取可扩展性,使其适用于传统的数据库应用程序。

对等复制则提供了针对故障和负载分配的稳健性,非常适合去中心化网络。

区别:

  • 控制和流:在主-副本复制中,主节点控制写入操作,数据从主节点到副本的流向清晰。在对等复制中,每个节点都可以执行读写操作,数据流是多向的。
  • 架构:Primary-Replica 遵循更集中的架构,而 Peer-to-Peer 则是分散的。
  • 用例:主副本模式非常适合需要扩展读取密集型操作的应用程序。点对点模式适用于去中心化和负载分配至关重要的分布式网络,例如文件共享或区块链技术。

数据压缩与数据重复数据删除

数据压缩和数据重复数据删除是用于优化数据存储的两种技术。

数据压缩有助于减小单个文件的大小,从而提高存储和传输效率。

重复数据删除技术则非常适合需要多次存储或备份相同数据的大规模存储系统。

区别:

  • 减少方法:数据压缩通过消除文件中的冗余来减小文件大小,而数据重复数据删除则消除整个系统中的冗余文件或数据块。
  • 范围:压缩适用于单个文件或数据流,而重复数据删除适用于更大的数据集或存储系统。
  • 恢复:压缩数据可以解压回其原始形式,但重复数据删除后的数据依赖于对原始数据的引用进行恢复。

服务器端缓存与客户端缓存

服务器端缓存可有效降低服务器负载并加快服务器数据传输速度。

客户端缓存则通过减少加载时间和支持离线内容访问来提升最终用户体验。

区别:

  • 缓存位置:服务器端缓存发生在服务器上,使所有用户受益,而客户端缓存特定于单个用户的设备。
  • 数据新鲜度:服务器端缓存可以集中管理数据新鲜度,而客户端缓存如果没有正确更新,可能会提供过时的数据。
  • 资源利用率:服务器端缓存充分利用服务器资源,非常适合多用户使用的数据。客户端缓存利用客户端资源,非常适合用户特定数据或静态数据。

REST 与 RPC

用于设计网络应用程序(尤其是 Web 服务和 API)的两种架构方法

  • REST通常更适合可扩展性、缓存和统一接口非常重要的 Web 服务和公共 API。
  • RPC通常用于与服务器端操作紧密耦合的操作,尤其是当效率和速度至关重要时,例如内部微服务通信
特征REST (资源导向)RPC (动作导向)
思维中心名词 (资源):文章、用户动词 (操作):获取文章()、创建用户()
交互模型操作“资源的状态”调用“远程的函数”
通信语言统一的 HTTP 动词 (GET, POST...)自定义的函数名 (getArticle, publishArticle...)
耦合度松耦合 (客户端只关心资源和标准动词)紧耦合 (客户端必须知道确切的函数名和参数)
典型例子GET /api/users/123userService.getUser({userId: 123})
都是远程吗?

轮询 vs 长轮询 vs Webhook

轮询、长轮询和 Webhook 之间的选择取决于应用程序对实时更新的要求、服务器和客户端的功能以及效率方面的考虑。

轮询简单但效率较低;长轮询提供了更及时的更新,介于两者之间;Webhook 可以高效地提供实时更新,但需要客户端处理传入的请求

  • 启动和流量:轮询由客户端发起,流量频繁;长轮询也从客户端开始,但通过保持请求开放来减少流量;webhook 由服务器发起,不需要轮询。
  • 实时更新:Webhooks 提供最实时的更新,而轮询和长轮询则存在固有的延迟。

CDN 使用与直接服务器服务

全球受众+大量内容: CDN 可以显著缩短加载时间并高效处理流量

本地用户群+内容有限的小型网站:直接服务器服务的简便性和较低的成本更为有利

  • 内容交付:CDN 将内容分布在全球多个服务器上以实现更快的交付,而直接服务器服务则依靠单一位置进行所有内容交付。
  • 性能和可扩展性:CDN 提供增强的性能和可扩展性,特别是对于全球受众而言,而直接服务器服务可能足以满足小规模或本地化网站的需求。
  • 用户体验:CDN 通常在速度方面提供更好的用户体验,尤其是对于远离原始服务器的用户。

无服务器架构 vs. 传统服务器架构

无服务器架构(AWS 的 Lambda、阿里云的函数计算 (FC) )适合工作负载多变或不可预测的应用程序,这些应用程序的首要任务是简化运营管理和降低成本。传统的基于服务器的架构(阿里云/AWS的ECS/EC2云服务器)则适用于需要对环境进行广泛控制且性能可预测的应用程序。

  • 基础设施管理:无服务器抽象了服务器管理,而传统架构需要主动管理服务器。
  • 扩展:无服务器可根据需求自动扩展,而传统架构则需要手动扩展。
  • 成本模型:无服务器采用现用现付模式,而传统架构通常涉及服务器运行的持续成本。

有状态与无状态架构

有状态架构提供了更加个性化的用户体验,但复杂性和资源占用更高;而无状态架构则提供了简单性和可扩展性,适用于每个请求都独立的分布式系统。

  • 会话内存:有状态保留用户会话信息,影响未来的交互,而无状态将每个请求视为独立的事务,独立于先前的请求。
  • 服务器设计:有状态服务器会维护状态,这使得它们更加复杂且资源密集。无状态服务器更简单,可扩展性更强。
  • 用例:有状态适用于需要持续用户交互和个性化的应用程序。无状态则适用于每个请求可以独立处理的服务,例如许多 Web API。

例子:

  • 无状态——淘宝商品页:服务器是一次性计算器,请求+原材料=结果,然后服务器可以完全忘掉这些,历史信息会储存在客户端、数据库和缓存,适用于海量的、可重复的、读多写少的场景
  • 有状态——在线协作文档:服务器是长期项目经理,需要记住项目的上下文、所有参与者以及他们的状态,适用于需要低延迟、实时、双向通信的交互密集型场景

混合云存储与全云存储

混合云存储:注重安全性、控制力和合规性以及云服务可扩展性,适合那些处理敏感数据或受严格监管要求约束,但又需要公有云提供的可扩展性和高级服务的组织。

全云存储:注重可扩展性、灵活性和最低限度基础设施维护的企业。对于初创公司、小型企业或完全在线运营的公司来说,全云存储可能会更具优势,因为它易于使用且前期成本较低。

  • 基础设施:混合云存储结合了本地/私有云和公有云存储,在控制力和可扩展性之间实现了平衡。相比之下,全云存储则完全使用云服务,完全依赖外部提供商进行数据存储。
  • 灵活性 vs. 简易性:混合云在数据部署和安全性方面提供灵活性,非常适合具有不同合规性需求的企业。全云存储提供简易性和易用性,适合倾向于外包整个基础设施的企业。

令牌桶与漏桶

令牌桶和漏桶是两种用于网络流量整形和速率限制的算法,令牌桶更灵活,更适合突发流量场景;而漏桶则更适合维持均匀的输出速率。

令牌桶算法:基于以固定速率向桶中添加令牌。每个令牌代表发送一定量数据的权限。当需要发送数据包(数据)时,只有当有可用令牌时才能发送,然后令牌会从桶中移除

漏桶算法:数据包被添加到队列(桶)中,并以恒定的速率释放。如果桶(缓冲区)已满,则传入的数据包将被丢弃或排队等待稍后传输。

  • 流量突发处理:令牌桶允许数据突发,直至桶中的令牌耗尽,因此非常适合经常发生突发流量的应用。相比之下,漏桶则能够平滑数据流,以稳定、恒定的速率释放数据包。
  • 用例:令牌桶非常适合需要灵活性且能够承受突发流量的应用程序,例如视频流。漏桶适用于需要稳定、连续数据流的场景,例如 IP 语音 (VoIP) 或实时流媒体。

读密集型与写密集型系统

为读取密集型系统进行设计

读取密集型系统的特点是读取操作量远大于写入操作量。常见于内容分发网络、报告系统或读取密集型 API 等场景

  1. 缓存:Redis 或 Memcached,不同级别缓存
  2. 数据库复制:复制创建主数据库的只读副本,读副本,写主库
  3. 内容分发网络(CDN) :使用 CDN 在地理位置上更靠近用户的位置缓存静态内容
  4. 负载平衡:使用负载平衡器将传入的读取请求均匀分布在多个服务器或副本之间
  5. 优化数据检索:使用数据索引来加快搜索
  6. 数据分区:用户数据根据用户 ID 或地理位置进行分片或水平分区,减少单个数据库服务器的读取负载
  7. 异步处理:对于不需要实时完成的操作使用异步处理

为写入密集型系统进行设计

写入密集型系统的特点是写入操作量大,例如日志系统、实时数据收集系统或事务数据库。

  1. 数据库写入优化:选择针对高写入吞吐量进行优化的数据库(如 NoSQL 数据库:Cassandra、MongoDB),并优化数据库模式和索引
  2. 写入批处理和缓冲:将多个写入操作批量处理,以减少写入请求的数量
  3. 异步处理:允许应用程序继续处理而无需等待写入操作完成
  4. CQRS(命令查询职责分离) :将写入(命令)和读取(查询)操作分离到不同的模型中
  5. 数据分区:使用分片或分区将写入操作分布在不同的数据库实例或服务器之间
  6. 预写日志(WAL)的使用:先将更改写入日志,然后再将其应用到数据库,这可确保数据完整性并提高写入性能
  7. 事件溯源:将变化保留为一系列不可变的事件,而不是直接修改数据库状态

第一类取舍:一致性 vs. 可用性/性能 (Consistency vs. Availability/Performance)

这是最核心、最根本的权衡,直接源于 CAP 定理(在一个分布式系统中,一致性、可用性、分区容错性,三者最多只能同时满足两个)。因为网络分区(P)总是可能发生,所以我们必须在一致性(C)和可用性(A)之间做出选择。

  • 强一致性 vs. 最终一致性

    • 取舍:要“所有人都看到一样的新数据”,还是要“系统永远能响应,数据晚点再同步”?
  • ACID vs. BASE

    • 取舍:要“银行级的、绝对可靠的事务”,还是要“互联网级的、高可用的、能容忍暂时不一致的服务”?
  • SQL vs. NoSQL

    • 取舍:要“结构化、强一致的关系模型”,还是要“灵活、高可用、易扩展的非关系模型”?
  • 主副本 vs. 对等复制

    • 取舍:要“一个明确的权威(Master),写操作简单可控”,还是要“人人平等,没有单点故障,但可能写冲突”?

第二类取舍:延迟 vs. 吞吐量 (Latency vs. Throughput)

这两个概念描述了系统的“速度”和“容量”,它们常常是相互制约的。

  • 延迟与吞吐量

    • 取舍:我是要“让每个请求都尽可能快地响应”,还是要“让系统单位时间内能处理尽可能多的请求”?
  • 批处理 vs. 流处理

    • 取舍:我是要“攒一大批数据再处理以获得最高吞吐量”,还是要“数据一来就处理以获得最低延迟”?
  • 令牌桶 vs. 漏桶

    • 取舍:我是要“允许突发流量(优化延迟),但可能超出平均速率”,还是要“严格平滑流量(保证稳定),但可能拒绝突发请求”?
  • 读密集型 vs. 写密集型系统设计

    • 取舍:我的系统瓶颈在读还是写?我的架构应该优先优化哪个方向?(比如为读密集系统加大量缓存和读副本,为写密集系统用 NoSQL 和消息队列)

第三类取舍:架构的复杂性 vs. 组件的职责 (Complexity vs. Responsibility)

这一类权衡,关注的是如何在不同组件之间划分责任,以达到整体最优。

  • 负载均衡器 vs. API 网关 vs. 反向代理

    • 取舍:我需要一个简单的“交通警察”(LB/反向代理),还是一个懂业务的“全能总服务台”(API 网 g关)?我是否需要将它们分层,让它们各司其职?
  • API 网关 vs. 直接服务公开

    • 取舍:我是要把复杂性(路由、认证)集中在网关,让客户端变简单,还是把复杂性推给客户端,让后端保持纯粹?
  • 服务器端缓存 vs. 客户端缓存

    • 取舍:这个缓存应该为“所有用户”(服务器端)服务,还是为“单个用户”(客户端)的重复访问加速?
  • 无服务器 vs. 传统服务器

    • 取舍:我是要把运维的复杂性完全外包给云厂商(Serverless),还是要自己掌控服务器以获得最高的灵活性和性能(传统架构)?