微服务

微服务这个话题也算有段时间了,而且并不神秘,很多公司都在走这条路。

有条推是这么解释微服务的,很有趣,引用一下:

@arungupta Microservices = SOA -ESB -SOAP -Centralized governance/persistence -Vendors +REST/HTTP +CI/CD +DevOps +True Polyglot +Containers +PaaS

从微服务的角度看,Docker意味着什么?这两种技术之间应该是什么关系呢?

我认为,Docker和微服务的关系应该是——好基友 :-)

微服务的要点是“微”,与之对立的另一方是所谓的单体架构(monolith)。在团队实践中,这两种思路在不同的方面体现出了优劣差异。

  • 应用开发
    • 微服务便于支持多元技术栈
    • 单体架构有利于IDE和其它开发工具的配合支持
  • 组织结构
    • 微服务便于团队分裂,促进局部功能的业务深入
    • 单体架构有利于开发者从全局角度理解和掌控功能
  • 系统演化
    • 微服务能够有助于用“碎片化”的方式推动系统演化,降低变更风险
    • 单体架构便于”整体交付“,可以减少向下兼容的需要
  • 运维相关
    • 微服务增加了运维单元数量,对自动化运维比较依赖
    • 单体架构可以手工运维,或者配合简单的自动化发布工具

其实,微服务架构还有符合单一职责原则和便于接口依赖等好处,不过这些和Docker没什么关系,是单独在设计环节有价值的优点,所以这里就不提了。

简单来说,微服务区别于单体架构的地方就在于“分而治之”,即通过切分服务以明确模块或者功能边界。然而,仅有“分”是不行的,软件系统是一个整体,很多功能来自若干服务模块的配合,因此必然要有“合”的手段,这对矛盾会体现在多个方面,我们分别说明。

应用开发

之前已经讨论过语言技术栈多元化的趋势,但是,对于单体应用来说,多元化技术栈并不是值得推荐的实践方法,因为这里涉及到混合语言编程和不兼容的软件组织方式。实际上,现实中一些团队之所以没有办法拥抱多元化的技术栈,往往首先是因为他们的系统是一个或者几个“单体”应用,开发者已经习惯了原有的IDE和相关开发工具,引入其它技术带来的好处还不如制造的麻烦多。

微服务则可以很好地避免这种情况,它通过切分系统的方式为不同功能模块划定了清晰的边界,边界之间的通信方式很容易可以做到独立于某种技术栈,因此也就为纳入其它技术带来了空间。

现实中也可以看到这样的例子,一些公司在初期会固定一个技术选型(比如facebook用php,google用python,阿里巴巴用java),而发展(或者收购)新的部门和组织以后,要么原有的应用被分裂,要么新的业务催生出新的独立应用,这时往往逐渐开始扩展技术选择。

有拆就有合,不同技术栈的微服务之间,除了需要考虑通信机制,还要确保这些技术能够(以较低成本)变成一个系统——不同服务可以使用不同的语言框架,但是在线上应当成为一个整体。于是我们会在发布中遇到“合”的困难,而这正是docker能解决的问题,具体讨论之前已经展开过,这里强调一下结论——docker将所有应用都标准化为可管理、可测试、易迁移的镜像/容器,因此为不同技术栈提供了整合管理的途径

在这种情况下,开发人员可以自由选择或者保持自己的常用工具,不必因为微服务的分裂产生过高的学习成本。

组织结构

说到团队和组织,不能绕开的一个话题就是“康威定律”(Conway's law:软件系统的结构受制于其生产者组织的沟通结构)。从这个角度看,微服务的拆分会对团队扩张带来帮助,这不难理解,因为系统拆分为若干微服务会促进这些微服务之间的边界更清晰,我们知道,边界清晰等于在边界之间协作信息量少,如果按照微服务拆分团队,团队之间的协作成本将是比较低的。

然而,“边界之间协作信息少”是有代价的,这代价就是团队的每个人对系统失去了整体视角和掌控能力,在这一点上,单体架构显然要好很多——每个开发者的开发环境都有完整的系统构建,所以很容易就可以获得对系统的整体印象和理解。

这是微服务的短板,但是这个问题要分两种情况考虑——

  1. 目前的技术发展使得在本机搭建一个完整的系统成本越来越高,即使不考虑微服务的影响,也会因为其它因素而推高这个代价。比如:一个普通的Web应用也许会购买push.io这样的手机端推送服务,因此期望本机能够重现所有的系统功能有时并不现实。
  2. 微服务架构在这方面的短板,其核心在于构建成本,由于微服务来自不同团队和部门,因此如何搭建它就成为一个,同时由于不能低成本的获得一个完整的系统,系统整体的知识也就容易被开发者忽略,最终导致整体视角缺失。

问题不同,处理起来也是各异——

  1. 对于关系不大的其它服务,可以保持我们常见的与外部服务进行协作的方案——要么单独申请(或分配)开发测试用的只读账号,要么进行mock,不影响系统的整体性即可。
  2. 对于大多数外部服务,我们需要考虑建立自动化系统构建和测试的方法,这是微服务架构带来的研发挑战。

显然,方法一是绕道而行,适合少数场景,方法二是正面强攻,能够应对绝大多数情况。这个方法二就是Docker可以发力的地方。

笔者之前曾经在公司内部做过持续集成平台,服务的研发团队不少,其中有些虽然没有提出微服务的概念,但是“将系统功能服务化,然后再整合”的做法其实已经有很多运用了,于是我们也在建立自动化联调和测试机制。

大致的作法是每个服务说明自己如何构建(给出构建脚本),并申明依赖的外部服务(运行时依赖,不同于软件包的依赖),然后由CI系统进行全局构建,这种做法非常好的节约了联调时间,并使这一工作变得可重复。

遗憾的是,由于没有Docker这样的技术,构建这件事很难做到“整齐划一”,为了让它们相互配合,需要编写一些协作的脚本,这加重了研发团队的工作,因此真正能够自动联调的应用系统并不多。

微服务架构中的一个系统往往是运行时的多个系统,而多系统联调通常费时费力的,这不仅是由于编译时间往往很长,更由于多系统的构建过程往往互不相同,服务之间的依赖往往又不太简单,所以自动化的成本很高。但是,如果首先对各系统进行Docker化,就很容易通过统一的docker build,建立一致性的构建服务,再结合compose等基础设施处理服务依赖,这些工作最终就可以产生一个平台,(自动化的)将被微服务打散的整个系统再构建出来(由于使用了微服务,构建速度在理论上就可以是并行的,因此甚至会比单体架构更敏捷)。

这个思路最有意思的地方在于,建立这样的基础设施,是可以与具体公司的技术路线无关的,因此实际上可以构建独立的服务平台为多个公司提供服务。

可能有人觉得这个平台很像一个PaaS,确实,这么发展下去有可能演化出一个独立的PaaS平台,不过它可以做的更多,而且没有传统PaaS那样对技术进行限制,这是一个很有吸引力的方向,也是很多Docker创业公司可以做的事情。

系统变更碎片化

理论上,由于进行了分解,微服务架构的系统应该更加有利于系统的“改良”,不必动辄就伤筋动骨甚至另起炉灶。

但是实际上并不一定会这样。

微服务架构是一种思想,它的合理运用还是要依靠团队成员的,如果是从“单体”应用演变过来的系统,团队成员很容易感受到相反的体验——系统升级更复杂更难了。

比如下面这个变更(以java应用为例):

- public List<User> getUser(String id)</s>
+ public List<User> getUser(Long id)

对于单体应用来说,开发过程就是使用IDE的refactory功能变更一下,上线也很简单,更换一下war文件即可,而对于微服务来说就复杂了。

微服务的开发过程可能是这样的:

  1. 变更接口,发布新的接口jar包。
  2. 找到所有使用getUser(String id)这个接口的调用方应用。
  3. 升级调用方的pom.xml文件,修改相应代码,提交、测试,但不上线。
  4. 同时修改服务提供方的代码,对新接口进行实现。

而发布过程是这样的:

  1. 调用方服务停止。
  2. 服务方服务停止。
  3. 服务方服务升级软件包。
  4. 服务方服务启动。
  5. 服务方验证服务是否正常。
  6. 调用方服务启动。
  7. 调用方验证服务是否正常。

这种操作十分脆弱,一旦发现服务上线失败就会陷入两难,有时会导致开发人员在线上解决问题,更进一步引入了风险。

当然,这个问题其实有标准解决方法——向下兼容,也就是每个服务的升级都至少兼容之前一个版本,这样所有的依赖服务的升级就可以灵活进行而不必将上线变成一件大事。

但是这样做增加了向下兼容的压力,虽然是比较好的实践,但并不能覆盖所有情况,有时升级的内容影响并不是很大,大家都会觉得“还不如一块搞掉更简单些”。

而如果使用docker,由于每个服务打包可以封装为一个docker镜像,每个运行时的服务都表现为一个独立容器,我们之前建立的容器依赖就可以很容易的对应到服务依赖上,基于这种统一性,系统升级就很容易配合一些自动化工具实现“整体升级”(甚至还可以“整体降级”)。

运维相关

应用的依赖

运维环节的情形和研发环节有同有异。

相同之处是,运维环节的工作和研发一样,都会因为引入了微服务而“支离破碎”,容易丢失全局视角,服务间的联系可能会变成管理的灰色地带,这些地方在讨论组织结构的时候已经涉及到了。

不同之处在于,服务间关系的信息其实是来自研发环节的,如果这些信息能够完整无误的传递到运维环节,那么服务治理将会变得容易很多。所以,在对微服务进行运维管理时,我们其实是可以“偷懒”的。

之前已经提到,在Docker的帮助下,持续集成平台可以建立统一的服务,在不涉及具体技术细节的情况下为研发团队提供服务,同时又不至于需要维护大量形式各异的脚本......其实故事并没有结束,持续集成最终是要推进到持续交付的,这时就会和运维发生联系。

在持续集成平台上,我们要解决多服务协作的问题,办法是让每个服务声明自己的依赖,然后在平台上获得全局图像,当这个平台延伸或者对接至交付环节时,之前的全局图像信息将发挥作用,我们可以在它的指导下更加“智能”的进行系统扩容、自动故障降级等一系列工作。

举个例子,开发人员在编写服务A时显然会知道它所依赖的服务(假定叫B),所以可以在源码中使用docker-compose.yml申明,那么在服务A进行持续集成时,持续集成平台将会找到服务B的镜像,并创建相应的容器和A连接,此时这个依赖信息是为了测试,但它可以被平台获得并记录下来(这是真正有效的信息)。

当线上进行服务B扩容时,平台根据之前的依赖信息,反向查询到依赖B的其它服务(包括服务A),于是我们就可以根据预先的扩容策略,自动化的执行对服务A等一些服务的restart/reload操作(这里的reload针对的是整个服务,实际上基于Docker的服务reload一般也就是依次restart而已)

上述的例子也适用于有依赖的服务自动升级降级,做法类似。

打包构建

对运维而言,还有一个话题值得专门提出——打包构建,作为基础设施之一的构建系统,面对微服务的趋势,需要有什么样的变化呢?

为了说明问题,我写了一篇文章讨论软件打包,文章结尾提到了Docker对软件打包的价值,这里结合微服务话题简单总结一下文中要点:

软件打包的目的是为了降低软件交付物对外部环境的依赖,这个目的对于大规模分布式或者集群应用特别有意义。因此这种以“自带干粮”为特征的很多技术(比如静态编译的golang)会从google这样的大规模互联网公司中诞生,而为分布式系统服务的J2EE技术族中会出现专门为打包设计的war/ear规范。

显然,按照微服务架构的系统,特点正是分布式越来越复杂,其中某些服务可能会扩展成很大的集群,这和Google、facebook这样的公司面临的是同样性质的问题,所以解决方向也类似——让构建系统输出的软件交付物更加完备,即所谓的“自带干粮”。

Docker技术交付的正是这样“自带干粮”的镜像文件,通过这个文件,打包系统交付的产品可以自由分发和管理,大大降低对环境的依赖。

总结

面对膨胀的未来,微服务走了一条拆解之路,但要想完整的实现你的业务,还要能够在某些情况下自由融合、彼此协作,Docker开启的正是这样一个方便之门。无论是协同不同语言技术栈,降低运维的成本,还是支持分布式系统的自动化测试和持续交付,甚至是从单体架构向微服务的逐步演化,Docker相关技术都可以为微服务提供有力帮助。

推荐阅读更多精彩内容