尘世随缘
作者尘世随缘2019-07-01 13:42
技术总监, 上海某互联网金融公司

某互联网金融企业如何从0到4500W用户将单体应用迁移到微服务实践经验

字数 7923阅读 4628评论 0赞 10

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

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

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

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

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

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

关于微服务的调用框架先前曾经写过文章做过比较,详见《微服务架构技术路线对比:Dubbo VS Spring Cloud | 争议》,本文将介绍在金融行业如何从0开始到4500W用户将单体应用迁移到微服务过程,在金融行业中数据安全性,数据一致性、响应时间短、数据量大,业务逻辑复杂,在内部经过多轮讨论,最后技术选型微服务RPC框架选择了Dubbo 2.6.3版本,从2015年到2019年,整体服务的系统架构的演变过程如下:

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

一、单体应用时期

公司处于创业期,讲究需求能快速上线,快速试错,所有的所谓设计模式,高并发、可扩展、容错机制等都不需要考虑。唯一的述求就是快速上线。所以2015年的项目架构非常简单,虽然速度快了,但是也暴露出一些问题。

二、负载均衡期

为了解决发版期间不影响投放,支持横向扩展机器,提升系统整体的TPS。所以引入了Nginx做反向代理,session共享,工程支持横向扩充。这个方案维持了线上一段时间,但是随着功能的逐步迭代,线上质量越来越低,整个团队效率很差。整个工程越来越臃肿,改动任何接口都是小心翼翼 ,因为不知道会不会影响其他模块。

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

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

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

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

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

术语约定:统一的术语约定,能降低团队在沟通过程中的歧义。在微服务中聚合层使用back-工程名称命名,基础服务以basic-工程名称命名;
1.back-工程名称:业务聚合层,以war的方式在容器中运行,提供聚合服务;
2.basic-工程名:基础服务层,以jar的方式直接运行,提供原子服务;

任何工程代码结构需要标准化,都具备相同的module结构,这样团队中任何人看到相关module都知道该module所负责的功能;例如以产品product为例,涉及的工程结构如下:
back-product(产品服务聚合层)

 back-product-business:业务处理层,如果需要调用过多个服务在这里做聚合
 back-product-façade:外部服务调用统一出口
 back-product-model:定义VO相关的字段
 back-product-web:接收网关转过来的请求

basic-product(产品基础服务层)

basic-product-api:接口层,外部服务依赖该接口提供的服务;
basic-product-service:接口实现层,实现api接口
basic-product- business:数据相关的处理;以及PO
basic-product- model:定义DTO对象
basic-product-façade:如果基础服务调用需要调用外部服务,则统一在此调用。
定义好了back和basic之后,完整的业务调用逻辑就比较清晰了,如下图:

三、系统拆分

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

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

2、session共享:一般系统都通过判断session是否有效来判断用户是否能访问平台,所以新老系统首先要做的就是session共享,session放入分布式缓存中。访问模式如下:

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

•在Nginx层做流量拆分
•切分比例2/8 ,5/5,100%
•服务化的部分初始阶段和老平台使用相同DB
•新业务迭代优化在微服务上开发,DB隔离
•微服务上线后做好流量切换后的监控

整个微服务的访问流程如下:

四、化串行为并行

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

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

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

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

五、服务熔断降级

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

我们针对如下几项做了个性化配置:
错误率:可以设置每个服务错误率到达制定范围后开始熔断或降级;
人工干预:可以人工手动干预,主动触发降级服务;
时间窗口:可配置化来设置熔断或者降级触发的统计时间窗口;
主动告警:当接口熔断之后,需要主动触发短信告知当前熔断的接口信息;

六、服务解耦

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

用户完成注册动作后,只需要往MQ发送一个注册的通知消息,下游业务如需要依赖注册相关的数据,只需要订阅注册消息的topic即可,从而实现了业务的解耦。使用MQ的好处包括以下几点:

解耦:用户注册服务只需要关注注册相关的逻辑,简化了用户注册的流程;
可靠投递:消息投递由MQ来保障,无需程序来保障必须调用成功;
流量削峰:大流量的新用户注册,只需要新增用户服务,并发流量由MQ来做缓冲,消费方通过消费MQ来完成业务逻辑;
异步通信(支持同步):由于消息只需要进入MQ即可,完成同步转异步的操作;
提高系统吞吐、健壮性:调用链减少了,系统的健壮性和吞吐量提高了;

对于MQ做了如下约定:
应用层必须支持消息幂等
支持消息回溯
支持消息重放
消息的消费的机器IP以及消息时间

七、缓存

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

如果业务聚合层直接访问远端缓存,虽然减少了RPC调用次数以及DB的压力,但却增加了网络开销,所以调整为先本地缓存,如果没有命中则自动调用后端基础服务,基础服务首先会先用远端缓存,如未命中再查询DB;

对于缓存做了如下的改动:
使用自定义@anntation:程序员只需要在需要使用缓存的地方增加一个@Cache即可;
本地缓存TTL:本地缓存的TTL时间可以自动配置;
远程缓存 :如本地未命中可直接转发到远端缓存;
自动注册到配置中心 :所有的缓存节点和配置时间都再配置中心可见;
支持手动修改TTL时间 :可手动调整每个节点的TTL缓存时间;
防止缓存雪崩

八、分布式事务

用户在平台上支付他订购某种业务的时候,需要涉及到支付服务、账户服务、优惠券服务、积分服务,在单体模式下这种业务非常容易实现,通过事务即可完成,伪代码如下:

然而在微服务的情况下,原本通过简单事务处理的却变得非常负责,若引入两阶段提交(2PC)或者补偿事务(TCC)方案则系统的复杂程度会增加。我们的做法是通过本地事务+MQ消息的方式来解决:

消息上游:需要额外建一个tc_message表,并记录消息发送状态。消息表和业务数据在同一个数据库里面,而且要在一个事务里提交。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送;
消息上游:开启定时任务扫描tc_message表,如果超过设置的时间没有变更状态,会再次发送消息到MQ,如重试次数达到上限则发起告警操作;
消息下游:需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,需要发起业务回调通知业务方;

九、安全

暴露在外部的接口有身份认证、验签等步骤可以有效验证用户的合法性,但是服务内部之间的接口调用完全是透明的,无任何验证的。这个对于金融行业来说是不可容忍的,所以基于Dubbo的Filter机制做了一套认证授权系统。

Consumer:提供一个ACLFilter,读取给定的用户名和密码,把数据放入Attaches中传给Provier端;
Provider:提供一个ACLFilter,验证Consumer提交过来的用户名和密码,并验证IP,访问时间,对于异常访问的IP会发出告警信息;

十、持续集成

持续集成是微服务的必经过程,即团队开发成员经常集成他们的代码,通常每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽快地发现集成错误。

代码管理用git管理,Jenkins编译打包。每个模块独立一个工程,要求要求每天都提交代码,代码提交之后会进入静态代码扫描CI服务,通过Sonar服务器进行观察代码是否存在异常的代码;编译发布成功后,会自动触发接口测试功能,对于测试有异常的接口自动邮件通知相关人员。

十一、实战经验

1、线程池满
突然有反馈说服务非常慢,甚至出现异常,通过后台日志查询发现了如下的错误信息“Caused by: java.util.concurrent.RejectedExecutionException: Thread pool is EXHAUSTED! Thread Name: DubboServerHandler-XX.XX.XX.XX:XXXX, Pool Size: 200 (active: 200, core: 200, max: 200, largest: 200)“,因为所有服务设置的都是固定线程池大小,看到这样的错误日志,第一反应就是调整线程池大小,由200调整为500。但是运行一段时间后发现500后很快满了,改到1000很快又满了,增加服务器后线程池很快又满了,但是服务器的CPU和内存使用率都比较低。通过接口压测,又出现不了任何问题,问题似乎陷入僵局。突然运维说线上有大量慢查询SQL,通过分析这些SQL发现都是通过这个异常服务的SQL语句。优化掉这些SQL后,Dubbo线程池再也没有出现线程池满的情况。总结:高频调用服务的SQL响应时间一定要快,必须增加缓存,同时需要做熔断处理。

2、数据库CPU飚高
客服反馈经常在一段时期内系统响应非常慢,阻碍了客户的正常操作。此时观察日志后发现,同时刻后端大量基础服务抛出线程池满的异常。观察MYSQL数据库,发现CPU飙风到95%以上。但是当时的访问量并不大,是什么问题导致CPU占用如此之高?

运维分析后发现存在一次性将全部用户表的数据拉取出来的情况,当时注册用户量已经达到了3000W,一旦出现这样的情况必然导致CPU高。我们持久层框架选择MyBatis,SQL文件类似

如果传入的条件是空或者没有传任何条件,那么必然导致拉取全部数据。所以在内部做了约定,禁止不带条件的查询,并在框架层面做了一层拦截,如果PO对象是空那么直接抛出异常。总结:SQL查询语句禁止不带where条件的查询,禁止多表join查询,接口调用的DTO禁止为null或者禁止是一个没有任何数据的DTO对象;

3、无止境的循环依赖
由于服务之间通过接口调用,原则只要上发布出来的服务都可以,会形成A调用B,B也有可能调用A。本以为只需要在配置端增加check="false",例如
<dubbo:reference interface="com.**.UserService" check="false" />就可以解决了问题。但是上线后出现了RPC服务死循环般依赖调用,但是服务之间的依赖又是不可避免的,最终约定如下:基础服务(basic)层主要做数据库的操作和一些简单的业务逻辑,不允许调用其他任何服务;**

聚合服务(back)层,允许调用基础服务层,完成复杂的业务逻辑聚合操作;不运行调用其他back层.

4、接口定义规则
我们的服务端代码70%通过代码生成器完成,但是另外新增的接口是由业务端开发人员自己编写的,当初未定义规则,出现了PO层直接返回到业务聚合层,甚至使用Json或者Map方式来定义接口,给后续维护带来非常大的困难。约定:

接口数据透传规则:全部按照PO、DTO、VO来设计接口,禁止混用,因为数据定义的改变,影响面仅仅在调用方和被调用方,后续升级和扩展更方便;
接口参数类型:禁止json格式的参数、禁止Map类型的参数;

5、日志格式
微服务初期阶段,没有考虑到日志的打印策略和格式,大部分研发人员还是按照之前的方式来打印日志。当服务器数量增加时,查询线上日志变得非常困难。在启用ELK的时候缺发现日志格式无规则可循。约定:不管微服务在什么阶段,必须事先明确约定日志格式,例如:日志类别|所属于模块|业务操作|具体事件|操作人|自定义。有了统一的日志根式,方便后续日志的收集和报警处理。

总结
构建复杂的应用真的是非常困难,单体式的架构更适合轻量级的简单应用。如果你用它来开发复杂应用,那真的会很糟糕。微服务架构模式可以用来构建复杂应用,当然,这种架构模型也有自己的缺点和挑战。微服务实施过程中会出现各种各样异常信息,对于开发人员和架构师来说都是一种挑战。最后如果能真正成功实施微服务,无非是合理的组织架构、对于项目的认可、工具的合理化、持续集成过程。特别是在金融行业,尤其会关注到性能、准确性、数据一致性等一系列指标。Dubbo RPC框架的高效、稳定、可扩展这些特点,在整个服务化过程中起到了非常大的作用,线上运行中也一直稳定的提供服务。然而架构的演变是随着业务的演变而演变的,微服务入口层“网关“是整个微服务的最核心层,后续的文章中会介绍下本次架构演变过程中网关的演变历史,会介绍如何搭建一套支持亿级流量的网关。

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

10

添加新评论0 条评论

Ctrl+Enter 发表

本文隶属于专栏

最佳实践
不同的领域,都有先行者,实践者,用他们的最佳实践来加速更多企业的建设项目落地。

关于TWT  使用指南  社区专家合作  厂商入驻社区  企业招聘  投诉建议  版权与免责声明  联系我们
© 2019  talkwithtrend — talk with trend,talk with technologist 京ICP备09031017号-30