打怪升级微服务聊聊微服务拆分设计

Posted fisher

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了打怪升级微服务聊聊微服务拆分设计相关的知识,希望对你有一定的参考价值。

并不是所有的场景都适合微服务,我理解技术开发者都有一颗追求新技术的心,但是更重要的是业务场景及团队。

  • 关于微服务

  微服务架构,说白了就是一种上层体系的演变。从最早的单体架构,到前后分离,SOA,甚至微服务架构,其实它们都在做一件事,并且都朝着一个方向去发展:那就是分而治之!从简!

  分而治之有什么好处呢? 对于服务来说,可能每个服务实例都是集群化、容器化、并且轻量级;对于设计人员就是低耦合,高内聚;

  有了这种思想,对整体架构其实是进一步的升级。因为我们都知道微服务架构一方面也是由于对服务性能要求更好的进化。我们追求“三高”,从某种意义而言,通过叠加机器的方式更能有效的提升性能,毕竟现在的机器,甚至云服务器成本不是那么高不可攀!

  readMe:

  我接触过很多公司,它们的产品体系也是一步步转化而来的!很多产品设计的前期,都追求快速上线,功能正常,并不需要太多的设计,但是慢慢产品体系、功能模块甚至日活到达一个量级后,会做一些上层的设计拆分,重构,它们的目的是为了满足我们具体的业务场景更流畅、甚至交付更快速等等...

  在技术讨论交流中,很多开发者对上层设计甚至架构能力会持有不同的看法,比如某某功能模块业务边界、是否应该做服务拆分、是否需要上微服务生态,而且大部分开发者和领导决策层往往会有不同的看法,因为大家站在角度不同:如果是技术开发者,更习惯于追求新的技术,或者希望做出一定的改变。但是对于管理者甚至技术CEO往往会从不同的角度思考:微服务带来的问题、微服务业务边界、微服务DDD领域模型、以及上层聚合数据中台或业务网关

 

  • 微服务带来的问题

  技术选型其实就跟买衣服一样,看着好看的衣服不一定合身、也不定适合你;适合你的可能你会一开始觉得很一般,但是穿起来会很舒服!

  我们不妨思考一下,如果你们的公司产品现在是一个单体架构,从现有的结构上考虑,如果做微服务拆分,会有哪些棘手的事情?

  1. 微服务生态组件的选择
  2. 服务拆分边界  
  3. 微服务数据聚合问题
  4. 线上监控、排查
  5. 分布式老生常谈的问题 :数据一致性
  6. 如果目前是单体服务,从哪些角度进行拆分靠谱

  

  微服务生态组件:目前以java的生态为例,目前基于springcloud、springcloud&alibaba或者zookeeper dubbo 都可以实现分布式架构,当然它们也会有有缺点,比如nacos CP AP的选择、cloud config的运维配置、持久化、这个可以根据团队的技术方向、以及具体业务考虑、但是这往往是最简单的一步,因为我相信很少有公司会自己写分布式组件、像阿波罗或者其他组件,很多公司都是拿来即用即可。

  服务拆分边界:这个是一个棘手的问题,往往前期做服务拆分会伴随着各种各样的问题。比如我们应该根据功能模块进行拆分还是根据场景压力进行拆分?各个服务之间耦合性怎么控制?

  微服务数据聚合:往往我们有这么一个场景、例如我的用户服务和订单服务是两个独立的微服务,但是有些场景我需要将各自的数据聚合查找,这种方式怎么处理?

  线上监控、排查:微服务通信,主要还是采用rest或者异步MQ的方式进行通信的,那么一个场景多个服务直接调用,如果发生异常我们怎么排查问题?

  数据一致性:每个服务都有自己的数据,怎么保证数据一致性?

  单体演变微服务过程:如果我现在是一个单体服务,我需要做微服务拆分,慢慢将架构转变到微服务架构,怎么做?

 

  • 微服务带来的好处

  有了微服务带来的问题,那么我们要考虑微服务带来的好处有哪些?

  1. 微服务基于容器部署,单个服务相比较小,快速启动
  2. 每一个服务都是一个独立的server、不于其他服务向耦合
  3. 如果需要敏捷开发,微服务一定是最适合的,服务拆分可以快速迭代,不需要整体发布
  4. 服务单一职责,任务拆分
  5. 缓解业务场景压力,比如某个服务压力大,粗暴加机器扩容
  6. 很多公共组件可以抽象出来,避免重复造轮子
  7. 交付场景某种意义更适合

 

  部署:基于容器自动化部署,更方便,更迅速

  敏捷开发:敏捷开发一定是更适合微服务,如果单体要考虑敏捷迭代,合并甚至其他一系列问题

  单一职责:可以由技术团队去控制服务的分配,目标更明确

  性能:集群实例,这个没什么说的,毕竟现在机器的成本不大。而且如果是单体出现性能问题,那么可能上层做负载,下层的服务很笨重

  公共组件:很多公司会有这样的程序员,我们在一个单体开发,很多类似的功能每个人都会造一套轮子,冗余代码超级多。

  交付:这个不同公司的情况是不一样的,举个我们之前的公司,我们基于产品做开发,这时候每次有交付就会从产品拉一份然后做二开,而且交付场景也很笨重。例如某些客户只想要我们的A不想要我们的B,它只给A模块付费,那么传统的方式就是lic控制,把B禁掉,这是一个很繁琐的流程

  

  • 如何解决微服务的问题

服务拆分问题


 

  服务拆分,其实总体思想很简单,就是一个服务要做什么事情?有了这个结论,其实我们就可以做服务拆分了。

  具体一点,就是:单一服务高内聚低耦合,每个服务只需要关系自己干的是什么事情就够了,例如用户服务,那它只应该管用户的事情,如果这时候它又要管订单的职责,那么这个服务边界就是一个不合理的。这样有个好处就是,用户服务发生改变不需要处理其他服务的代码,当然(如果对外提供对应的api发生变化,那没有办法,当然也有办法避免,各有好坏)

  但是,在某些场景下,有可能服务违背了这个原则:(就跟数据库设计三大范式,它是一种规范,但是它不适用所有场景!)例如几何耦合性很高的服务,同时又大量有服务聚合的场景,那么它需要拆吗?很明显它是不需要的,何苦给自己找罪受呢?要知道服务多了也会有问题,整体架构会更加复杂!

  如果我是一个单体项目,我要开始做服务拆分应该怎么做呢?首先,拆分的过程一定不能影响目前迭代的进度。一个不完善的服务想去拆分,往往最后的结果是拆不如不拆!我们可以先从相对独立的模块进行拆分,比如通知服务,或其他,它的选择应该是核心功能很明确,并且业务耦合性不高,而且趋于稳定。

  同时,在考虑服务拆分的时候,一定要结合业务场景!要了解业务模型后,整理出比如对外提供的结构、设计、甚至一些api之后,再考虑这件事情!举个例子:如果我们有两个服务,它们本身有一定的依赖,但是它们被拆分了,那么某个场景的动作应该就是A服务调用 -> B服务调用。如果我们有大量的场景需要数据聚合,那么这两个服务是否应该拆分呢?往往我们得出的是否定的结论,除非处于其他的角度,比如这个服务场景性能要求或者其他。

  所以,服务拆分一般来说主要取决这么几点方式:

    基于业务模块:业务A是一个服务、业务B是一个服务

    公共拆分:我们会有很多基础的服务,这些是业务提供的核心,但它们往往不需要变动,就需要将上层抽离出来,那么往往我们再变动业务,只需要关心具体的业务变动就好了,不需要考虑基础建设(试想一下,如果每个业务处理都要伴随着基础模块修改,然后其他的业务又都依赖于这个基础模块,那是不是乱套了?只要发生变动就会从头改到尾,或者针对变动单独写一套,周而复始这个代码得屎山成什么样?)

    场景拆分:比如某个场景很重要,对性能要求很高,那么可以将这个抽出来,针对这个服务进行优化

 

数据一致性


 

  微服务要保证数据一致性,但其实这也是个死命题,例如CAP,要保证高可用就要牺牲部分一致,鱼和熊掌不可兼得!想想我们的服务都是通过网络去通信的,再好的方式也避免不了网路抖动。这里想到了之前有一个面试官跟我聊过的微服务数据库短暂不一致的问题:如果我们通过主从 binlog同步,会有短暂的不一致。master写入数据还没来得及同步到slave,那这时候slave没有数据,怎么办?其实这个问题不需要解决,很多场景我们允许短暂不一致,但是我们要保证最终一致性!再者如果要求这么高,可以将同步方式设置为串行同步,写数据就立刻刷到slave,同步完成后再返回,那么主线程就会有短暂的阻塞,这个看业务的取舍了。

  那么,从微服务数据读写来说,我们要关注的问题是:数据一致性读、数据一致性写、操作幂等。

 

  首先我们来说说幂等,这个我相信更多在第三方sdk、暴露服务、甚至MQ消费过程中会用的更多,它的本身含义是:相同的参数在同一个方法里,无论执行一次还是多次都会响应相同的结果

  对于查询和删除,其实本身就是幂等的。只要数据没有变化,那么多次查是没有问题的。至于删除,如果你想删除一个已经删除的元素应该怎么做?那就不操作了呀,反正已经删了。

  新增数据的幂等,这个有很多种方式控制,例如通过key签名校验、客户端防抖、甚至unique唯一索引都可以做到。

  修改数据的幂等,就是一个相对复杂的问题了。首先,并发问题下会导致数据重复修改导致脏数据,第二来说也会导致获取的是脏数据。例如我查询和修改同时进行,查询的数据可能是还没来得及修改的数据。

  所以 这里引申出的几种方案,分别是读写共享锁、乐观锁ABA,一方面控制并发处理,一方面控制数据安全。

 

  数据一致性读,这个不是但个服务的查询,而是服务之间的数据处理。具体请看数据聚合模块

  

  数据一致性写,写的一致性说白了,就是分布式事务。我们知道分布式事务是一个很繁琐的问题。分布式事务主要有这么几种方式:TCC、XA二阶段提交(seate)、异步事务中间件、异步请求、回调补偿等等

  首先我们说最特殊的,异步事务中间件:举个例子:Rocket的分布式事务消息,TCC+本地消息表的变种,具体请查看:【打怪升级】【rocketMq】如何保证消息零丢失

  其实,更多时候我们通过seata或者其他全局事务中间件去处理,使用方便,集成简单。

  常规的场景不过多描述,但是在某些业务场景下还有其他的处理方式:例如事务场景也有等级之分,那么核心的事务执行完成后,要保证其他事务必须完成。举个例子:电商下单完成后,可能需要调用会员积分,给账号加积分。那我们允许积分没加成导致的下单回滚吗?很明显是不行的。这种情况我们会将失败但必须要完成的事务操作缓存起来,让异步线程或者其他操作去重新执行,如果一直执行失败我甚至可以直接操作数据库(特定场景...)保证这个数据没有问题。

  

服务聚合问题


 

  微服务数据聚合问题,是一个比较常见的问题。通常我们会有这样一个疑问,如果我的两个服务需要关联查询数据怎么办?

  首先,给出结论:常见的方式有这么几种:

  1. 部分冗余字段,违反数据库范式
  2. 广播数据(binlog主从)
  3. 上层聚合服务
  4. 内存整理数据,例如服务A先查出一部分数据 然后拿着数据去查B

 

  上面的几点,其实都是我们常用的方式,那么我们在设计中如何选择呢?

  首先,部分冗余字段是大家的常见手段。这种方式实现起来很简单,但是它的问题是对应的数据更新后要及时同步到冗余字段里,所以它更适合比较重要但是不常变动的列上。

  广播数据,说白了就是mysql binlog主从同步,mysql我们不光可以指定不同库的同步,也可以指定某张表的binlog 同步,那说白了其实它只适用一种场景,就是数据统计数据整理,你想想有一个服务它的db包含其他服务的db,并且它的db数据由其他服务db同步而来,这种其实违背了微服务的设计原则,这种只适合做复杂查询、复杂关联、甚至报表统计之类的场景需要。可以通过sql处理过滤很多数据。

  上层聚合服务,这个方式也用的很多。最常见的方式是例如我们有AB两个服务,但是api有很多AB关联查询的大量场景,并且这个服务一般是需要暴露API的,那这种方式就会更好,因为如果api变动只需要考虑上层聚合服务的变动,而底层服务变动只需要考虑api的变动,对于外在是不感知的。但是它会导致我们的结构更繁琐,根据业务场景慎用,毕竟它增加了多维护一个服务的压力。

  内存数据整理,这个从字面意思大家已经知道它是什么了。例如在数据量很小的情况下,我将AB的数据都查出来内存处理就够了;或者业务以A为主,我先查出A的数据,然后根据A的数据查出B的数据

  

微服务监控运维


  微服务的运维监控主要分为这几部分:线上问题排查、自动化运维监控、调用链路

  对于开发者来说,这个角度其实就转换成:线上日志、链路追踪、系统监控指标

  

  首先,很多微服务生态日志都采用kfaka + ES + kibana,因为如果服务数量庞大,你不可能切换各种服务登陆上去看对应的服务log,所以更好的是把所有服务的实时日志收集起来,统一存储ES,并且不同服务不同场景有不同的key方便排查。再通过ES的索引进行搜索。这样运维也可以通过kibana去实时查看日志信息。

  再者,我们的服务调用可能会有很多层,这是针对服务调用链路追踪也是一个必要的场景,比如通过Zipkin集成做服务链路调用追踪。

  系统监控,这个就很常见了。现在很多公司喜欢采用Kubernetes,通过Kubernetes也可以做一些线上系统监控。

  举个场景,例如某个场景业务出现问题,那么排查的第一思想就是:哪个模块哪个接口出现问题?问题的症状?服务的状态?其实这恰恰是上文说的几个问题的体现。

  

微服务和SOA,DDD


  很多人喜欢把微服务和SOA放在一起。其实SOA跟微服务的角度是不同的。SOA的思想是服务横向拓展能力:说白了就是通过SOA思想达到服务耦合的过程。

  我们都知道SOA中有ESB消息总线,那么其实SOA更像一个服务网关、甚至是一个上层业务网关:因为SOA体现的能力就是服务间各自独立,但是某种规范、甚至方式将服务间的处理联合起来,这么看它是不是更像一个上层的业务网关?

  而微服务主要体现在架构设计上,微服务的思想就是服务分而治之,每个服务职责单一,至于你怎么通信,怎么聚合其实根据个人的选择了。

  

  再来说DDD,DDD理念很多人第一印象就是服务拆分思想。其实这个角度没有错,我最开始也认为DDD是为了帮助我们完成服务拆分的。首先DDD的思想就是分而治之,化繁为简。那这个跟微服务设计其实不谋而合了。

  但是一般来说,如果我们拆分某个服务,如果这个服务在业务变动或者因为其他服务而频繁变动,甚至不能在短时间内完成,需要大量的考虑其他服务带来的影响,那么其实这个服务拆分是错误的。而DDD则完全遵循了分而治之的原则,我们知道服务拆分的颗粒度由很多因素决定:场景、团队、时间成本等等... 所以DDD只是一种思想,我们抛开DDD提供的领域服务之类的概念闭口不谈,它更像一个标准,就像数据库的三大范式我们设计都满足吗?如果都满足这公司的开发人员估计要骂娘了把。

  

写在后面


  其实我们发现,微服务设计是一个方式,也是一种手段。它不一定适合所有的场景!例如金融或者军工行业它们就比较适合,一方面它们的周期会比较长,另一方面它们也涉及者大量的变动,甚至作为服务能力的提供者,这种是由必要的。只有这样才能满足敏捷开发,并且满足一些使用场景;那如果你们开发一个小程序,一个简单的BS,没有大的体量也没有时间成本,更没有复杂的功能模块,你问我需要拆分微服务吗?跑着就行了,拆它干嘛?

  同时,一些做上层服务重构的团队来说,也要考虑时间成本,团队人员成本(一个开发写一个微服务?头疼?),并且实际的场景。很多公司高层不看重这些,觉得这些就是白出力,浪费时间的事情;往往等到体量上来了发觉后悔了,太过笨重成本太高都是很常见的事情。

  tips:如果你是一个技术CEO或者团队技术老大,这些事情一定要慎重,而且要了解公司上层的想法,不然出力不讨好的锅可能就在你身上了....

  

    

  

微服务化之服务拆分与服务发现

本文章为《互联网高并发微服务化架构实践》系列课程的第六篇

前五篇为:

微服务化的基石——持续集成

微服务的接入层设计与动静资源隔离

微服务化的数据库设计与读写分离

微服务化之无状态化与容器化

微服务化之缓存的设计

 

一、服务拆分的前提

 

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

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

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

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

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

 

二、服务拆分的时机

 

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

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

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

 

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

 

互联网产品的特点就是迭代速度快,一般一年半就能决出胜负,第一一统天下,第二被第一收购,其他死翘翘。所以快速上线,快速迭代,就是生命线,而且一旦成功就是百亿身家,所以无论付出多大运维成本,使用微服务架构都是值得的。

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

 

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

 

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

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

所以应该拆分成不同的模块,每十个人左右维护一个模块,也即一个工程,首先代码冲突的概率小多了,而且有了冲突,一个小组一吼,基本上问题就解决了。

每个模块对外提供接口,其他依赖模块可以不用关注具体的实现细节,只需要保证接口正确就可以。

 

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

 

微服务对于快速迭代的效果,首先是上线独立。如果没有拆分微服务,每次上线都是一件很痛苦的事情。当你修改了一个边角的小功能,但是你不敢马上上线,因为你依赖的其他模块才开发了一半,你要等他,等他好了,也不敢马上上线,因为另一个被依赖的模块也开发了一半,当所有的模块都耦合在一起,互相依赖,谁也没办法独立上线,而是需要总监协调各个团队,大家开大会,约定一个时间点,无论大小功能,死活都要这天上线。

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

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

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

 

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

 

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

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

 

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

 

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

 

微服务拆分时机四:熔断降级全靠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开发人员的首选。

 

技术分享图片

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

技术分享图片

 

dubbo vs. springcloud的对比,dubbo更加注重服务治理,原生功能不够全面,而springcloud注重整个微服务生态,工具链非常全面。

技术分享图片

 

springcloud可定制性强,通过各种组件满足各种微服务场景,使用springboot统一编程模型,能够快速构建应用,基于注解,使用方便,但是学习门槛比较高。

技术分享图片

 

 

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

 

第三是kubernetes,Kubernetes虽然是容器平台,但是他设计出来,就是为了跑微服务的,因而提供了微服务运行的很多组件。

技术分享图片

 

很多springcloud可以做的事情,kubernetes也有相应的机制,而且由于是容器平台,相对比较通用,可以支持多语言,对于业务无侵入,但是也正因为是容器平台,对于微服务的运行生命周期的维护比较全面,对于服务之间的调用和治理,比较弱,service只能满足最最基本的服务发现需求。

 

因而实践中使用的时候,往往是kubernetes和springcloud结合使用,kubernetes负责提供微服务的运行环境,服务之间的调用和治理,由springlcoud搞定。

 

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

 

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

技术分享图片

 

欢迎关注微信公众号

 

技术分享图片

 

以上是关于打怪升级微服务聊聊微服务拆分设计的主要内容,如果未能解决你的问题,请参考以下文章

聊聊微服务架构及分布式事务解决方案!

微服务设计模式(系列)-微服务拆分

微服务设计模式(系列)-微服务拆分

微服务化之服务拆分与服务发现

了解微服务

阿里P9:做了 6 年架构设计,这次聊聊微服务与分布式事务细节