Skip to main content

A Little Throught About Microservices

· 22 min read

知乎在 4 年前已经开始尝试服务化,至今也经历了好几个架构的变迁演化。我大约是 2013 年开始在知乎负责服务化的工作,对服务化的理解也从最初的模糊逐渐变得清晰,前段时间看了一篇叫做 Microservices - Not A Free Lunch! 的文章,也想趁着这个机会梳理总结目前为止我的一些感悟和想法。

SOA 与 Microservices

SOA(Service Oriented Architecture)是一个很「古老」的概念,而 microservices 似乎是这两年才开始流行起来的。很多人把 microservices 看作一个全新的概念(我们都是喜新厌旧的人),Martin Fowler 觉得它跟 SOA 差别非常大,Netflix 也把他们目前的架构称作 microservices architecture。但是有人站出来 microservices 根本就是 SOA 很多年前已经提出的概念嘛,甚至还有一份相当冗长的标准文档,SOA 在互联网界的流行很大程度上可能也要归功于 Amazon。在 microservices(这个单词真的好长。。)这个名词流行之前,我对服务化的理解一直就是 SOA,不过我并不是想说 SOA 跟 microservices 是两个完全不同的东西,对于后者我的理解是它是 SOA 的一个 dialect,很多核心的思想还是来源于 SOA,只不过随着时代的发展必然会产生差异(也可以说是标准制定得太慢)。至于 microservices 的标准定义,我想目前应该没有,就连 Wikipedia 的条目也讲得不清不楚(还不如看前面提到的 Netflix 的文章,里面与 SOA 比较的文字我也觉得有待商榷),每个人、每个团队、每个公司都应该有自己的理解,后文提到的知乎目前的服务化架构姑且用 microservices 指代。

Microservices 的代价

服务化的好处可能很多人都了解了,你可以在任何一篇相关文章中很轻易地找到关于服务化的各种优点,很多人选择服务化的时候也正是被这个「看起来」很美好的概念打动。一切模块都是天然解耦的,这简直就是软件工程的理想境界。但凡事有利必有弊,告诉你这个东西很好的人并不一定会告诉你背后隐含的一些注意事项(所以我特别欣赏那些可以把不管优点缺点都告诉你的开源项目)。文章开头提到的那篇文章就讲述了几个在实践过程中才会真正发现的「问题」,我也大概循着作者的思路,以及附上其它一些在工作中体会到的事情。

显著增加运维(DevOps)成本

这里的成本包括人力和物力成本。先说说物力,在服务化之前,一个项目的所有代码应该都在一个代码仓库里,在部署的时候很自然地我们把代码 clone 下来,可能还会编译打包,最后把整个项目放到生产环境。采用服务化意味着你的项目可能会从一个变成几十个(曾经有新同事来了之后惊讶于知乎内部居然有这么多项目,其实里面有很多都是一个个小的服务),想象一下此时你的部署流程会变成什么样子?当然我们并不会每次部署都要把这几十个项目挨个部署一遍,但最坏情况下你需要关心的项目的确变多了。比如所有项目依赖的一个特殊的服务有变化,需要依赖方重启,这将会是一场「浩大」的工程,不同项目大部分情况下拥有不同的维护者,通知到所有人并且完成这件事情本身就变得比较困难(难度取决于团队大小,当然这个例子并不会是经常发生的事情)。

在 microservices 的思想里不同的服务应该拥有完全「独立」的资源,包括代码、机器、存储等,理论上每台机器应该只运行一个服务,存储也应该只供这一个服务读写。如果再考虑不同服务的负载和高可用,那么需要为每个服务分配 2 至多台机器。此时从运维角度上来看已经增加了「数量庞大」的机器,不过考虑到成本问题,我们可能会把服务都部署在虚拟机里,而单个存储实例也可能是被多个服务共享。但这只是物理机器的数目变少了,实际上需要管理的机器还是很多。不过现在越来越流行的容器(container)的概念也许是一个不错的解决方案,有效利用了集群的资源,同时还能做到自动伸缩(auto scaling,前提是你的服务必须是无状态的)。

有了这么多服务,找到它们变成了一件困难的事情。这个时候我们需要一个 proxy,它的功能很简单,帮你找到你想使用的服务,再高级一点的,也许还会帮你完成负载均衡。但有网络必有开销,即使是内网,何况还是单点,有一天你会发现某个服务的调用量已经大到无法忽视 proxy 带来的网络开销。于是我们把一个 proxy 变成多个,来分担压力。但维护这些 proxy 的信息其实也是一件麻烦事,服务的机器可能会调整,可能会有很多新的服务出现,对于运维来说是不容忽视的成本。也许你在某个地方看到了另一种方案,我们其实可以不需要 proxy,直连服务岂不更好?直连减少了多余的网络开销,同时也意味着你需要自己做负载均衡和高可用,以及发现新的或者死掉的服务。其实很多人已经想到了一个解决的办法:服务发现(service discovery)。这是一个已经很成熟的方案,你甚至可以找到实现,这里有一篇文章比较详细地介绍、对比了服务发现相关的技术。当然这些开源实现各有利弊,也许最终你会选择自己开发。但服务发现终归引入了一个新的概念,意味着你需要单独为它部署、配置、管理,也许还会与你的代码耦合。

另一个必须关心的事情就是监控,当然你说监控本来就是运维需要做的事情,但无形中增加了这么多机器监控肯定值得关注。并且监控不仅仅是指服务是否正常运行,还包括服务的请求量、负载、响应时间,这些都不是现成的,需要额外统计。

然后就是人力成本。前面提到的架构已经比服务化之前复杂了许多,这也许就不是一个人能完成的事情。还有很多组件并不一定是现成的,于是运维同学还需要具备一定的开发能力,DevOps 这个称谓其实是一个蛮高的要求。伴随而来的就是招人的标准也得提高,考虑到我们是家小公司,技术团队规模也不会太大,必须在招人上做出取舍。

接口

有了服务之后接口变成了一件很重要的事情。我们需要制定一些接口规范,讲究一点的可能还会要求命名风格;需要考虑接口的粒度,不能过细,尽量通用;不能让接口的使用者对服务内部产生太大影响,比如调用一个非常消耗服务资源的接口,这时服务的开发者就需要对接口参数进行必要的检验;最重要的,接口一旦发布,之后的任何改动都必须向后兼容。Protocol Buffers 就是一个很好的例子,因为是强类型,所以接口参数验证可以很方便地完成,Google 还给出了一套更新接口的准则,例如新增的参数必须是 optional 或者 repeated,不能删除 required 参数。但有时候难免会做出不兼容的改动或者发布了新的接口,这时就需要告知所有服务的调用者。但你会发现找到服务是一个难题,找到服务的调用者其实也是一个难题。糙一点的可能就是发邮件给所有人或者通过经验来逐一排查,智能一点的就得在服务的框架里做些统计,自动生成服务的调用关系图。总之接口是一个你不可避免需要考虑的问题。

重复逻辑

软件工程一个比较重要的思想就是要避免重复代码,有这样一句耳熟能详的话:当你第二次写下同样的代码的时候就得思考是否可以抽象出一段新的代码。在一个项目里这件事很容易,可以是封装好一些函数、mixin 或者类。服务化之后有好几种方案可以选择:

  • 抽象出一个新的服务
  • 把这段逻辑封装为一个库
  • 管他的,我们就直接复制粘贴了吧

每一个其实都有优缺点,挨个说一下。抽象新服务有滥用服务化的嫌疑,并且新的服务意味着更多的网络开销,多个服务也跟这个新服务显式地绑在了一起,稳定性有待商榷。封装库少了刚才提到的不稳定因素,但同时带来了维护成本,只要维护过库的同学应该都了解版本更新是一件很麻烦的事情,在迭代速度上肯定要逊于第一种方案。最后一种,嗯。。就像武侠小说中的锦囊一样,不到万不得已千万不要用。目前我们更倾向于第二种。

分布式系统带来的复杂性

服务化打破了长久以来的三层架构(3-tier architecture),有人称之为四层架构。分层在软件工程里是一件好事,可以有效减少单层实现的复杂度,但同时也会给整个系统产生额外的代价。网络开销、网络的不稳定性、架构的容错性、消息的序列化和反序列化、不同服务之间负载的变化等等。四层架构里多了很重要的一层「服务层」,这一层内部的网络通信需要与上层隔离,客户端需要对服务的某些异常进行捕获,必要的时候重发请求,服务如何做到 graceful 部署,分布式事务(如果你真的需要事务),不要因为某个服务挂掉而导致整个系统宕机,序列化是采用二进制还是 JSON,序列化程序的性能如何,服务层内部又如何分层,如何避免循环调用。前面这些都必须考虑。

还有一个问题可能很多人刚开始并不一定会想到,那就是分布式系统的整体跟踪(tracing)。这是干嘛的?当一个问题出现时,你需要准确判断是哪一层出了问题,而不是靠猜或者逐一排查;当你需要优化整体性能时,你需要判断是哪次调用拖了后腿。早在 2010 年 Google 发表了 Dapper 的论文,之后 Twitter 开源了他们的实现 Zipkin,目前知乎也是在 Zipkin 的基础上针对我们自己的服务化框架定制了一套 tracing 系统。

系统架构的变化也会影响到开发者的某些设计,在以前这就是一些普通的函数调用,我们可以自由地控制调用的顺序、处理相关的异常,现在我们需要考虑到网络调用的因素,时序性也是一个问题,什么时候需要重试,什么时候又不行。某些问题是一个好的服务框架可以解决的,但某些不可以。

异步

由于众所周知的原因,Python 的多线程并不是一个效率很高的方案(事实上多线程本身也不是一个很好的方案),于是异步大行其道。但是 Python 的异步毕竟不是语言级别的,虽然有很多实现,但都不是特别好用(Python 3 这货也不知道要何年何月才能普及)。某些异步实现也会带来编程习惯上的改变,在使用的时候需要特别注意,否则可能会遇到一些看似「莫名其妙」的 bug。异步也不是银弹,当一个服务既有异步请求又有同步请求的时候,异步请求的性能反而会因为同步请求变差,因此一个服务最好是完完全全的异步。

开发与测试

这可能是比较容易忽视的一块,毕竟是给自己用的东西,不好用也许还可以忍忍,但我觉得这反而是最影响开发效率的环节。当一个项目的运行需要依赖十几个、几十个服务的时候,开发、测试就成了一个难题。我们也许可以分别在开发环境和测试环境将这些服务部署好,但是维护这些服务的可用性和稳定性会变成新的问题。比较理想的情况是项目的开发和测试不依赖任何第三方服务,从单元测试的角度上来讲你也只需要测试自己代码的逻辑就够了,这也许就得从服务框架的角度入手,不管开发还是测试都要保证接口正常调用,必要的时候把接口 mock 掉(但不能滥用 mock),不过集成测试还是不可避免大量服务的依赖。

RPC 框架

知乎从一开始就没有使用 Thrift 这样现成的 RPC 框架,而是基于 Protocol Buffers 自己搞了一个,后来又有了 JSON 序列化的框架,也逐渐从 Python 版扩展到 Node.js、Java 等语言。对于使用 HTTP 协议或者现成框架的团队来说可能这不是什么问题,但对于我们来说几乎是从零开始。凡事有利有弊,但知乎技术团队还是更倾向于简单的解决方案,服务化是一个生态,每个组件都需要完成自己的工作,框架可能是其中的胶水,把各个组件连接起来。2015 年我们会继续完善最新一版的 RPC 框架,同时还有整个服务化的生态。

组织结构与 Microservices

这是一个比较有趣的话题,我也是在看 Martin Fowler 的文章时才第一次了解到。引用一段著名的理论——Conway's Law(这个理论因《The Mythical Man-Month》而得名):

Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization's communication structure.

Conway's Law http://www.melconway.com/Home/Conways_Law.html

翻译过来就是:一个组织设计的系统(广义的指代,并不一定指软件系统)往往就是公司管理组织结构的翻版。在大公司通常是这样的,设计师、产品经理、工程师、测试、运维分别属于不同的团队,但是他们又共同负责一个产品,于是这个产品可能就变成产品经理想好需求,设计师负责界面交互,弄好之后交给工程师,工程师弄好之后交给测试,测试通过最后交给运维部署,软件架构上每个角色负责的东西都是独立的。而 microservices 更加强调小团队,每个团队的成员可以承担多种角色(感觉跟敏捷开发好像),microservices 往往又跟 CICD 联系紧密,因此这样的组织结构能够更加契合新的软件架构。前段时间知乎内部也有关于这个话题的讨论,最后觉得软件架构和组织结构其实是相互影响的,任何一方不契合另外一方,都会造成这两个的融合。

一点总结

写了这么多,并不是想说服务化有多么可怕,相反,在我看来在如今移动互联网的时代,microservices 架构是一个趋势,但每个团队应该根据自己当前的需要合理选择服务化架构,这个架构可以很复杂也可以很简单,没有必要完全相同,适合的就是最好的,这绝对是软件工程第一准则。

相关文章