系统设计系列初衷
System Design Primer: 英文文档 GitHub - donnemartin/system-design-primer: Learn how to design large-scale systems. Prep for the system design interview. Includes Anki flashcards.
中文版: https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md
初衷主要还是为了学习系统设计,但是这个中文版看起来就像机器翻译的一样,所以还是手动做一些简单的笔记,并且在难以理解的地方对照英文版,根据自己的理解在AI的帮助下进行翻译和知识扩展。
异步
资料来源:可缩放系统构架介绍
同步和异步
同步和异步是计算机通信和编程中常见的两种数据传输和处理方式,它们在处理任务和数据交换时有着不同的时间关系和特性。
同步传输(Synchronous Transmission)指在数据通信过程中,收发双方必须保持时间上的同步。在同步传输中,数据被组织成固定的数据块或字符,每个数据块或字符之间有明确的起始时间和结束时间。同步传输的主要特点是:
a. 传输发生在特定的时间;
b. 即使没有发送有效载荷数据,也能进行同步传输;
c. 在没有数据传输时,会发送空包。
异步传输(Asynchronous Transmission)指在数据通信过程中,收发双方不需要保持时间上的严格同步。异步传输通常用于通信速率不固定的场景,如电话通信、网络聊天等。在异步传输中,数据被组织成不规则的数据块或字符,每个数据块或字符之间的起止时间可以不固定。异步传输的主要特点是:
a. 数据传输不需要固定的时间间隔;
b. 可以在任何时间发送数据;
c. 接收方需要对数据进行解析,以确定数据块的开始和结束。
异步工作流有助于减少那些原本顺序执行的请求时间。它们可以通过提前进行一些耗时的工作来帮助减少请求时间,比如定期汇总数据。
实现方式
消息队列
消息队列接收,保留和传递消息。如果按顺序执行操作太慢的话,你可以使用有以下工作流的消息队列:
- 应用程序将作业发布到队列,然后通知用户作业状态
- 一个 worker 从队列中取出该作业,对其进行处理,然后显示该作业完成
不去阻塞用户操作,作业在后台处理。在此期间,客户端可能会进行一些处理使得看上去像是任务已经完成了。例如,如果要发送一条推文,推文可能会马上出现在你的时间线上,但是可能需要一些时间才能将你的推文推送到你的所有关注者那里去。
Redis 是一个令人满意的简单的消息代理,但是消息有可能会丢失。
RabbitMQ 很受欢迎但是要求你适应「AMQP」协议并且管理你自己的节点。
Amazon SQS 是被托管的,但可能具有高延迟,并且消息可能会被传送两次。
任务队列
任务队列接收任务及其相关数据,运行它们,然后传递其结果。 它们可以支持调度,并可用于在后台运行计算密集型作业。
Celery 支持调度,主要是用 Python 开发的。
背压(Back pressure)
如果队列开始明显增长,那么队列大小可能会超过内存大小,导致高速缓存未命中,磁盘读取,甚至性能更慢。背压可以通过限制队列大小来帮助我们,从而为队列中的作业保持高吞吐率和良好的响应时间。一旦队列填满,客户端将得到服务器忙或者 HTTP 503 状态码,以便稍后重试。客户端可以在稍后时间重试该请求,也许是指数退避。
异步的缺点:
- 简单的计算和实时工作流等用例可能更适用于同步操作,因为引入队列可能会增加延迟和复杂性。
通讯
资料来源:OSI 7层模型
超文本传输协议(HTTP)
HTTP 是一种在客户端和服务器之间编码和传输数据的方法。它是一个请求/响应协议:客户端和服务端针对相关内容和完成状态信息的请求和响应。HTTP 是独立的,允许请求和响应流经许多执行负载均衡,缓存,加密和压缩的中间路由器和服务器。
一个基本的 HTTP 请求由一个动词(方法)和一个资源(端点)组成。 以下是常见的 HTTP 动词:
动词 | 描述 | *幂等 | 安全性 | 可缓存 |
GET | 读取资源 | Yes | Yes | Yes |
POST | 创建资源或触发处理数据的进程 | No | No | Yes,如果回应包含刷新信息 |
PUT | 创建或替换资源 | Yes | No | No |
PATCH | 部分更新资源 | No | No | Yes,如果回应包含刷新信息 |
DELETE | 删除资源 | Yes | No | No |
多次执行不会产生不同的结果。
HTTP 是依赖于较低级协议(如 TCP 和 UDP)的应用层协议。
传输控制协议(TCP)
资料来源:如何制作多人游戏
TCP 是通过 IP 网络的面向连接的协议。 使用握手建立和断开连接。 发送的所有数据包保证以原始顺序到达目的地,用以下措施保证数据包不被损坏:
- 每个数据包的序列号和校验码
- 确认包
如果发送者没有收到正确的响应,它将重新发送数据包。如果多次超时,连接就会断开。TCP 实行流量控制和拥塞控制。这些确保措施会导致延迟,而且通常导致传输效率比 UDP 低。
为了确保高吞吐量,Web 服务器可以保持大量的 TCP 连接,从而导致高内存使用。在 Web 服务器线程间拥有大量开放连接可能开销巨大,消耗资源过多,也就是说,一个 memcached 服务器。连接池 可以帮助除了在适用的情况下切换到 UDP。
TCP 对于需要高可靠性但时间紧迫的应用程序很有用。比如包括 Web 服务器,数据库信息,SMTP,FTP 和 SSH。
以下情况使用 TCP 代替 UDP:
- 你需要数据完好无损。
- 你想对网络吞吐量自动进行最佳评估。
用户数据报协议(UDP)
资料来源:如何制作多人游戏
UDP 是无连接的。数据报(类似于数据包)只在数据报级别有保证。数据报可能会无序的到达目的地,也有可能会遗失。UDP 不支持拥塞控制。虽然不如 TCP 那样有保证,但 UDP 通常效率更高。
UDP 可以通过广播将数据报发送至子网内的所有设备。这对 DHCP 很有用,因为子网内的设备还没有分配 IP 地址,而 IP 对于 TCP 是必须的。
UDP 可靠性更低但适合用在网络电话、视频聊天,流媒体和实时多人游戏上。
以下情况使用 UDP 代替 TCP:
- 你需要低延迟
- 相对于数据丢失更糟的是数据延迟
- 你想实现自己的错误校正方法
远程过程调用协议(RPC)
Source: Crack the system design interview
在 RPC 中,客户端会去调用另一个地址空间(通常是一个远程服务器)里的方法。调用代码看起来就像是调用的是一个本地方法,客户端和服务器交互的具体过程被抽象。远程调用相对于本地调用一般较慢而且可靠性更差,因此区分两者是有帮助的。热门的 RPC 框架包括 Protobuf、Thrift 和 Avro。
RPC 是一个“请求-响应”协议:
- 客户端程序
- 客户端 stub 程序
- 客户端通信模块
- 服务端通信模块
- 服务端 stub 程序
RPC 调用示例:
GET /someoperation?data=anId POST /anotheroperation { "data":"anId"; "anotherdata": "another value" }
RPC 专注于暴露方法。RPC 通常用于处理内部通讯的性能问题,这样你可以手动处理本地调用以更好的适应你的情况。
当以下情况时选择本地库(也就是 SDK):
- 你知道你的目标平台。
- 你想控制如何访问你的“逻辑”。
- 你想对发生在你的库中的错误进行控制。
- 性能和终端用户体验是你最关心的事。
遵循 REST 的 HTTP API 往往更适用于公共 API。
缺点:RPC
- RPC 客户端与服务实现捆绑地很紧密。
- 一个新的 API 必须在每一个操作或者用例中定义。
- RPC 很难调试。
- 你可能没办法很方便的去修改现有的技术。举个例子,如果你希望在 Squid RPC 被正确缓存
表述性状态转移(REST)
REST 是一种强制的客户端/服务端架构设计模型,客户端基于服务端管理的一系列资源操作。服务端提供修改或获取资源的接口。所有的通信必须是无状态和可缓存的。
RESTful 接口有四条规则:
- 标志资源(HTTP 里的 URI)
- 表示的改变(HTTP 的动作)
- 可自我描述的错误信息(HTTP 中的 status code)
- HATEOAS(HTTP 中的HTML 接口)
REST 请求的例子:
GET /someresources/anId PUT /someresources/anId {"anotherdata": "another value"}
REST 关注于暴露数据。它减少了客户端/服务端的耦合程度,经常用于公共 HTTP API 接口设计。REST 使用更通常与规范化的方法来通过 URI 暴露资源,通过 header 来表述并通过 GET、POST、PUT、DELETE 和 PATCH 这些动作来进行操作。因为无状态的特性,REST 易于横向扩展和隔离。
缺点:REST
- 由于 REST 将重点放在暴露数据,所以当资源不是自然组织的或者结构复杂的时候它可能无法很好的适应。举个例子,返回过去一小时中与特定事件集匹配的更新记录这种操作就很难表示为路径。使用 REST,可能会使用 URI 路径,查询参数和可能的请求体来实现。
- REST 一般依赖几个动作(GET、POST、PUT、DELETE 和 PATCH),但有时候仅仅这些没法满足你的需要。举个例子,将过期的文档移动到归档文件夹里去,这样的操作可能没法简单的用上面这几个 verbs 表达。
- 为了渲染单个页面,获取被嵌套在层级结构中的复杂资源需要客户端,服务器之间多次往返通信。例如,获取博客内容及其关联评论。对于使用不确定网络环境的移动应用来说,这些多次往返通信是非常麻烦的。
- 随着时间的推移,更多的字段可能会被添加到 API 响应中,较旧的客户端将会接收到所有新的数据字段,即使是那些它们不需要的字段,结果它会增加负载大小并引起更大的延迟。
RPC 与 REST 比较
操作 | RPC | REST |
注册 | POST /signup | POST /persons |
注销 | POST /resign { "personid": "1234" } | DELETE /persons/1234 |
读取用户信息 | GET /readPerson?personid=1234 | GET /persons/1234 |
读取用户物品列表 | GET /readUsersItemsList?personid=1234 | GET /persons/1234/items |
向用户物品列表添加一项 | POST /addItemToUsersItemsList { "personid": "1234"; "itemid": "456" } | POST /persons/1234/items { "itemid": "456" } |
更新一个物品 | POST /modifyItem { "itemid": "456"; "key": "value" } | PUT /items/456 { "key": "value" } |
删除一个物品 | POST /removeItem { "itemid": "456" } | DELETE /items/456 |