braden-collum-75XHJzEIeUc-unsplash

经常在面试的时候,会被问到系统设计类的题目,比如如何设计微信朋友圈、如何设计12306系统、如何设计一个抢票系统等等。如果是没有准备过,一般都会不知所措,难以找到切入点。今天这里码老思会介绍一个解决系统设计类问题的通用框架,无论什么问题,朝这几步走,一定能找到解决办法。

系统设计在考察什么?

系统设计面试中,经常会被问到如何设计微信、如何设计微博、如何设计百度……我们怎么能在如此短的时间内设计出来一个由成千上万的码农、PM,经年累月地迭代出来的如此优秀的产品?如果面试者这么优秀,那还面试啥?百度、谷歌也不可能只是一个搜索框而已,底下的东西复杂去了。

所以首先要明确,系统问题的答案一定不可能是全面的,面试官也不会期望我们给出一个满分答案。所谓的系统设计面试实际上是在模拟一个场景:两名同事在一起就一个模糊的问题,讨论一番,得出一个还不错的解决方案。

因此在这个过程中,我们需要与面试官进行充分的沟通,了解清楚需求,回应面试管的各种问题,阐述我们设计的方案。最终设计的结果是开放性的,没有标准答案;而面试官会在这个过程中,会充分挖掘我们的沟通、分析、解决问题的能力,最后给出通过与否的结论。

常见误区

没有方向,只提出各种关键词

在面试中,常见的错误是面试官给出问题后,候选人就开始怼各种关键词,什么Load Balancer,Memcache,NodeJS,MongoDB,MySQL.

不是说我们不能说这些技术词汇,但是系统设计首先我们需要搞清楚具体的需求,然后在大概确定系统架构,最后才根据场景进行技术选型,因为并不存在一个完全完美的系统设计,往往都是各方面之间的均衡,至于如何均衡,需要结合面试官的要求和具体实际来选择。

一开始直奔最优解

系统设计考察我们如何去解决一个具体的问题,在实际工作中,我们也是先实现功能之后,再在此基础上去进行针对性的优化;在面试中同样也十分重要,一方面,面试官希望看到你从一开始设计系统,到慢慢完善的过程,另外,系统设计其实没有最优解,往往都是各种因素之间权衡后的结果,就像CAP理论一样,无法同时满足,我们的系统只要能满足面试官提出来的要求,刚好够用就行。

闷头设计不沟通

很多人在听到问题之后,就开始闷头设计,丝毫不会和面试官进行沟通交流。这样不仅不利于自己理解题目意图,而且让面试官无法了解你是如何一步步去解决问题的过程。

这个时候可以把面试官当做一位同事,比如你对题目不理解,可以提出问题搞清楚题目意图;又比如你在哪个环节卡住了,也不要一直闷在那,可以大胆向面试官求助寻求提示,也能节省不少时间。

就像开始说的,系统设计没有最优解,你的思路和解决问题的过程很重要,而这些就是通过不断的沟通来传递给面试官的。

如何搞定系统设计 - 4步法

针对系统设计,这里给大家提供一个4步法解决方案,无论是任何系统设计的题目,都可以按照这几步去思考解决。

1. 确定问题需求

这一步主要是确定问题的需求和系统使用场景,为了搞清楚这个问题,需要和面试官你来我往地问问题。

在做设计的同时,问面试官的要求是什么,做出合理的假设、取舍,让面试官看出你的思考过程,最终综合所有的信息完成一个还不错的设计。不要害怕问问题。那并不会说明我们不懂,而是让面试官理解我们的思考过程。当我们问出问题后,面试官要么给出明确的回答;要么让我们自己做出假设。当需要做出假设时,我们需要把假设写下来备用。

这个举个例子,比如面试官让你设计weibo,因为weibo的功能较为庞大,例如发微博、微博时间线、关注、取消关注、微博热搜榜等等,我们无法在短短的面试时间内完成这么多功能设计,所以这时候可以询问面试官我们需要实现哪些功能。

比如需要实现微博时间线的功能,我们得进一步确认,整体用户量多大,系统的QPS多大,因为这涉及到我们后续的系统设计,而且如果对于QPS特别高的情况,在后续的设计中需要针对此进行专门的扩展性优化。

QPS有什么用?

  • QPS = 100,那么用你的笔记本作Web服务器就好了;
  • QPS = 1K,一台好点的Web 服务器也能应付,需要考虑Single Point Failure;
  • QPS = 1m,则需要建设一个1000台Web服务器的集群,并且要考虑如何Maintainance(某一台挂了怎么办)。

QPS 和 服务器/数据库之间的关系:

  • 一台Web Server承受量约为 1K的QPS(考虑到逻辑处理时间以及数据库查询的瓶颈);
  • 一台SQL Database承受量约为 1K的QPS(如果JOIN和INDEX query比较多的话,这个值会更小);
  • 一台 NoSQL Database (Cassandra) 约承受量是 10k 的 QPS;
  • 一台 NoSQL Database (Memcached) 约承受量是 1M 的 QPS。

以下是一些通用的问题,可以通过询问系统相关的问题,搞清楚面试官的意图和系统的使用场景。

  • 系统的功能是什么
  • 系统的目标群体是什么
  • 系统的用户量有多大
  • 希望每秒钟处理多少请求?
  • 希望处理多少数据?
  • 希望的读写比率?
  • 系统的扩张规模怎么样,这涉及到后续的扩展

总结,在这一步,不要设计具体的系统,把问题搞清楚就行。不要害怕问问题,那并不会说明我们不懂,而是让面试官理解我们的思考过程。

2. 完成整体设计

这一步根据系统的具体要求,我们将其划分为几个功能独立的模块,然后做出一张整体的架构图,并依据此架构图,看是否能实现上一步所提出来的需求。

如果可能的话,设想几个具体的例子,对着图演练一遍。这让我们能更坚定当前的设计,有时候还能发现一些未考虑到的边界case。

这里说的比较抽象,具体可以参考下面的实战环节,来理解如何完成整体设计。

3. 深入模块设计

至此,我们已经完成了系统的整体设计,接下来需要根据面试官的要求对模块进行具体设计。

比如需要设计一个短网址的系统,上一步中可能已经把系统分为了如下三个模块:

  • 生成完整网址的hash值,并进行存储。
  • 短网址转换为完整url。
  • 短网址转换的API。

这一步中我们需要对上面三个模块进行具体设计,这里面就涉及到实际的技术选型了。下面举个简单的例子。

比如说生成网址的hash值,假设别名是http://tinyurl.com/<alias_hash>,alias_hash是固定长度的字符串。

如果长度为 7,包含[A-Z, a-z, 0-9],则可以提供62 ^ 7 ~= 3500 亿个 URL。至于3500亿的网址数目是否能满足要求,目前世界上总共有2亿多个网站,平均每个网站1000个网址来计算,也是够用的。而且后续可以引入网址过期的策略。

首先,我们将所有的映射存储在一个数据库中。 一个简单的方法是使用alias_hash作为每个映射的 ID,可以生成一个长度为 7 的随机字符串。

所以我们可以先存储<ID,URL>。 当用户输入一个长 URL
http://www.lfeng.tech时
,系统创建一个随机的 7 个字符的字符串,如abcd123作为 ID,并插入条目<"abcd123", "
http://www.lfeng.tech
">进入数据库。

在运行期间,当有人访问http://tinyurl.com/abcd123时,我们通过 ID abcd123查找并重定向到相应的 URL
http://www.lfeng.tech

当然,上面的例子中只解决了生成网址的问题,还有网址的存储、网址生成hash值之后产生碰撞如何解决等等,都需要在这个阶段解决。这里面涉及到各种存储方案的选择、数据库的设计等等,后面会有专门的文章进行介绍。

4. 可扩展性设计

这是最后一步,面试官可能会针对系统中的某个模块,给出扩展性相关的问题,这块的问题可以分为两类:

  1. 当前系统的优化
    。因为没有完美的系统,我们的设计的也不例外,因此这类问题需要我们认真反思系统的设计,找出其中可能的缺陷,提出具体的解决方案。
  2. 扩展当前系统
    。例如我们当前的设计能够支撑100w 用户,那么当用户数达到 1000w 时,需要如何应对等等。

这里面可能涉及到水平扩展、垂直扩展、缓存、负载均衡、数据库的拆分的同步等等话题,后续会有专门的文章进行讲解。

实战 - 4步法解决Weibo设计

这部分我以Weibo的设计为例,带大家过一遍如何用4步法解决实际的系统设计。

确定weibo的使用场景

因为weibo功能较多,这里没有面试官的提示,我们假定需要实现
微博的时间线以及搜索
的功能。

基于这个设定,我们需要解决如下几个问题:

  • 用户发微博:服务能够将微博推送给对应的关注者,同时可能需要相应的提醒。
  • 用户浏览自己的微博时间线。
  • 用户浏览主页微博,也就是需要将用户关注对象的微博呈现出来。
  • 用户能够搜索微博。
  • 整个系统具有高可用性。

初步估算

基于上面的需求,我们进一步做出一些假设,然后计算出大概的储存需求和QPS,因为后续的技术选型依赖于当前系统的规模。这里我做出如下假定:

  • 假设有1亿活跃用户,每人平均每天5条微博,也就是每天5亿条微博,每月150亿条。
  • 每条微博的平均阅读量是20,也就是每月3000亿阅读量。
  • 对于搜索,假定每人每天搜索5次,一个月也就是150亿次搜索请求。

下面进一步对存储和QPS进行估算,

首先是储存,每条微博至少包含如下几个内容:

  • 微博id
    :8 bytes
  • 用户id
    :32 bytes
  • 微博正文
    :140 bytes
  • 媒体文件
    :10 KB (这里只考虑媒体文件对应的链接)
    总共10KB左右。

每月150亿条微博,也就是0.14PB,每年1.68PB。

其次是QPS,这里涉及到3个接口:

  • 发微博接口:每天5亿条微博,也就是大约6000QPS
  • 阅读微博接口:每天100亿阅读,也就是大约12万QPS
  • 搜索微博接口:每天每人搜索5次,那也大约6000QPS。

这里估算出来的数据为后续技术选型做参考。

完成weibo整体设计

根据上面的分析,这一步将主要服务拆分出来,可以分为读微博服务、发微博服务、搜索服务;同时还有相关的时间线服务、微博信息服务,用户信息服务、用户关系图服务、消息推送服务等等。考虑到服务高可用,这里也引入了缓存。

拆分好了之后,根据用户使用场景,可以设计出如下图所示的系统架构图,注意到目前为止,都是粗略设计,我们只需要将服务抽象出来,完成具体的功能即可。后续步骤会对主要服务进行详细设计。

下图的架构中,主要实现了用户发微博、浏览微博时间线和搜索的场景。

GJtsyu

设计核心模块

上一步我们完成了微博的架构设计,这一步从用户场景入手,详细设计核心模块。

用户发微博

用户发微博的时候,发微博服务需要完成如下几项工作:

  • 将微博写入到MySQL等关系型数据库,考虑到流量较大,这里可以引入Kafka等MQ来进行削峰。
  • 查询用户关系图服务,找到该用户的所有follower,然后把微博插入到所有follower的时间线上,注意到这里时间线的信息都是存放在缓存中的。
  • 将微博数据导入到搜索集群中,提供给后续搜索使用。
  • 将微博的媒体文件存放到对象储存中。
  • 调用消息推送服务,将消息推送到所有的followers。这里同样可以采用一个MQ来进行异步消息推送。

对于每个用户,我们可以用一个Redis的list,来存放所有该用户关注对象的微博信息,类似如下:

第N条微博 第N+1条微博 第N+2条微博
8 bytes 8 bytes 1 byte 8 bytes 8 bytes 1 byte 8 bytes 8 bytes 1 byte
weibo_id user_id meta weibo_id user_id meta weibo_id user_id meta

后续的时间线服务,可以根据这个列表生成用户的时间线微博。

用户浏览微博时间线

当用户浏览主页时间线时,微博时间线服务会完成如下的工作:

  • 从上面设计的Redis list中拿到时间线上所有的微博id和用户id,可以在O(1)时间内完成。
  • 查询微博信息服务,来获取这些微博的详细信息,这些信息可以存放在缓存中,O(N)时间内可以完成。
  • 查询用户信息服务,来获取每条微博对应用户的详细信息,同样也是O(N)时间完成。

注意到这里有一个特殊的情形,就是用户浏览自己的微博列表,对于这种情况,如果频率不是很高的情况,可以直接从MySQL中取出用户所有的微博即可。

用户搜索微博

当用户搜索微博时,会发生下面的事情:

  • 搜索服务首先会进行预处理,包括对输入文本的分词、正则化、词语纠错等等处理。
  • 接着将处理好的结果组装成查询语句,在集群中完成对应的查询,获取搜索结果。
  • 最后根据用户的设置,可能需要对结果进行排序、聚合等等,最后将处理好的结果返回给用户。

系统扩展设计

在做系统扩展设计之前,我们可以依据下面几个步骤,找出系统中可能存在的瓶颈,然后进行针对性优化。

(1)对各个模块进行benckmark测试,并记录对应的响应时间等重要数据;
(2)综合各种数据,找到系统的瓶颈所在;
(3)解决瓶颈问题,并在各种可选方案之间的做权衡,就像开头所说,没有完美的系统。

下面针对我们刚才设计的微博系统,可能的瓶颈存在于下面几个地方:

  • 微博服务器的入口。这里承受了最大的流量,因此可以引入负载均衡进缓解。
  • 发微博服务。可以看到这里需要和大量的服务进行交互,在流量很大的情况下,很容易成为整个系统的瓶颈,因此可以考虑将其进行水平扩展,或者将发微博服务进行进一步拆分,拆成各个小组件之后,再进行单独优化。
  • MySQL服务器。这里存储着用户信息、微博信息等,也承载了很大的流量,可以考虑将读写进行分离,同时引入主从服务器来保证高可用。
  • 读微博服务。在某些热点事件时,读微博服务会接收巨大的流量,可以引入相应的手段对读服务进行自动扩展,比如K8s的水平扩展等,方便应对各种突发情况。

QK5lHU

下面是进一步优化之后的系统架构图:

m5YDbe

针对这个系统,我们还可以进一步优化,以下是几个思路,也可以自己思考看看,

  • 对于有大量粉丝的用户,比如很多明星;在现有架构上明星每次都会将微博利用数据传输服务,发到每个粉丝的时间线上去,这样其实会造成大量的流量,针对这种情况,可以将这些明星用户单独处理,每当粉丝在刷新主页的时候,会根据这些粉丝的时间线以及明星的微博信息,合并来生成粉丝的主页时间线,避免了不必要的流量浪费。
  • 只保存最近一段时间的微博数据在缓存中, 这主要是为了节省集群空间,并且一般热点微博都是最近的微博。
  • 只保存活跃用户的主页时间线在缓存中,对于过去一段时间内,比如30天内未活跃的用户,我们只有在该用户首次浏览的时候,从DB中将数据load出来组成时间线。

扩展的方向不止上面说的这几点,大家可以从缓存(CDN,用户缓存、服务器缓存)、异步(MQ、微服务等等)、数据库调优等方向去思考,看如何提升整体性能。这些相关内容我会在后续文章中仔细讲述,欢迎关注【码老思】,后续文章敬请期待。


参考:

可以关注公众号【码老思】,一时间获取最通俗易懂的原创技术干货。

标签: none

添加新评论