经常在面试的时候,会被问到系统设计类的题目,比如如何设计微信朋友圈、如何设计12306系统、如何设计一个抢票系统等等。如果是没有准备过,一般都会不知所措,难以找到切入点。今天这里码老思会介绍一个解决系统设计类问题的通用框架,无论什么问题,朝这几步走,一定能找到解决办法。
系统设计面试中,经常会被问到如何设计微信、如何设计微博、如何设计百度……我们怎么能在如此短的时间内设计出来一个由成千上万的码农、PM,经年累月地迭代出来的如此优秀的产品?如果面试者这么优秀,那还面试啥?百度、谷歌也不可能只是一个搜索框而已,底下的东西复杂去了。
所以首先要明确,系统问题的答案一定不可能是全面的,面试官也不会期望我们给出一个满分答案。所谓的系统设计面试实际上是在模拟一个场景:两名同事在一起就一个模糊的问题,讨论一番,得出一个还不错的解决方案。
因此在这个过程中,我们需要与面试官进行充分的沟通,了解清楚需求,回应面试管的各种问题,阐述我们设计的方案。最终设计的结果是开放性的,没有标准答案;而面试官会在这个过程中,会充分挖掘我们的沟通、分析、解决问题的能力,最后给出通过与否的结论。
在面试中,常见的错误是面试官给出问题后,候选人就开始怼各种关键词,什么Load Balancer,Memcache,NodeJS,MongoDB,MySQL.
不是说我们不能说这些技术词汇,但是系统设计首先我们需要搞清楚具体的需求,然后在大概确定系统架构,最后才根据场景进行技术选型,因为并不存在一个完全完美的系统设计,往往都是各方面之间的均衡,至于如何均衡,需要结合面试官的要求和具体实际来选择。
系统设计考察我们如何去解决一个具体的问题,在实际工作中,我们也是先实现功能之后,再在此基础上去进行针对性的优化;在面试中同样也十分重要,一方面,面试官希望看到你从一开始设计系统,到慢慢完善的过程,另外,系统设计其实没有最优解,往往都是各种因素之间权衡后的结果,就像CAP理论一样,无法同时满足,我们的系统只要能满足面试官提出来的要求,刚好够用就行。
很多人在听到问题之后,就开始闷头设计,丝毫不会和面试官进行沟通交流。这样不仅不利于自己理解题目意图,而且让面试官无法了解你是如何一步步去解决问题的过程。
这个时候可以把面试官当做一位同事,比如你对题目不理解,可以提出问题搞清楚题目意图;又比如你在哪个环节卡住了,也不要一直闷在那,可以大胆向面试官求助寻求提示,也能节省不少时间。
就像开始说的,系统设计没有最优解,你的思路和解决问题的过程很重要,而这些就是通过不断的沟通来传递给面试官的。
针对系统设计,这里给大家提供一个4步法解决方案,无论是任何系统设计的题目,都可以按照这几步去思考解决。
这一步主要是确定问题的需求和系统使用场景,为了搞清楚这个问题,需要和面试官你来我往地问问题。
在做设计的同时,问面试官的要求是什么,做出合理的假设、取舍,让面试官看出你的思考过程,最终综合所有的信息完成一个还不错的设计。不要害怕问问题。那并不会说明我们不懂,而是让面试官理解我们的思考过程。当我们问出问题后,面试官要么给出明确的回答;要么让我们自己做出假设。当需要做出假设时,我们需要把假设写下来备用。
这个举个例子,比如面试官让你设计weibo,因为weibo的功能较为庞大,例如发微博、微博时间线、关注、取消关注、微博热搜榜等等,我们无法在短短的面试时间内完成这么多功能设计,所以这时候可以询问面试官我们需要实现哪些功能。
比如需要实现微博时间线的功能,我们得进一步确认,整体用户量多大,系统的QPS多大,因为这涉及到我们后续的系统设计,而且如果对于QPS特别高的情况,在后续的设计中需要针对此进行专门的扩展性优化。
QPS有什么用?
QPS 和 服务器/数据库之间的关系:
以下是一些通用的问题,可以通过询问系统相关的问题,搞清楚面试官的意图和系统的使用场景。
总结,在这一步,不要设计具体的系统,把问题搞清楚就行。不要害怕问问题,那并不会说明我们不懂,而是让面试官理解我们的思考过程。
这一步根据系统的具体要求,我们将其划分为几个功能独立的模块,然后做出一张整体的架构图,并依据此架构图,看是否能实现上一步所提出来的需求。
如果可能的话,设想几个具体的例子,对着图演练一遍。这让我们能更坚定当前的设计,有时候还能发现一些未考虑到的边界case。
这里说的比较抽象,具体可以参考下面的实战环节,来理解如何完成整体设计。
至此,我们已经完成了系统的整体设计,接下来需要根据面试官的要求对模块进行具体设计。
比如需要设计一个短网址的系统,上一步中可能已经把系统分为了如下三个模块:
这一步中我们需要对上面三个模块进行具体设计,这里面就涉及到实际的技术选型了。下面举个简单的例子。
比如说生成网址的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值之后产生碰撞如何解决等等,都需要在这个阶段解决。这里面涉及到各种存储方案的选择、数据库的设计等等,后面会有专门的文章进行介绍。
这是最后一步,面试官可能会针对系统中的某个模块,给出扩展性相关的问题,这块的问题可以分为两类:
这里面可能涉及到水平扩展、垂直扩展、缓存、负载均衡、数据库的拆分的同步等等话题,后续会有专门的文章进行讲解。
这部分我以Weibo的设计为例,带大家过一遍如何用4步法解决实际的系统设计。
因为weibo功能较多,这里没有面试官的提示,我们假定需要实现微博的时间线以及搜索的功能。
基于这个设定,我们需要解决如下几个问题:
基于上面的需求,我们进一步做出一些假设,然后计算出大概的储存需求和QPS,因为后续的技术选型依赖于当前系统的规模。这里我做出如下假定:
下面进一步对存储和QPS进行估算,
首先是储存,每条微博至少包含如下几个内容:
微博id
:8 bytes用户id
:32 bytes微博正文
:140 bytes媒体文件
:10 KB (这里只考虑媒体文件对应的链接)
总共10KB左右。每月150亿条微博,也就是0.14PB,每年1.68PB。
其次是QPS,这里涉及到3个接口:
这里估算出来的数据为后续技术选型做参考。
根据上面的分析,这一步将主要服务拆分出来,可以分为读微博服务、发微博服务、搜索服务;同时还有相关的时间线服务、微博信息服务,用户信息服务、用户关系图服务、消息推送服务等等。考虑到服务高可用,这里也引入了缓存。
拆分好了之后,根据用户使用场景,可以设计出如下图所示的系统架构图,注意到目前为止,都是粗略设计,我们只需要将服务抽象出来,完成具体的功能即可。后续步骤会对主要服务进行详细设计。
下图的架构中,主要实现了用户发微博、浏览微博时间线和搜索的场景。
上一步我们完成了微博的架构设计,这一步从用户场景入手,详细设计核心模块。
用户发微博的时候,发微博服务需要完成如下几项工作:
对于每个用户,我们可以用一个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 |
后续的时间线服务,可以根据这个列表生成用户的时间线微博。
当用户浏览主页时间线时,微博时间线服务会完成如下的工作:
注意到这里有一个特殊的情形,就是用户浏览自己的微博列表,对于这种情况,如果频率不是很高的情况,可以直接从MySQL中取出用户所有的微博即可。
当用户搜索微博时,会发生下面的事情:
在做系统扩展设计之前,我们可以依据下面几个步骤,找出系统中可能存在的瓶颈,然后进行针对性优化。
(1)对各个模块进行benckmark测试,并记录对应的响应时间等重要数据; (2)综合各种数据,找到系统的瓶颈所在; (3)解决瓶颈问题,并在各种可选方案之间的做权衡,就像开头所说,没有完美的系统。
下面针对我们刚才设计的微博系统,可能的瓶颈存在于下面几个地方:
下面是进一步优化之后的系统架构图:
针对这个系统,我们还可以进一步优化,以下是几个思路,也可以自己思考看看,
扩展的方向不止上面说的这几点,大家可以从缓存(CDN,用户缓存、服务器缓存)、异步(MQ、微服务等等)、数据库调优等方向去思考,看如何提升整体性能。这些相关内容我会在后续文章中仔细讲述,欢迎关注【码老思】,后续文章敬请期待。
参考: