系统设计实践(02)- 文本存储服务

前言

系统设计实践篇的文章将会根据《系统设计面试的万金油》为前置模板,讲解数十个常见系统的设计思路。

前置阅读:

设计目标

让我们设计一个类似于Pastebin的网站,用户可以在其中存储纯文本。该服务的用户将输入一段文本,并获得一个随机生成的URL来访问它。

一. 什么是Pastebin?

Pastebin是一个文本存储的网站,用户可以在网站上储存(粘贴)纯文本 ,例如代码片段,生成一个网址,打开该网址就可以看到对应的文字。可以选择文字的类型(代码所属的编程语言)、文字保存的时间(1天、7天、30天、阅后即焚等等)、文字分享者的昵称等信息。因为第一个文本分享网站叫 http://pastebin.com,所以文本存储网站也常被称为Pastebin。

二. 系统的需求与目标

Pastebin服务应满足以下要求:

功能性需求
  1. 用户应该能够上传或粘贴他们的文本数据,并获得访问它的唯一URL。
  2. 用户只能上传文本。
  3. 数据和链接地址将在特定时间间隔后自动过期; 用户可以指定过期时间。
  4. 用户可以为他们的文本内容选择一个自定义的别名。
非功能性需求
  1. 系统应该是高度可靠的,任何上传的数据都不应该丢失。
  2. 系统应该是高度可用的。这是必须的,因为如果我们的服务关闭,用户将无法访问他们的粘贴内容。
  3. 用户应该能够以最小的延迟实时访问他们的粘贴。
  4. 粘贴链接地址不应该是可猜测的(不可预测的)。
扩展需求
  1. 分析,例如,粘贴地址被访问多少次
  2. 我们的服务也应该可以通过REST API被其他服务访问。

三. 系统相似性

Pastebin与上一篇《系统设计实践(01) - 短链服务》有很多相似性的地方,所以我建议在开始阅读前再去读一读短链服务那篇文章,此外还有一些额外的设计注意事项。

用户一次可以粘贴的文本数量的限制是什么?

我们可以限制用户的粘贴不超过10MB,以防止滥用服务。

我们应该对自定义url施加大小限制吗?

由于我们的服务支持自定义URL,用户可以自定义他们喜欢URL,但提供自定义URL不是强制性的。然而,对自定义URL施加大小限制是合理的(通常也是可取的),这样我们就有了一致的URL数据库。

四. 容量估算与约束

与短链服务类似,我们的服务读请求会更多,与创建新的粘贴相比,将有更多的读取请求。我们可以假设读和写的比例是5:1。

流量估计

我们假设系统每天有100万新粘贴生成, 这样我们每天就有500万次读取。

每秒新粘贴

1M / (24 hours * 3600 seconds) ~= 12 pastes/sec

粘贴每秒读取:

5M / (24 hours * 3600 seconds) ~= 58 reads/sec

存储估计

用户最多可以上传10MB的数据; 通常,Pastebin之类的服务用于共享源代码、配置或日志。这样的文本并不大,所以我们假设每个粘贴平均包含10KB。

按照这个速度,我们每天将存储10GB的数据。

1M * 10KB => 10 GB/day

如果我们想将这些数据存储10年,我们需要36TB的总存储容量。

每天有 100 万个粘贴,我们将在 10 年内拥有 36 亿个粘贴。 我们需要生成并存储密钥以唯一标识这些粘贴。 如果我们使用 base64 编码([A-Z, a-z, 0-9, ., -]),我们将需要六个字母字符串:

64^6 ~= 68.7 billion unique strings

如果存储一个字符需要一个字节,那么存储3.6B键所需的总大小将是

3.6B * 6 => 22 GB

与36TB相比,22GB可以忽略不计。为了保持一定的余量,我们将采用70%容量模型(即任何时候都不希望使用超过70%的总存储容量),从而将存储容量增加到51.4TB。

带宽估计

对于写请求,我们预计每秒12个新粘贴,每秒会有120KB的输入。

12 * 10KB => 120 KB/s

至于读取请求,我们预计每秒有 58 个请求。 因此,总数据出口(发送给用户)将为 0.6 MB/s。

58 * 10KB => 0.6 MB/s

虽然总入口和出口不是很大,但我们在设计服务时应该记住这些数字

内存估计

我们可以缓存一些经常访问的热粘贴。遵循80-20规则,即20%的热点粘贴会产生80%的流量,我们希望缓存这20%的粘贴,因为我们每天有5M的读请求,要缓存这些请求的20%,我们需要

0.2 * 5M * 10KB ~= 10 GB

五. 系统API设计

我们可以使用 SOAP 或 REST API 来公开我们服务的功能。 以下可能是用于创建/检索/删除粘贴的 API 的定义:

addPaste(api_dev_key, paste_data, custom_url=None, user_name=None, paste_name=None, expire_date=None)

参数
  • api_dev_key (string): 注册帐户的API开发者密钥.
  • paste_data (string): 粘贴的文本内容.
  • custom_url (string): 可选的用户指定url.
  • user_name (string): 可选的用户吗,用于生成URL.
  • paste_name (string): 可选的粘贴名称.
  • expire_date (string): 可选的过期时间.
返回

成功将返回可以访问粘贴的URL,否则将返回错误代码。

getPaste(api_dev_key, api_paste_key)

其中api粘贴键是一个字符串,表示要检索的粘贴键。这个API将返回粘贴的文本数据。

deletePaste(api_dev_key, api_paste_key)

成功删除返回true,否则返回false。

六. 数据库设计

关于我们正在存储的数据的性质的一些观察

  • 我们需要存储数十亿条记录。
  • 我们存储的每个元数据对象都很小(小于100字节)
  • 我们存储的每个粘贴对象可以是中等大小(可以是几MB)。
  • 记录之间没有关系,除非我们想要存储哪个用户创建了什么粘贴。
  • 我们的服务读请求很多

数据库选型

我们需要两张表,一个用于存储关于paste的信息,另一个用于存储用户数据。

Paste User
[PK] URL Hash: varchar(16) [PK] UserID: int
ContentKey: varchar(512) Name: varchar(20)
CreationDate: datetime Email: varchar(20)
ExpirationDate: datatime CreationDate: datetime
LastLoginDate: datetime

在这里,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一样将内容存储在对象存储中。当我们想要在内容存储上达到最大容量时,我们可以通过添加更多服务器来轻松增加容量。

posted @ 2021-09-14 10:44  AntzUhl  阅读(424)  评论(0编辑  收藏  举报