微服务实战(六):如何作好服务拆分?

原文连接:https://mp.weixin.qq.com/s/mcBdtqBRQbY4D5i6G7o-7g

 

服务拆分的前提

说到微服务,服务拆分是绕不过去的话题,可是微服务不是说拆就能拆的,有不少的前提条件,须要完成前面几节所论述的部分。

首先要有一个持续集成的平台,使得服务在拆分的过程当中,功能的一致性,这种一致性不能经过人的经验来,而须要通过大量的回归测试集,而且持续的拆分,持续的演进,持续的集成,从而保证系统时刻处于能够验证交付的状态,而非闭门拆分一段时间,最终谁也不知道功能最终究竟有没有bug,于是须要另一个月的时间专门修改bug。

其次在接入层,API和UI要动静分离,API由API网关统一的管理,这样后端不管如何拆分,能够保证对于前端来说,统一的入口,并且能够实现拆分过程当中的灰度发布,路由分发,流量切分,从而保证拆分的平滑进行。并且拆分后的微服务之间,为了高性能,是不建议每次调用都进行认证鉴权的,而是在API网关上作统一的认证鉴权,一旦进入网关,服务之间的调用就是可信的。

其三对于数据库,须要进行良好的设计,不该该有大量的联合查询,而是将数据库当成一个简单的key-value查询,复杂的联合查询经过应用层,或者经过Elasticsearch进行。若是数据库表之间耦合的很是严重,其实服务拆分是拆不出来的。

其四要作应用的无状态化,只有无状态的应用,才能横向扩展,这样拆分才有意义。前端

服务拆分的时机

知足了服务拆分的前提以后,那先拆哪一个模块,后拆哪一个模块呢?什么状况下一个模块应该拆分出来呢?

微服务拆分绝非一个大跃进运动,由高层发起,把一个应用拆分的七零八落的,最终大大增长运维成本,可是并不会带来收益。

微服务拆分的过程,应该是一个由痛点驱动的,是业务真正遇到了快速迭代和高并发的问题,若是不拆分,将对于业务的发展带来影响,只有这个时候,微服务的拆分是有肯定收益的,增长的运维成本才是值得的。

微服务解决的问题之一,就是快速迭代。

互联网产品的特色就是迭代速度快,通常一年半就能决出胜负,第一一统天下,第二被第一收购,其余死翘翘。因此快速上线,快速迭代,就是生命线,并且一旦成功就是百亿身家,因此不管付出多大运维成本,使用微服务架构都是值得的。

这也就是为何大部分使用微服务架构的都是互联网企业,由于对于这些企业来说收益明显。而对于不少传统的应用,半年更新一次,企业运营相对平稳,IT系统的好坏对于业务没有关键性影响,在他们眼中,微服务化改造带来的效果,还不如开发多加几回班。数据库

微服务拆分时机一:提交代码频繁出现大量冲突

微服务对于快速迭代的效果,首先是开发独立,若是是一单体应用,几百人开发一个模块,若是使用Git作代码管理,则常常会遇到的事情就是代码提交冲突。

一样一个模块,你也改,他也改,几百人根本没办法沟通。因此当你想提交一个代码的时候,发现和别人提交的冲突了,因而由于你是后提交的人,你有责任去merge代码,好不容易merge成功了,等再次提交的时候,发现又冲突了,你是否是很恼火。随着团队规模越大,冲突几率越大。

因此应该拆分红不一样的模块,每十我的左右维护一个模块,也即一个工程,首先代码冲突的几率小多了,并且有了冲突,一个小组一吼,基本上问题就解决了。

每一个模块对外提供接口,其余依赖模块能够不用关注具体的实现细节,只须要保证接口正确就能够。编程

微服务拆分时机二:小功能要积累到大版本才能上线,上线开总监级别大会

微服务对于快速迭代的效果,首先是上线独立。若是没有拆分微服务,每次上线都是一件很痛苦的事情。当你修改了一个边角的小功能,可是你不敢立刻上线,由于你依赖的其余模块才开发了一半,你要等他,等他好了,也不敢立刻上线,由于另外一个被依赖的模块也开发了一半,当全部的模块都耦合在一块儿,互相依赖,谁也没办法独立上线,而是须要总监协调各个团队,你们开大会,约定一个时间点,不管大小功能,死活都要这天上线。

这种模式致使上线的时候,单次上线的需求列表很是长,这样风险比较大,可能小功能的错误会致使大功能的上线不正常,将如此长的功能,须要一点点check,很是当心,这样上线时间长,影响范围大。于是这种的迭代速度快不了,顶多一个月一次就不错了。

服务拆分后,在接口稳定的状况下,不一样的模块能够独立上线。这样上线的次数增多,单次上线的需求列表变小,能够随时回滚,风险变小,时间变短,影响面小,从而迭代速度加快。

对于接口要升级部分,保证灰度,先作接口新增,而非原接口变动,当注册中心中监控到的调用状况,发现接口已经不用了,再删除。

微服务解决的问题之二,就是高并发。

互联网一个产品的特色就是在短时间内要积累大量的用户,这甚至比营收和利润还重要,若是没有大量的用户基数,融资都会有问题。

于是对于并发量不大的系统,进行微服务化的驱动力差一些,若是只有很少的用户在线,多线程就能解决问题,最多作好无状态化,前面部署个负载均衡,单体应用部署多份。后端

微服务拆分时机三:横向扩展流程复杂,主要业务和次要业务耦合

单体应用无状态化以后,虽然经过部署多份,能够承载必定的并发量,可是资源很是浪费。由于有的业务是须要扩容的,例以下单和支付,有的业务是不须要扩容的,例如注册。若是一块儿扩容,消耗的资源多是拆分后的几倍,成本可能多出几个亿。并且因为配置复杂,在同一个工程里面,每每在配置文件中是这样组织的,这一块是这个模块的,下一块是另外一个模块的,这样扩容的时候,一些边角的业务,也是须要对配置进行详细审核,不然不敢贸然扩容。api

微服务拆分时机四:熔断降级全靠if-else

在高并发场景下,咱们但愿一个请求若是不成功,不要占用资源,应该尽快失败,尽快返回,并且但愿当一些边角的业务不正常的状况下,主要业务流程不受影响。这就须要熔断策略,也即当A调用B,而B老是不正常的时候,为了让B不要波及到A,能够对B的调用进行熔断,也即A不调用B,而是返回暂时的fallback数据,当B正常的时候,再放开熔断,进行正常的调用。

有时候为了保证核心业务流程,边角的业务流程,如评论,库存数目等,人工设置为降级的状态,也即默认不调用,将全部的资源用于大促的下单和支付流程。

若是核心业务流程和边角业务流程在同一个进程中,就须要使用大量的if-else语句,根据下发的配置来判断是否熔断或者降级,这会使得配置异常复杂,难以维护。

若是核心业务和边角业务分红两个进程,就可使用标准的熔断降级策略,配置在某种状况下,放弃对另外一个进程的调用,能够进行统一的维护。缓存

服务拆分的方法

好了,当你以为要将一个程序的某个部分拆分出来的时候,有什么方法能够保障平滑吗?

首先要作的,就是原有工程代码的标准化,咱们常称为“任何人接手任何一个模块都能看到熟悉的面孔”

例如打开一个Java工程,应该有如下的package:数据结构

  • API接口包:全部的接口定义都在这里,对于内部的调用,也要实现接口,这样一旦要拆分出去,对于本地的接口调用,就能够变为远程的接口调用。
  • 访问外部服务包:若是这个进程要访问其余进程,对于外部访问的封装都在这里,对于单元测试来说,对于这部分的Mock,可使得不用依赖第三方,就能进行功能测试。对于服务拆分,调用其余的服务,也是在这里。
  • 数据库DTO:若是要访问数据库,在这里定义原子的数据结构。
  • 访问数据库包:访问数据库的逻辑所有在这个包里面。
  • 服务与商务逻辑:这里实现主要的商业逻辑,拆分也是从这里拆分出来。
  • 外部服务:对外提供服务的逻辑在这里,对于接口的提供方,要实如今这里。


另外是测试文件夹,每一个类都应该有单元测试,要审核单元测试覆盖率,模块内部应该经过Mock的方法实现集成测试。

接下来是配置文件夹,配置profile,配置分为几类:多线程

  • 内部配置项(启动后不变,改变须要重启)
  • 集中配置项(配置中心,可动态下发)
  • 外部配置项(外部依赖,和环境相关)


当一个工程的结构很是标准化以后,接下来在原有服务中,先独立功能模块 ,规范输入输出,造成服务内部的分离。在分离出新的进程以前,先分离出新的jar,只要可以分离出新的jar,基本也就实现了松耦合。

接下来,应该新建工程,新启动一个进程,尽早的注册到注册中心,开始提供服务,这个时候,新的工程中的代码逻辑能够先没有,只是转调用原来的进程接口。

为何要越早独立越好呢?哪怕还没实现逻辑先独立呢?由于服务拆分的过程是渐进的,伴随着新功能的开发,新需求的引入,这个时候,对于原来的接口,也会有新的需求进行修改,若是你想把业务逻辑独立出来,独立了一半,新需求来了,改旧的,改新的都不合适,新的还没独立提供服务,旧的若是改了,会形成从旧工程迁移到新工程,边迁移边改变,合并更加困难。若是尽早独立,全部的新需求都进入新的工程,全部调用方更新的时候,都改成调用新的进程,对于老进程的调用会愈来愈少,最终新进程将老进程所有代理。

接下来就能够将老工程中的逻辑逐渐迁移到新工程,因为代码迁移不能保证逻辑的彻底正确,于是须要持续集成,灰度发布,微服务框架可以在新老接口之间切换。

最终当新工程稳定运行,而且在调用监控中,已经没有对于老工程的调用的时候,就能够将老工程下线了。架构

服务拆分的规范

微服务拆分以后,工程会比较的多,若是没有必定的规范,将会很是混乱,难以维护。

首先人们常常问的一个问题是,服务拆分以后,原来都在一个进程里面的函数调用,如今变成了A调用B调用C调用D调用E,会不会由于调用链路过长而使得相应变慢呢?并发

服务拆分的规范一:服务拆分最多三层,两次调用

服务拆分是为了横向扩展,于是应该横向拆分,而非纵向拆成一串的。也即应该将商品和订单拆分,而非下单的十个步骤拆分,而后一个调用一个。

纵向的拆分最多三层:

  • 基础服务层:用于屏蔽数据库,缓存层,提供原子的对象查询接口,有这一层,为了数据层作必定改变的时候,例如分库分表,数据库扩容,缓存替换等,对于上层透明,上层仅仅调用这一层的接口,不直接访问数据库和缓存。
  • 组合服务层:这一层调用基础服务层,完成较为复杂的业务逻辑,实现分布式事务也多在这一层
  • Controller层:接口层,调用组合服务层对外

 

服务拆分的规范二:仅仅单向调用,严禁循环调用

微服务拆分后,服务之间的依赖关系复杂,若是循环调用,升级的时候就很头疼,不知道应该先升级哪一个,后升级哪一个,难以维护。

于是层次之间的调用规定以下:

  • 基础服务层主要作数据库的操做和一些简单的业务逻辑,不容许调用其余任何服务。
  • 组合服务层,能够调用基础服务层,完成复杂的业务逻辑,能够调用组合服务层,不容许循环调用,不容许调用Controller层服务
  • Controller层,能够调用组合业务层服务,不容许被其余服务调用


若是出现循环调用,例如A调用B,B也调用A,则分红Controller层和组合服务层两层,A调用B的下层,B调用A的下层。也可使用消息队列,将同步调用,改成异步调用。

服务拆分的规范三:将串行调用改成并行调用,或者异步化

若是有的组合服务处理流程的确很长,须要调用多个外部服务,应该考虑如何经过消息队列,实现异步化和解耦。

例以下单以后,要刷新缓存,要通知仓库等,这些都不须要再下单成功的时候就要作完,而是能够发一个消息给消息队列,异步通知其余服务。

并且使用消息队列的好处是,你只要发送一个消息,不管下游依赖方有一个,仍是有十个,都是一条消息搞定,只不过多几个下游监听消息便可。

对于下单必须同时作完的,例如扣减库存和优惠券等,能够进行并行调用,这样处理时间会大大缩短,不是屡次调用的时间之和,而是最长的那个系统调用时间。

服务拆分的规范四:接口应该实现幂等

微服务拆分以后,服务之间的调用当出现错误的时候,必定会重试,可是为了避免要下两次单,支付两次,须要全部的接口实现幂等。

幂等通常须要设计一个幂等表来实现,幂等表中的主键或者惟一键能够是transaction id,或者business id,能够经过这个id的惟一性标识一个惟一的操做。

也有幂等操做使用状态机,当一个调用到来的时候,每每触发一个状态的变化,当下次调用到来的时候,发现已经不是这个状态,就说明上次已经调用过了。

状态的变化须要是一个原子操做,也即并发调用的时候,只有一次能够执行。可使用分布式锁,或者乐观锁CAS操做实现。

服务拆分的规范五:接口数据定义严禁内嵌,透传

微服务接口之间传递数据,每每经过数据结构,若是数据结构透传,从底层一直到上层使用同一个数据结构,或者上层的数据结构内嵌底层的数据结构,当数据结构中添加或者删除一个字段的时候,波及的面会很是大。

于是接口数据定义,在每两个接口之间约定,严禁内嵌和透传,即使差很少,也应该从新定义,这样接口数据定义的改变,影响面仅仅在调用方和被调用方,当接口须要更新的时候,比较可控,也容易升级。

服务拆分的规范六:规范化工程名

微服务拆分后,工程名很是多,开发人员,开发团队也很是多,如何让一个开发人员看到一个工程名,或者jar的名称,就大概知道是干什么的,须要一个规范化的约定。

例如出现pay就是支付,出现order就是下单,出现account就是用户。

再如出现compose就是组合层,controller就是接口层,basic就是基础服务层。

出现api就是接口定义,impl就是实现。

pay-compose-api就是支付组合层接口定义。

account-basic-impl就是用户基础服务层的实现。

服务发现的选型

微服务拆分后,服务之间的调用须要服务发现和注册中心进行维护。也能主流的有几种方法。

第一是Dubbo,Dubbo是SOA架构的微服务框架的标准,已经被大量使用,虽然中间中断维护过一段时间,可是随着微服务的兴起,从新进行了维护,是不少熟悉Dubbo RPC开发人员的首选。

1.jpg


第二种是Spring Cloud,Spring Cloud为微服务而生,在Dubbo已经没有人维护的状况下,推出了支撑微服务的成熟框架。

2.jpg


Dubbo vs. Spring Cloud的对比,Dubbo更加注重服务治理,原生功能不够全面,而Spring Cloud注重整个微服务生态,工具链很是全面。

3.jpg


Spring Cloud可定制性强,经过各类组件知足各类微服务场景,使用Spring Boot统一编程模型,可以快速构建应用,基于注解,使用方便,可是学习门槛比较高。

Dubbo注册到ZooKeeper里面的是接口,而Spring Cloud注册到Eureka或者Consul里面的是实例,在规模比较小的状况下没有分别,可是规模一旦大了,例如实例数目万级别,接口数据就算十万级别,对于ZooKeeper中的树规模比较大,并且ZooKeeper是强一致性的,当一个节点挂了的时候,节点之间的数据同步会影响线上使用,而Spring Cloud就好不少,实例级别少一个量级,另外Consul也非强一致的。

第三是Kubernetes,Kubernetes虽然是容器平台,可是他设计出来,就是为了跑微服务的,于是提供了微服务运行的不少组件。

4.jpg


不少Spring Cloud能够作的事情,Kubernetes也有相应的机制,并且因为是容器平台,相对比较通用,能够支持多语言,对于业务无侵入,可是也正由于是容器平台,对于微服务的运行生命周期的维护比较全面,对于服务之间的调用和治理,比较弱,Service只能知足最最基本的服务发现需求。

于是实践中使用的时候,每每是Kubernetes和Spring Cloud结合使用,Kubernetes负责提供微服务的运行环境,服务之间的调用和治理,由Spring Cloud搞定。

5.jpg


第四是Service Mesh,Service Mesh必定程度上弥补了kubernetes对于服务治理方面的不足,对业务代码0侵入,将服务治理下沉到平台层,是服务治理的一个趋势。

然而Service Mesh须要使用单独的进程进行请求转发,性能还不能让人满意,另外社区比较新,成熟度不足,暂时没有达到大规模生产使用的标准。

6.png
相关文章
相关标签/搜索