你好,我是四火。
在上一讲,我们谈到对于数据结构和算法部分考察的把控,这一讲,我们来聊聊系统设计。
我们经常说软件工程师要注意积累工作经验,那么,年复一年的工作中,我们到底积累了哪些技术方面的经验?
我想,其中很重要的一个就是对于系统的理解。对于刚毕业和工作不久的工程师来说,我们不应苛求他们对系统具备相当深度的理解;另一方面,即便工作年数相同,不同候选人对于系统的认识深度也有着非常大的差距。
和能够明确判断出正误的算法类问题不同,系统设计我们往往没法给出丁是丁卯是卯的正误判断,因而面试中讨论的更多是可行性,以及对于不同实现方案的利弊权衡。这也是符合现实逻辑的,有一些相似的系统,它们的实现技术截然不同,有着不同的优势和短板,但是最终都解决了最核心的问题。
好,我们先来说几个典型的反面例子,和算法、数据结构的面试不同,系统设计的面试往往能遵循的简单套路更少,因而也能见到更多的“踩坑”实践。
最典型的例子就是面试官缺乏包容心。就像“看自己的代码越看越开心,看别人的代码越看越糟心”一样,在讨论一些经典系统设计思路的时候,如果对方的思路和常规思路明显不同,某些面试官可能会立即觉得对方的想法很“奇葩”。
这种想法其实是非常危险的,因为面试官在心里默默建立了一个护城墙,凡是不符合“标准”的解决思路,都会产生排斥心理。
下面我举一个具体例子说明吧。有一次,某位面试官和候选人讨论,怎样设计一个篮球比赛的文字直播系统。
这个系统需要实时显示当前的比分等信息,那么一种思路是使用普通的查询,客户端主动发起,即定时地不断询问服务端最新的比分;还有一种思路是使用长连接,或者long poll这样的机制,使得服务端可以更实时地推送最新的比分更新。
很多系统都采用后者的思路来实现,这主要是基于实时性等方面的考虑,但是当时候选人考虑使用前一种方案。于是,面试官就直接中断了他的陈述,并直接解释为什么应该选择后者。
暂且不说所谓的正误,或者好与不好。这个技术实现选择的不一致,本来可以引出一个很好的面试实践,也就是对两种实现机制的利弊做讨论,但很遗憾,这种打断的做法直接终止了这样的讨论。
也就是说,我们完全可以跟着候选人的思路,用上一讲提到的“要求澄清”的办法让他自己来分析出实现的利弊来。
之后,如果候选人自己意识到,或者面试官使用“给出挑战”这样的办法,通过讨论这个方案的不足,来引出后一个方案,并放到一起进行优劣比较。
这样一来就可以有比较多的讨论与互动,我们也能得到更多的关于候选人对于系统理解的数据点。我想强调的是,最后采用哪一个方案其实并不重要,重要的是这个过程和我们得到的考察数据。
还有种类似情况是,有时候选人对于系统的设计思路是自底向上的,而面试官则是自上而下的。
一般说来,自底向上去思考和设计系统的时候,确实存在“捡了芝麻丢了西瓜”的情况,但是如果面试中候选人这么做,我们不要去打断他,或者破坏他的这种思维模式,而要顺着他最熟悉的方法来讨论这个问题。
我们做系统设计面试的目的,是要考察候选人对于系统的理解,抓住一些有意义的数据点,而不是要尝试去把系统设计做到最好。
换言之,我们在面试中,一定要摒弃掉所谓的“最佳设计”或者“推荐方案”这样的想法,而是尽量追着候选人的思路跑!
最后设计出的系统也许有这样那样的问题,但请记住面试官可以主导面试,却最好不要主导设计——完成的系统一定是一个候选人主导设计的系统,而不是面试官。
我们当然可以针对系统的某一个点进行深挖,但是如果候选人已经暴露出,他还不具备深挖该问题的基础知识,那么这个继续深挖很可能就是徒劳的,因为它除了给候选人带来沮丧以外,并不能得到太多更有价值的考察数据点。
举个例子,有一位面试官和候选人一起讨论“短URL系统”的设计问题。其中一个点是,在做短URL重定向的时候,返回码应该选择301还是302?
302是临时重定向,301是永久重定向,虽然301可能会减轻一定的访问压力,但通常来说,我们一般都选择302。这里的原因是,这些系统有一个很大的价值就是可以获取用户的访问数据,使用301可能会使得用户后续的访问直接跳过了这个系统,而去访问重定向后的系统,这样,这些有价值的URL访问数据就丢失了。
但是,当时候选人只对HTTP返回码略有了解,并不知道301和302的意义是什么,回答也明显有些搪塞,这种情况下,面试官还想继续往下挖,问到底应该选择301还是302。我觉得,这种深挖的意义就不大了。
好了,说完反面,下面来从正面讲讲系统设计考察的技巧。通常来说,系统设计的考察有两种主要形式:
第一种面试方式非常常见。让候选人谈论做过的系统,把握一条原则,是候选人必须非常熟悉,最好是“最”熟悉的系统,这样我们才能了解到,候选人在过去的工作中,表现如何,有多深入的理解,又有多少思考。
我们不妨回想一下其它的技术方面的考察路径,如果是算法和数据结构,问“做过的算法”,好像很难这么问,而且这么问很可能也挖不到什么有价值的信息;如果是面向对象,问“做过的项目中,类和方法的设计”,听起来就觉得非常别扭,对不对?
确实如此,对于技术方面的考察点,唯有系统设计,问“做过的系统”,是一个非常好的途径。
话说回来,这样的面试方式,也存在着一定的局限性。
最大的问题,就是它只考察了候选人对于“已有系统”的理解,多为“经验之谈”,这是可以事先充分准备的。它并不能很好反映出,对于一个陌生的新问题,候选人能否灵活地运用掌握的套路和方法来解决它。
事实上,在面试中,我也确实遇到个别候选人,虽然对于自己做过的项目说得头头是道,但给出一个新问题,却畏手畏脚,缺乏想法,表现不佳。
其次,必须指出的是,这种方式,对于面试官的素质也有着相当高的要求。
因为相对而言,这种方式下,谈论怎样的系统,包含着怎样的问题,涉及到怎样的技术,都是候选人所主导的,如果面试官并不具备丰富的经验,又恰好遇到一个不属于自己熟悉领域的软件系统,有时候就没办法提出一针见血的问题,也没法采集到充分且有说服力的考察数据。
在某些大厂的面试培训中,培训老师甚至会明确建议,经验较少的面试官不要主导系统设计面试,而是主导相对容易操作的算法和数据结构的面试。
对于发问,我们可以这样做:
你能否介绍一下,过去有哪一个你最熟悉,或是最引以为傲的软件系统?
我们也可以找到简历上最加以渲染的项目,而发问:
你在简历上说,你主导了一个半年期的项目X,我对于这个系统很感兴趣,你能否介绍一下?
其实这两个方式的目的是一致的。
对于考察的过程,我觉得要从宏观和微观层面去分别把握。
在宏观上,观察候选人能否对他介绍的系统有着清晰的把握,有着整体的认识,而不是一下子就跳到某一个具体细节实现上去。比如说:
在微观上,对于候选人自己熟悉的、做过的部分,我们要选择几个点向下挖,并且挖掘得足够深,到“具体是怎样实现的”这样的程度,毕竟在现实中,夸夸其谈的人不在少数。
“深入”的核心,就是要抓住两个词——“具体”和“细节”,下面我通过一个小例子来说明。
比方说,候选人说,他最近主导了一个三个月期的性能优化项目,那我们就可以抓住这一点进一步细化。请看下面这个对话片段:
候选人:我最近在项目中把门户网站的性能优化了一倍。
面试官:我很感兴趣,能具体说说是什么方面的性能优化了一倍吗?
候选人:是网页的加载时间优化了一倍,以前平均一个页面要将近3秒,现在1.5秒就可以打开了。
面试官:是什么网页,门户网站的所有网页吗?
候选人:不是,是媒体详情页,不是媒体列表页。
面试官:哦,那你做了哪些改进才做到的呢?
候选人:我把详情页上的评论部分改成Ajax调用了,这样页面加载出来,用户浏览评论区时再去异步请求获取评论。
面试官:了解了,那你通过什么方法得知这个页面加载时间从3秒优化成1.5秒的呢?
候选人:我们做了性能测试,压测我们beta环境的系统,1.5秒是TP99的值。
面试官:那在压测的过程中,系统的吞吐量是多少呢?
候选人:只有一个主请求,没有模拟静态资源访问的情况下,QPS上限大概是2000。
面试官:那在测试中你是怎样确认这个QPS已经达到上限了?
候选人:根据我们的单机压测模型,开始并发数逐渐增加,吞吐量线性增大,后来增长减缓,但到大概第10分钟的时候出现拐点,延迟突然增大,吞吐量也过了峰值走低。
这里抓住一个点深挖的目的,并不是考察候选人能否记得性能优化的每一处细节,而是要确认候选人是否真正参与了这个过程,是否对这个优化过程的几个核心问题有清晰准确的认识。
换言之,如果候选人重新做一个类似的性能优化项目,要看看他是否确实因为这个项目而具备了相当的功底。从中我们也可以看出候选人对于某项技术,到底把握到何种程度,对于做过的项目和系统,是否具备一定深度的理解。
需要说明的是,一个技术点的深挖要以拿到预期的考察数据为目标,可以问1分钟,可以问10分钟,这都是没有限制的。一旦拿到了预期考察的数据,不必过多纠缠,继续下一个环节。
当然,这个话题点一定要是候选人(至少是自称)熟悉的,并且,深挖和关注细节不是纠缠于细节,我们不要把追问变成钻牛角尖,也不要把追问变成“考记忆力”。
前面讲了我们怎样去考察候选人对于他做过的、熟悉的系统,到底有多少理解,下面我们再来讲讲,如果是一个新问题呢,候选人能否顺利地设计一个全新的系统来解决它?这两个方面其实是互相独立的。
那我们就用一个模拟案例来介绍,如果我们像第4讲说的,像做一个迷你项目一样,给出一个实际问题,层层递进,逐步细化、分解和延伸成一个系统设计的问题(如果你忘记了,请一定回看第4讲),我们又该注意哪些方面。那个具体问题就是:
“请你设计一个网约车系统。”
并且我们也谈到了,经过细化以后,下面的系统设计考察,就是要求设计一个系统,来完成定位、叫车、搜索、接单等网约车系统的核心功能,如:
对于非功能需求,特别是系统容量的估算,我们后续将在专题中谈到;而对于功能需求的系统实现方面,我们继续来看后面候选人和面试官之间的对话片段。
第一阶段:客户端 - Service - 存储的基本层次建立
面试官:根据我们的讨论所确定的需求,这个系统中会有哪些主要模块?(要求澄清)
候选人:我觉得这个系统中,乘客可以叫车,司机可以接单,因此我打算创建一个Ride Service来接受乘客的请求,ride的信息存放在数据库中。整个交互过程如下图:
第二阶段:Service 解耦
面试官:嗯,那么其中的第2步和第4步,消息怎样从服务端传递给客户端呢?(要求澄清)
候选人:我觉得可以使用长连接,来保证消息能够第一时间送达,如果这一步失败,iOS或者安卓平台还有统一的消息推送机制。
面试官:好,那么Ride Service怎么来寻找合适的司机来转发乘客的乘坐请求呢?(给出挑战)
候选人:我觉得Ride Service知道所有乘客和司机的位置,那就可以寻找最近的一些匹配,位置可以通过司机和乘客各自手机上的app来汇报,比如每十秒钟就汇报一次GPS位置。
面试官:可是我没有看到你画的图中有这个啊?(要求澄清)
候选人:哦,他们都汇报给Ride Service。
面试官:所以,位置信息归Ride Service管,乘车请求的匹配归Ride Service管,匹配上了以后,结果的通知也归Ride Service管,它一个service要管那么多事情吗?(给出提示)
候选人:嗯……我觉得可以解耦,根据单一职责的原则,让一个service只做一件主要的事情:
第三阶段:进一步解耦,补足缺失
面试官:嗯,确实看起来好多了。但是我还有个问题,司机和乘客汇报GPS位置的时候,从图上看,只有在乘客请求和司机接受ride的时候才会汇报位置信息吗?。(要求澄清)
候选人:哦,系统是需要实时获取位置信息,才好做匹配,我觉得GPS的信息也应该从ride的逻辑解耦开,应该有单独的位置汇报请求发给Location Service。
第四阶段:细化存储层设计
面试官:嗯,不错。我注意到ride数据和位置数据也随着Ride Service和Location Service的解耦而分离开了,这很好。那么,对于这两类数据的存储,你打算分别选用怎样的技术来实现呢?
候选人:ride数据需要接受关系查询,位置信息需要根据司机、乘客的ID来查询,还需要能够快速找到邻近位置的司机和乘客……(对于这两类数据的存储,可以作为下一个话题来详细展开)
不知道你发现没有,这个小片段,就模拟了面试官使用“要求澄清”和“给出挑战”两大法宝,怎样领着候选人一步步攻克系统设计问题的过程,这个过程和上一讲的算法和数据结构面试,是类似的。
其中,对于位置数据的存储,如果感兴趣的话《全栈工程师修炼指南》的第26讲中,我做了详细的介绍,你可以做延伸阅读。
好,今天的内容主要就是这些。
我先给你举了一些系统设计面试的踩坑案例,然后又给你分享了两类主要的面试方式,深挖候选人做过的系统,或者和他一起讨论一个全新迷你项目的实现,它们都能考察候选人对于系统的理解,但是又各有侧重。
你可以结合我为你还原的面试现场对话,仔细体会系统面试的操作方法。
这里我需要额外强调的是,系统设计就和算法一样,我们还是更应该关注基础,选择合适的问题,可以问,但不要一味追求高大上的“海量存储”、“大数据”这样的问题,这和算法和数据结构的面试不要过度追求“偏题”、“怪题”是一样的。
毕竟,能将生活中那些实际的问题,选择合适的工程技术来解决,才是软件工程师最根本的使命。
在最后,我想请你聊一聊你在阅读后的想法,特别是你是否有系统面试的经历,你觉得有哪些特别重要的方面,能否在留言区谈一谈呢?我会积极回复你的想法和问题。
好,我是四火,我们下一讲见!
评论