微服务越来越火。很多互联网公司,甚至一些传统行业的系统都采用了微服务架构。体会到微服务带来好处的同时,很多公司也明显感受到微服务化带来的一系列让人头疼的问题。本文是笔者对自己多年微服务化经历的总结。如果你正准备做微服务转型,或者在微服务化过程中遇到了困难。此文很可能会帮到你!
写在前面
正文开始前,为了让各位读友更好的理解本文内容,先花两分钟了解一下微服务的优缺点。
聊起微服务,很多朋友都了解微服务带来的好处,罗列几点:
微服务确实带来不少好处,那么微服务有没有什么问题呢?答案是肯定的!例如:
如何保障微服务系统稳定性?
互联网系统为大量的C端用户提供服务,如果隔三差五的出问题宕机,会严重影响用户体验,甚至导致用户流失。所以稳定性对互联网系统非常重要!接下来笔者根据自己的实际经验来聊聊基于微服务的互联网系统的稳定性。
雪崩效应产生原因,如何避免?
微服务化后,服务变多,调用链路变长,如果一个调用链上某个服务节点出问题,很可能引发整个调用链路崩溃,也就是所谓的雪崩效应。
举个例子,详细理解一下雪崩。如上图,现在有A,B,C三个服务,A调B,B调C。假如C发生故障,B方法1调用C方法1的请求不能及时返回,B的线程会发生阻塞等待。B会在一定时间后因为线程阻塞耗尽线程池所有线程,这时B就会无法响应A的请求。A调用B的请求不能及时返回,A的线程池线程资源也会逐渐被耗尽,最终A也无法对外提供服务。这样就引发了连锁故障,发生了雪崩。纵向:C故障引发B故障,B故障引发A故障,最终发生连锁故障。横向:方法1出问题,导致线程阻塞,进而线程池线程资源耗尽,最终服务内所有方法都无法访问,这就是“线程池污染”
为了避免雪崩效应,我们可以从两个方面考虑:
常用开源熔断隔离组件:Hystrix,Resilience4j
如何应对突发流量对服务的巨大压力?
促销活动或秒杀时,访问量往往会猛增数倍。技术团队在活动开始前一般都会根据预估访问量适当增加节点,但是假如流量预估少了(实际访问量远大于预估的访问量),系统就可能会被压垮。所以我们可以在网关层(Zuul,Gateway,Nginx等)做限流,如果访问量超出系统承载能力,就按照一定策略抛弃超出阈值的访问请求(也要注意用户体验,可以给用户返回一个友好的页面提示)。
可以从全局,IP,userID等多维度做限流。限流的两个主要目的:1,应对突发流量,避免系统被压垮(全局限流和IP限流)2,防刷,防止机器人脚本等频繁调用服务(userID限流和IP限流)
数据冗余
在核心链路上,服务可以冗余它依赖的服务的数据,依赖的服务故障时,服务尽量做到自保。比如订单服务依赖库存服务。我们可以在订单服务冗余库存数据(注意控制合理的安全库存,防超卖)。下单减库存时,如果库存服务挂了,我们可以直接从订单服务取库存。可以结合熔断一起使用,作为熔断的Fallback(后备)方案。
服务降级
可能很多人都听过服务降级,但是又不知道降级是怎么回事。实际上,上面说的熔断,限流,数据冗余,都属于服务降级的范畴。还有手动降级的例子,比如大促期间我们会关掉第三方物流接口,页面上也关掉物流查询功能,避免拖垮自己的服务。这种降级的例子很多。不管什么降级方式,目的都是让系统可用性更高,容错能力更强,更稳定。关于服务降级详见本文后面的内容。
缓存要注意什么?
关于隔离的考虑
CI测试&性能测试
CI测试,持续集成测试,在我们每次提交代码到发布分支前自动构建项目并执行所有测试用例,如果有测试用例执行失败,拒绝将代码合并到发布分支,本次集成失败。CI测试可以保证上线质量,适用于用例不会经常变化的稳定业务。
性能测试,为了保证上线性能,所有用户侧功能需要进行性能测试。上线前要保证性能测试通过。而且要定期做全链路压测,有性能问题可以及时发现。
监控
我们需要一套完善的监控系统,系统出问题时能够快速告警,最好是系统出问题前能提前预警。包括系统监控(CPU,内存,网络IO,带宽等监控),数据库监控(QPS,TPS,慢查询,大结果集等监控),缓存中间件监控(如Redis),JVM监控(堆内存,GC,线程等监控),全链路监控(pinpoint,skywaking,cat等),各种接口监控(QPS,TPS等)
CDN
可以充分利用CDN。除了提高用户访问速度之外,页面静态化之后存放到CDN,用CDN扛流量,可以大幅减少系统(源站)的访问压力。同时也减少了网站带宽压力。对系统稳定性非常有好处。
避免单点问题
除了服务要多点部署外,网关,数据库,缓存也要避免单点问题,至少要有一个Backup,而且要可以自动发现上线节点和自动摘除下线和故障节点。
网络带宽
避免带宽成为瓶颈,促销和秒杀开始前提前申请带宽。不光要考虑外网带宽,还要考虑内网带宽,有些旧服务器网口是千兆网口,访问量高时很可能会打满。
此外,一套完善的灰度发布系统,可以让上线更加平滑,避免上线大面积故障。DevOps工具,CI,CD对系统稳定性也有很大意义。
关于服务降级
提起服务降级,估计很多人都听说过,但是又因为亲身经历不多,所以可能不是很理解。下面结合具体实例从多方面详细阐述服务降级。
互联网分布式系统中,经常会有一些异常状况导致服务器压力剧增,比如促销活动时访问量会暴增,为了保证系统核心功能的稳定性和可用性,我们需要一些应对策略。这些应对策略也就是所谓的服务降级。下面根据笔者的实际经历,跟大家聊聊服务降级那些事儿。希望对大家有所启发!
关闭次要功能
在服务压力过大时,关闭非核心功能,避免核心功能被拖垮。
例如,电商平台基本都支持物流查询功能,而物流查询往往要依赖第三方物流公司的系统接口。物流公司的系统性能往往不会太好。所以我们经常会在双11这种大型促销活动期间把物流接口屏蔽掉,在页面上也关掉物流查询功能。这样就避免了我们自己的服务被拖垮,也保证了重要功能的正常运行。
降低一致性之读降级
对于读一致性要求不高的场景。在服务和数据库压力过大时,可以不读数据库,降级为只读缓存数据。以这种方式来减小数据库压力,提高服务的吞吐量。
例如,我们会把商品评论评价信息缓存在Redis中。在服务和数据库压力过大时,只读缓存中的评论评价数据,不在缓存中的数据不展示给用户。当然评论评价这种不是很重要的数据可以考虑用NOSQL数据库存储,不过我们曾经确实用Mysql数据库存储过评论评价数据。
降低一致性之写入降级
在服务压力过大时,可以将同步调用改为异步消息队列方式,来减小服务压力并提高吞吐量。既然把同步改成了异步也就意味着降低了数据一致性,保证数据最终一致即可。
例如,秒杀场景瞬间生成订单量很高。我们可以采取异步批量写数据库的方式,来减少数据库访问频次,进而降低数据库的写入压力。详细步骤:后端服务接到下单请求,直接放进消息队列,消费端服务取出订单消息后,先将订单信息写入Redis,每隔100ms或者积攒100条订单,批量写入数据库一次。前端页面下单后定时向后端拉取订单信息,获取到订单信息后跳转到支付页面。用这种异步批量写入数据库的方式大幅减少了数据库写入频次,从而明显降低了订单数据库写入压力。不过,因为订单是异步写入数据库的,就会存在数据库订单和相应库存数据暂时不一致的情况,以及用户下单后不能及时查到订单的情况。因为是降级方案,可以适当降低用户体验,所以我们保证数据最终一致即可。流程如下图:
屏蔽写入
很多高并发场景下,查询请求都会走缓存,这时数据库的压力主要是写入压力。所以对于某些不重要的服务,在服务和数据库压力过大时,可以关闭写入功能,只保留查询功能。这样可以明显减小数据库压力。
例如,商品的评论评价功能。为了减小压力,大促前可以关闭评论评价功能,关闭写接口,用户只能查看评论评价。而大部分查询请求走查询缓存,从而大幅减小数据库和服务的访问压力。
数据冗余
服务调用者可以冗余它所依赖服务的数据。当依赖的服务故障时,服务调用者可以直接使用冗余数据。
例如,我之前在某家自营电商公司。当时的商品服务依赖于价格服务,获取商品信息时,商品服务要调用价格服务获取商品价格。因为是自营电商,商品和SKU数量都不太多,一两万的样子。所以我们在商品服务冗余了价格数据。当价格服务故障后,商品服务还可以从自己冗余的数据中取到价格。当然这样做价格有可能不是最新的,但毕竟这是降级方案,牺牲一些数据准确性,换来系统的可用性还是很有意义的!(注:由于一个商品会有多个价格,比如普通价,会员价,促销直降价,促销满减价,所以我们把价格做成了单独的服务)
数据冗余可以结合熔断一起使用,实现自动降级。下面的熔断部分会详细说明。
熔断和Fallback
熔断是一种自动降级手段。当服务不可用时,用来避免连锁故障,雪崩效应。发生在服务调用的时候,在调用方做熔断处理。熔断的意义在于,调用方快速失败(Fail Fast),避免请求大量阻塞。并且保护被调用方。
详细解释一下,假设A服务调用B服务,B发生故障后,A开启熔断:
还是以电商的商品和价格服务为例。获取商品信息时,商品服务要调用价格服务获取商品价格。为了提高系统稳定性,我们要求各个服务要尽量自保。所以我们在商品服务加了熔断,当价格服务故障时,商品服务请求能够快速失败返回,保证商品服务不被拖垮,进而避免连锁故障。
看到这,可能有读者会问,快速失败后价格怎么返回呢?因为是自营电商,商品和SKU数量都不太多,一两万的样子。所以我们做了数据冗余,在商品服务冗余了价格数据。这样我们在熔断后获取价格的fallback方案就变成了从商品服务冗余的数据去取价格。下图为商品服务熔断关闭和开启的对比图。
开源熔断组件:Hystrix,Resilience4j等
限流
说起服务降级,就不可避免的要聊到限流。我们先考虑一个场景,例如电商平台要搞促销活动,我们按照预估的峰值访问量,准备了30台机器。但是活动开始后,实际参加的人数比预估的人数翻了5倍,这就远远超出了我们的服务处理能力,给后端服务、缓存、数据库等带来巨大的压力。随着访问请求的不断涌入,最终很可能造成平台系统崩溃。对于这种突发流量,我们可以通过限流来保护后端服务。因为促销活动流量来自于用户,用户的请求会先经过网关层再到后端服务,所以网关层是最合适的限流位置,如下图。
另外,考虑到用户体验问题,我们还需要相应的限流页面。当某些用户的请求被限流拦截后,把限流页面返回给用户。页面如下图。
另外一个场景,假如有一个核心服务,有几十个服务都调用他。如果其中一个服务调用者出了Bug,频繁调用这个核心服务,可能给这个核心服务造成非常大的压力,甚至导致这个核心服务无法响应。同时也会影响到调用他的几十个服务。所以每个服务也要根据自己的处理能力对调用者进行限制。
对于服务层的限流,我们一般可以利用spring AOP,以拦截器的方式做限流处理。这种做法虽然可以解决问题,但是问题也比较多。比如一个服务中有100个接口需要限流,我们就要写100个拦截器。而且限流阈值经常需要调整,又涉及到动态修改的问题。为了应对这些问题,很多公司会有专门的限流平台,新增限流接口和阈值变动可以直接在限流平台上配置。
关于限流,还有很多细节需要考虑,比如限流算法、毛刺现象等。篇幅原因,这些问题就不在本文讨论了。
开源网关组件:Nginx,Zuul,Gateway,阿里Sentinel等
服务降级总结和思考
上面我们结合具体案例解释了多种降级方式。实际上,关于服务降级的方式和策略,并没有什么定式,也没有标准可言。上面的降级方式也没有涵盖所有的情况。不同公司不同平台的做法也不完全一样。不过,所有的降级方案都要以满足业务需求为前提,都是为了提高系统的可用性,保证核心功能正常运行。
降级分类
一般我们可以把服务降级分为手动和自动两类。手动降级应用较多,可以通过开关的方式开启或关闭降级。自动降级,比如熔断和限流等属于自动降级的范畴。大多手动降级也可以做成自动的方式,可以根据各种系统指标设定合理阈值,在相应指标达到阈值上限自动开启降级。在很多场景下,由于业务过于复杂,需要参考的指标太多,自动降级实现起来难度会比较大,而且也很容易出错。所以在考虑做自动降级之前一定要充分做好评估,相应的自动降级方案也要考虑周全。
大规模分布式系统如何降级?
在大规模分布式系统中,经常会有成百上千的服务。在大促前往往会根据业务的重要程度和业务间的关系批量降级。这就需要技术和产品提前对业务和系统进行梳理,根据梳理结果确定哪些服务可以降级,哪些服务不可以降级,降级策略是什么,降级顺序怎么样。大型互联网公司基本都会有自己的降级平台,大部分降级都在平台上操作,比如手动降级开关,批量降级顺序管理,熔断阈值动态设置,限流阈值动态设置等等。
本节的主要目的是通过具体实例,让大家了解服务降级,并提供一些降级的思路。具体的降级方式和方案还是要取决于实际的业务场景和系统状况。
微服务架构下数据一致性问题
服务化后单体系统被拆分成多个服务,各服务访问自己的数据库。而我们的一次请求操作很可能要跨多个服务,同时要操作多个数据库的数据,我们发现以前用的数据库事务不好用了。那么基于微服务的架构如何保证数据一致性呢?
好,咱们这次就盘一盘分布式事务,最终一致,补偿机制,事务型消息!
提起这些,大家可能会想到两阶段提交,XA,TCC,Saga,还有最近阿里开源的Seata(Fescar),这些概念网上一大堆文章,不过都太泛泛,不接地气,让人看了云里雾里。
我们以TCC分布式事务和RocketMQ事务型消息为例,做详细分享!这个弄明白了,也就清楚分布式事务,最终一致,补偿机制这些概念啦!
TCC分布式事务
TCC(Try-Confirm-Cancel)是分布式事务的一种模式,可以保证不同服务的数据最终一致。目前有不少TCC开源框架,比如Hmily,ByteTCC,TCC-Transaction (我们之前用过Hmily和公司架构组自研组件)
下面以电商下单流程为例对TCC做详细阐述
流程图如下:
基本步骤如下:
1,修改订单状态为“已支付”
2,扣减库存
3,扣减优惠券
4,通知WMS(仓储管理系统)捡货出库(异步消息)
我们先看扣减库存,更新订单状态和扣减优惠券这三步同步调用,通知WMS的异步消息会在后面的“基于消息的最终一致”部分详细阐述!
下面是伪代码。不同公司的产品逻辑会有差异,相关代码逻辑也可能会有不同,大家不用纠结代码逻辑正确性。
public void makePayment() {
orderService.updateStatus(OrderStatus.Payed); //订单服务更新订单为已支付状态
inventoryService.decrStock(); //库存服务扣减库存
couponService.updateStatus(couponStatus.Used); //卡券服务更新优惠券为已使用状态
}
看完这段代码,大家可能觉得很简单!那么有什么问题吗?
答案是肯定的。没法保证数据一致性,也就是说不能保证这几步操作全部成功或者全部失败!因为这几步操作是在分布式环境下进行的,每个操作分布在不同的服务中,不同的服务又对应不同的数据库,本地事务已经用不上了!假如第一步更新订单为“已支付”成功了,第二步扣减库存时,库存服务挂了或者网络出问题了,导致扣减库存失败。你告诉用户支付成功了,但是库存没扣减。这怎么能行!
接下来,我们来看看TCC是如何帮我们解决这个问题并保证数据最终一致的。
TCC分为两个阶段:
阶段一:Try(预留冻结相关业务资源,设置临时状态,为下个阶段做准备)
阶段二:Confirm 或者 Cancel(Confirm:对资源进行最终操作,Cancel:取消资源)
第一阶段:Try
第二阶段:Confirm
假如第一阶段几个try操作都成功了!既然第一阶段已经预留了库存,而且订单状态和优惠券状态也设置了临时状态,第二阶段的确认提交从业务上来说应该没什么问题了。
Confirm 阶段我们需要做下面三件事:
第二阶段:Cancel
假如第一阶段失败了,
基于Hmily框架的代码:
//订单服务
public class OrderService{
//tcc接口
@Hmily(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus")
public void makePayment() {
更新订单状态为支付中
冻结库存,rpc调用
优惠券状态改为使用中,rpc调用
}
public void confirmOrderStatus() {
更新订单状态为已支付
}
public void cancelOrderStatus() {
恢复订单状态为待支付
}
}
//库存服务
public class InventoryService {
//tcc接口
@Hmily(confirmMethod = "confirmDecr", cancelMethod = "cancelDecr")
public void lockStock() {
//防悬挂处理(下面有说明)
if (分支事务记录表没有二阶段执行记录)
冻结库存
else
return;
}
public void confirmDecr() {
确认扣减库存
}
public void cancelDecr() {
释放冻结的库存
}
}
//卡券服务
public class CouponService {
//tcc接口
@Hmily(confirmMethod = "confirm", cancelMethod = "cancel")
public void handleCoupon() {
//防悬挂处理(下面有说明)
if (分支事务记录表没有二阶段执行记录)
优惠券状态更新为临时状态Inuse
else
return;
}
public void confirm() {
优惠券状态改为Used
}
public void cancel() {
优惠券状态恢复为Unused
}
}
疑问?
问题1:有些朋友可能会问了!这些关于流程的逻辑也要手动编码吗?这也太麻烦了吧!
实际上TCC分布式事务框架帮我们把这些事都干了。比如我们前面提到的Hmily,ByteTCC,TCC-transaction 这些框架。因为try,confirm,cancel这些操作都在TCC分布式事务框架控制范围之内,所以try的各个步骤成功了或者失败了,框架本身都知道,try成功了框架就会自动执行各个服务的confirm,try失败了框架就会执行各个服务的cancel(各个服务内部的TCC分布式事务框架会互相通信)。所以我们不用关心流程,只需要关注业务代码就可以啦!
问题2:仔细想想,好像还有问题!假如confirm阶段更新订单状态成功了,但是扣减库存失败了怎么办呢?
比如网络出问题了或者库存服务(宕机,重启)出问题了。当然,分布式事务框架也会考虑这些场景,框架会记录操作日志,假如confirm阶段扣减库存失败了,框架会不断重试调用库存服务直到成功(考虑性能问题,重试次数应该有限制)。cancel过程也是一样的道理。注意,既然需要重试,我们就要保证接口的幂等性。什么?不太懂幂等性。简单说:一个操作不管请求多少次,结果都要保证一样。这里就不详细介绍啦!
再考虑一个场景,try阶段冻结库存的时候,因为是rpc远程调用,在网络拥塞等情况下,是有可能超时的。假如冻结库存时发生超时,tcc框架会回滚(cancel)整个分布式事务,回滚完成后冻结库存请求才到达参与者(库存服务)并执行,这时被冻结的库存就没办法处理(恢复)了。这种情况称之为“悬挂”,也就是说预留的资源后续无法处理。解决方案:第二阶段已经执行,第一阶段就不再执行了,可以加一个“分支事务记录表”,如果表里有相关第二阶段的执行记录,就不再执行try(上面代码有防悬挂处理)。有人可能注意到还有些小纰漏,对,加锁,分布式环境下,我们可以考虑对第二阶段执行记录的查询和插入加上分布式锁,确保万无一失。
基于消息的最终一致
还是以上面的电商下单流程为例
上图,下单流程最后一步,通知WMS捡货出库,是异步消息走消息队列。
public void makePayment() {
orderService.updateStatus(OrderStatus.Payed); //订单服务更新订单为已支付状态
inventoryService.decrStock(); //库存服务扣减库存
couponService.updateStatus(couponStatus.Used); //卡券服务更新优惠券为已使用状态
发送MQ消息捡货出库; //发送消息通知WMS捡货出库
}
按上面代码,大家不难发现问题!如果发送捡货出库消息失败,数据就会不一致!有人说我可以在代码上加上重试逻辑和回退逻辑,发消息失败就重发,多次重试失败所有操作都回退。这样一来逻辑就会特别复杂,回退失败要考虑,而且还有可能消息已经发送成功了,但是由于网络等问题发送方没得到MQ的响应,这些问题都要考虑进来!
幸好,有些消息队列帮我们解决了这些问题。比如阿里开源的RocketMQ(目前已经是Apache开源项目),4.3.0版本开始支持事务型消息(实际上早在贡献给Apache之前曾经支持过事务消息,后来被阉割了,4.3.0版本重新开始支持事务型消息)。
先看看RocketMQ发送事务型消息的流程:
1,发送半消息(所有事务型消息都要经历确认过程,从而确定最终提交或回滚(抛弃消息),未被确认的消息称为“半消息”或者“预备消息”,“待确认消息”)
2,半消息发送成功并响应给发送方
3,执行本地事务,根据本地事务执行结果,发送提交或回滚的确认消息
4,如果确认消息丢失(网络问题或者生产者故障等问题),MQ向发送方回查执行结果
5,根据上一步骤回查结果,确定提交或者回滚(抛弃消息)
看完事务型消息发送流程,有些读者可能没有完全理解,不要紧,我们来分析一下!
疑问?
问题1:假如发送方发送半消息失败怎么办?
半消息(待确认消息)是消息发送方发送的,如果失败,发送方自己是知道的并可以做相应处理。
问题2:假如发送方执行完本地事务后,发送确认消息通知MQ提交或回滚消息时失败了(网络问题,发送方重启等情况),怎么办?
没关系,当MQ发现一个消息长时间处于半消息(待确认消息)的状态,MQ会以定时任务的方式主动回查发送方并获取发送方执行结果。这样即便出现网络问题或者发送方本身的问题(重启,宕机等),MQ通过定时任务主动回查发送方基本都能确认消息最终要提交还是回滚(抛弃)。当然出于性能和半消息堆积方面的考虑,MQ本身也会有回查次数的限制。
问题3:如何保证消费一定成功呢?
RocketMQ本身有ack机制,来保证消息能够被正常消费。如果消费失败(消息订阅方出错,宕机等原因),RocketMQ会把消息重发回Broker,在某个延迟时间点后(默认10秒后)重新投递消息。
结合上面几个同步调用hmily完整代码如下:
//TransactionListener是rocketmq接口用于回调执行本地事务和状态回查
public class TransactionListenerImpl implements TransactionListener {
//执行本地事务
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
记录orderID,消息状态键值对到共享map中,以备MQ回查消息状态使用;
return LocalTransactionState.COMMIT_MESSAGE;
}
//回查发送者状态
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
String status = 从共享map中取出orderID对应的消息状态;
if("commit".equals(status))
return LocalTransactionState.COMMIT_MESSAGE;
else if("rollback".equals(status))
return LocalTransactionState.ROLLBACK_MESSAGE;
else
return LocalTransactionState.UNKNOW;
}
}
//订单服务
public class OrderService{
//tcc接口
@Hmily(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus")
public void makePayment() {
1,更新订单状态为支付中
2,冻结库存,rpc调用
3,优惠券状态改为使用中,rpc调用
4,发送半消息(待确认消息)通知WMS捡货出库 //创建producer时这册TransactionListenerImpl
}
public void confirmOrderStatus() {
更新订单状态为已支付
}
public void cancelOrderStatus() {
恢复订单状态为待支付
}
}
//库存服务
public class InventoryService {
//tcc接口
@Hmily(confirmMethod = "confirmDecr", cancelMethod = "cancelDecr")
public void lockStock() {
//防悬挂处理
if (分支事务记录表没有二阶段执行记录)
冻结库存
else
return;
}
public void confirmDecr() {
确认扣减库存
}
public void cancelDecr() {
释放冻结的库存
}
}
//卡券服务
public class CouponService {
//tcc接口
@Hmily(confirmMethod = "confirm", cancelMethod = "cancel")
public void handleCoupon() {
//防悬挂处理
if (分支事务记录表没有二阶段执行记录)
优惠券状态更新为临时状态Inuse
else
return;
}
public void confirm() {
优惠券状态改为Used
}
public void cancel() {
优惠券状态恢复为Unused
}
}
如果执行到TransactionListenerImpl.executeLocalTransaction方法,说明半消息已经发送成功了,也说明OrderService.makePayment方法的四个步骤都执行成功了,此时tcc也到了confirm阶段,所以在TransactionListenerImpl.executeLocalTransaction方法里可以直接返回LocalTransactionState.COMMIT_MESSAGE 让 MQ提交这条消息,同时将该订单信息和对应的消息状态保存在共享map里,以备确认消息发送失败时MQ回查消息状态使用。
微服务化过程,无感知数据迁移
微服务化,其中一个重要意义在于数据隔离,即不同的服务对应各自的数据库表,避免不同业务模块间数据的耦合。这也就意味着微服务化过程要拆分现有数据库,把单体数据库根据业务模块拆分成多个,进而涉及到数据迁移。
数据迁移过程我们要注意哪些关键点呢?第一,保证迁移后数据准确不丢失,即每条记录准确而且不丢失记录;第二,不影响用户体验(尤其是访问量高的C端业务需要不停机平滑迁移);第三,保证迁移后的性能和稳定性。
数据迁移我们经常遇到的两个场景:
1,业务重要程度一般或者是内部系统,数据结构不变,这种场景下可以采用挂从库,数据同步完找个访问低谷时间段,停止服务,然后将从库切成主库,再启动服务。简单省时,不过需要停服避免切主库过程数据丢失
2,重要业务,并发高,数据结构改变。这种场景一般需要不停机平滑迁移。下面就重点介绍这部分经历。
互联网行业,很多业务访问量很大,即便凌晨低谷时间,仍然有相当的访问量,为了不影响用户体验,很多公司对这些业务会采用不停机平滑迁移的方式。因为对老数据迁移的同时,线上还不断有用户访问,不断有数据产生,不断有数据更新,所以我们不但要考虑老数据迁移的问题,还要考虑数据更新和产生新数据的问题。下面介绍一下我们之前的做法。
关键步骤如下:
第二步的老数据迁移脚本程序和第三步的检验程序可以工具化,以后再做类似的数据迁移可以复用。
目前各云服务平台也提供数据迁移解决方案,大家有兴趣也可以了解一下!
全链路APM监控
在体会到微服务带来好处的同时,很多公司也会明显感受到微服务化后那些让人头疼的问题。比如,服务化之后调用链路变长,排查性能问题可能要跨多个服务,定位问题更加困难;服务变多,服务间调用关系错综复杂,以至于很多工程师不清楚服务间的依赖和调用关系,之后的系统维护过程也会更加艰巨。诸如此类的问题还很多!
这时就迫切需要一个工具帮我们解决这些问题,于是APM全链路监控工具就应运而生了。有开源的Pinpoint、Skywalking等,也有收费的Saas服务听云、OneAPM等。有些实力雄厚的公司也会自研APM。
下面我们介绍一下如何利用开源APM工具Pinpoint应对上述问题。
拓扑图
微服务化后,服务数量变多,服务间调用关系也变得更复杂,以至于很多工程师不清楚服务间的依赖和调用关系,给系统维护带来很多困难。通过拓扑图我们可以清晰地看到服务与服务,服务与数据库,服务与缓存中间件的调用和依赖关系。对服务关系了如指掌之后,也可以避免服务间循依赖、循环调用的问题。
请求调用栈(Call Stack)监控
微服务化后,服务变多,调用链路变长,跨多个服务排查问题会更加困难。上图是一个请求的调用栈,我们可以清晰看到一次请求调用了哪些服务和方法、各个环节的耗时以及发生在哪个服节点。上图的请求耗时过长,根据监控(红框部分)我们可以看到时间主要消耗在数据库SQL语句上。点击数据库图表还可以看详细sql语句,如下图:
如果发生错误,会显示为红色,错误原因也会直接显示出来。如下图:
类似性能问题和错误的线上排查。我们如果通过查日志的传统办法,可能会耗费大量的时间。但是通过APM工具分分钟就可以搞定了!
请求Server Map
Server Map是Pinpoint另一个比较重要的功能。如上图,我们不但能清晰地看到一个请求的访问链路,而且还能看到每个节点的访问次数,为系统优化提供了有力的依据。如果一次请求访问了多次数据库,说明代码逻辑可能有必要优化了!
JVM监控
此外,Pinpoint还支持堆内存,活跃线程,CPU,文件描述符等监控。