大厂面试官让我设计一个短链接服务[系统设计面试]

2,465 阅读5分钟

更多分布式系统设计面试资料

github.com/xiajunhust/…

什么是短链接服务

短链接服务的目的是将普通url转换为比较短的url,当用户点击短链接的时候,会跳转到原始链接。短链接会带来几个好处:节省展示的空间、不容易出错、隐藏原始链接。常见的工具如:tinyurl,大家可以打开页面试用体验,帮助理解短链接服务的作用。

示例如下:

原始链接:

www.educative.io/collection/…

短链接:

tinyurl.com/jlg8zpc

系统功能需求分析

短链接服务需要满足如下需求:

  • 功能性需求
    • 指定一个url能够生成一个短链接
    • 当用户点击短链接的时候会跳转到原始链接
    • 用户可以定义短链接格式
    • 用户可以设置链接的过期时间
  • 非功能性需求
    • 系统必须保证高可用,否则如果服务挂了,所有的链接跳转将会失败
    • 链接跳转延迟须尽可能低
    • 短链接内容不能被预测到
  • 扩展性需求
    • 服务可以通过REST API的方式被其他服务访问

系统容量评估

流量评估

假设每个月产生新的短链接是500M,读写流量比例是100:1.

100*500M=>50B

短链接生成请求QPS:

500Million /(30243600)=~200 URLs/S

那么读,也即跳转请求QPS:

100*200=20K/S

存储空间

假设短链接会保存5年,那么总共需要保存的链接数:

500milion5years12months=30billion

估算每行数据所占存储大小是500bytes,那么需要的空间大小是:

30billion*500bytes=15TB。

网络带宽

写:

200*500bytes=100KB/S

读:

20K*500bytes=~10MB/S

内存

我们将频繁访问的热点url缓存在内存中,根据28原则,我们需要缓存20%的url。

那么每天的读请求量级是:

20K3600S24hours=~1.7billion

缓存20%:

0.21.7billion500bytes=~170GB

系统API设计

//创建短链接
createURL(api_dev_key, original_url, custom_alias=None, user_name=None,
expire_date=None)
    
//删除短链接
deleteURL(api_dev_key, url_key)
    

数据库设计

在面试整个过程的早期进行数据库设计,能够帮助理解数据在不同组件之间的流向,以及为后续的数据分片提供必要的指引。

数据存储特性分析

关于数据存储,我们分析出具备如下特性:

  • 需要存储的记录条数量级非常大,可能达到十亿级别
  • 每条记录的大小比较小(<1K)
  • 数据记录之间无关联关系
  • 提供的服务读多写少

数据表的schema设计

我们需要创建2张表,分别存储URL的映射关系以及创建短连接的用户信息。

表-Url

字段名字段类型含义
hashvarchar短连接
original_urlvarchar原始url
create_datetimestamp创建时间
expiration_datatimestamp过期时间

表-User

字段名字段类型含义
user_idvarchar用户id
namevarchar用户名
emailvarchar用户邮箱
create_datetimestamp创建时间
expiration_datetimestamp过期时间
last_login_datetimestamp上次登录时间

数据库技术选型

由于我们需要存储的数据量级很大,而且我们不需要去关心记录之间的关系,因此我们可以采用nosql(如dynamodb、Cassandra、riak等),它比关系型数据库拥有更好的伸缩性。

系统概要设计

如何生成短连接?有2种可行的方案。

编码

我们可以通过编码计算的方式(MD5或SHA256)生成一个唯一hash串来组成短连接。如下面的短连接末尾的jlg8zpc是生成的hash字符串。

tinyurl.com/jlg8zpc

\

编码的方式存在如下问题:

\

对于短连接相同的情况,我们可以考虑在url后增加一个自增的唯一序列号来解决这个问题。而且我们无须存储这个序列号。但是带来一个新的问题是这个自增序列号是否会溢出。另一个问题是会带来性能上的损耗。

另一种方案是我们可以将用户userid拼接在url后面,但是如果用户没有登录,那么就拿不到userid。

离线生成key的方式

我们可以部署一个独立的key生成服务(key generation service),提前生成6位(假设我们要生成的短连接中hash长度是6位)并存储于db中。每当我们需要生成短连接的时候直接从db中取提前生成好的key即可。这种方案的好处是非常简单而且性能好。我们不仅不需要进行编码计算,也不需要担心重复的问题,KGS保证每个key是唯一的。

并发问题考虑

每一个key只能被使用一次,那么当一个key被取出使用之后需要进行打标。那么如果有多台服务器同时对外提供短连接生成服务,那么存在并发问题。

那么如何解决并发问题呢?KGS可以使用2张不同的表来分别存储未使用的key和已使用的key。每当KGS将一个key提供给一台短连接服务器的时候,就将其移动到已使用key存储表值。KGS在内存维持一个本地缓存,这样可以快速返回key给短连接服务器。

\

KGS是否存在单点失败问题?

是的,我们可以部署一主一备的方式,当主宕机后切换至备。

下图是短连接生成服务的整体概要设计图:

\

数据分片和副本

由于我们需要支撑十亿级别的数据量,我们必须对数据进行分片存储,需要定义对数据进行拆分和存储的schema。

  • 按字符串内容的范围进行拆分。比如按照首字母进行拆分,A的在一个分片,B的在另一个分片,以此类推。或者可以将多个字符存储在同一个分片来减少区数。
    • 这个方案最大的问题是可能导致数据倾斜。
  • 基于hash的拆分。通过一个hash函数计算url得到一个hash值,比如得到1-256之间的数值,这个数值表示url应该落在哪个分片。推荐采用一致性hash的方式。

缓存

由于需要支持的读流量可能非常大,直接打到DB会对DB造成很大的压力。我们可以考虑使用缓存如memcache。

缓存大小评估

前面我们估计过,如果需要存放每天全量流量的20%需要170GB,对于现在的服务器来说内存大小是足够的。

缓存淘汰策略

比较合适的是使用LRU(Least recently used)策略,淘汰最近最少使用的url。我们可以使用类似Linked Hash Map来跟踪url的使用情况。

通常在海量流量的情况,为了性能和可用性考虑,缓存服务器需要设置多个副本。那么怎么更新缓存服务器?在某条数据缓存miss的时候,流量会打到后端DB,这个时候可以把DB返回的数据更新至所有的缓存服务器。

负载均衡

在我们的短连接服务系统中,有3个地方需要用到负载均衡:

  1. 客户端和应用服务器之间
  2. 应用服务器和缓存服务器之间
  3. 缓存服务器和数据库之间

常见的负载均衡算法有如下几种:

  1. 轮询法。将请求顺序轮流分配到后端服务器。好处是实现简单,不会给系统额外带来负荷,另外是一台服务器宕机后会自动停止发送流量至该服务器。缺点是没有考虑每台服务器的负载情况。
  2. 随机法。随机将流量发送到一台服务器。
  3. 源地址hash法。获取客户端的ip地址进行hash计算,用其数值对服务器数目进行取模运算,得到的结果即要发送到的服务器的序列号。
  4. 加权轮询法。不同的服务器的配置和系统负载可能不同,因此可以给配置高负载低的机器配置更高的权重。将请求顺序按照权重分配到后端。
  5. 加权随机法。和加权轮询法类似,不同的是按照权重随机请求后端服务器而不是顺序。
  6. 最小连接法。根据后端服务器的连接请情况动态选择积压连接数最少的一台服务器。

数据库过期数据清除

过期的数据需要进行清除处理,可以降低数据库的存储空间,降低成本,提高查询性能。不过需要保证清除工作不会影响正常的短连接生成服务。可行的方案:

  • 当用户请求到了一个过期的链接的时候,删除过期链接,并返回给用户错误码
  • 单独设置一个过期数据清除服务,这个服务必须保证很轻量,可以在用户流量低峰期定时执行
  • 若用户未设置过期时间,系统可以设置一个默认的过期时间,确保数据不会永久存在,导致DB无限增长

完整的短连接服务系统

完整的系统设计方案: