system-design - 了解如何大规模设计系统并准备系统设计面试

Created at: 2022-08-16 02:59:54
开发语言:
授权协议: NOASSERTION

系统设计课程

嘿,欢迎来到这个课程。我希望这门课程能提供一个很好的学习体验。

本课程也可在我的网站上找到。如果这是有帮助的,请留下一个动机!

目录

什么是系统设计?

在开始本课程之前,让我们来谈谈什么是系统设计。

系统设计是为满足特定要求的系统定义体系结构、接口和数据的过程。系统设计通过连贯高效的系统满足你的业务或组织的需求。它需要一种系统化的建筑和工程系统方法。一个好的系统设计需要我们考虑一切,从基础设施一直到数据以及如何存储。

为什么系统设计如此重要?

系统设计帮助我们定义满足业务需求的解决方案。这是我们在构建系统时可以做出的最早的决定之一。通常,从高层次思考至关重要,因为这些决定以后很难纠正。它还使得随着系统的发展而更容易推理和管理体系结构更改。

知识产权

IP地址是标识互联网或本地网络上的设备的唯一地址。IP代表“互联网协议”,这是一套管理通过互联网或本地网络发送的数据格式的规则。

从本质上讲,IP地址是允许在网络上的设备之间发送信息的标识符。它们包含位置信息,并使设备可访问以进行通信。互联网需要一种方法来区分不同的计算机,路由器和网站。IP地址提供了一种这样做的方式,并构成了互联网工作方式的重要组成部分。

版本

现在,让我们了解不同版本的 IP 地址:

互联网4

最初的互联网协议是IPv4,它使用32位数字点十进制表示法,仅允许大约40亿个IP地址。最初,这已经足够了,但随着互联网采用率的增长,我们需要更好的东西。

示例:102.22.192.181

互联网网络6

IPv6是1998年引入的新协议。部署始于2000年代中期,由于互联网用户呈指数级增长,因此仍在进行中。

此新协议使用 128 位字母数字十六进制表示法。这意味着 IPv6 可以提供大约 340e+36 个 IP 地址。这足以满足未来几年不断增长的需求。

示例:2001:0db8:85a3:0000:0000:8a2e:0370:7334

类型

让我们讨论一下 IP 地址的类型:

公共

公共 IP 地址是一个主地址与整个网络关联的地址。在这种类型的 IP 地址中,每个连接的设备都具有相同的 IP 地址。

示例:ISP 提供给路由器的 IP 地址。

私有

专用 IP 地址是分配给连接到你的互联网网络的每台设备的唯一 IP 号码,其中包括家庭中使用的计算机、平板电脑和智能手机等设备。

示例:家用路由器为设备生成的 IP 地址。

静态的

静态 IP 地址不会更改,并且是手动创建的,而不是已分配的。这些地址通常更昂贵,但更可靠。

示例:它们通常用于重要的事情,例如可靠的地理位置服务,远程访问,服务器托管等。

动态

动态 IP 地址会不时更改,并且并不总是相同的。它已由动态主机配置协议 (DHCP) 服务器分配。动态IP地址是最常见的互联网协议地址类型。它们的部署成本更低,并允许我们根据需要在网络中重复使用IP地址。

示例:它们更常用于消费类设备和个人用途。

操作系统模型

OSI 模型是一个逻辑和概念模型,用于定义开放互连和与其他系统通信的系统所使用的网络通信。开放系统互连(OSI 模型)还定义了一个逻辑网络,并使用各种协议层有效地描述了计算机数据包传输。

OSI模型可以看作是计算机网络的通用语言。它基于将通信系统分成七个抽象层的概念,每个抽象层堆叠在最后一个层上。

为什么 OSI 模型很重要?

开放系统互连 (OSI) 模型定义了网络讨论和文档中使用的常用术语。这使我们能够将一个非常复杂的通信过程分开并评估其组件。

虽然此模型不是在当今最常见的TCP / IP网络中直接实现的,但它仍然可以帮助我们做更多的事情,例如:

  • 使故障排除更容易,并帮助识别整个堆栈中的威胁。
  • 鼓励硬件制造商创建可以通过网络相互通信的网络产品。
  • 对于培养安全第一的心态至关重要。
  • 将复杂函数分离为更简单的组件。

OSI 模型的七个抽象层可以从上到下定义如下:

osi-model

应用

这是与来自用户的数据直接交互的唯一层。Web 浏览器和电子邮件客户端等软件应用程序依赖于应用程序层来启动通信。但应该明确的是,客户端软件应用程序不是应用程序层的一部分,而是应用程序层负责软件所依赖的协议和数据操作,以向用户呈现有意义的数据。应用层协议包括 HTTP 和 SMTP。

介绍

表示层也称为转换层。此处提取来自应用层的数据,并根据所需的格式进行操作,以通过网络进行传输。表示层的功能是转换、加密/解密和压缩。

会期

这是负责打开和关闭两个设备之间通信的层。打开通信和关闭通信之间的时间称为会话。会话层确保会话保持打开状态足够长的时间以传输正在交换的所有数据,然后立即关闭会话以避免浪费资源。会话层还将数据传输与检查点同步。

运输

传输层(也称为第 4 层)负责两个设备之间的端到端通信。这包括从会话层获取数据,然后将其分解为称为段的块,然后再将其发送到网络层(第 3 层)。它还负责将接收设备上的段重新组装为会话层可以使用的数据。

网络

网络层负责促进两个不同网络之间的数据传输。网络层在发送方的设备上将传输层中的段分解为更小的单元(称为数据包),并在接收设备上重新组装这些数据包。网络层还为数据找到到达其目的地的最佳物理路径,这称为路由。如果通信的两个设备位于同一网络上,则不需要网络层。

数据链路

数据链路层与网络层非常相似,不同之处在于数据链路层便于在同一网络上的两个设备之间传输数据。数据链路层从网络层获取数据包,并将它们分解成称为帧的较小部分。

物理的

该层包括数据传输中涉及的物理设备,例如电缆和交换机。这也是将数据转换为位流的层,位流是 1 和 0 的字符串。两个设备的物理层还必须就信号约定达成一致,以便可以将 1 与两个设备上的 0 区分开来。

网络通信和统一通信

断续器

传输控制协议(TCP)是面向连接的,这意味着一旦建立了连接,数据就可以在两个方向上传输。TCP具有内置系统来检查错误并保证数据将按发送顺序交付,使其成为传输静止图像,数据文件和网页等信息的完美协议。

断续器

但是,虽然TCP本能地可靠,但其反馈机制也会导致更大的开销,从而更多地利用网络上的可用带宽。

统一数据库

用户数据报协议 (UDP) 是一种更简单、无连接的互联网协议,其中不需要错误检查和恢复服务。使用 UDP 时,打开连接、维护连接或终止连接不会产生任何开销。数据会不断发送给收件人,无论他们是否收到数据。

断续器

它在很大程度上是广播或多播网络传输等实时通信的首选。当我们需要最低的延迟并且延迟数据比数据丢失更糟糕时,我们应该通过TCP使用UDP。

技术合作计划与统一数据库

TCP 是面向连接的协议,而 UDP 是无连接的协议。TCP 和 UDP 之间的一个关键区别是速度,因为 TCP 比 UDP 相对较慢。总体而言,UDP是一种更快,更简单,更有效的协议,但是,只有使用TCP才能重新传输丢失的数据包。

TCP提供从用户到服务器的有序数据交付(反之亦然),而UDP不专用于端到端通信,也不检查接收方的准备情况。

特征 断续器 统一数据库
连接 需要已建立的连接 无连接协议
保证交货 可以保证数据的交付 无法保证数据的交付
再传输 可以重新传输丢失的数据包 不会重新传输丢失的数据包
速度 比 UDP 慢 比技术合作计划更快
广播 不支持广播 支持广播
使用案例 HTTPS,HTTP,SMTP,POP,FTP等 视频流,域名系统,网络电话等

域名系统

之前,我们了解了使每台计算机都能够与其他计算机连接的IP地址。但正如我们所知,人类对名字比对数字更满意。记住类似的名字比记住类似的名字更容易。

google.com
122.250.192.232

这就把我们带到了域名系统(DNS),这是一个分层和分散的命名系统,用于将人类可读的域名转换为IP地址。

域名系统的工作原理

如何-dns-工作

DNS 查找涉及以下八个步骤:

  1. 客户端 example.com 键入 Web 浏览器,查询将传输到互联网并由 DNS 解析程序接收。
  2. 然后,解析程序以递归方式查询 DNS 根域名服务器。
  3. 根服务器使用顶级域 (TLD) 的地址响应解析程序。
  4. 然后,解析程序向 TLD 发出请求。
    .com
  5. 然后,TLD 服务器使用域的名称服务器的 IP 地址进行响应,example.com
  6. 最后,递归解析程序将查询发送到域的名称服务器。
  7. 然后,example.com 的 IP 地址将从名称服务器返回到解析程序。
  8. 然后,DNS 解析程序使用最初请求的域的 IP 地址响应 Web 浏览器。

解析 IP 地址后,客户端应该能够从解析的 IP 地址请求内容。例如,解析的 IP 可能会返回要在浏览器中呈现的网页。

服务器类型

现在,让我们看一下组成 DNS 基础结构的四组关键服务器。

域名解析程序

DNS 解析程序(也称为 DNS 递归解析程序)是 DNS 查询中的第一个停靠点。递归解析器充当客户端和 DNS 名称服务器之间的中间人。从 Web 客户端接收到 DNS 查询后,递归解析器将使用缓存的数据进行响应,或者向根域名服务器发送请求,然后向 TLD 名称服务器发送另一个请求,然后向权威名称服务器发送最后一个请求。在收到来自包含所请求 IP 地址的权威名称服务器的响应后,递归解析程序随后会向客户端发送响应。

域名解析根服务器

根服务器接受递归解析器的查询,其中包括域名,根名称服务器根据该域的扩展名(、等)将递归解析器定向到 TLD 域名服务器来响应。根域名服务器由一个名为互联网名称与数字地址分配机构(ICANN)的非营利组织监督。

.com
.net
.org

每个递归解析器都知道 13 个 DNS 根域名服务器。请注意,虽然有 13 个根域名服务器,但这并不意味着根域名服务器系统中只有 13 台计算机。有 13 种类型的根域名服务器,但世界各地都有每个版本的多个副本,它们使用 Anycast 路由来提供快速响应。

顶级域名服务器

TLD 域名服务器维护共享公共域名后缀的所有域名的信息,例如 、 或 URL 中最后一个点之后的任何内容。

.com
.net

TLD 域名服务器的管理由互联网号码分配机构 (IANA) 负责,该机构是 ICANN 的一个分支机构。IANA 将 TLD 服务器分为两个主要组:

  • 通用顶级域:这些域类似于 、 、 、 和 。
    .com
    .org
    .net
    .edu
    .gov
  • 国家/地区代码顶级域名:这些域名包括特定于某个国家/地区或州/省/市/自治区的任何域名。示例包括 、 、 和 。
    .uk
    .us
    .ru
    .jp

权威域名解析服务器

权威域名服务器通常是解析器在 IP 地址旅程中的最后一步。权威域名服务器包含特定于其所服务的域名的信息(例如 google.com),它可以提供递归解析器,其中包含在 DNS A 记录中找到的该服务器的 IP 地址,或者如果域具有 CNAME 记录(别名),它将为递归解析器提供别名域,此时递归解析器将不得不执行全新的 DNS 查找,以便从权威服务器获取记录名称服务器(通常是包含 IP 地址的 A 记录)。如果找不到域,则返回 NXD 域消息。

查询类型

DNS 系统中有三种类型的查询:

递归的

在递归查询中,DNS 客户端要求 DNS 服务器(通常是 DNS 递归解析程序)使用请求的资源记录或错误消息(如果解析程序找不到该记录)响应客户端。

迭 代

在迭代查询中,DNS 客户端提供主机名,DNS 解析程序将返回它所能提供的最佳答案。如果 DNS 解析程序的缓存中具有相关的 DNS 记录,它将返回这些记录。如果不是,它将 DNS 客户端引用到根服务器或最接近所需 DNS 区域的其他权威名称服务器。然后,DNS 客户端必须直接对它所引用的 DNS 服务器重复查询。

非递归

非递归查询是 DNS 解析程序已经知道答案的查询。它要么立即返回DNS记录,因为它已经将其存储在本地缓存中,要么查询对该记录具有权威性的DNS名称服务器,这意味着它肯定拥有该主机名的正确IP。在这两种情况下,都不需要额外的查询轮次(如递归或迭代查询)。相反,响应会立即返回到客户端。

记录类型

DNS 记录(也称为区域文件)是位于权威 DNS 服务器中的指令,提供有关域的信息,包括与该域关联的 IP 地址以及如何处理对该域的请求。

这些记录由一系列以所谓的 DNS 语法编写的文本文件组成。DNS 语法只是一串字符,用作命令,告诉 DNS 服务器要执行的操作。所有 DNS 记录还具有“TTL”,它代表生存时间,并指示 DNS 服务器刷新该记录的频率。

还有更多的记录类型,但现在,让我们来看看一些最常用的记录类型:

  • A(地址记录):这是保存域的 IP 地址的记录。
  • AAAA(IP 版本 6 地址记录):包含域的 IPv6 地址的记录(与存储 IPv4 地址的 A 记录相反)。
  • CNAME(规范名称记录):将一个域或子域转发到另一个域,不提供 IP 地址。
  • MX(邮件交换器记录):将邮件定向到电子邮件服务器。
  • TXT(文本记录):此记录允许管理员在记录中存储文本注释。这些记录通常用于电子邮件安全。
  • NS(名称服务器记录):存储 DNS 条目的名称服务器。
  • SOA(权限启动):存储有关域的管理员信息。
  • SRV(服务位置记录):指定特定服务的端口。
  • PTR(反向查找指针记录):在反向查找中提供域名。
  • CERT(证书记录):存储公钥证书。

子域

子域名是我们主域名的附加部分。它通常用于在逻辑上将网站分成几个部分。我们可以在主域上创建多个子域或子域。

例如,其中 是子域,是主域,是顶级域 (TLD)。类似的例子可以是 或 。

blog.example.com
blog
example
.com
support.example.com
careers.example.com

域名系统区域

DNS 区域是域命名空间的一个不同部分,它委派给负责维护 DNS 区域的法人实体(如个人、组织或公司)。DNS 区域也是一种管理功能,允许对 DNS 组件(如权威名称服务器)进行精细控制。

域名缓存

DNS缓存(有时称为DNS解析器缓存)是一个临时数据库,由计算机的操作系统维护,其中包含所有最近访问和尝试访问网站和其他互联网域的记录。换句话说,DNS缓存只是最近DNS查找的内存,我们的计算机在尝试弄清楚如何加载网站时可以快速引用。

域名系统在每个 DNS 记录上实现生存时间 (TTL)。TTL 指定 DNS 客户端或服务器可以缓存记录的秒数。当记录存储在缓存中时,它附带的任何 TTL 值也会被存储。服务器继续更新存储在缓存中的记录的 TTL,每秒倒计时一次。当它达到零时,记录将从缓存中删除或清除。此时,如果收到对该记录的查询,则 DNS 服务器必须启动解析过程。

反向域名解析

反向 DNS 查找是对与给定 IP 地址关联的域名的 DNS 查询。这与更常用的正向 DNS 查找相反,在正向 DNS 查找中,将查询 DNS 系统以返回 IP 地址。反向解析 IP 地址的过程使用 PTR 记录。如果服务器没有 PTR 记录,则无法解析反向查找。

电子邮件服务器通常使用反向查找。电子邮件服务器在将电子邮件引入其网络之前,会检查并查看电子邮件是否来自有效服务器。许多电子邮件服务器将拒绝来自任何不支持反向查找的服务器的邮件,或者来自极不可能合法的服务器的邮件。

注意:反向DNS查找并未被普遍采用,因为它们对互联网的正常功能并不重要。

例子

以下是一些广泛使用的托管 DNS 解决方案:

负载均衡

负载平衡允许我们在多个资源之间分配传入的网络流量,通过仅向联机资源发送请求来确保高可用性和可靠性。这提供了根据需求添加或减少资源的灵活性。

负载平衡

为了获得额外的可扩展性和冗余性,我们可以尝试在系统的每一层进行负载平衡:

负载平衡层

但是为什么?

现代高流量网站必须为来自用户或客户端的数十万(如果不是数百万)并发请求提供服务。为了经济高效地进行扩展以满足这些高容量,现代计算最佳实践通常需要添加更多服务器。

负载平衡器可以位于服务器前面,并在能够以最大化速度和容量利用率的方式满足这些请求的所有服务器之间路由客户端请求。这可确保没有单个服务器过度工作,这可能会降低性能。如果单个服务器出现故障,负载平衡器会将流量重定向到其余的联机服务器。将新服务器添加到服务器组后,负载平衡器会自动开始向其发送请求。

工作负载分配

这是负载均衡器提供的核心功能,具有几种常见的变体:

  • 基于主机:根据请求的主机名分发请求。
  • 基于路径:使用整个 URL 来分发请求,而不仅仅是主机名。
  • 基于内容:检查请求的消息内容。这允许基于内容(如参数值)进行分发。

一般来说,负载均衡器在以下两个级别之一运行:

网络层

这是在网络传输层(也称为第 4 层)工作的负载均衡器。这将根据网络信息(如 IP 地址)执行路由,并且无法执行基于内容的路由。这些通常是可以高速运行的专用硬件设备。

应用层

这是在应用程序层(也称为第 7 层)运行的负载均衡器。负载均衡器可以完整读取请求并执行基于内容的路由。这允许基于对流量的充分理解来管理负载。

类型

让我们看一下不同类型的负载均衡器:

软件

软件负载平衡器通常比硬件版本更易于部署。它们也往往更具成本效益和灵活性,并且它们与软件开发环境结合使用。软件方法使我们能够灵活地根据环境的特定需求配置负载平衡器。灵活性的提高可能是以必须做更多工作来设置负载均衡器为代价的。与提供更多封闭式方法的硬件版本相比,软件平衡器为我们提供了更大的更改和升级自由度。

软件负载平衡器被广泛使用,可作为需要配置和管理的可安装解决方案或托管云服务提供。

硬件

顾名思义,硬件负载平衡器依赖于物理本地硬件来分发应用程序和网络流量。这些设备可以处理大量流量,但通常价格昂贵,并且在灵活性方面相当有限。

硬件负载平衡器包括专有固件,需要作为新版本进行维护和更新,并发布安全修补程序。

域名解析

DNS 负载平衡是在域名系统 (DNS) 中配置域的做法,以便客户端对域的请求分布在一组服务器计算机中。

不幸的是,DNS负载平衡存在固有的问题,限制了其可靠性和效率。最重要的是,DNS 不会检查服务器和网络中断或错误。它始终为域返回同一组 IP 地址,即使服务器已关闭或无法访问也是如此。

路由算法

现在,让我们讨论一下常用的路由算法:

  • 轮循机制:请求以轮换方式分发到应用程序服务器。
  • 加权轮循机制:基于简单的轮循机制技术构建,使用管理员可通过 DNS 记录分配的权重来考虑不同的服务器特征,例如计算和流量处理容量。
  • 最少连接数:将新请求发送到当前与客户端的连接最少的服务器。每个服务器的相对计算能力在确定哪台服务器的连接最少时都会被考虑在内。
  • 最短响应时间:将请求发送到由公式选择的服务器,该公式结合了最快的响应时间和最少的活动连接。
  • 最小带宽:此方法以兆比特每秒 (Mbps) 为单位测量流量,将客户端请求发送到流量最小 Mbps 的服务器。
  • 哈希:根据我们定义的密钥(如客户端 IP 地址或请求 URL)分发请求。

优势

负载平衡在防止停机方面也起着关键作用,负载平衡的其他优点包括:

  • 可伸缩性
  • 冗余
  • 灵活性
  • 效率

冗余负载平衡器

正如你一定已经猜到的那样,负载均衡器本身可能是单点故障。为了克服这个问题,可以在群集模式下使用第二个或多个负载均衡器。

N

而且,如果发生故障检测并且主动负载均衡器发生故障,则另一个被动负载均衡器可以接管,这将使我们的系统更具容错能力。

冗余负载平衡

特征

以下是负载均衡器的一些常用功能:

  • 自动缩放:启动和关闭资源以响应需求条件。
  • 粘性会话:能够将同一用户或设备分配给同一资源,以维护资源的会话状态。
  • 运行状况检查:能够确定资源是否已关闭或性能不佳,以便从负载平衡池中删除该资源。
  • 持久连接:允许服务器打开与客户端(如 WebSocket)的持久连接。
  • 加密:处理加密连接,如 TLS 和 SSL。
  • 证书:向客户端提供证书并对客户端证书进行身份验证。
  • 压缩:压缩响应。
  • 缓存:应用层负载均衡器可能提供缓存响应的功能。
  • 日志记录:请求和响应元数据的日志记录可以作为分析数据的重要审核跟踪或源。
  • 请求跟踪:为每个请求分配一个唯一的 ID,以便进行日志记录、监视和故障排除。
  • 重定向:能够根据请求的路径等因素重定向传入请求。
  • 固定响应:返回请求的静态响应,如错误消息。

例子

以下是业界常用的一些负载平衡解决方案:

聚类

在较高级别上,计算机群集是由两台或多台计算机或节点组成的组,它们并行运行以实现共同目标。这允许由大量单独的、可并行化的任务组成的工作负载分布在群集中的节点之间。因此,这些任务可以利用每台计算机的组合内存和处理能力来提高整体性能。

要构建计算机群集,应将各个节点连接到网络以启用节间通信。然后,该软件可用于将节点连接在一起并形成集群。它可能在每个节点上都有一个共享存储设备和/或本地存储。

簇

通常,至少有一个节点被指定为领导节点,并充当群集的入口点。领导节点可能负责将传入的工作委派给其他节点,并在必要时聚合结果并将响应返回给用户。

理想情况下,集群的功能就像一个系统一样。访问群集的用户不需要知道系统是群集还是单个计算机。此外,群集的设计应尽量减少延迟并防止节点到节点通信中的瓶颈。

类型

计算机群集通常可分为三种类型:

  • 高可用性或故障转移
  • 负载均衡
  • 高性能计算

配置

两种最常用的高可用性 (HA) 群集配置是主动-主动和主动-被动。

主动-主动

主动-主动

主动-主动群集通常由至少两个节点组成,这两个节点同时主动运行同一类型的服务。主动-主动群集的主要目的是实现负载平衡。负载均衡器在所有节点之间分配工作负载,以防止任何单个节点过载。由于有更多的节点可供使用,因此吞吐量和响应时间也将得到改善。

主动-被动

主动-被动

与主动-主动群集配置一样,主动-被动群集也至少包含两个节点。但是,正如主动-被动名称所暗示的那样,并非所有节点都将是主动的。例如,在两个节点的情况下,如果第一个节点已经处于活动状态,则第二个节点必须是被动节点或处于备用状态。

优势

集群计算的四个关键优势如下:

  • 高可用性
  • 可伸缩性
  • 性能
  • 高性价比

负载平衡与群集

负载平衡与群集共享一些共同特征,但它们是不同的进程。群集提供冗余并提高容量和可用性。群集中的服务器相互了解,并协同工作以实现共同目标。但是通过负载平衡,服务器彼此之间并不了解。相反,它们会对从负载均衡器收到的请求做出 React 。

我们可以将负载平衡与群集结合使用,但它也适用于涉及共享共同目的的独立服务器的情况,例如运行网站,业务应用程序,Web服务或其他一些IT资源。

挑战

群集带来的最明显挑战是安装和维护的复杂性增加。必须在每个节点上安装和更新操作系统、应用程序及其依赖项。

如果群集中的节点不是同构的,这将变得更加复杂。还必须密切监视每个节点的资源利用率,并应聚合日志以确保软件正常运行。

此外,存储变得更加难以管理,共享存储设备必须防止节点相互覆盖,并且分布式数据存储必须保持同步。

例子

群集在行业中很常见,并且通常许多技术都提供某种群集模式。例如:

缓存

“计算机科学中只有两件困难的事情:缓存失效和命名东西。

缓存

缓存的主要目的是通过减少访问底层较慢存储层的需要来提高数据检索性能。为了换取速度,缓存通常暂时存储数据子集,而数据库的数据通常是完整且持久的。

缓存利用参考位置原则“最近请求的数据可能会再次请求”。

缓存和内存

与计算机的内存一样,缓存是一种紧凑、快速执行的内存,它将数据存储在级别层次结构中,从级别开始,然后按顺序从那里开始。它们被标记为 L1、L2、L3 等。如果收到请求,也会写入缓存,例如,当有更新并且需要将新内容保存到缓存中以替换已保存的旧内容时。

无论缓存是读取还是写入,它都是一次一个块完成的。每个块还具有一个标记,其中包含数据存储在缓存中的位置。从缓存请求数据时,将通过标记进行搜索,以查找内存的级别 1 (L1) 中所需的特定内容。如果未找到正确的数据,则会在 L2 中执行更多搜索。

如果在那里找不到数据,则在L3中继续搜索,然后在L4中继续搜索,依此类推,直到找到它,然后读取并加载它。如果在缓存中根本找不到数据,则会将其写入缓存中,以便下次快速检索。

缓存命中和缓存未命中

缓存命中

缓存命中描述从缓存成功提供内容的情况。在内存中快速搜索标签,当找到并读取数据时,它被视为缓存命中。

冷缓存、热缓存和热缓存

缓存命中也可以描述为冷、暖或热。在其中每个中,都描述了读取数据的速度。

热缓存是以尽可能的速率从内存中读取数据的实例。从 L1 检索数据时会发生这种情况。

冷缓存是读取数据的最慢速率,但是,它仍然成功,因此它仍然被视为缓存命中。数据只是在内存层次结构(如 L3)中较低或更低。

热缓存用于描述在 L2 或 L3 中找到的数据。它不如热缓存快,但它仍然比冷缓存快。通常,调用缓存为热缓存用于表示它比热缓存更慢,更接近冷缓存。

缓存未命中

缓存未命中是指搜索内存时的实例,并且找不到数据。发生这种情况时,内容将被传输并写入缓存。

缓存失效

缓存失效是计算机系统将缓存条目声明为无效并删除或替换它们的过程。如果数据被修改,它应该在缓存中失效,否则,这可能会导致不一致的应用程序行为。有三种类型的缓存系统:

直写式缓存

直通式缓存写入

数据同时写入缓存和相应的数据库。

优点:快速检索,缓存和存储之间的数据完全一致性。

缺点:写入操作的延迟更高。

绕写缓存

绕写缓存

写入直接转到数据库或永久存储,绕过缓存。

优点:这可以减少延迟。

缺点:它会增加缓存未命中,因为缓存系统必须在缓存未命中的情况下从数据库中读取信息。因此,对于快速写入和重新读取信息的应用程序,这可能会导致更高的读取延迟。读取发生在较慢的后端存储中,并且会遇到更高的延迟。

回写式缓存

回写式缓存

其中,仅对缓存层执行写入操作,并且在写入缓存完成后立即确认写入。然后,缓存将此写入异步同步到数据库。

优点:这将减少写入密集型应用程序的延迟和高吞吐量。

缺点:如果缓存层崩溃,则存在数据丢失的风险。我们可以通过让多个副本确认缓存中的写入来改善这一点。

驱逐政策

以下是一些最常见的缓存逐出策略:

  • 先进先出 (FIFO):缓存逐出首先访问的第一个块,而不考虑之前访问的频率或次数。
  • 后进先出(LIFO):缓存逐出最近首先访问的块,而不考虑之前访问的频率或次数。
  • 最近最少使用 (LRU):首先丢弃最近最少使用的项目。
  • 最近使用 (MRU):与 LRU 相比,丢弃最近使用的项目。
  • 最不常用 (LFU):计算需要某个项目的频率。那些最不经常使用的那些首先被丢弃。
  • 随机替换 (RR):随机选择候选项,并在必要时丢弃它以腾出空间。

分布式缓存

分布式缓存

分布式缓存是将多台联网计算机的随机存取内存 (RAM) 汇集到用作数据缓存的单个内存中数据存储中的系统,以提供对数据的快速访问。虽然大多数缓存传统上位于一个物理服务器或硬件组件中,但通过将多台计算机链接在一起,分布式缓存可能会超出单台计算机的内存限制。

全局缓存

全局缓存

顾名思义,我们将有一个所有应用程序节点都将使用的单个共享缓存。如果在全局缓存中找不到请求的数据,则缓存负责从基础数据存储中找出丢失的数据片段。

使用案例

缓存可以有许多实际用例,例如:

  • 数据库缓存
  • 内容交付网络
  • 域名系统 (DNS) 缓存
  • 接口缓存

何时不使用缓存?

我们还让我们看一些不应该使用缓存的场景:

  • 当访问缓存所需的时间与访问主数据存储所需的时间一样长时,缓存没有帮助。
  • 当请求具有低重复(较高随机性)时,缓存不起作用,因为缓存性能来自重复的内存访问模式。
  • 当数据频繁更改时,缓存没有帮助,因为缓存版本不同步,并且每次都必须访问主数据存储。

请务必注意,缓存不应用作永久数据存储。它们几乎总是在易失性存储器中实现,因为它更快,因此应该被认为是瞬态的。

优势

以下是缓存的一些优点:

  • 提高性能
  • 减少延迟
  • 减少数据库上的负载
  • 降低网络成本
  • 提高读取吞吐量

例子

以下是一些常用的缓存技术:

内容交付网络

内容交付网络 (CDN) 是一组地理位置分散的服务器,它们协同工作以提供互联网内容的快速交付。通常,静态文件(如 HTML/CSS/JS、照片和视频)从 CDN 提供。

光盘 map

为什么要使用 CDN?

内容交付网络 (CDN) 可提高内容可用性和冗余性,同时降低带宽成本并提高安全性。从 CDN 提供内容可以显著提高性能,因为用户从靠近他们的数据中心接收内容,并且我们的服务器不必为 CDN 满足的请求提供服务。

CDN 如何工作?

断续器

在 CDN 中,源服务器包含内容的原始版本,而边缘服务器数量众多,分布在世界各地的不同位置。

为了最大限度地减少访问者与网站服务器之间的距离,CDN 将其内容的缓存版本存储在称为边缘站点的多个地理位置。每个边缘站点都包含多个缓存服务器,负责将内容交付给其附近的访问者。

一旦静态资产缓存在特定位置的所有CDN服务器上,所有后续网站访问者对静态资产的请求都将从这些边缘服务器而不是源服务器传递,从而减少源负载并提高可伸缩性。

例如,当英国的某人请求我们可能托管在美国的网站时,将从最近的边缘站点(如伦敦边缘站点)为他们提供服务。这比让访问者向源服务器发出完整请求要快得多,这将增加延迟。

类型

CDN一般分为两种类型:

推送光盘

推送 CDN 会在服务器上发生更改时接收新内容。我们全权负责提供内容、直接上传到 CDN 以及重写 URL 以指向 CDN。我们可以配置内容何时过期以及何时更新。仅当内容是新的或更改的内容时才上载,从而最大限度地减少流量,但最大化存储。

流量较少的站点或内容不经常更新的站点与推送 CDN 配合使用效果很好。

拉动光盘

在“拉取 CDN”情况下,缓存会根据请求进行更新。当客户端发送一个请求,该请求需要从 CDN 提取静态资产(如果 CDN 没有静态资产)时,它将从源服务器获取新更新的资产,并使用此新资产填充其缓存,然后将此新缓存的资产发送给用户。

与 Push CDN 相反,这需要较少的维护,因为 CDN 节点上的缓存更新是根据从客户端到源服务器的请求执行的。流量大的网站与拉取CDN配合得很好,因为流量分布得更均匀,CDN上只剩下最近请求的内容。

众所周知,好事会带来额外的成本,所以让我们讨论一下CDN的一些缺点:

  • 额外费用:使用CDN可能很昂贵,特别是对于高流量服务。
  • 限制:某些组织和国家/地区已阻止流行 CDN 的域或 IP 地址。
  • 位置:如果我们的大多数受众位于CDN没有服务器的国家/地区,则我们网站上的数据可能必须比不使用任何CDN更远。

例子

以下是一些广泛使用的 CDN:

代理

代理服务器是位于客户端和后端服务器之间的中间硬件/软件。它接收来自客户端的请求,并将其中继到源服务器。通常,代理用于筛选请求、记录请求,或者有时转换请求(通过添加/删除标头、加密/解密或压缩)。

类型

有两种类型的代理:

转发代理

转发代理(通常称为代理、代理服务器或 Web 代理)是位于一组客户端计算机前面的服务器。当这些计算机向互联网上的站点和服务发出请求时,代理服务器会拦截这些请求,然后代表这些客户端(如中间人)与Web服务器进行通信。

转发代理

优势

以下是转发代理的一些优点:

  • 阻止访问某些内容
  • 允许访问受地理限制的内容
  • 提供匿名性
  • 避免其他浏览限制

虽然代理提供了匿名的好处,但它们仍然可以跟踪我们的个人信息。代理服务器的设置和维护可能成本高昂,并且需要配置。

反向代理

反向代理是位于一个或多个 Web 服务器前面的服务器,用于拦截来自客户端的请求。当客户端向网站的源服务器发送请求时,这些请求会被反向代理服务器拦截。

正向代理和反向代理之间的区别很微妙,但很重要。总结一下,一种简化的方法是说转发代理位于客户端前面,并确保没有源服务器直接与该特定客户端通信。另一方面,反向代理位于源服务器的前面,并确保没有客户端直接与该源服务器通信。

反向代理

引入反向代理会导致复杂性增加。单个反向代理是单点故障,配置多个反向代理(即故障转移)会进一步增加复杂性。

优势

以下是使用反向代理的一些优点:

  • 提高安全性
  • 缓存
  • 软件安全加密
  • 负载均衡
  • 可扩展性和灵活性

负载均衡器与反向代理

等等,反向代理不是类似于负载均衡器吗?好吧,当我们有多个服务器时,作为负载平衡器是有用的。通常,负载均衡器将流量路由到一组提供相同功能的服务器,而反向代理即使只有一个 Web 服务器或应用程序服务器也很有用。反向代理也可以充当负载均衡器,但不能相反。

例子

以下是一些常用的代理技术:

可用性

可用性是指系统在特定时间段内保持运行以执行其所需功能的时间。它是系统、服务或机器在正常条件下保持运行的时间百分比的简单度量。

九人的可用性

可用性通常通过正常运行时间(或停机时间)作为服务可用时间的百分比来量化。它通常以9的数量来衡量。

$$ 可用性 = \frac{正常运行时间}{(正常运行时间 + 停机时间)} $$

如果可用性为 99.00%,则表示具有“2 个 9”的可用性,如果为 99.9%,则称为“3 个 9”,依此类推。

可用性(百分比) 停机时间(年) 停机时间(月) 停机时间(周)
90% (一个九) 36.53天 72 小时 16.8 小时
99%(两个九) 3.65 天 7.20 小时 1.68 小时
99.9%(三个九) 8.77 小时 43.8 分钟 10.1 分钟
99.99%(四个九) 52.6 分钟 4.32 分钟 1.01 分钟
99.999%(五个九) 5.25 分钟 25.9 秒 6.05 秒
99.9999% (六个九) 31.56 秒 2.59 秒 604.8 毫秒
99.99999% (七个九) 3.15 秒 263 毫秒 60.5 毫秒
99.999999% (八个九) 315.6 毫秒 26.3 毫秒 6 毫秒
99.9999999% (九个九) 31.6 毫秒 2.6 毫秒 0.6 毫秒

顺序可用性与并行可用性

如果服务由多个容易发生故障的组件组成,则服务的整体可用性取决于这些组件是按顺序排列还是并行。

序列

当两个组件按顺序排列时,总体可用性会降低。

$$ 可用性 \空间(总计) = 可用性 \空间 (Foo) * 可用性 \空间(条) $$

例如,如果两者都有 99.9% 的可用性,则其顺序的总可用性将为 99.8%。

Foo
Bar

平行

当两个组件并行时,总体可用性会提高。

$$ 可用性 \空间(总计)= 1 - (1 - 可用性 \空间 (Foo)) * (1 - 可用性 \空间(条)) $$

例如,如果 两者都有 99.9% 的可用性,则它们的并行总可用性将为 99.9999%。

Foo
Bar

可用性与可靠性

如果系统可靠,则可用。但是,如果可用,则不一定可靠。换句话说,高可靠性有助于实现高可用性,但即使使用不可靠的系统,也可以实现高可用性。

高可用性与容错

高可用性和容错都适用于提供高正常运行时间级别的方法。但是,它们以不同的方式实现目标。

容错系统没有服务中断,但成本明显较高,而高可用性系统的服务中断最小。容错需要完全的硬件冗余,就好像主系统发生故障一样,在不损失正常运行时间的情况下,另一个系统应该接管。

可伸缩性

可伸缩性是衡量系统通过添加或删除资源来满足需求来响应更改的程度。

可伸缩性

让我们讨论不同类型的缩放:

垂直缩放

垂直扩展(也称为纵向扩展)通过为现有计算机添加更多功能来扩展系统的可伸缩性。换句话说,垂直扩展是指通过增加硬件容量来提高应用程序的能力。

优势

  • 易于实施
  • 更易于管理
  • 数据一致

  • 高停机时间风险
  • 更难升级
  • 可能是单点故障

水平缩放

水平缩放(也称为横向扩展)通过添加更多计算机来扩展系统的规模。它通过向现有服务器池添加更多实例来提高服务器的性能,从而允许负载更均匀地分布。

优势

  • 增加冗余
  • 更好的容错能力
  • 灵活高效
  • 更易于升级

  • 复杂性增加
  • 数据不一致
  • 下游服务负载增加

存储

存储是一种使系统能够临时或永久保留数据的机制。在系统设计的上下文中,本主题通常会跳过,但是,对一些常见的存储技术类型有一个基本的了解非常重要,这些技术可以帮助我们微调存储组件。让我们讨论一些重要的存储概念:

袭击

RAID(独立磁盘冗余阵列)是一种在多个硬盘或固态驱动器(SSD)上存储相同数据的方法,以便在驱动器发生故障时保护数据。

但是,有不同的 RAID 级别,并且并非所有级别都有提供冗余的目标。让我们讨论一些常用的 RAID 级别:

  • RAID 0:也称为条带化,数据在阵列中的所有驱动器上平均分配。
  • RAID 1:也称为镜像,至少两个驱动器包含一组数据的精确副本。如果驱动器出现故障,其他驱动器仍将正常工作。
  • RAID 5:使用奇偶校验进行条带化。需要使用至少 3 个驱动器,在多个驱动器(如 RAID 0)之间条带化数据,但也具有分布在驱动器上的奇偶校验。
  • RAID 6:使用双奇偶校验进行条带化。RAID 6 与 RAID 5 类似,但奇偶校验数据写入两个驱动器。
  • RAID 10:结合条带化和来自 RAID 0 和 RAID 1 的镜像。它通过镜像辅助驱动器上的所有数据,同时在每组驱动器上使用条带化来加快数据传输速度,从而提供安全性。

比较

让我们比较不同 RAID 级别的所有功能:

特征 磁盘阵列 0 突袭 1 突袭 5 突袭 6 磁盘阵列 10
描述 条纹 镜像 使用奇偶校验进行条带化 使用双奇偶校验进行条带化 条带化和镜像
最少磁盘数 2 2 3 4 4
读取性能
写入性能 中等 中等
成本
容错 没有 单驱动器故障 单驱动器故障 双驱动器故障 每个子阵列中最多有一个磁盘故障
产能利用率 100% 50% 67%-94% 50%-80% 50%

卷是磁盘或磁带上的固定存储量。术语“卷”通常用作存储本身的同义词,但单个磁盘可能包含多个卷,或者一个卷可能跨越多个磁盘。

文件存储

文件存储是一种将数据存储为文件并将其作为分层目录结构呈现给最终用户的解决方案。主要优点是提供用户友好的解决方案来存储和检索文件。要在文件存储中查找文件,需要该文件的完整路径。它经济且易于构建,通常位于硬盘驱动器上,这意味着它们对用户和硬盘驱动器的外观完全相同。

示例:亚马逊 EFSAzure 文件谷歌云文件存储等。

块存储

块存储将数据划分为块(块),并将它们存储为单独的部分。每个数据块都有一个唯一的标识符,该标识符允许存储系统将较小的数据块放置在最方便的位置。

块存储还可以将数据与用户环境分离,从而允许该数据分布在多个环境中。这将创建数据的多个路径,并允许用户快速检索数据。当用户或应用程序从块存储系统请求数据时,底层存储系统会重新组合数据块并将数据呈现给用户或应用程序

示例:亚马逊 EBS

对象存储

对象存储(也称为基于对象的存储)将数据文件分解为称为对象的部分。然后,它将这些对象存储在单个存储库中,该存储库可以分布在多个网络系统中。

示例:亚马逊 S3Azure Blob 存储谷歌云存储等。

网络存储

NAS(网络附加存储)是连接到网络的存储设备,允许授权网络用户从中心位置存储和检索数据。NAS设备非常灵活,这意味着当我们需要额外的存储时,我们可以增加我们所拥有的。它更快,更便宜,并提供现场公共云的所有好处,使我们能够完全控制。

高密度纤维板

Hadoop 分布式文件系统 (HDFS) 是一种分布式文件系统,旨在在商用硬件上运行。HDFS具有高度的容错能力,旨在部署在低成本硬件上。HDFS提供对应用程序数据的高吞吐量访问,适用于具有大型数据集的应用程序。它与现有的分布式文件系统有许多相似之处。

HDFS旨在可靠地跨大型集群中的机器存储非常大的文件。它将每个文件存储为一系列块,除最后一个块外,文件中的所有块的大小相同。复制文件的块以实现容错。

数据库和数据库管理系统

什么是数据库?

数据库是结构化信息或数据的有组织集合,通常以电子方式存储在计算机系统中。数据库通常由数据库管理系统 (DBMS) 控制。数据和 DBMS 以及与其关联的应用程序一起被称为数据库系统,通常简称为数据库系统。

什么是数据库管理系统?

数据库通常需要一个称为数据库管理系统 (DBMS) 的综合数据库软件程序。DBMS 充当数据库与其最终用户或程序之间的接口,允许用户检索、更新和管理信息的组织和优化方式。DBMS 还有助于监督和控制数据库,从而实现各种管理操作,如性能监视、调整以及备份和恢复。

组件

以下是在不同数据库中找到的一些常见组件:

图式

架构的作用是定义数据结构的形状,并指定哪些类型的数据可以放在哪里。架构可以在整个数据库中严格实施,也可以在数据库的一部分上松散地实施,或者它们可能根本不存在。

桌子

每个表都包含各种列,就像在电子表格中一样。一个表可以有两列和一百列或更多列,具体取决于表中的信息类型。

列包含一组特定类型的数据值,数据库的每一行对应一个值。列可以包含文本值、数字、枚举、时间戳等。

表中的数据记录在行中。表中可能有数千或数百万行包含任何特定信息。

类型

数据库类型

以下是不同类型的数据库:

SQL 和 NoSQL 数据库是广泛的主题,将在 SQL 数据库NoSQL 数据库中单独讨论。了解它们在 SQL 数据库与 NoSQL 数据库中的相互比较。

挑战

大规模运行数据库时面临的一些常见挑战:

  • 吸收数据量的显著增长:来自传感器、互联机器和数十个其他来源的数据的爆炸式增长。
  • 确保数据安全:如今,数据泄露无处不在,确保数据安全且用户易于访问比以往任何时候都更加重要。
  • 跟上需求:公司需要实时访问其数据,以支持及时的决策并利用新的机会。
  • 管理和维护数据库和基础架构:随着数据库变得越来越复杂,数据量不断增长,公司面临着雇用更多人才来管理其数据库的费用。
  • 消除对可扩展性的限制:如果企业要生存下去,它需要增长,并且其数据管理必须随之增长。但是很难预测公司需要多少容量,特别是对于本地数据库。
  • 确保数据驻留、数据主权或延迟要求:某些组织具有更适合在本地运行的用例。在这些情况下,为运行数据库而预先配置和预优化的工程系统是理想的选择。

SQL 数据库

SQL(或关系)数据库是数据项的集合,它们之间具有预定义的关系。这些项目被组织为一组具有列和行的表。表用于保存有关要在数据库中表示的对象的信息。表中的每一列都包含某种类型的数据,字段存储属性的实际值。表中的行表示一个对象或实体的相关值的集合。

表中的每一行都可以使用称为主键的唯一标识符进行标记,并且可以使用外键使多个表之间的行相关。可以通过许多不同的方式访问此数据,而无需重新组织数据库表本身。SQL 数据库通常遵循 ACID 一致性模型

实例化视图

实例化视图是从查询规范派生并存储以供以后使用的预先计算的数据集。由于数据是预先计算的,因此查询实例化视图比对视图的基表执行查询更快。当查询频繁运行或足够复杂时,这种性能差异可能很大。

它还支持数据子集化,并提高在大型数据集上运行的复杂查询的性能,从而减少网络负载。实例化视图还有其他用途,但它们主要用于性能和复制。

N+1 查询问题

当数据访问层执行 N 个附加 SQL 语句以获取执行主 SQL 查询时可能已检索到的相同数据时,会发生 N+1 查询问题。N 的值越大,执行的查询越多,对性能的影响就越大。

这在 GraphQL 和 ORM(对象关系映射)工具中很常见,可以通过优化 SQL 查询或使用数据加载器来解决,该加载器对连续请求进行批处理,并在后台发出单个数据请求。

优势

让我们看一下使用关系数据库的一些优点:

  • 简单准确
  • 可及性
  • 数据一致性
  • 灵活性

以下是关系数据库的缺点:

  • 维护成本高昂
  • 困难的模式演变
  • 性能命中(加入、非规范化等)
  • 由于水平可扩展性差,难以扩展

例子

以下是一些常用的关系数据库:

数据库

NoSQL是一个广泛的类别,包括任何不使用SQL作为其主要数据访问语言的数据库。这些类型的数据库有时也称为非关系数据库。与关系数据库不同,NoSQL数据库中的数据不必符合预定义的架构。无 SQL 数据库遵循基本一致性模型

以下是不同类型的 NoSQL 数据库:

公文

文档数据库(也称为面向文档的数据库或文档存储)是在文档中存储信息的数据库。它们是通用数据库,为事务和分析应用程序提供各种用例。

优势

  • 直观灵活
  • 轻松水平缩放
  • 无架构

  • 无架构
  • 非关系型

例子

键值

键值数据库是最简单的 NoSQL 数据库类型之一,它将数据保存为一组键值对,每个键值对由两个数据项组成。它们有时也称为键值存储。

优势

  • 简单且高性能
  • 高度可扩展,适用于高流量
  • 会话管理
  • 优化的查找

  • 基本型克鲁德
  • 无法筛选值
  • 缺乏索引和扫描功能
  • 未针对复杂查询进行优化

例子

图形数据库是一种 NoSQL 数据库,它使用图形结构进行语义查询,其中包含节点、边缘和属性来表示和存储数据,而不是表或文档。

该图将存储中的数据项与节点和边缘的集合相关联,这些边缘表示节点之间的关系。这些关系允许将存储中的数据直接链接在一起,并且在许多情况下,只需一次操作即可检索。

优势

  • 查询速度
  • 敏捷灵活
  • 显式数据表示

  • 复杂
  • 无标准化查询语言

使用案例

  • 欺诈检测
  • 推荐引擎
  • 社交网络
  • 网络映射

例子

时间序列

时序数据库是针对带时间戳或时序的数据进行优化的数据库。

优势

  • 快速插入和检索
  • 高效的数据存储

使用案例

  • 物联网数据
  • 指标分析
  • 应用监控
  • 了解金融趋势

例子

宽列

宽列数据库(也称为宽列存储)与架构无关。数据存储在列系列中,而不是行和列中。

优势

  • 高度可扩展,可处理 PB 级数据
  • 实时大数据应用的理想选择

  • 增加写入时间

使用案例

  • 业务分析
  • 基于属性的数据存储

例子

多模型

多模型数据库将不同的数据库模型(即关系、图形、键值、文档等)组合到单个集成后端中。这意味着它们可以容纳各种数据类型、索引、查询,并将数据存储在多个模型中。

优势

  • 灵活性
  • 适用于复杂项目
  • 数据一致

  • 复杂
  • 不太成熟

例子

SQL 与 NoSQL 数据库

在数据库世界中,有两种主要类型的解决方案,SQL(关系)和NoSQL(非关系)数据库。它们的不同之处在于它们的构建方式,存储的信息类型以及存储方式。关系数据库是结构化的,具有预定义的架构,而非关系数据库是非结构化的、分布式的,并且具有动态架构。

高级差异

以下是 SQL 和 NoSQL 之间的一些高级差异:

存储

SQL 将数据存储在表中,其中每行表示一个实体,每列表示有关该实体的一个数据点。

NoSQL数据库具有不同的数据存储模型,例如键值,图形,文档等。

图式

在 SQL 中,每条记录都符合固定的架构,这意味着必须在数据输入之前确定和选择列,并且每行必须具有每列的数据。架构可以在以后更改,但它涉及使用迁移来修改数据库。

而在 NoSQL 中,架构是动态的。可以动态添加字段,并且每个记录(或等效记录)不必包含每个字段的数据

查询

SQL数据库使用SQL(结构化查询语言)来定义和操作数据,这是非常强大的。

在 NoSQL 数据库中,查询侧重于文档集合。不同的数据库具有不同的查询语法。

可伸缩性

在大多数常见情况下,SQL 数据库是垂直可伸缩的,这可能会变得非常昂贵。可以跨多个服务器扩展关系数据库,但这是一个具有挑战性且耗时的过程。

另一方面,NoSQL数据库是水平可扩展的,这意味着我们可以轻松地向NoSQL数据库基础架构添加更多服务器来处理大量流量。任何廉价的商用硬件或云实例都可以托管NoSQL数据库,因此比垂直扩展更具成本效益。许多NoSQL技术也会自动在服务器之间分发数据。

可靠性

绝大多数关系数据库都符合 ACID 标准。因此,当涉及到数据可靠性和执行事务的安全保证时,SQL数据库仍然是更好的选择。

大多数 NoSQL 解决方案在性能和可伸缩性方面牺牲了 ACID 合规性。

原因

与往常一样,我们应该始终选择更适合要求的技术。那么,让我们来看看选择基于SQL或NoSQL的数据库的一些原因:

对于 SQL

  • 具有严格架构的结构化数据
  • 关系数据
  • 需要复杂的连接
  • 交易
  • 按索引查找非常快

对于诺索

  • 动态或灵活的架构
  • 非关系数据
  • 无需复杂的连接
  • 非常数据密集型工作负载
  • 针对 IOPS 的极高吞吐量

数据库复制

复制是一个过程,涉及共享信息以确保冗余资源(如多个数据库)之间的一致性,以提高可靠性、容错能力或可访问性。

主从复制

主站提供读取和写入,将写入复制到一个或多个从站,这些从站仅提供读取。从属还可以以树状方式复制其他从站。如果主站脱机,系统可以继续以只读模式运行,直到从站提升为主站或配置新的主站。

主从复制

优势

  • 整个数据库的备份对主数据库的备份相对没有影响。
  • 应用程序可以从从站读取数据,而不会影响主站。
  • 从站可以离线并同步回主站,而不会造成任何停机。

  • 复制增加了更多的硬件和额外的复杂性。
  • 主服务器发生故障时,停机时间和可能的数据丢失。
  • 所有写入也必须在主从架构中对主服务器进行。
  • 读取从站越多,我们必须复制的就越多,这将增加复制滞后。

主-主复制

两个主节点都提供读/写功能并相互协调。如果任一主站出现故障,系统可以继续运行读取和写入操作。

主-主-复制

优势

  • 应用程序可以从两个主节点读取。
  • 在两个主节点之间分配写入负载。
  • 简单、自动和快速的故障转移。

  • 配置和部署不像主从那么简单。
  • 要么松散一致,要么由于同步而增加了写入延迟。
  • 随着添加更多写入节点和延迟增加,冲突解决将发挥作用。

同步与异步复制

同步复制和异步复制之间的主要区别在于如何将数据写入副本。在同步复制中,数据同时写入主存储和副本。因此,主副本和副本应始终保持同步。

相反,异步复制在数据已写入主存储后将数据复制到副本。尽管复制过程可能近乎实时地发生,但按计划进行复制更为常见,而且成本效益更高。

指标

索引在数据库方面是众所周知的,它们用于提高数据存储上数据检索操作的速度。索引需要权衡增加的存储开销和较慢的写入速度(因为我们不仅要写入数据,还必须更新索引),以便更快地读取。索引用于快速查找数据,而无需检查数据库表中的每一行。可以使用数据库表的一列或多列创建索引,从而为快速随机查找和高效访问有序记录提供基础。

指标

索引是一种数据结构,可以将其视为指向实际数据所在的位置的目录。因此,当我们在表的列上创建索引时,我们会将该列和指向整行的指针存储在索引中。索引还用于创建相同数据的不同视图。对于大型数据集,这是指定不同筛选器或排序方案的绝佳方法,而无需创建数据的多个附加副本。

数据库索引可以具有的一个品质是它们可以是密集的,也可以是稀疏的。这些指数品质中的每一个都有自己的权衡。让我们看一下每种索引类型的工作原理:

密集指数

在密集索引中,将为表的每一行创建一条索引记录。可以直接找到记录,因为索引的每条记录都包含搜索键值和指向实际记录的指针。

密集指数

在写入时,密集索引比稀疏索引需要更多的维护。由于每行都必须有一个条目,因此数据库必须维护插入、更新和删除的索引。每行都有一个条目也意味着密集索引将需要更多的内存。密集索引的好处是,只需二进制搜索即可快速找到值。密集索引也不会对数据施加任何排序要求。

稀疏指数

在稀疏索引中,仅为某些记录创建记录。

稀疏索引

与写入时的密集索引相比,稀疏索引需要的维护更少,因为它们仅包含值的子集。这种较轻的维护负担意味着插入、更新和删除的速度会更快。条目越少也意味着索引将使用较少的内存。查找数据的速度较慢,因为整个页面的扫描通常遵循二进制搜索。在处理有序数据时,稀疏索引也是可选的。

规范化和非规范化

条款

在继续之前,让我们看一下规范化和非规范化中的一些常用术语。

钥匙

主键:可用于唯一标识表的每一行的列或列组。

复合键:由多个列组成的主键。

超级键:可以唯一标识表中所有行的所有键的集合。

候选键:在表中唯一标识行的属性。

外键:它是对另一个表的主键的引用。

备用键:不是主键的键称为备用键。

代理键:系统生成的值,当没有其他列能够保存主键的属性时,该值唯一标识表中的每个条目。

依赖

部分依赖关系:当主键确定某些其他属性时发生。

功能依赖关系:它是存在于两个属性之间的关系,通常存在于表中的主键和非键属性之间。

传递功能依赖关系:当某些非键属性确定某些其他属性时发生。

异常

当由于不正确的规划或将所有内容存储在平面数据库中而导致数据库中存在缺陷时,就会发生数据库异常。这通常通过规范化过程来解决。

有三种类型的数据库异常:

插入异常:当我们无法在数据库中插入某些属性而不存在其他属性时,就会发生这种情况。

更新异常:在数据冗余和部分更新的情况下发生。换句话说,正确更新数据库需要其他操作,如添加、删除或两者。

删除异常:当删除某些数据需要删除其他数据时,会发生这种情况。

让我们考虑下表,该表未规范化:

编号 名字 角色 团队
1 彼得 软件工程师 一个
2 布莱恩 开发运营工程师 B
3 海利 产品经理 C
4 海利 产品经理 C
5 史蒂夫 前端工程师 D

让我们想象一下,我们雇佣了一个新人“约翰”,但他们可能不会立即被分配到一个团队。这将导致插入异常,因为 team 属性尚不存在。

接下来,假设来自团队C的Hailey被提升了,为了反映数据库中的变化,我们需要更新2行以保持一致性,这可能会导致更新异常

最后,我们想删除团队B,但要做到这一点,我们还需要删除其他信息,例如名称和角色,这是删除异常的一个例子。

正常化

规范化是在数据库中组织数据的过程。这包括创建表并根据旨在保护数据的规则在这些表之间建立关系,并通过消除冗余和不一致的依赖关系使数据库更加灵活。

为什么我们需要规范化?

规范化的目标是消除冗余数据并确保数据一致。完全规范化的数据库允许扩展其结构以适应新的数据类型,而无需过多地更改现有结构。因此,与数据库交互的应用程序受到的影响最小。

普通形式

正常形式是确保数据库规范化的一系列准则。让我们讨论一些基本的正态形式:

1寸

对于要采用第一范式 (1NF) 的表,它应遵循以下规则:

  • 不允许重复组。
  • 使用主键标识每组相关数据。
  • 相关数据集应具有单独的表。
  • 不允许在同一列中混合数据类型。

2寸

对于要采用第二范式 (2NF) 的表,它应遵循以下规则:

  • 满足第一个正态形式 (1NF)。
  • 不应有任何部分依赖项。

3寸

对于要采用第三范式 (3NF) 的表,它应遵循以下规则:

  • 满足第二范式 (2NF)。
  • 不允许传递功能依赖项。

断续器

博伊斯-科德范式(或BCNF)是第三范式(3NF)的一个稍微强一些的版本,用于解决最初定义的3NF未处理的某些类型的异常。有时它也被称为3.5范式(3.5NF)。

对于博伊斯-科德范式 (BCNF) 的表,它应遵循以下规则:

  • 满足第三范式 (3NF)。
  • 对于每个功能依赖关系 X → Y,X 都应该是超级键。

还有更多正常形式,例如4NF,5NF和6NF,但我们不会在这里讨论它们。看看这个精彩的视频,详细介绍了。

在关系数据库中,如果关系符合第三范式,则通常将其描述为“规范化”。大多数 3NF 关系没有插入、更新和删除异常。

与许多正式规则和规范一样,实际场景并不总是允许完美的合规性。如果决定违反规范化的前三个规则之一,请确保应用程序预见到可能发生的任何问题,如冗余数据和不一致的依赖项。

优势

以下是规范化的一些优点:

  • 减少数据冗余。
  • 更好的数据设计。
  • 提高数据一致性。
  • 强制实施参照完整性。

让我们看一下规范化的一些缺点:

  • 数据设计很复杂。
  • 性能较慢。
  • 维护开销。
  • 需要更多联接。

非规范化

非规范化是一种数据库优化技术,在这种技术中,我们将冗余数据添加到一个或多个表中。这可以帮助我们避免在关系数据库中进行代价高昂的联接。它想以牺牲一些写入性能为代价来提高读取性能。数据的冗余副本写入多个表中,以避免代价高昂的联接。

一旦数据使用联合和分片等技术进行分发,跨网络管理联接会进一步增加复杂性。非规范化可能会规避对这种复杂联接的需求。

注意:非规范化并不意味着反转规范化。

优势

让我们来看看非规范化的一些优点:

  • 检索数据的速度更快。
  • 编写查询更容易。
  • 减少表的数量。
  • 管理方便。

以下是非规范化的一些缺点:

  • 昂贵的插入和更新。
  • 增加了数据库设计的复杂性。
  • 增加数据冗余。
  • 数据不一致的可能性更大。

酸和碱一致性模型

让我们讨论一下 ACID 和基本一致性模型。

术语 ACID 代表原子性、一致性、隔离性和持久性。ACID 属性用于在事务处理期间维护数据完整性。

为了保持事务前后的一致性,关系数据库遵循 ACID 属性。让我们理解这些术语:

原子

事务中的所有操作都成功或每个操作都回滚。

一致

在事务完成后,数据库在结构上是合理的。

孤立

交易不会相互竞争。对数据的有争议的访问由数据库调节,以便事务看起来是按顺序运行的。

耐用

事务完成并且写入和更新已写入磁盘后,即使发生系统故障,磁盘也将保留在系统中。

基础

随着数据量和高可用性要求的增加,数据库设计方法也发生了巨大变化。为了提高扩展能力并同时保持高可用性,我们将逻辑从数据库移动到单独的服务器。通过这种方式,数据库变得更加独立,并专注于存储数据的实际过程。

在NoSQL数据库世界中,ACID事务不太常见,因为一些数据库已经放宽了对即时一致性,数据新鲜度和准确性的要求,以获得其他好处,如规模和弹性。

BASE 属性比 ACID 保证宽松得多,但两个一致性模型之间没有直接的一对一映射。让我们理解这些术语:

基本可用性

数据库似乎在大多数时间都正常工作。

软状态

存储不必是写一致的,不同的副本也不必一直相互一致。

最终一致性

数据可能不会立即保持一致,但最终会变得一致。即使由于不一致,系统中的读取仍然是可能的,因为它们可能无法给出正确的响应。

酸与碱的权衡

对于我们的应用程序是否需要 ACID 或 BASE 一致性模型,没有正确的答案。两种型号的设计都以满足不同的要求。在选择数据库时,我们需要牢记模型的属性和应用程序的要求。

鉴于 BASE 的松散一致性,如果开发人员为其应用程序选择 BASE 存储,他们需要对一致性数据更加了解和严格。熟悉所选数据库的 BASE 行为并在这些约束内工作至关重要。

另一方面,与 ACID 事务的简单性相比,围绕 BASE 限制进行规划有时可能是一个主要缺点。完全 ACID 数据库非常适合数据可靠性和一致性至关重要的用例。

电容定理

CAP 定理指出,分布式系统只能提供三个所需特征中的两个 一致性、可用性和分区容差 (CAP)。

帽定理

让我们详细看一下CAP定理所指的三个分布式系统特征。

一致性

一致性意味着所有客户端都同时看到相同的数据,无论它们连接到哪个节点。为此,每当将数据写入一个节点时,必须立即在系统中的所有节点之间转发或复制数据,然后才能将写入视为“成功”。

可用性

可用性意味着发出数据请求的任何客户端都会得到响应,即使一个或多个节点已关闭也是如此。

分区容差

分区容错意味着系统在消息丢失或部分故障后仍继续工作。分区容错的系统可以承受任何数量的网络故障,但不会导致整个网络故障。数据在节点和网络的组合之间充分复制,以便在间歇性中断期间保持系统正常运行。

一致性-可用性权衡

我们生活在一个物理世界中,不能保证网络的稳定性,因此分布式数据库必须选择分区容错(P)。这意味着在一致性 (C) 和可用性 (A) 之间进行权衡。

加州数据库

CA 数据库跨所有节点提供一致性和可用性。如果系统中任何两个节点之间存在分区,则无法执行此操作,因此无法提供容错能力。

示例后格雷SQL玛丽亚数据库

正压数据库

CP 数据库以牺牲可用性为代价来提供一致性和分区容错。当在任何两个节点之间发生分区时,系统必须关闭不一致的节点,直到分区被解析。

示例蒙哥数据库 Apache HBase

接入点数据库

AP 数据库以牺牲一致性为代价来提供可用性和分区容错。发生分区时,所有节点都保持可用,但位于分区错误端的节点可能会返回比其他节点更旧版本的数据。解析分区后,AP 数据库通常会重新同步节点以修复系统中的所有不一致。

示例 Apache ·卡桑德拉库奇DB

帕切尔定理

太平洋共同体定理是CAP定理的延伸。CAP定理指出,在分布式系统中进行网络分区(P)的情况下,必须在可用性(A)和一致性(C)之间进行选择。

PACELC 通过将延迟 (L) 作为分布式系统的附加属性来扩展 CAP 定理。该定理指出,否则(E),即使系统在没有分区的情况下正常运行,也必须在延迟(L)和一致性(C)之间进行选择。

PACELC定理首先由丹尼尔·阿巴迪描述。

佩斯尔克定理

PACELC定理的开发是为了解决CAP定理的一个关键限制,因为它没有规定性能或延迟。

例如,根据 CAP 定理,如果查询在 30 天后返回响应,则可以将数据库视为可用数据库。显然,对于任何实际应用程序来说,这种延迟都是不可接受的。

交易

事务是一系列被视为“单个工作单元”的数据库操作。事务中的操作要么全部成功,要么全部失败。通过这种方式,当系统的一部分发生故障时,事务的概念支持数据完整性。并非所有数据库都选择支持 ACID 事务,通常是因为它们优先考虑其他难以或理论上不可能一起实现的优化。

通常,关系数据库支持 ACID 事务,而非关系数据库不支持(也有例外)。

国家

数据库中的事务可以处于以下状态之一:

事务状态

积极

在此状态下,正在执行事务。这是每个事务的初始状态。

部分提交

当事务执行其最终操作时,它被称为处于部分提交状态。

承诺

如果事务成功执行其所有操作,则称其已提交。它的所有影响现在都永久地建立在数据库系统上。

失败

如果数据库恢复系统所做的任何检查失败,则称事务处于失败状态。失败的事务不能再继续。

中止

如果任何检查失败并且事务已达到失败状态,则恢复管理器将回滚其对数据库的所有写入操作,以使数据库恢复到执行事务之前的原始状态。处于此状态的事务将中止。

数据库恢复模块可以在事务中止后选择以下两个操作之一:

  • 重新启动事务
  • 终止交易

终止

如果没有任何回滚或事务来自已提交状态,则系统是一致的,并且已准备好进行新事务,并且旧事务将终止。

分布式事务

分布式事务是跨两个或多个数据库对数据执行的一组操作。它通常在由网络连接的单独节点之间进行协调,但也可能跨越单个服务器上的多个数据库。

为什么我们需要分布式事务?

与单个数据库上的 ACID 事务不同,分布式事务涉及更改多个数据库上的数据。因此,分布式事务处理更加复杂,因为数据库必须将事务中的更改作为自包含单元进行协调提交或回滚。

换句话说,所有节点都必须提交,或者所有节点都必须中止并且整个事务回滚。这就是为什么我们需要分布式事务。

现在,让我们看一些流行的分布式事务解决方案:

两阶段提交

两阶段提交

两阶段提交 (2PC) 协议是一种分布式算法,它协调参与分布式事务的所有进程,以确定是提交还是中止(回滚)事务。

即使在许多临时系统故障的情况下,该协议也能实现其目标,因此被广泛使用。但是,它不能对所有可能的故障配置进行弹性,在极少数情况下,需要手动干预来补救结果。

该协议需要一个协调器节点,该节点基本上协调和监督不同节点之间的事务。协调员想分两个阶段在一组进程之间建立共识,因此得名。

阶段

两阶段提交包括以下阶段:

准备阶段

准备阶段涉及协调器节点从每个参与者节点收集共识。除非每个节点响应它们已准备好,否则事务将被中止。

提交阶段

如果所有参与者都响应他们准备好的协调器,则协调器会要求所有节点提交事务。如果发生故障,事务将被回滚。

问题

两阶段提交协议中可能会出现以下问题:

  • 如果其中一个节点崩溃怎么办?
  • 如果协调器本身崩溃怎么办?
  • 它是一个阻止协议。

三阶段提交

三阶段提交

三阶段提交 (3PC) 是两阶段提交的扩展,其中提交阶段分为两个阶段。这有助于解决两阶段提交协议中出现的阻塞问题。

阶段

三阶段提交包括以下阶段:

准备阶段

此阶段与两阶段提交相同。

预提交阶段

协调器发出提交前消息,所有参与节点都必须确认该消息。如果参与者未能及时收到此消息,则事务将中止。

提交阶段

此步骤也类似于两阶段提交协议。

为什么预提交阶段有帮助?

预提交阶段完成以下任务:

  • 如果在此阶段找到参与者节点,则意味着每个参与者都已完成第一阶段。保证准备阶段的完成。
  • 现在,每个阶段都可以超时,避免无限期等待。

传说

传说

传奇是一系列本地事务。每个本地事务都会更新数据库并发布消息或事件,以触发 saga 中的下一个本地事务。如果本地事务因违反业务规则而失败,则 saga 将执行一系列补偿事务,以撤消前面的本地事务所做的更改。

协调

有两种常见的实现方法:

  • 编排:每个本地事务发布触发其他服务中的本地事务的域事件。
  • 业务流程:业务流程协调程序告诉参与者要执行哪些本地事务。

问题

  • 佐贺模式特别难以调试。
  • 传奇参与者之间存在循环依赖的风险。
  • 缺乏参与者数据隔离会带来持久性挑战。
  • 测试很困难,因为必须运行所有服务才能模拟事务。

分片

在讨论分片之前,我们先来谈谈数据分区:

数据分区

数据分区是一种将数据库分解为许多较小部分的技术。它是在多台计算机之间拆分数据库或表以提高数据库的可管理性、性能和可用性的过程。

方法

有许多不同的方法可以用来决定如何将应用程序数据库分解为多个较小的数据库,以下是各种大型应用程序使用的三种最常用的方法:

水平分区(或分片)

在此策略中,我们根据分区键定义的值范围水平拆分表数据。它也被称为数据库分片

垂直分区

在垂直分区中,我们根据列对数据进行垂直分区。我们将表划分为相对较小的表,其中包含很少的元素,并且每个部分都存在于单独的分区中。

在本教程中,我们将特别关注分片。

什么是分片?

分片是一种与水平分区相关的数据库体系结构模式,水平分区是将一个表的行分成多个不同表(称为分区分片)的做法。每个分区具有相同的架构和列,但也有共享数据的子集。同样,每个分区中保存的数据都是唯一的,并且独立于其他分区中保存的数据。

分片

数据分片的理由是,在某一点之后,通过添加更多计算机进行水平扩展比通过添加功能强大的服务器进行垂直扩展更便宜,更可行。分片可以在应用程序或数据库级别实现。

分区标准

有大量条件可用于数据分区。一些最常用的标准是:

基于哈希

此策略根据哈希算法将行划分为不同的分区,而不是基于连续索引对数据库行进行分组。

此方法的缺点是动态添加/删除数据库服务器的成本很高。

基于列表

在基于列表的分区中,每个分区都是根据列上的值列表而不是一组连续的值范围来定义和选择的。

基于范围

范围分区根据分区键的值范围将数据映射到各种分区。换句话说,我们对表进行分区,使每个分区都包含由分区键定义的给定范围内的行。

范围应为连续的,但不能重叠,其中每个范围指定分区的非包含性下限和上限。任何等于或大于范围上限的分区键值都将添加到下一个分区。

复合

顾名思义,复合分区基于两种或多种分区技术对数据进行分区。在这里,我们首先使用一种技术对数据进行分区,然后使用相同的方法将每个分区进一步细分为子分区。

优势

但是为什么我们需要分片呢?以下是一些优点:

  • 可用性:为分区数据库提供逻辑独立性,确保应用程序的高可用性。在这里,可以独立管理各个分区。
  • 可伸缩性:事实证明,通过跨多个分区分布数据来提高可伸缩性。
  • 安全性:通过将敏感和非敏感数据存储在不同的分区中,帮助提高系统的安全性。这可以为敏感数据提供更好的可管理性和安全性。
  • 查询性能:提高系统性能。现在,系统不必查询整个数据库,而只需查询较小的分区。
  • 数据可管理性:将表和索引划分为更小、更易于管理的单元。

  • 复杂性:分片通常会增加系统的复杂性。
  • 跨分片的联接:一旦数据库被分区并分布在多台机器上,通常就无法执行跨多个数据库分片的联接。此类联接不会提高性能,因为必须从多个服务器检索数据。
  • 重新平衡:如果数据分布不均匀或单个分片上存在大量负载,在这种情况下,我们必须重新平衡分片,以便请求尽可能均匀地分布在分片之间。

何时使用分片?

以下是分片可能是正确选择的一些原因:

  • 利用现有硬件而不是高端计算机。
  • 在不同地理区域维护数据。
  • 通过添加更多分片快速扩展。
  • 性能更好,因为每台机器的负载都更轻。
  • 当需要更多并发连接时。

一致的哈希

让我们首先了解我们想解决的问题。

我们为什么需要这个?

在传统的基于哈希的分发方法中,我们使用哈希函数来散列我们的分区键(即请求ID或IP)。然后,如果我们对节点(服务器或数据库)的总数使用模。这将为我们提供要路由请求的节点。

简单散列

$$ \begin{对齐*} & 哈希(key_1) \到H_1 \bmod N = Node_0 \ && 哈希(key_2) \到H_2 \bmod N = Node_1 \ && key_3) \到H_3 \bmod N = Node_2 \ && key_n) \到H_n \bmod N = Node_{n-1} \end{对齐*} $$

哪里

key
:请求标识或 IP。

H
:哈希函数结果。

N
:节点总数。

Node
:将路由请求的节点。

这样做的问题是,如果我们添加或删除一个节点,它将导致更改,这意味着我们的映射策略将中断,因为相同的请求现在将映射到不同的服务器。因此,大多数请求将需要重新分发,这是非常低效的。

N

我们希望在不同节点之间均匀地分配请求,以便我们应该能够以最少的工作量添加或删除节点。因此,我们需要一个不直接依赖于节点(或服务器)数量的分发方案,以便在添加或删除节点时,需要重新定位的密钥数量最小化。

一致的哈希通过确保每次我们扩展或缩减时,我们不必重新排列所有键或触摸所有服务器来解决这种水平可伸缩性问题。

现在我们已经了解了问题,让我们详细讨论一致的哈希。

它是如何工作的

一致哈希是一种分布式哈希方案,它通过在抽象圆圈或哈希环上为节点分配一个位置来独立于分布式哈希表中的节点数运行。这允许服务器和对象在不影响整个系统的情况下进行扩展。

一致散列

使用一致的哈希,只有数据需要重新分发。

K/N

$$ R = K/N $$

哪里

R
:需要重新分发的数据。

K
:分区键数。

N
:节点数。

哈希函数的输出是一个范围,让我们说我们可以在哈希环上表示。我们对请求进行哈希处理,并根据输出内容将它们分布在环上。同样,我们也对节点进行哈希处理,并将它们分布在同一个环上。

0...m-1

$$ \begin{对齐*} & 哈希(key_1) = P_1 \ & 哈希(key_2) = P_2 \ & 哈希(key_3) = P_3 \ &... \ & 哈希(key_n) = P_{m-1} \end{对齐*} $$

哪里

key
:请求/节点 ID 或 IP。

P
:哈希环上的位置。

m
:哈希环的总范围。

现在,当请求传入时,我们可以简单地以顺时针(也可以逆时针)的方式将其路由到最近的节点。这意味着,如果添加或删除了新节点,我们可以使用最近的节点,并且只需要重新路由一小部分请求。

从理论上讲,一致的哈希应该均匀地分配负载,但在实践中不会发生。通常,负载分布不均匀,一台服务器可能最终处理大部分请求,成为热点,本质上是系统的瓶颈。我们可以通过添加额外的节点来解决这个问题,但这可能很昂贵。

让我们看看如何解决这些问题。

虚拟节点

为了确保负载分布更均匀,我们可以引入虚拟节点的概念,有时也称为VNode。

哈希范围不是将单个位置分配给节点,而是将其划分为多个较小的范围,并且为每个物理节点分配其中的几个较小范围。这些子范围中的每一个都被视为一个VNode。因此,虚拟节点基本上是在哈希环上多次映射的现有物理节点,以最大程度地减少对节点分配范围的更改。

虚拟节点

为此,我们可以使用一些哈希函数。

k

$$ \begin{对齐*} & Hash_1(key_1) = P_1 \ & Hash_2(key_2) = P_2 \ & Hash_3(key_3) = P_3 \ . . \ & Hash_k(key_n) = P_{m-1} \end{对齐*} $$

哪里

key
:请求/节点 ID 或 IP。

k
:哈希函数的数量。

P
:哈希环上的位置。

m
:哈希环的总范围。

由于 VNodes 通过将哈希范围划分为更小的子范围,帮助在群集上的物理节点上更均匀地分配负载,因此这加快了添加或删除节点后的重新平衡过程。这也有助于我们降低热点的可能性。

数据复制

为确保高可用性和持久性,一致哈希会在系统中值等于复制因子的多个节点上复制每个数据项。

N
N

复制因子是将接收相同数据副本的节点数。在最终一致性系统中,这是异步完成的。

优势

让我们看一下一致哈希的一些优点:

  • 使快速向上和向下扩展更具可预测性。
  • 便于跨节点进行分区和复制。
  • 实现可扩展性和可用性。
  • 减少热点。

以下是一致哈希的一些缺点:

  • 增加复杂性。
  • 级联故障。
  • 负载分布仍然可能不均匀。
  • 当节点暂时发生故障时,密钥管理可能代价高昂。

例子

让我们看一些使用一致哈希的示例:

数据库联合

联合(或功能分区)按功能拆分数据库。联合体系结构使几个不同的物理数据库在最终用户看来是一个逻辑数据库。

联合中的所有组件都由一个或多个联邦架构绑定在一起,这些架构表示整个联合中数据的通用性。这些联合架构用于指定可由联合组件共享的信息,并为它们之间的通信提供通用基础。

数据库联合

联邦还提供了从多个来源派生的数据的一个有凝聚力的统一视图。联合系统的数据源可以包括数据库和各种其他形式的结构化和非结构化数据。

特性

让我们看一下联合数据库的一些关键特征:

  • 透明度:联合数据库掩码用户差异和基础数据源的实现。因此,用户不需要知道数据存储的位置。
  • 异质性:数据源在很多方面可能有所不同。联合数据库系统可以处理不同的硬件、网络协议、数据模型等。
  • 可扩展性:可能需要新的源来满足不断变化的业务需求。一个好的联合数据库系统需要使添加新源变得容易。
  • 自治:联合数据库不会更改现有数据源,接口应保持不变。
  • 数据集成:联合数据库可以集成来自不同协议、数据库管理系统等的数据。

优势

以下是联合数据库的一些优点:

  • 灵活的数据共享。
  • 数据库组件之间的自治。
  • 以统一的方式访问异构数据。
  • 应用程序与旧数据库没有紧密耦合。

以下是联合数据库的一些缺点:

  • 增加更多硬件和额外的复杂性。
  • 联接来自两个数据库的数据非常复杂。
  • 依赖自主数据源。
  • 查询性能和可伸缩性。

N 层体系结构

N 层体系结构将应用程序划分为逻辑层和物理层。层是分离职责和管理依赖项的一种方法。每一层都有一个具体的责任。较高层可以使用较低层中的服务,但不能反过来使用。

n 层体系结构

层在物理上是分开的,在单独的计算机上运行。一个层可以直接调用另一个层,也可以使用异步消息传递。尽管每个层可能都托管在其自己的层中,但这不是必需的。多个层可能托管在同一层上。以物理方式分隔这些层可提高可伸缩性和复原能力,并增加其他网络通信的延迟。

N 层体系结构可以有两种类型:

  • 在封闭层体系结构中,一个层只能立即调用下一层。
  • 在开放层体系结构中,层可以调用其下的任何层。

封闭层体系结构限制了层之间的依赖关系。但是,如果一个层只是将请求传递到下一层,则可能会创建不必要的网络流量。

N 层体系结构的类型

让我们看一下 N 层体系结构的一些示例:

3 层架构

3层被广泛使用,由以下不同的层组成:

  • 表示层:处理用户与应用程序的交互。
  • 业务逻辑层:接受来自应用程序层的数据,根据业务逻辑对其进行验证,并将其传递到数据层。
  • 数据访问层:从业务层接收数据,并对数据库执行必要的操作。

2 层架构

在此体系结构中,表示层在客户端上运行并与数据存储进行通信。客户端和服务器之间没有业务逻辑层或即时层。

单层或 1 层体系结构

这是最简单的一个,因为它相当于在个人计算机上运行应用程序。运行应用程序所需的所有组件都位于单个应用程序或服务器上。

优势

以下是使用 N 层体系结构的一些优点:

  • 可以提高可用性。
  • 更好的安全性,因为层可以像防火墙一样运行。
  • 单独的层允许我们根据需要扩展它们。
  • 改进维护,因为不同的人可以管理不同的层。

以下是 N 层体系结构的一些缺点:

  • 增加了整个系统的复杂性。
  • 随着层数的增加,网络延迟增加。
  • 价格昂贵,因为每一层都有自己的硬件成本。
  • 难以管理网络安全。

消息代理

消息代理是一种软件,它使应用程序、系统和服务能够相互通信并交换信息。消息代理通过在正式消息传递协议之间转换消息来执行此操作。这允许相互依赖的服务直接相互“对话”,即使它们是用不同的语言编写的或在不同的平台上实现的。

消息代理

消息代理可以验证、存储、路由消息并将其传递到适当的目标。它们充当其他应用程序之间的中介,允许发送方在不知道接收方在哪里、接收方是否处于活动状态或有多少接收方的情况下发出消息。这有助于系统内流程和服务的解耦。

模型

消息代理提供两种基本的消息分发模式或消息传递样式:

  • 点对点消息传递:这是消息队列中使用的分发模式,消息的发送方和接收方之间具有一对一的关系。
  • 发布-订阅消息传递:在这种消息分发模式(通常称为“pub/sub”)中,每条消息的创建者将其发布到一个主题,并且多个消息使用者订阅他们想要从中接收消息的主题。

我们将在后面的教程中详细讨论这些消息传递模式。

消息代理与事件流

消息代理可以支持两个或多个消息传递模式,包括消息队列和发布/订阅,而事件流平台仅提供发布/订阅样式的分发模式。事件流平台专为处理大量消息而设计,易于扩展。它们能够将记录流排序到称为主题的类别中,并将它们存储预定的时间。但是,与消息代理不同,事件流平台无法保证消息传递或跟踪哪些使用者已接收消息。

事件流平台提供比消息代理更多的可伸缩性,但确保容错的功能更少,如消息重新发送,以及更有限的消息路由和排队功能。

消息代理与企业服务总线 (ESB)

企业服务总线 (ESB) 基础结构非常复杂,集成起来可能具有挑战性,维护成本高昂。当生产环境中出现问题时,很难对它们进行故障排除,它们不容易扩展,并且更新很繁琐。

而消息代理是ESB的“轻量级”替代方案,它以较低的成本提供类似的功能,即服务间通信的机制。它们非常适合在微服务架构中使用,随着ESB的失宠,微服务架构变得越来越普遍。

例子

以下是一些常用的消息代理:

消息队列

消息队列是服务到服务通信的一种形式,可促进异步通信。它异步接收来自生产者的消息并将其发送给使用者。

队列用于有效管理大规模分布式系统中的请求。在处理负载最小的小型系统和小型数据库中,写入速度可以预测。但是,在更复杂和大型的系统中,写入操作可能需要几乎不确定的时间。

消息队列

加工

消息存储在队列中,直到它们被处理和删除。每条消息仅由单个使用者处理一次。以下是它的工作原理:

  • 创建者将作业发布到队列,然后将作业状态通知用户。
  • 使用者从队列中选取作业,对其进行处理,然后发出作业已完成的信号。

优势

让我们讨论一下使用消息队列的一些优点:

  • 可伸缩性:消息队列使得在我们需要的地方精确缩放成为可能。当工作负载达到峰值时,我们应用程序的多个实例可以将所有请求添加到队列中,而不会有冲突的风险。
  • 解耦:消息队列消除了组件之间的依赖关系,并显著简化了解耦应用程序的实现。
  • 性能:消息队列支持异步通信,这意味着生成和使用消息的终结点与队列交互,而不是彼此交互。创建者可以将请求添加到队列中,而无需等待处理它们。
  • 可靠性:队列使我们的数据持久化,并减少系统不同部分脱机时发生的错误。

特征

现在,让我们讨论一下消息队列的一些所需功能:

推送或拉取交付

大多数消息队列都提供用于检索消息的推送和拉取选项。“拉取”意味着持续查询队列中的新消息。推送意味着当消息可用时,使用者会收到通知。我们还可以使用长轮询来允许拉取等待指定的时间量,以便新消息到达。

先进先出队列

在这些队列中,首先处理最旧(或第一个)条目,有时称为队列的“头”

安排或延迟交货

许多邮件队列都支持为邮件设置特定的传递时间。如果我们需要为所有消息设置一个共同的延迟,我们可以设置一个延迟队列。

至少一次交付

消息队列可以存储消息的多个副本以实现冗余和高可用性,并在发生通信故障或错误时重新发送消息,以确保它们至少传递一次。

精确一次交付

当不能容忍重复项时,FIFO(先进先出)消息队列将通过自动过滤掉重复项来确保每条消息仅传递一次(并且仅传递一次)。

死信队列

死信队列是其他队列可以向其发送无法成功处理的消息的队列。这样可以很容易地将它们放在一边进行进一步检查,而不会阻塞队列处理或将 CPU 周期花费在可能永远不会成功使用的消息上。

订购

大多数消息队列都提供尽力而为的排序,这可确保消息通常以与发送相同的顺序传递,并且消息至少传递一次。

毒丸消息

毒丸是可以接收但无法处理的特殊消息。它们是一种机制,用于向使用者发出信号以结束其工作,以便它不再等待新的输入,并且类似于在客户端/服务器模型中关闭套接字。

安全

消息队列将对尝试访问队列的应用程序进行身份验证,这使我们能够通过网络以及队列本身加密消息。

任务队列

任务队列接收任务及其相关数据,运行它们,然后传递其结果。它们可以支持计划,并可用于在后台运行计算密集型作业。

背压

如果队列开始显著增长,队列大小可能会大于内存,从而导致缓存未命中、磁盘读取甚至性能降低。背压可以通过限制队列大小来提供帮助,从而为队列中已有的作业保持高吞吐率和良好的响应时间。队列填满后,客户端将获取服务器繁忙或 HTTP 503 状态代码,以便稍后重试。客户端可以在以后重试请求,可能使用指数退避策略。

例子

以下是一些广泛使用的消息队列:

发布-订阅

与消息队列类似,发布-订阅也是一种服务到服务通信的形式,可促进异步通信。在发布/订阅模型中,发布到主题的任何消息都会立即推送给该主题的所有订阅者。

发布-订阅

消息主题的订阅者通常执行不同的功能,并且每个人都可以并行执行不同的消息操作。发布者不需要知道谁在使用它正在广播的信息,订阅者也不需要知道消息来自哪里。这种消息传递样式与消息队列略有不同,在消息队列中,发送消息的组件通常知道要发送到的目标。

加工

与消息队列不同,消息队列对消息进行批处理,直到它们被检索,消息主题传输消息时很少或没有排队,并立即将它们推送给所有订阅者。以下是它的工作原理:

  • 消息主题提供了一种轻量级机制来广播异步事件通知和终结点,这些通知和终结点允许软件组件连接到主题以发送和接收这些消息。
  • 要广播消息,称为发布者的组件只需将消息推送到主题即可。
  • 订阅该主题的所有组件(称为订阅者)都将收到广播的每条消息。

优势

让我们讨论一下使用发布-订阅的一些优点:

  • 消除轮询:消息主题允许即时的、基于推送的传递,使消息使用者无需定期检查或“轮询”以获取新信息和更新。这促进了更快的响应时间并减少了交付延迟,这在不能容忍延迟的系统中尤其成问题。
  • 动态定位:发布/订阅使服务的发现更轻松、更自然、更不容易出错。发布者不会维护应用程序可以发送消息的对等方名单,而是只需将消息发布到主题即可。然后,任何感兴趣的参与方都将为其终端节点订阅该主题,并开始接收这些消息。订阅者可以更改,升级,乘法或消失,系统会动态调整。
  • 解耦和独立扩展:发布者和订阅者是分离的,彼此独立工作,这使我们能够独立开发和扩展它们。
  • 简化通信:发布-订阅模型通过删除与消息主题的单个连接的所有点对点连接来降低复杂性,该连接将管理订阅并决定应将哪些消息传递到哪些终结点。

特征

现在,让我们讨论一下发布-订阅的一些所需功能:

推送交付

发布/订阅消息在消息发布到消息主题时会立即推送异步事件通知。当消息可用时,订阅者会收到通知。

多种传递协议

在发布-订阅模型中,主题通常可以连接到多种类型的终结点,例如消息队列、无服务器函数、HTTP 服务器等。

扇出

当消息发送到主题,然后复制并推送到多个终结点时,会发生这种情况。扇出提供异步事件通知,这反过来又允许并行处理。

滤波

此功能使订阅者能够创建消息过滤策略,以便它仅获取感兴趣的通知,而不是接收发布到主题的每条消息。

耐久性

发布/订阅消息传递服务通常通过在多个服务器上存储同一消息的副本来提供非常高的持久性,并且至少传递一次。

安全

消息主题对尝试发布内容的应用程序进行身份验证,这允许我们使用加密的终结点并加密通过网络传输的消息。

例子

以下是一些常用的发布-订阅技术:

企业服务总线 (ESB)

企业服务总线 (ESB) 是一种体系结构模式,集中式软件组件通过这种模式在应用程序之间执行集成。它执行数据模型的转换,处理连接,执行消息路由,转换通信协议,并可能管理多个请求的组成。ESB 可以将这些集成和转换作为服务接口提供,以供新应用程序重用。

企业-服务-总线

优势

从理论上讲,集中式 ESB 提供了标准化和显著简化整个企业中服务之间的通信、消息传递和集成的潜力。以下是使用 ESB 的一些优点:

  • 提高开发人员的工作效率:使开发人员能够将新技术合并到应用程序的一个部分,而无需接触应用程序的其余部分。
  • 更简单、更具成本效益的可扩展性:组件可以独立于其他组件进行扩展。
  • 更大的弹性:一个组件的故障不会影响其他组件,并且每个微服务都可以遵守自己的可用性要求,而不会危及系统中其他组件的可用性。

虽然 ESB 在许多组织中都已成功部署,但在许多其他组织中,ESB 被视为一个瓶颈。以下是使用 ESB 的一些缺点:

  • 对一个集成进行更改或增强可能会破坏使用相同集成的其他集成的稳定性。
  • 单点故障可能导致所有通信中断。
  • 对 ESB 的更新通常会影响现有集成,因此执行任何更新都需要大量的测试。
  • ESB 是集中管理的,这使得跨团队协作具有挑战性。
  • 配置和维护复杂性高。

例子

以下是一些广泛使用的企业服务总线 (ESB) 技术:

整体式架构和微服务

巨石

整体式架构是一个自包含且独立的应用程序。它是作为一个单元构建的,不仅负责特定任务,还可以执行满足业务需求所需的每个步骤。

整体

优势

以下是整体式架构的一些优点:

  • 易于开发或调试。
  • 快速可靠的沟通。
  • 易于监控和测试。
  • 支持酸性物质交换事务。

整体式架构的一些常见缺点是:

  • 随着代码库的增长,维护变得困难。
  • 紧密耦合的应用,难以扩展。
  • 需要对特定技术堆栈的承诺。
  • 每次更新时,都会重新部署整个应用程序。
  • 可靠性降低,因为单个错误可能会使整个系统瘫痪。
  • 难以扩展或采用新技术。

模块化单体

模块化Monolix是一种方法,我们构建和部署单个应用程序(这是Monolit部分),但我们构建它的方式是将代码分解为应用程序所需的每个功能的独立模块。

这种方法减少了模块的依赖性,例如我们可以在不影响其他模块的情况下增强或更改模块。如果做得好,从长远来看,这可能是非常有益的,因为它降低了随着系统增长而维护整体的复杂性。

微服务

微服务体系结构由一组小型自治服务组成,其中每个服务都是自包含的,应在有界上下文中实现单个业务功能。有界上下文是业务逻辑的自然划分,它提供了域模型存在的显式边界。

微服务

每个服务都有一个单独的代码库,可以由一个小型开发团队来管理。服务可以独立部署,团队可以更新现有服务,而无需重新生成和重新部署整个应用程序。

服务负责保存自己的数据或外部状态(每个服务的数据库)。这与传统模型不同,传统模型由单独的数据层处理数据持久性。

特性

微服务架构风格具有以下特点:

  • 松散耦合:服务应松散耦合,以便可以独立部署和缩放。这将导致开发团队的权力下放,从而使他们能够以最小的限制和操作依赖性更快地开发和部署。
  • 小而专注:这是关于范围和责任,而不是规模,服务应该专注于一个特定的问题。基本上,“它只做一件事,而且做得很好”。理想情况下,它们可以独立于底层架构。
  • 为企业构建:微服务架构通常围绕业务功能和优先级进行组织。
  • 弹性和容错能力:服务的设计方式应使其在发生故障或错误时仍能正常工作。在具有可独立部署服务的环境中,容错能力至关重要。
  • 高度可维护:服务应易于维护和测试,因为无法维护的服务将被重写。

优势

以下是微服务架构的一些优势:

  • 松散耦合的服务。
  • 服务可以独立部署。
  • 高度敏捷,适用于多个开发团队。
  • 改进了容错能力和数据隔离。
  • 更好的可伸缩性,因为每个服务都可以独立扩展。
  • 消除对特定技术堆栈的任何长期承诺。

微服务架构带来了一系列挑战:

  • 分布式系统的复杂性。
  • 测试更加困难。
  • 维护成本高昂(单个服务器、数据库等)。
  • 服务间通信也有其自身的挑战。
  • 数据完整性和一致性。
  • 网络拥塞和延迟。

最佳实践

让我们讨论一些微服务最佳实践:

  • 围绕业务域对服务进行建模。
  • 服务应具有松散耦合和高功能内聚力。
  • 隔离故障并使用复原策略来防止服务中的故障级联。
  • 服务应仅通过设计良好的 API 进行通信。避免泄露实施细节。
  • 数据存储应专用于拥有数据的服务
  • 避免服务之间的耦合。耦合的原因包括共享数据库架构和严格的通信协议。
  • 分散一切。各个团队负责设计和构建服务。避免共享代码或数据架构。
  • 通过使用断路器实现容错,实现快速故障。
  • 确保 API 更改向后兼容。

陷阱

以下是微服务架构的一些常见陷阱:

  • 服务边界不基于业务域。
  • 低估了构建分布式系统的难度。
  • 共享数据库或服务之间的常见依赖关系。
  • 缺乏业务一致性。
  • 缺乏明确的所有权。
  • 缺乏幂等性。
  • 尝试做所有酸而不是碱
  • 缺乏容错设计可能会导致级联故障。

当心分布式整体式架构

分布式 Monolith 是一个类似于微服务体系结构的系统,但在自身内部像整体式应用程序一样紧密耦合。采用微服务架构有很多好处。但是在制作一个时,我们很有可能最终得到一个分布式的单体。

我们的微服务只是一个分布式整体式架构,如果其中任何一个适用于它:

  • 需要低延迟通信。
  • 服务不容易扩展。
  • 服务之间的依赖关系。
  • 共享相同的资源,如数据库。
  • 紧密耦合的系统。

使用微服务体系结构构建应用程序的主要原因之一是具有可伸缩性。因此,微服务应具有松散耦合的服务,使每个服务都是独立的。分布式单体架构消除了这一点,并导致大多数组件相互依赖,从而增加了设计复杂性。

微服务与面向服务的体系结构 (SOA)

你可能已经看到互联网上提到了面向服务的体系结构(SOA),有时甚至可以与微服务互换,但它们彼此不同,两种方法之间的主要区别归结为范围

面向服务的体系结构 (SOA) 定义了一种通过服务接口使软件组件可重用的方法。这些接口利用通用通信标准,专注于最大化应用程序服务的可重用性,而微服务则构建为各种最小的独立服务单元的集合,专注于团队自治和分离。

为什么不需要微服务

架构范围

所以,你可能想知道,巨石一开始似乎是一个坏主意,为什么有人会使用它?

嗯,这要视情况而定。虽然每种方法都有自己的优点和缺点,但在构建新系统时,建议从整体开始。重要的是要明白,微服务不是灵丹妙药,相反,它们解决了组织问题。微服务架构与你的组织优先级和团队有关,与技术有关。

在决定迁移到微服务架构之前,你需要问自己以下问题:

  • “团队是否太大而无法在共享代码库上有效工作?”
  • “球队是否被其他球队封杀?”
  • “微服务是否为我们提供了明确的业务价值?”
  • “我的业务是否成熟到足以使用微服务?”
  • “我们目前的架构是否限制了我们的通信开销?”

如果你的应用程序不需要分解为微服务,则不需要这样做。并非绝对需要将所有应用程序分解为微服务。

我们经常从Netflix等公司及其对微服务的使用中汲取灵感,但我们忽略了一个事实,即我们不是Netflix。在他们有一个市场就绪的解决方案之前,他们经历了大量的迭代和模型,当他们确定并解决他们想解决的问题时,这种架构对他们来说是可以接受的。

这就是为什么深入了解你的业务是否确实需要微服务至关重要的原因。我想说的是,微服务是解决复杂问题的解决方案,如果你的企业没有复杂的问题,那么你就不需要它们。

事件驱动架构

事件驱动架构(EDA)是关于使用事件作为在系统内进行通信的一种方式。通常,利用消息代理异步发布和使用事件。发布者不知道谁在消费事件,而消费者彼此之间也不知道。事件驱动架构只是一种在系统内的服务之间实现松散耦合的方法。

什么是事件?

事件是表示系统中状态更改的数据点。它没有指定应该发生什么以及更改应该如何修改系统,它只通知系统特定的状态更改。当用户执行操作时,他们会触发事件。

组件

事件驱动架构有三个关键组件:

  • 事件生产者:将事件发布到路由器。
  • 事件路由器:过滤事件并将其推送给使用者。
  • 事件使用者:使用事件来反映系统中的更改。

事件驱动的体系结构

注: 图中的点表示系统中的不同事件。

模式

有几种方法可以实现事件驱动的体系结构,我们使用哪种方法取决于用例,但这里有一些常见的例子:

注意:这些方法中的每一个都单独讨论。

优势

让我们讨论一些优点:

  • 生产者和消费者脱钩。
  • 高度可扩展和分布式。
  • 易于添加新消费者。
  • 提高敏捷性。

挑战

以下是事件驱动器体系结构的一些挑战:

  • 保证交货。
  • 错误处理很困难。
  • 事件驱动系统通常很复杂。
  • 正好按顺序处理事件一次。

使用案例

以下是事件驱动架构有益的一些常见用例:

  • 元数据和指标。
  • 服务器和安全日志。
  • 集成异构系统。
  • 扇出和并行处理。

例子

以下是一些广泛用于实现事件驱动架构的技术:

事件溯源

不要只存储域中数据的当前状态,而是使用仅追加存储来记录对该数据执行的一系列操作。存储充当记录系统,可用于具体化域对象。

事件溯源

这样可以简化复杂域中的任务,因为不需要同步数据模型和业务域,同时提高了性能、可伸缩性和响应能力。它还可以为事务数据提供一致性,并维护完整的审计跟踪和历史记录,从而实现补偿操作。

事件溯源与事件驱动架构 (EDA)

事件溯源似乎经常与事件驱动架构 (EDA) 混淆。事件驱动架构是关于使用事件在服务边界之间进行通信。通常,利用消息代理在其他边界内异步发布和使用事件。

然而,事件溯源是关于使用事件作为状态,这是一种不同的数据存储方法。我们不是存储当前状态,而是存储事件。此外,事件溯源是实现事件驱动架构的几种模式之一。

优势

让我们讨论一下使用事件溯源的一些优点:

  • 非常适合实时数据报告。
  • 非常适合故障安全,可以从事件存储中重建数据。
  • 非常灵活,可以存储任何类型的消息。
  • 为高合规性系统实现审核日志功能的首选方式。

以下是事件溯源的缺点:

  • 需要极其高效的网络基础设施。
  • 需要一种可靠的方法来控制消息格式,如架构注册表。
  • 不同的事件将包含不同的有效负载。

命令和查询责任分离 (CQRS)

命令查询责任分离 (CQRS) 是一种体系结构模式,它将系统的操作划分为命令和查询。它首先由格雷格·杨描述。

在CQRS中,命令是一个指令,一个执行特定任务的指令。它是改变某些东西的意图,不返回值,只是成功或失败的指示。而且,查询是对不会更改系统状态或导致任何副作用的信息的请求。

命令和查询责任分离

CQRS 的核心原则是命令和查询的分离。它们在系统中执行根本不同的角色,将它们分开意味着每个角色都可以根据需要进行优化,分布式系统可以真正从中受益。

具有事件溯源的断续器

CQRS 模式通常与事件溯源模式一起使用。基于 CQRS 的系统使用单独的读取和写入数据模型,每个模型都是针对相关任务量身定制的,并且通常位于物理上独立的存储中。

当与事件溯源模式一起使用时,事件的存储是写入模型,并且是官方的信息源。基于 CQRS 的系统的读取模型提供数据的实例化视图,通常为高度非规范化的视图。

优势

让我们讨论一下 CQRS 的一些优点:

  • 允许独立扩展读取和写入工作负载。
  • 更易于扩展、优化和架构更改。
  • 更接近具有松散耦合的业务逻辑。
  • 应用程序可以在查询时避免复杂的联接。
  • 清除系统行为之间的界限。

以下是 CQRS 的一些缺点:

  • 更复杂的应用程序设计。
  • 可能会发生消息失败或重复消息。
  • 处理最终的一致性是一个挑战。
  • 增加了系统维护工作。

使用案例

以下是 CQRS 将有所帮助的一些方案:

  • 数据读取的性能必须与数据写入的性能分开进行微调。
  • 预计系统将随着时间的推移而发展,并且可能包含模型的多个版本,或者业务规则定期更改。
  • 与其他系统的集成,特别是与事件溯源的结合使用,其中一个子系统的时间故障不应影响其他子系统的可用性。
  • 更好的安全性,以确保只有正确的域实体对数据执行写入。

接口网关

API 网关是一种 API 管理工具,位于客户端和后端服务集合之间。它是系统中的单一入口点,封装了内部系统体系结构,并为每个客户端提供了量身定制的 API。它还具有其他职责,例如身份验证,监视,负载平衡,缓存,限制,日志记录等。

api-gateway

为什么我们需要 API 网关?

微服务提供的 API 的粒度通常与客户端需要的粒度不同。微服务通常提供细粒度的 API,这意味着客户端需要与多个服务进行交互。因此,API 网关可以为具有一些附加功能和更好管理的所有客户端提供单一入口点。

特征

以下是 API 网关的一些所需功能:

优势

让我们看一下使用 API 网关的一些优点:

  • 封装 API 的内部结构。
  • 提供 API 的集中视图。
  • 简化客户端代码。
  • 监视、分析、跟踪和其他此类功能。

以下是 API 网关的一些可能缺点:

  • 可能的单点故障。
  • 可能会影响性能。
  • 如果缩放不当,可能会成为瓶颈。
  • 配置可能具有挑战性。

前端后端 (BFF) 模式

在前端后端 (BFF) 模式中,我们创建单独的后端服务,供特定的前端应用程序或接口使用。当我们想要避免为多个接口自定义单个后端时,此模式很有用。这种模式最初是由山姆·纽曼描述的。

此外,有时微服务返回给前端的数据输出不是确切的格式,也不是前端根据需要进行筛选。为了解决这个问题,前端应该有一些逻辑来重新格式化数据,因此,我们可以使用BFF将一些逻辑转移到中间层。

后端为前端

前端模式的后端的主要功能是从适当的服务获取所需的数据,格式化数据,并将其发送到前端。

图形QL作为前端(BFF)的后端表现非常好。

何时使用此模式?

在以下情况下,我们应该考虑使用前端后端 (BFF) 模式:

  • 必须维护共享或通用后端服务,并产生大量的开发开销。
  • 我们希望针对特定客户端的要求优化后端。
  • 对通用后端进行自定义以容纳多个接口。

例子

以下是一些广泛使用的网关技术:

REST, GraphQL, gRPC

一个好的API设计始终是任何系统的关键部分。但是,选择合适的 API 技术也很重要。因此,在本教程中,我们将简要讨论不同的 API 技术,例如 REST、图形 QL 和 gRPC。

什么是接口?

在我们进入API技术之前,让我们首先了解什么是API。

API 代表 应用程序编程接口。它是一组用于构建和集成应用软件的定义和协议。它有时被称为信息提供者和信息用户之间的合同,用于建立生产者所需的内容和消费者所需的内容。

换句话说,如果要与计算机或系统交互以检索信息或执行功能,API可以帮助你将所需内容传达给该系统,以便它能够理解并完成请求。

休息

REST API(也称为 RESTAPI)是一种应用程序编程接口,它符合 REST 体系结构样式的约束,并允许与 REST 的 Web 服务进行交互。REST代表代表性国家转移,它是由罗伊·菲尔丁在2000年首次引入的。

在 REST API 中,基本单元是资源。

概念

让我们讨论一下恢复性 API 的一些概念。

约束

为了使 API 被视为 RESTful,它必须符合以下体系结构约束:

  • 统一接口:应该有一种与给定服务器交互的统一方式。
  • 客户端-服务器:通过 HTTP 管理的客户端-服务器体系结构。
  • 无状态:在请求之间,任何客户端上下文都不应存储在服务器上。
  • 可缓存:每个响应都应包括响应是否可缓存,以及响应可以在客户端缓存多长时间。
  • 分层系统:应用程序体系结构需要由多个层组成。
  • 按需代码:返回可执行代码以支持应用程序的一部分。(可选)

动词

HTTP 定义了一组请求方法,以指示要对给定资源执行的所需操作。虽然它们也可以是名词,但这些请求方法有时被称为HTTP动词。它们中的每一个都实现了不同的语义,但是一些共同的特征由它们中的一组共享。

以下是一些常用的 HTTP 动词:

  • GET:请求指定资源的表示形式。
  • HEAD:响应与请求相同,但没有响应正文。
    GET
  • POST:将实体提交到指定的资源,这通常会导致状态更改或对服务器的副作用。
  • PUT:将目标资源的所有当前表示形式替换为请求负载。
  • 删除:删除指定的资源。
  • 修补程序:对资源应用部分修改。

响应代码

HTTP 响应状态代码指示特定 HTTP 请求是否已成功完成。

标准定义了五个类:

  • 1xx - 信息响应。
  • 2xx - 成功的响应。
  • 3xx - 重定向响应。
  • 4xx - 客户端错误响应。
  • 5xx - 服务器错误响应。

例如,HTTP 200 表示请求成功。

优势

让我们讨论一下 REST API 的一些优点:

  • 简单易懂。
  • 灵活便携。
  • 良好的缓存支持。
  • 客户端和服务器是分离的。

让我们讨论一下 REST API 的一些缺点:

  • 过度获取数据。
  • 有时需要多次往返服务器。

使用案例

REST API 几乎被普遍使用,并且是设计 API 的默认标准。整体 REST API 非常灵活,几乎可以适应所有方案。

下面是对用户资源进行操作的 REST API 的用法示例。

乌里安盟 动词 描述
/用户 获取 获取所有用户
/用户/{id} 获取 通过 ID 获取用户
/用户 发布 添加新用户
/用户/{id} 补丁 按 ID 更新用户
/用户/{id} 删除 按 id 删除用户

当涉及到REST API时,还有很多东西需要学习,我强烈建议将超媒体视为应用程序状态的引擎(HATEOAS)

图形

GraphQL 是一种用于 API 的查询语言和服务器端运行时,它优先为客户端提供他们请求的数据,而不是更多。它由Facebook开发,后来在2015年开源。

GraphQL 旨在使 API 快速、灵活且对开发人员友好。此外,GraphQL 使 API 维护人员能够灵活地添加或弃用字段,而不会影响现有查询。开发人员可以使用他们喜欢的任何方法构建API,GraphQL规范将确保它们以可预测的方式向客户端运行。

在图形 QL 中,基本单位是查询。

概念

让我们简要讨论一下 GraphQL 中的一些关键概念:

图式

图形 QL 模式描述了客户端在连接到图形 QL 服务器后可以利用的功能。

查询

查询是客户端发出的请求。它可以由查询的字段和参数组成。查询的操作类型也可以是一种突变,它提供了一种修改服务器端数据的方法。

解析 器

解析程序是为 GraphQL 查询生成响应的函数的集合。简单来说,解析器充当 GraphQL 查询处理程序。

优势

让我们讨论一下图形QL的一些优点:

  • 消除过度获取数据。
  • 强定义的架构。
  • 代码生成支持。
  • 有效负载优化。

让我们讨论一下图形QL的一些缺点:

  • 将复杂性转移到服务器端。
  • 缓存变得困难。
  • 版本控制是模棱两可的。
  • N+1 问题。

使用案例

事实证明,在以下情况下,图形QL是必不可少的:

  • 减少应用带宽使用量,因为我们可以在单个查询中查询多个资源。
  • 复杂系统的快速原型设计。
  • 当我们使用类似图形的数据模型时。

下面是定义类型和类型的 GraphQL 架构。

User
Query

type Query {
  getUser: User
}

type User {
  id: ID
  name: String
  city: String
  state: String
}

使用上述架构,客户端可以轻松请求必填字段,而无需获取整个资源或猜测 API 可能返回的内容。

{
  getUser {
    id
    name
    city
  }
}

这将向客户端提供以下响应。

{
  "getUser": {
    "id": 123,
    "name": "Karan",
    "city": "San Francisco"
  }
}

有关图形QL的更多信息,请访问 graphql.org

断续器

gRPC 是一个现代开源高性能远程过程调用 (RPC) 框架,可以在任何环境中运行。它可以有效地连接数据中心内和数据中心之间的服务,并支持负载平衡、跟踪、运行状况检查、身份验证等。

概念

让我们讨论 gRPC 的一些关键概念。

协议缓冲区

协议缓冲区提供了一种语言和平台中立的可扩展机制,用于以向前和向后兼容的方式序列化结构化数据。它类似于 JSON,只是它更小、更快,并且它生成本机语言绑定。

服务定义

与许多 RPC 系统一样,gRPC 基于定义服务并指定可使用其参数和返回类型远程调用的方法的理念。gRPC 使用协议缓冲区作为接口定义语言 (IDL),用于描述服务接口和有效负载消息的结构。

优势

让我们讨论一下 gRPC 的一些优点:

  • 轻巧高效。
  • 高性能。
  • 内置代码生成支持。
  • 双向流。

让我们讨论一下 gRPC 的一些缺点:

  • 与世界其他地区资源和图形QL相比,相对较新。
  • 有限的浏览器支持。
  • 更陡峭的学习曲线。
  • 不是人类可读的。

使用案例

以下是 gRPC 的一些良好用例:

  • 通过双向流进行实时通信。
  • 微服务中的高效服务间通信。
  • 低延迟和高吞吐量通信。
  • 多语言环境。

下面是在文件中定义的 gRPC 服务的基本示例。使用此定义,我们可以轻松地用我们选择的编程语言编写代码生成服务。

*.proto
HelloService

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}

REST vs GraphQL vs gRPC

现在我们知道了这些 API 设计技术的工作原理,让我们根据以下参数对它们进行比较:

  • 它会导致紧密耦合吗?
  • API 的喋喋不休(用于获取所需信息的不同 API 调用)如何?
  • 性能如何?
  • 集成有多复杂?
  • 缓存的效果如何?
  • 内置工具和代码生成?
  • API 可发现性是怎样的?
  • 对 API 进行版本控制有多容易?
类型 耦合 健谈 性能 复杂性 缓存 科德根 可发现性 版本控制
休息 中等 伟大 容易
图形 中等 习惯 习惯
断续器 中等 伟大 习惯 伟大

哪种 API 技术更好?

好吧,答案是没有一个。没有银弹,因为这些技术中的每一种都有自己的优点和缺点。用户只关心以一致的方式使用我们的 API,因此在设计 API 时,请务必关注你的域和要求。

长轮询、网页封口、服务器发送的事件 (SSE)

Web应用程序最初是围绕客户端 - 服务器模型开发的,其中Web客户端始终是事务的发起者,例如从服务器请求数据。因此,服务器没有机制可以在客户端不首先发出请求的情况下独立地将数据发送或推送到客户端。让我们讨论一些克服这个问题的方法。

长轮询

HTTP 长轮询是一种用于将信息尽快从服务器推送到客户端的技术。因此,服务器不必等待客户端发送请求。

在长轮询中,服务器在收到来自客户端的请求后不会关闭连接。相反,仅当任何新消息可用或达到超时阈值时,服务器才会响应。

长轮询

一旦客户端收到响应,它就会立即向服务器发送一个新请求,以具有新的挂起连接以将数据发送到客户端,并重复该操作。使用此方法,服务器模拟实时服务器推送功能。

加工

让我们了解轮询的工作时间:

  1. 客户端发出初始请求并等待响应。
  2. 服务器接收请求并延迟发送任何内容,直到更新可用。
  3. 更新可用后,响应将发送到客户端。
  4. 客户端收到响应并立即或在定义的某个时间间隔后发出新请求以再次建立连接。

优势

以下是长轮询的一些优点:

  • 易于实施,适合小型项目。
  • 几乎得到普遍支持。

长轮询的一个主要缺点是它通常不可扩展。以下是其他一些原因:

  • 每次都创建一个新连接,该连接在服务器上可能很密集。
  • 对于多个请求,可靠的消息排序可能是一个问题。
  • 由于服务器需要等待新请求,因此延迟增加。

网页套

网页别名通过单个 TCP 连接提供全双工通信通道。它是客户端和服务器之间的持久连接,双方都可以使用它随时开始发送数据。

客户端通过称为“Web主机握手”的进程建立 Web主机连接。如果该过程成功,则服务器和客户端可以随时在两个方向上交换数据。WebSocket 协议能够以较低的开销实现客户端和服务器之间的通信,从而促进与服务器之间的实时数据传输。

网页套筒

这是通过为服务器提供一种标准化的方式来实现的,该方式可以在不被询问的情况下将内容发送到客户端,并允许在保持连接打开的同时来回传递消息。

加工

让我们了解网页套牌的工作原理:

  1. 客户端通过发送请求来启动 WebSocket 握手过程。
  2. 该请求还包含一个 HTTP 升级标头,该标头允许请求切换到 WebSocket 协议 ()。
    ws://
  3. 服务器向客户端发送响应,确认 WebSocket 握手请求。
  4. 一旦客户端收到成功的握手响应,就会打开 WebSocket 连接。
  5. 现在,客户端和服务器可以开始双向发送数据,从而实现实时通信。
  6. 一旦服务器或客户端决定关闭连接,连接即告关闭。

优势

以下是网页套牌的一些优点:

  • 全双工异步消息传递。
  • 更好的基于源的安全模型。
  • 客户端和服务器都是轻量级的。

让我们讨论一下Web别名的一些缺点:

  • 终止的连接不会自动恢复。
  • 较旧的浏览器不支持WebSocket(变得不那么相关)。

服务器发送的事件 (SSE)

服务器发送事件 (SSE) 是一种在客户端和服务器之间建立长期通信的方法,使服务器能够主动将数据推送到客户端。

服务器发送事件

它是单向的,这意味着一旦客户端发送请求,它只能接收响应,而不能通过同一连接发送新请求。

加工

让我们了解服务器发送的事件的工作原理:

  1. 客户端向服务器发出请求。
  2. 客户端和服务器之间的连接已建立并保持打开状态。
  3. 当有新数据可用时,服务器会向客户端发送响应或事件。

优势

  • 易于实现和用于客户端和服务器。
  • 受大多数浏览器支持。
  • 防火墙没有问题。

  • 单向性质可能是有限的。
  • 最大打开连接数的限制。
  • 不支持二进制数据。

地理哈希和四叉树

地理哈希

地理哈希是一种地理编码方法,用于将地理坐标(如纬度和经度)编码为短字母数字字符串。它是由古斯塔沃·尼迈耶在2008年创建的。

例如,具有坐标的旧金山可以在 geohash 中表示为 。

37.7564, -122.4016
9q8yy9mf

地理哈希如何工作?

Geohash 是一种使用 Base-32 字母编码的分层空间索引,geohash 中的第一个字符将初始位置标识为 32 个像元之一。此单元格还将包含 32 个单元格。这意味着为了表示一个点,世界被递归地分成越来越小的单元,每个额外的位,直到达到所需的精度。精度系数还决定了像元的大小。

地理哈希

如果点的 Geohash 共享更长的前缀,则 Geohashing 可保证点在空间上更接近,这意味着字符串中的字符越多,位置就越精确。例如,地理哈希和在空间上更接近,因为它们共享前缀 。

9q8yy9mf
9q8yy9vx
9q8yy9

Geohashing也可用于提供一定程度的匿名性,因为我们不需要公开用户的确切位置,因为根据地理哈希的长度,我们只知道他们在某个区域内的某个地方。

不同长度的地理哈希的像元大小如下:

地理哈希长度 单元格宽度 单元格高度
1 5000 千米 5000 千米
2 1250 公里 1250 公里
3 156 公里 156 公里
4 39.1公里 19.5公里
5 4.89公里 4.89公里
6 1.22公里 0.61 千米
7 153 米 153 米
8 38.2 米 19.1 米
9 4.77 米 4.77 米
10 1.19 米 0.596 米
11 149 毫米 149 毫米
12 37.2 毫米 18.6 毫米

使用案例

以下是地理哈希的一些常见用例:

  • 这是在数据库中表示和存储位置的简单方法。
  • 它也可以作为URL在社交媒体上共享,因为它比纬度和经度更容易共享和记忆。
  • 我们可以通过非常简单的字符串比较和有效的索引搜索来有效地找到点的最近邻。

例子

地理哈希被广泛使用,并得到流行数据库的支持。

四叉树

四叉树是一种树数据结构,其中每个内部节点正好有四个子节点。它们通常用于通过将二维空间递归细分为四个象限或区域来划分二维空间。每个子节点或叶节点存储空间信息。四叉树是八进制的二维模拟,用于分割三维空间。

四叉树

四叉树的类型

四叉树可以根据它们所表示的数据类型进行分类,包括面积、点、线和曲线。以下是常见的四叉树类型:

  • 点四叉树
  • 点区域 (PR) 四叉树
  • 多边形 map (PM) 四叉树
  • 压缩四叉树
  • 边缘四叉树

为什么我们需要四叉树?

纬度和经度还不够吗?为什么我们需要四叉树?虽然在理论上使用纬度和经度,我们可以确定诸如使用欧几里得距离的点之间的距离之类的事情,但对于实际用例,它根本无法扩展,因为它具有CPU密集型性质和大型数据集。

四叉树细分

四叉树使我们能够有效地搜索二维范围内的点,其中这些点被定义为纬度/经度坐标或笛卡尔(x,y)坐标。此外,我们可以通过仅在特定阈值后细分节点来节省进一步的计算。并且通过应用希尔伯特曲线等映射算法,我们可以轻松提高范围查询性能。

使用案例

以下是四叉树的一些常见用法:

  • 图像表示、处理和压缩。
  • 空间索引和范围查询。
  • 基于位置的服务,如谷歌 map,优步等。
  • 网格生成和计算机图形。
  • 稀疏数据存储。

断路器

断路器是一种用于检测故障的设计模式,它封装了防止故障在维护、临时外部系统故障或意外系统故障期间不断重复发生的逻辑。

断路器

断路器背后的基本思想非常简单。我们将受保护的函数调用包装在断路器对象中,该对象监视故障。一旦故障达到某个阈值,断路器就会跳闸,并且所有对断路器的进一步调用都会返回错误,而根本不进行受保护的调用。通常,如果断路器跳闸,我们还需要某种监视器警报。

为什么我们需要断路?

软件系统通常会远程调用在不同进程中运行的软件,可能是在网络上的不同计算机上运行的软件。内存中调用和远程调用之间的一个巨大区别是,远程调用可能会失败,或者在没有响应的情况下挂起,直到达到某个超时限制。更糟糕的是,如果我们在一个没有响应的供应商上有许多呼叫者,那么我们可能会耗尽关键资源,从而导致跨多个系统的级联故障。

国家

让我们讨论断路器状态:

当一切正常时,断路器保持关闭状态,所有请求都像往常一样传递到服务。如果故障次数超过阈值,断路器将跳闸并进入打开状态。

打开

在此状态下,断路器会立即返回错误,甚至无需调用服务。断路器在经过一定超时后进入半开状态。通常,它将有一个监视系统,其中将指定超时。

半开式

在此状态下,断路器允许来自服务的有限数量的请求通过并调用操作。如果请求成功,则断路器将进入关闭状态。但是,如果请求继续失败,则会返回到打开状态。

速率限制

速率限制是指防止操作的频率超过定义的限制。在大型系统中,速率限制通常用于保护基础服务和资源。速率限制通常用作分布式系统中的防御机制,以便共享资源可以保持可用性。它还通过限制在给定时间段内可以到达我们的 API 的请求数来保护我们的 API 免受意外或恶意过度使用。

速率限制

为什么我们需要速率限制?

速率限制是任何大规模系统中非常重要的一部分,可用于实现以下目的:

  • 避免因拒绝服务 (DoS) 攻击而导致的资源匮乏。
  • 速率限制有助于控制运营成本,因为它对资源的自动扩展设置了虚拟上限,如果不对其进行监控,可能会导致指数级账单。
  • 速率限制可用作防御或缓解某些常见攻击。
  • 对于处理大量数据的 API,可以使用速率限制来控制该数据的流。

算法

有各种用于 API 速率限制的算法,每种算法都有其优点和缺点。让我们简要讨论一下其中的一些算法:

漏水的桶

泄漏存储桶是一种算法,它提供了一种简单,直观的方法来通过队列进行速率限制。注册请求时,系统会将其附加到队列的末尾。队列中第一个项目的处理以固定间隔或先进先出 (FIFO) 进行。如果队列已满,则丢弃(或泄漏)其他请求。

令牌存储桶

在这里,我们使用存储桶的概念。当请求传入时,必须获取并处理存储桶中的令牌。如果存储桶中没有可用的令牌,则请求将被拒绝,请求者必须稍后重试。因此,令牌存储桶在特定时间段后会刷新。

固定窗口

系统使用秒的窗口大小来跟踪固定的窗口算法速率。每个传入请求都会递增窗口的计数器。如果计数器超过阈值,它将丢弃请求。

n

滑动日志

滑动日志速率限制涉及跟踪每个请求的带时间戳的日志。系统将这些日志存储在按时间排序的哈希集或表中。它还会丢弃时间戳超过阈值的日志。当有新请求传入时,我们会计算日志的总和以确定请求速率。如果请求超过阈值速率,则保留该请求。

滑动窗口

滑动窗口是一种混合方法,结合了固定窗口算法的低处理成本和滑动日志的改进边界条件。与固定窗口算法一样,我们跟踪每个固定窗口的计数器。接下来,我们根据当前时间戳考虑前一个窗口的请求速率的加权值,以平滑流量突发。

分布式系统中的速率限制

当涉及分布式系统时,速率限制变得复杂。分布式系统中速率限制带来的两大问题是:

不一致

使用包含多个节点的集群时,我们可能需要强制实施全局速率限制策略。因为如果每个节点都要跟踪其速率限制,则使用者在向不同节点发送请求时可能会超过全局速率限制。节点数越多,用户就越有可能超过全局限制。

解决此问题的最简单方法是在我们的负载均衡器中使用粘性会话,以便每个使用者仅发送到一个节点,但这会导致缺乏容错和扩展问题。另一种方法可能是使用像 Redis 这样的集中式数据存储,但这会增加延迟并导致争用条件。

竞争条件

当我们使用一种朴素的“获取-然后设置”方法时,就会发生此问题,在该方法中,我们检索当前速率限制计数器,递增它,然后将其推送回数据存储。此模型的问题在于,在执行读取增量存储的完整周期所需的时间内,其他请求可能会通过,每个请求都尝试使用无效(较低)计数器值存储增量计数器。这允许使用者发送大量请求以绕过速率限制控件。

避免此问题的一种方法是在密钥周围使用某种分布式锁定机制,以防止任何其他进程访问或写入计数器。虽然锁将成为一个重大瓶颈,并且不会很好地扩展。更好的方法可能是使用“先设置后获取”的方法,允许我们快速递增和检查计数器值,而不会让原子操作妨碍。

服务发现

服务发现是对计算机网络中的服务的检测。服务发现协议 (SDP) 是一种网络标准,通过识别资源来完成网络检测。

为什么我们需要服务发现?

在整体式应用程序中,服务通过语言级方法或过程调用相互调用。但是,基于微服务的现代应用程序通常在虚拟化或容器化环境中运行,其中服务的实例数及其位置动态变化。因此,我们需要一种机制,使服务客户端能够向一组动态变化的临时服务实例发出请求。

实现

有两种主要的服务发现模式:

客户端发现

客户端服务发现

在这种方法中,客户端通过查询负责管理和存储所有服务的网络位置的服务注册表来获取另一个服务的位置。

服务器端发现

服务器端服务发现

在此方法中,我们使用中间组件,如负载均衡器。客户端通过负载均衡器向服务发出请求,然后负载均衡器将请求转发到可用的服务实例。

服务注册表

服务注册表基本上是一个数据库,其中包含客户端可以访问的服务实例的网络位置。服务注册表必须高度可用且最新。

服务注册

我们还需要一种方法来获取服务信息,通常称为服务注册。让我们看一下两种可能的服务注册方法:

自助注册

使用自注册模型时,服务实例负责在服务注册表中注册和取消注册自身。此外,如有必要,服务实例会发送检测信号请求以保持其注册处于活动状态。

第三方注册

注册表通过轮询部署环境或订阅事件来跟踪对正在运行的实例的更改。当它检测到新可用的服务实例时,它会将其记录在其数据库中。服务注册表还会取消注册已终止的服务实例。

服务网格

服务到服务通信在分布式应用程序中是必不可少的,但随着服务数量的增长,在应用程序群集内和跨应用程序群集路由此通信变得越来越复杂。服务网格支持各个服务之间的托管、 Observable 和安全通信。它与服务发现协议配合使用以检测服务。Istio特使是一些最常用的服务网格技术。

例子

以下是一些常用的服务发现基础结构工具:

SLA, SLO, SLI

让我们简要讨论一下 SLA、SLO 和 SLI。这些主要与业务和站点可靠性方面有关,但很高兴知道。

为什么它们很重要?

SLA、SLO 和 SLI 允许公司定义、跟踪和监控对其用户提供的服务做出的承诺。SLA、SLO 和 SLI 应该共同帮助团队在其服务中产生更多的用户信任,并更加强调持续改进事件管理和响应流程。

服务水平协议

SLA或服务级别协议是公司与其给定服务的用户之间达成的协议。SLA 定义了公司就特定指标(如服务可用性)向用户做出的不同承诺。

SLA通常由公司的业务或法律团队编写。

断续器

SLO或服务级别目标是公司向用户做出的有关特定指标(如事件响应或正常运行时间)的承诺。SLO 作为完整用户协议中包含的单个承诺存在于 SLA 中。SLO 是服务必须满足的特定目标,以便符合 SLA。SLO应始终简单,定义清晰,并且易于衡量,以确定目标是否得到实现。

断续器

SLI 或服务级别指示器是用于确定是否满足 SLO 的关键指标。它是 SLO 中描述的指标的测量值。为了保持符合 SLA,SLI 的值必须始终达到或超过 SLO 确定的值。

灾难复原

灾难恢复 (DR) 是在自然灾害、网络攻击甚至业务中断等事件发生后重新获得基础架构的访问权限和功能的过程。

灾难恢复依赖于在不受灾难影响的外部位置复制数据和计算机处理。当服务器因灾难而关闭时,企业需要从备份数据的第二个位置恢复丢失的数据。理想情况下,组织也可以将其计算机处理转移到该远程位置,以便继续运营。

在系统设计访谈中通常不会积极讨论灾难恢复,但对这个主题有一些基本的了解是很重要的。你可以从 AWS 架构完善的框架中了解有关灾难恢复的更多信息。

为什么灾难恢复很重要?

灾难恢复具有以下优点:

  • 最大限度地减少中断和停机时间
  • 限制损害
  • 快速恢复
  • 更好的客户保留率

条款

让我们讨论一些与灾难恢复相关的重要术语:

灾难恢复

专利合作计

恢复时间目标 (RTO) 是服务中断和服务还原之间可接受的最大延迟。这决定了当服务不可用时,什么被视为可接受的时间窗口。

退货单

恢复点目标 (RPO) 是自上次数据恢复点以来可接受的最长时间。这决定了在最后一个恢复点和服务中断之间什么被认为是可接受的数据丢失。

策略

各种灾难恢复 (DR) 策略都可以成为灾难恢复计划的一部分。

备份

这是最简单的灾难恢复类型,涉及异地或可移动驱动器上存储数据。

冷站点

在这种类型的灾难恢复中,组织在第二个站点中设置基础结构。

热门网站

热站点始终维护数据的最新拷贝。热站点的设置非常耗时,并且比冷站点更昂贵,但它们大大减少了停机时间。

虚拟机 (VM) 和容器

在讨论虚拟化与容器化之前,让我们了解什么是虚拟机 (VM) 和容器。

虚拟机 (VM)

虚拟机 (VM) 是一个虚拟环境,它充当虚拟计算机系统,具有自己的 CPU、内存、网络接口和存储,这些系统是在物理硬件系统上创建的。称为虚拟机管理程序的软件将计算机的资源与硬件分开,并对其进行适当的置备,以便 VM 可以使用它们。

VM 与系统的其余部分隔离,多个 VM 可以存在于单个硬件(如服务器)上。它们可以根据需要在主机服务器之间移动,也可以更有效地使用资源。

什么是虚拟机管理程序?

虚拟机监控程序有时称为虚拟机监视器 (VMM),它将操作系统和资源与虚拟机隔离开来,并允许创建和管理这些 VM。虚拟机管理程序将 CPU、内存和存储等资源视为可在现有来宾或新虚拟机之间轻松重新分配的资源池。

为什么要使用虚拟机?

服务器整合是使用 VM 的首要原因。大多数操作系统和应用程序部署仅使用少量可用的物理资源。通过虚拟化我们的服务器,我们可以将许多虚拟服务器放置在每个物理服务器上,以提高硬件利用率。这使我们不需要购买额外的物理资源。

VM 提供与系统其余部分隔离的环境,因此 VM 内运行的任何内容都不会干扰主机硬件上运行的任何其他内容。由于 VM 是隔离的,因此它们是测试新应用程序或设置生产环境的不错选择。我们还可以运行单用途 VM 来支持特定用例。

器皿

容器是一个标准的软件单元,它将代码及其所有依赖项(如特定版本的运行时和库)打包在一起,以便应用程序从一个计算环境快速可靠地运行到另一个计算环境。容器提供了一种逻辑打包机制,在这种机制中,应用程序可以从实际运行的环境中抽象出来。这种分离允许轻松、一致地部署基于容器的应用程序,而不管目标环境如何。

为什么我们需要容器?

让我们讨论一下使用容器的一些优点:

责任分离

容器化提供了明确的责任分离,因为开发人员专注于应用程序逻辑和依赖项,而运营团队可以专注于部署和管理。

工作负载可移植性

容器几乎可以在任何地方运行,大大简化了开发和部署。

应用程序隔离

容器在操作系统级别虚拟化 CPU、内存、存储和网络资源,为开发人员提供与其他应用程序在逻辑上隔离的操作系统视图。

敏捷开发

容器允许开发人员通过避免对依赖关系和环境的担忧来更快地移动。

高效运营

容器是轻量级的,允许我们只使用我们需要的计算资源。

虚拟化与容器化

虚拟化与容器化

在传统虚拟化中,虚拟机管理程序虚拟化物理硬件。结果是,每个虚拟机都包含一个来宾操作系统、操作系统运行所需的硬件的虚拟副本以及应用程序及其关联的库和依赖项。

容器不是虚拟化底层硬件,而是虚拟化操作系统,因此每个容器仅包含应用程序及其依赖项,使它们比 VM 更轻量级。

OAuth 2.0 和开放ID连接 (OIDC)

奥傲 2.0

OAuth 2.0代表开放授权,是一种标准,旨在代表用户提供对资源的同意访问,而无需共享用户的凭据。OAuth 2.0是一种授权协议,而不是身份验证协议,它主要设计为授予对一组资源(例如,远程API或用户数据)的访问权限。

概念

OAuth 2.0 协议定义了以下实体:

  • 资源所有者:拥有受保护资源并可以授予对它们的访问权限的用户或系统。
  • 客户端:客户端是需要访问受保护资源的系统。
  • 授权服务器:此服务器接收来自客户端的访问令牌请求,并在成功进行身份验证并得到资源所有者的同意后发出这些请求。
  • 资源服务器:保护用户资源并接收来自客户端的访问请求的服务器。它接受并验证来自客户端的访问令牌,并返回相应的资源。
  • 作用域:它们用于准确指定授予资源访问权限的原因。可接受的作用域值及其与哪些资源相关取决于资源服务器。
  • 访问令牌:表示代表最终用户访问资源的授权的一段数据。

OAuth 2.0 如何工作?

让我们来了解 OAuth 2.0 的工作原理:

哎呀2

  1. 客户端从授权服务器请求授权,并提供客户端 ID 和机密作为标识。它还提供用于发送访问令牌或授权代码的作用域和终结点 URI。
  2. 授权服务器对客户端进行身份验证,并验证是否允许请求的作用域。
  3. 资源所有者与授权服务器交互以授予访问权限。
  4. 授权服务器使用授权代码或访问令牌重定向回客户端,具体取决于授权类型。刷新令牌也可能被返回。
  5. 使用访问令牌,客户端可以从资源服务器请求对资源的访问权限。

以下是 OAuth 2.0 最常见的缺点:

  • 缺少内置的安全功能。
  • 无标准实现。
  • 没有一组通用的作用域。

连接

OAuth 2.0 仅用于授权,用于授予从一个应用程序到另一个应用程序的数据和功能的访问权限。OpenID 连接 (OIDC) 是位于 OAuth 2.0 之上的薄层,它添加了有关登录者的登录和配置文件信息。

当授权服务器支持 OIDC 时,它有时称为身份提供程序 (IdP),因为它会将有关资源所有者的信息返回给客户端。OpenID 连接相对较新,与 OAuth 相比,最佳实践的采用率和行业实施率较低。

概念

开放 ID 连接 (OIDC) 协议定义了以下实体:

  • 信赖方:当前应用程序。
  • OpenID 提供程序:这实质上是一个中间服务,它向信赖方提供一次性代码。
  • 令牌终端节点:接受一次性代码 (OTC) 并提供有效期为一小时的访问代码的 Web 服务器。OIDC 和 OAuth 2.0 之间的主要区别在于,令牌是使用 JSON 网络令牌 (JWT) 提供的。
  • UserInfo 终结点:信赖方与此终结点通信,提供安全令牌并接收有关最终用户的信息

OAuth 2.0 和 OIDC 都易于实施,并且基于 JSON,大多数 Web 和移动应用程序都支持这一点。但是,开放 ID 连接 (OIDC) 规范比基本 OAuth 规范更严格。

单点登录

单一登录 (SSO) 是一个身份验证过程,在此过程中,仅使用一组登录凭据即可向用户提供对多个应用程序或网站的访问权限。这可以防止用户单独登录到不同的应用程序。

用户凭据和其他标识信息由称为身份提供商 (IdP) 的集中式系统存储和管理。身份提供程序是一个受信任的系统,提供对其他网站和应用程序的访问。

基于单点登录 (SSO) 的身份验证系统通常用于员工需要访问其组织的多个应用程序的企业环境中。

组件

让我们讨论一下单一登录 (SSO) 的一些关键组件。

身份提供商 (IdP)

用户身份信息由称为身份提供商 (IdP) 的集中式系统存储和管理。身份提供程序对用户进行身份验证,并提供对服务提供程序的访问权限。

标识提供者可以通过验证用户名和密码或验证由单独的标识提供者提供的有关用户标识的断言来直接对用户进行身份验证。标识提供者处理用户标识的管理,以便将服务提供者从此责任中解放出来。

服务供应商

服务提供商向最终用户提供服务。它们依赖于标识提供者来断言用户的身份,并且通常有关用户的某些属性由标识提供者管理。服务提供商还可以维护用户的本地帐户以及其服务所特有的属性。

身份代理

身份代理充当中介,将多个服务提供程序与各种不同的标识提供程序连接起来。使用身份代理,我们可以对任何应用程序执行单点登录,而无需遵循协议的麻烦。

萨姆尔

安全断言标记语言是一种开放标准,允许客户端在不同系统之间共享有关标识、身份验证和权限的安全信息。SAML 是使用用于共享数据的可扩展标记语言 (XML) 标准实现的。

SAML 专门支持联合身份验证,使身份提供商 (IdP) 能够无缝、安全地将经过身份验证的身份及其属性传递给服务提供商。

单点登录如何工作?

现在,让我们讨论单一登录的工作原理:

索

  1. 用户从所需的应用程序请求资源。
  2. 应用程序将用户重定向到身份提供程序 (IdP) 进行身份验证。
  3. 用户使用其凭据(通常为用户名和密码)登录。
  4. 身份提供程序 (IdP) 将单一登录响应发送回客户端应用程序。
  5. 应用程序向用户授予访问权限。

SAML 与 OAuth 2.0 和开放 ID 连接 (OIDC)

萨姆拉、奥奥斯和运通组织之间存在许多差异。萨姆拉使用 XML 来传递消息,而 OAuth 和 OIDC 则使用 JSON。OAuth 提供了更简单的体验,而 SAML 则面向企业安全。

OAuth 和 OIDC 广泛使用 REST 通信,这就是为什么移动和现代 Web 应用程序发现 OAuth 和 OIDC 为用户提供了更好的体验。另一方面,SAML在浏览器中放置一个会话cookie,允许用户访问某些网页。这非常适合短期工作负荷。

OIDC 对开发人员友好且更易于实现,从而拓宽了可能实现它的用例。它可以通过所有常见编程语言的免费库从头开始快速实现。SAML的安装和维护可能很复杂,只有企业规模的公司才能很好地处理。

开放ID连接本质上是OAuth框架之上的一层。因此,它可以提供内置的权限层,要求用户同意服务提供商可能访问的内容。尽管 SAML 也能够允许同意流,但它是通过开发人员执行的硬编码而不是作为其协议的一部分来实现的。

这两种身份验证协议都擅长它们所做的事情。与往常一样,很大程度上取决于我们的特定用例和目标受众。

优势

以下是使用单一登录的好处:

  • 易于使用,因为用户只需要记住一组凭据。
  • 易于访问,无需经过漫长的授权过程。
  • 强制实施安全性和合规性以保护敏感数据。
  • 通过降低 IT 支持成本和管理时间来简化管理。

以下是单一登录的一些缺点:

  • 单一密码漏洞,如果主 SSO 密码遭到入侵,则所有受支持的应用程序都将受到损害。
  • 使用单一登录的身份验证过程比传统身份验证慢,因为每个应用程序都必须请求 SSO 提供程序进行验证。

例子

以下是一些常用的身份提供商 (IdP):

SSL、 TLS, mTLS

让我们简要讨论一些重要的通信安全协议,例如 SSL、TLS 和 mTLS。我想说的是,从“大局”系统设计的角度来看,这个话题不是很重要,但仍然很好了解。

断续器

SSL代表安全套接字层,它指的是用于加密和保护互联网上发生的通信的协议。它于1995年首次开发,但此后已被弃用,取而代之的是TLS(传输层安全性)。

如果它不推荐使用,为什么它被称为SSL证书?

大多数主要证书提供商仍将证书称为 SSL 证书,这就是命名约定保持不变的原因。

为什么 SSL 如此重要?

最初,网络上的数据以明文形式传输,如果截获了消息,任何人都可以阅读。创建 SSL 是为了更正此问题并保护用户隐私。通过加密用户和Web服务器之间的任何数据,SSL还可以通过防止攻击者篡改传输中的数据来阻止某些类型的网络攻击。

红绿灯系统

传输层安全性(TLS)是一种广泛采用的安全协议,旨在促进互联网通信的隐私和数据安全。TLS是从以前的加密协议(称为安全套接字层(SSL)演变而来的。TLS的一个主要用例是加密Web应用程序和服务器之间的通信。

TLS协议实现的目标有三个主要组成部分:

  • 加密:隐藏从第三方传输的数据。
  • 身份验证:确保交换信息的各方是他们声称的身份。
  • 完整性:验证数据是否未被伪造或篡改。

断续器

相互 TLS 或 mTLS 是一种相互身份验证的方法。mTLS 通过验证网络连接两端的各方是否都具有正确的私钥,确保它们都是它们所声称的身份。其各自的 TLS 证书中的信息提供了额外的验证。

为什么使用移动语言处理?

mTLS 有助于确保客户端和服务器之间的流量在两个方向上都是安全且受信任的。这为登录到组织的网络或应用程序的用户提供了额外的安全层。它还验证与不遵循登录过程的客户端设备(如物联网 (IoT) 设备)的连接。

如今,微服务或分布式系统通常使用mTLS在零信任安全模型中相互验证。

系统设计面试

系统设计是一个非常广泛的主题,系统设计访谈旨在评估你为抽象问题提供技术解决方案的能力,因此,它们不是为特定答案而设计的。系统设计面试的独特之处在于候选人和面试官之间的双向性。

在不同的工程水平上,期望也大不相同。这是因为具有丰富实践经验的人会与业内新手截然不同。因此,很难提出一个单一的策略来帮助我们在面试期间保持井井有条。

让我们来看看系统设计访谈的一些常见策略:

需求说明

系统设计面试问题,本质上是模糊或抽象的。询问有关问题确切范围的问题,并在面试早期澄清功能要求至关重要。通常,要求分为三个部分:

功能要求

这些是最终用户作为系统应提供的基本功能特别要求的要求。所有这些功能都必须作为合同的一部分纳入系统。

例如:

  • “我们需要为这个系统设计哪些功能?”
  • “在我们的设计中,我们需要考虑哪些边缘情况(如果有的话)?”

非功能性需求

这些是系统必须根据项目合同满足的质量约束。这些因素的实施的优先次序或程度因项目而异。它们也被称为非行为要求。例如,可移植性、可维护性、可靠性、可扩展性、安全性等。

例如:

  • “应以最小的延迟处理每个请求”
  • “系统应该具有高可用性”

扩展要求

这些基本上是“很高兴有”的要求,可能超出了系统的范围。

例如:

  • “我们的系统应该记录指标和分析”
  • “服务运行状况和性能监视?”

估计和约束

估计我们将要设计的系统的规模。重要的是要提出以下问题:

  • “这个系统需要处理的期望规模是多少?”
  • “我们系统的读/写比率是多少?”
  • “每秒有多少个请求?”
  • “需要多少存储空间?”

这些问题将帮助我们以后扩展我们的设计。

数据模型设计

一旦我们有了估计值,我们就可以开始定义数据库架构。在面试的早期阶段这样做将有助于我们了解数据流,这是每个系统的核心。在此步骤中,我们基本上定义了所有实体以及它们之间的关系。

  • “系统中有哪些不同的实体?”
  • “这些实体之间有什么关系?”
  • “我们需要多少张桌子?”
  • “这里不是更好的选择吗?”

原料药设计

接下来,我们可以开始为系统设计 API。这些 API 将帮助我们显式定义来自系统的期望。我们不必编写任何代码,只需一个简单的接口来定义API要求,例如参数,函数,类,类型,实体等。

例如:

createUser(name: string, email: string): User

建议保持界面尽可能简单,稍后在满足扩展要求时再回来讨论。

高级组件设计

现在我们已经建立了数据模型和API设计,现在是时候确定解决我们的问题所需的系统组件(例如负载均衡器,API网关等)并起草我们系统的第一个设计了。

  • “最好是设计整体式架构还是微服务架构?”
  • “我们应该使用什么类型的数据库?”

一旦我们有了一个基本的图表,我们就可以开始与面试官讨论系统将如何从客户的角度工作。

详细设计

现在是时候详细介绍我们设计的系统的主要组件了。一如既往地与面试官讨论哪个组件可能需要进一步改进。

这是一个很好的机会来展示你在专业领域的经验。介绍不同的方法、优点和缺点。解释你的设计决策,并用示例支持它们。这也是讨论系统可能能够支持的任何其他功能的好时机,尽管这是可选的。

  • “我们应该如何对数据进行分区?”
  • “负载分配呢?”
  • “我们应该使用缓存吗?”
  • “我们将如何应对突然的交通高峰?”

此外,尽量不要对某些技术过于固执己见,像“我相信NoSQL数据库更好,SQL数据库不可扩展”这样的陈述反映很差。作为一个多年来采访过很多人的人,我在这里的两分钱就是对你所知道的和你不知道的谦卑。使用你现有的知识和示例来浏览面试的这一部分。

识别并解决瓶颈

最后,是时候讨论瓶颈和缓解瓶颈的方法了。以下是一些需要问的重要问题:

  • “我们是否有足够的数据库副本?”
  • “是否存在单点故障?”
  • “是否需要数据库分片?”
  • “我们如何使我们的系统更加强大?”
  • “如何提高缓存的可用性?”

请务必阅读你正在面试的公司的工程博客。这将帮助你了解他们正在使用的技术堆栈以及哪些问题对他们很重要。

网址缩短器

让我们设计一个URL缩短器,类似于像比特利TinyURL这样的服务。

什么是网址缩短器?

URL 缩短器服务为长 URL 创建别名或短 URL。当用户访问这些短链接时,他们会被重定向到原始 URL。

例如,可以将以下长 URL 更改为较短的 URL。

长网址https://karanpratapsingh.com/courses/system-design/url-shortener

短网址https://bit.ly/3I71d3o

为什么我们需要一个URL缩短器?

当我们共享 URL 时,URL 缩短器通常会节省空间。用户也不太可能输入较短的URL。此外,我们还可以跨设备优化链接,这使我们能够跟踪单个链接。

要求

我们的URL缩短系统应满足以下要求:

功能要求

  • 给定一个URL,我们的服务应该为其生成一个更短且唯一的别名。
  • 当用户访问短链接时,应将其重定向到原始 URL。
  • 链接应在默认时间跨度后过期。

非功能性需求

  • 高可用性和最小延迟。
  • 系统应该是可扩展和高效的。

扩展要求

  • 防止滥用服务。
  • 记录重定向的分析和指标。

估计和约束

让我们从估计和约束开始。

注意:请务必与面试官核实任何与规模或流量相关的假设。

交通

这将是一个读取密集型系统,因此让我们假设每月生成1亿个链接的读/写比率。

100:1

每月读取/写入次数

对于每月读取次数:

$$ 100 \乘以 100 \空间百万 = 10 \空间十亿/月 $$

同样对于写入:

$$ 1 \乘以 100 \空间百万 = 100 \空间百万/月 $$

对于我们的系统,每秒请求数 (RPS) 是多少?

每月 1 亿个请求转换为每秒 40 个请求。

$$ \frac{100 \空间百万}{(30 \空间日 \乘以 24 \空间小时 \乘以 3600 \空格秒)} = \sim 40 \空格 URL/秒 $$

使用读/写比率,重定向次数将为:

100:1

$$ 100 \乘以 40 \空间 URL/秒 = 4000 \空间请求/秒 $$

带宽

由于我们预计每秒大约有 40 个 URL,并且如果我们假设每个请求的大小为 500 字节,那么写入请求的总传入数据将为:

$$ 40 \ 乘以 500 \空间字节 = 20 \空间 KB/秒 $$

同样,对于读取请求,由于我们预计大约有 4K 重定向,因此总传出数据将为:

$$ 4000 \空间 URL/秒 \乘以 500 \空间字节 = \sim 2 \空间 MB/秒 $$

存储

对于存储,我们假设我们将每个链接或记录存储在我们的数据库中10年。由于我们预计每月约有 1 亿个新请求,因此我们需要存储的记录总数为:

$$ 100 \空间百万 \乘以 10\空间年 \乘以 12 \空间月 = 12 \空间十亿 $$

像前面一样,如果我们假设每个存储的记录大约是500字节。我们需要大约6TB的存储空间:

$$ 12 \空间十亿 \乘以 500 \空间字节 = 6 \空间 TB $$

缓存

对于缓存,我们将遵循经典的帕累托原则,也称为 80/20 规则。这意味着 80% 的请求是针对 20% 的数据,因此我们可以缓存大约 20% 的请求。

由于我们每秒收到大约4K读取或重定向请求,因此这意味着每天有3.5亿个请求。

$$ 4000 \空间 URL/秒 \乘以 24 \空间小时 \乘以 3600 \空间秒 = \sim 350 \空间百万 \空间请求/天 $$

因此,我们每天需要大约35GB的内存。

$$ 20 \空间百分比 \乘以 350 \空间百万 \乘以 500 \空间字节 = 35 \空间 GB/天 $$

高级估计

以下是我们的高级估计:

类型 估计
写入(新网址) 40/秒
读取(重定向) 4K/秒
带宽(传入) 20 千字节/秒
带宽(传出) 2 兆字节/秒
存储(10 年) 6 结核病
内存(缓存) ~35 千兆字节/天

数据模型设计

接下来,我们将重点介绍数据模型设计。以下是我们的数据库架构:

url-shortener-datamodel

最初,我们可以从两个表开始:

用户

存储用户的详细信息,如 、 、 等。

name
email
createdAt

网址

包含新短 URL 的属性,如 、 、 和,以及创建短 URL 的用户的属性。我们还可以使用该列作为索引来提高查询性能。

expiration
hash
originalURL
userID
hash

我们应该使用什么样的数据库?

由于数据不是强关系的,因此NoSQL数据库(如亚马逊DynamoDB Apache 卡桑德拉MongoDB)将是更好的选择,如果我们决定使用SQL数据库,那么我们可以使用Azure SQL数据库亚马逊RDS之类的东西。

有关更多详细信息,请参阅 SQL 与非 SQL

原料药设计

让我们为我们的服务做一个基本的API设计:

创建网址

这个API应该在给定原始URL的情况下在我们的系统中创建一个新的短URL。

createURL(apiKey: string, originalURL: string, expiration?: Date): string

参数

API 密钥 ():用户提供的 API 密钥。

string

原始网址 ():要缩短的原始网址。

string

过期 ():新 URL 的到期日期(可选)

Date

返回

短网址 ():新的缩短网址。

string

获取网址

此 API 应从给定的短 URL 中检索原始 URL。

getURL(apiKey: string, shortURL: string): string

参数

API 密钥 ():用户提供的 API 密钥。

string

短网址 ():映射到原始网址的短网址。

string

返回

原始网址 ():要检索的原始网址。

string

删除网址

此 API 应从我们的系统中删除给定的短 URL。

deleteURL(apiKey: string, shortURL: string): boolean

参数

API 密钥 ():用户提供的 API 密钥。

string

短网址 ():要删除的短网址。

string

返回

结果 ():表示操作是否成功。

boolean

为什么我们需要 API 密钥?

你必须已经注意到,我们正在使用API密钥来防止滥用我们的服务。使用此 API 密钥,我们可以将用户限制为每秒或每分钟一定数量的请求。对于开发人员 API 来说,这是一种非常标准的做法,应该可以满足我们的扩展要求。

高级设计

现在让我们对系统进行高级设计。

网址编码

我们系统的主要目标是缩短给定的URL,让我们看看不同的方法:

Base62 方法

在这种方法中,我们可以使用 Base62 对原始 URL 进行编码,该 URL 由大写字母 A-Z、小写字母 a-z 和数字 0-9 组成。

$$ 空间 URL 的数量 \空间 = 62^N $$

哪里

N
:生成的 URL 中的字符数。

因此,如果我们想生成一个长度为7个字符的URL,我们将生成大约3.5万亿个不同的URL。

$$ \begin{collect*} 62^5 = \sim 916 \空间百万 \空间 URL \ 62^6 = \sim 56.8 \空间十亿 \空间 URL \ 62^7 = \sim 3.5 \空间万亿 \空间 URL \end{聚集*} $$

这是最简单的解决方案,但它不能保证非重复或抗碰撞键。

MD5 方法

MD5 消息摘要算法是一种广泛使用的哈希函数,可生成 128 位哈希值(或 32 个十六进制数字)。我们可以使用这 32 个十六进制数字来生成 7 个字符长的 URL。

$$ MD5(original_url) \右倾基地62encode \右倾角哈希 $$

然而,这给我们带来了一个新问题,那就是重复和碰撞。我们可以尝试重新计算哈希值,直到找到唯一的哈希值,但这会增加我们系统的开销。最好寻找更具可扩展性的方法。

反方法

在这种方法中,我们将从单个服务器开始,该服务器将维护生成的密钥计数。一旦我们的服务收到请求,它就可以联系到计数器,该计数器返回一个唯一的编号并递增计数器。当下一个请求出现时,计数器再次返回唯一编号,这将继续。

$$ 计数器(0-3.5 \空间万亿) \右箭头 base62encode \右箭头哈希 $$

这种方法的问题在于,它可能很快成为单一故障点。如果我们运行计数器的多个实例,我们可能会发生冲突,因为它本质上是一个分布式系统。

为了解决这个问题,我们可以使用分布式系统管理器,例如Zookeeper,它可以提供分布式同步。动物园管理员可以为我们的服务器维护多个范围。

$$ \begin{对齐*} & 范围 \空间 1: \空间 1 \向右行 1,000,000 \ &范围 \空间 2: \空间 1,000,001 \向右行 2,000,000 \ &范围 \空间 3: \空间 2,000,001 \向右行 3,000,000 \ \结束{对齐*} $$

一旦服务器达到其最大范围,Zookeeper 将为新服务器分配一个未使用的计数器范围。这种方法可以保证URL的不重复和抗冲突。此外,我们可以运行多个 Zookeeper 实例来消除单点故障。

Key Generation Service (KGS)

As we discussed, generating a unique key at scale without duplication and collisions can be a bit of a challenge. To solve this problem, we can create a standalone Key Generation Service (KGS) that generates a unique key ahead of time and stores it in a separate database for later use. This approach can make things simple for us.

How to handle concurrent access?

Once the key is used, we can mark it in the database to make sure we don't reuse it, however, if there are multiple server instances reading data concurrently, two or more servers might try to use the same key.

The easiest way to solve this would be to store keys in two tables. As soon as a key is used, we move it to a separate table with appropriate locking in place. Also, to improve reads, we can keep some of the keys in memory.

KGS database estimations

As per our discussion, we can generate up to ~56.8 billion unique 6 character long keys which will result in us having to store 300 GB of keys.

$$ 6 \space characters \times 56.8 \space billion = \sim 390 \space GB $$

While 390 GB seems like a lot for this simple use case, it is important to remember this is for the entirety of our service lifetime and the size of the keys database would not increase like our main database.

Caching

Now, let's talk about caching. As per our estimations, we will require around ~35 GB of memory per day to cache 20% of the incoming requests to our services. For this use case, we can use Redis or Memcached servers alongside our API server.

For more details, refer to caching.

Design

Now that we have identified some core components, let's do the first draft of our system design.

url-shortener-basic-design

Here's how it works:

Creating a new URL

  1. When a user creates a new URL, our API server requests a new unique key from the Key Generation Service (KGS).
  2. Key Generation Service provides a unique key to the API server and marks the key as used.
  3. API server writes the new URL entry to the database and cache.
  4. Our service returns an HTTP 201 (Created) response to the user.

Accessing a URL

  1. When a client navigates to a certain short URL, the request is sent to the API servers.
  2. The request first hits the cache, and if the entry is not found there then it is retrieved from the database and an HTTP 301 (Redirect) is issued to the original URL.
  3. If the key is still not found in the database, an HTTP 404 (Not found) error is sent to the user.

Detailed design

It's time to discuss the finer details of our design.

Data Partitioning

To scale out our databases we will need to partition our data. Horizontal partitioning (aka Sharding) can be a good first step. We can use partitions schemes such as:

  • Hash-Based Partitioning
  • List-Based Partitioning
  • Range Based Partitioning
  • Composite Partitioning

The above approaches can still cause uneven data and load distribution, we can solve this using Consistent hashing.

For more details, refer to Sharding and Consistent Hashing.

Database cleanup

This is more of a maintenance step for our services and depends on whether we keep the expired entries or remove them. If we do decide to remove expired entries, we can approach this in two different ways:

Active cleanup

In active cleanup, we will run a separate cleanup service which will periodically remove expired links from our storage and cache. This will be a very lightweight service like a cron job.

Passive cleanup

For passive cleanup, we can remove the entry when a user tries to access an expired link. This can ensure a lazy cleanup of our database and cache.

Cache

Now let us talk about caching.

Which cache eviction policy to use?

As we discussed before, we can use solutions like Redis or Memcached and cache 20% of the daily traffic but what kind of cache eviction policy would best fit our needs?

Least Recently Used (LRU) can be a good policy for our system. In this policy, we discard the least recently used key first.

How to handle cache miss?

Whenever there is a cache miss, our servers can hit the database directly and update the cache with the new entries.

Metrics and Analytics

Recording analytics and metrics is one of our extended requirements. We can store and update metadata like visitor's country, platform, the number of views, etc alongside the URL entry in our database.

Security

For security, we can introduce private URLs and authorization. A separate table can be used to store user ids that have permission to access a specific URL. If a user does not have proper permissions, we can return an HTTP 401 (Unauthorized) error.

We can also use an API Gateway as they can support capabilities like authorization, rate limiting, and load balancing out of the box.

Identify and resolve bottlenecks

url-shortener-advanced-design

Let us identify and resolve bottlenecks such as single points of failure in our design:

  • "What if the API service or Key Generation Service crashes?"
  • "How will we distribute our traffic between our components?"
  • "How can we reduce the load on our database?"
  • "What if the key database used by KGS fails?"
  • "How to improve the availability of our cache?"

To make our system more resilient we can do the following:

  • Running multiple instances of our Servers and Key Generation Service.
  • Introducing load balancers between clients, servers, databases, and cache servers.
  • Using multiple read replicas for our database as it's a read-heavy system.
  • Standby replica for our key database in case it fails.
  • Multiple instances and replicas for our distributed cache.

WhatsApp

Let's design a WhatsApp like instant messaging service, similar to services like Facebook Messenger, and WeChat.

What is WhatsApp?

WhatsApp is a chat application that provides instant messaging services to its users. It is one of the most used mobile applications on the planet, connecting over 2 billion users in 180+ countries. WhatsApp is also available on the web.

Requirements

Our system should meet the following requirements:

Functional requirements

  • Should support one-on-one chat.
  • Group chats (max 100 people).
  • Should support file sharing (image, video, etc.).

Non-functional requirements

  • High availability with minimal latency.
  • The system should be scalable and efficient.

Extended requirements

  • Sent, Delivered, and Read receipts of the messages.
  • Show the last seen time of users.
  • Push notifications.

Estimation and Constraints

Let's start with the estimation and constraints.

Note: Make sure to check any scale or traffic-related assumptions with your interviewer.

Traffic

Let us assume we have 50 million daily active users (DAU) and on average each user sends at least 10 messages to 2 different people every day. This gives us 2 billion messages per day.

$$ 50 \space million \times 20 \space messages = 2 \space billion/day $$

Messages can also contain media such as images, videos, or other files. We can assume that 5 percent of messages are media files shared by the users, which gives us additional 200 million files we would need to store.

$$ 5 \space percent \times 2 \space billion = 200 \space million/day $$

What would be Requests Per Second (RPS) for our system?

2 billion requests per day translate into 24K requests per second.

$$ \frac{2 \space billion}{(24 \space hrs \times 3600 \space seconds)} = \sim 24K \space requests/second $$

Storage

If we assume each message on average is 100 bytes, we will require about 200 GB of database storage every day.

$$ 2 \space billion \times 100 \space bytes = \sim 200 \space GB/day $$

As per our requirements, we also know that around 5 percent of our daily messages (100 million) are media files. If we assume each file is 50 KB on average, we will require 10 TB of storage every day.

$$ 100 \space million \times 100 \space KB = 10 \space TB/day $$

And for 10 years, we will require about 38 PB of storage.

$$ (10 \space TB + 0.2 \space TB) \times 10 \space years \times 365 \space days = \sim 38 \space PB $$

Bandwidth

As our system is handling 10.2 TB of ingress every day, we will require a minimum bandwidth of around 120 MB per second.

$$ \frac{10.2 \space TB}{(24 \space hrs \times 3600 \space seconds)} = \sim 120 \space MB/second $$

High-level estimate

Here is our high-level estimate:

Type Estimate
Daily active users (DAU) 50 million
Requests per second (RPS) 24K/s
Storage (per day) ~10.2 TB
Storage (10 years) ~38 PB
Bandwidth ~120 MB/s

Data model design

This is the general data model which reflects our requirements.

whatsapp-datamodel

We have the following tables:

users

This table will contain a user's information such as , , and other details.

name
phoneNumber

messages

As the name suggests, this table will store messages with properties such as (text, image, video, etc.), , and timestamps for message delivery. The message will also have a corresponding or .

type
content
chatID
groupID

chats

This table basically represents a private chat between two users and can contain multiple messages.

users_chats

This table maps users and chats as multiple users can have multiple chats (N:M relationship) and vice versa.

groups

This table represents a group made up of multiple users.

users_groups

This table maps users and groups as multiple users can be a part of multiple groups (N:M relationship) and vice versa.

What kind of database should we use?

While our data model seems quite relational, we don't necessarily need to store everything in a single database, as this can limit our scalability and quickly become a bottleneck.

We will split the data between different services each having ownership over a particular table. Then we can use a relational database such as PostgreSQL or a distributed NoSQL database such as Apache Cassandra for our use case.

API design

Let us do a basic API design for our services:

Get all chats or groups

This API will get all chats or groups for a given .

userID

getAll(userID: UUID): Chat[] | Group[]

Parameters

User ID (): ID of the current user.

UUID

Returns

Result (): All the chats and groups the user is a part of.

Chat[] | Group[]

Get messages

Get all messages for a user given the (chat or group id).

channelID

getMessages(userID: UUID, channelID: UUID): Message[]

Parameters

User ID (): ID of the current user.

UUID

Channel ID (): ID of the channel (chat or group) from which messages need to be retrieved.

UUID

Returns

Messages (): All the messages in a given chat or group.

Message[]

Send message

Send a message from a user to a channel (chat or group).

sendMessage(userID: UUID, channelID: UUID, message: Message): boolean

Parameters

User ID (): ID of the current user.

UUID

Channel ID (): ID of the channel (chat or group) user wants to send a message to.

UUID

Message (): The message (text, image, video, etc.) that the user wants to send.

Message

Returns

Result (): Represents whether the operation was successful or not.

boolean

Join or leave a group

Send a message from a user to a channel (chat or group).

joinGroup(userID: UUID, channelID: UUID): boolean
leaveGroup(userID: UUID, channelID: UUID): boolean

Parameters

User ID (): ID of the current user.

UUID

Channel ID (): ID of the channel (chat or group) the user wants to join or leave.

UUID

Returns

Result (): Represents whether the operation was successful or not.

boolean

High-level design

Now let us do a high-level design of our system.

Architecture

We will be using microservices architecture since it will make it easier to horizontally scale and decouple our services. Each service will have ownership of its own data model. Let's try to divide our system into some core services.

User Service

This is an HTTP-based service that handles user-related concerns such as authentication and user information.

Chat Service

The chat service will use WebSockets and establish connections with the client to handle chat and group message-related functionality. We can also use cache to keep track of all the active connections sort of like sessions which will help us determine if the user is online or not.

Notification Service

This service will simply send push notifications to the users. It will be discussed in detail separately.

Presence Service

The presence service will keep track of the last seen status of all users. It will be discussed in detail separately.

Media service

This service will handle the media (images, videos, files, etc.) uploads. It will be discussed in detail separately.

What about inter-service communication and service discovery?

Since our architecture is microservices-based, services will be communicating with each other as well. Generally, REST or HTTP performs well but we can further improve the performance using gRPC which is more lightweight and efficient.

Service discovery is another thing we will have to take into account. We can also use a service mesh that enables managed, observable, and secure communication between individual services.

Note: Learn more about REST, GraphQL, gRPC and how they compare with each other.

Real-time messaging

How do we efficiently send and receive messages? We have two different options:

Pull model

The client can periodically send an HTTP request to servers to check if there are any new messages. This can be achieved via something like Long polling.

Push model

The client opens a long-lived connection with the server and once new data is available it will be pushed to the client. We can use WebSockets or Server-Sent Events (SSE) for this.

The pull model approach is not scalable as it will create unnecessary request overhead on our servers and most of the time the response will be empty, thus wasting our resources. To minimize latency, using the push model with WebSockets is a better choice because then we can push data to the client once it's available without any delay, given the connection is open with the client. Also, WebSockets provide full-duplex communication, unlike Server-Sent Events (SSE) which are only unidirectional.

Note: Learn more about Long polling, WebSockets, Server-Sent Events (SSE).

Last seen

To implement the last seen functionality, we can use a heartbeat mechanism, where the client can periodically ping the servers indicating its liveness. Since this needs to be as low overhead as possible, we can store the last active timestamp in the cache as follows:

Key Value
User A 2022-07-01T14:32:50
User B 2022-07-05T05:10:35
User C 2022-07-10T04:33:25

This will give us the last time the user was active. This functionality will be handled by the presence service combined with Redis or Memcached as our cache.

Another way to implement this is to track the latest action of the user, once the last activity crosses a certain threshold, such as "user hasn't performed any action in the last 30 seconds", we can show the user as offline and last seen with the last recorded timestamp. This will be more of a lazy update approach and might benefit us over heartbeat in certain cases.

Notifications

Once a message is sent in a chat or a group, we will first check if the recipient is active or not, we can get this information by taking the user's active connection and last seen into consideration.

If the recipient is not active, the chat service will add an event to a message queue with additional metadata such as the client's device platform which will be used to route the notification to the correct platform later on.

The notification service will then consume the event from the message queue and forward the request to Firebase Cloud Messaging (FCM) or Apple Push Notification Service (APNS) based on the client's device platform (Android, iOS, web, etc). We can also add support for email and SMS.

Why are we using a message queue?

Since most message queues provide best-effort ordering which ensures that messages are generally delivered in the same order as they're sent and that a message is delivered at least once which is an important part of our service functionality.

While this seems like a classic publish-subscribe use case, it is actually not as mobile devices and browsers each have their own way of handling push notifications. Usually, notifications are handled externally via Firebase Cloud Messaging (FCM) or Apple Push Notification Service (APNS) unlike message fan-out which we commonly see in backend services. We can use something like Amazon SQS or RabbitMQ to support this functionality.

Read receipts

Handling read receipts can be tricky, for this use case we can wait for some sort of Acknowledgment (ACK) from the client to determine if the message was delivered and update the corresponding field. Similarly, we will mark the message as seen once the user opens the chat and update the corresponding timestamp field.

deliveredAt
seenAt

Design

Now that we have identified some core components, let's do the first draft of our system design.

whatsapp-basic-design

Detailed design

It's time to discuss our design decisions in detail.

Data Partitioning

To scale out our databases we will need to partition our data. Horizontal partitioning (aka Sharding) can be a good first step. We can use partitions schemes such as:

  • Hash-Based Partitioning
  • List-Based Partitioning
  • Range Based Partitioning
  • Composite Partitioning

The above approaches can still cause uneven data and load distribution, we can solve this using Consistent hashing.

For more details, refer to Sharding and Consistent Hashing.

Caching

In a messaging application, we have to be careful about using cache as our users expect the latest data, but many users will be requesting the same messages, especially in a group chat. So, to prevent usage spikes from our resources we can cache older messages.

Some group chats can have thousands of messages and sending that over the network will be really inefficient, to improve efficiency we can add pagination to our system APIs. This decision will be helpful for users with limited network bandwidth as they won't have to retrieve old messages unless requested.

Which cache eviction policy to use?

We can use solutions like Redis or Memcached and cache 20% of the daily traffic but what kind of cache eviction policy would best fit our needs?

Least Recently Used (LRU) can be a good policy for our system. In this policy, we discard the least recently used key first.

How to handle cache miss?

Whenever there is a cache miss, our servers can hit the database directly and update the cache with the new entries.

For more details, refer to Caching.

Media access and storage

As we know, most of our storage space will be used for storing media files such as images, videos, or other files. Our media service will be handling both access and storage of the user media files.

But where can we store files at scale? Well, object storage is what we're looking for. Object stores break data files up into pieces called objects. It then stores those objects in a single repository, which can be spread out across multiple networked systems. We can also use distributed file storage such as HDFS or GlusterFS.

Fun fact: WhatsApp deletes media on its servers once it has been downloaded by the user.

We can use object stores like Amazon S3, Azure Blob Storage, or Google Cloud Storage for this use case.

Content Delivery Network (CDN)

Content Delivery Network (CDN) increases content availability and redundancy while reducing bandwidth costs. Generally, static files such as images, and videos are served from CDN. We can use services like Amazon CloudFront or Cloudflare CDN for this use case.

API gateway

Since we will be using multiple protocols like HTTP, WebSocket, TCP/IP, deploying multiple L4 (transport layer) or L7 (application layer) type load balancers separately for each protocol will be expensive. Instead, we can use an API Gateway that supports multiple protocols without any issues.

API Gateway can also offer other features such as authentication, authorization, rate limiting, throttling, and API versioning which will improve the quality of our services.

We can use services like Amazon API Gateway or Azure API Gateway for this use case.

Identify and resolve bottlenecks

whatsapp-advanced-design

Let us identify and resolve bottlenecks such as single points of failure in our design:

  • "What if one of our services crashes?"
  • "How will we distribute our traffic between our components?"
  • "How can we reduce the load on our database?"
  • "How to improve the availability of our cache?"
  • "Wouldn't API Gateway be a single point of failure?"
  • "How can we make our notification system more robust?"
  • "How can we reduce media storage costs"?
  • "Does chat service has too much responsibility?"

To make our system more resilient we can do the following:

  • Running multiple instances of each of our services.
  • Introducing load balancers between clients, servers, databases, and cache servers.
  • Using multiple read replicas for our databases.
  • Multiple instances and replicas for our distributed cache.
  • We can have a standby replica of our API Gateway.
  • Exactly once delivery and message ordering is challenging in a distributed system, we can use a dedicated message broker such as Apache Kafka or NATS to make our notification system more robust.
  • We can add media processing and compression capabilities to the media service to compress large files similar to WhatsApp which will save a lot of storage space and reduce cost.
  • We can create a group service separate from the chat service to further decouple our services.

Twitter

Let's design a Twitter like social media service, similar to services like Facebook, Instagram, etc.

What is Twitter?

Twitter is a social media service where users can read or post short messages (up to 280 characters) called tweets. It is available on the web and mobile platforms such as Android and iOS.

Requirements

Our system should meet the following requirements:

Functional requirements

  • Should be able to post new tweets (can be text, image, video, etc.).
  • Should be able to follow other users.
  • Should have a newsfeed feature consisting of tweets from the people the user is following.
  • Should be able to search tweets.

Non-Functional requirements

  • High availability with minimal latency.
  • The system should be scalable and efficient.

Extended requirements

  • Metrics and analytics.
  • Retweet functionality.
  • Favorite tweets.

Estimation and Constraints

Let's start with the estimation and constraints.

Note: Make sure to check any scale or traffic-related assumptions with your interviewer.

Traffic

This will be a read-heavy system, let us assume we have 1 billion total users with 200 million daily active users (DAU), and on average each user tweets 5 times a day. This gives us 1 billion tweets per day.

$$ 200 \space million \times 5 \space tweets = 1 \space billion/day $$

Tweets can also contain media such as images, or videos. We can assume that 10 percent of tweets are media files shared by the users, which gives us additional 100 million files we would need to store.

$$ 10 \space percent \times 1 \space billion = 100 \space million/day $$

What would be Requests Per Second (RPS) for our system?

1 billion requests per day translate into 12K requests per second.

$$ \frac{1 \space billion}{(24 \space hrs \times 3600 \space seconds)} = \sim 12K \space requests/second $$

Storage

If we assume each message on average is 100 bytes, we will require about 100 GB of database storage every day.

$$ 1 \space billion \times 100 \space bytes = \sim 100 \space GB/day $$

We also know that around 10 percent of our daily messages (100 million) are media files per our requirements. If we assume each file is 50 KB on average, we will require 5 TB of storage every day.

$$ 100 \space million \times 100 \space KB = 5 \space TB/day $$

And for 10 years, we will require about 19 PB of storage.

$$ (5 \space TB + 0.1 \space TB) \times 365 \space days \times 10 \space years = \sim 19 \space PB $$

Bandwidth

As our system is handling 5.1 TB of ingress every day, we will require a minimum bandwidth of around 60 MB per second.

$$ \frac{5.1 \space TB}{(24 \space hrs \times 3600 \space seconds)} = \sim 60 \space MB/second $$

High-level estimate

Here is our high-level estimate:

Type Estimate
Daily active users (DAU) 100 million
Requests per second (RPS) 12K/s
Storage (per day) ~5.1 TB
Storage (10 years) ~19 PB
Bandwidth ~60 MB/s

Data model design

This is the general data model which reflects our requirements.

twitter-datamodel

We have the following tables:

users

This table will contain a user's information such as , , , and other details.

name
email
dob

tweets

As the name suggests, this table will store tweets and their properties such as (text, image, video, etc.), , etc. We will also store the corresponding .

type
content
userID

favorites

This table maps tweets with users for the favorite tweets functionality in our application.

followers

This table maps the followers and followees as users can follow each other (N:M relationship).

feeds

This table stores feed properties with the corresponding .

userID

feeds_tweets

This table maps tweets and feed (N:M relationship).

What kind of database should we use?

While our data model seems quite relational, we don't necessarily need to store everything in a single database, as this can limit our scalability and quickly become a bottleneck.

We will split the data between different services each having ownership over a particular table. Then we can use a relational database such as PostgreSQL or a distributed NoSQL database such as Apache Cassandra for our use case.

API design

Let us do a basic API design for our services:

Post a tweet

This API will allow the user to post a tweet on the platform.

postTweet(userID: UUID, content: string, mediaURL?: string): boolean

Parameters

User ID (): ID of the user.

UUID

Content (): Contents of the tweet.

string

Media URL (): URL of the attached media (optional).

string

Returns

Result (): Represents whether the operation was successful or not.

boolean

Follow or unfollow a user

This API will allow the user to follow or unfollow another user.

follow(followerID: UUID, followeeID: UUID): boolean
unfollow(followerID: UUID, followeeID: UUID): boolean

Parameters

Follower ID (): ID of the current user.

UUID

Followee ID (): ID of the user we want to follow or unfollow.

UUID

Media URL (): URL of the attached media (optional).

string

Returns

Result (): Represents whether the operation was successful or not.

boolean

Get newsfeed

This API will return all the tweets to be shown within a given newsfeed.

getNewsfeed(userID: UUID): Tweet[]

Parameters

User ID (): ID of the user.

UUID

Returns

Tweets (): All the tweets to be shown within a given newsfeed.

Tweet[]

High-level design

Now let us do a high-level design of our system.

Architecture

We will be using microservices architecture since it will make it easier to horizontally scale and decouple our services. Each service will have ownership of its own data model. Let's try to divide our system into some core services.

User Service

This service handles user-related concerns such as authentication and user information.

Newsfeed Service

This service will handle the generation and publishing of user newsfeeds. It will be discussed in detail separately.

Tweet Service

The tweet service will handle tweet-related use cases such as posting a tweet, favorites, etc.

Search Service

The service is responsible for handling search-related functionality. It will be discussed in detail separately.

Media service

This service will handle the media (images, videos, files, etc.) uploads. It will be discussed in detail separately.

Notification Service

This service will simply send push notifications to the users.

Analytics Service

This service will be used for metrics and analytics use cases.

What about inter-service communication and service discovery?

Since our architecture is microservices-based, services will be communicating with each other as well. Generally, REST or HTTP performs well but we can further improve the performance using gRPC which is more lightweight and efficient.

Service discovery is another thing we will have to take into account. We can also use a service mesh that enables managed, observable, and secure communication between individual services.

Note: Learn more about REST, GraphQL, gRPC and how they compare with each other.

Newsfeed

When it comes to the newsfeed, it seems easy enough to implement, but there are a lot of things that can make or break this feature. So, let's divide our problem into two parts:

Generation

Let's assume we want to generate the feed for user A, we will perform the following steps:

  1. Retrieve the IDs of all the users and entities (hashtags, topics, etc.) user A follows.
  2. Fetch the relevant tweets for each of the retrieved IDs.
  3. Use a ranking algorithm to rank the tweets based on parameters such as relevance, time, engagement, etc.
  4. Return the ranked tweets data to the client in a paginated manner.

Feed generation is an intensive process and can take quite a lot of time, especially for users following a lot of people. To improve the performance, the feed can be pre-generated and stored in the cache, then we can have a mechanism to periodically update the feed and apply our ranking algorithm to the new tweets.

Publishing

Publishing is the step where the feed data is pushed according to each specific user. This can be a quite heavy operation, as a user may have millions of friends or followers. To deal with this, we have three different approaches:

  • Pull Model (or Fan-out on load)

newsfeed-pull-model

When a user creates a tweet, and a follower reloads their newsfeed, the feed is created and stored in memory. The most recent feed is only loaded when the user requests it. This approach reduces the number of write operations on our database.

The downside of this approach is that the users will not be able to view recent feeds unless they "pull" the data from the server, which will increase the number of read operations on the server.

  • Push Model (or Fan-out on write)

newsfeed-push-model

In this model, once a user creates a tweet, it is "pushed" to all the follower's feeds immediately. This prevents the system from having to go through a user's entire followers list to check for updates.

However, the downside of this approach is that it would increase the number of write operations on the database.

  • Hybrid Model

A third approach is a hybrid model between the pull and push model. It combines the beneficial features of the above two models and tries to provide a balanced approach between the two.

The hybrid model allows only users with a lesser number of followers to use the push model. For users with a higher number of followers such as celebrities, the pull model is used.

Ranking Algorithm

As we discussed, we will need a ranking algorithm to rank each tweet according to its relevance to each specific user.

For example, Facebook used to utilize an EdgeRank algorithm. Here, the rank of each feed item is described by:

$$ Rank = Affinity \times Weight \times Decay $$

Where,

Affinity
: is the "closeness" of the user to the creator of the edge. If a user frequently likes, comments, or messages the edge creator, then the value of affinity will be higher, resulting in a higher rank for the post.

Weight
: is the value assigned according to each edge. A comment can have a higher weightage than likes, and thus a post with more comments is more likely to get a higher rank.

Decay
: is the measure of the creation of the edge. The older the edge, the lesser will be the value of decay and eventually the rank.

Nowadays, algorithms are much more complex and ranking is done using machine learning models which can take thousands of factors into consideration.

Retweets

Retweets are one of our extended requirements. To implement this feature, we can simply create a new tweet with the user id of the user retweeting the original tweet and then modify the enum and property of the new tweet to link it with the original tweet.

type
content

For example, the enum property can be of type tweet, similar to text, video, etc and can be the id of the original tweet. Here the first row indicates the original tweet while the second row is how we can represent a retweet.

type
content

id userID type content createdAt
ad34-291a-45f6-b36c 7a2c-62c4-4dc8-b1bb text Hey, this is my first tweet… 1658905644054
f064-49ad-9aa2-84a6 6aa2-2bc9-4331-879f tweet ad34-291a-45f6-b36c 1658906165427

This is a very basic implementation. To improve this we can create a separate table itself to store retweets.

Search

Sometimes traditional DBMS are not performant enough, we need something which allows us to store, search, and analyze huge volumes of data quickly and in near real-time and give results within milliseconds. Elasticsearch can help us with this use case.

Elasticsearch is a distributed, free and open search and analytics engine for all types of data, including textual, numerical, geospatial, structured, and unstructured. It is built on top of Apache Lucene.

How do we identify trending topics?

Trending functionality will be based on top of the search functionality. We can cache the most frequently searched queries, hashtags, and topics in the last seconds and update them every seconds using some sort of batch job mechanism. Our ranking algorithm can also be applied to the trending topics to give them more weight and personalize them for the user.

N
M

Notifications

Push notifications are an integral part of any social media platform. We can use a message queue or a message broker such as Apache Kafka with the notification service to dispatch requests to Firebase Cloud Messaging (FCM) or Apple Push Notification Service (APNS) which will handle the delivery of the push notifications to user devices.

For more details, refer to the WhatsApp system design where we discuss push notifications in detail.

Detailed design

It's time to discuss our design decisions in detail.

Data Partitioning

To scale out our databases we will need to partition our data. Horizontal partitioning (aka Sharding) can be a good first step. We can use partitions schemes such as:

  • Hash-Based Partitioning
  • List-Based Partitioning
  • Range Based Partitioning
  • Composite Partitioning

The above approaches can still cause uneven data and load distribution, we can solve this using Consistent hashing.

For more details, refer to Sharding and Consistent Hashing.

Mutual friends

For mutual friends, we can build a social graph for every user. Each node in the graph will represent a user and a directional edge will represent followers and followees. After that, we can traverse the followers of a user to find and suggest a mutual friend. This would require a graph database such as Neo4j and ArangoDB.

This is a pretty simple algorithm, to improve our suggestion accuracy, we will need to incorporate a recommendation model which uses machine learning as part of our algorithm.

Metrics and Analytics

Recording analytics and metrics is one of our extended requirements. As we will be using Apache Kafka to publish all sorts of events, we can process these events and run analytics on the data using Apache Spark which is an open-source unified analytics engine for large-scale data processing.

Caching

In a social media application, we have to be careful about using cache as our users expect the latest data. So, to prevent usage spikes from our resources we can cache the top 20% of the tweets.

To further improve efficiency we can add pagination to our system APIs. This decision will be helpful for users with limited network bandwidth as they won't have to retrieve old messages unless requested.

Which cache eviction policy to use?

We can use solutions like Redis or Memcached and cache 20% of the daily traffic but what kind of cache eviction policy would best fit our needs?

Least Recently Used (LRU) can be a good policy for our system. In this policy, we discard the least recently used key first.

How to handle cache miss?

Whenever there is a cache miss, our servers can hit the database directly and update the cache with the new entries.

For more details, refer to Caching.

Media access and storage

As we know, most of our storage space will be used for storing media files such as images, videos, or other files. Our media service will be handling both access and storage of the user media files.

But where can we store files at scale? Well, object storage is what we're looking for. Object stores break data files up into pieces called objects. It then stores those objects in a single repository, which can be spread out across multiple networked systems. We can also use distributed file storage such as HDFS or GlusterFS.

Content Delivery Network (CDN)

Content Delivery Network (CDN) increases content availability and redundancy while reducing bandwidth costs. Generally, static files such as images, and videos are served from CDN. We can use services like Amazon CloudFront or Cloudflare CDN for this use case.

Identify and resolve bottlenecks

twitter-advanced-design

Let us identify and resolve bottlenecks such as single points of failure in our design:

  • "What if one of our services crashes?"
  • "How will we distribute our traffic between our components?"
  • "How can we reduce the load on our database?"
  • "How to improve the availability of our cache?"
  • "How can we make our notification system more robust?"
  • "How can we reduce media storage costs"?

To make our system more resilient we can do the following:

  • Running multiple instances of each of our services.
  • Introducing load balancers between clients, servers, databases, and cache servers.
  • Using multiple read replicas for our databases.
  • Multiple instances and replicas for our distributed cache.
  • Exactly once delivery and message ordering is challenging in a distributed system, we can use a dedicated message broker such as Apache Kafka or NATS to make our notification system more robust.
  • We can add media processing and compression capabilities to the media service to compress large files which will save a lot of storage space and reduce cost.

Netflix

Let's design a Netflix like video streaming service, similar to services like Amazon Prime Video, Disney Plus, Hulu, Youtube, Vimeo, etc.

What is Netflix?

Netflix is a subscription-based streaming service that allows its members to watch TV shows and movies on an internet-connected device. It is available on platforms such as the Web, iOS, Android, TV, etc.

Requirements

Our system should meet the following requirements:

Functional requirements

  • Users should be able to stream and share videos.
  • The content team (or users in YouTube's case) should be able to upload new videos (movies, tv shows episodes, and other content).
  • Users should be able to search for videos using titles or tags.
  • Users should be able to comment on a video similar to YouTube.

Non-Functional requirements

  • High availability with minimal latency.
  • High reliability, no uploads should be lost.
  • The system should be scalable and efficient.

Extended requirements

  • Certain content should be geo-blocked.
  • Resume video playback from the point user left off.
  • Record metrics and analytics of videos.

Estimation and Constraints

Let's start with the estimation and constraints.

Note: Make sure to check any scale or traffic-related assumptions with your interviewer.

Traffic

This will be a read-heavy system, let us assume we have 1 billion total users with 200 million daily active users (DAU), and on average each user watches 5 videos a day. This gives us 1 billion videos watched per day.

$$ 200 \space million \times 5 \space videos = 1 \space billion/day $$

Assuming a read/write ratio, about 50 million videos will be uploaded every day.

200:1

$$ \frac{1}{200} \times 1 \space billion = 50 \space million/day $$

What would be Requests Per Second (RPS) for our system?

1 billion requests per day translate into 12K requests per second.

$$ \frac{1 \space billion}{(24 \space hrs \times 3600 \space seconds)} = \sim 12K \space requests/second $$

Storage

If we assume each video is 100 MB on average, we will require about 5 PB of storage every day.

$$ 50 \space million \times 100 \space MB = 5 \space PB/day $$

And for 10 years, we will require an astounding 18,250 PB of storage.

$$ 5 \space PB \times 365 \space days \times 10 \space years = \sim 18,250 \space PB $$

Bandwidth

As our system is handling 5 PB of ingress every day, we will require a minimum bandwidth of around 58 GB per second.

$$ \frac{5 \space PB}{(24 \space hrs \times 3600 \space seconds)} = \sim 58 \space GB/second $$

High-level estimate

Here is our high-level estimate:

Type Estimate
Daily active users (DAU) 200 million
Requests per second (RPS) 12K/s
Storage (per day) ~5 PB
Storage (10 years) ~18,250 PB
Bandwidth ~58 GB/s

Data model design

This is the general data model which reflects our requirements.

netflix-datamodel

We have the following tables:

users

This table will contain a user's information such as , , , and other details.

name
email
dob

videos

As the name suggests, this table will store videos and their properties such as , , , etc. We will also store the corresponding .

title
streamURL
tags
userID

tags

This table will simply store tags associated with a video.

views

This table helps us to store all the views received on a video.

comments

This table stores all the comments received on a video (like YouTube).

What kind of database should we use?

While our data model seems quite relational, we don't necessarily need to store everything in a single database, as this can limit our scalability and quickly become a bottleneck.

We will split the data between different services each having ownership over a particular table. Then we can use a relational database such as PostgreSQL or a distributed NoSQL database such as Apache Cassandra for our use case.

API design

Let us do a basic API design for our services:

Upload a video

Given a byte stream, this API enables video to be uploaded to our service.

uploadVideo(title: string, description: string, data: Stream<byte>, tags?: string[]): boolean

Parameters

Title (): Title of the new video.

string

Description (): Description of the new video.

string

Data (): Byte stream of the video data.

Byte[]

Tags (): Tags for the video (optional).

string[]

Returns

Result (): Represents whether the operation was successful or not.

boolean

Streaming a video

This API allows our users to stream a video with the preferred codec and resolution.

streamVideo(videoID: UUID, codec: Enum<string>, resolution: Tuple<int>, offset?: int): VideoStream

Parameters

Video ID (): ID of the video that needs to be streamed.

UUID

Codec (): Required codec of the requested video, such as , , , etc.

Enum<string>
h.265
h.264
VP9

Resolution (): Resolution of the requested video.

Tuple<int>

Offset (): Offset of the video stream in seconds to stream data from any point in the video (optional).

int

Returns

Stream (): Data stream of the requested video.

VideoStream

Search for a video

This API will enable our users to search for a video based on its title or tags.

searchVideo(query: string, nextPage?: string): Video[]

Parameters

Query (): Search query from the user.

string

Next Page (): Token for the next page, this can be used for pagination (optional).

string

Returns

Videos (): All the videos available for a particular search query.

Video[]

Add a comment

This API will allow our users to post a comment on a video (like YouTube).

comment(videoID: UUID, comment: string): boolean

Parameters

VideoID (): ID of the video user wants to comment on.

UUID

Comment (): The text content of the comment.

string

Returns

Result (): Represents whether the operation was successful or not.

boolean

High-level design

Now let us do a high-level design of our system.

Architecture

We will be using microservices architecture since it will make it easier to horizontally scale and decouple our services. Each service will have ownership of its own data model. Let's try to divide our system into some core services.

User Service

This service handles user-related concerns such as authentication and user information.

Stream Service

The tweet service will handle video streaming-related functionality.

Search Service

The service is responsible for handling search-related functionality. It will be discussed in detail separately.

Media service

This service will handle the video uploads and processing. It will be discussed in detail separately.

Analytics Service

This service will be used for metrics and analytics use cases.

What about inter-service communication and service discovery?

Since our architecture is microservices-based, services will be communicating with each other as well. Generally, REST or HTTP performs well but we can further improve the performance using gRPC which is more lightweight and efficient.

Service discovery is another thing we will have to take into account. We can also use a service mesh that enables managed, observable, and secure communication between individual services.

Note: Learn more about REST, GraphQL, gRPC and how they compare with each other.

Video processing

video-processing-pipeline

There are so many variables in play when it comes to processing a video. For example, an average data size of two-hour raw 8K footage from a high-end camera can easily be up to 4 TB, thus we need to have some kind of processing to reduce both storage and delivery costs.

Here's how we can process videos once they're uploaded by the content team (or users in YouTube's case) and are queued for processing in our message queue.

Let's discuss how this works:

  • File Chunker

file-chunking

This is the first step of our processing pipeline. File chunking is the process of splitting a file into smaller pieces called chunks. It can help us eliminate duplicate copies of repeating data on storage, and reduces the amount of data sent over the network by only selecting changed chunks.

Usually, a video file can be split into equal size chunks based on timestamps but Netflix instead splits chunks based on scenes. This slight variation becomes a huge factor for a better user experience since whenever the client requests a chunk from the server, there is a lower chance of interruption as a complete scene will be retrieved.

  • Content Filter

This step checks if the video adheres to the content policy of the platform. This can be pre-approved as in the case of Netflix according to content rating of the media or can be strictly enforced like by YouTube.

This entire process is done by a machine learning model which performs copyright, piracy, and NSFW checks. If issues are found, we can push the task to a dead-letter queue (DLQ) and someone from the moderation team can do further inspection.

  • Transcoder

Transcoding is a process in which the original data is decoded to an intermediate uncompressed format, which is then encoded into the target format. This process uses different codecs to perform bitrate adjustment, image downsampling, or re-encoding the media.

This results in a smaller size file and a much more optimized format for the target devices. Standalone solutions such as FFmpeg or cloud-based solutions like AWS Elemental MediaConvert can be used to implement this step of the pipeline.

  • Quality Conversion

This is the last step of the processing pipeline and as the name suggests, this step handles the conversion of the transcoded media from the previous step into different resolutions such as 4K, 1440p, 1080p, 720p, etc.

It allows us to fetch the desired quality of the video as per the user's request, and once the media file finishes processing, it gets uploaded to a distributed file storage such as HDFS, GlusterFS, or an object storage such as Amazon S3 for later retrieval during streaming.

Note: We can add additional steps such as subtitles and thumbnails generation as part of our pipeline.

Why are we using a message queue?

Processing videos as a long-running task and using a message queue makes much more sense. It also decouples our video processing pipeline from the upload functionality. We can use something like Amazon SQS or RabbitMQ to support this.

Video streaming

Video streaming is a challenging task from both the client and server perspectives. Moreover, internet connection speeds vary quite a lot between different users. To make sure users don't re-fetch the same content, we can use a Content Delivery Network (CDN).

Netflix takes this a step further with its Open Connect program. In this approach, they partner with thousands of Internet Service Providers (ISPs) to localize their traffic and deliver their content more efficiently.

What is the difference between Netflix's Open Connect and a traditional Content Delivery Network (CDN)?

Netflix Open Connect is a purpose-built Content Delivery Network (CDN) responsible for serving Netflix's video traffic. Around 95% of the traffic globally is delivered via direct connections between Open Connect and the ISPs their customers use to access the internet.

Currently, they have Open Connect Appliances (OCAs) in over 1000 separate locations around the world. In case of issues, Open Connect Appliances (OCAs) can failover, and the traffic can be re-routed to Netflix servers.

Additionally, we can use Adaptive bitrate streaming protocols such as HTTP Live Streaming (HLS) which is designed for reliability and it dynamically adapts to network conditions by optimizing playback for the available speed of the connections.

Lastly, for playing the video from where the user left off (part of our extended requirements), we can simply use the property we stored in the table to retrieve the scene chunk at that particular timestamp and resume the playback for the user.

offset
views

Searching

Sometimes traditional DBMS are not performant enough, we need something which allows us to store, search, and analyze huge volumes of data quickly and in near real-time and give results within milliseconds. Elasticsearch can help us with this use case.

Elasticsearch is a distributed, free and open search and analytics engine for all types of data, including textual, numerical, geospatial, structured, and unstructured. It is built on top of Apache Lucene.

How do we identify trending content?

Trending functionality will be based on top of the search functionality. We can cache the most frequently searched queries in the last seconds and update them every seconds using some sort of batch job mechanism.

N
M

Sharing

Sharing content is an important part of any platform, for this, we can have some sort of URL shortener service in place that can generate short URLs for the users to share.

For more details, refer to the URL Shortener system design.

Detailed design

It's time to discuss our design decisions in detail.

Data Partitioning

To scale out our databases we will need to partition our data. Horizontal partitioning (aka Sharding) can be a good first step. We can use partitions schemes such as:

  • Hash-Based Partitioning
  • List-Based Partitioning
  • Range Based Partitioning
  • Composite Partitioning

The above approaches can still cause uneven data and load distribution, we can solve this using Consistent hashing.

For more details, refer to Sharding and Consistent Hashing.

Geo-blocking

Platforms like Netflix and YouTube use Geo-blocking to restrict content in certain geographical areas or countries. This is primarily done due to legal distribution laws that Netflix has to adhere to when they make a deal with the production and distribution companies. In the case of YouTube, this will be controlled by the user during the publishing of the content.

We can determine the user's location either using their IP or region settings in their profile then use services like Amazon CloudFront which supports a geographic restrictions feature or a geolocation routing policy with Amazon Route53 to restrict the content and re-route the user to an error page if the content is not available in that particular region or country.

Recommendations

Netflix uses a machine learning model which uses the user's viewing history to predict what the user might like to watch next, an algorithm like Collaborative Filtering can be used.

However, Netflix (like YouTube) uses its own algorithm called Netflix Recommendation Engine which can track several data points such as:

  • User profile information like age, gender, and location.
  • Browsing and scrolling behavior of the user.
  • Time and date a user watched a title.
  • The device which was used to stream the content.
  • The number of searches and what terms were searched.

For more detail, refer to Netflix recommendation research.

Metrics and Analytics

Recording analytics and metrics is one of our extended requirements. We can capture the data from different services and run analytics on the data using Apache Spark which is an open-source unified analytics engine for large-scale data processing. Additionally, we can store critical metadata in the views table to increase data points within our data.

Caching

In a streaming platform, caching is important. We have to be able to cache as much static media content as possible to improve user experience. We can use solutions like Redis or Memcached but what kind of cache eviction policy would best fit our needs?

Which cache eviction policy to use?

Least Recently Used (LRU) can be a good policy for our system. In this policy, we discard the least recently used key first.

How to handle cache miss?

Whenever there is a cache miss, our servers can hit the database directly and update the cache with the new entries.

For more details, refer to Caching.

Media streaming and storage

As most of our storage space will be used for storing media files such as thumbnails and videos. Per our discussion earlier, the media service will be handling both the upload and processing of media files.

We will use distributed file storage such as HDFS, GlusterFS, or an object storage such as Amazon S3 for storage and streaming of the content.

Content Delivery Network (CDN)

Content Delivery Network (CDN) increases content availability and redundancy while reducing bandwidth costs. Generally, static files such as images, and videos are served from CDN. We can use services like Amazon CloudFront or Cloudflare CDN for this use case.

Identify and resolve bottlenecks

netflix-advanced-design

Let us identify and resolve bottlenecks such as single points of failure in our design:

  • "What if one of our services crashes?"
  • "How will we distribute our traffic between our components?"
  • "How can we reduce the load on our database?"
  • "How to improve the availability of our cache?"

To make our system more resilient we can do the following:

  • Running multiple instances of each of our services.
  • Introducing load balancers between clients, servers, databases, and cache servers.
  • Using multiple read replicas for our databases.
  • Multiple instances and replicas for our distributed cache.

Uber

Let's design an Uber like ride-hailing service, similar to services like Lyft, OLA Cabs, etc.

What is Uber?

Uber is a mobility service provider, allowing users to book rides and a driver to transport them in a way similar to a taxi. It is available on the web and mobile platforms such as Android and iOS.

Requirements

Our system should meet the following requirements:

Functional requirements

We will design our system for two types of users: Customers and Drivers.

Customers

  • Customers should be able to see all the cabs in the vicinity with an ETA and pricing information.
  • Customers should be able to book a cab to a destination.
  • Customers should be able to see the location of the driver.

Drivers

  • Drivers should be able to accept or deny the customer-requested ride.
  • Once a driver accepts the ride, they should see the pickup location of the customer.
  • Drivers should be able to mark the trip as complete on reaching the destination.

Non-Functional requirements

  • High reliability.
  • High availability with minimal latency.
  • The system should be scalable and efficient.

Extended requirements

  • Customers can rate the trip after it's completed.
  • Payment processing.
  • Metrics and analytics.

Estimation and Constraints

Let's start with the estimation and constraints.

Note: Make sure to check any scale or traffic-related assumptions with your interviewer.

Traffic

Let us assume we have 100 million daily active users (DAU) with 1 million drivers and on average our platform enables 10 million rides daily.

If on average each user performs 10 actions (such as request a check available rides, fares, book rides, etc.) we will have to handle 1 billion requests daily.

$$ 100 \space million \times 10 \space actions = 1 \space billion/day $$

What would be Requests Per Second (RPS) for our system?

1 billion requests per day translate into 12K requests per second.

$$ \frac{1 \space billion}{(24 \space hrs \times 3600 \space seconds)} = \sim 12K \space requests/second $$

Storage

If we assume each message on average is 400 bytes, we will require about 400 GB of database storage every day.

$$ 1 \space billion \times 400 \space bytes = \sim 400 \space GB/day $$

And for 10 years, we will require about 1.4 PB of storage.

$$ 400 \space GB \times 10 \space years \times 365 \space days = \sim 1.4 \space PB $$

Bandwidth

As our system is handling 400 GB of ingress every day, we will require a minimum bandwidth of around 4 MB per second.

$$ \frac{400 \space GB}{(24 \space hrs \times 3600 \space seconds)} = \sim 5 \space MB/second $$

High-level estimate

Here is our high-level estimate:

Type Estimate
Daily active users (DAU) 100 million
Requests per second (RPS) 12K/s
Storage (per day) ~400 GB
Storage (10 years) ~1.4 PB
Bandwidth ~5 MB/s

Data model design

This is the general data model which reflects our requirements.

uber-datamodel

We have the following tables:

customers

This table will contain a customer's information such as , , and other details.

name
email

drivers

This table will contain a driver's information such as , , and other details.

name
email
dob

trips

This table represents the trip taken by the customer and stores data such as , , and of the trip.

source
destination
status

cabs

This table stores data such as the registration number, and type (like Uber Go, Uber XL, etc.) of the cab that the driver will be driving.

ratings

As the name suggests, this table stores the and for the trip.

rating
feedback

payments

The payments table contains the payment-related data with the corresponding .

tripID

What kind of database should we use?

While our data model seems quite relational, we don't necessarily need to store everything in a single database, as this can limit our scalability and quickly become a bottleneck.

We will split the data between different services each having ownership over a particular table. Then we can use a relational database such as PostgreSQL or a distributed NoSQL database such as Apache Cassandra for our use case.

API design

Let us do a basic API design for our services:

Request a Ride

Through this API, customers will be able to request a ride.

requestRide(customerID: UUID, source: Tuple<float>, destination: Tuple<float>, cabType: Enum<string>, paymentMethod: Enum<string>): Ride

Parameters

Customer ID (): ID of the customer.

UUID

Source (): Tuple containing the latitude and longitude of the trip's starting location.

Tuple<float>

Destination (): Tuple containing the latitude and longitude of the trip's destination.

Tuple<float>

Returns

Result (): Associated ride information of the trip.

Ride

Cancel the Ride

This API will allow customers to cancel the ride.

cancelRide(customerID: UUID, reason?: string): boolean

Parameters

Customer ID (): ID of the customer.

UUID

Reason (): Reason for canceling the ride (optional).

UUID

Returns

Result (): Represents whether the operation was successful or not.

boolean

Accept or Deny the Ride

This API will allow the driver to accept or deny the trip.

acceptRide(driverID: UUID, rideID: UUID): boolean
denyRide(driverID: UUID, rideID: UUID): boolean

Parameters

Driver ID (): ID of the driver.

UUID

Ride ID (): ID of the customer requested ride.

UUID

Returns

Result (): Represents whether the operation was successful or not.

boolean

Start or End the Trip

Using this API, a driver will be able to start and end the trip.

startTrip(driverID: UUID, tripID: UUID): boolean
endTrip(driverID: UUID, tripID: UUID): boolean

Parameters

Driver ID (): ID of the driver.

UUID

Trip ID (): ID of the requested trip.

UUID

Returns

Result (): Represents whether the operation was successful or not.

boolean

Rate the Trip

This API will enable customers to rate the trip.

rateTrip(customerID: UUID, tripID: UUID, rating: int, feedback?: string): boolean

Parameters

Customer ID (): ID of the customer.

UUID

Trip ID (): ID of the completed trip.

UUID

Rating (): Rating of the trip.

int

Feedback (): Feedback about the trip by the customer (optional).

string

Returns

Result (): Represents whether the operation was successful or not.

boolean

High-level design

Now let us do a high-level design of our system.

Architecture

We will be using microservices architecture since it will make it easier to horizontally scale and decouple our services. Each service will have ownership of its own data model. Let's try to divide our system into some core services.

Customer Service

This service handles customer-related concerns such as authentication and customer information.

Driver Service

This service handles driver-related concerns such as authentication and driver information.

Ride Service

This service will be responsible for ride matching and quadtree aggregation. It will be discussed in detail separately.

Trip Service

This service handles trip-related functionality in our system.

Payment Service

This service will be responsible for handling payments in our system.

Notification Service

This service will simply send push notifications to the users. It will be discussed in detail separately.

Analytics Service

This service will be used for metrics and analytics use cases.

What about inter-service communication and service discovery?

Since our architecture is microservices-based, services will be communicating with each other as well. Generally, REST or HTTP performs well but we can further improve the performance using gRPC which is more lightweight and efficient.

Service discovery is another thing we will have to take into account. We can also use a service mesh that enables managed, observable, and secure communication between individual services.

Note: Learn more about REST, GraphQL, gRPC and how they compare with each other.

How is the service expected to work?

Here's how our service is expected to work:

uber-working

  1. Customer requests a ride by specifying the source, destination, cab type, payment method, etc.
  2. Ride service registers this request, finds nearby drivers, and calculates the estimated time of arrival (ETA).
  3. The request is then broadcasted to the nearby drivers for them to accept or deny.
  4. If the driver accepts, the customer is notified about the live location of the driver with the estimated time of arrival (ETA) while they wait for pickup.
  5. The customer is picked up and the driver can start the trip.
  6. Once the destination is reached, the driver will mark the ride as complete and collect payment.
  7. After the payment is complete, the customer can leave a rating and feedback for the trip if they like.

Location Tracking

How do we efficiently send and receive live location data from the client (customers and drivers) to our backend? We have two different options:

Pull model

The client can periodically send an HTTP request to servers to report its current location and receive ETA and pricing information. This can be achieved via something like Long polling.

Push model

The client opens a long-lived connection with the server and once new data is available it will be pushed to the client. We can use WebSockets or Server-Sent Events (SSE) for this.

The pull model approach is not scalable as it will create unnecessary request overhead on our servers and most of the time the response will be empty, thus wasting our resources. To minimize latency, using the push model with WebSockets is a better choice because then we can push data to the client once it's available without any delay, given the connection is open with the client. Also, WebSockets provide full-duplex communication, unlike Server-Sent Events (SSE) which are only unidirectional.

Additionally, the client application should have some sort of background job mechanism to ping GPS location while the application is in the background.

Note: Learn more about Long polling, WebSockets, Server-Sent Events (SSE).

Ride Matching

We need a way to efficiently store and query nearby drivers. Let's explore different solutions we can incorporate into our design.

SQL

We already have access to the latitude and longitude of our customers, and with databases like PostgreSQL and MySQL we can perform a query to find nearby driver locations given a latitude and longitude (X, Y) within a radius (R).

SELECT * FROM locations WHERE lat BETWEEN X-R AND X+R AND long BETWEEN Y-R AND Y+R

However, this is not scalable, and performing this query on large datasets will be quite slow.

Geohashing

Geohashing is a geocoding method used to encode geographic coordinates such as latitude and longitude into short alphanumeric strings. It was created by Gustavo Niemeyer in 2008.

Geohash is a hierarchical spatial index that uses Base-32 alphabet encoding, the first character in a geohash identifies the initial location as one of the 32 cells. This cell will also contain 32 cells. This means that to represent a point, the world is recursively divided into smaller and smaller cells with each additional bit until the desired precision is attained. The precision factor also determines the size of the cell.

geohashing

For example, San Francisco with coordinates can be represented in geohash as .

37.7564, -122.4016
9q8yy9mf

Now, using the customer's geohash we can determine the nearest available driver by simply comparing it with the driver's geohash. For better performance, we will index and store the geohash of the driver in memory for faster retrieval.

Quadtrees

A Quadtree is a tree data structure in which each internal node has exactly four children. They are often used to partition a two-dimensional space by recursively subdividing it into four quadrants or regions. Each child or leaf node stores spatial information. Quadtrees are the two-dimensional analog of Octrees which are used to partition three-dimensional space.

quadtree

Quadtrees enable us to search points within a two-dimensional range efficiently, where those points are defined as latitude/longitude coordinates or as cartesian (x, y) coordinates.

We can save further computation by only subdividing a node after a certain threshold.

quadtree-subdivision

Quadtree seems perfect for our use case, we can update the Quadtree every time we receive a new location update from the driver. To reduce the load on the quadtree servers we can use an in-memory datastore such as Redis to cache the latest updates. And with the application of mapping algorithms such as the Hilbert curve, we can perform efficient range queries to find nearby drivers for the customer.

What about race conditions?

Race conditions can easily occur when a large number of customers will be requesting rides simultaneously. To avoid this, we can wrap our ride matching logic in a Mutex to avoid any race conditions. Furthermore, every action should be transactional in nature.

For more details, refer to Transactions and Distributed Transactions.

How to find the best drivers nearby?

Once we have a list of nearby drivers from the Quadtree servers, we can perform some sort of ranking based on parameters like average ratings, relevance, past customer feedback, etc. This will allow us to broadcast notifications to the best available drivers first.

Dealing with high demand

In cases of high demand, we can use the concept of Surge Pricing. Surge pricing is a dynamic pricing method where prices are temporarily increased as a reaction to increased demand and mostly limited supply. This surge price can be added to the base price of the trip.

For more details, learn how surge pricing works with Uber.

Payments

Handling payments at scale is challenging, to simplify our system we can use a third-party payment processor like Stripe or PayPal. Once the payment is complete, the payment processor will redirect the user back to our application and we can set up a webhook to capture all the payment-related data.

Notifications

Push notifications will be an integral part of our platform. We can use a message queue or a message broker such as Apache Kafka with the notification service to dispatch requests to Firebase Cloud Messaging (FCM) or Apple Push Notification Service (APNS) which will handle the delivery of the push notifications to user devices.

For more details, refer to the WhatsApp system design where we discuss push notifications in detail.

Detailed design

It's time to discuss our design decisions in detail.

Data Partitioning

To scale out our databases we will need to partition our data. Horizontal partitioning (aka Sharding) can be a good first step. We can shard our database either based on existing partition schemes or regions. If we divide the locations into regions using let's say zip codes, we can effectively store all the data in a given region on a fixed node. But this can still cause uneven data and load distribution, we can solve this using Consistent hashing.

For more details, refer to Sharding and Consistent Hashing.

Metrics and Analytics

Recording analytics and metrics is one of our extended requirements. We can capture the data from different services and run analytics on the data using Apache Spark which is an open-source unified analytics engine for large-scale data processing. Additionally, we can store critical metadata in the views table to increase data points within our data.

Caching

In a location services-based platform, caching is important. We have to be able to cache the recent locations of the customers and drivers for fast retrieval. We can use solutions like Redis or Memcached but what kind of cache eviction policy would best fit our needs?

Which cache eviction policy to use?

Least Recently Used (LRU) can be a good policy for our system. In this policy, we discard the least recently used key first.

How to handle cache miss?

Whenever there is a cache miss, our servers can hit the database directly and update the cache with the new entries.

For more details, refer to Caching.

Identify and resolve bottlenecks

uber-advanced-design

Let us identify and resolve bottlenecks such as single points of failure in our design:

  • "What if one of our services crashes?"
  • "How will we distribute our traffic between our components?"
  • "How can we reduce the load on our database?"
  • "How to improve the availability of our cache?"
  • "How can we make our notification system more robust?"

To make our system more resilient we can do the following:

  • Running multiple instances of each of our services.
  • Introducing load balancers between clients, servers, databases, and cache servers.
  • Using multiple read replicas for our databases.
  • Multiple instances and replicas for our distributed cache.
  • Exactly once delivery and message ordering is challenging in a distributed system, we can use a dedicated message broker such as Apache Kafka or NATS to make our notification system more robust.

Next Steps

Congratulations, you've finished the course!

Now that you know the fundamentals of System Design, here are some additional resources:

It is also recommended to actively follow engineering blogs of companies putting what we learned in the course into practice at scale:

最后但并非最不重要的一点是,自愿参加贵公司的新项目,并向高级工程师和架构师学习,以进一步提高你的系统设计技能。

我希望这门课程是一次很棒的学习经历。我很想听听你的反馈。

祝你进一步学习一切顺利!

引用

以下是创建本课程时引用的资源。

所有图表都是使用埃克斯卡利德鲁制作的,可在此处找到。