现在让我们设计一个像TinyURL一样的URL缩短服务。这个服务将提供短别名来重定向到长URL。
系统难度级别:初级
URL缩短用于为长URL创建更短的别名。我们将这些缩短后的别名称为“短链接”。当用户点击这些短链接时,他们会被重定向到原始URL。短链接在显示、打印、发送信息或发推文时节省了大量空间。此外,用户输入错误的可能性也会随着URL的缩短而减少。
例如,如果我们通过TinyURL缩短以下URL:
你应该在面试开始时就明确需求。务必提问,以找出面试官心中系统的准确范围。
我们的URL缩短系统应满足以下要求:
功能性需求:
非功能性需求:
扩展需求:
我们的系统将是读取密集型的。与新的URL缩短相比,将会有大量的重定向请求。我们假设读取和写入的比例是100:1。
流量估计:假设我们每月将有5亿次新的URL缩短,以100:1的读/写比率,我们可以预期在同一时期内有500亿次重定向:
我们系统的每秒查询数(QPS)是多少?每秒新的URL缩短数量:
考虑到100:1的读/写比率,每秒的URL重定向数量将是:
存储估计:我们假设存储每个URL缩短请求(及关联的缩短链接)5年。由于我们预计每月会有5亿个新的URL,因此我们预计存储的对象总数将是300亿个:
我们假设每个存储的对象大约为500字节(这只是一个大致的估计——我们稍后会深入研究)。我们需要15TB的总存储:
带宽估计:对于写请求,由于我们预计每秒会有200个新的URL,我们的服务总入站数据将是每秒100KB:
对于读请求,由于我们预计每秒将有大约20K个URL重定向,我们的服务总出站数据将是每秒10MB:
由于我们每秒有20K个请求,我们每天将获得17亿个请求:
为了缓存这20%的请求,我们需要170GB的内存。
这里需要注意的一点是,由于会有许多重复请求(相同的URL),我们实际的内存使用量将少于170GB。
高层次估计:假设每月有5亿个新的URL,并且读写比率为100:1,以下是我们服务的高层次估计的概述:
一旦我们确定了需求,接下来需要定义系统API。需要明确说明系统的预期功能。
我们可以使用SOAP或RESTAPI来公开我们服务的功能。创建和删除URL的API定义可能如下:
createURL(api_dev_key,original_url,custom_alias=None,user_name=None,expire_date=None)参数:
返回:(字符串)
成功插入返回缩短后的URL;否则,返回一个错误码。
deleteURL(api_dev_key,url_key)其中url_key是表示要检索的缩短URL的字符串;成功删除后返回URLRemoved。
在早期系统设计阶段定义DB模式对理解数据在各个组件之间的流动非常有帮助,随后会引导我们进行数据分区。
我们将存储的数据具有以下特点:
数据库架构:
我们需要两个表:一个用于存储URL映射信息,另一个用于存储创建短链接的用户的数据。
我们应该使用什么类型的数据库?由于我们预计将存储数十亿行,而且我们不需要使用对象之间的关系—一个NoSQL存储,如DynamoDB,Cassandra或Riak会是一个更好的选择。一个NoSQL选择也将更容易扩展。
我们这里要解决的问题是如何为给定的URL生成一个短而唯一的键。
A.编码实际的URL
我们可以计算给定URL的唯一哈希值(例如D5或SHA256等)。然后,哈希可以进行编码以便显示。这种编码可以是Base36([a-z,0-9])或Base62([A-Z,a-z,0-9]),如果我们添加'+'和'/',我们可以使用Base64编码。一个合理的问题是,短键的长度应该是多少?6、8还是10个字符?
假设对于我们的系统来说,有687亿个唯一字符串的六个字符键就足够了。
如果我们使用MD5算法作为我们的哈希函数,它将产生一个128位的哈希值。经过Base64编码后,我们会得到一个超过21个字符的字符串(因为每个Base64字符编码了哈希值的6位)。现在我们每个短键只有6(或8)个字符的空间;那么我们怎么选择我们的键呢?我们可以取前6(或8)个字符作为键。这可能会导致键重复;为了解决这个问题,我们可以从编码字符串中选择一些其他字符或交换一些字符。
我们的解决方案有哪些问题?我们的编码方案有以下几个问题:
解决问题的方法:我们可以在每个输入URL后附加一个递增的序列号使其唯一,然后生成哈希。然而,我们不需要在数据库中存储这个序列号。这种方法可能的问题可能是一个不断增加的序列号。它会溢出吗?附加一个递增的序列号也将影响服务的性能。
B.离线生成键
我们可以设立一个独立的键生成服务(KGS),预先生成随机的六位字符串,并将其存储在数据库中(我们称之为key-DB)。每当我们想要缩短一个URL,我们就会拿一个已经生成的键来使用。这种方式非常简单和快速。我们不仅不需要编码URL,而且不必担心重复或冲突。KGS会确保插入key-DB的所有键都是唯一的。
并发会引起问题吗?一旦一个键被使用,就应该在数据库中标记,以确保它不再被使用。如果有多个服务器同时读取键,我们可能会遇到两个或更多的服务器试图从数据库中读取同一个键的情况。我们如何解决这个并发问题?
服务器可以使用KGS在数据库中读取/标记键。KGS可以使用两个表来存储键:一个用于尚未使用的键,另一个用于所有已使用的键。一旦KGS将键提供给其中一个服务器,就可以将它们移动到已使用的键表中。KGS可以始终在内存中保留一些键,以便在服务器需要时快速提供。
为了简单起见,一旦KGS在内存中加载了一些键,就可以将它们移动到已使用的键表中。这确保每个服务器得到的键都是唯一的。如果KGS在将所有加载的键分配给某个服务器之前死机,我们将会浪费这些键——鉴于我们拥有的键的数量庞大,这可能是可以接受的。
KGS还必须确保不给多个服务器相同的键。为此,它必须在从中移除键并将它们提供给服务器之前,对保存键的数据结构进行同步(或对其加锁)。
key-DB的大小会是多少?**使用base64编码,我们可以生成687亿个唯一的六位键。如果我们需要一个字节来存储一个字母数字字符,我们可以将所有这些键存储在:
KGS会出现单点故障吗?会的。为了解决这个问题,我们可以设置一个备用的KGS副本。当主服务器出现故障时,备用服务器可以接管,生成并提供键。
每个应用服务器可以从key-DB中缓存一些键吗?是的,这肯定可以加快速度。然而,在这种情况下,如果应用服务器在消耗完所有键之前出现故障,我们将最终失去这些键。鉴于我们有687亿个唯一的六位键,这是可以接受的。
我们如何执行键查找?我们可以在数据库中查找键以获取完整的URL。如果它存在于数据库中,就向浏览器返回HTTP302Redirect状态,并在请求的Location字段中传递存储的URL。如果我们的系统中没有该键,则返回HTTP404NotFound状态或将用户重定向回主页。
我们应该对自定义别名设定大小限制吗?我们的服务支持自定义别名。用户可以自由选择他们喜欢的key,但提供自定义别名并不是必须的。然而,为了确保我们有一个一致的URL数据库,设定一个自定义别名的大小限制是合理的(甚至是用户期望的)。我们假设用户可以为每个自定义键指定最多16个字符(如上述数据库模式所示)。
为了扩展我们的数据库,我们需要对其进行分区,以便它能存储数十亿个URL的信息。因此,我们需要设计一个分区方案,将我们的数据划分并存储到不同的数据库中。
A.基于范围的分区:我们可以根据哈希键的第一个字母在不同的分区中存储URL。因此,我们将所有以字母“A”(和“a”)开头的URL哈希键存储在一个分区中,将以字母“B”开头的URL存储在另一个分区中,以此类推。这种方法被称为基于范围的分区。我们甚至可以将某些出现频率较低的字母组合到一个数据库分区中。因此,我们应开发一个静态分区方案,始终以可预测的方式存储/查找URL。
这种方法的主要问题是可能导致数据库服务器不平衡。例如,我们决定将所有以字母“E”开头的URL放入一个数据库分区,但后来我们发现以字母“E”开头的URL过多。
B.基于哈希的分区:在这种方案中,我们对存储的对象进行哈希。然后,我们根据哈希值计算使用哪个分区。在我们的情况下,我们可以获取key或短链接的哈希值,以确定我们存储数据对象的分区。
我们的哈希函数将随机地将URL分配到不同的分区(例如,我们的哈希函数可以始终将任何key映射到[1...256]之间的一个数字)。这个数字将表示我们存储对象的分区。
这种方法仍然可能导致分区负载过大,这可以通过使用'一致性哈希'来解决。
我们可以缓存频繁访问的URL。我们可以使用任何现成的解决方案,比如Memcached,它可以存储完整的URL及其对应的哈希值。因此,应用服务器在访问后端存储之前,可以快速检查缓存是否有所需的URL。
为了进一步提高效率,我们可以复制我们的缓存服务器来分配它们之间的负载。
每个缓存副本如何更新?每当有一个缓存未命中,我们的服务器会访问后端数据库。当这种情况发生,我们可以更新缓存,并将新的条目传递给所有的缓存副本。每个副本都可以通过添加新的条目来更新其缓存。如果副本已经有了该条目,它可以简单地忽略它。
我们可以在系统中的三个位置添加负载均衡层:
最初,我们可以使用一个简单的轮询方法,将进入的请求均等地分配到后端服务器。这种负载均衡方法简单易行,不会引入任何额外的开销。这种方法的另一个优点是,如果一个服务器死机,负载均衡器会将其从轮询中移除,停止向其发送任何流量。
轮询负载均衡的一个问题是,我们没有考虑到服务器的负载。如果一个服务器过载或运行缓慢,负载均衡器不会停止向该服务器发送新的请求。为了处理这个问题,我们可以放置一个更优的负载均衡解决方案,它定期查询后端服务器的负载,并根据负载情况调整流量。
我们如何统计短链接被使用的次数、用户的位置等信息?我们如何储存这些统计信息?如果它是数据库中的一部分,每次查看都需要更新,那么当一个流行的短链接被大量并发请求瞬间涌入时,会发生什么?
用户能否创建私有URL或者允许特定的用户组访问某个URL?
我们可以在数据库中每个URL的条目里存储访问权限级别(公开/私有)。我们也可以创建一个单独的表来存储有权访问特定URL的用户的UserID。如果一个用户没有权限但试图访问一个URL,我们可以返回一个错误(HTTP401)。考虑到我们的数据储存在一个类似Cassandra的NoSQL宽列数据库中,储存权限的表的key将是‘哈希值’(或KGS生成的key)。列将储存有权查看URL的用户的UserID。