前言
系统设计实践篇的文章将会根据《系统设计面试的万金油》为前置模板,讲解数十个常见系统的设计思路。
前置阅读:
设计目标
让我们设计一个类似于Pastebin的网站,用户可以在其中存储纯文本。该服务的用户将输入一段文本,并获得一个随机生成的URL来访问它。
一. 什么是Pastebin?
Pastebin是一个文本存储的网站,用户可以在网站上储存(粘贴)纯文本 ,例如代码片段,生成一个网址,打开该网址就可以看到对应的文字。可以选择文字的类型(代码所属的编程语言)、文字保存的时间(1天、7天、30天、阅后即焚等等)、文字分享者的昵称等信息。因为第一个文本分享网站叫 http://pastebin.com,所以文本存储网站也常被称为Pastebin。
二. 系统的需求与目标
Pastebin服务应满足以下要求:
功能性需求
- 用户应该能够上传或粘贴他们的文本数据,并获得访问它的唯一URL。
- 用户只能上传文本。
- 数据和链接地址将在特定时间间隔后自动过期; 用户可以指定过期时间。
- 用户可以为他们的文本内容选择一个自定义的别名。
非功能性需求
- 系统应该是高度可靠的,任何上传的数据都不应该丢失。
- 系统应该是高度可用的。这是必须的,因为如果我们的服务关闭,用户将无法访问他们的粘贴内容。
- 用户应该能够以最小的延迟实时访问他们的粘贴。
- 粘贴链接地址不应该是可猜测的(不可预测的)。
扩展需求
- 分析,例如,粘贴地址被访问多少次
- 我们的服务也应该可以通过REST API被其他服务访问。
三. 系统相似性
Pastebin与上一篇《系统设计实践(01) - 短链服务》有很多相似性的地方,所以我建议在开始阅读前再去读一读短链服务那篇文章,此外还有一些额外的设计注意事项。
用户一次可以粘贴的文本数量的限制是什么?
我们可以限制用户的粘贴不超过10MB,以防止滥用服务。
我们应该对自定义url施加大小限制吗?
由于我们的服务支持自定义URL,用户可以自定义他们喜欢URL,但提供自定义URL不是强制性的。然而,对自定义URL施加大小限制是合理的(通常也是可取的),这样我们就有了一致的URL数据库。
四. 容量估算与约束
与短链服务类似,我们的服务读请求会更多,与创建新的粘贴相比,将有更多的读取请求。我们可以假设读和写的比例是5:1。
流量估计
我们假设系统每天有100万新粘贴生成, 这样我们每天就有500万次读取。
每秒新粘贴
粘贴每秒读取:
存储估计
用户最多可以上传10MB的数据; 通常,Pastebin之类的服务用于共享源代码、配置或日志。这样的文本并不大,所以我们假设每个粘贴平均包含10KB。
按照这个速度,我们每天将存储10GB的数据。
如果我们想将这些数据存储10年,我们需要36TB的总存储容量。
每天有 100 万个粘贴,我们将在 10 年内拥有 36 亿个粘贴。 我们需要生成并存储密钥以唯一标识这些粘贴。 如果我们使用 base64 编码([A-Z, a-z, 0-9, ., -]),我们将需要六个字母字符串:
如果存储一个字符需要一个字节,那么存储3.6B键所需的总大小将是
与36TB相比,22GB可以忽略不计。为了保持一定的余量,我们将采用70%容量模型(即任何时候都不希望使用超过70%的总存储容量),从而将存储容量增加到51.4TB。
带宽估计
对于写请求,我们预计每秒12个新粘贴,每秒会有120KB的输入。
至于读取请求,我们预计每秒有 58 个请求。 因此,总数据出口(发送给用户)将为 0.6 MB/s。
虽然总入口和出口不是很大,但我们在设计服务时应该记住这些数字
内存估计
我们可以缓存一些经常访问的热粘贴。遵循80-20规则,即20%的热点粘贴会产生80%的流量,我们希望缓存这20%的粘贴,因为我们每天有5M的读请求,要缓存这些请求的20%,我们需要
0.2 * 5M * 10KB ~= 10 GB
五. 系统API设计
我们可以使用 SOAP 或 REST API 来公开我们服务的功能。 以下可能是用于创建/检索/删除粘贴的 API 的定义:
参数
- api_dev_key (string): 注册帐户的API开发者密钥.
- paste_data (string): 粘贴的文本内容.
- custom_url (string): 可选的用户指定url.
- user_name (string): 可选的用户吗,用于生成URL.
- paste_name (string): 可选的粘贴名称.
- expire_date (string): 可选的过期时间.
返回
成功将返回可以访问粘贴的URL,否则将返回错误代码。
其中api粘贴键是一个字符串,表示要检索的粘贴键。这个API将返回粘贴的文本数据。
成功删除返回true,否则返回false。
六. 数据库设计
关于我们正在存储的数据的性质的一些观察
- 我们需要存储数十亿条记录。
- 我们存储的每个元数据对象都很小(小于100字节)
- 我们存储的每个粘贴对象可以是中等大小(可以是几MB)。
- 记录之间没有关系,除非我们想要存储哪个用户创建了什么粘贴。
- 我们的服务读请求很多
数据库选型
我们需要两张表,一个用于存储关于paste的信息,另一个用于存储用户数据。
在这里,URl Hash是TinyURL的URL等价物,ContentKey是存储粘贴内容的对象键。
七. 高级设计
在更高的层次上,我们需要一个应用程序层来服务于所有的读写请求。应用层将与存储层通信以存储和检索数据。我们可以隔离存储层,一个数据库存储与每个粘贴、用户等相关的元数据,而另一个数据库将粘贴内容存储在某些对象存储中(如Amazon S3)。这种数据划分也将允许我们对它们进行单独的缩放。
八. 组件设计
应用层
我们的应用层将处理所有传入和传出的请求。应用服务器将与后端数据存储组件通信来处理请求。
如何处理写请求?
在接收到写请求时,我们的应用服务器将生成一个6个字母的随机字符串,它将作为粘贴的密钥(如果用户没有提供自定义密钥)。然后,应用程序服务器将在数据库中存储粘贴的内容和生成的键。成功插入后,服务器可以将密钥返回给用户。这里的一个可能问题是,由于密钥重复,插入失败。因为我们生成了一个随机密钥,所以新生成的密钥有可能与现有密钥匹配。在这种情况下,我们应该重新生成一个新的密钥并再试一次,直到没有发现因为重复密钥。如果用户提供的自定义键已经存在于数据库中,则应该向用户返回一个错误。
上述问题的另一个解决方案是运行一个独立的密钥生成服务(KGS),它事先生成随机的6个字母字符串,并将它们存储在一个数据库中(我们称之为Key-db)。每当我们想要存储一个新的粘贴时,我们只需要一个已经生成的键并使用它。这种方法将使事情变得非常简单和快速,因为我们不需要担心重复或碰撞。KGS将确保插入到key-DB中的所有键是唯一的。KGS可以使用两个表来存储键,一个用于尚未使用的键,另一个用于所有已使用的键。一旦KGS向应用服务器提供了一些键,它就可以将这些键移动到所使用的键表中。KGS可以在内存中保存一些密钥,以便每当服务器需要它们时,它可以快速提供它们。一旦KGS在内存中加载了一些键,它就可以将它们移动到已使用的键表中,这样我们就可以确保每个服务器获得唯一的键。如果KGS在使用内存中加载的所有键之前宕机,这些键会被浪费,不过可以忽略,因为KGS中6个字母可生成的字符串足够多。
KGS不是单点故障吗?
是的。为了解决这个问题,我们可以有一个KGS的备用副本,每当主服务器死亡时,它可以接管生成并提供密钥。
每个应用服务器是否可以从key-DB中缓存一些key?
是的,这肯定能加快响应速度。尽管在这种情况下,如果应用服务器在使用所有密钥之前就挂掉了,我们最终会丢失这些密钥。这是可以接受的,因为我们有68B唯一的6个字母的钥匙,这比我们需要的多得多。
它如何处理粘贴读请求?
在接收到读粘贴请求后,应用程序服务层请求数据存储。数据存储搜索密钥,如果找到,返回粘贴的内容。否则,返回错误代码。
数据层
我们可以讲数据存储划为两层。
- 元数据数据库:我们可以使用关系数据库如MySQL或分布式键值存储如Dynamo或Cassandra。
- 对象存储:可以像Amazon S3一样将内容存储在对象存储中。当我们想要在内容存储上达到最大容量时,我们可以通过添加更多服务器来轻松增加容量。