系统设计:完整课程
目录
什么是系统设计?
知识产权
OSI 模型
TCP 和 UDP
域名系统 (DNS)
负载均衡
聚类
缓存
内容分发网络 (CDN)
代理人
可用性
可扩展性
贮存
数据库和 DBMS
SQL 数据库
NoSQL 数据库
SQL 与 NoSQL 数据库
数据库复制
索引
规范化和非规范化
ACID 和 BASE 一致性模型
CAP定理
PACELC定理
交易
分布式事务
分片
一致性哈希
数据库联合
N层架构
消息代理
消息队列
发布-订阅
企业服务总线(ESB)
单体应用和微服务
事件驱动架构(EDA)
事件溯源
命令和查询责任分离(CQRS)
API 网关
REST、GraphQL、gRPC
长轮询、WebSocket、服务器发送事件 (SSE)
地理散列和四叉树
断路器
速率限制
服务发现
SLA、SLO、SLI
灾难恢复
虚拟机 (VM) 和容器
OAuth 2.0 和 OpenID Connect (OIDC)
单点登录 (SSO)
SSL、TLS、mTLS
系统设计面试
URL缩短器
叽叽喳喳
Netflix
优步
后续步骤
参考
嘿,欢迎来到本课程。希望本课程能带给您良好的学习体验!
本课程也可在Github上免费获取
目录
-
入门
-
第一章
-
第二章
-
第三章
-
第四章
-
第五章
-
附录
什么是系统设计?
在开始本课程之前,让我们先讨论一下什么是系统设计。
系统设计是定义
满足特定需求的系统架构、接口和数据的过程。系统设计
通过构建一致且高效的系统来满足您的业务或组织的需求。它需要采用
系统化的方法来构建和设计系统。良好的系统设计需要
我们全面考虑,从基础设施到数据及其存储方式。
为什么系统设计如此重要?
系统设计帮助我们定义满足业务需求的解决方案。它是
构建系统时最早可以做的决策之一。通常,
从高层次思考至关重要,因为这些决策日后很难纠正。
随着系统的发展,它还能让我们更容易地推理和管理架构变更。
知识产权
IP地址是用于标识互联网或本地网络上设备的唯一地址。IP代表“互联网协议”,它是一组用于管理通过互联网或本地网络发送的数据格式的规则。
本质上,IP 地址是允许信息在网络上的设备之间发送的标识符。它们包含位置信息,并使设备能够进行通信。互联网需要一种方法来区分不同的计算机、路由器和网站。IP 地址提供了一种实现这一点的方法,并且是互联网运作方式的重要组成部分。
版本
现在,让我们了解一下 IP 地址的不同版本:
IPv4
最初的互联网协议 IPv4 采用 32 位点分十进制数字表示法,仅允许大约 40 亿个 IP 地址。最初,这个数字绰绰有余,但随着互联网普及率的提高,我们需要更好的方案。
例子:102.22.192.181
IPv6
IPv6 是 1998 年推出的新协议。部署始于 2000 年代中期,由于互联网用户呈指数级增长,因此它仍在进行中。
这项新协议采用 128 位字母数字十六进制表示法。这意味着 IPv6 可以提供约 340 万亿亿亿个 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 模型为何重要?
开放系统互连 (OSI) 模型定义了网络讨论和文档中使用的通用术语。这使我们能够分解非常复杂的通信过程并评估其各个组成部分。
虽然该模型尚未直接应用于当今最常见的 TCP/IP 网络中,但它仍然可以帮助我们做更多的事情。例如:
- 使故障排除更容易并帮助识别整个堆栈中的威胁。
- 鼓励硬件制造商创造能够通过网络相互通信的网络产品。
- 对于培养安全第一的心态至关重要。
- 将复杂的功能分解为更简单的组件。
图层
OSI 模型的七个抽象层从上到下定义如下:
应用
这是唯一直接与用户数据交互的层。Web 浏览器和电子邮件客户端等软件应用程序依赖应用层来发起通信。但需要明确的是,客户端软件应用程序不属于应用层,而是应用层负责软件向用户呈现有意义数据所依赖的协议和数据操作。应用层协议包括 HTTP 和 SMTP。
推介会
表示层也称为翻译层。来自应用层的数据在此提取,并按照所需的格式进行处理,以便在网络上传输。表示层的功能包括翻译、加密/解密和压缩。
会议
此层负责开启和关闭两个设备之间的通信。通信开启和关闭之间的时间间隔称为会话。会话层确保会话保持足够长的时间以传输所有正在交换的数据,然后及时关闭会话以避免浪费资源。会话层还会通过检查点同步数据传输。
运输
传输层(也称为第 4 层)负责两个设备之间的端到端通信。这包括从会话层获取数据,并将其拆分成称为段的块,然后将其发送到网络层(第 3 层)。它还负责在接收设备上将段重新组装成会话层可以使用的数据。
网络
网络层负责促进两个不同网络之间的数据传输。网络层在发送方设备上将传输层的数据段分解成更小的单元(称为数据包),并在接收方设备上重新组装这些数据包。网络层还会找到数据到达目的地的最佳物理路径,这被称为路由。如果通信的两个设备位于同一网络上,则网络层是不必要的。
数据链路
数据链路层与网络层非常相似,不同之处在于数据链路层用于促进同一网络上两个设备之间的数据传输。数据链路层从网络层获取数据包,并将其分解成称为帧的较小部分。
身体的
这一层包括参与数据传输的物理设备,例如电缆和交换机。数据也在这一层被转换成比特流,即由 1 和 0 组成的字符串。两个设备的物理层还必须就信号约定达成一致,以便在两个设备上区分 1 和 0。
TCP 和 UDP
TCP
传输控制协议 (TCP) 是面向连接的,这意味着一旦建立连接,数据就可以双向传输。TCP 内置了错误检查机制,并确保数据按发送顺序传输,使其成为传输静态图像、数据文件和网页等信息的理想协议。
但是,尽管 TCP 本质上是可靠的,但其反馈机制也会导致更大的开销,从而意味着更多地使用网络上的可用带宽。
UDP
用户数据报协议 (UDP) 是一种更简单的无连接互联网协议,无需错误校验和恢复服务。使用 UDP,打开、维护或终止连接均无需任何开销。无论接收方是否收到数据,数据都会持续发送给接收方。
它在很大程度上适用于广播或多播网络传输等实时通信。当我们需要最低延迟,且数据延迟比数据丢失更糟糕时,我们应该使用 UDP 而不是 TCP。
TCP 与 UDP
TCP 是面向连接的协议,而 UDP 是无连接的协议。TCP 和 UDP 之间的一个关键区别在于速度,因为 TCP 比 UDP 相对较慢。总体而言,UDP 是一种速度更快、更简单、更高效的协议,但是,只有 TCP 才能重传丢失的数据包。
TCP 提供从用户到服务器(反之亦然)的有序数据传送,而 UDP 并不专用于端到端通信,也不检查接收方的准备情况。
特征 | TCP | UDP |
---|---|---|
联系 | 需要建立连接 | 无连接协议 |
保证送达 | 可以保证数据的传递 | 无法保证数据传输 |
重传 | 可以重新传输丢失的数据包 | 无需重新传输丢失的数据包 |
速度 | 比UDP慢 | 比TCP更快 |
广播 | 不支持广播 | 支持广播 |
用例 | HTTPS、HTTP、SMTP、POP、FTP 等 | 视频流、DNS、VoIP 等 |
域名系统 (DNS)
之前我们了解了 IP 地址,它使每台机器能够与其他机器连接。但众所周知,人类对名称的适应性比数字更强。像 这样的名称google.com
比 这样的更容易记住122.250.192.232
。
这给我们带来了域名系统 (DNS),它是一个分层的、分散的命名系统,用于将人类可读的域名转换为 IP 地址。
DNS 的工作原理
DNS 查找涉及以下八个步骤:
- 客户端在 Web 浏览器中输入example.com,查询传输到 Internet 并由 DNS 解析器接收。
- 然后,解析器递归查询 DNS 根名称服务器。
- 根服务器使用顶级域名 (TLD) 的地址来响应解析器。
- 然后,解析器向
.com
TLD 发出请求。 - 然后,TLD 服务器使用该域名的名称服务器的 IP 地址example.com进行响应。
- 最后,递归解析器向域的名称服务器发送查询。
- 然后, example.com的 IP 地址从名称服务器返回到解析器。
- 然后,DNS 解析器会使用最初请求的域名的 IP 地址来响应 Web 浏览器。
IP 地址解析完成后,客户端应该能够从解析后的 IP 地址请求内容。例如,解析后的 IP 地址可能会返回一个要在浏览器中呈现的网页
服务器类型
现在,让我们看看构成 DNS 基础设施的四个主要服务器组。
DNS解析器
DNS 解析器(也称为 DNS 递归解析器)是 DNS 查询的第一站。递归解析器充当客户端和 DNS 域名服务器之间的中间人。收到来自 Web 客户端的 DNS 查询后,递归解析器要么使用缓存数据进行响应,要么先向根域名服务器发送请求,然后再向 TLD 域名服务器发送另一个请求,最后再向权威域名服务器发送一个请求。在收到权威域名服务器发送的包含所请求 IP 地址的响应后,递归解析器会将响应发送给客户端。
DNS 根服务器
根服务器接受包含域名的递归解析器查询,然后根域名服务器根据该域名的后缀(.com
、.net
、等)将递归解析器定向到顶级域名服务器。根域名服务器由一个名为互联网名称与数字地址分配机构 (ICANN) 的.org
非营利组织负责监管。
每个递归解析器都知道 13 个 DNS 根域名服务器。请注意,虽然有 13 个根域名服务器,但这并不意味着根域名服务器系统中只有 13 台机器。根域名服务器有 13 种类型,但每种类型的根域名服务器在世界各地都有多个副本,这些副本使用任播路由来提供快速响应。
TLD 域名服务器
TLD 名称服务器维护所有共享公共域扩展名的域名的信息,例如.com
、.net
或 URL 中最后一个点后面的任何内容。
TLD 域名服务器的管理由互联网号码分配机构 (IANA)负责,该机构是ICANN的一个分支机构。IANA 将 TLD 服务器分为两大类:
- 通用顶级域名:这些域名包括
.com
、.org
、.net
、.edu
和.gov
。 - 国家代码顶级域名:这些域名包括任何特定于某个国家或州的域名。例如
.uk
, 、.us
、.ru
和.jp
。
权威 DNS 服务器
权威域名服务器通常是解析器在 IP 地址解析过程中的最后一步。权威域名服务器包含其所服务域名(例如google.com)的特定信息,它可以向递归解析器提供在 DNS A 记录中找到的该服务器的 IP 地址;或者,如果域名具有 CNAME 记录(别名),它将向递归解析器提供一个别名域名,此时递归解析器必须执行全新的 DNS 查找,才能从权威域名服务器获取记录(通常是包含 IP 地址的 A 记录)。如果找不到该域名,则返回 NXDOMAIN 消息。
查询类型
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(证书记录):存储公钥证书。
子域名
子域名是主域名的附加部分。它通常用于在逻辑上将网站划分为多个部分。我们可以在主域名上创建多个子域名。
例如,blog.example.com
其中blog
是子域名,example
是主域名,.com
是顶级域名 (TLD)。类似的例子还有support.example.com
或careers.example.com
。
DNS 区域
DNS 区域是域名空间的一个独特组成部分,它被委托给负责维护 DNS 区域的法人实体,例如个人、组织或公司。DNS 区域也具有管理功能,允许对 DNS 组件(例如权威域名服务器)进行精细控制。
DNS缓存
DNS 缓存(有时也称为 DNS 解析器缓存)是由计算机操作系统维护的临时数据库,其中包含所有最近访问和尝试访问网站及其他互联网域名的记录。换句话说,DNS 缓存只是近期 DNS 查询的内存,我们的计算机在尝试确定如何加载网站时可以快速参考它。
域名系统 (DNS) 为每个 DNS 记录设置了生存时间 (TTL)。TTL 指定 DNS 客户端或服务器可以缓存该记录的秒数。当记录存储在缓存中时,其附带的任何 TTL 值也会被存储。服务器会持续更新缓存中记录的 TTL,每秒倒计时一次。当 TTL 达到零时,该记录将从缓存中删除或清除。此时,如果收到针对该记录的查询,DNS 服务器必须启动解析过程。
反向 DNS
反向 DNS 查询是指针对与给定 IP 地址关联的域名进行的 DNS 查询。这与更常用的正向 DNS 查询相反,后者会查询 DNS 系统并返回 IP 地址。反向解析 IP 地址的过程使用 PTR 记录。如果服务器没有 PTR 记录,则无法解析反向查询。
电子邮件服务器通常使用反向查找功能。电子邮件服务器在将电子邮件发送到其网络之前,会检查其是否来自有效的服务器。许多电子邮件服务器会拒绝来自不支持反向查找或不太可能合法的服务器的邮件。
注意:反向 DNS 查找并未被普遍采用,因为它们对于互联网的正常功能并不重要。
示例
以下是一些广泛使用的托管 DNS 解决方案:
负载均衡
负载均衡使我们能够将传入的网络流量分配到多个资源,从而仅向在线的资源发送请求,从而确保高可用性和可靠性。这使我们能够根据需求灵活地添加或减少资源。
为了获得额外的可扩展性和冗余度,我们可以尝试在系统的每一层进行负载平衡:
但为什么?
现代高流量网站必须处理来自用户或客户端的数十万甚至数百万个并发请求。为了经济高效地扩展以满足如此高的数据量,现代计算最佳实践通常要求添加更多服务器。
负载均衡器可以位于服务器前端,将客户端请求路由到所有能够处理这些请求的服务器,从而最大限度地提高速度和容量利用率,并确保任何一台服务器都不会超负荷运行,从而避免性能下降。如果某台服务器出现故障,负载均衡器会将流量重定向到其余在线服务器。当新服务器添加到服务器组时,负载均衡器会自动开始向该服务器发送请求。
工作负载分配
这是负载均衡器提供的核心功能,有几种常见的变体:
- 基于主机:根据请求的主机名分发请求。
- 基于路径:使用整个 URL 来分发请求,而不是仅仅使用主机名。
- 基于内容:检查请求的消息内容。这允许基于内容(例如参数的值)进行分发。
图层
一般来说,负载均衡器在两个级别之一上运行:
网络层
这是工作在网络传输层(也称为第 4 层)的负载均衡器。它根据 IP 地址等网络信息执行路由,但无法执行基于内容的路由。这些通常是高速运行的专用硬件设备。
应用层
这是在应用层(也称为第 7 层)运行的负载均衡器。负载均衡器可以完整读取请求并执行基于内容的路由。这使得我们可以在全面了解流量的基础上管理负载。
类型
让我们看看不同类型的负载均衡器:
软件
软件负载均衡器通常比硬件负载均衡器更容易部署。它们也往往更具成本效益和灵活性,并且可以与软件开发环境结合使用。软件方法使我们能够灵活地根据环境的特定需求配置负载均衡器。灵活性的提升可能会以需要进行更多工作来设置负载均衡器为代价。与提供更多封闭式方法的硬件版本相比,软件均衡器为我们提供了更大的更改和升级自由。
软件负载均衡器被广泛使用,可以作为需要配置和管理的可安装解决方案或托管云服务。
硬件
顾名思义,硬件负载均衡器依靠物理本地硬件来分配应用程序和网络流量。这些设备可以处理大量流量,但通常价格昂贵,灵活性也相当有限。
硬件负载平衡器包括专有固件,随着新版本和安全补丁的发布,需要维护和更新。
DNS
DNS 负载平衡是在域名系统 (DNS) 中配置域的做法,以便将对域的客户端请求分布到一组服务器机器上。
遗憾的是,DNS负载均衡本身存在一些问题,限制了其可靠性和效率。最重要的是,DNS不会检查服务器、网络中断或错误,因此即使服务器宕机或无法访问,DNS也始终会为域名返回相同的IP地址。
路由算法
现在,我们来讨论一下常用的路由算法:
- 循环:请求轮流分发到应用服务器。
- 加权循环:基于简单的循环技术,使用管理员可以通过 DNS 记录分配的权重来考虑不同的服务器特性,例如计算和流量处理能力。
- 最少连接数:新请求将发送到当前客户端连接数最少的服务器。每个服务器的相对计算能力会被考虑在内,以确定哪个服务器的连接数最少。
- 最短响应时间:将请求发送到根据最快响应时间和最少活动连接相结合的公式选择的服务器。
- 最小带宽:此方法以兆比特每秒 (Mbps) 为单位测量流量,将客户端请求发送到流量 Mbps 最少的服务器。
- 哈希:根据我们定义的密钥(例如客户端 IP 地址或请求 URL)来分发请求。
优势
负载平衡在防止停机方面也起着关键作用,负载平衡的其他优点包括:
- 可扩展性
- 冗余
- 灵活性
- 效率
冗余负载均衡器
您可能已经猜到了,负载均衡器本身可能存在单点故障。为了解决这个问题,N
可以在集群模式下使用第二个或多个负载均衡器。
而且,如果检测到故障并且主动负载均衡器发生故障,则另一个被动负载均衡器可以接管,这将使我们的系统更具容错能力。
特征
以下是负载均衡器的一些常见所需功能:
- 自动缩放:根据需求条件启动和关闭资源。
- 粘性会话:将同一用户或设备分配给同一资源以维持资源上的会话状态的能力。
- 健康检查:确定资源是否已关闭或性能不佳,以便将资源从负载平衡池中移除。
- 持久连接:允许服务器与客户端建立持久连接,例如 WebSocket。
- 加密:处理加密连接,如 TLS 和 SSL。
- 证书:向客户端提供证书并对客户端证书进行身份验证。
- 压缩:响应的压缩。
- 缓存:应用层负载均衡器可以提供缓存响应的能力。
- 日志记录:请求和响应元数据的日志记录可以作为重要的审计跟踪或分析数据来源。
- 请求跟踪:为每个请求分配一个唯一的 ID,以便记录、监控和故障排除。
- 重定向:根据请求的路径等因素重定向传入请求的能力。
- 固定响应:对请求返回静态响应,例如错误消息。
示例
目前业界常用的负载均衡方案有以下几种:
聚类
从高层次上讲,计算机集群是由两台或多台计算机(或节点)组成的集群,它们并行运行以实现共同目标。这使得由大量可并行执行的独立任务组成的工作负载能够分布在集群的各个节点上。因此,这些任务可以利用每台计算机的综合内存和处理能力来提高整体性能。
要构建计算机集群,各个节点应连接到网络以实现节点间通信。然后,可以使用软件将节点连接在一起并形成集群。每个节点上可能有一个共享存储设备和/或本地存储。
通常,至少会指定一个节点作为集群的入口点。领导节点可能负责将传入的工作委托给其他节点,并在必要时汇总结果并返回给用户。
理想情况下,集群的运行方式应该像单个系统一样。访问集群的用户无需知道系统是集群还是单个机器。此外,集群的设计应尽量减少延迟,并避免节点间通信出现瓶颈。
类型
计算机集群通常可分为三类:
- 高可用性或故障转移
- 负载均衡
- 高性能计算
配置
两种最常用的高可用性 (HA) 集群配置是主动-主动和主动-被动。
双活
双活集群通常由至少两个节点组成,这两个节点同时运行同一种服务。双活集群的主要目的是实现负载均衡。负载均衡器将工作负载分配到所有节点,以防止任何单个节点过载。由于有更多节点可供服务,吞吐量和响应时间也会得到提升。
主动-被动
与主动-主动集群配置类似,主动-被动集群也至少包含两个节点。然而,顾名思义,主动-被动集群并非所有节点都会处于主动状态。例如,在两个节点的情况下,如果第一个节点已经处于主动状态,则第二个节点必须处于被动状态或待机状态。
优势
集群计算有四个主要优势:
- 高可用性
- 可扩展性
- 表现
- 经济高效
负载平衡与集群
负载均衡与集群有一些共同点,但它们是不同的过程。集群提供冗余,并提升容量和可用性。集群中的服务器彼此感知,并协同工作以实现共同目标。但在负载均衡中,服务器彼此感知不到。相反,它们会对从负载均衡器收到的请求做出反应。
我们可以将负载平衡与集群结合使用,但它也适用于涉及具有共同目的的独立服务器的情况,例如运行网站、业务应用程序、Web 服务或其他 IT 资源。
挑战
集群带来的最明显挑战是增加了安装和维护的复杂性。操作系统、应用程序及其依赖项必须在每个节点上分别安装和更新。
如果集群中的节点结构不均匀,情况会变得更加复杂。还必须密切监控每个节点的资源利用率,并汇总日志以确保软件正常运行。
此外,存储变得更加难以管理,共享存储设备必须防止节点相互覆盖,并且分布式数据存储必须保持同步。
示例
集群在业界非常常用,而且很多技术通常都提供某种集群模式。例如:
- 容器(例如Kubernetes、Amazon ECS)
- 数据库(例如Cassandra、MongoDB)
- 缓存(例如Redis)
缓存
“计算机科学中只有两件难事:缓存失效和命名。”——Phil Karlton
缓存的主要目的是通过减少访问底层较慢存储层的需求来提高数据检索性能。缓存通常会以容量换取速度,因此通常只会暂时存储一部分数据,而数据库的数据通常都是完整且持久的。
缓存利用了引用局部性原理“最近请求的数据很可能再次被请求”。
缓存和内存
缓存与计算机内存类似,是一种紧凑、快速的内存,它以多层级结构存储数据,从第一层开始,依次递增。这些层级分别标记为 L1、L2、L3,以此类推。缓存也会在需要写入数据时进行写入,例如,当发生更新,需要将新内容保存到缓存中以替换已保存的旧内容时。
无论是读取还是写入缓存,都是一次一个块地进行。每个块都有一个标签,其中包含数据存储在缓存中的位置。当从缓存中请求数据时,会通过标签进行搜索,以在内存的一级 (L1) 中找到所需的特定内容。如果找不到正确的数据,则会在二级 (L2) 中进行更多搜索。
如果在缓存中找不到数据,则继续在 L3、L4 等层级上搜索,直到找到数据,然后读取并加载。如果在缓存中找不到数据,则将数据写入缓存,以便下次快速检索。
缓存命中和缓存未命中
缓存命中
缓存命中是指内容成功从缓存中读取的情况。系统会在内存中快速搜索标签,找到并读取到数据后,即视为缓存命中。
冷缓存、暖缓存和热缓存
缓存命中也可以描述为冷、温或热。每种情况都描述了数据读取的速度。
热缓存是指以最快的速度从内存中读取数据的情况。这种情况发生在从 L1 检索数据时。
冷缓存是指读取数据的速度最慢的缓存,尽管如此,它仍然能够成功读取,因此仍然被视为缓存命中。只不过,这些数据位于内存层级结构较低的位置,例如 L3 或更低层级。
暖缓存 (warm cache) 指的是 L2 或 L3 缓存中的数据。它的速度不如热缓存,但仍比冷缓存快。通常,我们称缓存为暖缓存,是指它比热缓存速度更慢,更接近冷缓存。
缓存未命中
缓存未命中是指搜索内存但未找到数据的情况。发生这种情况时,内容将被传输并写入缓存。
缓存失效
缓存失效是指计算机系统将缓存条目声明为无效,并移除或替换它们的过程。如果数据被修改,则应在缓存中使其失效;否则,这可能会导致应用程序行为不一致。缓存系统有三种类型:
直写缓存
数据同时写入缓存和相应的数据库。
优点:检索速度快,缓存和存储之间的数据完全一致。
缺点:写入操作的延迟较高。
绕写缓存
写入操作直接进入数据库或永久存储,绕过缓存。
优点:这可能会减少延迟。
缺点:它会增加缓存未命中率,因为缓存系统必须在发生缓存未命中时从数据库读取信息。因此,对于需要快速写入和重读信息的应用程序来说,这会导致更高的读取延迟。读取操作发生在速度较慢的后端存储中,因此延迟会更高。
回写缓存
写入操作仅针对缓存层,缓存写入完成后立即确认。然后,缓存会异步同步此写入操作到数据库。
优点:这将减少写入密集型应用程序的延迟并提高吞吐量。
缺点:如果缓存层崩溃,存在数据丢失的风险。我们可以通过在缓存中设置多个副本来确认写入操作来改善这个问题。
驱逐政策
以下是一些最常见的缓存驱逐策略:
- 先进先出 (FIFO):缓存会逐出最先访问的第一个块,而不考虑之前访问的频率或次数。
- 后进先出 (LIFO):缓存首先逐出最近访问的块,而不考虑之前访问的频率或次数。
- 最近最少使用(LRU):首先丢弃最近最少使用的项目。
- 最近使用(MRU):与 LRU 相反,首先丢弃最近使用的项目。
- 最不常用(LFU):计算某项物品被需要的频率。最不常用的物品会被优先丢弃。
- 随机替换(RR):随机选择一个候选项目并在必要时丢弃它以腾出空间。
分布式缓存
分布式缓存是一种将多台联网计算机的随机存取存储器 (RAM) 集中到一个内存数据存储中的系统,该数据存储用作数据缓存,以提供快速数据访问。虽然大多数缓存传统上都位于一台物理服务器或硬件组件中,但分布式缓存可以通过连接多台计算机来扩展,从而超越单台计算机的内存限制。
全局缓存
顾名思义,我们将拥有一个所有应用程序节点都将使用的共享缓存。当在全局缓存中找不到请求的数据时,缓存会负责从底层数据存储中查找缺失的数据。
用例
缓存可以有许多现实世界的用例,例如:
- 数据库缓存
- 内容分发网络 (CDN)
- 域名系统 (DNS) 缓存
- API缓存
何时不使用缓存?
我们还来看一些不应该使用缓存的场景:
- 当访问缓存的时间与访问主数据存储的时间一样长时,缓存就没有帮助了。
- 当请求重复性较低(随机性较高)时,缓存效果不佳,因为缓存性能来自重复的内存访问模式。
- 当数据频繁更改时,缓存没有帮助,因为缓存版本不同步,并且每次都必须访问主数据存储。
需要注意的是,缓存不应用作永久数据存储。由于易失性存储器速度更快,缓存几乎总是实现在易失性存储器中,因此应被视为瞬态存储器。
优势
以下是缓存的一些优点:
- 提高性能
- 减少延迟
- 减少数据库负载
- 降低网络成本
- 提高读取吞吐量
示例
下面介绍一些常用的缓存技术:
内容分发网络 (CDN)
内容分发网络 (CDN) 是一组地理分布的服务器,它们协同工作,以快速分发互联网内容。通常,静态文件(例如 HTML/CSS/JS)、照片和视频均由 CDN 提供。
为什么要使用 CDN?
内容分发网络 (CDN) 可提高内容可用性和冗余度,同时降低带宽成本并提升安全性。通过 CDN 提供内容可以显著提升性能,因为用户可以从附近的数据中心接收内容,而我们的服务器无需处理 CDN 所处理的请求。
CDN 如何工作?
在 CDN 中,原始服务器包含内容的原始版本,而边缘服务器数量众多且分布在世界各地。
为了最大程度地缩短访问者与网站服务器之间的距离,CDN 会将其内容的缓存版本存储在多个地理位置(称为边缘站点)。每个边缘站点包含多个缓存服务器,负责将内容分发给其附近的访问者。
一旦静态资产被缓存在特定位置的所有 CDN 服务器上,所有后续网站访问者对静态资产的请求都将从这些边缘服务器而不是原站传送,从而减少原站负载并提高可扩展性。
例如,当英国用户访问我们可能托管在美国的网站时,我们会从最近的边缘站点(例如伦敦边缘站点)为他们提供服务。这比让访问者直接向源服务器发出完整请求(这会增加延迟)要快得多。
类型
CDN一般分为两种类型:
推送 CDN
每当服务器发生变更时,推送 CDN 都会接收新内容。我们全权负责提供内容、直接上传到 CDN 以及重写 URL 以指向 CDN。我们可以配置内容的过期时间和更新时间。仅在内容新增或变更时上传,从而最大限度地减少流量,同时最大限度地提高存储空间。
流量较少或内容不经常更新的网站适合使用推送 CDN。内容一次性上传到 CDN,无需定期重新拉取。
拉取 CDN
在拉取式 CDN 情况下,缓存会根据请求进行更新。当客户端发送请求,要求从 CDN 获取静态资源(如果 CDN 中没有),则客户端会从源服务器获取最新更新的资源,并将其填充到 CDN 的缓存中,然后将新缓存的资源发送给用户。
与推送 CDN 相比,这种 CDN 所需的维护较少,因为 CDN 节点上的缓存更新是根据客户端向源服务器发送的请求进行的。流量较大的网站更适合使用拉取 CDN,因为流量会更均匀地分布,并且 CDN 上只会保留最近请求的内容。
缺点
众所周知,好东西是有额外成本的,所以让我们来讨论一下 CDN 的一些缺点:
- 额外费用:使用 CDN 可能很昂贵,尤其是对于高流量服务。
- 限制:一些组织和国家已经封锁了流行 CDN 的域名或 IP 地址。
- 位置:如果我们的大多数受众位于 CDN 没有服务器的国家/地区,那么我们网站上的数据可能需要比不使用任何 CDN 时传输得更远。
示例
以下是一些广泛使用的 CDN:
代理人
代理服务器是位于客户端和后端服务器之间的中间硬件/软件。它接收来自客户端的请求并将其转发到源服务器。通常,代理用于过滤请求、记录请求,有时还会转换请求(通过添加/删除标头、加密/解密或压缩)。
类型
代理有两种类型:
正向代理
正向代理,通常也称为代理、代理服务器或 Web 代理,是位于一组客户端计算机前端的服务器。当这些计算机向互联网上的网站和服务发出请求时,代理服务器会拦截这些请求,然后像中间人一样代表这些客户端与 Web 服务器进行通信。
优势
以下是正向代理的一些优点:
- 阻止访问某些内容
- 允许访问受地理限制的内容
- 提供匿名性
- 避免其他浏览限制
虽然代理服务器提供了匿名的优势,但它们仍然可以追踪我们的个人信息。代理服务器的设置和维护成本高昂,并且需要进行配置。
反向代理
反向代理服务器是位于一个或多个 Web 服务器前端的服务器,用于拦截来自客户端的请求。当客户端向网站的源服务器发送请求时,这些请求会被反向代理服务器拦截。
正向代理和反向代理之间的区别虽然微妙,但却至关重要。简而言之,正向代理位于客户端前端,确保任何源服务器都不会直接与该客户端通信。而反向代理位于源服务器前端,确保任何客户端都不会直接与该源服务器通信。
引入反向代理会增加复杂性。单个反向代理很容易导致单点故障,而配置多个反向代理(即故障转移)则进一步增加了复杂性。
优势
以下是使用反向代理的一些优点:
- 提高安全性
- 缓存
- SSL加密
- 负载均衡
- 可扩展性和灵活性
负载均衡器与反向代理
等等,反向代理和负载均衡器不是很相似吗?嗯,不是,因为当我们有多台服务器时,负载均衡器很有用。通常,负载均衡器会将流量路由到一组提供相同功能的服务器,而反向代理即使只有一台 Web 服务器或应用服务器也很有用。反向代理也可以充当负载均衡器,但反过来不行。
示例
以下是一些常用的代理技术:
可用性
可用性是指系统在特定时间段内保持运行并执行其所需功能的时间。它简单衡量了系统、服务或机器在正常条件下保持运行的时间百分比。
可用性的九个因素
可用性通常用正常运行时间(或停机时间)来量化,即服务可用时间的百分比。通常以“9”来衡量。
如果可用性为 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
两者的Bar
可用性均为 99.9%,则它们依次相加的可用性将为 99.8%。
平行线
当两个组件并行时,总体可用性会增加。
例如,如果Foo
两者的Bar
可用性均为 99.9%,则它们的并行总可用性将为 99.9999%。
可用性与可靠性
如果系统可靠,那么它就是可用的。然而,即使系统可用,也不一定可靠。换句话说,高可靠性有助于高可用性,但即使系统不可靠,也有可能实现高可用性。
高可用性与容错
高可用性和容错性都适用于提供高正常运行时间水平的方法。然而,它们实现目标的方式不同。
容错系统不会中断服务,但成本会显著增加,而高可用性系统则能最大程度地减少服务中断。容错需要完全的硬件冗余,即使主系统发生故障,另一个系统也能接管,而不会影响正常运行时间。
可扩展性
可扩展性是衡量系统通过添加或删除资源来满足需求对变化的响应程度的标准。
让我们讨论一下不同类型的扩展:
垂直扩展
垂直扩展(也称为纵向扩展)通过为现有机器添加更多功能来扩展系统的可扩展性。换句话说,垂直扩展是指通过增加硬件容量来提升应用程序的性能。
优势
- 易于实现
- 更易于管理
- 数据一致
缺点
- 停机时间长的风险
- 升级更困难
- 可能存在单点故障
水平扩展
水平扩展(也称为横向扩展)通过添加更多机器来扩展系统规模。它通过向现有服务器池添加更多实例来提高服务器性能,从而使负载分配更均匀。
优势
- 增加冗余
- 更好的容错能力
- 灵活高效
- 更容易升级
缺点
- 增加复杂性
- 数据不一致
- 下游服务负载增加
贮存
存储是一种使系统能够临时或永久保存数据的机制。在系统设计中,这个主题通常被忽略,然而,了解一些常见的存储技术类型非常重要,它们可以帮助我们优化存储组件。让我们讨论一些重要的存储概念:
袭击
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 级别的所有功能:
特征 | RAID 0 | RAID 1 | RAID 5 | RAID 6 | RAID 10 |
---|---|---|---|---|---|
描述 | 条纹 | 镜像 | 带奇偶校验的条带化 | 双重奇偶校验条带化 | 条带化和镜像 |
最小磁盘数 | 2 | 2 | 3 | 4 | 4 |
读取性能 | 高的 | 高的 | 高的 | 高的 | 高的 |
写入性能 | 高的 | 中等的 | 高的 | 高的 | 中等的 |
成本 | 低的 | 高的 | 低的 | 低的 | 高的 |
容错 | 没有任何 | 单驱动器故障 | 单驱动器故障 | 双驱动器故障 | 每个子阵列最多有一个磁盘故障 |
产能利用率 | 100% | 50% | 67%-94% | 50%-80% | 50% |
卷
卷是磁盘或磁带上的固定存储量。“卷”一词通常用作存储本身的同义词,但单个磁盘可以包含多个卷,或者一个卷可以跨多个磁盘。
文件存储
文件存储是一种将数据存储为文件并以分层目录结构呈现给最终用户的解决方案。其主要优势在于提供一种用户友好的文件存储和检索解决方案。要在文件存储中定位文件,需要文件的完整路径。文件存储经济实惠且易于构建,通常存储在硬盘上,这意味着用户看到的和硬盘上看到的完全相同。
例如:Amazon EFS、Azure 文件、Google Cloud Filestore等。
块存储
块存储将数据划分为块(数据块),并将它们存储为单独的数据块。每个数据块都被赋予一个唯一的标识符,这使得存储系统能够将较小的数据块放置在最方便的位置。
块存储还将数据与用户环境分离,允许数据分布在多个环境中。这创建了多条数据路径,方便用户快速检索。当用户或应用程序从块存储系统请求数据时,底层存储系统会重新组装数据块,并将数据呈现给用户或应用程序。
例如:Amazon EBS。
对象存储
对象存储,也称为基于对象的存储,将数据文件分解成称为对象的块。然后,它将这些对象存储在单个存储库中,该存储库可以分布在多个联网系统中。
例如:Amazon S3、Azure Blob Storage、Google Cloud Storage等。
网络存储
NAS(网络附加存储)是一种连接到网络的存储设备,允许授权网络用户从中心位置存储和检索数据。NAS 设备非常灵活,这意味着当我们需要额外存储空间时,可以随时扩容。它速度更快、价格更低,并且具备公有云的所有优势,让我们拥有完全的控制权。
HDFS
Hadoop 分布式文件系统 (HDFS) 是一个专为在商用硬件上运行而设计的分布式文件系统。HDFS 具有高度容错能力,并且旨在部署在低成本硬件上。HDFS 提供对应用程序数据的高吞吐量访问,适用于拥有海量数据集的应用程序。它与现有的分布式文件系统有许多相似之处。
HDFS 旨在可靠地跨大型集群中的机器存储超大文件。它将每个文件存储为一系列数据块,文件中除最后一个数据块外,所有数据块的大小均相同。为了实现容错,文件中的数据块会被复制。
数据库和 DBMS
什么是数据库?
数据库是结构化信息(或数据)的有组织的集合,通常以电子形式存储在计算机系统中。数据库通常由数据库管理系统 (DBMS) 控制。数据、DBMS 以及与之关联的应用程序统称为数据库系统,通常简称为数据库。
什么是 DBMS?
数据库通常需要一个功能全面的数据库软件程序,即数据库管理系统 (DBMS)。DBMS 充当数据库与其最终用户或程序之间的接口,允许用户检索、更新和管理信息的组织和优化方式。DBMS 还有助于对数据库进行监督和控制,从而支持各种管理操作,例如性能监控、调优以及备份和恢复。
成分
以下是不同数据库中的一些常见组件:
架构
模式的作用是定义数据结构的形状,并指定哪些类型的数据可以存储在何处。模式可以在整个数据库中严格执行,也可以在数据库的某个部分松散地执行,或者根本不存在。
桌子
每个表格都包含各种列,就像电子表格一样。一个表格可以只有两列,也可以有一百列甚至更多,具体取决于表格中存储的信息类型。
柱子
列包含一组特定类型的数据值,数据库中的每一行对应一个值。列可以包含文本值、数字、枚举值、时间戳等。
排
表中的数据以行的形式记录。一个表中可能有数千行或数百万行,包含任何特定的信息。
类型
以下是不同类型的数据库:
SQL 和 NoSQL 数据库是两个广泛的话题,我们将在“SQL 数据库”和“NoSQL 数据库”中分别讨论。在“SQL 与 NoSQL 数据库”中了解它们的比较。
挑战
大规模运行数据库时面临的一些常见挑战:
- 吸收数据量的显著增加:来自传感器、连接机器和其他数十个来源的数据激增。
- 确保数据安全:如今数据泄露事件随处可见,确保数据安全且用户可以轻松访问比以往任何时候都更加重要。
- 满足需求:公司需要实时访问其数据以支持及时决策并利用新机遇。
- 管理和维护数据库和基础设施:随着数据库变得越来越复杂,数据量越来越大,公司面临着雇用额外人才来管理数据库的费用。
- 打破可扩展性的限制:企业要想生存,就需要发展,其数据管理也必须随之发展。但预测公司需要多少容量非常困难,尤其是在本地数据库方面。
- 确保数据驻留、数据主权或延迟要求:某些组织的用例更适合在本地运行。在这种情况下,预先配置并预先优化用于运行数据库的工程化系统是理想的选择。
SQL 数据库
SQL(或关系型)数据库是一组具有预定义关系的数据项的集合。这些数据项被组织成一组包含列和行的表。表用于保存数据库中对象的信息。表中的每一列包含特定类型的数据,而每个字段则存储属性的实际值。表中的行表示一个对象或实体的相关值的集合。
表中的每一行都可以用一个称为主键的唯一标识符标记,并且可以使用外键将多个表之间的行关联起来。这些数据可以通过多种方式访问,而无需重新组织数据库表本身。SQL 数据库通常遵循ACID 一致性模型。
物化视图
物化视图是根据查询规范派生并存储以供日后使用的预先计算的数据集。由于数据是预先计算的,因此查询物化视图比针对视图基表执行查询更快。当查询频繁运行或足够复杂时,这种性能差异可能非常显著。
它还支持数据子集化,并提高了在大型数据集上运行的复杂查询的性能,从而减少了网络负载。物化视图还有其他用途,但主要用于性能和复制。
N+1查询问题
N+1 查询问题是指数据访问层执行 N 条额外的 SQL 语句来获取与执行主 SQL 查询时相同的数据。N 值越大,执行的查询越多,性能影响也越大。
这在 GraphQL 和 ORM(对象关系映射)工具中很常见,可以通过优化 SQL 查询或使用批量处理连续请求并在后台发出单个数据请求的数据加载器来解决。
优势
让我们看看使用关系数据库的一些优点:
- 简单、准确
- 无障碍设施
- 数据一致性
- 灵活性
缺点
以下是关系数据库的缺点:
- 维护成本高昂
- 困难的模式演化
- 性能下降(连接、非规范化等)
- 由于水平可扩展性差,难以扩展
示例
以下是一些常用的关系数据库:
NoSQL 数据库
NoSQL 是一个广泛的类别,涵盖所有不使用 SQL 作为主要数据访问语言的数据库。这类数据库有时也被称为非关系型数据库。与关系型数据库不同,NoSQL 数据库中的数据不必遵循预定义的模式。NoSQL 数据库遵循BASE 一致性模型。
以下是不同类型的 NoSQL 数据库:
文档
文档数据库(也称为面向文档的数据库或文档存储)是一种以文档形式存储信息的数据库。它们是一种通用数据库,适用于事务型和分析型应用程序的各种用例。
优势
- 直观、灵活
- 轻松水平扩展
- 无模式
缺点
- 无模式
- 非关系型
示例
键值
键值数据库是最简单的 NoSQL 数据库类型之一,它将数据存储为一组键值对,每个键值对由两个数据项组成。有时,它们也被称为键值存储。
优势
- 简单且高效
- 高度可扩展,可应对高流量
- 会话管理
- 优化查找
缺点
- 基本 CRUD
- 无法过滤值
- 缺乏索引和扫描功能
- 没有针对复杂查询进行优化
示例
图形
图形数据库是一种 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
- 具有严格模式的结构化数据
- 关系数据
- 需要复杂的连接
- 交易
- 通过索引查找非常快
对于 NoSQL
- 动态或灵活的模式
- 非关系数据
- 无需复杂的连接
- 数据密集型工作负载
- IOPS 吞吐量非常高
数据库复制
复制是一个涉及共享信息以确保冗余资源(例如多个数据库)之间的一致性的过程,以提高可靠性、容错性或可访问性。
主从复制
主服务器负责读取和写入操作,并将写入操作复制到一个或多个从服务器,而从服务器仅负责读取操作。从服务器还可以以树状结构复制其他从服务器。如果主服务器离线,系统可以继续以只读模式运行,直到某个从服务器被提升为主服务器或配置了新的主服务器。
优势
- 整个数据库的备份对主数据库相对没有影响。
- 应用程序可以从从属设备读取数据,而不会影响主设备。
- 从服务器可以离线并同步回主服务器,无需任何停机时间。
缺点
- 复制增加了更多的硬件和额外的复杂性。
- 当主机发生故障时,可能会造成停机并丢失数据。
- 在主从架构中,所有写入也必须向主服务器进行。
- 读取从属越多,我们需要复制的就越多,这将增加复制滞后。
主-主复制
两个主服务器都提供读写服务,并相互协调。如果其中一个主服务器发生故障,系统仍可继续运行读写操作。
优势
- 应用程序可以从两个主服务器读取。
- 在两个主节点之间分配写入负载。
- 简单、自动、快速的故障转移。
缺点
- 配置和部署不像主从那么简单。
- 由于同步,要么一致性较差,要么写入延迟增加。
- 随着更多写入节点的添加和延迟的增加,冲突解决开始发挥作用。
同步与异步复制
同步复制和异步复制之间的主要区别在于数据写入副本的方式。在同步复制中,数据同时写入主存储和副本。因此,主副本和副本应始终保持同步。
相比之下,异步复制是在数据写入主存储后才将数据复制到副本。虽然复制过程可能接近实时进行,但更常见的是按计划进行复制,而且更具成本效益。
索引
索引在数据库中广为人知,它用于提高数据存储中数据检索操作的速度。索引牺牲了更高的存储开销和更慢的写入速度(因为我们不仅需要写入数据,还需要更新索引),以换取更快的读取速度。索引用于快速定位数据,而无需检查数据库表中的每一行。索引可以使用数据库表的一列或多列创建,为快速随机查找和高效访问有序记录奠定基础。
索引是一种数据结构,可以理解为指向实际数据所在位置的目录。因此,当我们在表的某一列上创建索引时,我们会将该列以及指向整行的指针存储在索引中。索引还用于创建同一数据的不同视图。对于大型数据集,这是一种指定不同过滤器或排序方案的绝佳方式,无需创建多个额外的数据副本。
数据库索引的一个特性是它们可以是密集的,也可以是稀疏的。每种索引特性都有其自身的权衡。让我们看看每种索引类型的工作原理:
密集索引
在密集索引中,表的每一行都会创建一个索引记录。由于索引的每个记录都包含搜索键值和指向实际记录的指针,因此可以直接定位记录。
密集索引在写入时比稀疏索引需要更多维护。由于每行都必须有一个条目,因此数据库必须在插入、更新和删除时维护索引。每行都有一个条目也意味着密集索引需要更多内存。密集索引的好处是只需二分查找即可快速找到值。密集索引也不对数据施加任何排序要求。
稀疏索引
在稀疏索引中,仅为部分记录创建记录。
稀疏索引在写入时所需的维护比密集索引少,因为它们只包含值的子集。较轻的维护负担意味着插入、更新和删除操作会更快。条目较少也意味着索引将占用更少的内存。由于二分查找之后通常会进行页面扫描,因此查找数据的速度较慢。处理有序数据时,稀疏索引也是可选的。
规范化和非规范化
条款
在进一步讨论之前,让我们先了解一下规范化和非规范化中常用的一些术语。
按键
主键:可用于唯一标识表的每一行的列或列组。
复合键:由多列组成的主键。
超级键:可以唯一标识表中所有行的所有键的集合。
候选键:唯一标识表中行的属性。
外键:它是对另一个表的主键的引用。
备用键:不是主键的键称为备用键。
代理键:当没有其他列能够容纳主键的属性时,系统生成的值可以唯一地标识表中的每个条目。
依赖项
部分依赖:当主键决定其他一些属性时发生。
函数依赖:它是两个属性之间存在的关系,通常是表中的主键和非键属性之间存在的关系。
传递函数依赖:当某些非关键属性决定某些其他属性时发生。
异常
数据库异常是指由于规划错误或将所有内容存储在扁平数据库中而导致数据库出现缺陷的情况。这类问题通常通过规范化流程来解决。
数据库异常有三种类型:
插入异常:当我们无法在没有其他属性的情况下在数据库中插入某些属性时发生。
更新异常:数据冗余或部分更新时发生。也就是说,数据库的正确更新需要其他操作,例如添加、删除或两者兼而有之。
删除异常:删除某些数据需要删除其他数据。
例子
让我们考虑以下未标准化的表格:
ID | 姓名 | 角色 | 团队 |
---|---|---|---|
1 | 彼得 | 软件工程师 | 一个 |
2 | 布莱恩 | DevOps工程师 | B |
3 | 海莉 | 产品经理 | 碳 |
4 | 海莉 | 产品经理 | 碳 |
5 | 史蒂夫 | 前端工程师 | D |
假设我们雇佣了一位新员工“John”,但他可能不会立即被分配到团队。由于团队属性尚未存在,这将导致插入异常。
接下来,假设 C 团队的 Hailey 晋升了,为了在数据库中反映这一变化,我们需要更新 2 行以保持一致性,这可能会导致更新异常。
最后,我们想删除团队 B,但要做到这一点,我们还需要删除姓名和角色等其他信息,这是删除异常的一个例子。
正常化
规范化是组织数据库中数据的过程。这包括创建表并根据规则建立表之间的关系。这些规则旨在保护数据,并通过消除冗余和不一致的依赖关系来提高数据库的灵活性。
为什么我们需要规范化?
规范化的目标是消除冗余数据并确保数据的一致性。完全规范化的数据库允许扩展其结构以容纳新类型的数据,而无需过多地更改现有结构。因此,与数据库交互的应用程序受到的影响最小。
范式
范式是一系列确保数据库规范化的准则。让我们讨论一些基本的范式:
1NF
要使表满足第一范式 (1NF),它应遵循以下规则:
- 不允许重复组。
- 使用主键标识每组相关数据。
- 相关数据集应该有一个单独的表。
- 不允许在同一列中混合数据类型。
2NF
对于满足第二范式(2NF)的表,它应该遵循以下规则:
- 满足第一范式(1NF)。
- 不应有任何部分依赖。
第三范式
对于满足第三范式(3NF)的表,它应该遵循以下规则:
- 满足第二范式(2NF)。
- 不允许传递函数依赖。
BCNF
Boyce-Codd范式(简称BCNF)是第三范式(3NF)的略强版本,用于处理3NF最初定义时未处理的某些类型的异常。有时它也被称为3.5范式(3.5NF)。
对于符合 Boyce-Codd 范式 (BCNF) 的表,它应遵循以下规则:
- 满足第三范式(3NF)。
- 对于每个函数依赖 X → Y,X 应该是超键。
还有更多范式,例如 4NF、5NF 和 6NF,但我们在此不做讨论。观看这个精彩的视频,它会详细介绍这些范式。
在关系数据库中,如果一个关系满足第三范式,则通常将其描述为“规范化的” 。大多数 3NF 关系不存在插入、更新和删除异常。
与许多正式的规则和规范一样,现实世界的情况并不总是允许完美遵守。如果您决定违反规范化的前三个规则之一,请确保您的应用程序能够预见可能发生的任何问题,例如冗余数据和不一致的依赖关系。
优势
以下是规范化的一些优点:
- 减少数据冗余。
- 更好的数据设计。
- 提高数据一致性。
- 强制参照完整性。
缺点
让我们看看规范化的一些缺点:
- 数据设计很复杂。
- 性能较慢。
- 维护开销。
- 需要更多连接。
非规范化
非规范化是一种数据库优化技术,我们会将冗余数据添加到一个或多个表中。这可以帮助我们避免关系数据库中昂贵的连接操作。它试图以牺牲部分写入性能为代价来提高读取性能。数据的冗余副本会被写入多个表中,以避免昂贵的连接操作。
一旦数据通过联邦和分片等技术实现分布式,管理跨网络的连接操作就会进一步增加复杂性。非规范化或许可以避免这种复杂的连接操作。
注意:非规范化并不意味着逆转规范化。
优势
让我们看看非规范化的一些优点:
- 检索数据更快。
- 编写查询更加容易。
- 减少表格数量。
- 管理方便。
缺点
以下是非规范化的一些缺点:
- 昂贵的插入和更新。
- 增加了数据库设计的复杂性。
- 增加数据冗余。
- 数据不一致的可能性更大。
ACID 和 BASE 一致性模型
让我们讨论一下 ACID 和 BASE 一致性模型。
酸
ACID 代表原子性、一致性、隔离性和持久性。ACID 属性用于在事务处理期间维护数据完整性。
为了在事务前后保持一致性,关系数据库遵循 ACID 属性。让我们理解以下术语:
原子
事务中的所有操作都成功,或者每个操作都回滚。
持续的
交易完成后,数据库的结构是健全的。
孤立
事务之间不会相互竞争。数据库会调节对数据的访问竞争,使事务看起来是按顺序运行的。
耐用的
一旦事务完成并且写入和更新已写入磁盘,即使发生系统故障,它也会保留在系统中。
根据
随着数据量的增长和高可用性需求的提升,数据库设计方法也发生了巨大的变化。为了提高扩展能力并同时保持高可用性,我们将逻辑从数据库迁移到独立的服务器。这样,数据库变得更加独立,并专注于实际的数据存储过程。
在 NoSQL 数据库世界中,ACID 事务不太常见,因为一些数据库放宽了对即时一致性、数据新鲜度和准确性的要求,以获得其他好处,如规模和弹性。
BASE 属性比 ACID 保证要宽松得多,但这两个一致性模型之间并没有直接的一一对应关系。让我们理解以下术语:
基本可用性
该数据库似乎大部分时间都在运行。
软状态
存储不必具有写一致性,不同的副本也不必始终保持相互一致性。
最终一致性
数据可能不会立即保持一致,但最终会变得一致。即使由于不一致而无法给出正确的响应,系统中的读取仍然是可能的。
ACID 与 BASE 的权衡
我们的应用程序究竟需要 ACID 还是 BASE 一致性模型,并没有一个标准的答案。这两种模型的设计初衷都是为了满足不同的需求。在选择数据库时,我们需要兼顾这两种模型的特性以及应用程序的实际需求。
鉴于 BASE 的松散一致性,如果开发人员为其应用程序选择 BASE 存储,他们需要对数据一致性有更深入的了解,并且更加严格。熟悉所选数据库的 BASE 行为并在这些约束下工作至关重要。
另一方面,与 ACID 事务的简单性相比,围绕 BASE 限制进行规划有时会成为一个重大劣势。完全 ACID 数据库非常适合那些对数据可靠性和一致性至关重要的用例。
CAP定理
CAP 定理指出,分布式系统只能提供三个所需特性中的两个:一致性、可用性和分区容错性 (CAP)。
我们来详细了解一下CAP定理所指的三个分布式系统特征。
一致性
一致性意味着所有客户端无论连接到哪个节点,都能同时看到相同的数据。为了实现这一点,每当数据写入一个节点时,都必须立即转发或复制到系统中的所有节点,写入才会被视为“成功”。
可用性
可用性意味着任何发出数据请求的客户端都会得到响应,即使一个或多个节点发生故障。
分区容错
分区容错意味着即使出现消息丢失或部分故障,系统仍能继续工作。具有分区容错能力的系统可以承受任何程度的网络故障,但不会造成整个网络瘫痪。数据在节点和网络组合之间进行充分复制,确保系统在间歇性中断的情况下保持正常运行。
一致性-可用性权衡
我们生活在物理世界中,无法保证网络的稳定性,因此分布式数据库必须选择分区容错性(P)。这意味着在一致性(C)和可用性(A)之间进行权衡。
CA数据库
CA 数据库在所有节点上保持一致性和可用性。如果系统中任意两个节点之间出现分区,则 CA 数据库无法做到这一点,因此无法提供容错能力。
例如:PostgreSQL、MariaDB。
CP数据库
CP 数据库以牺牲可用性为代价,实现了一致性和分区容忍性。当任意两个节点之间发生分区时,系统必须关闭不一致的节点,直到分区问题得到解决。
例如:MongoDB、Apache HBase。
AP数据库
AP 数据库以牺牲一致性为代价,实现了可用性和分区容忍度。发生分区时,所有节点仍然可用,但分区错误端的节点可能会返回比其他节点更旧的数据版本。分区问题解决后,AP 数据库通常会重新同步节点,以修复系统中的所有不一致性。
PACELC定理
PACELC 定理是 CAP 定理的扩展。CAP 定理指出,在分布式系统中,当出现网络分区 (P) 时,必须在可用性 (A) 和一致性 (C) 之间做出选择。
PACELC 扩展了 CAP 定理,引入了延迟 (L) 作为分布式系统的附加属性。该定理指出,否则 (E),即使系统在没有分区的情况下正常运行,也必须在延迟 (L) 和一致性 (C) 之间做出选择。
PACELC 定理最早由Daniel J. Abadi描述。
PACELC 定理是为了解决 CAP 定理的一个关键限制而开发的,因为它没有为性能或延迟做出任何规定。
例如,根据CAP定理,如果查询在30天后返回响应,则可以认为数据库可用。显然,这样的延迟对于任何实际应用来说都是不可接受的。
交易
事务是一系列被视为“单个工作单元”的数据库操作。事务中的操作要么全部成功,要么全部失败。这样,事务的概念在系统部分发生故障时仍能维持数据完整性。并非所有数据库都选择支持 ACID 事务,通常是因为它们优先考虑其他难以或理论上不可能同时实现的优化。
通常关系数据库支持ACID事务,非关系数据库不支持(有例外)。
州
数据库中的事务可以处于以下状态之一:
积极的
在此状态下,交易正在执行。这是每个交易的初始状态。
部分承诺
当事务执行其最终操作时,它被称为处于部分提交状态。
坚定的
如果一个事务成功执行了所有操作,则称该事务已提交。此时,其所有影响均已永久地在数据库系统上建立。
失败的
如果数据库恢复系统进行的任何检查失败,则该事务处于失败状态。失败的事务将无法继续进行。
已中止
如果任何检查失败,导致事务进入失败状态,则恢复管理器将回滚其在数据库上的所有写入操作,使数据库恢复到执行事务之前的原始状态。处于此状态的事务将被中止。
事务中止后,数据库恢复模块可以选择以下两种操作之一:
- 重新开始交易
- 终止交易
已终止
如果没有任何回滚或事务来自已提交状态,则系统是一致的并准备好进行新事务,并且旧事务被终止。
分布式事务
分布式事务是跨两个或多个数据库执行的一组数据操作。它通常由通过网络连接的不同节点协调,但也可能跨越单个服务器上的多个数据库。
为什么需要分布式事务?
与单个数据库上的 ACID 事务不同,分布式事务涉及更改多个数据库上的数据。因此,分布式事务处理更加复杂,因为数据库必须将事务中更改的提交或回滚作为一个独立单元进行协调。
换句话说,所有节点必须提交,否则所有节点必须中止,整个事务回滚。这就是我们需要分布式事务的原因。
现在,让我们看一些流行的分布式事务解决方案:
两阶段提交
两阶段提交(2PC)协议是一种分布式算法,它协调参与分布式事务的所有进程是否提交或中止(回滚)事务。
该协议即使在许多系统发生临时故障的情况下也能实现其目标,因此被广泛使用。然而,它并非对所有可能的故障配置都具有弹性,在极少数情况下,需要手动干预来补救。
该协议需要一个协调器节点,该节点主要负责协调和监督不同节点之间的事务。协调器会尝试分两个阶段在一组进程之间建立共识,因此得名。
阶段
两阶段提交包含以下几个阶段:
准备阶段
准备阶段涉及协调器节点从每个参与节点收集共识。除非每个节点都响应“已准备好”,否则交易将被中止。
提交阶段
如果所有参与者都向协调器响应“已准备好” ,则协调器会要求所有节点提交事务。如果发生故障,则事务将被回滚。
问题
两阶段提交协议可能存在以下问题:
- 如果其中一个节点崩溃了怎么办?
- 如果协调器本身崩溃了怎么办?
- 它是一个阻塞协议。
三阶段提交
三阶段提交(3PC)是两阶段提交的扩展,其中提交阶段分为两个阶段。这有助于解决两阶段提交协议中出现的阻塞问题。
阶段
三阶段提交包含以下几个阶段:
准备阶段
此阶段与两阶段提交相同。
预提交阶段
协调器发出预提交消息,所有参与节点必须确认该消息。如果参与者未能及时收到此消息,则事务将被中止。
提交阶段
这一步也和两阶段提交协议类似。
为什么预提交阶段很有用?
预提交阶段完成以下任务:
- 如果在此阶段找到参与者节点,则表示所有参与者均已完成第一阶段。准备阶段的完成得到保证。
- 每个阶段现在都可以超时并避免无限期的等待。
传奇
Saga 是一系列本地事务。每个本地事务都会更新数据库,并发布一条消息或事件来触发 Saga 中的下一个本地事务。如果某个本地事务因违反业务规则而失败,Saga 会执行一系列补偿事务,以撤销先前本地事务所做的更改。
协调
常见的实现方式有两种:
- 编排:每个本地事务都会发布触发其他服务中的本地事务的领域事件。
- 编排:编排器告诉参与者要执行哪些本地事务。
问题
- Saga 模式特别难以调试。
- 传奇参与者之间存在循环依赖的风险。
- 缺乏参与者数据隔离带来了持久性挑战。
- 测试很困难,因为必须运行所有服务才能模拟交易。
分片
在讨论分片之前,我们先来讨论一下数据分区:
数据分区
数据分区是一种将数据库拆分成多个较小部分的技术。它是将数据库或表拆分到多台计算机上的过程,以提高数据库的可管理性、性能和可用性。
方法
有很多不同的方法可以将应用程序数据库拆分成多个较小的数据库。以下是各种大型应用程序最常用的三种方法:
水平分区(或分片)
在这个策略中,我们根据分区键定义的值范围,对表数据进行水平拆分,也称为数据库分片。
垂直分区
在垂直分区中,我们根据列对数据进行垂直分区。我们将表划分为相对较小的表,每个表包含较少的元素,每个部分都位于单独的分区中。
在本教程中,我们将特别关注分片。
什么是分片?
分片是一种与水平分区相关的数据库架构模式,水平分区是指将一个表的行分成多个不同的表(称为分区或分片)的做法。每个分区具有相同的架构和列,但也包含共享数据的子集。同样,每个分区中保存的数据都是唯一的,并且与其他分区中保存的数据相互独立。
数据分片的合理性在于,在达到一定程度后,通过添加更多机器进行水平扩展比通过添加高性能服务器进行垂直扩展更经济、更可行。分片可以在应用程序级别或数据库级别实现。
分区标准
数据分区有许多可用的标准。一些最常用的标准是:
基于哈希
该策略根据散列算法将行划分为不同的分区,而不是根据连续索引对数据库行进行分组。
该方法的缺点是动态添加/删除数据库服务器的成本变得昂贵。
基于列表
在基于列表的分区中,每个分区都是根据列上的值列表而不是一组连续的值范围来定义和选择的。
基于范围
范围分区根据分区键值的范围将数据映射到不同的分区。换句话说,我们对表进行分区,使得每个分区包含分区键定义的给定范围内的行。
范围应连续但不重叠,每个范围指定分区的下限和上限(不包含)。任何等于或高于该范围上限的分区键值都将添加到下一个分区。
合成的
顾名思义,复合分区基于两种或多种分区技术对数据进行分区。我们首先使用一种技术对数据进行分区,然后使用相同或其他方法将每个分区进一步细分为子分区。
优势
但是为什么我们需要分片呢?以下是一些优点:
- 可用性:为分区数据库提供逻辑独立性,确保应用程序的高可用性。各个分区可以独立管理。
- 可扩展性:通过将数据分布在多个分区上,可以提高可扩展性。
- 安全性:通过将敏感数据和非敏感数据存储在不同的分区,有助于提高系统的安全性。这可以为敏感数据提供更好的可管理性和安全性。
- 查询性能:提高系统性能。系统不再需要查询整个数据库,而是只需查询较小的分区。
- 数据可管理性:将表和索引划分为更小、更易于管理的单元。
缺点
- 复杂性:分片通常会增加系统的复杂性。
- 跨分片连接:一旦数据库被分区并分布在多台机器上,执行跨多个数据库分片的连接通常不可行。由于需要从多台服务器检索数据,此类连接的性能效率不高。
- 重新平衡:如果数据分布不均匀或单个分片上的负载很大,在这种情况下,我们必须重新平衡分片,以便请求尽可能均匀地分布在分片之间。
何时使用分片?
以下是分片可能是正确选择的一些原因:
- 利用现有硬件代替高端机器。
- 维护不同地理区域的数据。
- 通过添加更多分片来快速扩展。
- 由于每台机器的负载较小,因此性能更好。
- 当需要更多并发连接时。
一致性哈希
让我们首先了解我们要解决的问题。
我们为什么需要这个?
在传统的基于哈希的分布式方法中,我们使用哈希函数对分区键(即请求 ID 或 IP)进行哈希运算。然后,我们将该哈希值与节点总数(服务器或数据库)进行模运算。这样就能得到我们想要路由请求的节点。
在哪里,
key
:请求ID或IP。
H
:哈希函数结果。
N
:节点总数。
Node
:请求将被路由到的节点。
问题在于,如果我们添加或删除一个节点,就会导致N
映射策略发生变化,因为相同的请求会映射到不同的服务器,从而破坏我们的映射策略。结果,大多数请求需要重新分配,效率非常低。
我们希望将请求均匀地分布在不同节点之间,以便能够以最小的代价添加或移除节点。因此,我们需要一个不直接依赖于节点(或服务器)数量的分布方案,以便在添加或移除节点时,最大限度地减少需要迁移的键的数量。
一致性哈希解决了这个水平可扩展性问题,它确保每次扩大或缩小规模时,我们不必重新排列所有键或触及所有服务器。
现在我们了解了问题,让我们详细讨论一致性哈希。
它是如何工作的
一致性哈希是一种分布式哈希方案,它通过在抽象圆环(或哈希环)上分配节点位置来独立于分布式哈希表中的节点数量运行。这使得服务器和对象能够在不影响整个系统的情况下进行扩展。
使用一致性哈希,只有K/N
数据需要重新分配。
在哪里,
R
:需要重新分发的数据。
K
:分区键的数量。
N
:节点数。
哈希函数的输出是一个0...m-1
我们可以在哈希环上表示的范围。我们对请求进行哈希处理,并根据输出结果将其分布在环上。同样,我们也对节点进行哈希处理,并将其分布在同一个环上。
在哪里,
key
:请求/节点ID或IP。
P
:哈希环上的位置。
m
:哈希环的总范围。
现在,当请求到达时,我们可以简单地将其按顺时针(也可以逆时针)路由到最近的节点。这意味着,如果添加或删除了新节点,我们可以使用最近的节点,而只有一小部分请求需要重新路由。
理论上,一致性哈希应该能够均匀分布负载,但实际并非如此。通常情况下,负载分布不均,一台服务器最终可能会处理大部分请求,从而成为热点,最终成为系统瓶颈。我们可以通过添加额外节点来解决这个问题,但这可能代价高昂。
让我们看看如何解决这些问题。
虚拟节点
为了确保负载分布更均匀,我们可以引入虚拟节点的概念,有时也称为 VNode。
哈希范围并非为节点分配单一位置,而是被划分为多个较小的范围,每个物理节点被分配多个较小的范围。每个子范围都被视为一个虚拟节点 (VNode)。因此,虚拟节点本质上是现有的物理节点,它们在哈希环上进行了多次映射,以最大限度地减少对节点分配范围的更改。
为此,我们可以使用k
多个哈希函数。
在哪里,
key
:请求/节点ID或IP。
k
:哈希函数的数量。
P
:哈希环上的位置。
m
:哈希环的总范围。
由于 VNode 通过将哈希范围划分为更小的子范围,帮助将负载更均匀地分布在集群中的物理节点上,因此在添加或删除节点后,这加快了重新平衡的过程。这也有助于我们降低出现热点的可能性。
数据复制
为了确保高可用性和持久性,一致性哈希会N
在系统中的多个节点上复制每个数据项,其值N
等于复制因子。
复制因子是指接收相同数据副本的节点数。在最终一致性系统中,此操作是异步完成的。
优势
让我们看看一致性哈希的一些优点:
- 使快速扩大和缩小变得更加可预测。
- 促进跨节点的分区和复制。
- 实现可扩展性和可用性。
- 减少热点。
缺点
以下是一致性哈希的一些缺点:
- 增加了复杂性。
- 连锁故障。
- 负载分布仍然可能不均匀。
- 当节点暂时发生故障时,密钥管理的成本可能会很高。
示例
让我们看一些使用一致性哈希的例子:
- Apache Cassandra中的数据分区。
- 在Amazon DynamoDB中的多个存储主机之间分配负载。
数据库联合
联合(或功能分区)按功能拆分数据库。联合架构使多个不同的物理数据库在最终用户看来,看起来像是一个逻辑数据库。
联邦中的所有组件都通过一个或多个联邦模式绑定在一起,这些模式表达了整个联邦中数据的共性。这些联邦模式用于指定联邦组件之间可共享的信息,并为它们之间的通信提供通用基础。
联合还能提供来自多个数据源的统一、有凝聚力的视图。联合系统的数据源可以包括数据库以及各种其他形式的结构化和非结构化数据。
特征
让我们看一下联合数据库的一些关键特征:
- 透明性:联邦数据库屏蔽了用户之间的差异以及底层数据源的实现。因此,用户无需了解数据的存储位置。
- 异构性:数据源可能存在诸多差异。联邦数据库系统可以处理不同的硬件、网络协议、数据模型等。
- 可扩展性:可能需要新的数据源来满足不断变化的业务需求。一个好的联邦数据库系统需要能够轻松添加新的数据源。
- 自主性:联合数据库不会改变现有的数据源,接口应该保持不变。
- 数据集成:联合数据库可以集成来自不同协议、数据库管理系统等的数据。
优势
以下是联合数据库的一些优点:
- 灵活的数据共享。
- 数据库组件之间的自主性。
- 以统一的方式访问异构数据。
- 应用程序与遗留数据库之间没有紧密耦合。
缺点
以下是联合数据库的一些缺点:
- 增加更多硬件和额外的复杂性。
- 合并两个数据库的数据很复杂。
- 依赖自主数据源。
- 查询性能和可扩展性。
N层架构
N 层架构将应用程序划分为逻辑层和物理层。层是分离职责和管理依赖关系的一种方式。每层都有特定的职责。较高层可以使用较低层的服务,但反之则不行。
各个层在物理上是分离的,运行在不同的机器上。一个层可以直接调用另一个层,也可以使用异步消息传递。虽然每个层可能托管在各自的层中,但这不是必需的。多个层可以托管在同一层上。物理上分离层可以提高可扩展性和弹性,但会增加额外网络通信带来的延迟。
N 层体系结构可以有两种类型:
- 在封闭的层架构中,一个层只能调用紧接着的下一层。
- 在开放层体系结构中,一个层可以调用其下方的任何层。
封闭层架构限制了层与层之间的依赖关系。然而,如果一层只是简单地将请求传递到下一层,则可能会产生不必要的网络流量。
N 层体系结构的类型
让我们看一些 N-Tier 架构的示例:
三层架构
3 层应用广泛,由以下不同层组成:
- 表示层:处理用户与应用程序的交互。
- 业务逻辑层:接受来自应用层的数据,根据业务逻辑验证它并将其传递给数据层。
- 数据访问层:从业务层接收数据并对数据库执行必要的操作。
两层架构
在此架构中,表示层在客户端运行并与数据存储进行通信。客户端和服务器之间没有业务逻辑层或直接层。
单层或 1 层架构
它是最简单的一种,因为它相当于在个人计算机上运行应用程序。应用程序运行所需的所有组件都在单个应用程序或服务器上。
优势
以下是使用 N 层架构的一些优点:
- 可以提高可用性。
- 由于各层可以像防火墙一样运作,因此安全性更高。
- 单独的层级允许我们根据需要扩展它们。
- 改善维护,因为不同的人可以管理不同的层级。
缺点
以下是 N 层架构的一些缺点:
- 增加了整个系统的复杂性。
- 随着层数的增加,网络延迟也会增加。
- 由于每一层都有自己的硬件成本,因此成本较高。
- 网络安全难以管理。
消息代理
消息代理是一种软件,它使应用程序、系统和服务能够相互通信并交换信息。消息代理通过在正式的消息传递协议之间转换消息来实现这一点。这使得相互依赖的服务能够直接相互“对话” ,即使它们是用不同的语言编写的或在不同的平台上实现的。
消息代理可以验证、存储、路由并将消息传递到适当的目的地。它们充当其他应用程序之间的中介,允许发送者在不知道接收者位置、是否处于活动状态或接收者数量的情况下发送消息。这有助于解耦系统内的流程和服务。
模型
消息代理提供两种基本的消息分发模式或消息传递样式:
- 点对点消息传递:这是消息队列中使用的分发模式,消息的发送者和接收者之间存在一对一的关系。
- 发布-订阅消息传递:在这种消息分发模式中,通常称为“发布/订阅”,每条消息的生产者将其发布到一个主题,多个消息消费者订阅他们想要从中接收消息的主题。
我们将在后面的教程中详细讨论这些消息传递模式。
消息代理与事件流
消息代理可以支持两种或多种消息传递模式,包括消息队列和发布/订阅模式,而事件流平台仅提供发布/订阅式分发模式。事件流平台专为处理大量消息而设计,易于扩展。它们能够将记录流按类别(称为主题)排序,并将其存储预定的时间。然而,与消息代理不同,事件流平台无法保证消息的传递,也无法跟踪哪些消费者已收到消息。
事件流平台比消息代理具有更高的可扩展性,但确保容错的功能(如消息重新发送)较少,而且消息路由和排队功能也较为有限。
消息代理与企业服务总线(ESB)
企业服务总线 (ESB)基础设施非常复杂,集成起来可能非常困难,维护成本也非常高昂。生产环境中出现问题时,故障排除非常困难,而且 ESB 难以扩展,更新过程也非常繁琐。
而消息代理是ESB 的“轻量级”替代方案,它以较低的成本提供类似的功能,即一种服务间通信机制。它们非常适合在微服务架构中使用,而随着 ESB 的失宠,微服务架构变得越来越流行。
示例
以下是一些常用的消息代理:
消息队列
消息队列是一种服务间通信形式,用于实现异步通信。它异步地从生产者接收消息并将其发送给消费者。
队列用于在大型分布式系统中有效地管理请求。在处理负载最小且数据库规模较小的小型系统中,写入速度可以预期地快。然而,在更复杂、更大型的系统中,写入所需的时间几乎是不确定的。
在职的
消息会存储在队列中,直到被处理并删除。每条消息仅由一个消费者处理一次。工作原理如下:
- 生产者将作业发布到队列,然后通知用户作业状态。
- 消费者从队列中取出作业,进行处理,然后发出作业完成的信号。
优势
让我们讨论一下使用消息队列的一些优点:
- 可扩展性:消息队列使我们能够在需要的地方进行精确扩展。当工作负载达到峰值时,我们应用程序的多个实例都可以将请求添加到队列中,而不会发生冲突。
- 解耦:消息队列消除了组件之间的依赖关系,大大简化了解耦应用程序的实现。
- 性能:消息队列支持异步通信,这意味着生产者和消费消息的端点只与队列交互,而不会相互交互。生产者可以将请求添加到队列,而无需等待请求被处理。
- 可靠性:队列使我们的数据持久,并减少系统不同部分离线时发生的错误。
特征
现在,让我们讨论一下消息队列的一些所需功能:
推式或拉式交付
大多数消息队列都提供推送和拉取两种消息检索方式。拉取意味着持续查询队列中的新消息。推送意味着当有消息可用时通知消费者。我们还可以使用长轮询,让拉取等待指定的时间以等待新消息到达。
FIFO(先进先出)队列
在这些队列中,最旧(或第一个)的条目(有时称为队列的“头”)首先被处理。
安排或延迟交货
许多消息队列支持为消息设置特定的投递时间。如果我们需要为所有消息设置一个共同的延迟时间,我们可以设置一个延迟队列。
至少一次投递
消息队列可以存储消息的多个副本以实现冗余和高可用性,并在发生通信故障或错误时重新发送消息以确保它们至少被传递一次。
恰好一次交付
当无法容忍重复时,FIFO(先进先出)消息队列将通过自动过滤重复项来确保每条消息只传递一次(且仅传递一次)。
死信队列
死信队列是指其他队列可以向其发送无法成功处理的消息的队列。这样,可以轻松地将这些消息留出以供进一步检查,而不会阻塞队列处理,也不会在可能永远无法成功使用的消息上浪费 CPU 周期。
订购
大多数消息队列都提供尽力排序,以确保消息通常按照发送的顺序传递,并且一条消息至少传递一次。
毒丸计划信息
毒丸消息是一种可以接收但无法处理的特殊消息。它是一种机制,用于向消费者发出信号,使其结束工作,不再等待新的输入,类似于在客户端/服务器模型中关闭套接字。
安全
消息队列将对尝试访问队列的应用程序进行身份验证,这使我们能够通过网络以及在队列本身中加密消息。
任务队列
任务队列接收任务及其相关数据,运行它们,然后交付结果。它们支持调度,并可用于在后台运行计算密集型作业。
背压
如果队列开始大幅增长,队列大小可能会变得大于内存,导致缓存未命中、磁盘读取,甚至性能下降。背压可以通过限制队列大小来提供帮助,从而保持高吞吐率和队列中已有作业的良好响应时间。一旦队列填满,客户端会收到服务器繁忙或 HTTP 503 状态代码,以便稍后重试。客户端可以稍后重试请求,或许可以使用指数退避策略。
示例
以下是一些广泛使用的消息队列:
发布-订阅
与消息队列类似,发布-订阅模式也是一种服务间通信形式,可以促进异步通信。在发布/订阅模型中,任何发布到主题的消息都会立即推送给该主题的所有订阅者。
消息主题的订阅者通常执行不同的功能,并且可以并行地对消息执行不同的操作。发布者无需知道谁在使用其广播的信息,订阅者也无需知道消息的来源。这种消息传递方式与消息队列略有不同,在消息队列中,发送消息的组件通常知道消息的目的地。
在职的
与消息队列(会批量处理消息直至被检索)不同,消息主题传输消息时几乎无需排队,而是立即将其推送给所有订阅者。其工作原理如下:
- 消息主题提供了一种轻量级机制来广播异步事件通知和端点,允许软件组件连接到主题以发送和接收这些消息。
- 为了广播消息,称为发布者的组件只需将消息推送到主题即可。
- 订阅该主题的所有组件(称为订阅者)将收到广播的每条消息。
优势
让我们讨论一下使用发布-订阅的一些优点:
- 消除轮询:消息主题支持即时推送,无需消息消费者定期检查或“轮询”新信息和更新。这可以加快响应速度,并降低延迟问题。延迟问题在无法容忍延迟的系统中尤为严重。
- 动态定位:发布/订阅使服务发现更加轻松、自然且不易出错。发布者无需维护应用程序可以发送消息的对等节点名册,而是只需向主题发布消息即可。然后,任何感兴趣的方都可以将其端点订阅到该主题,并开始接收这些消息。订阅者可以更改、升级、增加或消失,系统会进行动态调整。
- 解耦和独立扩展:发布者和订阅者是解耦的,彼此独立工作,这使我们能够独立开发和扩展它们。
- 简化通信:发布-订阅模型通过删除所有点对点连接并使用与消息主题的单一连接来降低复杂性,这将管理订阅并决定应将哪些消息传递到哪些端点。
特征
现在,让我们讨论一下发布-订阅模式的一些理想特性:
推送
当消息发布到消息主题时,发布/订阅消息传递会立即推送异步事件通知。当消息可用时,订阅者会收到通知。
多种交付协议
在发布-订阅模型中,主题通常可以连接到多种类型的端点,例如消息队列、无服务器功能、HTTP 服务器等。
扇出
这种场景发生在一条消息被发送到一个主题,然后被复制并推送到多个端点时。扇出提供异步事件通知,从而允许并行处理。
过滤
此功能使订阅者能够创建消息过滤策略,以便它只收到它感兴趣的通知,而不是接收发布到该主题的每条消息。
耐用性
发布/订阅消息服务通常通过在多台服务器上存储同一条消息的副本来提供非常高的持久性,并且至少传递一次。
安全
消息主题对尝试发布内容的应用程序进行身份验证,这使我们能够使用加密端点并加密通过网络传输的消息。
示例
以下是发布-订阅常用的一些技术:
企业服务总线(ESB)
企业服务总线 (ESB) 是一种架构模式,通过集中式软件组件执行应用程序之间的集成。它执行数据模型转换、处理连接、执行消息路由、转换通信协议,并可能管理多个请求的组合。ESB 可以将这些集成和转换作为服务接口提供,以供新应用程序重用。
优势
理论上,集中式 ESB 可以标准化并显著简化整个企业服务之间的通信、消息传递和集成。使用 ESB 的优势如下:
- 提高开发人员的工作效率:使开发人员能够将新技术融入应用程序的一部分,而无需触及应用程序的其余部分。
- 更简单、更具成本效益的可扩展性:组件可以独立于其他组件进行扩展。
- 更强的弹性:一个组件的故障不会影响其他组件,并且每个微服务都可以遵守自己的可用性要求,而不会危及系统中其他组件的可用性。
缺点
虽然许多组织已经成功部署了 ESB,但在许多其他组织中,ESB 却被视为瓶颈。以下是使用 ESB 的一些缺点:
- 对一个集成进行更改或增强可能会破坏使用相同集成的其他集成。
- 单点故障可能会导致所有通信中断。
- ESB 的更新通常会影响现有的集成,因此执行任何更新都需要进行大量测试。
- ESB 是集中管理的,这使得跨团队协作具有挑战性。
- 配置和维护复杂度高。
示例
以下是一些广泛使用的企业服务总线(ESB)技术:
单体应用和微服务
巨石
单体应用是一个独立的、自包含的应用程序。它以单一单元的形式构建,不仅负责某项特定任务,还能执行满足业务需求所需的所有步骤。
优势
以下是整体式架构的一些优点:
- 开发或调试简单。
- 快速可靠的通信。
- 易于监控和测试。
- 支持ACID事务。
缺点
整体式架构的一些常见缺点是:
- 随着代码库的增长,维护变得越来越困难。
- 紧密耦合的应用程序,难以扩展。
- 需要致力于特定的技术堆栈。
- 每次更新时,整个应用程序都会重新部署。
- 可靠性降低,因为一个错误就可能导致整个系统崩溃。
- 难以扩展或采用新技术。
模块化整体式
模块化整体是一种构建和部署单个应用程序(即整体部分)的方法,但我们以一种将代码分解为独立模块的方式构建它,以满足应用程序所需的每个功能。
这种方法减少了模块的依赖关系,这样我们就可以增强或修改某个模块而不会影响其他模块。如果做得好,从长远来看,这种方法非常有益,因为它降低了系统规模增长过程中维护单体应用的复杂性。
微服务
微服务架构由一系列小型、自治的服务组成,其中每个服务都是独立的,并且应在有界上下文中实现单一业务功能。有界上下文是对业务逻辑的自然划分,它提供了领域模型所在的明确边界。
每个服务都有独立的代码库,可以由小型开发团队管理。服务可以独立部署,并且团队无需重建和重新部署整个应用程序即可更新现有服务。
服务负责持久化自身数据或外部状态(每个服务对应一个数据库)。这与传统模型不同,传统模型中由单独的数据层处理数据持久化。
特征
微服务架构风格具有以下特点:
- 松散耦合:服务应该松散耦合,以便能够独立部署和扩展。这将导致开发团队的分散化,从而使他们能够以最小的限制和操作依赖性更快地进行开发和部署。
- 精耕细作,专心致志:服务关注的是范围和职责,而非规模。服务应该专注于特定问题。简而言之,“只做一件事,并且做好”。理想情况下,它们可以独立于底层架构。
- 为企业而建:微服务架构通常围绕业务能力和优先级来组织。
- 弹性与容错:服务的设计应确保其在发生故障或错误时仍能正常运行。在服务可独立部署的环境中,容错能力至关重要。
- 高度可维护:服务应该易于维护和测试,因为无法维护的服务将被重写。
优势
以下是微服务架构的一些优点:
- 松散耦合的服务。
- 服务可以独立部署。
- 对于多个开发团队来说高度敏捷。
- 提高容错能力和数据隔离能力。
- 更好的可扩展性,因为每个服务都可以独立扩展。
- 消除对特定技术堆栈的任何长期承诺。
缺点
微服务架构带来了一系列挑战:
- 分布式系统的复杂性。
- 测试更加困难。
- 维护成本高(单个服务器、数据库等)。
- 服务间通信有其自身的挑战。
- 数据完整性和一致性。
- 网络拥塞和延迟。
最佳实践
让我们讨论一些微服务的最佳实践:
- 围绕业务领域建模服务。
- 服务应该具有松散的耦合性和较高的功能内聚性。
- 隔离故障并使用弹性策略来防止服务内的故障级联。
- 服务应该仅通过精心设计的 API 进行通信。避免泄露实现细节。
- 数据存储应该对拥有数据的服务保密
- 避免服务之间的耦合。耦合的原因包括共享数据库模式和僵化的通信协议。
- 一切去中心化。各个团队负责设计和构建服务。避免共享代码或数据模式。
- 使用断路器快速失败以实现容错。
- 确保 API 更改向后兼容。
陷阱
以下是微服务架构的一些常见陷阱:
- 服务边界不基于业务领域。
- 低估了构建分布式系统的难度。
- 服务之间的共享数据库或共同依赖关系。
- 缺乏业务协调。
- 缺乏明确的所有权。
- 缺乏幂等性。
- 尝试用 ACID 代替 BASE来做所有事情。
- 缺乏容错设计可能会导致级联故障。
警惕分布式单体
分布式单体系统类似于微服务架构,但其内部紧密耦合,如同单体应用。采用微服务架构有很多优势。但在构建分布式单体系统时,我们很有可能最终得到一个分布式单体系统。
如果以下任何一项适用于微服务,那么它就只是一个分布式整体:
- 需要低延迟通信。
- 服务不容易扩展。
- 服务之间的依赖关系。
- 共享相同的资源,例如数据库。
- 紧密耦合的系统。
使用微服务架构构建应用程序的主要原因之一是可扩展性。因此,微服务应该具有松散耦合的服务,使每个服务都保持独立。分布式单体架构则破坏了这一点,导致大多数组件相互依赖,增加了设计的复杂性。
微服务与面向服务架构(SOA)
您可能已经在互联网上看到过面向服务架构(SOA)的提及,有时甚至与微服务互换使用,但它们彼此不同,并且两种方法之间的主要区别在于范围。
面向服务架构 (SOA) 定义了一种通过服务接口使软件组件可重用的方法。这些接口使用通用的通信标准,并专注于最大化应用服务的可重用性。而微服务则构建为各种最小独立服务单元的集合,注重团队自主性和解耦性。
为什么你不需要微服务
所以,你可能会想,整体式架构看起来似乎是一个坏主意,为什么有人会使用它呢?
嗯,这得看情况。虽然每种方法都有各自的优缺点,但建议在构建新系统时从单体架构开始。重要的是要理解,微服务并非灵丹妙药,而是解决组织问题。微服务架构不仅关乎技术,也关乎组织优先事项和团队。
在决定转向微服务架构之前,您需要问自己以下问题:
- “团队是否太大而无法在共享代码库上有效工作?”
- “其他队伍是否被封锁了?”
- “微服务能为我们带来明确的商业价值吗?”
- “我的业务是否足够成熟,可以使用微服务?”
- “我们当前的架构是否会限制我们的通信开销?”
如果你的应用程序不需要拆分成微服务,就不需要这样做。所有应用程序都不需要拆分成微服务。
我们经常从Netflix等公司及其对微服务的运用中汲取灵感,但却忽略了一个事实:我们并非Netflix。他们在最终确定一个市场适用的解决方案之前,经历了大量的迭代和模型调整。而当他们发现并解决了他们试图解决的问题后,这种架构才被他们接受。
这就是为什么深入了解你的企业是否真的需要微服务至关重要。我想说的是,微服务是解决复杂问题的解决方案,如果你的企业没有复杂的问题,你就不需要它们。
事件驱动架构(EDA)
事件驱动架构 (EDA) 是指使用事件作为系统内通信的一种方式。通常,它利用消息代理异步发布和消费事件。发布者不知道谁在消费事件,而消费者彼此之间也互不知情。事件驱动架构只是一种在系统内实现服务间松散耦合的方法。
什么是事件?
事件是表示系统状态变化的数据点。它不指定应该发生什么以及这些变化将如何影响系统,而仅通知系统特定的状态变化。当用户执行操作时,会触发事件。
成分
事件驱动架构有三个关键组件:
- 事件生产者:向路由器发布事件。
- 事件路由器:过滤事件并将事件推送给消费者。
- 事件消费者:使用事件来反映系统的变化。
注意:图中的点代表系统中的不同事件。
模式
实现事件驱动架构的方法有多种,我们使用哪种方法取决于用例,但以下是一些常见示例:
注意:每种方法都会单独讨论。
优势
让我们讨论一下一些优点:
- 解耦生产者和消费者。
- 高度可扩展和分布式。
- 轻松添加新消费者。
- 提高敏捷性。
挑战
以下是事件驱动架构的一些挑战:
- 保证交货。
- 错误处理很困难。
- 事件驱动系统通常很复杂。
- 恰好一次、按顺序处理事件。
用例
以下是事件驱动架构有益的一些常见用例:
- 元数据和指标。
- 服务器和安全日志。
- 集成异构系统。
- 扇出和并行处理。
示例
以下是一些广泛使用的实现事件驱动架构的技术:
事件溯源
不要仅仅存储领域中数据的当前状态,而要使用仅追加存储来记录对该数据执行的一系列操作。该存储充当记录系统,可用于具体化领域对象。
这可以简化复杂领域的任务,避免同步数据模型和业务领域,同时提高性能、可扩展性和响应能力。它还可以确保事务数据的一致性,并维护完整的审计跟踪和历史记录,以便采取补偿措施。
事件溯源与事件驱动架构(EDA)
事件溯源似乎经常与事件驱动架构 (EDA)混淆。事件驱动架构是指使用事件在服务边界之间进行通信。通常,它利用消息代理在其他边界内异步发布和消费事件。
而事件溯源则将事件视为状态,这是一种不同的数据存储方式。我们存储的不是当前状态,而是事件。此外,事件溯源是实现事件驱动架构的几种模式之一。
优势
让我们讨论一下使用事件源的一些优点:
- 非常适合实时数据报告。
- 非常适合故障安全,数据可以从事件存储中重建。
- 极其灵活,可以存储任何类型的消息。
- 实现高合规系统审计日志功能的首选方式。
缺点
以下是事件溯源的缺点:
- 需要极其高效的网络基础设施。
- 需要一种可靠的方法来控制消息格式,例如模式注册表。
- 不同的事件将包含不同的有效载荷。
命令和查询责任分离(CQRS)
命令查询职责分离 (CQRS) 是一种将系统操作划分为命令和查询的架构模式。它最初由Greg Young提出。
在 CQRS 中,命令是一种指令,用于执行特定任务。它旨在改变某些内容,不返回任何值,仅指示成功或失败。查询是一种信息请求,它不会改变系统状态或引起任何副作用。
CQRS 的核心原则是命令和查询的分离。它们在系统中扮演着截然不同的角色,而分离意味着可以根据需要对每个命令进行优化,这对分布式系统来说非常有益。
带有事件源的 CQRS
CQRS 模式通常与事件源模式一起使用。基于 CQRS 的系统使用独立的读写数据模型,每个模型都针对相关任务进行定制,并且通常位于物理上独立的存储中。
与事件源模式一起使用时,事件存储是写入模型,也是官方的信息来源。基于 CQRS 的系统的读取模型提供数据的物化视图,通常是高度非规范化的视图。
优势
让我们讨论一下 CQRS 的一些优点:
- 允许独立扩展读写工作负载。
- 更容易扩展、优化和架构变更。
- 更接近松散耦合的业务逻辑。
- 应用程序在查询时可以避免复杂的连接。
- 明确系统行为之间的界限。
缺点
以下是 CQRS 的一些缺点:
- 更复杂的应用程序设计。
- 可能会出现消息失败或消息重复的情况。
- 处理最终一致性是一项挑战。
- 加大系统维护力度。
用例
以下是 CQRS 有用的一些场景:
- 数据读取的性能必须与数据写入的性能分开进行微调。
- 该系统预计会随着时间的推移而发展,并且可能包含模型的多个版本,或者业务规则会定期发生变化。
- 与其他系统的集成,特别是与事件源结合,其中一个子系统的暂时故障不应影响其他子系统的可用性。
- 更好的安全性,确保只有正确的域实体才能对数据执行写入操作。
API 网关
API 网关是一种 API 管理工具,位于客户端和一组后端服务之间。它是系统的单一入口点,封装了内部系统架构,并为每个客户端提供定制的 API。它还承担其他职责,例如身份验证、监控、负载均衡、缓存、限流、日志记录等。
为什么我们需要 API 网关?
微服务提供的 API 粒度通常与客户端需求不同。微服务通常提供细粒度的 API,这意味着客户端需要与多个服务交互。因此,API 网关可以为所有客户端提供单一入口点,并提供一些附加功能和更佳的管理。
特征
以下是 API 网关的一些所需功能:
优势
让我们看看使用 API 网关的一些优点:
- 封装 API 的内部结构。
- 提供 API 的集中视图。
- 简化客户端代码。
- 监控、分析、跟踪和其他此类功能。
缺点
API 网关可能存在以下缺点:
- 可能存在单点故障。
- 可能会影响性能。
- 如果扩展不当,可能会成为瓶颈。
- 配置可能具有挑战性。
后端前端 (BFF) 模式
在“后端为前端”(BFF) 模式中,我们创建单独的后端服务,供特定的前端应用程序或接口使用。当我们想要避免为多个接口定制单个后端时,此模式非常有用。此模式最初由Sam Newman提出。
另外,有时微服务返回到前端的数据格式可能不正确,或者没有按照前端的需要进行过滤。为了解决这个问题,前端应该有一些逻辑来重新格式化数据,因此,我们可以使用 BFF 将部分逻辑转移到中间层。
后端对于前端模式的主要功能是从适当的服务获取所需的数据,格式化数据,并将其发送到前端。
GraphQL作为前端后端 (BFF) 的表现非常出色。
何时使用这种模式?
在以下情况下,我们应该考虑使用后端前端 (BFF) 模式:
- 共享或通用后端服务必须花费大量的开发开销来维护。
- 我们希望针对特定客户的需求优化后端。
- 对通用后端进行定制以适应多种接口。
示例
以下是一些广泛使用的网关技术:
REST、GraphQL、gRPC
良好的 API 设计始终是任何系统的关键部分。但选择正确的 API 技术也同样重要。因此,在本教程中,我们将简要讨论不同的 API 技术,例如 REST、GraphQL 和 gRPC。
什么是 API?
在我们了解 API 技术之前,让我们首先了解什么是 API。
API 是一组用于构建和集成应用软件的定义和协议。它有时被称为信息提供者和信息使用者之间的合同,用于确定生产者所需的内容以及消费者所需的内容。
换句话说,如果您想与计算机或系统交互以检索信息或执行功能,API 可以帮助您将您想要的内容传达给该系统,以便它能够理解并完成请求。
休息
REST API(也称为 RESTful API)是一种符合 REST 架构风格约束并允许与 RESTful Web 服务交互的应用程序编程接口。REST 代表表述性状态转移 (Representational State Transfer),由Roy Fielding于 2000 年首次提出。
在 REST API 中,基本单位是资源。
概念
让我们讨论一下 RESTful API 的一些概念。
约束
为了使 API 被视为RESTful,它必须符合以下架构约束:
- 统一接口:应该有一种与给定服务器交互的统一方式。
- 客户端-服务器:通过 HTTP 管理的客户端-服务器架构。
- 无状态:请求之间不应在服务器上存储任何客户端上下文。
- 可缓存:每个响应都应包括该响应是否可缓存以及在客户端可以缓存响应多长时间。
- 分层系统:应用程序架构需要由多层组成。
- 按需代码:返回可执行代码以支持应用程序的一部分。(可选)
HTTP 动词
HTTP 定义了一组请求方法来指示针对给定资源执行的期望操作。虽然它们也可以是名词,但这些请求方法有时被称为HTTP 动词。它们各自实现不同的语义,但其中一些方法共享一些共同的特征。
以下是一些常用的 HTTP 动词:
- GET:请求指定资源的表述。
- HEAD:响应与请求相同
GET
,但没有响应主体。 - POST:向指定资源提交实体,通常会导致服务器状态改变或产生副作用。
- PUT:用请求有效负载替换目标资源的所有当前表示。
- DELETE:删除指定的资源。
- PATCH:对资源应用部分修改。
HTTP 响应代码
HTTP 响应状态代码指示特定的 HTTP 请求是否已成功完成。
标准定义了五个类别:
- 1xx——信息响应。
- 2xx – 成功响应。
- 3xx——重定向响应。
- 4xx——客户端错误响应。
- 5xx——服务器错误响应。
例如,HTTP 200 表示请求成功。
优势
让我们讨论一下 REST API 的一些优点:
- 简单易懂。
- 灵活、便携。
- 良好的缓存支持。
- 客户端与服务器是解耦的。
缺点
让我们讨论一下 REST API 的一些缺点:
- 过度获取数据。
- 有时需要多次往返服务器。
用例
REST API 几乎被广泛使用,并且是 API 设计的默认标准。总体而言,REST API 非常灵活,几乎可以适用于所有场景。
例子
以下是对用户资源进行操作的 REST API 的示例用法。
URI | HTTP 动词 | 描述 |
---|---|---|
/用户 | 得到 | 获取所有用户 |
/用户/{id} | 得到 | 通过 ID 获取用户 |
/用户 | 邮政 | 添加新用户 |
/用户/{id} | 修补 | 通过 ID 更新用户 |
/用户/{id} | 删除 | 根据 ID 删除用户 |
关于 REST API,还有很多东西需要学习,我强烈建议研究一下超媒体作为应用程序状态引擎 (HATEOAS)。
GraphQL
GraphQL是一种查询语言和 API 的服务器端运行时,它优先向客户端提供他们请求的数据,而不会提供多余的数据。它由Facebook开发,并于 2015 年开源。
GraphQL 旨在使 API 快速、灵活且易于开发者使用。此外,GraphQL 还为 API 维护者提供了灵活性,可以在不影响现有查询的情况下添加或弃用字段。开发者可以使用自己喜欢的任何方法构建 API,GraphQL 规范将确保它们以客户端可预测的方式运行。
在 GraphQL 中,基本单位是查询。
概念
我们来简单讨论一下 GraphQL 中的一些关键概念:
架构
GraphQL 模式描述了客户端连接到 GraphQL 服务器后可以利用的功能。
查询
查询是客户端发出的请求。它可以由查询的字段和参数组成。查询的操作类型也可以是变异,它提供了一种修改服务器端数据的方法。
解析器
解析器是一组用于生成 GraphQL 查询响应的函数集合。简单来说,解析器充当 GraphQL 查询处理器。
优势
让我们讨论一下 GraphQL 的一些优点:
- 消除过度获取数据。
- 强定义的模式。
- 代码生成支持。
- 有效载荷优化。
缺点
让我们讨论一下 GraphQL 的一些缺点:
- 将复杂性转移到服务器端。
- 缓存变得困难。
- 版本控制不明确。
- N+1 问题。
用例
GraphQL 在以下场景中被证明是必不可少的:
- 由于我们可以在单个查询中查询多个资源,因此可以减少应用程序带宽使用量。
- 复杂系统的快速原型设计。
- 当我们使用类似图形的数据模型时。
例子
User
这是一个定义类型和类型的 GraphQL 模式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"
}
}
在graphql.org上了解有关 GraphQL 的更多信息。
gRPC
gRPC是一个现代开源高性能远程过程调用 (RPC)框架,可在任何环境中运行。它能够高效地连接数据中心内外的服务,并提供可插拔的负载均衡、跟踪、健康检查、身份验证等功能。
概念
让我们讨论一下 gRPC 的一些关键概念。
协议缓冲区
协议缓冲区提供了一种与语言和平台无关的可扩展机制,用于以向前和向后兼容的方式序列化结构化数据。它类似于 JSON,但更小、更快,并且能够生成原生语言绑定。
服务定义
与许多 RPC 系统一样,gRPC 基于定义服务并指定可使用其参数和返回类型进行远程调用的方法的思想。gRPC 使用协议缓冲区作为接口定义语言 (IDL)来描述服务接口和有效负载消息的结构。
优势
让我们讨论一下 gRPC 的一些缺点:
- 轻巧、高效。
- 高性能。
- 内置代码生成支持。
- 双向流。
缺点
让我们讨论一下 gRPC 的一些缺点:
- 与 REST 和 GraphQL 相比相对较新。
- 有限的浏览器支持。
- 学习曲线更陡峭。
- 人类无法阅读。
用例
以下是 gRPC 的一些良好用例:
- 通过双向流进行实时通信。
- 微服务中高效的服务间通信。
- 低延迟和高吞吐量通信。
- 多语言环境。
例子
以下是文件中定义的 gRPC 服务的基本示例。使用此定义,我们可以轻松地用我们选择的编程语言*.proto
生成该服务。HelloService
service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string greeting = 1;
}
message HelloResponse {
string reply = 1;
}
REST、GraphQL 和 gRPC
现在我们知道了这些 API 设计技术的工作原理,让我们根据以下参数对它们进行比较:
- 会不会造成紧耦合?
- API 的交流功能如何(通过不同的 API 调用来获取所需信息)?
- 表演怎么样?
- 集成起来有多复杂?
- 缓存效果如何?
- 内置工具和代码生成?
- API 的可发现性如何?
- 对 API 进行版本控制有多容易?
类型 | 耦合 | 健谈 | 表现 | 复杂 | 缓存 | 代码生成 | 可发现性 | 版本控制 |
---|---|---|---|---|---|---|---|---|
休息 | 低的 | 高的 | 好的 | 中等的 | 伟大的 | 坏的 | 好的 | 简单的 |
GraphQL | 中等的 | 低的 | 好的 | 高的 | 风俗 | 好的 | 好的 | 风俗 |
gRPC | 高的 | 中等的 | 伟大的 | 低的 | 风俗 | 伟大的 | 坏的 | 难的 |
哪种API技术更好?
答案是,两者都不是。没有灵丹妙药,因为每种技术都有各自的优缺点。用户只关心以一致的方式使用我们的 API,因此在设计 API 时,请务必专注于您的领域和需求。
长轮询、WebSocket、服务器发送事件 (SSE)
Web 应用程序最初是围绕客户端-服务器模型开发的,其中 Web 客户端始终是事务(例如向服务器请求数据)的发起者。因此,没有一种机制可以让服务器在客户端未先发出请求的情况下独立地向客户端发送或推送数据。让我们讨论一些解决这个问题的方法。
长轮询
HTTP 长轮询是一种用于从服务器尽快向客户端推送信息的技术。这样,服务器就不必等待客户端发送请求。
在长轮询中,服务器在收到客户端的请求后不会关闭连接。相反,服务器仅在有新消息可用或达到超时阈值时才响应。
一旦客户端收到响应,它就会立即向服务器发送新的请求,以建立新的待处理连接来向客户端发送数据,并重复该操作。通过这种方式,服务器模拟了实时服务器推送功能。
在职的
让我们了解一下长轮询是如何工作的:
- 客户端发出初始请求并等待响应。
- 服务器接收请求并延迟发送任何内容,直到有更新可用。
- 一旦有更新可用,响应就会发送给客户端。
- 客户端收到响应并立即或在一段定义的时间间隔后发出新请求以再次建立连接。
优势
以下是长轮询的一些优点:
- 易于实施,适合小规模项目。
- 几乎得到普遍支持。
缺点
长轮询的一个主要缺点是它通常不可扩展。以下是其他一些原因:
- 每次都会创建一个新的连接,这会对服务器造成很大的负担。
- 对于多个请求来说,可靠的消息排序可能是一个问题。
- 由于服务器需要等待新的请求,因此延迟增加。
WebSockets
WebSocket 通过单个 TCP 连接提供全双工通信通道。它是客户端和服务器之间的持久连接,双方可以随时开始发送数据。
客户端通过称为 WebSocket 握手的过程建立 WebSocket 连接。如果该过程成功,则服务器和客户端可以随时双向交换数据。WebSocket 协议能够以较低的开销实现客户端和服务器之间的通信,从而促进与服务器之间的实时数据传输。
这是通过为服务器提供一种标准化的方式来实现的,即服务器可以在无需询问的情况下向客户端发送内容,并允许在保持连接打开的情况下来回传递消息。
在职的
让我们了解一下 WebSocket 的工作原理:
- 客户端通过发送请求来发起WebSocket握手过程。
- 该请求还包含一个HTTP 升级标头,允许请求切换到 WebSocket 协议(
ws://
)。 - 服务器向客户端发送响应,确认 WebSocket 握手请求。
- 一旦客户端收到成功的握手响应,就会打开 WebSocket 连接。
- 现在客户端和服务器可以开始双向发送数据,实现实时通信。
- 一旦服务器或客户端决定关闭连接,连接就会关闭。
优势
以下是 WebSocket 的一些优点:
- 全双工异步消息传递。
- 更好的基于来源的安全模型。
- 对于客户端和服务器来说都是轻量级的。
缺点
让我们讨论一下 WebSockets 的一些缺点:
- 终止的连接不会自动恢复。
- 旧版浏览器不支持 WebSockets(变得不那么重要)。
服务器发送事件 (SSE)
服务器发送事件 (SSE) 是一种在客户端和服务器之间建立长期通信的方式,使服务器能够主动向客户端推送数据。
它是单向的,这意味着一旦客户端发送请求,它只能接收响应,而不能通过同一连接发送新的请求。
在职的
让我们了解服务器发送事件的工作原理:
- 客户端向服务器发出请求。
- 客户端和服务器之间的连接已建立并保持打开状态。
- 当有新数据可用时,服务器会向客户端发送响应或事件。
优势
- 对于客户端和服务器来说,实现和使用都很简单。
- 大多数浏览器都支持。
- 防火墙没有问题。
缺点
- 单向性可能会带来限制。
- 限制最大打开连接数。
- 不支持二进制数据。
地理散列和四叉树
地理散列
地理散列是一种地理编码方法,用于将地理坐标(例如经纬度)编码为短的字母数字字符串。它由古斯塔沃·尼迈耶 (Gustavo Niemeyer)于 2008 年创建。
例如,坐标为 的旧金山37.7564, -122.4016
在 geohash 中可以表示为9q8yy9mf
。
地理散列如何工作?
Geohash 是一种使用 Base-32 字母编码的分层空间索引,Geohash 中的第一个字符将初始位置标识为 32 个单元格之一。该单元格也包含 32 个单元格。这意味着,为了表示一个点,需要将世界递归地划分为越来越小的单元格,每个单元格的位数都增加,直到达到所需的精度。精度因子也决定了单元格的大小。
地理散列法可以保证,如果点的地理散列共享较长的前缀,则它们在空间上更接近。这意味着字符串中的字符越多,位置就越精确。例如,地理散列9q8yy9mf
和9q8yy9vx
由于共享前缀 ,在空间上更接近9q8yy9
。
地理散列还可用于提供一定程度的匿名性,因为我们不需要暴露用户的确切位置,因为根据地理散列的长度,我们只知道他们位于某个区域内的某个地方。
不同长度的geohash的单元格大小如下:
Geohash 长度 | 单元格宽度 | 单元格高度 |
---|---|---|
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 毫米 |
用例
以下是 Geohashing 的一些常见用例:
- 这是在数据库中表示和存储位置的简单方法。
- 它也可以作为 URL 在社交媒体上共享,因为它比经度和纬度更容易共享和记住。
- 我们可以通过非常简单的字符串比较和有效的索引搜索来有效地找到一个点的最近邻居。
示例
Geohashing 被广泛使用并且受到流行数据库的支持。
四叉树
四叉树是一种树形数据结构,每个内部节点恰好有四个子节点。它们通常用于通过递归方式将二维空间细分为四个象限或区域来划分空间。每个子节点或叶节点都存储着空间信息。四叉树是八叉树的二维类似物,八叉树用于划分三维空间。
四叉树的类型
四叉树可以根据其表示的数据类型进行分类,包括区域、点、线和曲线。以下是常见的四叉树类型:
- 点四叉树
- 点区域(PR)四叉树
- 多边形地图(PM)四叉树
- 压缩四叉树
- 边四叉树
为什么我们需要四叉树?
经纬度还不够吗?为什么还需要四叉树?虽然理论上,我们可以通过经纬度来确定点与点之间的距离,例如使用欧氏距离。但在实际应用中,由于其在处理大型数据集时会占用大量 CPU 资源,因此四叉树的可扩展性并不强。
四叉树使我们能够高效地搜索二维范围内的点,这些点定义为经纬度坐标或笛卡尔坐标 (x, y)。此外,我们可以通过仅在达到特定阈值后细分节点来节省进一步的计算量。借助希尔伯特曲线等映射算法,我们可以轻松提升范围查询的性能。
用例
以下是四叉树的一些常见用途:
- 图像表示、处理和压缩。
- 空间索引和范围查询。
- 基于位置的服务,如谷歌地图、Uber 等。
- 网格生成和计算机图形。
- 稀疏数据存储。
断路器
断路器是一种用于检测故障的设计模式,它封装了在维护、临时外部系统故障或意外的系统困难期间防止故障不断重复发生的逻辑。
断路器背后的基本思想非常简单。我们将一个受保护的函数调用包装在一个断路器对象中,该对象用于监控故障。一旦故障达到某个阈值,断路器就会跳闸,所有对断路器的后续调用都会返回错误,而受保护的函数调用则完全不会执行。通常,如果断路器跳闸,我们还需要某种监控警报。
为什么需要熔断?
软件系统经常会对运行在不同进程(可能位于网络上的不同机器)中的软件进行远程调用。内存调用和远程调用之间的一个主要区别是,远程调用可能会失败,或者挂起而无响应,直到达到某个超时限制。更糟糕的是,如果一个无响应的供应商有很多调用者,那么我们可能会耗尽关键资源,从而导致多个系统发生级联故障。
州
让我们讨论一下断路器状态:
关闭
当一切正常时,断路器保持关闭状态,所有请求都会照常传输到服务。如果故障次数超过阈值,断路器就会跳闸并进入打开状态。
打开
在此状态下,断路器会立即返回错误,甚至不会调用服务。经过一定的超时时间后,断路器会进入半开状态。通常,断路器会有一个监控系统,其中会指定超时时间。
半开
在此状态下,断路器允许有限数量的服务请求通过并调用操作。如果请求成功,断路器将进入关闭状态。但是,如果请求继续失败,断路器将返回打开状态。
速率限制
速率限制是指防止操作频率超过定义的限制。在大型系统中,速率限制通常用于保护底层服务和资源。速率限制通常用作分布式系统中的防御机制,以保持共享资源的可用性。它还可以通过限制在给定时间段内到达 API 的请求数量来保护我们的 API 免受意外或恶意的过度使用。
为什么我们需要速率限制?
速率限制是任何大型系统的一个非常重要的部分,它可以用来完成以下任务:
- 避免因拒绝服务 (DoS) 攻击而导致资源匮乏。
- 速率限制通过对资源的自动扩展设置虚拟上限来帮助控制运营成本,如果不进行监控,可能会导致账单呈指数级增长。
- 速率限制可用于防御或缓解一些常见的攻击。
- 对于处理大量数据的 API,可以使用速率限制来控制数据流。
算法
API 速率限制有多种算法,每种算法都有其优缺点。让我们简要讨论一下其中一些算法:
漏水桶
漏桶算法是一种通过队列提供简单直观的速率限制方法的算法。注册请求时,系统会将其附加到队列末尾。队列中第一个请求的处理以固定的时间间隔或先进先出 (FIFO) 的方式进行。如果队列已满,则剩余的请求将被丢弃(或泄漏)。
令牌桶
这里我们引入了一个“桶”的概念。当一个请求到来时,必须从桶中取出一个令牌进行处理。如果桶中没有可用的令牌,请求就会被拒绝,请求者需要稍后重试。因此,令牌桶会在一段时间后刷新。
固定窗口
系统使用以n
秒为单位的窗口大小来跟踪固定窗口算法的速率。每个传入请求都会增加窗口的计数器。如果计数器超过阈值,则会丢弃该请求。
滑动原木
滑动日志限速技术会跟踪每个请求的时间戳日志。系统会将这些日志存储在按时间排序的哈希集或表中。系统还会丢弃时间戳超过阈值的日志。当新的请求到来时,我们会计算日志总和来确定请求速率。如果请求超过阈值速率,则会被暂停。
滑动窗口
滑动窗口是一种混合方法,它结合了固定窗口算法的低处理成本和滑动日志改进的边界条件。与固定窗口算法类似,我们为每个固定窗口跟踪一个计数器。接下来,我们根据当前时间戳计算前一个窗口请求速率的加权值,以平滑突发流量。
分布式系统中的速率限制
当涉及分布式系统时,速率限制会变得复杂。分布式系统中的速率限制主要存在两个问题:
不一致之处
当使用多节点集群时,我们可能需要强制执行全局速率限制策略。因为如果每个节点都跟踪自身的速率限制,那么用户在向不同节点发送请求时可能会超出全局速率限制。节点数量越多,用户超出全局限制的可能性就越大。
解决这个问题最简单的方法是在负载均衡器中使用粘性会话,这样每个消费者只会被发送到一个节点,但这会导致容错能力不足和扩展问题。另一种方法是使用像Redis这样的集中式数据存储,但这会增加延迟并导致竞争条件。
竞争条件
当我们使用简单的“先获取后设置”方法时,就会出现此问题。在这种方法中,我们会检索当前的速率限制计数器,将其递增,然后将其推送回数据存储区。这种模型的问题在于,在执行完整的读取-递增-存储循环所需的时间内,可能会有其他请求通过,每个请求都会尝试使用无效(较低)的计数器值来存储递增计数器。这允许消费者发送大量请求来绕过速率限制控制。
避免此问题的一种方法是围绕密钥使用某种分布式锁定机制,阻止任何其他进程访问或写入计数器。尽管锁定会成为严重的瓶颈,并且扩展性不佳。更好的方法可能是使用“先设置后获取”的方法,这样我们就可以快速递增和检查计数器值,而不会让原子操作妨碍。
服务发现
服务发现是指在计算机网络中检测服务。服务发现协议 (SDP) 是一种网络标准,通过识别资源来实现网络检测。
为什么我们需要服务发现?
在单体应用中,服务通过语言级方法或过程调用相互调用。然而,现代基于微服务的应用通常运行在虚拟化或容器化环境中,其中服务实例的数量及其位置会动态变化。因此,我们需要一种机制,使服务的客户端能够向一组动态变化的临时服务实例发出请求。
实现
有两种主要的服务发现模式:
客户端发现
在这种方法中,客户端通过查询负责管理和存储所有服务的网络位置的服务注册表来获取另一个服务的位置。
服务器端发现
在这种方法中,我们使用一个中间组件,例如负载均衡器。客户端通过负载均衡器向服务发出请求,然后负载均衡器将请求转发到可用的服务实例。
服务注册中心
服务注册表本质上是一个数据库,其中包含客户端可以访问的服务实例的网络位置。服务注册表必须具有高可用性和最新性。
服务注册
我们还需要一种获取服务信息的方法,通常称为服务注册。让我们来看看两种可能的服务注册方法:
自行注册
使用自注册模型时,服务实例负责在服务注册表中注册和注销自身。此外,如有必要,服务实例还会发送心跳请求以保持其注册有效。
第三方注册
服务注册表通过轮询部署环境或订阅事件来跟踪正在运行的实例的变化。当它检测到新的可用服务实例时,它会将其记录到数据库中。服务注册表还会注销已终止的服务实例。
服务网格
服务间通信在分布式应用程序中至关重要,但随着服务数量的增长,在应用程序集群内部和跨集群路由这种通信变得越来越复杂。服务网格支持各个服务之间进行托管、可观察且安全的通信。它与服务发现协议配合使用来检测服务。Istio和Envoy是一些最常用的服务网格技术。
示例
以下是一些常用的服务发现基础设施工具:
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 Well-Architected Framework了解更多关于灾难恢复的信息。
为什么灾难恢复很重要?
灾难恢复可以带来以下好处:
- 最大限度地减少中断和停机时间
- 限制损害
- 快速恢复
- 更好的客户保留率
条款
让我们讨论一些与灾难恢复相关的重要术语:
恢复行动计划
恢复时间目标 (RTO) 是指服务中断和恢复之间可接受的最大延迟时间。这决定了服务不可用时可接受的时间窗口。
恢复点外包 (RPO)
恢复点目标 (RPO) 是指自上一个数据恢复点以来可接受的最大时间量。这决定了从上一个恢复点到服务中断之间可接受的数据丢失量。
策略
各种灾难恢复 (DR) 策略都可以成为灾难恢复计划的一部分。
备份
这是最简单的灾难恢复类型,涉及将数据存储在异地或可移动驱动器上。
冷站点
在这种类型的灾难恢复中,组织在第二个站点建立基本基础设施。
热门网站
热站点始终维护最新的数据副本。热站点的设置耗时较长,而且比冷站点更昂贵,但可以显著减少停机时间。
虚拟机 (VM) 和容器
在讨论虚拟化与容器化之前,让我们先了解一下什么是虚拟机(VM)和容器。
虚拟机(VM)
虚拟机 (VM) 是一种虚拟环境,它基于物理硬件系统创建,充当虚拟计算机系统,拥有独立的 CPU、内存、网络接口和存储。虚拟机管理程序 (hypervisor) 软件将虚拟机的资源与硬件分离,并进行相应的配置,以便虚拟机能够使用。
虚拟机与系统的其他部分相互隔离,多个虚拟机可以存在于单个硬件(例如服务器)上。它们可以根据需求或为了更高效地利用资源而在主机服务器之间移动。
什么是虚拟机管理程序?
虚拟机管理程序(Hypervisor)有时也称为虚拟机监视器 (VMM),它将操作系统和资源与虚拟机隔离,并支持创建和管理这些虚拟机。虚拟机管理程序将 CPU、内存和存储等资源视为一个资源池,可以轻松地在现有客户机或新虚拟机之间重新分配。
为什么要使用虚拟机?
服务器整合是使用虚拟机的首要原因。大多数操作系统和应用程序部署仅使用少量可用的物理资源。通过服务器虚拟化,我们可以在每台物理服务器上部署多个虚拟服务器,从而提高硬件利用率。这使我们无需购买额外的物理资源。
虚拟机提供了一个与系统其他部分隔离的环境,因此虚拟机内部运行的任何程序都不会干扰主机硬件上运行的任何其他程序。由于虚拟机是隔离的,因此它们是测试新应用程序或设置生产环境的理想选择。我们也可以运行单一用途的虚拟机来支持特定的用例。
容器
容器是一种标准的软件单元,它将代码及其所有依赖项(例如特定版本的运行时和库)打包在一起,以便应用程序能够快速可靠地从一个计算环境迁移到另一个计算环境。容器提供了一种逻辑打包机制,可以将应用程序与其实际运行的环境分离。这种解耦机制使得基于容器的应用程序能够轻松一致地部署,而不受目标环境的限制。
为什么我们需要容器?
让我们讨论一下使用容器的一些优点:
责任分离
容器化提供了明确的责任分离,因为开发人员专注于应用程序逻辑和依赖关系,而运营团队可以专注于部署和管理。
工作负载可移植性
容器几乎可以在任何地方运行,大大简化了开发和部署。
应用程序隔离
容器在操作系统级别虚拟化 CPU、内存、存储和网络资源,为开发人员提供与其他应用程序逻辑隔离的操作系统视图。
敏捷开发
容器避免了对依赖关系和环境的担忧,从而使开发人员能够更快地行动。
高效运营
容器是轻量级的,允许我们只使用我们需要的计算资源。
虚拟化与容器化
在传统的虚拟化中,虚拟机管理程序会将物理硬件虚拟化。其结果是,每个虚拟机都包含一个客户操作系统、一个操作系统运行所需的硬件虚拟副本,以及一个应用程序及其相关的库和依赖项。
容器并非虚拟化底层硬件,而是虚拟化操作系统,因此每个容器仅包含应用程序及其依赖项,这使得容器比虚拟机轻量级得多。容器还共享操作系统内核,并且仅使用虚拟机所需内存的一小部分。
OAuth 2.0 和 OpenID Connect (OIDC)
OAuth 2.0
OAuth 2.0,即开放授权 (Open Authorization),是一项旨在代表用户提供对资源的授权访问,而无需共享用户凭据的标准。OAuth 2.0 是一种授权协议,而非身份验证协议。它主要设计用于授予对一组资源(例如远程 API 或用户数据)的访问权限。
概念
OAuth 2.0 协议定义了以下实体:
- 资源所有者:拥有受保护资源并可授予访问权限的用户或系统。
- 客户端:客户端是需要访问受保护资源的系统。
- 授权服务器:此服务器接收来自客户端的访问令牌请求,并在资源所有者成功验证和同意后发出访问令牌。
- 资源服务器:保护用户资源并接收来自客户端的访问请求的服务器。它接受并验证来自客户端的访问令牌 (Access Token),并返回相应的资源。
- 范围:用于明确指定授予资源访问权限的原因。可接受的范围值及其所关联的资源取决于资源服务器。
- 访问令牌:代表最终用户访问资源的授权的数据。
OAuth 2.0 如何工作?
让我们了解一下 OAuth 2.0 的工作原理:
- 客户端向授权服务器请求授权,并提供客户端 ID 和密钥作为身份标识。授权服务器还提供范围和端点 URI,用于发送访问令牌或授权码。
- 授权服务器对客户端进行身份验证并验证所请求的范围是否被允许。
- 资源所有者与授权服务器交互以授予访问权限。
- 授权服务器会根据授权类型,使用授权码或访问令牌重定向回客户端。此外,也可能返回刷新令牌。
- 有了访问令牌,客户端就可以向资源服务器请求访问资源。
缺点
以下是 OAuth 2.0 最常见的缺点:
- 缺乏内置安全功能。
- 没有标准实施。
- 没有一套通用的范围。
OpenID 连接
OAuth 2.0 仅用于授权,用于授予从一个应用程序到另一个应用程序的数据和功能的访问权限。OpenID Connect (OIDC) 是位于 OAuth 2.0 之上的薄层,它添加了有关已登录用户的登录名和个人资料信息。
当授权服务器支持 OIDC 时,它有时被称为身份提供者 (IdP),因为它会将资源所有者的信息返回给客户端。OpenID Connect 相对较新,因此与 OAuth 相比,其采用率和行业最佳实践的实施率较低。
概念
OpenID Connect (OIDC) 协议定义了以下实体:
- 依赖方:当前应用程序。
- OpenID 提供者:这本质上是一个向依赖方提供一次性代码的中间服务。
- 令牌端点:接受一次性代码 (OTC) 并提供有效期为一小时的访问代码的 Web 服务器。OIDC 与 OAuth 2.0 的主要区别在于,OIDC 使用 JSON Web Token (JWT) 提供令牌。
- UserInfo 端点:依赖方与此端点通信,提供安全令牌并接收有关最终用户的信息
OAuth 2.0 和 OIDC 都易于实现,并且基于 JSON,大多数 Web 和移动应用程序都支持 JSON。然而,OpenID Connect (OIDC) 规范比基本 OAuth 规范更为严格。
单点登录 (SSO)
单点登录 (SSO) 是一种身份验证过程,用户只需使用一组登录凭据即可访问多个应用程序或网站。这样一来,用户无需分别登录不同的应用程序。
用户凭证和其他身份信息由名为身份提供者 (IdP) 的集中式系统存储和管理。身份提供者是一个值得信赖的系统,提供对其他网站和应用程序的访问权限。
基于单点登录 (SSO) 的身份验证系统通常用于员工需要访问其组织的多个应用程序的企业环境中。
成分
让我们讨论一下单点登录 (SSO) 的一些关键组件。
身份提供者 (IdP)
用户身份信息由称为身份提供者 (IdP) 的集中式系统存储和管理。身份提供者对用户进行身份验证,并提供对服务提供商的访问权限。
身份提供者可以通过验证用户名和密码,或通过验证由独立身份提供者提供的用户身份断言来直接验证用户身份。身份提供者负责管理用户身份,从而减轻服务提供商的这一责任。
服务提供商
服务提供商为最终用户提供服务。他们依赖身份提供商来断言用户的身份,并且通常由身份提供商管理用户的某些属性。服务提供商还可以为用户维护本地帐户以及其服务独有的属性。
身份经纪人
身份代理充当中介,将多个服务提供商与各种不同的身份提供商连接起来。使用身份代理,我们可以在任何应用程序上执行单点登录,而无需遵循其遵循的协议。
SAML
安全断言标记语言 (SAML) 是一种开放标准,允许客户端跨不同系统共享有关身份、身份验证和权限的安全信息。SAML 采用可扩展标记语言 (XML) 标准实现数据共享。
SAML 特别支持身份联合,使得身份提供者 (IdP) 能够无缝且安全地将经过身份验证的身份及其属性传递给服务提供商。
SSO 如何工作?
现在,让我们讨论一下单点登录的工作原理:
- 用户从他们想要的应用程序请求资源。
- 应用程序将用户重定向到身份提供者 (IdP) 进行身份验证。
- 用户使用其凭证(通常是用户名和密码)登录。
- 身份提供者 (IdP) 将单点登录响应发送回客户端应用程序。
- 应用程序授予用户访问权限。
SAML 与 OAuth 2.0 和 OpenID Connect (OIDC)
SAML、OAuth 和 OIDC 之间存在诸多差异。SAML 使用 XML 传递消息,而 OAuth 和 OIDC 使用 JSON。OAuth 提供更简单的体验,而 SAML 则更注重企业安全。
OAuth 和 OIDC 广泛使用 RESTful 通信,因此移动和现代 Web 应用程序认为 OAuth 和 OIDC 能为用户带来更佳体验。而 SAML 则会在浏览器中放置会话 Cookie,允许用户访问特定网页。这对于短期工作负载非常有用。
OIDC 对开发人员友好且易于实现,这拓宽了其应用场景。它可以通过所有常用编程语言中免费提供的库快速从零开始实现。SAML 的安装和维护可能比较复杂,只有企业级规模的公司才能胜任。
OpenID Connect 本质上是 OAuth 框架之上的一层。因此,它可以提供一个内置的权限层,要求用户同意服务提供商可能访问的内容。虽然 SAML 也能够允许同意流程,但它是通过开发人员执行的硬编码来实现的,而不是作为其协议的一部分。
这两种身份验证协议各有优劣。一如既往,很大程度上取决于我们的具体用例和目标受众。
优势
以下是使用单点登录的好处:
- 用户只需记住一组凭证,使用方便。
- 无需经过漫长的授权过程即可轻松访问。
- 强制执行安全和合规性以保护敏感数据。
- 通过降低 IT 支持成本和管理时间,简化管理。
缺点
以下是单点登录的一些缺点:
- 单一密码漏洞,如果主 SSO 密码被泄露,所有支持的应用程序都会被泄露。
- 使用单点登录的身份验证过程比传统身份验证慢,因为每个应用程序都必须请求 SSO 提供商进行验证。
示例
以下是一些常用的身份提供者 (IdP):
SSL、TLS、mTLS
让我们简要讨论一些重要的通信安全协议,例如 SSL、TLS 和 mTLS。我想说,从“全局”系统设计的角度来看,这个主题并不是很重要,但仍然值得了解。
SSL
SSL 代表安全套接字层,它是一种用于加密和保护互联网通信的协议。它最初于 1995 年开发,但后来被 TLS(传输层安全性)取代。
既然它已被弃用,为什么还称之为 SSL 证书?
大多数主要证书提供商仍将证书称为 SSL 证书,这就是命名约定仍然存在的原因。
SSL 为何如此重要?
最初,网络上的数据是以明文形式传输的,任何人如果截获了信息,都可以读取。SSL 的诞生就是为了解决这个问题,并保护用户隐私。通过加密用户和 Web 服务器之间传输的所有数据,SSL 还可以防止攻击者篡改传输中的数据,从而阻止某些类型的网络攻击。
TLS
传输层安全性 (TLS) 是一种广泛采用的安全协议,旨在保障互联网通信的隐私和数据安全。TLS 由之前的加密协议安全套接字层 (SSL) 发展而来。TLS 的主要用途是加密 Web 应用程序和服务器之间的通信。
TLS 协议主要由三个部分组成:
- 加密:隐藏从第三方传输的数据。
- 身份验证:确保交换信息的各方都是他们所声称的身份。
- 完整性:验证数据未被伪造或篡改。
移动TLS
相互 TLS(mTLS)是一种相互身份验证方法。mTLS 通过验证网络连接两端的各方是否拥有正确的私钥,来确保双方的身份与其所声称的身份相符。双方各自 TLS 证书中的信息可提供额外的验证。
为什么要使用 mTLS?
mTLS 有助于确保客户端和服务器之间双向流量的安全可信。这为登录组织网络或应用程序的用户提供了额外的安全保障。它还可以验证与不遵循登录流程的客户端设备(例如物联网 (IoT) 设备)的连接。
如今,mTLS 被微服务或分布式系统在零信任安全模型中广泛使用,用于相互验证。
系统设计面试
系统设计是一个非常广泛的主题,系统设计面试旨在评估你针对抽象问题提供技术解决方案的能力,因此并非针对特定答案。系统设计面试的独特之处在于候选人和面试官之间的双向互动。
不同工程级别的要求也大相径庭。因为经验丰富的人与行业新手的处理方式截然不同。因此,很难找到一个单一的策略来帮助我们在面试过程中保持条理清晰。
让我们来看看系统设计面试的一些常见策略:
要求澄清
系统设计面试题本质上比较模糊或抽象。在面试初期,询问问题的确切范围并明确功能需求至关重要。通常,需求分为三部分:
功能要求
这些是最终用户明确要求系统应提供的基本功能。所有这些功能都必须作为合同的一部分纳入系统。
例如:
- “我们需要为这个系统设计哪些功能?”
- “在我们的设计中,我们需要考虑哪些边缘情况?”
非功能性需求
这些是系统根据项目合同必须满足的质量约束。这些因素的优先级或实施程度因项目而异。它们也称为非行为需求。例如,可移植性、可维护性、可靠性、可扩展性、安全性等。
例如:
- “每个请求都应以最小的延迟进行处理”
- “系统应该高度可用”
扩展要求
这些基本上是“不错的”要求,可能超出了系统的范围。
例如:
- “我们的系统应该记录指标和分析”
- “服务健康和性能监控?”
估计和约束
估算我们要设计的系统的规模。重要的是要问以下问题:
- “这个系统需要处理的期望规模是多少?”
- “我们的系统的读写比率是多少?”
- “每秒有多少个请求?”
- “需要多少存储空间?”
这些问题将帮助我们以后扩展我们的设计。
数据模型设计
一旦有了估算,我们就可以开始定义数据库模式了。在面试的早期阶段这样做有助于我们理解数据流,这是每个系统的核心。在这一步,我们基本上定义了所有实体及其之间的关系。
- “系统中有哪些不同的实体?”
- “这些实体之间是什么关系?”
- “我们需要多少张桌子?”
- “NoSQL 在这里是更好的选择吗?”
API 设计
接下来,我们可以开始为系统设计 API。这些 API 将帮助我们明确定义对系统的期望。我们无需编写任何代码,只需一个简单的接口来定义 API 需求,例如参数、函数、类、类型、实体等。
例如:
createUser(name: string, email: string): User
建议保持界面尽可能简单,并在满足扩展需求时再返回该界面。
高级组件设计
现在我们已经建立了数据模型和 API 设计,是时候确定解决问题所需的系统组件(例如负载均衡器、API 网关等)并起草系统的第一个设计了。
- “设计单体架构还是微服务架构最好?”
- “我们应该使用什么类型的数据库?”
一旦我们有了基本图表,我们就可以开始与面试官讨论系统如何从客户的角度运作。
详细设计
现在是时候详细介绍我们设计的系统的主要组件了。像往常一样,与面试官讨论哪些组件可能需要进一步改进。
这是一个展示您在专业领域经验的好机会。请介绍不同的方法、优缺点。解释您的设计决策,并用示例进行佐证。这也是讨论系统可能支持的任何其他功能的好时机,尽管这是可选的。
- “我们应该如何划分数据?”
- “负载分配怎么样?”
- “我们应该使用缓存吗?”
- “我们该如何应对流量突然激增的情况?”
另外,尽量不要对某些技术抱有太过主观的看法,像“我认为 NoSQL 数据库更好,SQL 数据库不可扩展”这样的说法会让人感觉很不好。作为一个多年来面试过很多人的人,我的建议是,对于自己了解和不了解的事情保持谦虚的态度。运用你现有的知识和例子来应对面试的这一部分。
识别并解决瓶颈
最后,是时候讨论瓶颈问题以及缓解瓶颈的方法了。以下是一些重要的问题:
- “我们有足够的数据库副本吗?”
- “是否存在单点故障?”
- “数据库需要分片吗?”
- “我们如何才能让我们的系统更加健壮?”
- “如何提高我们的缓存的可用性?”
一定要阅读你面试公司的工程博客。这将帮助你了解他们使用的技术栈以及哪些问题对他们来说很重要。
URL缩短器
让我们设计一个 URL 缩短器,类似于Bitly、TinyURL等服务。
什么是 URL 缩短器?
URL 缩短服务会为长 URL 创建别名或短 URL。用户访问这些短链接时会被重定向到原始 URL。
例如,以下长 URL 可以更改为较短的 URL。
长网址:https://karanpratapsingh.com/courses/system-design/url-shortener
为什么我们需要 URL 缩短器?
当我们分享 URL 时,URL 缩短器通常可以节省空间。用户也不太可能输错较短的 URL。此外,我们还可以跨设备优化链接,从而追踪单个链接。
要求
我们的URL缩短系统应满足以下要求:
功能要求
- 给定一个 URL,我们的服务应该为其生成一个更短且唯一的别名。
- 用户访问短链接时应被重定向到原始 URL。
- 链接应在默认时间跨度后过期。
非功能性需求
- 高可用性和最小延迟。
- 该系统应具有可扩展性和高效性。
扩展要求
- 防止滥用服务。
- 记录重定向的分析和指标。
估计和约束
让我们从估计和约束开始。
注意:请务必与面试官核对任何与规模或交通相关的假设。
交通
这将是一个读取密集型的系统,因此我们假设100:1
读/写比率为每月生成 1 亿个链接。
每月读/写次数
每月阅读量:
对于写入也类似:
我们的系统的每秒请求数(RPS)是多少?
每月 1 亿个请求相当于每秒 40 个请求。
有了100:1
读/写比率,重定向的次数将是:
带宽
由于我们预计每秒会有大约 40 个 URL,并且如果我们假设每个请求的大小为 500 字节,那么写入请求的总传入数据将是:
类似地,对于读取请求,由于我们预计大约有 4K 重定向,因此总传出数据将是:
贮存
在存储方面,我们假设每个链接或记录在数据库中存储 10 年。由于我们预计每月会有大约 1 亿个新请求,因此我们需要存储的记录总数为:
和之前一样,假设每个存储的记录大约为 500 字节,那么我们需要大约 6TB 的存储空间:
缓存
对于缓存,我们将遵循经典的帕累托原则,也称为 80/20 规则。这意味着 80% 的请求针对 20% 的数据,因此我们可以缓存大约 20% 的请求。
由于我们每秒收到大约 4K 个读取或重定向请求,这相当于每天 3.5 亿个请求。
因此,我们每天需要大约 35GB 的内存。
高层估计
以下是我们的高级估计:
类型 | 估计 |
---|---|
写入(新 URL) | 40/秒 |
读取(重定向) | 4K/秒 |
带宽(传入) | 20 KB/秒 |
带宽(传出) | 2 MB/秒 |
储存(10年) | 6 TB |
内存(缓存) | 每天约 35 GB |
数据模型设计
接下来,我们将重点关注数据模型设计。以下是我们的数据库架构:
最初,我们可以从两个表开始:
用户
name
存储用户的详细信息,如email
、、createdAt
等。
网址
包含新建短网址的创建用户的expiration
、hash
、originalURL
等属性。我们也可以使用该列作为索引,提升查询性能。userID
hash
我们应该使用什么样的数据库?
由于数据不是强关系,因此 NoSQL 数据库(例如Amazon DynamoDB、Apache Cassandra或MongoDB)将是更好的选择,如果我们决定使用 SQL 数据库,那么我们可以使用Azure SQL 数据库或Amazon RDS之类的数据库。
有关更多详细信息,请参阅SQL 与 NoSQL。
API 设计
让我们为我们的服务做一个基本的 API 设计:
创建 URL
此 API 应根据原始 URL 在我们的系统中创建一个新的短 URL。
createURL(apiKey: string, originalURL: string, expiration?: Date): string
参数
API 密钥(string
):用户提供的 API 密钥。
原始网址(string
):要缩短的原始网址。
到期日期 ( Date
):新 URL 的到期日期(可选)。
返回
短网址(string
):新的缩短网址。
获取 URL
此 API 应从给定的短 URL 中检索原始 URL。
getURL(apiKey: string, shortURL: string): string
参数
API 密钥(string
):用户提供的 API 密钥。
短网址(string
):映射到原始网址的短网址。
返回
原始 URL ( string
):要检索的原始 URL。
删除网址
此 API 应从我们的系统中删除给定的短网址。
deleteURL(apiKey: string, shortURL: string): boolean
参数
API 密钥(string
):用户提供的 API 密钥。
短网址(string
):需要删除的短网址。
返回
结果(boolean
):表示操作是否成功。
为什么我们需要 API 密钥?
您肯定已经注意到了,我们使用 API 密钥来防止服务被滥用。使用此 API 密钥,我们可以限制用户每秒或每分钟的请求数量。这是开发者 API 的标准做法,应该能够满足我们的扩展需求。
高层设计
现在让我们对我们的系统进行高层设计。
URL 编码
我们系统的主要目标是缩短给定的 URL,让我们看看不同的方法:
Base62 方法
在这种方法中,我们可以使用Base62对原始 URL 进行编码,它由大写字母 AZ、小写字母 az 和数字 0-9 组成。
在哪里,
N
:生成的URL的字符数。
因此,如果我们想要生成一个长度为 7 个字符的 URL,我们将生成约 3.5 万亿个不同的 URL。
这是这里最简单的解决方案,但它不能保证密钥不重复或抗碰撞。
MD5方法
MD5 消息摘要算法是一种广泛使用的哈希函数,它生成一个 128 位哈希值(或 32 位十六进制数字)。我们可以使用这 32 位十六进制数字来生成 7 个字符长的 URL。
然而,这给我们带来了一个新问题,那就是重复和冲突。我们可以尝试重新计算哈希值,直到找到唯一的哈希值,但这会增加系统的开销。最好还是寻找更可扩展的方法。
反击方法
在这种方法中,我们将从一台服务器开始,该服务器将维护生成的密钥的数量。一旦我们的服务收到请求,它就会联系计数器,计数器返回一个唯一的数字并递增。当下一个请求到来时,计数器再次返回该唯一数字,如此循环。
这种方法的问题在于它很快就会成为单点故障。而且,如果我们运行多个计数器实例,可能会发生冲突,因为它本质上是一个分布式系统。
为了解决这个问题,我们可以使用分布式系统管理器,例如Zookeeper,它可以提供分布式同步功能。Zookeeper 可以为我们的服务器维护多个范围。
一旦服务器达到其最大范围,Zookeeper 就会将未使用的计数器范围分配给新服务器。这种方法可以保证 URL 不重复且不会发生冲突。此外,我们可以运行多个 Zookeeper 实例来消除单点故障。
密钥生成服务(KGS)
正如我们所讨论的,大规模生成唯一密钥且避免重复和冲突可能颇具挑战性。为了解决这个问题,我们可以创建一个独立的密钥生成服务 (KGS),它会提前生成唯一密钥并将其存储在单独的数据库中以供后续使用。这种方法可以简化我们的工作。
如何处理并发访问?
一旦使用了密钥,我们可以在数据库中对其进行标记,以确保不会重复使用它,但是,如果有多个服务器实例同时读取数据,则两个或多个服务器可能会尝试使用相同的密钥。
解决这个问题最简单的方法是将键存储在两个表中。一旦某个键被使用,我们就将其移动到一个单独的表中,并设置适当的锁定。此外,为了提高读取速度,我们可以将一些键保留在内存中。
KGS数据库估计
根据我们的讨论,我们可以生成最多约 568 亿个独特的 6 个字符长的密钥,这将导致我们必须存储 300 GB 的密钥。
虽然对于这个简单的用例来说 390 GB 似乎很多,但重要的是要记住这是我们整个服务生命周期的大小,并且密钥数据库的大小不会像我们的主要数据库那样增加。
缓存
现在,我们来谈谈缓存。根据我们的估算,我们每天大约需要 35 GB 的内存来缓存 20% 的服务请求。对于这种用例,我们可以将Redis或Memcached服务器与 API 服务器一起使用。
有关详细信息,请参阅缓存。
设计
现在我们已经确定了一些核心组件,让我们开始系统设计的初稿。
工作原理如下:
创建新的 URL
- 当用户创建新的 URL 时,我们的 API 服务器会从密钥生成服务 (KGS) 请求一个新的唯一密钥。
- 密钥生成服务向 API 服务器提供唯一密钥,并将该密钥标记为已使用。
- API 服务器将新的 URL 条目写入数据库和缓存。
- 我们的服务向用户返回 HTTP 201(已创建)响应。
访问 URL
- 当客户端导航到某个短 URL 时,请求就会发送到 API 服务器。
- 请求首先访问缓存,如果在那里找不到条目,则从数据库中检索,并向原始 URL 发出 HTTP 301(重定向)。
- 如果在数据库中仍然找不到该密钥,则会向用户发送 HTTP 404(未找到)错误。
详细设计
现在是时候讨论我们设计的细节了。
数据分区
为了扩展数据库,我们需要对数据进行分区。水平分区(又称分片)是一个很好的第一步。我们可以使用以下分区方案:
- 基于哈希的分区
- 基于列表的分区
- 基于范围的分区
- 复合分区
上述方法仍然会导致数据和负载分布不均匀,我们可以使用一致性哈希来解决这个问题。
数据库清理
这更像是我们服务的维护步骤,取决于我们是保留过期条目还是将其删除。如果我们决定删除过期条目,我们可以通过两种不同的方式进行:
主动清理
在主动清理中,我们将运行一个单独的清理服务,该服务将定期从存储和缓存中移除过期链接。这将是一个非常轻量级的服务,类似于cron 作业。
被动清理
对于被动清理,我们可以在用户尝试访问过期链接时删除相应条目。这可以确保数据库和缓存的延迟清理。
缓存
现在让我们来讨论一下缓存。
使用哪种缓存驱逐策略?
正如我们之前讨论过的,我们可以使用Redis或Memcached等解决方案并缓存 20% 的每日流量,但哪种缓存驱逐策略最适合我们的需求?
对我们的系统来说,最近最少使用(LRU)策略可能是一个不错的选择。在这个策略中,我们首先丢弃最近最少使用的键。
如何处理缓存未命中?
每当出现缓存未命中时,我们的服务器可以直接访问数据库并使用新条目更新缓存。
指标和分析
记录分析和指标是我们的扩展需求之一。我们可以将访客的国家/地区、平台、浏览次数等元数据与 URL 条目一起存储在数据库中并进行更新。
安全
为了安全起见,我们可以引入私有 URL 和授权机制。可以使用单独的表来存储有权访问特定 URL 的用户 ID。如果用户没有适当的权限,我们可以返回 HTTP 401(未授权)错误。
我们还可以使用API 网关,因为它们可以开箱即用地支持授权、速率限制和负载平衡等功能。
识别并解决瓶颈
让我们识别并解决设计中的单点故障等瓶颈:
- “如果 API 服务或密钥生成服务崩溃怎么办?”
- “我们将如何在组件之间分配流量?”
- “我们如何才能减轻数据库的负载?”
- “如果KGS使用的密钥数据库出现故障怎么办?”
- “如何提高我们的缓存的可用性?”
为了使我们的系统更具弹性,我们可以执行以下操作:
- 运行我们的服务器和密钥生成服务的多个实例。
- 在客户端、服务器、数据库和缓存服务器之间引入负载平衡器。
- 由于我们的数据库是一个读取密集型系统,因此对其使用多个读取副本。
- 我们的关键数据库的备用副本,以防万一它出现故障。
- 我们的分布式缓存有多个实例和副本。
让我们设计一个类似Whatsapp 的即时通讯服务,类似于Whatsapp、Facebook Messenger和微信等服务。
Whatsapp 是什么?
Whatsapp 是一款为用户提供即时通讯服务的聊天应用。它是全球使用最广泛的移动应用之一,连接着 180 多个国家的 20 多亿用户。Whatsapp 也提供网页版。
要求
我们的系统应满足以下要求:
功能要求
- 应该支持一对一聊天。
- 群聊(最多 100 人)。
- 应支持文件共享(图像、视频等)。
非功能性需求
- 高可用性和最小延迟。
- 该系统应具有可扩展性和高效性。
扩展要求
- 消息的已发送、已送达和已读回执。
- 显示用户最后上线时间。
- 推送通知。
估计和约束
让我们从估计和约束开始。
注意:请务必与面试官核对任何与规模或交通相关的假设。
交通
假设我们有 5000 万日活跃用户 (DAU),平均每个用户每天向 4 个不同的人发送至少 10 条消息。这样,我们每天的消息量就达到了 20 亿条。
消息中还可以包含图片、视频或其他文件等媒体文件。我们可以假设 5% 的消息是用户共享的媒体文件,这意味着我们需要额外存储 2 亿个文件。
我们的系统的每秒请求数(RPS)是多少?
每天 20 亿个请求相当于每秒 24000 个请求。
贮存
如果我们假设每条消息平均为 100 字节,那么我们每天将需要大约 200 GB 的数据库存储空间。
根据我们的需求,我们还知道每天大约有 5% 的消息(1 亿条)是媒体文件。假设每个文件平均大小为 50 KB,那么每天就需要 10 TB 的存储空间。
10 年后,我们将需要大约 38 PB 的存储空间。
带宽
由于我们的系统每天要处理 10.2 TB 的入口数据,因此我们需要每秒约 120 MB 的最低带宽。
高层估计
以下是我们的高级估计:
类型 | 估计 |
---|---|
每日活跃用户(DAU) | 5000万 |
每秒请求数 (RPS) | 24K/秒 |
存储(每天) | 约10.2 TB |
储存(10年) | ~38 PB |
带宽 | ~120 MB/s |
数据模型设计
这是反映我们要求的通用数据模型。
我们有下表:
用户
该表将包含用户的信息,例如name
、phoneNumber
和其他详细信息。
消息
顾名思义,该表将存储具有type
(文本、图像、视频等)属性的消息,content
以及用于消息传递的时间戳。消息还将具有相应的chatID
或groupID
。
聊天
该表基本上代表两个用户之间的私人聊天,并且可以包含多条消息。
用户聊天
该表将用户和聊天进行映射,因为多个用户可以有多个聊天(N:M 关系),反之亦然。
组
该表代表多个用户之间的组。
用户组
该表映射用户和组,因为多个用户可以成为多个组的一部分(N:M 关系),反之亦然。
我们应该使用什么样的数据库?
虽然我们的数据模型看起来相当相关,但我们不一定需要将所有内容存储在单个数据库中,因为这会限制我们的可扩展性并很快成为瓶颈。
我们将数据拆分到不同的服务,每个服务拥有特定表的所有权。然后,我们可以将关系数据库(例如PostgreSQL)或分布式 NoSQL 数据库(例如Apache Cassandra)用于我们的用例。
API 设计
让我们为我们的服务做一个基本的 API 设计:
获取所有聊天或群组
此 API 将获取给定的所有聊天或群组userID
。
getAll(userID: UUID): Chat[] | Group[]
参数
用户 ID(UUID
):当前用户的 ID。
返回
结果(Chat[] | Group[]
):用户所属的所有聊天和群组。
获取消息
channelID
获取给定(聊天或群组 ID)的用户的所有消息。
getMessages(userID: UUID, channelID: UUID): Message[]
参数
用户 ID(UUID
):当前用户的 ID。
频道 ID ( UUID
):需要从中检索消息的频道(聊天或群组)的 ID。
返回
消息(Message[]
):给定聊天或群组中的所有消息。
发送消息
将用户消息发送到频道(聊天或群组)。
sendMessage(userID: UUID, channelID: UUID, message: Message): boolean
参数
用户 ID(UUID
):当前用户的 ID。
频道 ID ( UUID
):用户想要发送消息的频道(聊天或群组)的 ID。
消息(Message
):用户想要发送的消息(文本、图像、视频等)。
返回
结果(boolean
):表示操作是否成功。
加入或离开群组
将用户消息发送到频道(聊天或群组)。
joinGroup(userID: UUID, channelID: UUID): boolean
leaveGroup(userID: UUID, channelID: UUID): boolean
参数
用户 ID(UUID
):当前用户的 ID。
频道 ID ( UUID
):用户想要加入或离开的频道(聊天或群组)的 ID。
返回
结果(boolean
):表示操作是否成功。
高层设计
现在让我们对我们的系统进行高层设计。
建筑学
我们将使用微服务架构,因为它可以更轻松地水平扩展和解耦我们的服务。每个服务都拥有自己的数据模型。让我们尝试将系统划分为几个核心服务。
用户服务
这是一个基于 HTTP 的服务,用于处理与用户相关的问题,例如身份验证和用户信息。
聊天服务
聊天服务将使用 WebSocket 并与客户端建立连接,以处理聊天和群组消息相关功能。我们还可以使用缓存来跟踪所有活动连接(类似于会话),这将有助于我们判断用户是否在线。
通知服务
该服务只会向用户发送推送通知。具体细节我们将另行讨论。
在线服务
在线状态服务会记录所有用户的最后在线状态。我们将另行详细讨论。
媒体服务
此服务将处理媒体(图片、视频、文件等)的上传。我们将另行详细讨论。
服务间通信和服务发现怎么样?
由于我们的架构基于微服务,因此服务之间也会相互通信。通常,REST 或 HTTP 的性能表现良好,但我们可以使用更轻量、更高效的gRPC来进一步提升性能。
服务发现是我们需要考虑的另一件事。我们还可以使用服务网格,实现各个服务之间可管理、可观察且安全的通信。
注意:了解有关REST、GraphQL、gRPC 的更多信息以及它们之间的比较。
实时消息传递
如何高效地发送和接收消息?我们有两种选择:
拉动模型
客户端可以定期向服务器发送 HTTP 请求,检查是否有新消息。这可以通过类似长轮询来实现。
推模型
客户端与服务器建立长连接,一旦有新数据可用,就会将其推送到客户端。我们可以使用WebSocket或服务器发送事件 (SSE)来实现这一点。
拉取模型不可扩展,因为它会在我们的服务器上产生不必要的请求开销,并且大多数情况下响应都是空的,从而浪费我们的资源。为了最大限度地降低延迟,使用WebSocket的推送模型是更好的选择,因为这样,只要与客户端的连接处于打开状态,我们就可以立即将数据推送到客户端,而不会有任何延迟。此外,WebSocket 提供全双工通信,而服务器发送事件 (SSE)则只是单向的。
注意:了解有关长轮询、WebSockets、服务器发送事件(SSE)的更多信息。
上次出现
为了实现“上次查看”功能,我们可以使用心跳机制,客户端可以定期 ping 服务器以指示其活动状态。由于需要尽可能降低开销,我们可以将上次活动时间戳存储在缓存中,如下所示:
钥匙 | 价值 |
---|---|
用户 A | 2022年7月1日14:32:50 |
用户B | 2022-07-05T05:10:35 |
用户C | 2022-07-10T04:33:25 |
这将返回用户上次活动的时间。此功能将由状态服务结合Redis或Memcached作为缓存来处理。
另一种实现方法是跟踪用户的最新操作。一旦用户最近的活动超过某个阈值,例如“用户在过去 30 秒内未执行任何操作”,我们就可以将用户显示为离线,并使用最后记录的时间戳来记录上次上线时间。这更像是一种惰性更新方法,在某些情况下可能比心跳更新更有优势。
通知
一旦在聊天或群组中发送消息,我们将首先检查收件人是否处于活动状态,我们可以通过考虑用户的活动连接和上次查看来获取此信息。
如果收件人不活跃,聊天服务将向消息队列中添加一个事件,其中包含额外的元数据,例如客户端的设备平台,稍后将用于将通知路由到正确的平台。
然后,通知服务将从消息队列中消费该事件,并根据客户端的设备平台(Android、iOS、Web 等)将请求转发到Firebase 云消息传递 (FCM)或Apple 推送通知服务 (APNS)。我们还可以添加对电子邮件和短信的支持。
我们为什么要使用消息队列?
由于大多数消息队列都提供尽力排序,这确保了消息通常按照发送的顺序传递,并且消息至少传递一次,这是我们服务功能的重要组成部分。
虽然这看起来像是一个典型的发布-订阅用例,但实际上并非如此,因为移动设备和浏览器各自都有处理推送通知的方式。通常,通知是通过 Firebase 云消息传递 (FCM) 或 Apple 推送通知服务 (APNS) 在外部处理的,这与我们在后端服务中常见的消息扇出不同。我们可以使用Amazon SQS或RabbitMQ之类的服务来支持此功能。
已读回执
处理已读回执可能比较棘手,对于这种情况,我们可以等待客户端的某种确认 (ACK)来确定消息是否已送达,并更新相应的deliveredAt
字段。同样,我们会将消息标记为用户打开聊天后看到的消息,并更新相应的seenAt
时间戳字段。
设计
现在我们已经确定了一些核心组件,让我们开始系统设计的初稿。
详细设计
现在是时候详细讨论我们的设计决策了。
数据分区
为了扩展数据库,我们需要对数据进行分区。水平分区(又称分片)是一个很好的第一步。我们可以使用以下分区方案:
- 基于哈希的分区
- 基于列表的分区
- 基于范围的分区
- 复合分区
上述方法仍然会导致数据和负载分布不均匀,我们可以使用一致性哈希来解决这个问题。
缓存
在消息传递应用中,我们必须谨慎使用缓存,因为用户期望获取最新数据,但许多用户会请求相同的消息,尤其是在群聊中。因此,为了防止资源使用量激增,我们可以缓存较旧的消息。
有些群聊可能包含数千条消息,通过网络发送这些消息效率极低。为了提高效率,我们可以在系统 API 中添加分页功能。这项功能对于网络带宽有限的用户非常实用,因为他们无需在需要时才检索旧消息。
使用哪种缓存驱逐策略?
我们可以使用Redis或Memcached等解决方案并缓存 20% 的每日流量,但哪种缓存驱逐策略最适合我们的需求?
对我们的系统来说,最近最少使用(LRU)策略可能是一个不错的选择。在这个策略中,我们首先丢弃最近最少使用的键。
如何处理缓存未命中?
每当出现缓存未命中时,我们的服务器可以直接访问数据库并使用新条目更新缓存。
有关详细信息,请参阅缓存。
媒体访问和存储
众所周知,我们的大部分存储空间将用于存储媒体文件,例如图像、视频或其他文件。我们的媒体服务将处理用户媒体文件的访问和存储。
但是,我们可以在哪里大规模存储文件呢?嗯,对象存储就是我们想要的。对象存储将数据文件分解成称为对象的块。然后,它将这些对象存储在一个存储库中,该存储库可以分布在多个联网系统中。我们也可以使用分布式文件存储,例如HDFS或GlusterFS。
有趣的事实:一旦用户下载了媒体,Whatsapp 就会在其服务器上删除该媒体。
对于这种用例,我们可以使用Amazon S3、Azure Blob Storage或Google Cloud Storage等对象存储。
内容分发网络 (CDN)
内容分发网络 (CDN)可以提高内容可用性和冗余度,同时降低带宽成本。通常,静态文件(例如图像和视频)由 CDN 提供。对于这种情况,我们可以使用Amazon CloudFront或Cloudflare CDN等服务。
API网关
由于我们将使用多种协议,例如 HTTP、WebSocket、TCP/IP,因此为每种协议分别部署多个 L4(传输层)或 L7(应用层)类型的负载均衡器将会非常昂贵。因此,我们可以使用支持多种协议的API 网关,而不会出现任何问题。
API Gateway 还可以提供其他功能,例如身份验证、授权、速率限制、节流和 API 版本控制,这些功能将提高我们服务的质量。
对于这种用例,我们可以采用Amazon API Gateway或Azure API Gateway等服务。
识别并解决瓶颈
让我们识别并解决设计中的单点故障等瓶颈:
- “如果我们的某项服务崩溃了怎么办?”
- “我们将如何在组件之间分配流量?”
- “我们如何才能减轻数据库的负载?”
- “如何提高我们的缓存的可用性?”
- “API 网关不会成为单点故障吗?”
- “我们如何才能使我们的通知系统更加强大?”
- “我们如何降低媒体存储成本”?
- “聊天服务的责任是否太重了?”
为了使我们的系统更具弹性,我们可以执行以下操作:
- 运行我们每项服务的多个实例。
- 在客户端、服务器、数据库和缓存服务器之间引入负载平衡器。
- 为我们的数据库使用多个读取副本。
- 我们的分布式缓存有多个实例和副本。
- 我们可以拥有 API 网关的备用副本。
- 在分布式系统中,精确一次传递和消息排序是一项挑战,我们可以使用专用消息代理(如Apache Kafka或NATS)来使我们的通知系统更加健壮。
- 我们可以在媒体服务中添加媒体处理和压缩功能,以压缩类似Whatsapp的大文件,这将节省大量存储空间并降低成本。
- 我们可以创建一个独立于聊天服务的群组服务,以进一步解耦我们的服务。
叽叽喳喳
让我们设计一个类似Twitter的社交媒体服务,类似于Facebook、Instagram等服务。
什么是 Twitter?
Twitter 是一项社交媒体服务,用户可以阅读或发布短消息(最多 280 个字符),即推文。它可在网页端以及 Android 和 iOS 等移动平台上使用。
要求
我们的系统应满足以下要求:
功能要求
- 应该能够发布新的推文(可以是文本、图像、视频等)。
- 应该能够关注其他用户。
- 应该具有新闻推送功能,其中包含用户关注的人的推文。
- 应该能够搜索推文。
非功能性需求
- 高可用性和最小延迟。
- 该系统应具有可扩展性和高效性。
扩展要求
- 指标和分析。
- 转发功能。
- 最喜歡的響言。
估计和约束
让我们从估计和约束开始。
注意:请务必与面试官核对任何与规模或交通相关的假设。
交通
这将是一个阅读量很大的系统。假设我们拥有 10 亿用户,其中每日活跃用户 (DAU) 为 2 亿,平均每位用户每天发推文 5 条。这样,我们每天的推文量就达到了 10 亿条。
推文还可以包含图片或视频等媒体文件。我们可以假设 10% 的推文是用户分享的媒体文件,这意味着我们需要额外存储 1 亿个文件。
我们的系统的每秒请求数(RPS)是多少?
每天 10 亿个请求相当于每秒 12000 个请求。
贮存
如果我们假设每条消息平均为 100 字节,那么我们每天将需要大约 100 GB 的数据库存储空间。
我们还知道,根据我们的要求,我们每天大约有 10% 的消息(1 亿条)是媒体文件。假设每个文件平均大小为 50 KB,那么我们每天将需要 5 TB 的存储空间。
10 年后,我们将需要大约 19 PB 的存储空间。
带宽
由于我们的系统每天要处理 5.1 TB 的入口数据,因此我们需要每秒约 60 MB 的最低带宽。
高层估计
以下是我们的高级估计:
类型 | 估计 |
---|---|
每日活跃用户(DAU) | 1亿 |
每秒请求数 (RPS) | 12K/秒 |
存储(每天) | ~5.1 TB |
储存(10年) | ~19 PB |
带宽 | ~60 MB/s |
数据模型设计
这是反映我们要求的通用数据模型。
我们有下表:
用户
该表将包含用户的信息,例如,,,name
和其他详细信息。email
dob
推文
顾名思义,该表将存储推文及其属性,例如type
(文本,图像,视频等)content
等。我们还将存储相应的userID
。
收藏夹
该表将推文与用户进行映射,以实现我们应用程序中的收藏推文功能。
追随者
该表将关注者和被关注者映射为用户可以互相关注(N:M 关系)。
提要
该表存储了饲料属性及其相应的属性userID
。
feeds_tweets
该表映射了推文和 feed(N:M 关系)。
我们应该使用什么样的数据库?
虽然我们的数据模型看起来相当相关,但我们不一定需要将所有内容存储在单个数据库中,因为这会限制我们的可扩展性并很快成为瓶颈。
我们将数据拆分到不同的服务,每个服务拥有特定表的所有权。然后,我们可以将关系数据库(例如PostgreSQL)或分布式 NoSQL 数据库(例如Apache Cassandra)用于我们的用例。
API 设计
让我们为我们的服务做一个基本的 API 设计:
发布推文
该 API 将允许用户在平台上发布推文。
postTweet(userID: UUID, content: string, mediaURL?: string): boolean
参数
用户 ID(UUID
):用户的 ID。
内容(string
):推文的内容。
媒体 URL ( string
):附加媒体的 URL (可选)。
返回
结果(boolean
):表示操作是否成功。
关注或取消关注用户
此 API 将允许用户关注或取消关注其他用户。
follow(followerID: UUID, followeeID: UUID): boolean
unfollow(followerID: UUID, followeeID: UUID): boolean
参数
关注者ID(UUID
):当前用户的ID。
关注者 ID(UUID
):我们想要关注或取消关注的用户的 ID。
媒体 URL ( string
):附加媒体的 URL (可选)。
返回
结果(boolean
):表示操作是否成功。
获取新闻源
此 API 将返回在给定新闻源中显示的所有推文。
getNewsfeed(userID: UUID): Tweet[]
参数
用户 ID(UUID
):用户的 ID。
返回
推文(Tweet[]
):在给定的新闻源中显示的所有推文。
高层设计
现在让我们对我们的系统进行高层设计。
建筑学
我们将使用微服务架构,因为它可以更轻松地水平扩展和解耦我们的服务。每个服务都拥有自己的数据模型。让我们尝试将系统划分为几个核心服务。
用户服务
该服务处理与用户相关的问题,例如身份验证和用户信息。
新闻推送服务
该服务将处理用户新闻推送的生成和发布。具体细节我们将另行讨论。
推特服务
推文服务将处理与推文相关的用例,例如发布推文、收藏等。
搜索服务
该服务负责处理与搜索相关的功能。我们将另行详细讨论。
媒体服务
此服务将处理媒体(图片、视频、文件等)的上传。我们将另行详细讨论。
通知服务
该服务只会向用户发送推送通知。
分析服务
该服务将用于指标和分析用例。
服务间通信和服务发现怎么样?
由于我们的架构基于微服务,因此服务之间也会相互通信。通常,REST 或 HTTP 的性能表现良好,但我们可以使用更轻量、更高效的gRPC来进一步提升性能。
服务发现是我们需要考虑的另一件事。我们还可以使用服务网格,实现各个服务之间可管理、可观察且安全的通信。
注意:了解有关REST、GraphQL、gRPC 的更多信息以及它们之间的比较。
新闻源
说到新闻推送,实现起来似乎很容易,但有很多因素会影响这个功能的成败。所以,让我们把问题分成两部分:
一代
假设我们要为用户 A 生成 feed,我们将执行以下步骤:
- 检索用户 A 关注的所有用户和实体(主题标签、主题等)的 ID。
- 获取每个检索到的 ID 的相关推文。
- 使用排名算法根据相关性、时间、参与度等参数对推文进行排名。
- 将排名后的推文数据以分页的方式返回给客户端。
生成推文是一个繁琐的过程,可能会耗费大量时间,尤其是对于关注人数较多的用户而言。为了提升性能,可以预先生成推文并将其存储在缓存中,然后我们可以通过一种机制定期更新推文,并将我们的排名算法应用于新推文。
出版
发布是根据每个特定用户推送动态数据的步骤。这可能是一个相当繁重的操作,因为一个用户可能有数百万的好友或粉丝。为了解决这个问题,我们有三种不同的方法:
- 拉动模型(或负载扇出)
当用户创建推文,关注者刷新其新闻源时,该新闻源会被创建并存储在内存中。只有当用户请求时,才会加载最新的新闻源。这种方法减少了数据库的写入操作次数。
这种方法的缺点是,除非用户从服务器“拉”数据,否则他们将无法查看最新的信息,这将增加服务器上的读取操作次数。
- 推送模型(或写入时扇出)
在这个模型中,用户一旦创建推文,就会立即“推送”到所有关注者的动态。这样一来,系统就无需逐一检查用户的整个关注者列表来查看更新。
然而,这种方法的缺点是它会增加数据库的写入操作次数。
- 混合模型
第三种方法是拉动模型和推动模型之间的混合模型。它结合了上述两种模型的优点,并试图在两者之间提供一种平衡的方法。
混合模型仅允许关注者数量较少的用户使用推送模型,而对于关注者数量较多的用户(名人),将使用拉取模型。
排名算法
正如我们所讨论的,我们需要一个排名算法来根据每条推文与每个特定用户的相关性对其进行排名。
例如,Facebook 曾经使用过EdgeRank算法,其中每个 feed 项的排名由以下公式描述:
在哪里,
Affinity
:表示用户与边线创建者的“亲密度”。如果用户经常点赞、评论或留言给边线创建者,那么亲密度值就会更高,从而导致帖子排名更高。
Weight
:是根据每条边分配的值。评论的权重可能比点赞更高,因此评论较多的帖子更有可能获得更高的排名。
Decay
:衡量边的生成时间。边越老,衰减值越小,最终的等级就越低。
如今,算法变得更加复杂,排名是使用机器学习模型来完成的,该模型可以考虑数千个因素。
转发
转发是我们的扩展需求之一。为了实现此功能,我们只需创建一条新推文,其中包含转发原推文的用户 ID,然后修改新推文的type
枚举和content
属性,使其与原推文关联起来。
例如,type
枚举属性可以是 tweet 类型,类似于文本、视频等,并且content
可以是原始推文的 ID。这里第一行表示原始推文,而第二行表示转发推文。
ID | 用户身份 | 类型 | 内容 | 创建于 |
---|---|---|---|---|
ad34-291a-45f6-b36c | 7a2c-62c4-4dc8-b1bb | 文本 | 嘿,这是我的第一条推文…… | 1658905644054 |
f064-49ad-9aa2-84a6 | 6aa2-2bc9-4331-879f | 鸣叫 | ad34-291a-45f6-b36c | 1658906165427 |
这是一个非常基本的实现,为了改进它,我们可以创建一个单独的表来存储转发。
搜索
有时,传统的 DBMS 性能不够强,我们需要一些能够快速、近乎实时地存储、搜索和分析海量数据,并在几毫秒内给出结果的工具。Elasticsearch可以帮助我们实现这一目标。
Elasticsearch是一个分布式、免费且开放的搜索和分析引擎,适用于所有类型的数据,包括文本、数字、地理空间、结构化和非结构化数据。它构建于Apache Lucene之上。
我们如何识别热门话题?
趋势功能将基于搜索功能。我们可以缓存最近N
几秒内搜索最频繁的查询、标签和主题,并M
使用某种批处理机制每秒更新一次。我们的排名算法也可以应用于趋势主题,赋予它们更高的权重,并为用户提供个性化服务。
通知
推送通知是任何社交媒体平台不可或缺的一部分。我们可以使用消息队列或消息代理(例如Apache Kafka)配合通知服务,将请求发送到Firebase 云消息传递 (FCM)或Apple 推送通知服务 (APNS),后者负责将推送通知发送到用户设备。
有关更多详细信息,请参阅我们在其中讨论推送通知的Whatsapp系统设计。
详细设计
现在是时候详细讨论我们的设计决策了。
数据分区
为了扩展数据库,我们需要对数据进行分区。水平分区(又称分片)是一个很好的第一步。我们可以使用以下分区方案:
- 基于哈希的分区
- 基于列表的分区
- 基于范围的分区
- 复合分区
上述方法仍然会导致数据和负载分布不均匀,我们可以使用一致性哈希来解决这个问题。
共同的朋友
对于共同好友,我们可以为每个用户构建一个社交图谱。图中的每个节点代表一个用户,一条有向边代表关注者和被关注者。之后,我们可以遍历用户的关注者,找到并推荐共同好友。这需要使用像Neo4j和ArangoDB这样的图数据库。
这是一个非常简单的算法,为了提高我们的建议准确性,我们需要结合使用机器学习作为我们算法一部分的推荐模型。
指标和分析
记录分析和指标是我们的扩展需求之一。由于我们将使用Apache Kafka发布各种事件,因此我们可以使用Apache Spark(一个用于大规模数据处理的开源统一分析引擎)来处理这些事件并对数据进行分析。
缓存
在社交媒体应用中,我们必须谨慎使用缓存,因为用户期望获取最新数据。因此,为了防止资源使用量激增,我们可以缓存排名前 20% 的推文。
为了进一步提高效率,我们可以在系统 API 中添加分页功能。这项功能对于网络带宽有限的用户来说非常实用,因为他们无需在需要时才检索旧消息。
使用哪种缓存驱逐策略?
我们可以使用Redis或Memcached等解决方案并缓存 20% 的每日流量,但哪种缓存驱逐策略最适合我们的需求?
对我们的系统来说,最近最少使用(LRU)策略可能是一个不错的选择。在这个策略中,我们首先丢弃最近最少使用的键。
如何处理缓存未命中?
每当出现缓存未命中时,我们的服务器可以直接访问数据库并使用新条目更新缓存。
有关详细信息,请参阅缓存。
媒体访问和存储
众所周知,我们的大部分存储空间将用于存储媒体文件,例如图像、视频或其他文件。我们的媒体服务将处理用户媒体文件的访问和存储。
但是,我们可以在哪里大规模存储文件呢?嗯,对象存储就是我们想要的。对象存储将数据文件分解成称为对象的块。然后,它将这些对象存储在一个存储库中,该存储库可以分布在多个联网系统中。我们也可以使用分布式文件存储,例如HDFS或GlusterFS。
内容分发网络 (CDN)
内容分发网络 (CDN)可以提高内容可用性和冗余度,同时降低带宽成本。通常,静态文件(例如图像和视频)由 CDN 提供。对于这种情况,我们可以使用Amazon CloudFront或Cloudflare CDN等服务。
识别并解决瓶颈
让我们识别并解决设计中的单点故障等瓶颈:
- “如果我们的某项服务崩溃了怎么办?”
- “我们将如何在组件之间分配流量?”
- “我们如何才能减轻数据库的负载?”
- “如何提高我们的缓存的可用性?”
- “我们如何才能使我们的通知系统更加强大?”
- “我们如何降低媒体存储成本”?
为了使我们的系统更具弹性,我们可以执行以下操作:
- 运行我们每项服务的多个实例。
- 在客户端、服务器、数据库和缓存服务器之间引入负载平衡器。
- 为我们的数据库使用多个读取副本。
- 我们的分布式缓存有多个实例和副本。
- 在分布式系统中,精确一次传递和消息排序是一项挑战,我们可以使用专用消息代理(如Apache Kafka或NATS)来使我们的通知系统更加健壮。
- 我们可以在媒体服务中添加媒体处理和压缩功能来压缩大文件,这将节省大量存储空间并降低成本。
Netflix
让我们设计一个类似Netflix的视频流服务,类似于Amazon Prime Video、Disney Plus、Hulu、Youtube、Vimeo等服务。
什么是 Netflix?
Netflix 是一项基于订阅的流媒体服务,允许其会员在联网设备上观看电视节目和电影。它可在 Web、iOS、Android、电视等平台上使用。
要求
我们的系统应满足以下要求:
功能要求
- 用户应该能够流式传输和共享视频。
- 内容团队(或 YouTube 的用户)应该能够上传新视频(电影、电视节目剧集和其他内容)。
- 用户应该能够使用标题或标签搜索视频。
- 用户应该能够对类似于 YouTube 的视频发表评论。
非功能性需求
- 高可用性和最小延迟。
- 可靠性高,上传不会丢失。
- 该系统应具有可扩展性和高效性。
扩展要求
- 某些内容应该受到地理封锁。
- 从用户停止的地方继续播放视频。
- 记录视频的指标和分析。
估计和约束
让我们从估计和约束开始。
注意:请务必与面试官核对任何与规模或交通相关的假设。
交通
这将是一个阅读量很大的系统。假设我们拥有 10 亿用户,其中每日活跃用户 (DAU) 为 2 亿,平均每位用户每天观看 5 个视频。这意味着每天观看的视频数量为 10 亿。
假设200:1
读写比率为 5000 万,每天将上传约 5000 万个视频。
我们的系统的每秒请求数(RPS)是多少?
每天 10 亿个请求相当于每秒 12000 个请求。
贮存
如果我们假设每个视频平均为 100 MB,那么我们每天将需要大约 5 PB 的存储空间。
而 10 年后,我们将需要高达 18,250 PB 的存储空间。
带宽
由于我们的系统每天要处理 5 PB 的入口数据,因此我们需要每秒约 58 GB 的最低带宽。
高层估计
以下是我们的高级估计:
类型 | 估计 |
---|---|
每日活跃用户(DAU) | 2亿 |
每秒请求数 (RPS) | 12K/秒 |
存储(每天) | ~5 PB |
储存(10年) | ~18,250 PB |
带宽 | ~58 GB/秒 |
数据模型设计
这是反映我们要求的通用数据模型。
我们有下表:
用户
该表将包含用户的信息,例如,,,name
和其他详细信息。email
dob
视频
顾名思义,该表将存储视频及其属性,例如title
,,等streamURL
。tags
我们还将存储相应的userID
。
标签
该表将仅存储与视频相关的标签。
视图
该表帮助我们存储视频收到的所有观看次数。
评论
该表存储了有关视频(如 YouTube)的所有评论。
我们应该使用什么样的数据库?
虽然我们的数据模型看起来相当相关,但我们不一定需要将所有内容存储在单个数据库中,因为这会限制我们的可扩展性并很快成为瓶颈。
我们将数据拆分到不同的服务,每个服务拥有特定表的所有权。然后,我们可以将关系数据库(例如PostgreSQL)或分布式 NoSQL 数据库(例如Apache Cassandra)用于我们的用例。
API 设计
让我们为我们的服务做一个基本的 API 设计:
上传视频
通过给定字节流,此 API 可以将视频上传到我们的服务。
uploadVideo(title: string, description: string, data: Stream<byte>, tags?: string[]): boolean
参数
标题(string
):新视频的标题。
描述 ( string
):新视频的描述。
数据(Byte[]
):视频数据的字节流。
标签(string[]
):视频的标签(可选)。
返回
结果(boolean
):表示操作是否成功。
流式传输视频
此 API 允许我们的用户使用首选的编解码器和分辨率来流式传输视频。
streamVideo(videoID: UUID, codec: Enum<string>, resolution: Tuple<int>, offset?: int): VideoStream
参数
视频ID(UUID
):需要流式传输的视频的ID。
编解码器(Enum<string>
):请求视频所需的编解码器h.265
,例如、、等h.264
。VP9
分辨率(Tuple<int>
):请求的视频的分辨率。
偏移量(int
):视频流与视频中任意点之间的数据流偏移量(以秒为单位)(可选)。
返回
流(VideoStream
):请求的视频的数据流。
搜索视频
该 API 将使我们的用户能够根据标题或标签搜索视频。
searchVideo(query: string, nextPage?: string): Video[]
参数
查询(string
):来自用户的搜索查询。
下一页(string
):下一页的令牌,可用于分页(可选)。
返回
视频(Video[]
):符合特定搜索查询的所有视频。
添加评论
此 API 将允许我们的用户对视频发表评论(如 YouTube)。
comment(videoID: UUID, comment: string): boolean
参数
VideoID(UUID
):用户想要评论的视频的ID。
评论(string
):评论的文本内容。
返回
结果(boolean
):表示操作是否成功。
高层设计
现在让我们对我们的系统进行高层设计。
建筑学
我们将使用微服务架构,因为它可以更轻松地水平扩展和解耦我们的服务。每个服务都拥有自己的数据模型。让我们尝试将系统划分为几个核心服务。
用户服务
该服务处理与用户相关的问题,例如身份验证和用户信息。
流服务
推文服务将处理与视频流相关的功能。
搜索服务
该服务负责处理与搜索相关的功能。我们将另行详细讨论。
媒体服务
该服务将负责视频的上传和处理。具体细节我们将另行讨论。
分析服务
该服务将用于指标和分析用例。
服务间通信和服务发现怎么样?
由于我们的架构基于微服务,因此服务之间也会相互通信。通常,REST 或 HTTP 的性能表现良好,但我们可以使用更轻量、更高效的gRPC来进一步提升性能。
服务发现是我们需要考虑的另一件事。我们还可以使用服务网格,实现各个服务之间可管理、可观察且安全的通信。
注意:了解有关REST、GraphQL、gRPC 的更多信息以及它们之间的比较。
视频处理
在处理视频时,变量非常多。例如,高端摄像机拍摄的两小时原始 8K 视频的平均数据量很容易就达到 4 TB,因此我们需要进行某种处理来降低存储和传输成本。
以下是内容团队(或 YouTube 的用户)上传视频后,我们在消息队列中排队等待处理时的处理方式。
让我们讨论一下这是如何工作的:
- 文件分块器
这是我们处理流程的第一步。文件分块是将文件分割成更小的块(称为“块”)的过程。它可以帮助我们消除存储中重复数据的重复副本,并通过仅选择已更改的块来减少通过网络发送的数据量。
通常,可以根据时间戳将视频文件分割成大小相等的块,但 Netflix 却根据场景分割块,这种细微的变化成为改善用户体验的一个重要因素,因为每当客户端从服务器请求一个块时,中断的可能性就会降低,因为会检索到完整的场景。
- 内容过滤器
此步骤检查视频是否符合平台的内容政策,对于 Netflix 来说,可以根据媒体的内容评级进行预先批准,或者可以像 YouTube 一样严格执行。
整个步骤由机器学习模型完成,该模型会执行版权、盗版和 NSFW 检查。如果发现问题,我们会将任务推送到死信队列 (DLQ),以便审核团队进行进一步检查。
- 转码器
转码是将原始数据解码为中间未压缩格式,然后将其编码为目标格式的过程。此过程使用不同的编解码器来执行比特率调整、图像下采样或媒体重新编码。
这样可以生成更小的文件,并针对目标设备提供更优化的格式。您可以使用FFmpeg等独立解决方案或AWS Elemental MediaConvert等云端解决方案来实现流水线的这一步骤。
- 质量转换
这是处理管道的最后一步,顾名思义,此步骤处理上一步转码媒体转换为不同分辨率,例如 4K、1440p、1080p、720p 等。
这使我们能够根据用户的要求获取所需的视频质量,并且一旦媒体文件完成处理,它将被上传到分布式文件存储(如HDFS、GlusterFS)或对象存储(如Amazon S3 ) ,以便在流式传输期间稍后检索。
注意:我们可以添加其他步骤(例如字幕和缩略图生成)作为管道的一部分。
我们为什么要使用消息队列?
将视频处理作为一项长期运行的任务更加合理,而且消息队列还能将视频处理流程与上传功能解耦。我们可以使用Amazon SQS或RabbitMQ之类的工具来支持这一点。
视频流
无论从客户端还是服务器的角度来看,视频流传输都是一项极具挑战性的任务。此外,不同用户的网络连接速度差异很大。为了确保用户不会重复获取相同的内容,我们可以使用内容分发网络 (CDN)。
Netflix 通过其Open Connect计划更进一步。通过这种方式,他们与数千家互联网服务提供商 (ISP) 合作,实现流量本地化,并更高效地交付内容。
Netflix 的 Open Connect 与传统内容分发网络 (CDN) 有何区别?
Netflix Open Connect 是我们专门构建的内容分发网络 (CDN),负责服务 Netflix 的视频流量。全球约 95% 的流量是通过 Open Connect 与其客户用于访问互联网的 ISP 之间的直接连接传输的。
目前,Netflix 在全球 1000 多个不同地点部署了 Open Connect Appliances(OCA)。一旦出现问题,Open Connect Appliances(OCA)可以进行故障转移,并将流量重新路由到 Netflix 服务器。
此外,我们可以使用自适应比特率流协议,例如HTTP 实时流 (HLS),该协议专为可靠性而设计,可通过优化播放以适应可用的连接速度来动态适应网络条件。
最后,为了从用户离开的地方播放视频(我们的扩展要求的一部分),我们可以简单地使用offset
存储在views
表中的属性来检索特定时间戳的场景块并为用户恢复播放。
搜索
有时,传统的 DBMS 性能不够强,我们需要一些能够快速、近乎实时地存储、搜索和分析海量数据,并在几毫秒内给出结果的工具。Elasticsearch可以帮助我们实现这一目标。
Elasticsearch是一个分布式、免费且开放的搜索和分析引擎,适用于所有类型的数据,包括文本、数字、地理空间、结构化和非结构化数据。它构建于Apache Lucene之上。
我们如何识别热门内容?
趋势功能将基于搜索功能。我们可以缓存过去几秒内搜索最频繁的查询,并使用某种批处理机制N
每秒更新一次。M
共享
共享内容是任何平台的重要组成部分,为此,我们可以提供某种 URL 缩短服务,为用户生成短 URL 以供共享。
更多详细信息,请参考URL Shortener系统设计。
详细设计
现在是时候详细讨论我们的设计决策了。
数据分区
为了扩展数据库,我们需要对数据进行分区。水平分区(又称分片)是一个很好的第一步。我们可以使用以下分区方案:
- 基于哈希的分区
- 基于列表的分区
- 基于范围的分区
- 复合分区
上述方法仍然会导致数据和负载分布不均匀,我们可以使用一致性哈希来解决这个问题。
地理封锁
Netflix 和 YouTube 等平台使用地理封锁功能来限制特定地理区域或国家/地区的内容。这主要是因为 Netflix 与制作和发行公司签订协议时必须遵守合法的发行法规。对于 YouTube 而言,这将由用户在内容发布期间控制。
我们可以使用用户个人资料中的IP或区域设置来确定用户的位置,然后使用支持地理限制功能的Amazon CloudFront等服务或使用Amazon Route53 的地理位置路由策略来限制内容,如果内容在特定地区或国家/地区不可用,则将用户重新路由到错误页面。
建议
Netflix 使用机器学习模型,该模型利用用户的观看历史来预测用户接下来可能想看什么,可以使用协同过滤之类的算法。
然而,Netflix(与 YouTube 类似)使用自己的算法,称为 Netflix 推荐引擎,该算法可以跟踪多个数据点,例如:
- 用户个人资料信息,如年龄、性别和位置。
- 用户的浏览和滚动行为。
- 用户观看影片的时间和日期。
- 用于传输内容的设备。
- 搜索次数以及搜索的术语。
有关更多详细信息,请参阅Netflix 推荐研究。
指标和分析
记录分析和指标是我们的扩展需求之一。我们可以从不同的服务中捕获数据,并使用Apache Spark(一个用于大规模数据处理的开源统一分析引擎)对数据进行分析。此外,我们可以将关键元数据存储在视图表中,以增加数据中的数据点。
缓存
在流媒体平台中,缓存至关重要。为了提升用户体验,我们必须尽可能多地缓存静态媒体内容。我们可以使用Redis或Memcached等解决方案,但哪种缓存驱逐策略最适合我们的需求呢?
使用哪种缓存驱逐策略?
对我们的系统来说,最近最少使用(LRU)策略可能是一个不错的选择。在这个策略中,我们首先丢弃最近最少使用的键。
如何处理缓存未命中?
每当出现缓存未命中时,我们的服务器可以直接访问数据库并使用新条目更新缓存。
有关详细信息,请参阅缓存。
媒体流和存储
由于我们的大部分存储空间将用于存储缩略图和视频等媒体文件。根据我们之前的讨论,媒体服务将负责媒体文件的上传和处理。
我们将使用分布式文件存储(例如HDFS、GlusterFS)或对象存储(例如Amazon S3)来存储和传输内容。
内容分发网络 (CDN)
内容分发网络 (CDN)可以提高内容可用性和冗余度,同时降低带宽成本。通常,静态文件(例如图像和视频)由 CDN 提供。对于这种情况,我们可以使用Amazon CloudFront或Cloudflare CDN等服务。
识别并解决瓶颈
让我们识别并解决设计中的单点故障等瓶颈:
- “如果我们的某项服务崩溃了怎么办?”
- “我们将如何在组件之间分配流量?”
- “我们如何才能减轻数据库的负载?”
- “如何提高我们的缓存的可用性?”
为了使我们的系统更具弹性,我们可以执行以下操作:
- 运行我们每项服务的多个实例。
- 在客户端、服务器、数据库和缓存服务器之间引入负载平衡器。
- 为我们的数据库使用多个读取副本。
- 我们的分布式缓存有多个实例和副本。
优步
让我们设计一个类似Uber的叫车服务,类似于Lyft、OLA Cabs等服务。
什么是 Uber?
Uber 是一家出行服务提供商,允许用户预订车辆和司机,类似出租车。Uber 提供网页端和 Android、iOS 等移动平台。
要求
我们的系统应满足以下要求:
功能要求
我们将为两种类型的用户设计我们的系统:客户和司机。
顾客
- 顾客应该能够看到附近所有出租车的预计到达时间和价格信息。
- 顾客应该能够预订前往目的地的出租车。
- 顾客应该能够看到司机的位置。
驱动程序
- 司机应该能够接受或拒绝顾客的乘车请求。
- 一旦司机接受行程,他们应该看到客户的接送地点。
- 司机到达目的地后应该能够将行程标记为完成。
非功能性需求
- 高可靠性。
- 高可用性和最小延迟。
- 该系统应具有可扩展性和高效性。
扩展要求
- 顾客可以在行程结束后对行程进行评分。
- 付款处理。
- 指标和分析。
估计和约束
让我们从估计和约束开始。
注意:请务必与面试官核对任何与规模或交通相关的假设。
交通
假设我们有 1 亿日活跃用户 (DAU) 和 100 万名司机,并且我们的平台平均每天可完成 1000 万次乘车。
如果平均每个用户执行 10 个操作(例如请求检查可用的乘车信息、票价、预订乘车等),我们每天将必须处理 10 亿个请求。
我们的系统的每秒请求数(RPS)是多少?
每天 10 亿个请求相当于每秒 12000 个请求。
贮存
如果我们假设每条消息平均为 400 字节,那么我们每天将需要大约 400 GB 的数据库存储空间。
10 年后,我们将需要大约 1.4 PB 的存储空间。
带宽
由于我们的系统每天要处理 400 GB 的入口数据,因此我们需要每秒约 4 MB 的最低带宽。
高层估计
以下是我们的高级估计:
类型 | 估计 |
---|---|
每日活跃用户(DAU) | 1亿 |
每秒请求数 (RPS) | 12K/秒 |
存储(每天) | ~400 GB |
储存(10年) | 约1.4PB |
带宽 | ~5 MB/秒 |
数据模型设计
这是反映我们要求的通用数据模型。
我们有下表:
顾客
该表将包含客户的信息,例如name
、email
和其他详细信息。
司机
该表将包含驾驶员的信息,例如name
、email
和dob
其他详细信息。
旅行
该表代表客户所进行的行程,并存储行程的source
、、destination
和等数据。status
出租车
该表存储了司机将要驾驶的出租车的注册号和类型(如 Uber Go、Uber XL 等)等数据。
评级
顾名思义,该表存储了行程的rating
信息。feedback
付款
付款表包含与付款相关的数据及其相应的数据tripID
。
我们应该使用什么样的数据库?
虽然我们的数据模型看起来相当相关,但我们不一定需要将所有内容存储在单个数据库中,因为这会限制我们的可扩展性并很快成为瓶颈。
我们将数据拆分到不同的服务,每个服务拥有特定表的所有权。然后,我们可以将关系数据库(例如PostgreSQL)或分布式 NoSQL 数据库(例如Apache Cassandra)用于我们的用例。
API 设计
让我们为我们的服务做一个基本的 API 设计:
叫车
通过此 API,客户将能够请求乘车。
requestRide(customerID: UUID, source: Tuple<float>, destination: Tuple<float>, cabType: Enum<string>, paymentMethod: Enum<string>): Ride
参数
客户 ID(UUID
):客户的 ID。
来源(Tuple<float>
):包含行程起始地点的纬度和经度的元组。
目的地(Tuple<float>
):包含行程目的地的纬度和经度的元组。
返回
结果(boolean
):表示操作是否成功。
取消行程
该 API 将允许客户取消行程。
cancelRide(customerID: UUID, reason?: string): boolean
参数
客户 ID(UUID
):客户的 ID。
原因(UUID
):取消行程的原因(可选)。
返回
结果(boolean
):表示操作是否成功。
接受或拒绝搭乘
该 API 将允许驾驶员接受或拒绝行程。
acceptRide(driverID: UUID, rideID: UUID): boolean
denyRide(driverID: UUID, rideID: UUID): boolean
参数
驾驶员 ID(UUID
):驾驶员的 ID。
乘车 ID ( UUID
):客户请求乘车的 ID。
返回
结果(boolean
):表示操作是否成功。
开始或结束行程
使用此 API,驾驶员将能够开始和结束行程。
startTrip(driverID: UUID, tripID: UUID): boolean
endTrip(driverID: UUID, tripID: UUID): boolean
参数
驾驶员 ID(UUID
):驾驶员的 ID。
行程 ID ( UUID
):请求行程的 ID。
返回
结果(boolean
):表示操作是否成功。
评价行程
该 API 将允许客户对行程进行评分。
rateTrip(customerID: UUID, tripID: UUID, rating: int, feedback?: string): boolean
参数
客户 ID(UUID
):客户的 ID。
行程 ID ( UUID
):已完成行程的 ID。
评分(int
):本次旅行的评分。
反馈(string
):客户对行程的反馈(可选)。
返回
结果(boolean
):表示操作是否成功。
高层设计
现在让我们对我们的系统进行高层设计。
建筑学
我们将使用微服务架构,因为它可以更轻松地水平扩展和解耦我们的服务。每个服务都拥有自己的数据模型。让我们尝试将系统划分为几个核心服务。
客户服务
该服务处理与客户相关的问题,例如身份验证和客户信息。
司机服务
该服务处理与驾驶员相关的问题,例如身份验证和驾驶员信息。
乘车服务
该服务将负责行程匹配和四叉树聚合。具体细节我们将另行讨论。
旅行服务
该服务处理我们系统中与旅行相关的功能。
支付服务
该服务将负责处理我们系统中的付款。
通知服务
该服务只会向用户发送推送通知。具体细节我们将另行讨论。
分析服务
该服务将用于指标和分析用例。
服务间通信和服务发现怎么样?
由于我们的架构基于微服务,因此服务之间也会相互通信。通常,REST 或 HTTP 的性能表现良好,但我们可以使用更轻量、更高效的gRPC来进一步提升性能。
服务发现是我们需要考虑的另一件事。我们还可以使用服务网格,实现各个服务之间可管理、可观察且安全的通信。
注意:了解有关REST、GraphQL、gRPC 的更多信息以及它们之间的比较。
这项服务预计如何运作?
我们的服务预期将按以下方式运作:
- 客户通过指定出发地、目的地、出租车类型、付款方式等来请求乘车。
- 乘车服务注册此请求,查找附近的司机,并计算预计到达时间(ETA)。
- 然后将请求广播给附近的司机,让他们接受或拒绝。
- 如果司机接受,客户在等待接客时会收到有关司机实时位置和预计到达时间 (ETA) 的通知。
- 乘客被接走后,司机可以开始行程。
- 一旦到达目的地,司机将标记行程完成并收取费用。
- 付款完成后,顾客可以根据自己的喜好对行程留下评分和反馈。
位置追踪
我们如何高效地从客户端(顾客和司机)向后端发送和接收实时位置数据?我们有两种选择:
拉动模型
客户端可以定期向服务器发送 HTTP 请求,报告其当前位置并接收预计到达时间和价格信息。这可以通过类似长轮询 的功能实现。
推模型
客户端与服务器建立长连接,一旦有新数据可用,就会将其推送到客户端。我们可以使用WebSocket或服务器发送事件 (SSE)来实现这一点。
拉取模型不可扩展,因为它会在我们的服务器上产生不必要的请求开销,并且大多数情况下响应都是空的,从而浪费我们的资源。为了最大限度地降低延迟,使用WebSocket的推送模型是更好的选择,因为这样,只要与客户端的连接处于打开状态,我们就可以立即将数据推送到客户端,而不会有任何延迟。此外,WebSocket 提供全双工通信,而服务器发送事件 (SSE)则只是单向的。
此外,客户端应用程序应该具有某种后台作业机制,以便在应用程序处于后台时 ping GPS 位置。
注意:了解有关长轮询、WebSockets、服务器发送事件(SSE)的更多信息。
行程匹配
我们需要一种高效存储和查询附近司机信息的方法。让我们探索一下可以融入设计的不同解决方案。
SQL
我们已经可以获取客户的经纬度,并且借助PostgreSQL和MySQL等数据库,我们可以执行查询,根据半径 (R) 内的经纬度 (X, Y) 查找附近的驾驶员位置。
SELECT * FROM locations WHERE lat BETWEEN X-R AND X+R AND long BETWEEN Y-R AND Y+R
然而,这是不可扩展的,并且在大型数据集上执行此查询会非常慢。
地理散列
地理散列是一种地理编码方法,用于将地理坐标(例如经纬度)编码为短的字母数字字符串。它由古斯塔沃·尼迈耶 (Gustavo Niemeyer)于 2008 年创建。
Geohash 是一种使用 Base-32 字母编码的分层空间索引,Geohash 中的第一个字符将初始位置标识为 32 个单元格之一。该单元格也包含 32 个单元格。这意味着,为了表示一个点,需要将世界递归地划分为越来越小的单元格,每个单元格的位数都增加,直到达到所需的精度。精度因子也决定了单元格的大小。
例如,坐标为 的旧金山37.7564, -122.4016
在 geohash 中可以表示为9q8yy9mf
。
现在,我们只需使用客户的 Geohash 与司机的 Geohash 进行比较,即可确定最近的可用司机。为了提高性能,我们会将司机的 Geohash 索引并存储在内存中,以便更快地检索。
四叉树
四叉树是一种树形数据结构,每个内部节点恰好有四个子节点。它们通常用于通过递归方式将二维空间细分为四个象限或区域来划分空间。每个子节点或叶节点都存储着空间信息。四叉树是八叉树的二维类似物,八叉树用于划分三维空间。
四叉树使我们能够有效地搜索二维范围内的点,其中这些点被定义为纬度/经度坐标或笛卡尔(x,y)坐标。
我们可以通过仅在某个阈值之后细分节点来节省进一步的计算。
四叉树似乎非常适合我们的用例,每次收到司机发来的位置更新时,我们都可以更新四叉树。为了减轻四叉树服务器的负载,我们可以使用Redis等内存数据存储来缓存最新更新。此外,通过应用希尔伯特曲线等映射算法,我们可以执行高效的范围查询,为客户找到附近的司机。
那么竞争条件又如何呢?
当大量乘客同时请求乘车时,很容易出现竞争条件。为了避免这种情况,我们可以将乘车匹配逻辑封装在互斥锁中,以避免任何竞争条件。此外,每个操作都应该具有事务性。
如何找到附近最好的司机?
一旦我们从 Quadtree 服务器获得了附近的司机列表,我们就可以根据平均评分、相关性、过去客户反馈等参数进行某种排名。这将使我们能够首先向最佳可用的司机广播通知。
应对高需求
在需求旺盛的情况下,我们可以使用“峰时定价”的概念。峰时定价是一种动态定价方法,即为了应对需求增加和供应受限的情况,暂时提高价格。此峰时定价可以添加到行程的基本价格中。
欲了解更多详情,请了解Uber 的动态定价机制。
付款
处理大规模支付是一项挑战,为了简化我们的系统,我们可以使用Stripe或PayPal等第三方支付处理器。付款完成后,支付处理器会将用户重定向回我们的应用程序,我们可以设置一个webhook来捕获所有与付款相关的数据。
通知
推送通知将成为我们平台不可或缺的一部分。我们可以使用消息队列或消息代理(例如Apache Kafka)配合通知服务,将请求分发到Firebase 云消息传递 (FCM)或Apple 推送通知服务 (APNS),后者负责将推送通知发送到用户设备。
有关更多详细信息,请参阅我们在其中讨论推送通知的Whatsapp系统设计。
详细设计
现在是时候详细讨论我们的设计决策了。
数据分区
要扩展数据库,我们需要对数据进行分区。水平分区(又称分片)是一个很好的第一步。我们可以基于现有的分区方案或区域对数据库进行分片。如果我们使用邮政编码等方式将位置划分为区域,就可以有效地将给定区域内的所有数据存储在固定节点上。但这仍然会导致数据和负载分布不均匀,我们可以使用一致性哈希来解决这个问题。
指标和分析
记录分析和指标是我们的扩展需求之一。我们可以从不同的服务中捕获数据,并使用Apache Spark(一个用于大规模数据处理的开源统一分析引擎)对数据进行分析。此外,我们可以将关键元数据存储在视图表中,以增加数据中的数据点。
缓存
在基于位置服务的平台中,缓存至关重要。我们必须能够缓存顾客和司机的近期位置,以便快速检索。我们可以使用Redis或Memcached等解决方案,但哪种缓存驱逐策略最适合我们的需求呢?
使用哪种缓存驱逐策略?
对我们的系统来说,最近最少使用(LRU)策略可能是一个不错的选择。在这个策略中,我们首先丢弃最近最少使用的键。
如何处理缓存未命中?
每当出现缓存未命中时,我们的服务器可以直接访问数据库并使用新条目更新缓存。
有关详细信息,请参阅缓存。
识别并解决瓶颈
让我们识别并解决设计中的单点故障等瓶颈:
- “如果我们的某项服务崩溃了怎么办?”
- “我们将如何在组件之间分配流量?”
- “我们如何才能减轻数据库的负载?”
- “如何提高我们的缓存的可用性?”
- “我们如何才能使我们的通知系统更加强大?”
为了使我们的系统更具弹性,我们可以执行以下操作:
- 运行我们每项服务的多个实例。
- 在客户端、服务器、数据库和缓存服务器之间引入负载平衡器。
- 为我们的数据库使用多个读取副本。
- 我们的分布式缓存有多个实例和副本。
- 在分布式系统中,精确一次传递和消息排序是一项挑战,我们可以使用专用消息代理(如Apache Kafka或NATS)来使我们的通知系统更加健壮。
后续步骤
恭喜,您已完成课程!
现在您已经了解了系统设计的基础知识,下面是一些额外的资源:
- 分布式系统(作者:Martin Kleppmann 博士)
- 系统设计面试:内部指南
- 微服务(作者:Chris Richardson)
- 无服务器计算
- Kubernetes
我们还建议积极关注那些将我们在课程中学到的知识大规模付诸实践的公司的工程博客:
- 微软工程
- Google 研究博客
- Netflix 技术博客
- AWS 博客
- Facebook 工程
- Uber 工程博客
- Airbnb工程
- GitHub 工程博客
- 英特尔软件博客
- LinkedIn 工程
- Paypal 开发者博客
- Twitter 工程
最后但同样重要的一点是,自愿参与公司的新项目,并向高级工程师和架构师学习,以进一步提高您的系统设计技能。
希望这门课程能给你带来美好的学习体验。我期待收到你的反馈。
祝您学习顺利!
参考
以下是创建本课程时参考的资源。
所有图表均使用Excalidraw制作,可在此处获取。
文章来源:https://dev.to/karanpratapsingh/system-design-the-complete-course-10fo