将单体应用迁移到微服务

/ 动手系列 / 没有评论 / 0浏览

微服务架构是互联网很热门的话题,是互联网技术发展的必然结果。它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,形成分布式调用,为用户提供最终价值。然而微服务概念提出者Martin Fowler却做了这样的强调“分布式调用的第一原则就是不要分布式”。这二者之间看似确实是矛盾体,那么在业务和用户高速发展的时候,到底要不要做微服务呢?其实答案很简单:“不痛就不要微服务”,所谓的“痛”表示着在项目开发过程中已经有东西已经阻碍了项目的正常推进。

存在的痛点

在传统的软件开发中会经常会遇到这样的“痛”:

代码冲突加剧

多个人或者一个团队一起维护一个模块,共同开发。当提交代码的时候发现大量冲突,每次提测或者发版的时候需要花大量的时间来解决冲突。随着团队规模的增大以及项目复杂程度增加,代码冲突的现象越严重;

模块耦合严重

模块之间通过接口或者DB相互依赖,耦合越来越严重。而且不同的人,写代码的风格不一样,代码质量也不一样,上线前需要协调多个团队,任何小模块的异常都会导致整个项目发布失败;

项目质量下降

由于所有的代码都是在一个服务里面,做一次改动,可能会牵一发而动全身,代码冲突以及耦合严重,导致测试覆盖范围不充分,经常会出现没有更改的模块在线上突然出现问题,查询后发现是由于工程师不小心做了某种改动,但是测试用例并没有覆盖;

团队效率下降

由于大量时间在处理代码冲突,消耗了研发人员大量的时间;而测试人员为了提高项目质量,不得不在每次发版之前做全方位的回归测试,本身一次小的迭代结果项目时间却很长。

如果上述的“痛”深深刺痛到了你,那么是时候开始做服务化改造了。将原来耦合在一起的复杂业务拆分为单个服务,规避了原本复杂度无止境的积累,每一个微服务专注于单一功能,并通过定义良好的接口清晰表述服务边界。每个微服务独立部署,当业务迭代时只需要发布相关服务的迭代即可,降低了测试的工作量同时也降低了服务发布的风险。

本文将介绍在教育行业如何从0开始到300W用户将单体应用迁移到微服务过程。考虑到数据安全性,响应时间短、业务逻辑复杂、迭代速度快,在内部经过多轮讨论,最后技术选型微服务Spring Cloud Greenwich.SR1版本,从2018年到2019年,整体服务的系统架构的演变过程如下:

alt

从上图可见,由单体应用演变为微服务的结构,不是一蹴而就而是循序渐进的演变。根据1年多服务化改造的经验,微服务改造的步骤如下:

单体应用时期

公司处于创业期,讲究需求能快速上线,快速试错,所有的所谓设计模式,高并发、可扩展、容错机制等都不需要考虑。唯一的述求就是快速上线。随着功能的逐步迭代,线上质量越来越低,整个团队效率很差。整个工程越来越臃肿,改动任何接口都是小心翼翼 ,因为不知道会不会影响其他模块。

潜在问题

微服务架构

将单体项目按照业务拆分成独立的服务功能,每个业务有独立的数据库,业务高内聚,低耦合。每个服务都能当成独立组件,可以轻易升级。需要扩展产品需求,只要在某个服务上进行修改测试,提高开发效率。在敏捷开发过程中,任何新功能都可以被快速开发或丢弃,快速响应市场变化。 每种技术架构的选型,也会影响组织架构的调整,我们将原来大的技术部(产品、测试、开发)拆成两个部门,每个部门负责不同的业务,不过这也带来了新的问题,如果需要需求对接,部门之间的沟通的成本会很高,只要定义好双方需要的成果,由上往下推进即可。

潜在问题

中台架构

在原有的微服务架构中,核心业务和非核心业务柔和一起,业务关系复杂,需要根据公司产品业务,将一些核心业务从微服务抽象出来提供单独的服务,以后再有新业务或产品,直接调用中台服务,而不是在重新写一次,中台的业务也是从微服务沉淀并抽象。

潜在问题

经历了上述阶段后,团队发现必须要拆分功能,需要按业务领域来划分功能,支持分模块来开发、提测、部署上线。团队在2018年开始启动微服务重构。在做微服务改造的时候,也经历了一些非常痛苦的事情。下面分享下在微服务改造的流程:

统一思想充分培训

微服务实施之前必须先得到高层的支持,给团队传递一个信号,公司全力支持微服务改造,如果不做业务就受阻,公司可能就会死。而不是小范围的试验或者是demo。再做团队培训,让团队中所有人员了解微服务开发的工作原理,并能熟悉使用。

工程结构标准化

当团队开始实时微服务重构的时候,大家按照之前各自负责的模块,从现有代码中剥离开,定义各种接口,相互依赖。看似高效的协作,但是当进入代码review阶段的时候发现了问题,每个人的工程结构不一样,代码风格不一样。研发人员在面对之前未接触的功能时候,发现自己是个新人,很难理解代码的业务逻辑。

我们做了一次复盘,最终得出如下的一致意见:需要同一个的工程结构,统一的代码风格,统一的变量命名规范,同时需要定义一些相关术语,以避免小组间沟通产生歧义。

代码未动,工具先行,一个好的工具能够提升团队的开发效率。代码自动生成工具能生成标准项目结构,DTO对象,PO和DTO之间互转以及生成相关的增删改查。

farm    #项目的集合,抽象名称或是一个集合
|-- farm-potato                 #子项目名称,完成的某件事
|   |-- farm-potato-api         #对外提供给用户接口服务,规范参考restful
|   |-- farm-potato-client      #外部服务依赖该接口提供的服务
|   |-- farm-potato-common      #公共数据,Entity,dto,vo等
|   |-- farm-potato-manage      #管理后台提供的接口
|   |-- farm-potato-mq          #服务之间解耦
|   |-- pom.xml                 #maven pom 文件,子工程需要依赖的jar 版本在父项目中定义
|   `-- README.md               #项目说明文件

服务鉴权标准

封装公司内部注解,开发任务无需关注,只需要增加相应的注解即可

常用注解

EnableCloudSwagger 在启动类上使用,集成swagger 文档

EnableGeneratorCode 在启动类上使用,代码生成工具,容器启动后访问:http//ip:port/generator.html,选择表,点击生成

EnableApiSecurity api接口权限认证,验证用户身份请求

Aubject 获取认证后的token 或用户信息,此注解只能在参数上使用

AuthIgnore 请求绕过认证,无需验证身份信息,此注解在方法上使用。需要注意的是,获取不到用户信息

EnableKingSecurity 管理后台权限认证,支持按钮级权限管理


服务乱窜

有两种方案

  1. 复制注册中心实例信息
  2. 重写负载均衡算法(采用)

king-basket

开发中需要本地调试,但需要其它服务支持,需要启动很多服务,给开发带来不便,于是产生该项目
通过配置路由,优先启用本地服务。提供具体实现,不需要加额外配置项,即刻使用

系统拆分

很多团队面临这样的问题,服务到底如何拆分,怎么样的拆分是合理的,拆分后新的微服务框架和老的系统如何做兼容运行,老系统如何逐步平滑过渡到微服务架构中,而且不影响线上业务运行,也不能影响正常的项目迭代。其实,业绩没有标准的方式来指导如何做拆分,我们主要围绕“拆“ 与 ”合“来做服务的拆分,所谓拆就是按业务功能拆分,所谓”合“,就是拆分后的模块经过多次迭代后可以做合并处理。

1、服务拆分:好的平台都是逐步演变出来的而不是设计出来的,所以微服务初期最简单的是按业务模块来拆分,比如用户、产品、订单、积分、活动等粗力度来拆分。比如订单服务改动非常的平凡,那么订单服务可以继续拆分,比如历史订单,待支付订单等等;经过多次迭服务拆分后,整体的服务框架如下: alt

功能迁移

增加Nginx做反向代理,并在Nginx层(或者网关层)做模块的流量切分,例如产品列表接口,切分了20%的流量到微服务,80%的流量到老的平台。微服务上线后,大部分业务逻辑还是访问老系统,迁移过来的业务逻辑以及新增的业务逻辑访问微服务系统

alt

alt

化串行为并行

我们在打开APP产品详情页的时候,大概需要调用后端二十几个服务,且每个服务之间通过RPC的方式来调用,初期的时候,由于产品结构简单,详情页调用后端服务比较少,程序员程序员根据以往的经验,串行方式逐个调用服务。但是随着迭代过程的持续,详情页调用后端服务越来越多,响应越来越慢。因为聚合层需要调用多个基础服务,会增加系统的响应时间。最后对于微服务后的产品详情页又做了多次重构,整个重构过程如下: alt

1、线程池并行调用:为了提高接口响应时间,把之前串行调用方式修改为把请求封装为各种Future放入线程池并行调用,最后通过Future的get方法拿到结果。这种方式暂时解决了详情页访问速度的问题,但是运行一段时间后发现在并发量大的时候整个product服务的tomcat线程池全部消耗完,出现假死的现象。

2、服务隔离并行调用:由于所有调用外部服务请求都在同一个线程池里面,所以任何一个服务响应慢就会导致tomcat线程池不能及时释放,高并发情况下出现假死现象。为解决这个问题,需要把调用外部每个服务独立成每个线程池,线程池满之后直接抛出异常,程序中增加各种异常判断,来解决因为个别服务慢导致的服务假死。

3、线程隔离服务降级:方式2似乎解决了问题,但是并没有解决根本问题。响应慢的服务仍然接收到大量请求,最终把基础服务压垮。程序中存在大量判断异常的代码,判断分支太多,考虑不完善接口就会出差。

服务熔断降级

在进行服务化拆分之后,应用中原有的本地调用就会变成远程调用,这样就引入了更多的复杂性。比如说服务A依赖于服务B,这个过程中可能会出现网络抖动、网络异常,或者说服务B变得不可用或者响应慢时,也会影响到A的服务性能,甚至可能会使得服务A占满整个线程池,导致这个应用上其它的服务也受影响,从而引发更严重的雪崩效应。所以引入了Hystrix做服务熔断和降级;

服务解耦

虽然服务拆分已经解决了模块之间的耦合,大量的RPC调用依然存在高度的耦合,不管是串行调用还是并行调用,都需要把所依赖的服务全部调用一次。但是有些场景不需要同步给出结果的,可以引入MQ来降低服务调用之间的耦合。

缓存

在高并发场景下,需要通过缓存来减少RPC的调用次数,减少数据库的压力,使得大量的访问进来能够命中缓存,只有少量的需要到数据库层。由于缓存基于内存,可支持的并发量远远大于基于硬盘的数据库。缓存分本地缓存(基于JVM的CACHE)和远端缓存(如Redis或MemCache),在产品详情页的场景,允许有短暂的数据一致情况,所以使用了本地缓存+远端缓存组合的模式,来降低RPC调用次数,网络开销和DB的压力。 alt

分布式事务

待完善......

maven私服

待完善......

持续集成

待完善......

自动化运维

待完善......

自动化发布

待完善......

APM监控

待完善......

接口监控

待完善......