目录


前言

作为一名程序员,我一直强迫自己使用简单的方式解决问题,一旦方案变得复杂,我就认为一定会有更好的方案,只是能力有限没能想出来。

下面我们以搜索引擎的实现为例子,来感受一下简单之美。

建立一个搜索引擎大致需要做这样的几件事:自动下载互联网上的网页;建立快速有效的索引;根据相关性对网页进行公平准确的排序。

一个高质量的搜索引擎要实现这3件事都不容易,但是呢,搜索引擎的基本原理还是相对简单的。我们平时使用的计算机可以处理复杂的逻辑计算,但其底层采用的是最简单的二进制计数方法,它只有两个数字:0 和 1。二进制除了是一种计数方法外,它还可以表示逻辑的 “是” 与 “非”,这个特征在搜索引擎的索引中非常有用。布尔运算是针对二进制的,搜索引擎的实现,从根本上就离不开布尔运算的框架。

布尔运算具有以下特点。布尔运算的元素只有两个:1(TRUE,真),0(FALSE,假)。基本的运算只有3种:与(AND)、或(OR)、非(NOT)。其对应的真值表如下:

与运算-真值表

AND10
110
000

或运算-真值表

OR10
111
010

非运算-真值表

NOT 
10
01

先看一个例子:如果你是图书馆管理员,如何才能让读者快速有效的找到一本书,一种方法就是为每一本书打上索引标签,我们不可能在图书馆书架上一本本地找,而是通过搜索索引标签定位到它的位置,然后直接去书架上拿。

我们可以把一个网页比当做一本书,那接下来就要为网页创建索引,最简单的索引结构是用一个很长的二进制数表示一个关键字是否出现在网页中,每一位对应一个网页,1 代表相应的网页包含这个关键字,0 代表没有。比如关键字 “简单” 对应的二进制数是 0100000101…,表示第二、第八、第十个网页包含这个关键字。同样,假定 “架构” 对应的二进制数是 0100110011…,那么要找到同时包含 “简单” 和 “架构” 的网页时,只需要将这两个二进制数进行布尔运算AND。根据上面的真值表,可以得到结果为 0100000001…,表示第二、第十个网页满足需求,而计算机做布尔运算的速度是非常快的,所以我们使用搜索引擎时,能够快速的搜索到想要的结果。然而互联网上的网页数量是庞大的,由于这些二进制数中的绝大部分位数都是 0,只需要记录那些等于 1的位数即可。这样,搜索引擎的索引就变成了一张大表:表的每一行对应一个关键字,每个关键字后面跟着一组二进制数字,包含了该关键字的网页索引号(这种索引的结构设计,也称 “倒排索引”)。

当然,要设计和实现一个高质量的搜索引擎还是非常复杂的,但是原理上还是相对简单的,即等价于布尔运算。

所以,即使是非常复杂的系统,其底层原理的实现应该保持简单。在实际开发过程中,尽量的保持问题解决方案的简单,这有利于系统的维护和扩展。

本章和大家探讨一下如何把大事化小,从而达到事倍功半。下面从6个规则来分别讲诉“如何简化”,有的规则比较宏观,适用多种场景,有的规则比较微观,只适用特定场景。

规则1-避免复杂设计

  • 内容:在设计中要警惕复杂的解决方案。
  • 场景:适用于任何项目的设计中,特别是庞大而复杂的系统或项目。
  • 用法:当设计出解决方案时,通过测试同事是否能够容易理解和实现你的方案,来验证是否存在复杂设计。
  • 原因:复杂的解决方案在实现和维护上成本高(包括人力成本和时间成本)。
  • 要点:过度复杂的系统限制了可扩展性。简单的系统易维护、易扩展、成本低。

关于复杂设计,主要包括两方面,一是系统或项目结构的复杂性,一是业务逻辑的复杂性。

(1)结构复杂的系统可以归为具有两个特点:组成复杂系统的组件数量过多,同时这些组件之间的关系依赖过于复杂。如果组件的抽象程度和具体程度没有设计好,很容易面临牵一发而动全身的局面。
结构复杂有如下缺点:
a、组件越多,某个组件出现故障的概率也就越大,从而越容易导致系统故障。
b、组件关系依赖越复杂,当某个组件有变动时,会影响所有和它有关系的其它组件,同样这些被影响的组件也会递归性的影响更多的组件。
c、定位一个复杂系统的问题比简单系统更加困难,在排查问题时,需要对所有相关组件都要逐一排查,降低解决问题的效率。

(2)逻辑复杂的系统的一个典型特点是单个组件承担了太多的功能。以电商系统为例,常见的功能有:商品管理、库存管理、订单管理、用户管理、支付、物流、客服……如果把这些功能都放在一个组件上实现,可想而知,这个组件简直就是一个巨型、笨重的巨无霸。逻辑复杂的另外一个典型特点是采用了复杂的处理算法,复杂处理算法导致的问题主要是理解上,从而导致难以实现、难以维护。

复杂设计是可扩展性的大敌之一。设计的越复杂,问题也就越多,越不易于后续的扩展。有一个好方法可以验证解决方案是否过于复杂,把解决方案展现给不同的技术团队,如果每个技术团队都能够轻松理解方案,并可以向其他人描述该方案,那就可以采用该方案,如果有其中一个技术团队表示不理解,那就要针对该方案是否过于复杂而进行辩论或调整。所以架构设计时谨遵一句话:Keep It Simple! 如果使用简单的解决方案就可以满足需求,那就选择简单方案。

规则2-方案中包含可扩展

Design(D)设计20倍的容量。
Implement(I)实施3倍的容量。
Deploy(D)部署1.5倍的容量。

我们经常遇到的一个问题是 “什么时候该在扩展上投入”,有些轻率的答案是,最好是在需要的前一天投入和部署,因为这样才能使得这笔投资的价值最大化,用最少的钱换取最大的收益,有助于公司财务和股东利益最大化。但是很遗憾,这是不现实的,及时投入和部署根本就不可能,而且会带来很大风险,影响及时投入和部署的因素有:无法确定的具体时间、无法预测的具体流量、无法感知的竞争波动等。
我们虽然不能做到及时投入和部署,但是我们尽可能的靠近 “实时”。这里采用AKF合伙公司在思考可扩展性时用的DID(设计-实施-部署)方法。这3个步骤和总所周知的认知阶段一致:思考问题和设计方案,为方案构建系统和代码实现,最后是安装和部署。
表:扩展的DID过程

 设计实施部署
扩展的目标20倍 ~ 无限3 ~ 20倍1.5 ~ 3倍
智力成本低到中
工程成本
资产成本低到中高到很高
总成本低到中

(1)设计(D,Design)

在项目的设计阶段,讨论和设计很明显要比我们在代码中实现该设计的成本更低,我们可以未雨绸缪,讨论好和草拟好如何扩展平台的设计。例如,最开始我们明显不想部署比现场的生产环境高出10倍、20倍甚至100倍的容量(这里的容量指的是系统的可扩展程度,包括:流量资源、计算资源、存储资源等),由于在设计阶段考虑扩展维度的成本比较低,建议在DID的D(设计)阶段聚焦在扩展到20倍到无限大之间。我们需要时刻进行 “头脑风暴”来思考和讨论 “大问题”,所以我们的智力成本很高。然而,我们不编写代码和部署昂贵的系统,所以工程成本和资产成本较低。最后我们归结到D阶段的总成本是低到中,所以我们应该好好利用该方法,在设计阶段发现和确定需要扩展的部分。

(2)实施(I,Implement)

设计方案确定后,我们可以开始编写代码实现设计。实施阶段是实现具体细节的阶段,建议在DID的I(实施)阶段实现当前规模的 3 ~ 20倍。“规模”在这里是指扩大最大瓶颈的系统组件,用以实现业务目标。可能在有些情况下,把一个组件的规模扩大100倍的成本与扩大20倍没有区别,如果是这样,我们可以一次完成该扩展的改变,而并非反复折腾。如随着用户数量的逐渐增大,平台需要基于用户属性(用户ID)取模来进行分库存储,把用户分散到多个(N个)数据库。我们可以定义一个可配置的变量User_Mod,其取值范围是1(现在) ~ 5(预测3年内使用5个数据库存储),如果经过1年后,发现用户的增长速度大于预期值,则可以把User_Mod的取值范围改为1 ~ 10,这样就达到可以轻易的扩展规模。这种改变的实施成本确实不会随着规模N的的变化而变化,这类改变以工程成本计算很高,以智力成本计算中等,以资产成本计算低到中。最后我们归结到I阶段的总成本是中,该阶段主要涉及到代码的具体实现,需要把代码架构涉及为易于扩展,尽可能的使用可配置的方式达到扩容。

(2)部署(D,Deploy)

DID的最后阶段是部署(D)。仍以前面的用户取模为例,我们希望以及时的方式部署系统,没有理由因为资产空闲而稀释股东的价值。该阶段的资产成本往往是最高的,部署相当于现有规模100倍的系统将会使很多公司破产,建议在该阶段的扩容提高到1.5 ~ 3倍(根据实际情况而定,如果是超高增长的公司,可以考虑提高到5倍)。然而,我们没有必要把33%甚至更多的的资源放在那里等待突然爆发的用户活动,云计算就是一个用来应付突发请求的不错选择。记住,扩展要具有弹性,它既可以扩展也可以收缩,随着用户流量的变动而调整容量。

对于可扩展的设计和思考的成本相对较低,因此应该经常进行。对于实施阶段的成本是中等,应该考虑到代码架构的实现和扩容,尽可能的实现可配扩容。最后,就是部署阶段的基础设施的扩容,根据需要提前做好设备的订购准备,建议使用云服务,在接近所需和接近实时的情况下,快速的把所有服务部署运行起来。

规则3-3次简化方案

采用帕累托(Pareto)原则简化范围。
考虑成本优化和可扩展性来简化设计。
依靠他人的经验来简化实施。

规则1是关于抑制某些方案过于复杂的冲动,而规则3这是关于采用什么方法来进一步简化方案,主要从3个方法来简化:如何简化方案范围?如何简化方案设计?如何简化方案实施?

(1)如何简化方案范围
对于这个简化问题的答案就是不断的应该用帕累托原则,也就是二八定理。如:“收益的80%来自于20%的工作”,也就是说“你收益的80%是由哪些20%的功能实现的?”,做的少同时取得显著的效益。也就是说要学会做“减法”,删除掉一些不必要的功能,最开始设计项目和产品时,避免设计的又全又大,把所有精力和注意力都放在最主要的功能上面。这样的好处是系统将会减少功能之间的依赖关系,可以更高效和更高本益比的进行扩展。借用马蒂·凯甘的理念 “最小化可行产品”,秉持着 “You Can Always Do Less”来简化方案范围。

(2)如何简化方案设计
简化设计于过度设计的复杂性密切相关,消除复杂性也就是忽略无关要紧的模块。简化设计是基于具体场景而定的,如:为了让请求及时响应,可以先在缓存中获取数据,缓存中没有再从数据库中获取,这就是缓存的应用。为了统计某个文件的单词频率,看起来可以使用流行的MapReduce算法来处理,但是如果只是一个小文件,就不必要使用重量级的MapReduce算法,直接使用一个简单的程序来实现更有道理。简单的说,简化设计的步骤要求易于理解、低成本、高效益和可扩展的方式来完成工作。

(3)如何简化方案实施
方案实施是方案的代码实现,这个也是基于具体场景来定的。如:某个问题,是使用递归还是循环来实现?某个解决方案,是自己研发还是使用开源项目?这些问题的答案都指向了一个公共主题:如何利用其它经验和已存在方案来简化方案实施。考虑到成本问题,我们应该首先寻找被广泛采用的开源项目满足需求,如果这些不存在,应该寻找本公司或其它组织内有类似解决方案经验的人来解决问题,如果都不存在,才考虑是否要自己研发来解决问题。

规则4-减少域名解析

  • 内容:从用户角度减少域名解析次数。
  • 场景:对性能敏感的所有网页。
  • 用法:尽量减少下载页面所需的域名解析次数,但要保持于浏览器的并发连接数平衡。
  • 原因:域名解析耗时而且大量域名解析会影响用户体验。
  • 要点:减少对象、任务、计算等是加快页面加载输的的好办法,但要权衡浏览器的并发连接数。

本规则让我们在用户的浏览器上考虑简化的问题。当你浏览一个网页时,使用浏览器的开发者工具(火狐firebug、chrome开发工具等)就会观察到一些有趣的结果。你会发现网页上的对象(html、图像、js、css等)的下载时间各不相同,而有一个额外的步骤就是域名解析。域名服务系统(DNS)是互联网中最重要的部分之一,它的作用是把一个域名(www.google.com )解析为对应的IP(x.x.x.x)。事实上,几乎所有的网页都是由许多不同的对象组成,浏览器就是基于此,通过并发连接同时下载过个对象。浏览器对每个服务器或网关代理的最大持久并发连接数有限制。根据HTTP/1.1 RFC协议,这个最大值应该设置为2。但是,现在许多浏览器都忽略此限制,把最大值设置为6或者更大。

由此可见,网页上的域名解析次数越少,网页下载的性能就越好。但是把所有的对象都放在同一个域中会带来问题,那就是浏览器对同一个域的并发连接数做了限制。如何权衡域和浏览器的并发数量,我们在下一个规则中来探讨这个问题。

规则5-减少页面目标

减少或者合并对象,但要平衡最大并发连接数。
寻找机会减轻对象的重量。
不断测试确保性能的提升。

2009年谷歌发布了一份白皮书,声称测试表明搜索延迟如果增加400毫秒将每日搜索量减少大约0.6%,可以看出网页下载时间对于保住流量的重要性。一个网页包含许多不同的对象(html、图像、js、css等),浏览器会分别独立下载这些对象,而且是以并发的方式下载的。改进网页的第一个方法是减少对象的数量。大多数网页中下载比较耗时的是图像对象,如果一个网页的图像对象过多,如上百个,该网页的平均响应时间超过12秒,那该网站已经面临着损失有价值的流量。一个好的方法就是使用图像精灵,把一些小图像组合成一个大的图像,通过CSS来单独显示其中的任何一个图像,这样图像的请求数量会显著减少。当然,不是说把网页对象删除的越多越好,必须在把重要信息传达给客户的前提下,做好简化工作。

从上一个规则中,我们知道浏览器支持从单一域名同时下载多个对象,如果所有的元素都集中到一个对象中,那么浏览器的并发下载的能力就无法起作用,我们需要考虑把这些对象拆分为多个较小的对象,以便同时下载。

流量器对每个域名服务的并发连接数存在的资源限制。如果网页上的多有对象都来自单个域名,那么浏览器设置的最大连接数是多少,支持同时下载的对象就是多少。因此,最好把网页的内容拆分为合适多的对象数,以便利用浏览器的并发下载功能。有一种技巧就是把不同网页对象分别存储在不同的子域名上(例如:google.com, static1.google.com, static2.google.com)。浏览器把这些当成不同的域名对待,允许每个子域名拥有自己的最大并发连接数。

理想的网页应该有多少个对象以及多大重量没有绝对的答案。提高网页性能和可扩展的关键是测试。这需要考虑很多因素,如尽可能的保持网页足够轻,在网页必须很重的情况下,采用Gzip压缩以减轻网页的传输压力,利用浏览器缓存、缩小文件、延迟加载等优化技术。总之,页面上的对象越少性能越好,当必须与其它因素平衡,通过不断测试和调整来优化达到最佳效果。

规则6-采用同构网络

不要混合使用来自不同OEM的交换机和路由器。
购买或者开源的其它网络设备(防火墙等)。

几乎所有的网络设备供应商都声称在他们的设备上实现了标准协议,允许来自不同供应商的设备之间进行通信,但是许多供应商也在设备上实现了专有协议,如思科的增强型内部网关路由协议。做过网站开发的就知道同一个网页在IE、Firefox和Chrome等不同浏览器的上的呈现效果就会不一样,这就是按照同一标准的不同实现上会有多么的不同了。因此,建议使用同一供应商的网络设备,这样就避免出现兼容性的问题。

总结

本章主要围绕简化这个主题来探讨了6个简化规则:

如何防止复杂性(规则1),

如何尽早考虑扩展(规则2),

如何从初始需求到最终实施做出简化(规则3),

如何减少域名解析次数和减少页面对象数量(规则4和5),

如何保持网络的简单和同构(规则6)。