技术圈流行一句话,凡脱离业务谈架构的,都是耍流氓。作为微服务改造系列的第一篇博客,首先介绍一下实施这次技术改造的背景。
第一,我所在公司(简称XR)的后台服务采用的主技术栈是Scala,虽然开发效率很高,但也带来一系列的副作用。1.由于Scala语言强大的表达能力和丰富的函数式特性,很容易写出俗称“意大利面条”式的代码,一个类文件动辄上千行,代码的可读性非常差,导致可维护性也很差。2.编译Scala源码时首先需要将Scala源码转换成Java源码然后再通过JVM进行编译,加上隐式类型的存在进一步拖慢了编译期间的类型推导,Scala的编译速度比Java足足慢了一个数量级,这个差异在代码量少的时候还不明显,但随着代码量的上升,就成了团队的一个nightmare,试想本地全量编译一次需要10+分钟。3.Scala小众语言的标签决定了Scala程序员的稀缺性,晦涩难懂的官方文档拔高了学习曲线,后果就是高昂的招聘成本和漫长的培养时间。以上这些副作用不但抵消了先期开发效率上的优势,而且使得对新需求的响应能力越来越慢,技术负债也越垒越高。
第二,历经2年多的产品迭代,整个后台服务项目越来越庞大,已经成为一个典型意义上的单体应用(也就是Martin Fowler常说的monolithic application):1.各个业务模块犬牙交错,重复代码随处可见,补丁代码越打越多。2.任何一个改动都需要一次全量发布,哪怕是修改一句文案。
第三,与微服务化改造同时进行的是容器化改造,如果不对上述单体应用进行拆分,很多容器化带来的好处就会被削弱,甚至毫无意义,比如提高资源利用率(CPU型应用和内存型应用搭配部署),异构应用的环境隔离能力等。
谷歌前研发总监Tiger曾经说过,一个系统的演化一般会经历三个阶段,首先是under-engineer,然后是over-engineer,最后才是right-engineer。考虑到参与此次微服务改造的人员有限(一人主导,多人配合),同时也是团队第一次尝试做这类系统性的改造,最后我们决定采取一条比较实用的改良式路线:
第一条定下了此次改造的基调,降低了方案无法落地的风险,确保了项目的整体可行性。第二条让我们站在巨人的肩膀上,不重复造轮子,聚焦在问题本身,而不是工具。第三条缩减项目范围,避免过度工程,以战养兵,不打无用之仗。
有关微服务的定义,最权威的版本莫属微服务之父Martin Fowler在microservices一文中所述:
In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. – James Lewis and Martin Fowler
注意其中有3个关键词,small,independently deployable和automated deployment。small对应的就是微服务的微,很多初次接触微服务的同学对微的理解往往会停留在实现层面,以为代码少就是微,但实际上,这里的微更多的是体现在逻辑层面。微服务的一个重要设计原则是share as little as possible,什么意思呢?就是说每个微服务应该设计成边界清晰不重叠,数据独享不共享,也就是我们常说的高内聚、低耦合。保证了small,才能做到independently deployable。而实现automated deployment的关键是DevOps文化,可参见Fowler另一篇谈DevOps的文章。
需要提醒的是,随着业务复杂度的上升,一个微服务可能需要拆分为更多更细粒度的微服务,比方说,一开始只是一个简单的订单服务,后面逐步拆分出清算,支付,结算,对账等其他服务。
与单体应用拆分为微服务的过程类似,随着公司规模的不断扩大,一个组织势必会分化出多个更小的组织。根据康威定律,组织结构决定系统结构,因此,从这个层面来说,微服务也是一种必然。
康威定律(Conway’s Law):“Any organization that design a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure. - Melvin Conway, 1968
从本质上来看,相对单体应用,微服务是以牺牲强一致性、提高部署复杂性为代价,换取更彻底的分布式特性,比如异构性和强隔离性。对应CAP理论,就是用Consistency换Partition。异构性比较容易理解,通过定义统一的API规范(一般采用REST风格),每个微服务团队可以根据各自的能力矩阵选用最适合的技术栈,而不是所有人必须使用相同的技术栈。强隔离性指的是,对于一个典型的单体应用,隔离性最高只能体现到模块级别,由于共享同一个代码仓库,模块的边界往往比较模糊,需要人为定义很多规范来保证良好的隔离性,但无论如何强调,稍一疏忽,就会产生“越界”行为,时间愈长,维护隔离性的成本愈高。而到了微服务阶段,自带应用级别的隔离性,“越界”的成本大大提升,无需任何规范,架构本身就保证了隔离性。
另一方面,由于采用了分布式架构,微服务无法再简单的通过数据库事务来保证强一致性,而是通过消息中间件或者某种事务补偿机制来保证最终一致性,比如微信朋友圈的点赞,淘宝订单的物流状态。其次,在微服务阶段,随着应用数量的激增,一次发布往往涉及多个应用,加上异构性带来的部署方式的多样性,对团队的运维水平尤其是自动化水平提出了更高的要求,运维和开发的边界进一步模糊。
除了组织架构和技术取舍,领域知识是另一个非常重要的决策因素。对于不熟悉的业务领域,很难第一次就把各个微服务的边界和接口定义正确,一旦开始开发,重构成本就会非常可观。反过来说,当对领域知识有了一定的积累,再重构一个单体应用就会容易的多。
综上所述,虽然微服务看上去很美,但在决定采用微服务架构之前,不仅要仔细考量团队的技术水平(包括知识结构,理论深度,经验积累和技术氛围),还应综合考虑项目的时间范围,领域知识的熟悉程度,以及所在组织的规模架构。除非这些前提条件都满足,否则单体应用是更适合的选择,就像Fowler建议的那样。
上图是XR微服务化第一阶段的整体架构图。可以看到,一些支撑微服务的必要组件都已包含其中:
在微服务化系列的后续文章中,我会针对服务注册、配置中心和授权中心分别展开介绍实施过程中的一些细节和经验。敬请期待。
这篇文章是微服务化改造系列的第二篇,主题是服务注册中心。作为微服务架构最基础也是最重要的组件之一,服务注册中心本质上是为了解耦服务提供者和服务消费者。对于任何一个微服务,原则上都应存在或者支持多个提供者,这是由微服务的分布式属性决定的。更进一步,为了支持弹性扩缩容特性,一个微服务的提供者的数量和分布往往是动态变化的,也是无法预先确定的。因此,原本在单体应用阶段常用的静态LB机制就不再适用了,需要引入额外的组件来管理微服务提供者的注册与发现,而这个组件就是服务注册中心。
设计或者选型一个服务注册中心,首先要考虑的就是服务注册与发现机制。纵观当下各种主流的服务注册中心解决方案,大致可归为三类:
注1:对于第一类注册方式,除了Eureka这种一站式解决方案,还可以基于ZooKeeper或者Etcd自行实现一套服务注册机制,这在大公司比较常见,但对于小公司而言显然性价比太低。
注2:由于DNS固有的缓存缺陷,本文不对第三类注册方式作深入探讨。
除了基本的服务注册与发现机制,从开发和运维角度,至少还要考虑如下五个方面:
以下就围绕上述几个方面,简单分析一下Eureka,SmartStack,Consul的利弊。
从设计角度来看,Eureka可以说是无懈可击,注册中心、提供者、调用者边界清晰,通过去中心化的集群支持保证了注册中心的整体可用性,但缺点是Eureka属于应用内的注册方式,对应用的侵入性太强,且只支持Java应用。
SmartStack可以说是三种方案中最复杂的,涉及了ZooKeeper、HAProxy、Nerve和Synapse四种异构组件,对运维提出了很高的要求。它最大的好处是对应用零侵入,且适用于任意类型的应用。
Consul本质上属于应用外的注册方式,但可以通过SDK简化注册流程。而服务发现恰好相反,默认依赖于SDK,但可以通过Consul Template(下文会提到)去除SDK依赖。
最终我们选择了Consul作为服务注册中心的实现方案,主要原因有两点:
上文提到使用Consul,默认服务调用者需要依赖Consul SDK来发现服务,这就无法保证对应用的零侵入性。所幸通过Consul Template,可以定时从Consul集群获取最新的服务提供者列表并刷新LB配置(比如nginx的upstream),这样对于服务调用者而言,只需要配置一个统一的服务调用地址即可。改造后的调用关系如下:
由于我们选用了Spring Boot作为统一的微服务实现框架,很自然的,可以利用Spring Cloud提供的Consul组件进一步简化服务注册流程,省去额外的服务提供端的Consul配置。
这篇文章是微服务化改造系列的第三篇,主题是配置中心。上一篇我们谈到服务注册中心,即通过提供某种注册和发现的机制,解决服务互通的问题。那么问题来了,一个服务如何知道服务注册中心的地址呢?这就涉及到服务配置了。我们知道,大至一个PaaS平台,小至一个缓存框架,一般都依赖于特定的配置以正常提供服务,微服务也不例外。
随着业务复杂度的上升和技术架构的演变,对应用的配置方式也提出了越来越高的要求。一个典型的演变过程往往是这样的,起初所有配置跟源代码一起放在代码仓库中;之后出于安全性的考虑,将配置文件从代码仓库中分离出来,或者放在CI服务器上通过打包脚本打入应用包中,或者直接放到运行应用的服务器的特定目录下,剩下的非文件形式的关键配置则存入数据库中。上述这种方式,在单体应用阶段非常常见,也往往可以运行的很好,但到了微服务阶段,面对爆发式增长的应用数量和服务器数量,就显得无能为力了。这时,就轮到配置中心大显身手了。那什么是配置中心?简单来说,就是一种统一管理各种应用配置的基础服务组件。
选型一个合格的配置中心,至少需要满足如下4个核心需求:
现在开源社区主流的配置中心框架有Spring Cloud Config和disconf,两者都满足了上述4个核心需求,但又有所区别。
Spring Cloud Config可以说是一个为Spring量身定做的轻量级配置中心,巧妙的将应用运行环境映射为profile,应用版本映射为label。在服务端,基于特定的外部系统(Git、文件系统或者Vault)存储和管理应用配置;在客户端,利用强大的Spring配置系统,在运行时加载应用配置。
disconf是前百度资深研发工程师廖绮绮的开源作品。在服务端,提供了完善的操作界面管理各种运行环境,应用和配置文件;在客户端,深度集成Spring,通过Spring AOP实现应用配置的自动加载和刷新。
不管是Spring Cloud Config还是disconf,默认提供的客户端都深度绑定了Spring框架,这对非Spring应用而言无疑增加了集成成本,即便它们都提供了获取应用配置的API。最终我们还是选用了微服务化改造之前自研的Matrix作为配置中心,一方面,可以保持新老系统使用同一套配置服务,降低维护成本,另一方面,在满足4个核心需求的前提下,Matrix还提供了一些独有的能力。
进一步信息可参考我之前写的Matrix设计文档。
Matrix架构图
下一篇我将给大家介绍微服务架构的另一个基础组件——授权中心,敬请期待!
这篇文章是微服务化改造系列的第四篇,主题是授权中心。有了服务注册中心和配置中心,下一步应该就可以发起服务调用了吧?Wait, 还有一个关键问题要解决。不同于单体应用内部的方法调用,服务调用存在一个服务授权的概念。打个比方,原本一家三兄弟住一屋,每次上山打猎喊一声就行,后来三兄弟分了家,再打猎就要挨家挨户敲门了。这一敲一应就是所谓的服务授权。
严格来说,服务授权包含鉴权(Authentication)和授权(Authorization)两部分。鉴权解决的是调用方身份识别的问题,即敲门的是谁。授权解决的是调用是否被允许的问题,即让不让进门。两者一先一后,缺一不可。为避免歧义,如不特殊指明,下文所述授权都是宽泛意义上的授权,即包含了鉴权。
常见的服务授权有三种,简单授权,协议授权和中央授权。
一般来说,简单授权在业务规则简单、安全性要求不高的场景下用的比较多。而协议授权,比较适用于点对点或者C/S架构的服务调用场景,比如Amazon S3 API。对于网状结构的微服务而言,中央授权是三种方式中最适合也是最灵活的选择:
说起具体的授权协议,很多人第一反应就是OAuth。事实上也的确如此,很多互联网公司的开放平台都是基于OAuth协议实现的,比如Google APIs, 微信网页授权接口。一次标准的OAuth授权过程如下:
对应到微服务场景,服务提供方相当于上图中的Resource Server,服务调用方相当于Client,而授权中心相当于Authorization Server和Resource Owner的合体。
想了解更多关于OAuth的信息,可移步OAuth2或者OAuth2中文版。
在标准的OAuth授权过程中,Resource Server收到Client发来的请求后,需要到Authorization Server验证Access Token,并获取Client的进一步信息。通过OAuth 2.0版本引入中的Beared Token,我们可以省去这一次调用,将Client信息存入Access Token,并在Resource Server端完成Access Token的鉴权。主流的Beared Token有SAML和JWT两种格式,SAML基于XML,而JWT基于JSON。由于大多数微服务都使用JSON作为序列化格式,JWT使用的更为广泛。
在选型OAuth框架时,我主要调研了CAS,Apache Oltu,Spring Security OAuth和OAuth-Apis,对比如下:
不考虑实际业务场景,CAS和Spring Security OAuth相对另外两种框架,无论是集成成本还是可扩展性,都有明显优势。前文提到,由于我们选用了Spring Boot作为统一的微服务实现框架,Spring Security OAuth是更自然的选择,并且维护成本相对低一些(服务端)。
最后我们基于Spring Security OAuth框架实现了自己的服务授权中心,鉴权部分做的比较简单,目前只支持私网认证。大致的服务授权流程如下:
值得一提的是,除了服务调用,我们的服务授权中心还增加了SSO的支持,通过微信企业号实现各个服务后台的单点登录/登出,以后有机会再详细介绍。
至此,这个微服务化改造系列就算告一段落,等以后有了更多的积累,我会继续写下去。微服务是一个很大的话题,自Martin Fowler于2014年3月提出以来,愈演愈热,并跟另一个话题容器化一起开创了一个全新的DevOps时代,引领了国内外大大小小各个互联网公司的技术走向,也影响了我们这一代程序员尤其是后端和运维的思维方式。从这个角度说,我写这个微服务化改造系列文章也是偶然中的必然,希望能给读过这些文章的你带来一些新的启发和思考。如果你对微服务也感兴趣或者有一些心得想跟我交流,欢迎在赞赏榜上留下你的微信号。
少年读书如隙中窥月,中年读书如庭中望月,老年读书如台上玩月,皆以阅历之浅深为所得之浅深耳。– 张潮 《幽梦影》