初学者系统设计:一篇文章涵盖你需要的一切

阅读此博客无需任何先决条件。所有内容都是为完全的初学者撰写的。

这是一篇详细的博客,涵盖了解决面试中任何系统设计问题所需掌握的所有主题。除了这篇博客之外,您不需要再学习任何理论。阅读完这篇博客后,直接开始尝试面试问题。

对于系统设计,在大多数地方,你只会看到理论的东西,但在这个博客中,我试图展示很多东西的实际实现,这样你不仅可以准备面试,还可以知道这些东西在现实世界中是如何使用的。

本博客内容

  1. 为什么要学习系统设计?
  2. 什么是服务器?
  3. 延迟和吞吐量
  4. 扩展及其类型
    – 垂直扩展
    – 水平扩展
  5. 自动扩展
  6. 信封背面估计
  7. CAP 定理
  8. 数据库扩展
    – 索引
    – 分区
    – 主从架构
    – 多主设置
    – 数据库分片
    – 分片的缺点
  9. SQL 与 NoSQL 数据库以及何时使用哪个数据库
    – SQL 数据库
    – NoSQL 数据库
    – NoSQL 与 SQL 的扩展
    – 何时使用哪个数据库?
  10. 微服务
    – 什么是单体和微服务?
    – 为什么我们要将应用程序分解为微服务?
    – 何时使用微服务?
    – 客户端如何在微服务架构中发出请求?
  11. 深入探究负载均衡器
    – 为什么我们需要负载均衡器?
    – 负载均衡器算法
  12. 缓存
    – 缓存简介
    – 缓存的好处
    – 缓存类型
    – Redis 深度解析
  13. Blob 存储
    – 什么是 Blob,为什么我们需要 Blob 存储?
    – AWS S3
  14. 内容分发网络 (CDN)
    – CDN 简介
    – CDN 如何工作?
    – CDN 中的关键概念
  15. 消息代理
    – 异步编程
    – 为什么我们要在中间放置一个消息代理?
    – 消息队列
    – 消息流
    – 何时使用消息代理
  16. Apache Kafka 深度探究
    – 何时使用 Kafka
    – Kafka 内部原理
  17. 实时发布订阅
  18. 事件驱动架构
    – EDA 简介
    – 为何使用 EDA?
    – 简单事件通知
    – 事件携带状态转移
  19. 分布式系统
  20. 使用领导者选举的自动恢复系统
  21. 大数据工具
  22. 一致性深度探究
    – 强一致性
    – 何时选择强一致性
    – 最终一致性
    – 何时选择最终一致性
    – 实现强一致性的方法
    – 实现最终一致性的方法
  23. 一致性哈希
  24. 数据冗余和数据恢复
    – 为什么要让数据库冗余?
    – 数据备份的不同方式
    – 连续冗余
  25. 代理
    – 什么是代理?
    – 正向代理
    – 反向代理
    – 构建我们自己的反向代理
  26. 如何解决任何系统设计问题?

为什么要学习系统设计?

您可能在大学期间建立过一些个人项目,其中有 NodeJS(或任何其他框架)的后端和数据库。

用户(或客户端)向您的应用程序发出请求。然后,您可能在后端服务器中进行一些计算,并在数据库中执行 CRUD 操作以返回响应。这对于原型和个人项目来说很好,但在现实世界中,当我们有实际用户(数百万或数十亿)时,这种简单的架构可能不起作用。我们需要考虑扩展、容错、安全性、监控和各种事情,以使我们的系统在所有情况下都可靠且高效地工作。为此,我们研究了系统设计中的不同概念。

*注意:*每当我为客户制作盒子时,这个客户可以是任何东西。它可以是 ReactJS 应用程序、Android 应用程序、IOS 应用程序或普通人在自己的设备(笔记本电脑、手机等)上使用的任何东西。

什么是服务器?

你们中的许多人可能已经知道什么是服务器,但这个博客是针对初学者的,所以我正在解释它。

服务器只不过是运行应用程序代码的物理机器(例如笔记本电脑)。当您构建 ReactJS 或 NodeJS 应用程序时,您的应用程序将在 上运行http://localhost:8080。Localhost 是解析为 IP 地址 的域名127.0.0.1。此 IP 地址是本地笔记本电脑的 IP 地址。

对于外部网站,请输入https://abc.com。当您在浏览器中输入此命令时,将发生以下事情:

  1. abc.com转到 DNS(域名服务)解析器以查找与该域对应的服务器的 IP 地址。每个服务器都有一个 IP 地址。它是每个设备唯一的物理地址。
  2. 你的浏览器获取了服务器的 IP 地址。借助 IP 地址,你的浏览器可以请求服务器。
  3. 现在,服务器收到了请求。在服务器上,有多个应用程序正在运行。(例如在您的笔记本电脑上,有多个应用程序同时运行,例如 Google Chrome、Netflix 等)。服务器在端口的帮助下找到正确的应用程序。然后它返回响应。

请求https://abc.com与 requesting 相同35.154.33.64:443。这里的 443 是端口(https 默认端口)。

记住 IP 地址很困难,所以人们通常为其购买域名并将域名指向其服务器的 IP 地址。

如何部署应用程序?
您的应用程序在端口 8080 上运行。现在,您想将其公开到互联网,以便其他人可以访问您的网站。为此,您需要购买一个公共 IP 地址并将此公共 IP 地址附加到您的笔记本电脑,以便人们可以https://<your_laptop_ip_address>:8080访问您的网站。

自己完成所有这些工作并管理服务器是一件很麻烦的事情,所以人们通常从 AWS、Azure、GCP 等云提供商那里租用服务器。他们会给你一个虚拟机,你可以在其中运行你的应用程序,而且该机器还附带一个公共 IP 地址,因此你可以从任何地方访问它。在 AWS 中,这个虚拟机称为EC2 实例。将你的应用程序代码从本地笔记本电脑放入云提供商的虚拟机中称为部署

*为您练习:*如果您想了解实际操作,如何在 AWS 中部署一个简单的应用程序,请阅读此博客

延迟和吞吐量

这是大家经常听到的两个词。

延迟

延迟是指单个请求从客户端传输到服务器并返回(或单个工作单元完成)所需的时间。通常以毫秒 (ms) 为单位。

加载网页:如果服务器需要200ms才能将页面数据发送回浏览器,则延迟为200ms。

简单来说,如果网站加载速度较快,则需要的时间较短,因此延迟较低。如果网站加载速度较慢,则需要的时间较长,因此延迟较高。

**往返时间 (RTT):**请求到达服务器并返回响应所需的总时间。有时,您还会听到 RTT 作为延迟的替代词。

吞吐量

吞吐量是系统每秒可处理的请求数或工作单元数。它通常以每秒请求数 (RPS) 或每秒事务数 (TPS) 来衡量。

每个服务器都有限制,这意味着它每个部分可以处理 X 个请求。进一步增加负载可能会使它阻塞,甚至可能瘫痪。

  • **高吞吐量:**系统可以同时处理许多请求。
  • **吞吐量低:**系统难以同时处理许多请求。

理想情况下,我们希望创建一个吞吐量高、延迟低的系统。

例子:

**延迟:**汽车从一个点行驶到另一个点所需的时间(例如 10 分钟)。

**吞吐量:**一小时内可在高速公路上行驶的汽车数量(例如 1,000 辆汽车)。

简而言之,

  • 延迟衡量处理单个请求的时间。
  • 吞吐量衡量可以同时处理多少个请求。

缩放及其类型

您经常会看到这样的情况:每当有人在互联网上启动他们的网站,流量突然增加时,他们的网站就会崩溃。为了防止这种情况,我们需要扩展我们的系统。

扩展意味着我们需要增加机器的规格(例如增加 RAM、CPU、存储空间等)或添加更多机器来处理负载。

您可以将其与手机示例联系起来,当您购买 RAM 和存储空间较少的廉价手机时,您的手机会因为同时使用大型游戏或大量应用程序而挂起。 EC2 实例也是如此,当大量流量同时出现时,它也会开始堵塞,那时我们就需要扩展我们的系统。

扩展类型

它有两种类型:

  1. 垂直扩展
  2. 水平扩展

垂直扩展(扩大/缩小)

如果我们增加同一台机器的规格(RAM、存储、CPU)来处理更多负载,则称为垂直扩展。增加规格(RAM,存储,CPU)的同一台机器来处理更多负载,这称为垂直扩展。

这种类型的扩展主要用于 SQL 数据库以及有状态的应用程序,因为在水平扩展设置中很难保持状态的一致性。

水平扩展(横向扩展/纵向扩展)

垂直扩展不可能超过某个点。我们无法无限增加机器的规格。我们会遇到瓶颈,除此之外,我们无法增加规格。

解决此问题的方法是添加更多机器并分配传入的负载。这称为水平扩展。

例如:我们有 8 个客户端和 2 台机器,并分配负载。我们希望平均分配负载,以便前 4 个客户端可以访问一台机器 1,而接下来的 4 个客户端可以访问机器 2。客户端并不聪明;我们不能给他们 2 个不同的 IP 地址,让他们决定访问哪台机器,因为他们不知道我们的系统。为此,我们在两者之间放置了一个负载平衡器。所有客户端都访问负载平衡器,这个负载平衡器负责将流量路由到最不繁忙的服务器。

在此设置中,客户端不会直接向服务器发出请求。相反,它们将请求发送到负载均衡器。负载均衡器接收传入的流量并将其传输到最不繁忙的机器。

在现实世界中,大多数时候我们使用水平扩展。

下图中您可以看到有 3 个客户端正在发出请求,负载均衡器将负载均匀分布在 3 个 EC2 实例上。

*为您准备的练习:*如果您想亲眼看到这一操作,了解如何进行水平扩展并使用 AWS 设置负载均衡器,请阅读此博客

自动扩展

假设您创办了一家公司并将其上线。您租用了一台 EC2 服务器来部署应用程序。如果很多用户同时访问您的网站,那么您的网站可能会崩溃,因为 EC2 服务器在同时为一定数量的用户提供服务方面存在限制(CPU、RAM 等)。此时,您将进行水平扩展并增加 EC2 实例的数量并附加负载均衡器。

假设一台 EC2 机器可以不间断地为 1000 名用户提供服务。如果我们网站上的用户数量不是恒定的。有些日子,我们的网站上有 10,000 名用户,这可以由 10 个实例提供服务。有些日子,我们有 100,000 名用户,那么我们需要 100 个 EC2 实例。一种解决方案可能是始终运行最大数量(100)的 EC2 实例。这样,我们就可以始终为所有用户提供服务而不会出现任何问题。但在流量低迷期间,我们在额外的实例上浪费了金钱。我们只需要 10 个实例,但我们一直在运行 100 个实例并为此付费。

最好的解决方案是每次只运行所需数量的实例。并添加某种机制,如果 EC2 实例的 CPU 使用率达到某个阈值(例如 90%),则启动另一个实例并分配流量,而无需我们手动执行此操作。这种根据流量动态更改服务器数量的方法称为自动扩展。

*注意:*这些数字都是假设的,以便您理解主题。如果您想找到实际阈值,则可以对实例进行负载测试。

*为您准备的练习:*如果您想了解实际操作,如何使用 AWS 配置 Auto Scaling,请阅读此博客

信封背面估计

您可以看到,在水平扩展中,我们看到需要更多服务器来处理负载。在粗略估计中,我们估计所需的服务器、存储等数量。

在系统设计面试中,最好花 5 分钟(但不要超过)来回答这个问题。

我们在这里进行近似,以便于计算。

下面是一个方便的表格。一定要记住。

|2 的幂| 近似值| 10 的幂 | 全名 | 简称 |
|----------|------------------|-------------|-------------|-------------| |
10 | 1 千 | 3 | 1 千字节 | 1 KB |
| 20 | 1 百万 | 6 | 1 兆字节 | 1 MB |
| 30 | 10 亿 | 9 | 1 千兆字节 | 1 GB |
| 40 | 1 万亿 | 12 | 1 太字节 | 1 TB |
| 50 | 1 千万亿 | 15 | 1 拍字节 | 1 PB |

这里可以计算很多东西,但我更喜欢只计算以下东西:

  1. 负荷估算
  2. 存储估算
  3. 资源估算

我们以 Twitter 为例进行此计算。

负荷估算

这里我们求一下DAU(日活跃用户数),然后计算一下读取次数和写入次数。

假设 Twitter 每天有 1 亿活跃用户,每个用户每天发布 10 条推文。

一天的推文数量

  • 1 亿 * 10 条推文 = 每天 10 亿条推文

这意味着写入次数 = 每天 10 亿条推文。

假设 1 个用户每天阅读 1000 条推文。

一天的阅读次数

  • 1 亿 * 1000 条推文 = 每天 1000 亿次阅读

存储估算

推文有两种类型:

普通推文和带有照片的推文。假设只有 10% 的推文包含照片。

假设一条推文包含 200 个字符。一个字符占 2 个字节,一张照片占 2 MB

一条不含照片的推文的大小 = 200 个字符 * 2 字节 = 400 字节 ~ 500 字节

一天的推文总数 = 10 亿条(如上所述)

带照片的推文总数 = 10 亿的 10% = 1 亿

每天所需的总存储量:

=> (一条推文的大小) * (推文总数) + (照片的大小) * (带照片的推文总数)

=> (500 字节 * 10 亿) + (2MB * 1 亿)

取约数以便于计算:

=> (1000 字节 * 10 亿) + (2MB * 5 亿)

=> 1 TB + 1 PB

约为 1 PB(忽略 1TB,因为对于 1PB 来说它非常小,所以添加它无所谓)

我们每天需要 1 PB 的存储空间。

资源估算

这里计算所需的CPU和服务器的总数。

假设我们每秒收到 1 万个请求,并且每个请求需要 CPU 10 毫秒来处理。

CPU 处理总时间:(
每秒 10,000 个请求)*(10 毫秒)= 每秒 CPU 需要 100,000 毫秒。

假设 CPU 的每个核心每秒可以处理 1000 毫秒,则所需的核心总数:
=> 100,000 / 1000 = 100 个核心。

让一台服务器有4个CPU核心。

因此,所需的服务器总数 = 100/4 = 25 台服务器。

因此,我们将保留 25 台服务器,并在它们前面安装一个负载均衡器来处理我们的请求。

CAP 定理

该定理指出了设计任何系统时非常重要的权衡。

CAP定理由3个词组成:

  • **C:**一致性
  • **答:**可用性
  • **P:**分区容忍度

我们将在 CAP 定理中讨论的所有内容都是从分布式系统的角度进行的。

分布式系统意味着数据存储在多台服务器中,而不是一台,就像我们在水平扩展中看到的那样。

为什么要把系统做成分布式的?

  • 通过多台服务器,我们可以分散工作负载并同时处理更多请求,从而提高整体性能。
  • 将数据库保存在不同位置,并从离用户最近的位置提供数据。这减少了访问和检索的时间。

作为整个分布式系统一部分的单个服务器称为Node

在这个系统中,我们在不同的服务器之间复制相同的数据以获得上述好处。

您可以在下图中看到这一点。相同的数据存储在位于印度不同位置的多个数据库服务器(节点)中。

如果在某个节点中添加了数据,则会自动复制到所有其他节点。我们将在本博客的后面部分讨论这种复制是如何发生的。

让我们讨论一下 CAP 的所有三个词:

  • **一致性:**无论我们从哪个节点读取,每个读取请求都会返回相同的结果。这意味着所有节点同时具有相同的数据。在上图中,您可以看到我们的数据库集群是一致的,因为每个节点都有相同的数据。
  • **可用性:**即使某些节点发生故障,系统仍然可用,并且始终能够响应请求。这意味着即使发生某些节点故障,系统仍应继续使用其他健康节点来处理请求。
  • **分区容忍性:**即使不同节点之间出现通信故障或网络分区,系统仍能继续运行。

当节点发生故障时,可用性仍可继续提供服务。

当发生“网络故障”时,分区容忍度仍会继续提供服务。

**例子:**假设有3个节点A,B,C。

  • 一致性:A、B 和 C 都具有相同的数据。如果节点 B 中有更新,则会发生数据复制,并且 B 会将该更新传播到 A 和 C。
  • 可用性:让节点 B 遇到硬件故障并离线。节点 A 和 C 仍可运行。尽管节点 B 发生故障,但整个系统仍然可用,因为节点 A 和 C 仍可响应客户端请求。
  • 分区容忍度:网络分区将 B 与 A 和 C 分开。节点 B 仍然可以运行并处理请求,但无法与 A 和 C 通信。

什么是 CAP 定理

CAP 定理指出,在分布式系统中,你只能同时保证这三个属性中的两个。不可能同时实现三个。

  • CA — 可能
  • AP — 可能
  • CP — 可能
  • CAP — 不可能

如果你仔细想想,这很合逻辑。

在分布式系统中,网络分区是必然会发生的,因此系统应该具有分区容忍性。这意味着对于分布式系统来说,“P”将始终存在。我们将在 CP 和 AP 之间进行权衡。

为什么只能实现CP或者AP而不能实现CAP呢?

  • 再次以节点 A、B 和 C 为例。
  • 假设发生网络分区,B 失去与 A 和 C 的通信。
  • 那么 B 将无法将其更改传播给 A 和 C。
  • 如果我们优先考虑可用性,那么我们将继续满足请求。B 无法将其更改传播到 A 和 C。因此,从 B 获得服务的用户可能与从 A 和 C 获得服务的用户获得的结果不同。因此,我们通过牺牲一致性来实现可用性。
  • 如果我们优先考虑一致性,那么在 B 的网络分区解决之前,我们将不会接受请求,因为我们不希望 B 的写操作无法到达节点 A 和 C。我们希望 A、B 和 C 中的数据相同。因此,我们通过牺牲可用性来获得一致性。

为什么不选择CA?

  • CA 意味着不会发生网络分区,您可以同时实现一致性和可用性,因为通信不是问题。
  • 在实际场景中,分布式系统必然会出现网络分区的情况,因此我们在CP和AP之间进行选择。

选择什么,CP 还是 AP?

  • 对于银行、支付、股票等安全应用,请遵循一致性。您不能承受显示不一致的数据。
  • 对于社交媒体等,请考虑可用性。如果帖子的点赞数对于不同的用户不一致,我们也没问题。

数据库扩展

您通常有一个数据库服务器;您的应用程序服务器从该数据库查询并获取结果。

当达到一定规模时,该数据库服务器会开始响应缓慢,甚至可能由于其限制而宕机。在这种情况下,我们将在本节中研究如何扩展数据库。

我们将逐步扩展数据库,这意味着如果我们只有 1 万名用户,那么将其扩展以支持 1000 万名用户是一种浪费。这是过度设计。我们只会扩展到足以满足我们业务需求的极限。

假设您有一个数据库服务器,其中有一个用户表。

应用服务器会发出大量读取请求来获取具有特定 ID 的用户。为了加快读取请求的速度,请执行以下操作:

索引

在建立索引之前,数据库会检查表中的每一行以查找您的数据。这称为全表扫描,对于大型表来说,该过程可能很慢。检查每个 ID 需要 O(N) 时间。

通过索引,数据库可以使用索引直接跳转到您需要的行,从而提高速度。

您对“id”列进行索引,然后数据库将在数据结构(称为 B 树)中复制该 id 列。它使用 B 树来搜索特定 id。这里的搜索速度更快,因为 ID 以排序方式存储,因此您可以应用二分搜索之类的东西在 O(logN) 内进行搜索。

如果您想在任何列中启用索引,那么您只需添加一行语法,创建 B 树等所有开销都由 DB 处理。您无需担心任何事情。

这是关于索引的非常简短且简单的解释。

分区

分区就是将大表分成多个小表。

您可以看到,我们将用户表分成了 3 个表:

  • user_table_1
  • user_table_2
  • user_table_3

这些表保存在同一个数据库服务器中。

**这种分区有什么好处?
**当索引文件变得非常大时,在大型索引文件上进行搜索时也会开始出现一些性能问题。现在,分区后,每个表都有自己的索引,因此在较小的表上进行搜索会更快。

您可能想知道我们如何知道要从哪个表进行查询。因为在此之前,我们可以点击SELECT * FROM users where ID=4。别担心,您可以再次点击相同的查询。在幕后,PostgreSQL 很聪明。它会找到合适的表并为您提供结果。但如果您愿意,您也可以在应用程序级别编写此配置。

主从架构

当遇到瓶颈时请使用此方法,例如即使在进行索引、分区和垂直扩展之后,您的查询仍然很慢,或者您的数据库无法处理单个服务器上的进一步请求。

在这种设置中,您可以将数据复制到多个服务器而不是一个服务器。

当您执行任何读取请求时,您的读取请求(SELECT 查询)将被重定向到最不繁忙的服务器。通过这种方式,您可以分配负载。

但是所有写入请求(INSERT,UPDATE,DELETE)只会由一台服务器处理。

处理写请求的节点(服务器)称为主节点

接受读取请求的节点(服务器)称为从节点

当您发出写入请求时,它会在主节点中被处理和写入,然后以异步方式(或根据配置同步)复制到所有从属节点。

多主设置

当写入查询变慢或一个主节点无法处理所有写入请求时,您就可以这样做。

在此,不使用单个主数据库,而是使用多个主数据库来处理写入。

例如:一种非常常见的做法是放置两个主节点,一个用于北印度,另一个用于南印度。来自北印度的所有写入请求都由 North-India-DB 处理,来自南印度的所有写入请求都由 South-India-DB 处理,并且它们会定期同步(或复制)其数据。

在多主设置中,最有挑战性的部分是如何处理冲突。如果对于同一个 ID,两个主数据库中都存在两个不同的数据,那么你必须在代码中编写逻辑,比如是否要同时接受两个数据、用最新的数据覆盖前一个数据、将其连接起来等。这里没有规则。这完全取决于业务用例。

数据库分片

分片是一件非常复杂的事情。在实际生活中尽量避免这样做,只有当上述所有事情都不够用并且需要进一步扩展时才这样做。

正如我们上面看到的,分片类似于分区,但我们不是将不同的表放在同一台服务器中,而是将其放在不同的服务器中。

你可以在上图中看到我们将表分成 3 部分,并放到 3 个不同的服务器上。这些服务器通常称为分片 (shard)

这里我们根据ID来进行分片,所以这个ID列我们称之为分片键

**注意:**分片键应将数据均匀分布在各个分片上,以避免单个分片过载。

每个分区都保存在一个独立的数据库服务器(称为分片)中。因此,现在您可以根据需要进一步单独扩展此服务器,例如对其中一个分片进行主从架构,这需要大量请求。

为什么分片很难?
在分区(将表的块保存在同一个数据库服务器中)中,您不必担心从哪个表进行查询。PostgreSQL 会为您处理。但在分片(将表的块保存在不同的数据库服务器中)中,您必须在应用程序级别处理这个问题。您需要编写一个代码,这样当从 id 1 到 2 查询时,它会转到 DB-1;当查询 id 5-6 时,它会转到 DB-3。此外,在添加新记录时,您需要手动处理应用程序代码中的逻辑,即您要在哪个分片中添加此新记录。

分片策略

  1. 基于范围的分片
    根据分片键中的值范围将数据划分为分片。
    示例:
    分片 1:具有 的用户user_id 1–1000
    分片 2:具有 的用户分片 3:具有 的用户优点:易于实施。缺点:如果数据倾斜(例如,某些范围有更多用户),则分布不均匀。user_id 1001–2000 ``user_id 2001–3000

  2. **基于哈希的分片:
    **将哈希函数应用于分片键,结果决定分片。
    示例:
    HASH(user_id) % number_of_shards决定分片。
    优点:确保数据均匀分布。
    缺点:添加新分片时,由于哈希结果发生变化,重新平衡很困难。

  3. 基于地理/实体的分片
    数据根据逻辑分组(如区域或部门)进行划分。
    示例:
    分片 1:来自美国的用户。
    分片 2:来自欧洲的用户。
    优点:适用于地理分布的系统。
    缺点:某些分片可能会成为流量不均匀的“热点”。

  4. 基于目录的分片
    映射目录跟踪哪个分片包含特定数据。
    示例:查找表user_id将范围映射到分片 ID。
    优点:无需更改应用程序逻辑即可灵活地重新分配分片。
    缺点:目录可能成为瓶颈。

分片的缺点

  1. 难以实现,因为您必须自己编写逻辑来知道从哪个分片查询以及在哪个分片中写入数据。
  2. 分区保存在不同的服务器(称为分片)中。因此,当您执行连接时,您必须从不同的分片中提取数据以与不同的表进行连接。这是一项昂贵的操作。
  3. 您会失去一致性。由于数据的不同部分存在于不同的服务器中。因此,保持一致性很困难。

数据库扩展总结

阅读完数据库扩展部分后,让我们记住这些规则:

  • 首先,始终优先考虑垂直扩展。这很容易。你只需要增加单个设备的规格。如果你在这里遇到瓶颈,那么只需做以下事情。
  • 当读到大流量的时候就做主从架构。
  • 当写入流量很大时,请进行分片,因为整个数据无法放在一台机器上。只需尽量避免跨分片查询即可。
  • 如果你遇到流量大的情况,但主从架构变慢或无法处理负载,那么你也可以进行分片并分配负载。但这通常发生在非常大的规模上。

SQL 与 NoSQL 数据库以及何时使用哪个数据库

确定正确的数据库是系统设计中最重要的部分,因此请仔细阅读本节。

SQL 数据库

  • 数据以表格的形式存储。
  • 它有一个预定义的模式,这意味着在插入数据之前必须定义数据的结构(表、列及其数据类型)。
  • 它遵循ACID特性,确保数据的完整性和可靠性。
  • 例如:MySQL、PostgreSQL、Oracle、SQL Server、SQLite。

NoSQL 数据库

  • 它分为 4 种类型:
    –**基于文档:将数据存储在文档中,如 JSON 或 BSON。例如:MongoDB。–
    键值
    存储:**将数据存储在键值对中。例如:Redis、AWS DynamoDB
    –**列系列存储:**将数据存储在列而不是行中。例如:Apache Cassandra
    –**图形数据库:**关注数据之间的关系,因为它以图形方式存储。在社交媒体应用程序中很有用,例如创建共同的朋友、朋友的朋友等。例如:Neo4j。
  • 它具有灵活的模式,这意味着我们可以插入初始模式中可能未定义的新数据类型或字段。
  • 它并不严格遵循 ACID。它优先考虑其他因素,例如可扩展性和性能。

SQL 与 NoSQL 的扩展

  • SQL 主要设计为垂直扩展,这意味着增加单个服务器的硬件(CPU、RAM、存储)来处理更大的数据量。
  • NoSQL 数据库主要设计为水平扩展,这意味着向集群添加更多服务器(节点)来处理不断增加的数据量。
  • 通常,NoSQL DB 中会进行分片以容纳大量数据。
  • 分片也可以在 SQL DB 中完成,但通常我们会避免这样做,因为我们使用 SQL DB 来实现ACID,但是当数据分布在多个服务器上时,确保数据一致性会变得非常困难,而且跨分片通过 JOINS 查询数据也很复杂且成本高昂。

何时使用哪个数据库?

  • 当数据是非结构化的并且想要使用灵活的模式时,请使用 NoSQL。
    例如:电子商务应用的评论、推荐
  • 当数据结构化且具有固定模式时,请使用 SQL。
    例如:电子商务应用程序的客户帐户表
  • 如果您想要数据完整性和一致性,请使用 SQL DB,因为它维护ACID 属性
    例如:金融交易、银行应用程序
    订单的账户余额、电子商务应用程序的付款。
    股票交易平台。
  • 如果您想要高可用性、可扩展性(意味着存储大量数据,而这些数据无法放在一台服务器上)和低延迟,那么请选择 NoSQL,因为它具有水平可扩展性和分片功能。
    例如:社交媒体应用的帖子、点赞、评论、消息。
    存储大量实时数据,例如配送应用的司机位置。
  • 当您想要执行复杂的查询、连接和聚合时,请使用 SQL。通常,我们在执行数据分析时必须执行复杂的查询、连接等。将这些所需的数据存储在 SQL 中。

微服务

什么是整体式和微服务?

**单体式架构:**整个应用程序以单体式架构中的单个单元构建。假设您正在构建一个电子商务应用程序。在单体式架构中,您只需在一个应用程序中创建一个后端和整个功能(如用户管理、产品列表、订单、付款等)。

**微服务:**将大型应用程序分解为更小的、可管理的、可独立部署的服务。

例如:对于电子商务应用程序,假设将其分为以下服务:

  • 用户服务
  • 产品服务
  • 订单服务
  • 支付服务

为每项服务制作单独的后端应用程序。

为什么要将应用程序分解为微服务?

  • 假设一个组件有大量流量并且需要更多资源;那么,您可以仅独立扩展该服务。
  • 灵活选择技术栈。在单体架构中,整个后端都使用一种语言/技术编写。但在微服务中,您可以将不同的服务编写到不同的技术栈中。例如:您可以在 NodeJS 中构建用户服务,在 Golang 中构建订单服务。
  • 一项服务的故障不一定会影响其他服务。在单体应用中,假设后端的一部分崩溃,那么整个应用就会崩溃。但在微服务中,如果订单服务仍然崩溃,其他部分(例如用户和产品服务)则不会受到影响。

何时使用微服务?

  • “任何初创公司的微服务都定义了其内部团队结构”。假设一家初创公司有 3 个团队负责 3 个不同的业务功能。那么它将拥有 3 个微服务,并且随着团队数量的增加,微服务也会分裂。
  • 大多数初创公司都是从整体式架构开始的,因为在开始时,只有 2-3 个人从事技术工作,但随着团队数量的增加,最终会转向微服务。
  • 当我们要避免单点故障时,我们也会选择微服务。

微服务架构中客户端如何请求?

不同的微服务有不同的后端,并且独立部署。

假设user service部署在一台具有 IP 地址的机器上192.168.24.32,其他服务product service也是192.168.24.38如此。所有服务都部署在不同的机器上。为每个微服务使用不同的 IP 地址(或域名)非常麻烦。因此,我们为此使用API 网关。

客户端在 API 网关的单个端点中执行每个请求。它将接收传入的请求并将其映射到正确的微服务。

你可以独立扩展每个服务。假设产品服务流量较大,需要 3 台机器,用户服务需要 2 台机器,支付服务 1 台机器就足够了,那么你也可以这样做。参见下图

API 网关还提供其他几个优点:

  • 速率限制
  • 缓存
  • 安全性(身份验证和授权)

负载均衡器深入探究

为什么我们需要负载均衡器?

正如我们前面看到的,在水平扩展中,如果我们有许多服务器来处理请求,那么我们就不能将所有机器的 IP 地址都提供给客户端,并让客户端决定哪个服务器来执行请求。

负载均衡器充当客户端的单一联系点。客户端请求负载均衡器的域名,负载均衡器会重定向到最不繁忙的服务器之一。

*为你练习:*我强烈建议你阅读这个博客来了解它是如何实现的。

负载均衡器遵循什么算法来决定将流量发送到哪个服务器?我们将在负载均衡器算法中研究它。

负载均衡器算法

  1. 循环算法

工作原理:请求按循环顺序依次分发到服务器。

假设我们有 3 个服务器:服务器 1、服务器 2 和服务器 3。
然后,在循环中,第一个请求发往服务器 1,第二个请求发往服务器 2,第三个请求发往服务器 3,第四个请求再次发往服务器 1,第五个请求发往服务器 2,第六个请求发往服务器 3,然后第七个请求再次发往服务器 1,第八个请求发往服务器 2,依此类推。

优点

  • 简单且易于实现。
  • 如果所有服务器都有相似的容量,则可以很好地运行。

缺点

  • 忽略服务器健康或负载。

2. 加权轮询算法

工作原理:与循环类似,但服务器根据其容量分配权重。权重较高的服务器接收更多请求。您可以在下图中看到请求数以了解其工作原理。在下图中,第 3 个服务器更大(拥有更多 RAM、存储空间等)。因此,发送给它的请求是第 1 个和第 2 个的两倍。

优点

  • 更好地处理容量不平等的服务器。

缺点

  • 静态权重可能无法反映实时服务器性能。

3. 最小连接算法

工作原理:将流量导向具有最少活动连接的服务器。此处的连接可以是任何形式,例如 HTTP、TCP、WebSocket 等。此处,负载均衡器将流量重定向到与负载均衡器具有最少活动连接的服务器。

优点

  • 根据实时服务器活动动态平衡负载。

缺点

  • 可能无法很好地与处理不同持续时间的连接的服务器配合使用。

4.基于哈希的算法

工作原理:负载均衡器将客户端的 IP、user_id 等任何内容作为输入,并对其进行哈希处理以查找服务器。这可确保特定客户端始终路由到同一服务器。

优点

  • 对于维持会话持久性很有用。

缺点

  • 服务器更改(例如,添加/删除服务器)可能会破坏散列和会话一致性。

这就是负载均衡器的全部内容。

*练习:*我强烈建议您阅读此博客,了解如何配置负载平衡。我还鼓励您使用任何编程语言(例如 Go、NodeJS 等)从头开始编写自己的负载平衡器。

缓存

缓存介绍

缓存是将经常访问的数据存储在高速存储层的过程,以便将来对该数据的请求能够得到更快的满足。

例如:假设从 MongoDB 数据库获取某些数据需要 500 毫秒,那么在后端对该数据进行一些计算并最终将其发送到客户端需要 100 毫秒。因此,客户端总共需要 600 毫秒才能获取数据。如果我们缓存这些计算数据并将其存储在 Redis 等高速存储中并从那里提供服务,那么我们可以将时间从 600 毫秒缩短到 60 毫秒。(这些是假设数字)。

缓存意味着将预先计算的数据存储在Redis之类的快速访问数据存储中,当用户请求该数据时,从 Redis 提供该数据,而不是从数据库查询。

**示例:**让我们以博客网站为例。当我们访问该路由/blogs时,我们会获取所有博客。如果用户第一次访问此路由,则缓存中没有数据,因此我们必须从数据库获取数据并假设其响应时间为 800 毫秒。现在我们将这些数据存储在 Redis 中。下次用户访问此路由时,他将从 Redis 获取数据,而不是从数据库获取数据。它的响应时间为 20 毫秒。当添加新博客时,我们必须以某种方式从 Redis 中删除博客的旧值并用新值更新它。这称为缓存失效。缓存失效有很多种方法。我们可以设置一个过期时间(生存时间 - TTL);每 24 小时后,Redis 将删除博客,并且当 24 小时后任何用户第一次发出请求时,他将从数据库获取数据。之后,它将被缓存以供下次请求使用。

缓存的好处:

  • **提高性能:**减少最终用户的延迟。
  • **减少负载:**卸载后端数据库和服务。
  • **成本效益:**降低网络和计算成本。
  • **可扩展性:**能够更好地处理高流量负载。

缓存类型

  1. **客户端缓存:
    -**存储在用户设备上(例如,浏览器缓存)。
  • 减少服务器请求和带宽使用。
  • 示例:HTML、CSS、JavaScript 文件。
  1. **服务器端缓存:
    -**存储在服务器上。
  • 示例:内存缓存,如 Redis 或 Memcached。
  1. **CDN 缓存:
    -**用于静态内容传送(HTML、CSS、PNG、MP4 等文件)。-
    缓存在地理分布的服务器中。-
    示例:AWS CloudFront、Cloudflare CDN
  2. **应用程序级缓存:
    -**嵌入在应用程序代码中。
  • 缓存中间结果或数据库查询结果。

这只是介绍。我们将深入研究每种类型的缓存。

深入探究 Redis

Redis 是一个内存数据结构存储。

内存意味着数据存储在 RAM 中。如果您具备计算机科学的基础知识,那么您就会知道,与磁盘相比,从 RAM 读取和写入数据的速度极快。

我们利用这种快速访问进行缓存。

数据库使用磁盘来存储数据。与 Redis 相比,读写速度非常慢。

你可能会想,如果 Redis 这么快,那为什么要使用数据库呢?我们不能依靠 Redis 来存储所有数据吗?
答案)Redis 将数据存储在 RAM 中,而 RAM 与磁盘相比内存非常小。如果你在 leetcode 或 codeforces 中编码,那么有时你可能会收到“超出内存限制”消息。同样,如果我们在 Redis 中存储了太多数据,它可能会出现内存泄漏错误。

Redis 将数据存储在键值对中。在数据库中,我们从表中访问数据的方式与在 Redis 中相同;我们从键访问数据。

值可以是任何数据类型,如字符串、列表等,如下图所示。

还有更多的数据类型,但上述类型是最常用的。

我将在 CLI 上展示有关 Redis 的所有内容,但您可以在任何应用程序中配置所有相应的内容,例如 NodeJS、Springboot 和 Go。

运行以下命令在本地笔记本电脑上安装并运行 Redis。

docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest

**Redis 中键的命名约定:
**你可以为键赋予任何名称,但一般来说,行业遵循以下方法:
— 用户 ID 为 1 的键为“user:1”
— 用户 ID 为 2 的电子邮件的键为“user:2:email”

使用“:”来分隔事物

现在,让我们讨论一下每种数据类型

  1. 细绳
  • SET 键值:设置具有特定值的键。
  • 获取键:检索与键关联的值。
  • SET 键值 NX:仅当键不存在时才存储字符串值
  • MGET key1 key2 … keyN:在一次操作中检索多个字符串值

  1. 列表
  • LPUSH键值:将值添加到左侧
  • RPUSH键值:将值添加到右侧
  • LLEN 键:返回列表的长度
  • LPOP 键:弹出左边的值并返回。 RPOP 键:弹出右边的值并返回。

要创建队列,只需执行 LPUSH 和 RPOP (即从左侧推送项目并从右侧弹出项目(FIFO))。

要创建堆栈,只需执行 LPUSH 和 LPOP 即从同一侧推送和弹出项目(LIFO)。

从 Redis 文档中自行尝试更多命令和不同的数据类型。

以下是 NodeJS 的基本用法。您可以随意在您喜欢的后端语言(如 Django、Go 等)上尝试。

我编写了相同的博客示例。如果/blog路由第一次命中,则数据将来自 apiCall 或数据库。但此后,它将被缓存并从 Redis(用于中间件)提供。Redis 中的数据有效期为 24 小时。此后,它将自动从 Redis 中删除。

下面是代码图。

Cash Hit 表示数据存在于缓存中。Cash
Miss 表示数据不存在缓存中。

缓存的另一种方式是,每当服务器写入数据库时​​,它也会写入缓存(redis)。
示例:当 codeforces 中有竞赛时,每当某个用户提交任何问题时,您都会立即更新数据库和 Redis 中的排名列表,这样如果您从 Redis 提供排名列表,用户就会看到当前排名。

从文档中尝试 Redis 的不同命令。

文档链接:https://redis.io/docs/

在您的项目中实施 Redis。

Blob 存储

什么是 Blob,为什么我们需要 Blob 存储?

在数据库中,文本和数字按原样存储。但是想想诸如 mp4、png、jpeg、pdf 等文件。这些东西不能仅仅存储在行和列中。

这些文件可以表示为一堆 0 和 1,这种二进制表示称为 Blob(二进制大对象)。将数据存储为 mp4 是不可行的,但存储其 blob 很容易,因为它只是一堆 0 和 1。

这个 Blob 的大小可能非常大,例如单个 mp4 视频可能达到 1 GB。如果将其存储在 MySQL 或 MongoDB 等数据库中,查询速度会变得太慢。存储如此大量的数据时,您还需要考虑扩展、备份和可用性。这就是我们不将 Blob 数据存储到数据库中的一些原因。相反,我们将其存储在作为托管服务的 Blob 存储中(托管服务意味着其扩展、安全性等由亚马逊等公司负责,我们将其用作黑匣子)。

Blob 存储示例: AWS S3、Cloudflare R2。

AWS S3

这只是对 S3 的介绍。如果您想通过示例和实现来详细了解它,那么您可以关注我的AWS 系列

S3 用于存储文件(blob 数据),例如 mp4、png、jpeg、pdf、html 或任何您能想到的文件类型。

您可以将 S3 视为 Google Drive,您将所有文件存储在其中。

S3 最大的优点是价格非常便宜。在 S3 中存储 1 GB 数据比在 RDS 中存储便宜得多(RDS 是 AWS 的 DB 服务,提供各种 DB,例如 PostgreSQL、MySQL 等)。

S3 的特点:

  • 可扩展性:自动扩展以处理大量数据。
  • 耐用性:S3 提供 99.999999999%(11 个 9)的耐用性。
  • 可用性:高可用性,具有各种服务级别协议。
  • 成本效率:现收现付模式。
  • 安全性:静态和传输中加密、存储桶策略和 IAM 权限。
  • 访问控制:使用策略、ACL 和预签名 URL 进行细粒度控制。

我不会在这里详细介绍 S3。

练习:您的任务是使用您所需的语言(例如 nodejs、spring boot、fast API 等)制作具有图像上传功能的应用程序。当用户上传图像时,它会存储在 S3 中。编写此练习的代码,然后您将学到很多有关 S3 的知识,而不是阅读理论内容。

内容分发网络 (CDN)

CDN 简介

CDN 是扩展静态文件(如 mp4、jpeg、pdf、png 等)的最简单方法。假设文件存储在位于印度地区的 S3 服务器中。当来自美国、澳大利亚或远离印度的用户请求这些文件时,将需要大量时间来为他们提供服务。从最近的位置提供的请求总是比从远距离提供的请求更快。现在,我们希望将这些文件存储在最近的位置以降低延迟。我们在 CDN 的帮助下实现了这一点。

CDN 是一组分布在世界各地的分布式服务器。这些服务器用于提供静态内容(如图片、视频、样式表等)。它的工作原理是缓存并从最靠近用户的服务器提供内容,从而减少延迟、加载时间和带宽成本。

CDN 示例: AWS CloudFront、Cloudflare CDN

CDN 如何工作?

当用户请求内容时,请求将转到最近的 CDN 服务器(称为边缘服务器)。如果内容存在于边缘服务器中,则从那里返回。如果不存在,则从源服务器(保存内容的原始 S3)获取内容,然后缓存到边缘服务器并最终返回给用户。对于后续请求,如果内容存在于边缘服务器中,则从那里返回。

CDN 中的关键概念

1. 边缘服务器

  • 这些是地理分布的服务器,CDN 提供商可在其中缓存您的内容。
  • 用户被路由到最近的边缘服务器以实现更快的传输。

2. 源服务器

  • 这是您的主 Web 服务器(例如,AWS S3)。
  • 如果内容尚未缓存在边缘服务器上,CDN 将从这里获取内容。

3.缓存

  • CDN 将您的静态内容(例如图像、视频、HTML)的副本存储在其边缘服务器中。
  • 缓存内容直接提供给用户,减少了频繁向原始服务器发出请求的需要。

4.TTL(生存时间)

  • 文件在 CDN 边缘服务器上缓存的时长。
  • 示例:图像的 TTL 可能是 24 小时,这意味着它会在缓存中保留 24 小时然后再刷新。

5. GeoDNS

  • CDN 使用GeoDNS根据用户的地理位置将用户路由到最近的边缘服务器。

这涵盖了 CDN 的理论部分。

*练习:*您的任务是使用 S3 存储桶配置 AWS CloudFront CDN,以在实践中看到这一点。您可以关注我的AWS 系列。我们将在那里介绍它。

消息代理

异步编程

在此之前,我们需要了解什么是同步编程。这意味着每当客户端发送请求时,服务器都会处理该请求并立即发回响应。我们构建的大多数东西本质上都是同步的。

假设有一些长时间运行的任务需要 10 分钟才能完成,那么同步方法就不合适了。我们不能让客户端等待很长时间,而且 HTTP 超时也会发生。在这种情况下,当客户端请求这样的任务时,我们不会发送该任务的结果;相反,我们会向他发送一些消息作为响应“您的任务正在处理”,并启动一些后台工作者来执行任务。当这个任务在一段时间后完成时,我们也许可以通过电子邮件或其他方式通知客户端。这称为异步编程,我们不会立即执行任务,而是在后台执行,而客户端不必等待它。

在这里,我们不直接将任务分配给工作人员;而是在其间放置一个消息代理。

这个消息代理就像中间的一个队列。服务器将任务作为消息放入队列,工作器从队列中拉取并处理。处理完成后,工作器可以从队列中删除任务消息。

将消息放入消息队列的服务器称为生产者**,拉取并处理消息的服务器称为消费者(或工作者)**。

为什么我们要在中间放置一个消息代理?

  1. 它提供了可靠性。假设生产者停工。工人仍然可以毫无问题地工作。
  2. 它还提供了重试功能。假设工作进程在中间处理失败,那么它可以在一段时间后重试,因为消息仍然存在于消息代理中。
  3. 它使系统解耦。生产者和消费者都可以按照自己的节奏完成任务,并且彼此不依赖。

消息代理有两种类型:

  1. 消息队列(例如:RabbitMQ、AWS SQS)
  2. 消息流(例如:Apache Kafka、AWS Kinesis)

消息队列

顾名思义它是一种队列,生产者从一端放消息,消费者从另一端拉出消息进行处理。

消息队列和消息流之间唯一的不同之处在于:一个消息队列对于一种消息类型只能有一种消费者。

假设您有一个消息队列,其中放置视频的元数据,并让消费者从元数据中获取视频并将其转码为各种格式(480p,720p等)。

转码服务完成视频转码后,会从消息队列中删除该消息。各种消息队列,例如 RabbitMQ、AWS SQS 等,都提供了从队列中删除消息的 API。

如果一台视频转码服务器不够用,那我们该怎么办?
答)我们会水平扩展它。任何一台空闲的服务器都可以从消息队列中获取消息,对其进行处理,并在处理后将其从队列中删除。

这样,我们也可以进行并行处理。意味着一次可以处理多条消息。让我们有 3 个消费者,如图所示,然后这三个消费者可以同时处理 3 条不同的消息。

**为什么我们需要消息流?
**假设您还想通过字幕生成器服务为视频添加字幕。您将如何做到这一点?因为视频转码服务完成其工作并从消息队列中删除消息。一个答案可能是您将有两个消息队列;一个连接到视频转码服务,另一个连接到字幕生成器服务。这不是一个好的解决方案。假设视频上传服务(制作者)写入一个队列,但在写入另一个队列之前失败了。那么,在这种情况下,您将遇到不一致的情况,因为您已经对视频进行了转码,但没有生成字幕。在这种情况下,我们不使用消息队列。相反,我们使用消息流来解决问题。

消息流

在此,对于一条消息,我们可以拥有多种类型的消费者。

**何时使用消息流?
**当你想要“写入一个并被多个读取”时。

您可能想知道,如果视频转码服务在处理后删除了该消息,那么字幕生成器服务将如何处理该消息?如果视频转码服务不删除该消息,那么它可以多次处理该消息。嗯,事实并非如此。

在消息流中,消费者服务会迭代消息。这意味着视频转码器服务、字幕生成器服务和任何其他消费者服务将逐一迭代消息,即它将首先转到视频 1 元数据,然后是视频 2 元数据,依此类推。因此,通过这种方式,任何类型的消费者服务都只会处理这些消息一次。对于删除部分,消息永远不会被删除。它会永远存在。您可以手动删除它,也可以设置到期时间,但消费者服务无法删除消息流中的消息。

我希望消息队列和消息流之间的区别是清楚的。

我们什么时候使用消息代理?

您有两个微服务。它们之间最常见的两种通信方式是通过 Rest API 和 Message Broker。

我们在以下情况下使用消息代理:

  • 任务不重要。这意味着我们可以延迟发送。
    例如:发送电子邮件
  • 该任务需要很长时间来计算。
    例如:视频转码、生成 PDF 等

这就是消息代理理论部分。

练习:您的任务是在您的项目中利用 RabbitMQ 或 AWS SQS(任何人)来查看其实际运行情况。

Apache Kafka 深度探究

何时使用 Kafka?

Kafka 用作消息流。

Kafka 的吞吐量也非常高。这意味着你可以同时将大量数据转储到 Kafka 中,而 Kafka 可以处理这些数据而不会崩溃。

例如:假设 Uber 正在跟踪司机的位置。每隔 2 秒,获取司机的位置并将其插入。如果有数千名司机,并且我们每 2 秒在数据库中执行数千次写入,那么数据库可能会因为数据库的吞吐量(每秒操作数)较低而瘫痪。我们可以每 2 秒将这些数据放入 Kafka,因为它的吞吐量非常高。每隔 10 分钟,消费者将从 Kafka 中获取这些批量数据并将其写入数据库。这样,我们每 10 分钟在数据库中执行一次操作,而不是每 2 秒一次。

Kafka 内部原理

  • **生产者:**将消息发布到 Kafka。对于发送电子邮件,生产者可以向 Kafka 发送 {“email”,“message”}。
  • **消费者:**订阅Kafka主题并处理消息的反馈。
  • **Broker:**存储和管理主题的 Kafka 服务器。
  • **主题:**发布记录的类别/提要名称。sendEmail
    可以是主题。writeLocationToDB
    可以是主题。
  • 让我们以数据库为例:
    代理 = 数据库服务器
    主题 = 表
  • **分区:**每个主题被划分为多个分区以实现并行性。分区类似于数据库表中的分片。我们根据什么进行分区?为此,我们必须自己决定并编码。
    假设,对于我们的 sendNotification 主题,我们根据位置对其进行分区。北印度的数据进入分区 1,南印度的数据进入分区 2。
  • **消费者组:**当我们创建订阅主题的消费者时,我们必须为该消费者分配一个组。组内的每个消费者从分区子集中执行一种类型的处理。
    例如:对于视频处理,如前所述,我们可以有两个消费者组。一个消费者组用于视频转码,另一个消费者组用于字幕生成。

假设一个主题有四个分区和一个消费者组,其中有三个消费者(可能是属于同一消费者组的 3 个不同服务器)订阅了该主题。

分区: [分区-0,分区-1,分区-2,分区-3]

消费者: [消费者-1,消费者-2,消费者-3]

在这种情况下,Kafka 会执行重新平衡以在消费者之间分配分区。Kafka 的重新平衡是自行完成的。我们不必为此编写代码。

消费者-1 被分配到分区-0,消费者-2 被分配到分区-1,消费者-3 被分配到分区-3。

如果某个组中订阅某个主题的消费者数量大于该主题的分区数量,则每个消费者处理 1 个分区,多余的消费者不执行任何操作。

这意味着一个主题的一个分区只能由一个组中的一个消费者处理,但不同组的不同消费者可以处理同一个主题。

你明白了吗?如果我们想要水平扩展我们的消费者,那么我们还必须至少对主题进行与消费者相同数量的分区。

下图中,你可以看到我们有两个不同的消费者组,一个用于视频转码,另一个用于字幕生成。你还可以看到 Kafka 在每个组的消费者之间平衡了所有分区。

这就是 Kafka 的理论部分。

*练习:*你的任务是在笔记本电脑上本地设置 Kafka,并使用 NodeJS(或任何其他框架)编写涉及 Kafka 的任何应用程序,以便你能够更好地理解。我在这里没有展示代码,但我已经讲解了所有必需的理论。当你理解了理论,编码部分就很容易了。

实时发布订阅

在消息代理中,每当发布者将消息推送到消息代理时,该消息都会保留在代理中,直到消费者从代理中拉取该消息。如何从代理中拉取消息?所有消息代理(如 AWS SQS)都提供 API(或 SDK)来执行此操作。

在 Pubsub 中,发布者将消息推送到 Pubsub Broker 后,该消息会立即传递给订阅此 Broker 的消费者。在这里,消费者无需执行 API 调用或执行任何其他操作即可获取消息。Pubsub Broker 会自动实时将消息传递给消费者。

简单来说,在消息代理中,消费者从代理拉取消息,而Pubsub代理则将消息推送给消费者。

这里需要注意的一点是,消息不会存储/保留在 Pubsub 代理中。Pubsub 代理收到消息后,会立即将其推送给订阅此频道的所有消费者并处理完毕。它不存储任何内容。

实时发布订阅代理的示例:Redis

Redis 不仅用于缓存,还用于实时Pubsub。

在哪里使用实时发布订阅 (Realtime Pubsub)?

用例多种多样。当您想要构建延迟极低的应用程序时,可以使用此 Pubsub 功能。

一个用例是当您想要构建实时聊天应用程序时。对于聊天应用程序,我们使用 Websocket。但在水平扩展环境中,可以有许多服务器连接到不同的客户端,如下图所示。

如果client-1想要发送消息给client-3的话,他无法直接发送,因为client-3并没有连接到server-1,所以server-1在收到client-1的消息之后,就无法再将消息传递给client-3。

您需要以某种方式将客户端 1 的消息传递给服务器 2,然后服务器 2 可以将此消息发送给客户端 3。您可以通过 Redis Pubsub 执行此操作。参见下图。

练习:您的任务是在本地设置 Redis(正如我们在缓存部分所做的那样)并探索其 Pubsub 功能。

事件驱动架构

EDA 简介

EDA 是事件驱动架构的缩写。

在学习 EDA 之前,我们先来了解一下为什么需要它。
假设我们正在构建一个像亚马逊这样的电子商务平台。当用户购买一件商品时,他会向订单服务发送请求。此订单服务会验证该商品并调用支付服务进行支付。如果支付成功,则用户将被重定向到成功页面。在将用户重定向到成功页面之前,会调用 2 个 API,即库存服务(用于更新此产品的库存数量)和通知服务(用于向用户发送有关此订单的电子邮件)。

您可以看到,库存和通知服务与成功页面无关。它不会向客户端发送任何响应。因此,客户端无需等待他们的响应。这些是可以异步完成的非关键任务。

我们可以做的是将订单成功信息的任务放在消息代理中,让库存和通知服务异步使用它,而无需客户端等待。这就是事件驱动架构,生产者将消息(称为事件)放在消息代理中,然后忘掉它。现在,消费者的职责是处理它。

为什么要使用 EDA?

  1. **解耦:**生产者不需要知道消费者。
    在上面的例子中,没有使用 EDA 时,耦合非常紧密。假设库存服务发生故障,那么它会影响整个系统,因为订单服务直接调用它。但是使用 EDA 后,订单服务与库存服务没有任何关系。
  2. 弹性:一个组件的故障不会阻碍其他组件。
  3. 可扩展性:每个服务都可以水平扩展,而不会互相影响。

偶数驱动模式的类型

EDA 模式有 4 种类型:

  1. 简单事件通知
  2. 事件携带的状态转移
  3. 事件溯源
  4. 使用 CQRS(命令查询职责分离)进行事件源

我们只研究前两种模式,因为在现实生活中,大多数情况下只使用前两种模式。后两种模式有非常具体的用例,因此我们不会在这里介绍它们。

简单事件通知

我们看到的上面的例子是一个简单的事件通知。在这里,生产者只通知事件发生。消费者需要根据需要获取其他详细信息。

在此情况下,生产者只会将轻量级信息放在消息代理上。它只需将 order_id 放入其中,然后消费者从消息代理中拉取此信息,如果消费者想要此 order_id 的其他信息,则可从数据库中查询。

事件携带的状态转移

它与简单事件通知模式相同。这里唯一的区别是生产者将所有必要的详细信息作为事件的一部分发送到消息代理中,因此消费者不需要从数据库(或外部源)获取任何内容。

**优点:**减少延迟,因为消费者不需要进行任何额外的网络调用来获取更多信息。

**缺点:**事件规模较大,这可能会增加代理的存储和带宽成本。

分布式系统

什么是分布式系统?

假设我们正在计算一些东西。例如,计算 0 到 10000 之间所有素数的总和。那么我们可以简单地运行一个 for 循环并计算它。但是假设我们需要计算从 0 到 10¹⁰⁰ 的素数,那么在一台计算机上运行这个是不可行的。我们将达到极限,我们的单台计算机将无法运行。如果你不相信,那么在你的笔记本电脑上运行一个从 0 到 10¹⁰⁰ 的 for 循环。

分布式系统可以帮到我们。它的作用是利用多台计算机来完成一项任务。我们将通过划分任务让 10 台计算机并行执行此任务。这意味着一台计算机将从 (0 到 10¹⁰) 计算,另一台计算机将从 (10¹⁰ + 1 到 10²⁰) 计算,依此类推。最后,合并结果并将其返回给客户端。

简单来说,分布式系统意味着工作是由一组多台机器而不是一台机器来完成的。

分片也是分布式系统的一个例子,其中一个表的数据无法放在一台机器中,因此我们将该表切分并将这些部分放入多台机器中。

水平扩展也是分布式系统的一个例子,因为多台服务器处理请求。

分布式系统最具挑战性的部分是多台机器如何相互协调完成任务。以及如何利用这些机器并平等分配任务。

从客户端的角度来看,在分布式系统中,客户端应该感觉自己是在向一台机器(而不是多台机器)发出请求。系统的工作是从客户端接收请求,以分布式方式执行请求,然后返回结果。

我们前面学习的CAP定理是适用于所有分布式系统的基本定理。

大多数分布式系统的常见实现方式是选择一台服务器作为领导者,所有其他服务器作为追随者。领导者的职责是接收来自客户端的请求并将工作分配给追随者。追随者完成其工作并将结果返回给领导者。领导者将所有追随者的结果合并并返回给客户端。

在两种情况下,系统需要决定哪个服务器应该成为领导者:

  1. 那么,当整个系统第一次启动时,它需要决定哪个服务器应该成为领导者。
  2. 当领导者因任何原因倒下时,任何追随者都应该站出来成为领导者。为此,追随者应该随时快速检测领导者何时倒下。

您如何决定哪个服务器将成为领导者?

为此,有几种领导者选举算法,例如:

  • LCR 算法:时间复杂度为 O(N²)
  • HS算法:时间复杂度为O(NlogN)
  • Bully 算法:时间复杂度为 O(N)
  • 八卦协议:其时间复杂度为O(logN)

我们不会在这里讨论这些领导者选举算法,因为这超出了系统设计的范围。分布式系统本身是一个非常广泛的主题。但从系统设计的角度来看,分布式系统的这么多理论已经足够了。

只需将领导者选举算法视为一个黑盒子即可。它将使其中一个服务器成为领导者,并且每当领导者崩溃或停机时,整个系统(即大多数服务器)都会自动检测到它并再次运行领导者选举算法,以使任何新服务器成为领导者。

分布式数据库的示例: Apache Cassandra、AWS DynamoDB、MongoDB、Google Spanner、Redis 等。这些数据库利用分片和水平扩展来容纳大量数据和高效查询。

*练习:*研究一些领导者选举算法并用你最喜欢的编程语言对其进行编码。

使用领导者选举的自动恢复系统

假设您构建了一个水平可扩展的系统,其中的一些服务器被放置在负载均衡器后面以处理请求。您希望每次都有至少 4 台服务器来处理请求,无论如何。

为此,您始终需要亲自监视服务器,以便如果某个服务器崩溃或停机,您可以手动重新启动它。您不觉得这是一项繁琐无聊的任务吗?在本节中,我们将研究如何实现自动化。每当任何服务器停机时,系统都会自动检测并重新启动它,而无需我们手动执行。

为此,我们需要一个 Orchestrator。它的工作是始终监控服务器,并且每当任何服务器出现故障时,其 Orchestrator 的工作就是重新启动它。

那么,如果编排器出现故障,会发生什么情况呢?谁在监视编排器?

在这种情况下,领导者选举算法就派上用场了。在这里,我们不会只保留一台编排器服务器;相反,我们会保留一组多台编排器服务器。我们使用领导者选举算法选择其中一台编排器服务器作为领导者编排器。这个领导者编排器会密切关注所有工作者编排器,而这些工作者编排器会密切关注服务器。每当领导者编排器出现故障时,其中一个工作者编排器就会使用领导者选举算法将自己提升为领导者。

在这种设置下,我们不需要任何人为干预。系统将自动恢复。

每当任何服务器发生故障时,工作协调器都会将其重新启动。当任何工作协调器发生故障时,领导者会将其重新启动。当领导者协调器发生故障时,其中一个工作协调器将使用领导者选举算法将自己提升为领导者。

大数据工具

大数据工具示例:Apache Spark

当你拥有非常大量的数据时,我们就会使用大数据工具来处理它。

Big Data Tools 采用分布式系统方法。

单台机器无法处理大量数据。它会阻塞或变得太慢。在这种情况下,我们使用分布式系统。

其中,我们有一个协调器和多个工作者。客户端向协调器发出请求,协调器的工作是将大数据集分成较小的数据集,将其分配给工作者,从工作者那里获取结果并将其组合并返回结果。每个工作者计算协调器给出的一个小数据集。

在这个系统中,协调员需要处理几件事:

  • 如果任何工作器崩溃,则将其数据移动到另一台机器进行处理。
  • 恢复:这意味着如果任何工作程序出现故障,则重新启动它。
  • 获取来自工作者的各个结果并将它们组合起来以返回最终结果。
  • 数据的扩展和重新分配
  • 日志记录

何时使用分布式系统(或大数据工具)?

当单个 EC2 实例无法执行任务和处理大量数据时。

一些用例是:

  • 训练机器学习模型。
  • 分析社交网络或推荐系统
  • 从多个来源获取大量数据并将其转储到任何仓库。

构建这个系统非常困难,正如您所看到的,需要处理一些事情。这是一个非常常见的问题,因此有几种开源工具(例如 Spark 和 Flink)可以提供协调器和工作器的基础架构。

我们只关心业务逻辑,比如编写用于训练 ML 模型的代码。Apache Spark 等大数据工具为我们提供了分布式系统基础架构。我们将其用作黑匣子,只用 Python、Java 或 Scala 以 Spark 中的作业形式编写业务逻辑,Spark 将以分布式方式处理它。

我们不会在这里详细研究 Apache Spark,因为设计数据密集型应用程序本身就是一个广阔的领域。只有在处理大数据的公司工作时,你才会学习它。对于系统设计面试来说,这个关于大数据的理论就足够了。

深入探究一致性

首先我们要知道,只有当我们有一个分布式状态系统的时候,才会考虑一致性。

**有状态系统:**机器(或服务器)存储一些数据以供将来使用。

**无状态系统:**机器不保存任何有用的数据。

大多数情况下,我们的应用服务器是无状态的。它们不保存任何数据。但是,数据库是有状态的,因为它们存储了我们应用程序的所有数据。

分布式意味着利用多台机器而不是一台机器来完成我们的工作。

因为一致性意味着数据在任何时间点在所有节点(机器)上都应该相同。所以,只有当我们拥有有状态的分布式系统时,它才会出现。这就是为什么一致性这个术语只出现在数据库中,而不是在应用服务器上。

我们将数据库用作黑匣子。因此,您可能不会在这里进行太多编码,但请不要跳过它。这非常重要。将来,如果您编写任何有状态应用程序或构建自己的数据库,那么这些概念将很有用。此外,无论您使用什么数据库,例如 DynamoDB、Cassandra、MongoDB 等,请检查它们提供哪种类型的一致性。

一致性主要分为两种:

  1. 强一致性
  2. 最终一致性

强一致性

  • 写入操作之后的任何读取操作将始终返回最近的写入。
  • 一旦写入被确认,所有后续读取都将反映该写入。
  • 所有副本在确认写入之前必须就当前值达成一致。
  • 系统的行为就像只存在一个数据副本一样。

什么时候选择强一致性?

  • 用户将钱从一个账户转移到另一个账户的银行系统。
  • 用户可以获得最新和正确的股票价格的交易应用程序。

最终一致性

  • 它不能保证写入操作后立即实现一致性,但可以确保最终经过一段时间后,所有读取都将返回相同的值。
  • 所有副本反映最新的写入之前可能会有延迟。
  • 由于您在这里对一致性做出了妥协,因此您将获得高可用性(CAP 定理)。
  • 如果两个副本有不同的数据,则必须实施任何“冲突解决机制”来解决冲突。

何时选择最终一致性?

  • 社交媒体应用。如果任何帖子的点赞数不一致,并且稍后得到更正,则没问题。
  • 电子商务应用程序的产品目录。

实现强一致性的方法

  1. **同步复制 执行
    **写入操作时,所有副本都会在向客户端确认写入之前进行更新。
    例如:Google Spanner 等分布式数据库使用同步复制。
  2. **基于仲裁的协议
    **在分布式数据库中,遵循领导者-追随者设置,正如我们在分布式系统部分所研究的那样。当追随者完成写入或读取操作时,它会向领导者发出确认。
  • 读取仲裁是指返回读取数据的追随者数量。假设,对于键 user_id_2,5 个节点返回值“Shivam”。那么它的读取仲裁就是 5。
  • 写入仲裁是指确认特定键成功权限的追随者数量。
    在强一致性中,如果W是写入仲裁并且R是读取仲裁,那么其中W + R > NN 是节点总数。
    例如:AWS DynamoDB 和 Cassandra
  1. 共识算法
    这是分布式系统的一个大话题,所以我们不会在这里讨论它。总结一下,它使用领导者选举算法。并且,当超过 50% 的节点确认时,写入或读取即成功。
    如果你想了解更多,那么学习
    Raft
    。这是一种简单的共识算法。
    例如:Docker Swarm 在内部使用 Raft

实现最终一致性的方法

  1. **异步复制
    **写入会立即得到确认,更新会在后台传播到副本。
    例如:在 Cassandra、MongoDB 和 DynamoDB 等 NoSQL 数据库中很常见(默认模式)。
  2. **基于仲裁的协议(宽松一致性)
    **读取仲裁 + 写入仲裁 ≤ 节点数
    例如:最终一致性模式下的 Amazon DynamoDB。
  3. **向量时钟
    **这也是一个很长且小众的话题。我们不会在这里讨论。
    例如:AWS DynamoDB
  4. **Gossip 协议
    **节点与其他节点子集交换心跳,将更新传播到整个系统。心跳只是每 2-3 秒定期发送的 HTTP 或 TCP 请求。这样,我们就可以检测到任何故障节点。对于故障节点,我们根据应允许的副本读写数量配置一致性程度。
    例如:DynamoDB,Cassandra 使用它

一致性哈希

一致性哈希是一种算法,它告诉您哪些数据属于哪个节点。它只是一种算法,没有别的。

由于涉及数据,一致性哈希主要用于有状态的应用程序,并且系统是分布式的。

正如我们在上一节中讨论的那样,应用服务器是无状态的,而数据库是有状态的,因此一致性哈希主要用于数据库。如果你编写任何后端应用程序,那么它可能不会被使用,但是如果你从头开始编写任何数据库,那么它可以在那里使用。

为什么我们需要一致性哈希?

假设我们有一个键值分布式数据库,并且我们不使用一致性哈希,那么我们将遵循以下简单的方法来查找特定键属于哪个数据库服务器:

  • 使用任意哈希函数(如 SHA256、SHA128、MD5 等)对密钥进行哈希处理。
  • 然后,用服务器数量对该值取模,找到它的归属。

假设服务器数量为 3。key1
属于 => (Hash(key1) % 3) 服务器

当服务器数量固定时,这种方法完全没问题。这意味着我们没有自动扩展或动态数量的服务器。

当服务器数量变得动态时,就会出现问题。

假设,对于我们之前的例子,服务器数量从 3 变为 2。那么现在,key1 属于 => (Hash(key1) % 2) 服务器。这个数字可以与前一个不同。

您看到 key1 现在可能属于不同的服务器。因此,我们需要将 key1 从一台服务器移动到另一台服务器。这是上述方法的缺点。如果我们按照上述简单方法查找哪个密钥属于哪台服务器,那么当服务器数量发生变化时,将会发生大量数据移动。

如果我们使用一致性哈希方法来查找哪个键属于哪个服务器,那么当服务器数量发生变化时,数据移动将会最少。

如何使用一致性哈希来查找哪些数据属于哪个节点?

  • 我们获取服务器身份(例如 IP 或 ID)并将其传递给哈希函数(例如 SHA128)。它将生成 [0, 2¹²⁸) 之间的某个随机数。
  • 为了便于理解,我们将其放在环中。将环划分为 [0, 2¹²⁸),即哈希值的范围。无论将 server_id 传递给哈希函数后得到什么数字,都将该服务器放在环的该位置上。
  • 同样,对密钥也做上述操作。将圆划分为与哈希函数相同的范围,然后将密钥传递给哈希函数,无论结果是多少,都将密钥放在圆环的该位置。

任何密钥都将发送到其最近的顺时针服务器。在上面的例子中:

  • key-1 和 key-4 属于 Node-1
  • key-3 属于 Node-2
  • key-2 和 key-5 属于 Node-3

假设从上述设置中移除了节点 2,那么键 3 将转到节点 1,并且不会影响其他键。这就是一致性哈希的强大之处。它使键的移动最小化。但一致性哈希的工作不是进行这种移动。您需要自己动手。一致性哈希只是一种算法,它告诉哪个键属于哪个节点。

在现实生活中,您可以手动复制粘贴或拍摄数据快照以将其从一台服务器移动到另一台服务器。

一致性哈希的使用: AWS DynamoDB、Apache Cassandra、Riak 等分布式数据库在内部使用它。

*给你的练习:*用你最喜欢的编程语言实现一致性哈希。我上面讲了逻辑。实现它只是一个 leetcode 中级问题。

数据冗余与数据恢复

首先,我们要理解英文单词Redundancy(冗余)的含义,它指的是为同一份数据创建多份副本。

我们使数据库冗余,并将数据的副本存储在多个数据库服务器(机器)中。

为什么要让数据库变得冗余?

  • 存储数据备份可确保如果发生某些自然灾害并且保存我们的数据库服务器的数据中心被洪水淹没或发生其他情况,我们不会丢失数据。
  • 如果发生某些技术故障,并且数据库服务器的磁盘因某种原因损坏或崩溃,那么如果我们在另一台服务器上拥有数据的副本,我们就不会丢失数据。

数据备份的不同方法

没有规则。这取决于您如何恢复和备份数据的个人偏好。但在这里我提到了科技公司遵循的一些一般方法:

  • 每天晚上进行每日备份。无论数据库中发生什么更改,都进行快照(复制)并将其存储在不同的数据库服务器中。
  • 每周进行一次备份。

这样,如果数据库服务器发生故障,那么您至少还能获得截至最后一天或最后一周的数据。

连续冗余

上面我提到的每日和每周备份的方式现在一般都不用做了,现在大部分公司都是采用连续冗余。

在连续冗余中,我们设置了两个不同的数据库服务器。一个是主服务器,另一个是副本服务器。

当我们执行任何读取或写入操作时,它都会在主数据库中完成,并同步或异步(根据您的配置选择)复制到副本数据库中。此副本数据库不参与来自客户端的任何读取/写入操作。它的唯一工作是保持与主数据库同步。当主数据库中发生数据库故障时,此副本数据库将成为主数据库并开始处理请求。通过这种方式,我们使我们的系统具有弹性。

您的任务是在任何云提供商(如 AWS RDS)中设置此副本,并查看此操作在现实世界中是如何完成的。
第二个任务是在本地执行。在笔记本电脑上本地设置两个 MySQL 服务器并配置此复制。写入主服务器并查看它是否被复制到副本中。

代理人

什么是代理?

代理是位于客户端和另一个服务器之间的中介服务器。

您可能想知道为什么我们在中间引入了开销。我们将在这里研究它的几个用例。

代理有两种:
1.正向代理
2.反向代理

正向代理

正向代理代表客户端行事。当客户端发出请求时,该请求将通过正向代理。服务器不知道客户端的身份(IP)。服务器只知道正向代理 IP。

如果您曾经使用 VPN 访问网站,那么这就是正向代理的一个例子。VPN 代表您发出请求。

**主要功能:**隐藏客户端。服务器只能看到正向代理,看不到客户端。

使用案例:

  • 客户端使用正向代理访问受限内容(例如,访问地理封锁的网站)。
  • 它也用于缓存。正向代理可以缓存经常访问的内容,这样客户端就不必向服务器发出请求。内容直接从正向代理返回。
  • 组织(例如公司、大学等)设置正向代理,通过过滤某些网站来控制员工的互联网使用。

流动:

  • 客户请求www.abc.com
  • 正向代理接收请求并将其转发给服务器。
  • 服务器将响应发送回正向代理。
  • 正向代理将响应发送给客户端。

正向代理就讲到这里。在系统设计中,我们主要关注反向代理,而不是正向代理,因为正向代理与客户端(前端)相关。在系统设计中,我们主要构建服务器端(后端)应用程序。

反向代理

反向代理代表服务器运行。客户端将请求发送到反向代理,后者将请求转发到后端的相应服务器。响应通过反向代理返回到客户端。

在正向代理中,服务器不知道客户端的身份,但在反向代理中,客户端不知道服务器。它们将请求发送到反向代理。反向代理的工作是将此请求路由到适当的服务器。

**主要功能:**隐藏服务器。客户端只能看到反向代理,而看不到服务器。

反向代理的一个例子是负载均衡器。在负载均衡器中,客户端不知道实际的服务器。它们将请求发送到负载均衡器。

使用案例:

  • 负载均衡器,如您上面所见。
  • SSL 终止**:**处理加密和解密,减少服务器工作量。
  • 缓存**:**存储静态内容以减少服务器负载。
  • 安全性:保护后端服务器免受互联网直接暴露。

流动:

  • 客户请求www.abc.com
  • 反向代理接收请求。
  • 反向代理将请求转发到多个后端服务器之一。
  • 服务器将响应发送回代理。
  • 代理将响应发送给客户端。

反向代理示例:Ngnix、HAProxy

*练习:*了解 Ngnix 并将其用于您的副项目中。

构建我们自己的反向代理

我们学习了很多有关反向代理的知识,但如果不编写代码,我们的学习就不够。因此,我们将使用 NodeJS 从头编写自己的反向代理。代码非常简单,因此即使您还不了解 NodeJS,您也会理解它。

假设我们有两个微服务:一个是订单微服务,另一个是产品微服务。 来的请求/product会到产品微服务,来的请求/order会到订单微服务。 客户端只会向反向代理发出请求。 而根据 URL 将请求转发到正确的微服务的工作是由反向代理完成的。

假设产品微服务托管在http://localhost:5001,订单微服务托管在http://localhost:5002。在这里,如果您将其托管在实际的 AWS EC2 中,则可以用其域或 IP 地址替换它们。

1.安装依赖项

您需要在系统上安装 Node.js。首先初始化一个新的 Node.js 项目并安装所需的库。

npm init -y
npm 安装 http-proxy

2.创建反向代理服务器

const http = require ( ‘http’ );
const httpProxy = require ( ‘http-proxy’ );

// 创建代理服务器
const proxy = httpProxy.createProxyServer ( );

// 定义目标服务器
const target = {
productService : ‘http://localhost:5001’ ,
orderService : ‘http://localhost:5002’ ,
};

// 创建反向代理服务器
const server = http. createServer((req,res)=> {
//根据URL路径路由请求,
if(req.url.startsWith ( ’ /product’)){ proxy.web ( req,res,{ target:targets.productService },(error)=> { console.error(‘代理到产品服务时出错:’ , error.message ) ;res.writeHead (502); res.end (‘Bad Gateway’ ) ; }); } else if ( req.url.startsWith (‘/order’ )){ proxy.web (req,res,{ target:targets.orderService},(error)=> { console.error('代理到订单服务时出错: ’ ,error.message ) ; res.writeHead ( 502 ) ;res.end (‘Bad Gateway’); }); } else { //处理不匹配的路由 res.writeHead (404) ;res.end ( ‘未找到路由’ ) ;} }); //启动代理服务器const PORT = 8080 ; server.listen ( PORT , () => { console.log (

在 http://localhost: ${PORT}运行的反向代理服务器 );
});

此反向代理在端口 8080 上运行。

如果客户端使用 发出请求http://localhost:8080/product,它将被转发到产品服务后端。

代码很简单。只要读一下,你就会明白。

虽然我们简单地实现了反向代理,但我们不会在生产中使用它。我们总是使用 Ngnix 等专用反向代理。它们经过高度优化,并提供各种其他功能,例如缓存、SSL 终止等。

我们在本博客中学到的大多数东西,我们都可以自己编写代码,比如负载均衡器、反向代理、消息代理、Redis 等,但我们不会再重新发明轮子。我们将这些工具用作黑匣子,这些黑匣子来自以最优化方式解决这些问题的著名开源或大型组织。

如何解决任何系统设计问题?

解决任何系统设计问题的方法有很多种,但我认为这是解决问题的好方法(基于我的经验):

  • 理解问题陈述
    例如:假设问题是构建一个电子商务应用程序(如亚马逊)。然后,了解需求是什么以及我们需要构建哪些功能。
  • **将大问题分解为多个可解决的小子问题
    **构建电子商务是一个大问题。像在电子商务应用中一样将其分解为子问题。主要功能包括:
    • 列出产品
    • 产品的搜索功能
    • 订单
    • 处理付款
  • **专注于有效解决每个子问题
    **对于每个子问题,关注以下 4 件事:
    • 数据库
    • 缓存
    • 扩展和容错
    • 通信(异步或同步)
      在解决任何子问题时,如果您认为需要将该子问题再次分解为更多子问题,那么就这样做并针对该问题确定上述 4 件事。

注意:只有真正需要时才创建新的子问题。不要不必要地创建子问题,以免设计过于复杂。

博客结束

如果您读到了最后,那么恭喜您。

我花了很多时间写这篇博客。希望你喜欢阅读它。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值