DockOne微信分享(一四五):乐高式微服务化改造

【编者的话】技术圈流行一句话,凡脱离业务谈架构的,都是耍流氓。当新需求响应越来越慢,当加班成为家常便饭,你可曾怀念当年一下午徒手写一千行代码的爽快?面对一个不断吞噬团队时间的庞然大物(单体应用),分而治之往往是最有效的方法。今天我就和大家聊聊我对小公司如何进行微服务化改造的理解和一手经验。

微服务总览

本文讲的是DockOne微信分享(一四五):乐高式微服务化改造在正式开始之前,我想先跟大家简单聊一下我对Martin Fowler的 微服务定义 中三个关键词(small,independently deployable和automated deployment)的理解,以及和传统的单体应用相比,微服务有哪些本质区别。

small对应的就是微服务的微,很多初次接触微服务的同学对微的理解往往会停留在实现层面,以为代码少就是微,但实际上,这里的微更多的是体现在逻辑层面。微服务的一个重要设计原则是share as little as possible,什么意思呢?就是说每个微服务应该设计成边界清晰不重叠,数据独享不共享,也就是我们常说的高内聚、低耦合。保证了small,才能做到independently deployable。而实现automated deployment的关键是DevOps文化。需要提醒的是,随着业务复杂度的上升,一个微服务可能需要拆分为更多更细粒度的微服务,比方说,一开始只是一个简单的订单服务,后面逐步拆分出清算,支付,结算,对账等其他服务。

从本质上来看,相对单体应用,微服务是以牺牲强一致性、提高部署复杂性为代价,换取更彻底的分布式特性,比如异构性和强隔离性。对应CAP理论,就是用Consistency换Partition。异构性比较容易理解,通过定义统一的API规范(一般采用REST风格),每个微服务团队可以根据各自的能力矩阵选用最适合的技术栈,而不是所有人必须使用相同的技术栈。强隔离性指的是,对于一个典型的单体应用,隔离性最高只能体现到模块级别,由于共享同一个代码仓库,模块的边界往往比较模糊,需要人为定义很多规范来保证良好的隔离性,但无论如何强调,稍一疏忽,就会产生“越界”行为,时间愈长,维护隔离性的成本愈高。而到了微服务阶段,自带应用级别的隔离性,“越界”的成本大大提升,无需任何规范,架构本身就保证了隔离性。

另一方面,由于采用了分布式架构,微服务无法再简单的通过数据库事务来保证强一致性,而是通过消息中间件或者某种事务补偿机制来保证最终一致性,比如微信朋友圈的点赞,淘宝订单的物流状态。其次,在微服务阶段,随着应用数量的激增,一次发布往往涉及多个应用,加上异构性带来的部署方式的多样性,对团队的运维水平尤其是自动化水平提出了更高的要求,运维和开发的边界进一步模糊。

讲完这些有关微服务的背景知识之后,现在就切入今天的正题,面对快速增长的业务需求,小公司如何进行微服务化改造?下面就以我在杏仁主导实施的微服务化改造的全过程为背景,给大家简单说一下我们微服务化改造的总体思路和核心中间件的技术选型过程。

项目背景

首先介绍一下微服务化改造的背景。去年年初,在历经2年多的产品迭代之后,整个后台应用越来越庞大,已经成为一个典型意义上的monolithic application:1. 各个业务模块犬牙交错,重复代码随处可见,补丁代码越打越多;2. 任何一个改动都需要一次全量发布,哪怕是修改一句文案,极大的拖慢了迭代速度。

与此同时,由于公司电商业务变得越来越复杂,老的业务模型越来越难以满足新的需求,急需对原有的订单模块进行重构,或者抽取一个独立的订单服务来进行支撑。反复考量之后,我们选择了后者。由于是团队第一次试水微服务,并且初期人员有限(一人主导,多人配合),最后我们决定走一条比较实用的改良式路线:
  • 最小化对已有应用的侵入性
  • 偏好主流的微服务框架
  • 只做必要的微服务治理

第一条定下了此次改造的基调,降低了方案无法落地的风险,确保了项目的整体可行性。第二条让我们站在巨人的肩膀上,不重复造轮子,聚焦在问题本身,而不是工具。第三条缩减项目范围,避免过度工程,以战养兵,不打无用之仗。

下图展示了目前杏仁微服务的整体架构,而今天的分享将着重介绍其中的三大核心组件,即注册中心,配置中心和授权中心。

  • 注册中心:所有服务注册到Consul集群,然后通过Consul Template刷新Nginx配置实现负载均衡
  • 配置中心:使用自研的Matrix系统,通过自定义构建插件覆写配置,最小化对已有应用的侵入性
  • 授权中心:基于Spring Security OAuth,同时支持基于微信企业号的SSO

基本框架

基本框架我们选择的是 Spring Boot 。Spring Boot是Spring开源社区提供的一个去容器、去XML配置的应用框架。和标准的基于war包的Web应用相比,Spring Boot应用可以直接以java -jar的方式运行,也就是说不再需要部署到一个独立的Web容器(比如Tomcat)中才能运行。其背后的运行机制简单来说就是,当一个Spring Boot应用启动时,在加载完核心框架类之后,会启动一个内嵌的Web容器(默认是Tomcat),然后再加载应用本身的各种配置类和Bean。也就是说不再是容器包应用,而是应用包容器。正是由于这个特性,Spring Boot非常适用于开发微服务,毫不夸张的说Spring Boot就是为微服务而生。

有同学可能会问,不是还有 Dubbo Spring Cloud 吗?Dubbo是阿里开源的第一代RPC框架,早在2011年就已经发布了2.0版本,三年后也就是2014年,Martin Fowler才提出了微服务的概念。虽然用Dubbo也能开发微服务,但这就好比用EJB的规范去开发Spring Bean,怎么用怎么别扭。Dubbo最大的问题是升级缓慢,最近一次发布还是2014年10月,支持的Spring版本是2.5.6.SEC03,要知道Spring 5都快出来了。

Spring Cloud可以说是目前Java社区最好最完整的微服务框架(没有之一),底层用的也是Spring Boot,照着Spring Cloud的新手指南,分分钟就可以搭建出一整套微服务应用,非常适合改革式但不是改良式的微服务改造,因为非Spring应用难以集成。

作为硬币的另一面,选用Spring Boot,意味着我们需要做大量的自定义工作,以弥补Spring Boot在微服务治理方面所欠缺的能力,比如接下来要说的注册中心、配置中心和授权中心。这也是为什么今天的分享标题起名乐高式微服务化改造。

注册中心

作为微服务架构最基础也是最重要的组件之一,服务注册中心本质上是为了解耦服务提供者和服务消费者。对于任何一个微服务,原则上都应存在或者支持多个提供者,这是由微服务的分布式属性决定的。更进一步,为了支持弹性扩缩容特性,一个微服务的提供者的数量和分布往往是动态变化的,也是无法预先确定的。因此,原本在单体应用阶段常用的静态LB机制就不再适用了,需要引入额外的组件来管理微服务提供者的注册与发现,而这个组件就是服务注册中心。

设计或者选型一个服务注册中心,首先要考虑的就是服务注册与发现机制。纵观当下各种主流的服务注册中心解决方案,大致可归为三类:
  • 应用内:直接集成到应用中,依赖于应用自身完成服务的注册与发现,最典型的是Netflix提供的Eureka
  • 应用外:把应用当成黑盒,通过应用外的某种机制将服务注册到注册中心,最小化对应用的侵入性,比如Airbnb的SmartStack,HashiCorp的Consul
  • DNS:将服务注册为DNS的SRV记录,严格来说,是一种特殊的应用外注册方式,SkyDNS是其中的代表

注1:对于第一类注册方式,除了Eureka这种一站式解决方案,还可以基于ZooKeeper或者Etcd自行实现一套服务注册机制,这在大公司比较常见,但对于小公司而言显然性价比太低。


注2:由于DNS固有的缓存缺陷,这里不对第三类注册方式作深入探讨。
除了基本的服务注册与发现机制,从开发和运维角度,至少还要考虑如下五个方面:
  • 测活:服务注册之后,如何对服务进行测活以保证服务的可用性?
  • 负载均衡:当存在多个服务提供者时,如何均衡各个提供者的负载?
  • 集成:在服务提供端或者调用端,如何集成注册中心?
  • 运行时依赖:引入注册中心之后,对应用的运行时环境有何影响?
  • 可用性:如何保证注册中心本身的可用性,特别是消除单点故障?

下面就围绕这几个方面,简单分析一下Eureka,SmartStack,Consul的利弊。

Eureka

从设计角度来看,Eureka可以说是无懈可击,注册中心、提供者、调用者边界清晰,通过去中心化的集群支持保证了注册中心的整体可用性,但缺点是Eureka属于应用内的注册方式,对应用的侵入性太强,且只支持Java应用。

SmartStack

SmartStack可以说是三种方案中最复杂的,涉及了ZooKeeper、HAProxy、Nerve和Synapse四种异构组件,对运维提出了很高的要求。它最大的好处是对应用零侵入,且适用于任意类型的应用。

Consul

Consul本质上属于应用外的注册方式,但可以通过集成Consul SDK+本地Agent的方式简化注册流程。当服务以容器方式运行时,可以更进一步通过Registrator实现自动注册。服务调用端的服务发现默认依赖于SDK,但可以通过Consul Template去除SDK依赖。

最终方案

最终我们选择了Consul作为服务注册中心的实现方案,主要原因有两点:
  1. 最小化对已有应用的侵入性,这也是贯穿我们整个微服务化改造的原则之一。
  2. 降低运维的复杂度,通过使用Registrator和Consul Template实现服务自注册和自发现。

配置中心

我们知道,大至一个PaaS平台,小至一个缓存框架,一般都依赖于特定的配置以正常提供服务,微服务也不例外。

配置分类

  • 按配置的来源划分,主要有源代码(俗称hard-code),文件,数据库和远程调用。
  • 按配置的适用环境划分,可分为开发环境,测试环境,预发布环境,生产环境等。
  • 按配置的集成阶段划分,可分为编译时,打包时和运行时。编译时,最常见的有两种,一是源代码级的配置,二是把配置文件和源代码一起提交到代码仓库中。打包时,即在应用打包阶段通过某种方式将配置(一般是文件形式)打入最终的应用包中。运行时,是指应用启动前并不知道具体的配置,而是在启动时,先从本地或者远程获取配置,然后再正常启动。
  • 按配置的加载方式划分,可分为单次加载型配置和动态加载型配置。

演变

随着业务复杂度的上升和技术架构的演变,对应用的配置方式也提出了越来越高的要求。一个典型的演变过程往往是这样的,起初所有配置跟源代码一起放在代码仓库中;之后出于安全性的考虑,将配置文件从代码仓库中分离出来,或者放在CI服务器上通过打包脚本打入应用包中,或者直接放到运行应用的服务器的特定目录下,剩下的非文件形式的关键配置则存入数据库中。上述这种方式,在单体应用阶段非常常见,也往往可以运行的很好,但到了微服务阶段,面对爆发式增长的应用数量和服务器数量,就显得无能为力了。这时,就轮到配置中心大显身手了。那什么是配置中心?简单来说,就是一种统一管理各种应用配置的基础服务组件。

框架选型

选型一个合格的配置中心,至少需要满足如下4个核心需求:
  • 非开发环境下应用配置的保密性,避免将关键配置写入源代码
  • 不同部署环境下应用配置的隔离性,比如非生产环境的配置不能用于生产环境
  • 同一部署环境下的服务器应用配置的一致性,即所有服务器使用同一份配置
  • 分布式环境下应用配置的可管理性,即提供远程管理配置的能力

现在开源社区主流的配置中心框架有Spring Cloud Config和disconf,两者都满足了上述4个核心需求,但又有所区别。

Spring Cloud Config

Spring Cloud Config 可以说是一个为Spring量身定做的轻量级配置中心,巧妙的将应用运行环境映射为profile,应用版本映射为label。在服务端,基于特定的外部系统(Git、文件系统或者Vault)存储和管理应用配置;在客户端,利用强大的Spring配置系统,在运行时加载应用配置。

Disconf

Disconf 是前百度资深研发工程师廖绮绮的开源作品。在服务端,提供了完善的操作界面管理各种运行环境,应用和配置文件;在客户端,深度集成Spring,通过Spring AOP实现应用配置的自动加载和刷新。

最终方案

不管是Spring Cloud Config还是Disconf,默认提供的客户端都深度绑定了Spring框架,这对非Spring应用而言无疑增加了集成成本,即便它们都提供了获取应用配置的API。最终我们还是选用了微服务化改造之前自研的Matrix作为配置中心,一方面,可以保持新老系统使用同一套配置服务,降低维护成本,另一方面,在满足4个核心需求的前提下,Matrix还提供了一些独有的能力。
  • 分离配置文件和配置项。对于配置文件,通过各类配套打包插件(SBT,Maven,Gradle),在打包时将配置文件打入应用包中,同时最小化对CI的侵入性;对于配置项,提供SDK,帮助应用从服务端获取配置项,同时支持简单的缓存机制。
  • 增加应用版本维度,即对于同一应用,可以在服务端针对不同版本或版本区间维护不同的应用配置。
  • 应用配置的版本化支持,类似于Git,可以将任一应用配置回退到任一历史版本。

授权中心

有了服务注册中心和配置中心,下一步应该就可以发起服务调用了吧?Wait,还有一个关键问题要解决。不同于单体应用内部的方法调用,服务调用存在一个服务授权的概念。打个比方,原本一家三兄弟住一屋,每次上山打猎喊一声就行,后来三兄弟分了家,再打猎就要挨家挨户敲门了。这一敲一应就是所谓的服务授权。

严格来说,服务授权包含鉴权(Authentication)和授权(Authorization)两部分。鉴权解决的是调用方身份识别的问题,即敲门的是谁。授权解决的是调用是否被允许的问题,即让不让进门。两者一先一后,缺一不可。为避免歧义,如不特殊指明,下文所述授权都是宽泛意义上的授权,即包含了鉴权。

常见的服务授权有三种,简单授权,协议授权和中央授权。
  • 简单授权:服务提供方并不进行真正的授权,而是依赖于外部环境进行自动授权,比如IP地址白名单,内网域名等。这就好比三兄弟互相留了一个后门。
  • 协议授权:服务提供方和服务调用方事先约定一个密钥,服务调用方每次发起服务调用请求时,用约定的密钥对请求内容进行加密生成鉴权头(包含调用方唯一识别ID),服务提供方收到请求后,根据鉴权头找到相应的密钥对请求进行鉴权,鉴权通过后再决定是否授权此次调用。这就好比三兄弟之间约定敲一声是大哥,敲两声是二哥,敲三声是三弟。
  • 中央授权:引入独立的授权中心,服务调用方每次发起服务调用请求时,先从授权中心获取一个授权码,然后附在原始请求上一起发给服务提供方,提供方收到请求后,先通过授权中心将授权码还原成调用方身份信息和相应的权限列表,然后决定是否授权此次调用。这就好比三兄弟每家家门口安装了一个110联网的指纹识别器,通过远程指纹识别敲门人的身份。

一般来说,简单授权在业务规则简单、安全性要求不高的场景下用的比较多。而协议授权,比较适用于点对点或者C/S架构的服务调用场景,比如 Amazon S3 API 。对于网状结构的微服务而言,中央授权是三种方式中最适合也是最灵活的选择:
  1. 简化了服务提供方的实现,让提供方专注于权限设计而非实现。
  2. 更重要的是提供了一套独立于服务提供方和服务调用方的授权机制,无需重新发布服务,只要在授权中心修改服务授权规则,就可以影响后续的服务调用。

OAuth

说起具体的授权协议,很多人第一反应就是OAuth。事实上也的确如此,很多互联网公司的开放平台都是基于OAuth协议实现的,比如 Google APIs 微信网页授权接口 。一次标准的OAuth授权过程如下:

对应到微服务场景,服务提供方相当于上图中的Resource Server,服务调用方相当于Client,而授权中心相当于Authorization Server和Resource Owner的合体。

Beared Token

在标准的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框架实现了自己的服务授权中心,鉴权部分目前支持私网认证,Scope校验和域名校验。大致的服务授权流程如下:

更多

微服务是一个很大的话题,自Martin Fowler在2014年3月提出以来,愈演愈热,并跟另一个话题容器化一起开创了一个全新的DevOps时代,引领了国内外大大小小各个互联网公司的技术走向,也影响了我们这一代程序员尤其是后端和运维的思维方式。

限于时间,今天的分享就先到这里。除了上面提到的三大核心组件,完整的微服务化改造还会涉及很多其他重要的工作,比如抽取内部类库,创建模板工程,服务调用的限流降级,服务上下文,服务调用链等,以后有机会我们再聊。希望今天的分享对你有所帮助,如果有问题想和我进一步沟通,欢迎加我微信emacoo。

Q&A

Q:配置中心使用Consul的配置共享,有没有考虑过?
A:我们的配置中心里面除了键值对形式的配置项,更多的是存储了文件形式的配置文件,而Consul一般存储的是键值对信息。另外,除了存储、读取配置的能力,我们还需要一些上层的能力,比如环境隔离、版本匹配、版本管理等,这些Consul也无法直接提供。
Q:今天讲的这些Spring Cloud、Spring Boot、配置中心、授权中心等等,有没有好的入门书籍推荐下?
A:可以关注一下我的博客,里面提供了很多参考资源。 http://emacoo.cn/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/
Q:服务边界的划分,有没有什么好的建议?
A:这是一个好问题,也是一个见仁见智的问题。按照我目前的理解,边界至少意味着:1. 单数据库,保证数据独享;2. 各个服务层次清晰,无循环依赖。另外,很重要的一点是,随着业务复杂度的上升,一个微服务可能会需要进一步拆分,也就是说边界是跟复杂度挂钩的。
Q:微服务多大程度应该独享数据库还是共享数据库资源?
A:建议独享数据库,数据通过接口暴露给其他服务。
Q:请问下如果有一个Spring Boot服务挂了,能自动发现然后重启么?有没有什么方案推荐?
A:要实现这一点有很多方案。我们所有的服务都是跑在容器里面,借助Marathon的测活机制实现了服务意外宕机后的自动恢复。如果是非容器环境,可以通过监控平台(比如Zabbix、Open-Falcon)实时监控服务并尝试恢复。
Q:看贵公司的架构,微服务是做了容器化部署的,微服务容器化之后采取什么样的网络方式暴露服务?
A:没错,我们所有微服务都是跑在容器里面的。测试环境通过Marathon LB暴露服务,生产环境通过Consul Registrator自动发现服务,然后由Consul Template定时刷新LB配置,LB前面还有一层内网DNS,最终服务调用方通过内网域名访问服务。
Q:集成Nginx实现负载均衡这个是怎么能实现的,能讲一下吗?
A:非常简单,利用Nginx自带的upstream特性,相当于一个虚拟主机挂多个服务地址。
Q:对一个系统做微服务改造时,什么样的功能或者应用不适合采用微服务模式,而是应该保留单体应用架构?
A:这也是一个很好的问题。在我看来,关键看两点,第一,业务复杂度以及团队对领域模型的熟悉程度;第二,团队技术实力,尤其是DevOps水平,看能不能Hold住微服务本身所需的技术框架,以及支撑微服务的各类中间件、运维平台。
Q:贵公司在进行微服务改造的时候,应该很难一下子全部改造成微服务同时上线,改造应用的顺序是横向还是纵向的?
A:对,我们采取的是改良式路线,基本的思路是,首先通过重构将原本单体应用中的某个模块的边界进行纵向划分,然后新建数据库、迁移老数据、双写新老数据库,最后等新服务开发完上线之后,再将原本同一应用里的代码调用切换为外部服务调用。

原文发布时间为:2017-10-10

本文作者:沈斌

本文来自云栖社区合作伙伴Dockerone.io,了解相关信息可以关注Dockerone.io。

原文标题:DockOne微信分享(一四五):乐高式微服务化改造


你可能感兴趣的:(DockOne微信分享(一四五):乐高式微服务化改造)