系统设计考察的是什么?
系统设计这个面试环节,首先当然是考察候选人的对系统(System)底层的了解,然后考察的是候选人的架构(Architect)能力,在各种工具和方案中组合出最合适的。最后,大部分系统设计题是开放型的题目,这时候沟通(Communication)很重要,要多问面试官,理解题意,给出一个能自圆其说的结论。
设计容量
搭建一个服务,跟建造一个房子一样,是有开发成本的。不同容量的系统,需要的人月数完全不一样,成本差别巨大。拿到一个题目,首先要问面试者,希望抗住多少流量,多大的QPS或者多少在线用户等。
这个题目我一直在考虑要不要写,因为有一天也许我们彼此会坐在一方小桌的两端,聊聊系统设计,而我这么做有泄题兜底之嫌。不过,考虑到不是所有的读者都会来 TubiTV 这座小庙面试,而这个方面的确是很多朋友的弱项,我就略说几句。
请听题:一个使用 rail(或者 django,或者 express,…)和 MySQL 做的 API 系统,最近流量从 6,000 RPM 激增至 20,000 RPM,整个系统的压力骤升,现在需要在应用层设计一套缓存方案来降低整个系统的负荷。要求是:缓存方案不能在 web 层(包括 proxy)做,也不能使用 framework 自带的,或者第三方的缓存模块。
大部分的面试者一看,这问题简单啊,使用 redis(或者 memcached),加在应用服务器和数据库服务器之间,读取数据的时候如果没有命中缓存,则读取数据库并写入缓存,下次再读相同的数据时就能命中缓存,大大减轻数据库的压力了。
这回答对么?不好说。也许对,也许不对。但你要这么快抢答的话基本上就会被面试官毙了。
系统设计的面试重在讨论和交流,厘清一切限制条件,然后在这些限制条件下面找到一个比较合理的解决方案。它不是编程题或者算法题,弄清楚题目的要求就可以写开始答案的。
好的面试者应该主动发问,来尽量找全限制条件,而不是直接假设。拿上述回答来说,面试者还没开始认真分析问题所在,就想当然认为压力在数据库一侧(是的,流量激增之后 90% 的可能性都是数据库先扛不住压力,但这是假设,不能化作前提),从可能错误的前提出发,必然会得出一个很可能错误的解决方案。
所以比较对路的思考过程是:
现有系统的架构是什么样子?
作为一个已有系统的优化项目,不了解现有系统的架构,历史(演变的过程和演变的原因,当然,在面试中这个可以省去)而立刻上手设计都是在耍流氓。
web 服务器,应用服务器,数据库服务器等之间的关系以及数量?
现在有没有使用类似于 redis / memcached 的缓存服务器?如果有,它们用来处理什么任务?(如果有人问道这个,会有大大的加分)
目前哪个部分在系统中压力最大?
这个问题非常重要,你得需要先知道问题在哪再考虑优化。如果问题出在应用服务器,那么,可能需要做页面级的缓存;如果问题出在数据库服务器上,可以做数据级或者页面级的缓存。
我们希望达到一个什么样的 capacity?
很多有多年一线工作经验的面试者在这样一个系统设计中竟然不去考虑究竟需要一个什么样的 capacity,就进入到具体的解决方案,这样是不妥的。capacity 因问题而异,在这例子中,起码要考虑 1) 缓存系统每秒的处理能力 2) 缓存系统的容量。对于一个 20k PRM 的请求数量,缓存系统应该能承受 50k,100k,甚至 200k PRM 的请求。至于容量,应该考虑假设所有不同的请求都被缓存(worst case),需要多大的容量,在限定的软硬件条件下,是否能达到这个容量,达不到的话,什么样的上限比较合理。
有了这样一个目标后,你还需要对你要使用的工具有个谱。有一个面试者说用 redis 做缓存,因为 redis 很快。「很快」是个很虚的概念,我于是问这个面试者你觉得 redis 对于 1k 大小的value,在 commodity hardware 上做 GET 操作每秒钟的 QPS 是多少?对此,面试者一点概念也没有,我让他猜,他竟然给了个 3-5k QPS 的估值。我自己印象中 redis benchmark GET 操作大概是 100k 这个数量级,当然,每次返回 1k 大小的数据会拖累这个结果,但绝不会差出来两个数量级。
有了设计容量的概念后,我们需要知道要缓存数据的大小,这其中,median size,average size,max size 都需要了解一下,起码知道是什么量级。返回 2k 大小的数据和 200k 大小的数据的处理方式可能是完全不同的,假设你的缓存系统的容量是 1M,2k 数据大小的缓存直接占用的内存是 2G,而 200k 则是 200G,后者显然不能使用内存来做缓存,只能用文件系统缓存。
讲到文件系统,多说两句。用文件系统做缓存则需要注意 unix 的目录实际上是一个记录文件名和 inode 对应关系的 map(你可以 ls -ai1 . 查看)。单个目录下的文件越多,这个 map 越大,需要的读取次数就越多(一般系统调用会每次读 32k 或者类似的量级),所以当一个目录下的文件特别多时,访问效率会急剧下降。这是为什么常见的文件缓存系统都是用两级甚至多级目录,1M 个文件,一级目录使用两个字母或数字,可以有 (26 + 10) 平方个二级目录,也就是 1296 个目录,每个目录名两个字节,加上 inode 和其他一些消耗, 10-20 字节完全够用,一次读取就能获得所有二级目录,而二级目录平均是 772 个文件,一次读取也能完成,总共两次读取,找到缓存文件,而如果把 1M 个文件放在一个目录下,如果每个记录 32 字节,需要 1000 次读取。这种分级缓存的思路在很多系统中都能见到,比如 TLB(不过多级 TLB 主要是为了节省内存)。
设计是否有优化的地方?
如今,内存,硬盘已经非常便宜,很多时候我们做系统设计,已经不需要一个比特或者一个字节地去扣细节,但这并不意味着更好的,更省内存,更快运行的方案就没有价值。我曾在一个面试中和面试者讨论一个系统设计的优化,那个面试者对我「逼」着他优化算法很不理解,他认为 computation 这么便宜,钱不是问题,多加几台机器并行运算就可以了。这是一种错误的做事态度。
永远不要忘了设计应该是面向未来的,如果通过更换更好的算法,能节省数十倍的内存(bitmap vs hashmap),或者数十倍的运算(bloom filter pre-filter vs raw computation),那么你省下的不仅仅是当下的资源(或者金钱),还有未来的时间 —— 因为,当你的应用有10倍的流量时,你还能够应对自如。
此外,优化可能会从量变转化为质变。一个 analytics 应用如果每五分钟才能完成一次分析,一小时仅能进行 12 次分析;如果将其优化成 30 秒完成,一个小时就可能完成 120 次,用户可以更快地掌握趋势。
一个认为资源不是问题,钱不是问题的设计者,只能是一个平庸的设计者。
每个认真的程序员应该这样看待自己:In me the tiger sniffs the rose.