一些好的规则
英文原文:A Few Good Rules
什么是明智的标准化?
想象一下第一次和特别的人约会。当你到达最喜欢的餐馆时,所有的灯都熄灭了,你身处黑暗之中。奇怪的是,从厨房传来的声音又表明这里像往常一样正在营业中。你听到一位女服务员走来,等待着引导你到没有灯光照射到的座位上。你的同伴不知所措,并且有一点害怕。你是打算留下,还是找个正常点的地方吃饭?
Web应用就像餐馆一样,人们通过其所提供的体验对其进行评价。即使是短暂的中断也会影响服务提供商的口碑或服务水平。政策和指导方针在防止代价高昂的服务中断中扮演着重要的角色。不幸的是,它们也能导致不理智决策的产生,从而造成更大的损害。比如公司内“DevOps团队”的建立。这将导致所有的运维知识都被隔离在一个单独的团队中。尽管这样一个管理层指令可能预示着DevOps的到来,但它什么都不是。
工程师鄙视无逻辑的、官僚主义的规则。这些规则是前进的障碍物。然而,每家公司至少都会有一些这样的规则。在过去,可能有好的理由在一些问题上制定这样的规则。渐渐地,这些规则过时了。但是,规则制定者不能(或不敢)取消它们。当使用C++代码库时,由于历史原因,被告知不能使用STL;参与的Java项目被坚定地拒绝从1.4迁移到新版本。任何有过这样经历的人都明白有些措施可能会对生产力产生消极的影响。
我们应该忘记这些规则吗?
面对这些像障碍物一样的规则,我们都很想将它们废除。不幸的是,“无为”的公司通常都没有成功地废除它们。好的规则是一种重要的交流形式。这种形式关乎到长期策略、从过去吸取到惨痛教训、以及来自用户需求中的发现。理想情况下,一个组织制定的与时俱进的规则,可以帮助个人增强做出正确决定的信心。在实践中, 这样的情况真的发生过吗?
一家公司是否拥有真正有效的指导方针,Netflix就是一个很好的例子。至少通过阅读他们的博客和开源的代码会给人留下这样的印象。比如,即使没有和Netflix的任何员工聊过,我也能确定,“构建它,运行它”是一个他们如何把开发和运维结合起来的不错的想法。另一个明确的原则是:写代码是为了构建一个可靠的、可扩展的服务,而不是为了其他目的。他们开源了所写的大部分后台软件。这个事实比任何事都更具有说服力。
Netflix已经构建了Netflix内部Web服务框架(NIWS)。这是一个自定义的软件栈,用于创建可靠地运行在云上的内部Web服务。NIWS采用了一些不太流行的技术和不太常用的方法。使用这种与最佳实践背道而驰的方法需要有相当强的自信。毫无疑问,部分可以归因于落实的政策。这些政策让工程师可以不受限制地考虑问题。
Netflix的负载均衡
在Netflix如何挑战常规的例子中,我最喜欢的是他们是如何在NIWS中实现负载均衡的。面向客户的流量仍使用传统的负载均衡处理器(一个标准的Amazon EC2 ELB),但对于Netflix服务器之间的流量,他们选择了一个完全不同的方案,称作客户端负载均衡(client-side loadbalancing)。基本思想很简单:取消专门用于负载均衡的节点。这些节点用于在Netflix服务器间转发流量。客户端本身维持着一张列表,记录了可用的后台节点。当客户端发送请求时,直接与所选的后台实例交互。而这样就没有必要使用专门的负载均衡器。
客户端负载均衡并不是Netflix发明的。但是, 它是有名的公司里第一个在基础架构中完全使用这种技术的(公平地说,同一时期内,Twitter和Yahoo也在做基于相同概念的实验)。在多个后台服务器上做均衡的标准方法是:通过一个负载均衡器,如Amazon EC2 ELB,或者在服务器上运行类似HAProxy的软件。对于这么关键的组件,使用保守的方法和一种大多数工程师都熟悉的技术是很有意义的。但是,几乎没有公司在Netflix之前试验客户端负载均衡的方法。其真正原因是,他们甚至都没有考虑到这种方法。
对于从事大规模应用程序开发的软件工程师,每天都要和各种库和组件打交道。这有点像鱼和水的关系。在能使用一种特定方法成功地构建系统这么多年后(也许几十年) ,对已经经过考验的方法或者系统的构建模块提出疑问,这看起来是在浪费时间。在许多公司里,这些决定已经被写进政策中。这些政策基本上是不可变的。但是,Netflix采用了客户端负载均衡的方法,并因此取得了显著的成功。首先,他们从系统中移除了一个单点故障点(对于频繁地在没有警告下就停止服务的EC2实例,这是一个重大的胜利)。其次,通过将负载均衡的逻辑集成进客户端,负载均衡的策略可以参考客户端提供的信息。比如,考虑以下的负载均衡规则:
向客户端的EC2可访问区域(EC2 Availability Zone)中的可用节点发送请求。如果这样的实例不存在,则在当前区域中找一个运行已超过一天的实例替代。
传统的负载均衡器并没有被设计成用来执行这种自定义逻辑。它们也无法获取太多关于客户端的信息(比如一个客户端所属的EC2可访问区域)。自定义负载均衡逻辑变成了应用的一部分,使用相同的语言编写。这意味着编写代码的单元测试用例变得容易。而在传统上,这被认为是“基础设施的东西”。因此,这不仅让制定更复杂、更智能的决策成为可能;也使得人们对工作能如期完成更有信心。从某方面看,NIWS将DevOps带入了下一个层次:开发人员和运维工程师不仅坐在一起,在同一个团队中工作;而且他们使用相同的开发语言,向相同的代码库提交更新。
Prezi加入客户端负载均衡俱乐部
用一个内部的客户端负载均衡实现替代标准的负载均衡器,这种让Netflix受益的技术只适用于Netflix吗?不一定。在prezi.com,我们对内部流量也采用了这种技术。我们的一些应用服务器运行着若干个服务。当这些服务通信时,我们希望它们优先选择本地的服务实例,而不是向网络中发送请求。然而,如果需要访问的服务没有运行在同一台服务器上,那么就可以访问任何一个该服务的实例。对于Prezi,获得的好处是,尽可能地避免了网络流量、减少了在AWS上的支出和响应时间。目前运行于prezi.com产品上的负载均衡逻辑由以下的这段Scala代码实现:
override def choose(key: scala.Any): Server = Option(getLoadBalancer).map(lb => lb.getServerList(true).filter{server => server.getHost == config.getHostname && serverIsAvailable(lb, server) }).getOrElse(Seq()) match { case Seq() => super.choose(key) case matchedServers => matchedServers(0) }
Netflix的工程师可以设计出NIWS,而不用担心质疑当前技术所带来的后果。因为公司的规则授权他们这么做。即使任何人都可以获得NIWS的技术,只有那些有着类似思维的公司才能够使用这种技术去搭建产品。具体来讲,期望工程师基于技术价值做出决定的公司和完美主义的公司是无法利用这样的技术的。
Netflix验证(Netflix test)
期望所有的工程师在做决定时不受办公室政治、流行技术和害怕改变的制约,这是不可能的。然而,减少这些方面的影响,对确保开发不会误入歧途有很大的帮助。一堆武断的限制规定会让工程师的设计缺乏创新和效用。相比之下,一些好的规则限制了问题空间、明确了约束、改善了产品的质量。
基于NIWS栈的源代码,Netflix在决定如何实现一个组件时会考虑两件事:
- 这个组件发生故障的可能性及后果是什么?
- 当这个组件的设想场景发生改变时,是否容易修改这个组件的行为?
我将这些问题成为Netflix验证。这两个问题是紧密关联的。甚至可以说,第二个问题包含了第一个问题。这两个问题之所以意义重大,是在于他们如实地镜射出了Netflix的商业目标。这个目标就是提供可靠的、可扩展的服务。其他也有相同目标的公司也能从这个验证中受益。但是,这个验证的真正力量在于它没有提到的东西。它没有提到任何具体的技术或者供应商。
不适用于完美主义者
真正让我惊讶的是,Netflix的代码只专注于足够好即可,而无过之。别误解,目前我所看到的代码容易阅读,并且有很高的单元测试覆盖率。即便如此,我也没有预料到Netflix能专注在足够好这个级别。比如,在代码的许多地方,当后台线程启动之后,就再没有任何操作停止它们。这看起来有很大的问题,直到你意识到Netflix不在节点上进行软件升级。他们通过启动一个新的EC2实例集群来部署新版本的应用。当通过监控验证新版本应用运行正常后,就将老集群关闭。如果有人使用了这些部署工具(也是开源的),那么就没有僵尸线程的问题。然而,如果有人在一个像Glassfish的应用服务器上使用Netflix的库,那么每次重新部署都将会触发内存泄漏。
代码中包含大量单例模式的类,也是我未预料到的。当我们以一种Netflix未预见的方式使用一个NIWS库时,我们很快会发现自己在不断挣扎地使用错综复杂的技术来处理问题。包括使用多个类加载器。
最后,尽管wiki页面上关于代码的文档有很大的帮助,但是这样的文档太少了,很多细节都没有描述。通常,代码就是文档。有几次,我在github issue tracker上找到了一些解决NIWS相关问题的最佳建议。
我的许多同事,在第一次接触Netflix生态圈时都有点不知所措。他们的第一反应是谴责那些写代码的工程师未经训练或者太懒惰。“应该有一些规则关闭这些没用的线程”,我听他们这样说着。然而,对于Netflix,我们所列出的NIWS的缺点,都不算是一个真正的问题。用于处理线程关闭的时间被用在了其他更需要的地方。如果有人想要以不支持的方式重用代码,那么单例模式的类只是其中一个会遇到的问题。最后,尽管写文档是一件好事。但是,可读性高的代码和大量内部专业知识让文档成为了一个可选项。Netflix建立了关于线程管理、恼人的设计模式和最小化文档量的规则。通过建立这些规则,让工程师可以专注于其他主要问题。
事实上,我已经意识到Netflix的软件栈之所以成功,是因为它有着旺盛的精力。这不仅让Netflix“砍掉了软件栈的一些边角”的事实可以被接受,而且实际上也催生了一个更好的产品。编写了大量描述代码的文档还得保证文档不会过期,因为代码总是在不断的演进。编写不会用到的特性会使开发者失去动力、且难已为团队证明自己,对社区也没有什么好处,因为这部分代码不会在产品中被验证到。在Prezi,我们有一些一直想开源的项目。但由于缺乏时间加入一些我们希望的改进,目前还不能将它们开源。Netflix成功地开源了大量的代码却没有破产。因为它们一直专注于代码的可读性和单元测试,而不是加入过多的亮点,以及保证其不会过时。Netflix实施的这些合理规则,使得它的设计开发可以应对不断快速增长的用户;甚至是不断开源所写的代码。
因此所有的特定规则都不好吗?
如果用Netflix验证再形成一些指导方针,那么这些方针是相当通用的。例如,通过努力获得成功的名言,像“花10%的时间用于偿还技术债”,或者技术信息,如“0.6.1版的NodeJS使我们的Web应用变得不稳定,别使用它”。如果把从过去失败中总结获取的教训忘了,这难道不是一种浪费吗?
这样的建议,和最佳实践、知名的组件一样,是非常有价值的。在加速开发和简化系统的运维方面,通过多年的验证,这些建议已经获得了工程师的信任。比如在Prezi,大多数后台系统都是用Python写的,并使用了gunicorn web服务器、Django web框架和MySQL数据库。在公司的初期,这个软件栈使得开发者能够专注于新产品的特性上。多年来,“使用Django和MySQL开发服务”就如同“不要在周五下午3点后部署”一样明确。这些都不是Prezi成文的规则,但却早已在实行中。
随着注册用户数从0攀升到4000万,许多当初采用这个平台的实际情况都已时过境迁。比如,当所有的网站流量都由一个应用处理时,将所有用户数据存入一个MySQL数据库中是有意义的。如今,Prezi拥有许多独立的服务。这些服务对响应延时、可靠性和一致性上都有着不同的需求。许多服务运行在EC2上,将数据库当做键-值存储的容器,通过主键访问数据。第一年制定的技术指导方针,尽管在那时有用,但没有一条能帮助我们应对目前工程上的挑战。
只要标准的技术和特定的规则没有过时,就能够激发工程师的产出。问题在于,当这些特定的规则不再适用时,仍然被强制实施。
固定的接口集
对于已经过时的规范而言,一个问题(而且很常见)是软件接口的过时。我最喜欢的例子是Java Servlet API。即使它并没有真的过时!实际上,它是一个优秀的接口:直观、稳定、有完整的文档、很多不同应用服务器都是使用它实现的。
当Prezi决定探索JVM,将其作为我们可靠的Django栈的一个可选平台时,我们选择了一个轻量级的代理应用作为我们的试用项目。我强烈地表明应该使用Jetty和Servlet API,而不是团队考虑的另一个可选方案。这个方案使用一个不知名的Scala Web服务器。6个月之后,我们关闭了原有的代理程序,而用一个基于Spray(这个技术我是投反对票的)写的程序取而代之。部分原因是:对于我们的用例,使用它可以获得更高的效率。因为在我们的用例中,响应时间主要受发出的HTTP请求的响应延时的影响。我开始从代码层面思考:我们想要什么样的目标,想使用什么样的接口。我们如何写单元测试。开发者社区有多大。这正是Servlet API在抽象层面解决的问题。我本应该考虑(或谈论)关于它是如何利用硬件资源的。具体来说,瓶颈在于:处理请求时是否需要大量的CPU或者IO资源。由于在我们的用例中,大部分时间都花费在等待发出的HTTP请求的响应上,所以没有这样的资源要求。这就是代理程序的本质。鉴于我们的用例,使用Servlet的方法对每一个请求都创建一个专门的线程,不仅毫无必要地限制了处理请求的并行数,而且也无法高效地利用内存。
Servlet API不适用于这个问题的事实,并不能说明那些常用的接口或Java编程语言不好。数以千计的公司使用Servlet构建了令人惊叹的产品。其他编程语言也具有相似语义的Web服务器接口。这个故事想表明的是,我在使用特定的指导方针时脱离了实际的场景。接口是用来解决某一个特定问题的。当问题不再是你尝试解决的问题时,使用给定的接口不是一个好的选择(无论这个接口有多流行或者多新颖)。
DevOps的规则
DevOps能量来自于合作中的人有着完全不同的技能。相比于成员技能单一的团队;一个拥有各种不同技能的团队,包括长满胡子的系统管理员、函数式编程的狂热粉丝,更有可能构建出可靠和可扩展的服务。
成员技术背景的不同使得团队更加需要明确的规则。开发者不需要知道为什么使用的自定义Linux内核有着一大串的编译参数。类似地,不是所有人都需要担心代码中有多少单例模式对象的存在。“写shell脚本时必须添加shebang行”,或者“解析用户数据的代码要有单元测试”。像这样的标准适用于团队中的每一个人,并且会帮助到那些在特定领域内没有足够经验做好事情的人。特定规则只有被适当的使用,才会对团队产生积极的作用。
更通用的规则,像这些Netflix验证只适用于制定高层级决策,但是能够应用地更久。管理团队既需要通用的规则也需要高层级的规则。诀窍是要及时发现我们制定的规则是否已不再发挥期望的作用。
如果我们回到文章开头的那个餐馆,打开冰箱门,不同盒子上有着不同的保质期时间。有的可能几个月,比如番茄酱;有的可能几个小时,比如鱼。做饭要用到不同的原料,而每种原料有自己的保质期。保持原料的新鲜,使得最终做出的食物可口,这是一个厨师的责任。同样,不仅在我们决定要将什么进行标准化这件事上需要智慧,在及时发现我们的标准是否已失去意义这件事上也需要真正的智慧。