<audio id="audio" title="07 | 系统设计能力考察：系统设计内功到底怎么考？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f4/4f/f46db6fd5322b1a4b44f39fdf63a1c4f.mp3"></audio>

你好，我是四火。

在上一讲，我们谈到对于数据结构和算法部分考察的把控，这一讲，我们来聊聊系统设计。

我们经常说软件工程师要注意积累工作经验，那么，年复一年的工作中，我们到底积累了哪些技术方面的经验？

我想，其中很重要的一个就是对于系统的理解。对于刚毕业和工作不久的工程师来说，我们不应苛求他们对系统具备相当深度的理解；另一方面，即便工作年数相同，不同候选人对于系统的认识深度也有着非常大的差距。

和能够明确判断出正误的算法类问题不同，系统设计我们往往没法给出丁是丁卯是卯的正误判断，因而面试中讨论的更多是可行性，以及对于不同实现方案的利弊权衡。这也是符合现实逻辑的，有一些相似的系统，它们的实现技术截然不同，有着不同的优势和短板，但是最终都解决了最核心的问题。

## 糟糕的例子

好，我们先来说几个典型的反面例子，和算法、数据结构的面试不同，系统设计的面试往往能遵循的简单套路更少，因而也能见到更多的“踩坑”实践。

### 排斥不一样的思路

最典型的例子就是面试官缺乏包容心。就像“看自己的代码越看越开心，看别人的代码越看越糟心”一样，在讨论一些经典系统设计思路的时候，如果对方的思路和常规思路明显不同，**某些面试官可能会立即觉得对方的想法很“奇葩”。**

**这种想法其实是非常危险的，因为面试官在心里默默建立了一个护城墙，凡是不符合“标准”的解决思路，都会产生排斥心理。**

下面我举一个具体例子说明吧。有一次，某位面试官和候选人讨论，怎样设计一个篮球比赛的文字直播系统。

这个系统需要实时显示当前的比分等信息，那么一种思路是使用普通的查询，客户端主动发起，即定时地不断询问服务端最新的比分；还有一种思路是使用长连接，或者long poll这样的机制，使得服务端可以更实时地推送最新的比分更新。

很多系统都采用后者的思路来实现，这主要是基于实时性等方面的考虑，但是当时候选人考虑使用前一种方案。于是，面试官就直接中断了他的陈述，并直接解释为什么应该选择后者。

暂且不说所谓的正误，或者好与不好。这个技术实现选择的不一致，本来可以引出一个很好的面试实践，也就是对两种实现机制的利弊做讨论，但很遗憾，这种打断的做法直接终止了这样的讨论。

也就是说，我们完全可以跟着候选人的思路，用上一讲提到的“要求澄清”的办法让他自己来分析出实现的利弊来。

之后，如果候选人自己意识到，或者面试官使用“给出挑战”这样的办法，通过讨论这个方案的不足，来引出后一个方案，并放到一起进行优劣比较。

这样一来就可以有比较多的讨论与互动，我们也能得到更多的关于候选人对于系统理解的数据点。我想强调的是，最后采用哪一个方案其实并不重要，重要的是这个过程和我们得到的考察数据。

还有种类似情况是，**有时候选人对于系统的设计思路是自底向上的，而面试官则是自上而下的。**

一般说来，自底向上去思考和设计系统的时候，确实存在“捡了芝麻丢了西瓜”的情况，但是如果面试中候选人这么做，我们不要去打断他，或者破坏他的这种思维模式，而要顺着他最熟悉的方法来讨论这个问题。

我们做系统设计面试的目的，是要考察候选人对于系统的理解，抓住一些有意义的数据点，而不是要尝试去把系统设计做到最好。

换言之，**我们在面试中，一定要摒弃掉所谓的“最佳设计”或者“推荐方案”这样的想法，而是尽量追着候选人的思路跑！**

最后设计出的系统也许有这样那样的问题，但请记住面试官可以主导面试，却最好不要主导设计——完成的系统一定是一个候选人主导设计的系统，而不是面试官。

### 无意义的深挖

我们当然可以针对系统的某一个点进行深挖，但是如果候选人已经暴露出，他还不具备深挖该问题的基础知识，那么这个继续深挖很可能就是徒劳的，因为它除了给候选人带来沮丧以外，并不能得到太多更有价值的考察数据点。

举个例子，有一位面试官和候选人一起讨论“短URL系统”的设计问题。其中一个点是，在做短URL重定向的时候，返回码应该选择301还是302？

302是临时重定向，301是永久重定向，虽然301可能会减轻一定的访问压力，但通常来说，我们一般都选择302。这里的原因是，这些系统有一个很大的价值就是可以获取用户的访问数据，使用301可能会使得用户后续的访问直接跳过了这个系统，而去访问重定向后的系统，这样，这些有价值的URL访问数据就丢失了。

但是，当时候选人只对HTTP返回码略有了解，并不知道301和302的意义是什么，回答也明显有些搪塞，这种情况下，面试官还想继续往下挖，问到底应该选择301还是302。我觉得，这种深挖的意义就不大了。

## 考察技巧

好了，说完反面，下面来从正面讲讲系统设计考察的技巧。通常来说，系统设计的考察有两种主要形式：

- 第一种，是让候选人讲他自己熟悉的、做过的系统，或者是引以为豪的项目；
- 第二种，是像前几讲中介绍的那样，给出一个实际问题，细化、分析，延伸成一个对于核心功能的系统设计问题，并加以讨论和解决。

### 做过的系统

第一种面试方式非常常见。**让候选人谈论做过的系统，把握一条原则，是候选人必须非常熟悉，最好是“最”熟悉的系统**，这样我们才能了解到，候选人在过去的工作中，表现如何，有多深入的理解，又有多少思考。

我们不妨回想一下其它的技术方面的考察路径，如果是算法和数据结构，问“做过的算法”，好像很难这么问，而且这么问很可能也挖不到什么有价值的信息；如果是面向对象，问“做过的项目中，类和方法的设计”，听起来就觉得非常别扭，对不对？

确实如此，对于技术方面的考察点，唯有系统设计，问“做过的系统”，是一个非常好的途径。

话说回来，这样的面试方式，**也存在着一定的局限性。**

**最大的问题，就是它只考察了候选人对于“已有系统”的理解，多为“经验之谈”，这是可以事先充分准备的。**它并不能很好反映出，对于一个陌生的新问题，候选人能否灵活地运用掌握的套路和方法来解决它。

事实上，在面试中，我也确实遇到个别候选人，虽然对于自己做过的项目说得头头是道，但给出一个新问题，却畏手畏脚，缺乏想法，表现不佳。

其次，必须指出的是，**这种方式，对于面试官的素质也有着相当高的要求。**

因为相对而言，这种方式下，谈论怎样的系统，包含着怎样的问题，涉及到怎样的技术，都是候选人所主导的，如果面试官并不具备丰富的经验，又恰好遇到一个不属于自己熟悉领域的软件系统，有时候就没办法提出一针见血的问题，也没法采集到充分且有说服力的考察数据。

在某些大厂的面试培训中，**培训老师甚至会明确建议，经验较少的面试官不要主导系统设计面试，而是主导相对容易操作的算法和数据结构的面试。**<br>
<img src="https://static001.geekbang.org/resource/image/4a/11/4a2872f07c2da3e22e5873ae4cfa4411.jpg" alt="">

对于发问，我们可以这样做：

> 
你能否介绍一下，过去有哪一个你最熟悉，或是最引以为傲的软件系统？


我们也可以找到简历上最加以渲染的项目，而发问：

> 
你在简历上说，你主导了一个半年期的项目X，我对于这个系统很感兴趣，你能否介绍一下？


其实这两个方式的目的是一致的。

对于考察的过程，我觉得要从宏观和微观层面去分别把握。

**在宏观上，观察候选人能否对他介绍的系统有着清晰的把握，有着整体的认识**，而不是一下子就跳到某一个具体细节实现上去。比如说：

- 系统解决了什么问题？
- 系统的架构是怎样的？
- 模块、组件之间的交互是怎样的？
- 系统存在哪些问题，有哪些瓶颈？
- 如果再给一次重新设计系统的机会，哪些地方可以改善？

**在微观上，对于候选人自己熟悉的、做过的部分，我们要选择几个点向下挖，并且挖掘得足够深，到“具体是怎样实现的”这样的程度**，毕竟在现实中，夸夸其谈的人不在少数。

“深入”的核心，就是要抓住两个词——“具体”和“细节”，下面我通过一个小例子来说明。

比方说，候选人说，他最近主导了一个三个月期的性能优化项目，那我们就可以抓住这一点进一步细化。请看下面这个对话片段：

候选人：我最近在项目中把门户网站的性能优化了一倍。

面试官：我很感兴趣，能具体说说是什么方面的性能优化了一倍吗？

候选人：是网页的加载时间优化了一倍，以前平均一个页面要将近3秒，现在1.5秒就可以打开了。

面试官：是什么网页，门户网站的所有网页吗？

候选人：不是，是媒体详情页，不是媒体列表页。

面试官：哦，那你做了哪些改进才做到的呢？

候选人：我把详情页上的评论部分改成Ajax调用了，这样页面加载出来，用户浏览评论区时再去异步请求获取评论。

面试官：了解了，那你通过什么方法得知这个页面加载时间从3秒优化成1.5秒的呢？

候选人：我们做了性能测试，压测我们beta环境的系统，1.5秒是TP99的值。

面试官：那在压测的过程中，系统的吞吐量是多少呢？

候选人：只有一个主请求，没有模拟静态资源访问的情况下，QPS上限大概是2000。

面试官：那在测试中你是怎样确认这个QPS已经达到上限了？

候选人：根据我们的单机压测模型，开始并发数逐渐增加，吞吐量线性增大，后来增长减缓，但到大概第10分钟的时候出现拐点，延迟突然增大，吞吐量也过了峰值走低。

这里抓住一个点深挖的目的，并不是考察候选人能否记得性能优化的每一处细节，而是要确认候选人是否真正参与了这个过程，是否对这个优化过程的几个核心问题有清晰准确的认识。

换言之，如果候选人重新做一个类似的性能优化项目，要看看他是否确实因为这个项目而具备了相当的功底。从中我们也可以看出候选人对于某项技术，到底把握到何种程度，对于做过的项目和系统，是否具备一定深度的理解。

需要说明的是，一个技术点的深挖要以拿到预期的考察数据为目标，可以问1分钟，可以问10分钟，这都是没有限制的。一旦拿到了预期考察的数据，不必过多纠缠，继续下一个环节。

当然，这个话题点一定要是候选人（至少是自称）熟悉的，并且，**深挖和关注细节不是纠缠于细节，我们不要把追问变成钻牛角尖，也不要把追问变成“考记忆力”。**

### 全新的问题

前面讲了我们怎样去考察候选人对于他做过的、熟悉的系统，到底有多少理解，下面我们再来讲讲，如果是一个新问题呢，候选人能否顺利地设计一个全新的系统来解决它？这两个方面其实是互相独立的。

那我们就用一个模拟案例来介绍，如果我们像[第4讲](https://time.geekbang.org/column/article/362407)说的，像做一个迷你项目一样，给出一个实际问题，层层递进，逐步细化、分解和延伸成一个系统设计的问题（如果你忘记了，请一定回看第4讲），我们又该注意哪些方面。那个具体问题就是：

> 
“请你设计一个网约车系统。”


并且我们也谈到了，经过细化以后，下面的系统设计考察，就是要求设计一个系统，来完成定位、叫车、搜索、接单等网约车系统的核心功能，如：

1. 乘客可以随时随地叫车；
1. 系统寻找邻近的司机并转发请求，如果无法接单，继续扩大寻找范围；
1. 司机可以抢单，即接受或者拒绝叫车请求；
1. 双方可以刷新自己在地图上的位置，也可以查看彼此当前的位置。

对于非功能需求，特别是系统容量的估算，我们后续将在专题中谈到；而对于功能需求的系统实现方面，我们继续来看后面候选人和面试官之间的对话片段。

**第一阶段：客户端 - Service - 存储的基本层次建立**

面试官：根据我们的讨论所确定的需求，这个系统中会有哪些主要模块？（要求澄清）

候选人：我觉得这个系统中，乘客可以叫车，司机可以接单，因此我打算创建一个Ride Service来接受乘客的请求，ride的信息存放在数据库中。整个交互过程如下图：

1. 乘客请求乘坐，提交一个ride（相当于一个请求乘坐的订单）；
1. Ride Service将乘坐请求发给多个司机；
1. 司机抢单；
1. Ride Service再将接单的信息告知乘客。

<img src="https://static001.geekbang.org/resource/image/e8/50/e8a3c8d7a4001152ed4772f7e41f1650.png" alt="">

**第二阶段：Service 解耦**

面试官：嗯，那么其中的第2步和第4步，消息怎样从服务端传递给客户端呢？（要求澄清）

候选人：我觉得可以使用长连接，来保证消息能够第一时间送达，如果这一步失败，iOS或者安卓平台还有统一的消息推送机制。

面试官：好，那么Ride Service怎么来寻找合适的司机来转发乘客的乘坐请求呢？（给出挑战）

候选人：我觉得Ride Service知道所有乘客和司机的位置，那就可以寻找最近的一些匹配，位置可以通过司机和乘客各自手机上的app来汇报，比如每十秒钟就汇报一次GPS位置。

面试官：可是我没有看到你画的图中有这个啊？（要求澄清）

候选人：哦，他们都汇报给Ride Service。

面试官：所以，位置信息归Ride Service管，乘车请求的匹配归Ride Service管，匹配上了以后，结果的通知也归Ride Service管，它一个service要管那么多事情吗？（给出提示）

候选人：嗯……我觉得可以解耦，根据单一职责的原则，让一个service只做一件主要的事情：

- Ride Service只管ride的匹配和决策；
- Notification Service负责通知消息；
- Location Service负责维护司机和乘客的位置。

<img src="https://static001.geekbang.org/resource/image/d8/fc/d8137ed34cbeaeb73ac5f0d402b6dbfc.png" alt="">

**第三阶段：进一步解耦，补足缺失**

面试官：嗯，确实看起来好多了。但是我还有个问题，司机和乘客汇报GPS位置的时候，从图上看，只有在乘客请求和司机接受ride的时候才会汇报位置信息吗？。（要求澄清）

候选人：哦，系统是需要实时获取位置信息，才好做匹配，我觉得GPS的信息也应该从ride的逻辑解耦开，应该有单独的位置汇报请求发给Location Service。

<img src="https://static001.geekbang.org/resource/image/cf/f4/cf4ae62f446880320000614bdb170bf4.png" alt="">

**第四阶段：细化存储层设计**

面试官：嗯，不错。我注意到ride数据和位置数据也随着Ride Service和Location Service的解耦而分离开了，这很好。那么，对于这两类数据的存储，你打算分别选用怎样的技术来实现呢？

候选人：ride数据需要接受关系查询，位置信息需要根据司机、乘客的ID来查询，还需要能够快速找到邻近位置的司机和乘客……（对于这两类数据的存储，可以作为下一个话题来详细展开）

不知道你发现没有，这个小片段，就模拟了面试官使用“要求澄清”和“给出挑战”两大法宝，怎样领着候选人一步步攻克系统设计问题的过程，这个过程和上一讲的算法和数据结构面试，是类似的。

其中，对于位置数据的存储，如果感兴趣的话《全栈工程师修炼指南》的[第26讲](https://time.geekbang.org/column/article/162965)中，我做了详细的介绍，你可以做延伸阅读。

<img src="https://static001.geekbang.org/resource/image/2c/fa/2c610e117e0ebdb75a614375ee70b9fa.png" alt="">

## 总结与思考

好，今天的内容主要就是这些。

我先给你举了一些系统设计面试的踩坑案例，然后又给你分享了两类主要的面试方式，深挖候选人做过的系统，或者和他一起讨论一个全新迷你项目的实现，它们都能考察候选人对于系统的理解，但是又各有侧重。

你可以结合我为你还原的面试现场对话，仔细体会系统面试的操作方法。

这里我需要额外强调的是，系统设计就和算法一样，我们还是更应该关注基础，选择合适的问题，可以问，但不要一味追求高大上的“海量存储”、“大数据”这样的问题，这和算法和数据结构的面试不要过度追求“偏题”、“怪题”是一样的。

**毕竟，能将生活中那些实际的问题，选择合适的工程技术来解决，才是软件工程师最根本的使命。**

<img src="https://static001.geekbang.org/resource/image/af/1d/af55fabf291cc6c979d439b7541cbd1d.jpg" alt="">

在最后，我想请你聊一聊你在阅读后的想法，特别是你是否有系统面试的经历，你觉得有哪些特别重要的方面，能否在留言区谈一谈呢？我会积极回复你的想法和问题。

好，我是四火，我们下一讲见！
