关于微服务:微服务应该这么搞万字长文谈微服务经历-IDCF

6次阅读

共计 15770 个字符,预计需要花费 40 分钟才能阅读完成。

微服务越来越火。很多互联网公司,甚至一些传统行业的零碎都采纳了微服务架构。领会到微服务带来益处的同时,很多公司也显著感触到微服务化带来的一系列让人头疼的问题。本文是笔者对本人多年微服务化经验的总结。如果你正筹备做微服务转型,或者在微服务化过程中遇到了艰难。此文很可能会帮到你!

写在后面

注释开始前,为了让各位读友更好的了解本文内容,先花两分钟理解一下微服务的优缺点。

聊起微服务,很多敌人都理解微服务带来的益处,列举几点:

  • 模块化,升高耦合。将单体利用按业务模块拆分成多个服务,如果某个性能须要改变,大多数状况,咱们只须要弄清楚并改变对应的服务即可。只改变一小部分就能满足要求,升高了其余业务模块受影响的可能性。从而升高了业务模块间的耦合性。
  • 屏蔽与本身业务无关技术细节。例如,很多业务须要查问用户信息,在单体利用的状况下,所有业务场景都通过 DAO 去查问用户信息,随着业务倒退,并发量减少,用户信息须要加缓存,这样所有业务场景都须要关注缓存,微服务化之后,缓存由各自服务保护,其余服务调用相干服务即可,不须要关注相似的缓存问题。
  • 数据隔离,防止不同业务模块间的数据耦合。不同的服务对应不同数据库表,服务之间通过服务调用的形式来获取数据。
  • 业务边界清晰,代码边界清晰。单体架构中不同的业务,代码耦合重大,随着业务量增长,业务简单后,一个小性能点的批改就可能影响到其余业务点,开发品质不可控,测试须要回归,老本继续进步。
  • 显著缩小代码抵触。在单体利用中,很多人在同一个工程上开发,会有大量的代码抵触问题。微服务化后,按业务模块拆分成多个服务,每个服务由专人负责,无效缩小代码抵触问题。
  • 可复用,显著缩小代码拷贝景象。

微服务的确带来不少益处,那么微服务有没有什么问题呢?答案是必定的!例如:

  • 微服务零碎稳定性问题。微服务化后服务数量大幅减少,一个服务故障就可能引发大面积系统故障。比方服务雪崩,连锁故障。当一个服务故障后,依赖他的服务受到牵连也产生故障。
  • 服务调用关系盘根错节,链路过长,问题难定位。微服务化后,服务数量剧增,大量的服务治理起来会变的更加简单。因为调用链路变长,定位问题也会更加艰难。
  • 数据一致性问题。微服务化后单体零碎被拆分成多个服务,各服务拜访本人的数据库。而咱们的一次申请操作很可能要跨多个服务,同时要操作多个数据库的数据,咱们发现以前用的数据库事务不好用了。跨服务的数据一致性和数据完整性问题也就随之而来了。
  • 微服务化过程中,用户无感知数据库拆分、数据迁徙的挑战。

一、如何保障微服务零碎稳定性?

互联网零碎为大量的 C 端用户提供服务,如果隔三差五的出问题宕机,会重大影响用户体验,甚至导致用户散失。所以稳定性对互联网零碎十分重要!接下来笔者依据本人的理论教训来聊聊基于微服务的互联网零碎的稳定性。

1.1 雪崩效应产生起因,如何防止?

微服务化后,服务变多,调用链路变长,如果一个调用链上某个服务节点出问题,很可能引发整个调用链路解体,也就是所谓的雪崩效应。

举个例子,具体了解一下雪崩。如上图,当初有 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 出问题,导致线程阻塞,进而线程池线程资源耗尽,最终服务内所有办法都无法访问,这就是“线程池净化”

为了防止雪崩效应,咱们能够从两个方面思考:

  • 在服务间加熔断。解决服务间纵向连锁故障问题。比方在 A 服务加熔断,当 B 故障时,开启熔断,A 调用 B 的申请不再发送到 B,间接疾速返回。这样就防止了线程期待的问题。当然疾速返回什么,fallback 计划是什么,也须要依据具体场景,比方返回默认值或者调用其余备用服务接口。如果你的场景适宜异步通信,能够采纳音讯队列,这样也能够无效防止同步调用的线程期待问题。

  • 服务内(JVM 内)线程隔离。解决横向线程池净化的问题。为了防止因为一个办法出问题导致线程期待最终引发线程资源耗尽的问题,咱们能够对 tomcat,dubbo 等的线程池分成多个小线程组,每个线程组服务于不同的类或办法。一个办法出问题,只影响本人不影响其余办法和类。

罕用开源熔断隔离组件:Hystrix,Resilience4j

1.2 如何应答突发流量对服务的微小压力?

促销流动或秒杀时,访问量往往会猛增数倍。技术团队在流动开始前个别都会依据预估访问量适当减少节点,然而如果流量预估少了(理论访问量远大于预估的访问量),零碎就可能会被压垮。所以咱们能够在网关层(Zuul,Gateway,Nginx 等)做限流,如果访问量超出零碎承载能力,就依照肯定策略摈弃超出阈值的拜访申请(也要留神用户体验,能够给用户返回一个敌对的页面提醒)。

能够从全局,IP,userID 等多维度做限流。限流的两个次要目标:1,应答突发流量,防止零碎被压垮(全局限流和 IP 限流)2,防刷,避免机器人脚本等频繁调用服务(userID 限流和 IP 限流)

1.3 数据冗余

在外围链路上,服务能够冗余它依赖的服务的数据,依赖的服务故障时,服务尽量做到自保。比方订单服务依赖库存服务。咱们能够在订单服务冗余库存数据(留神管制正当的平安库存,防超卖)。下单减库存时,如果库存服务挂了,咱们能够间接从订单服务取库存。能够联合熔断一起应用,作为熔断的 Fallback(后备)计划。

1.4 服务降级

可能很多人都听过服务降级,然而又不晓得降级是怎么回事。实际上,下面说的熔断,限流,数据冗余,都属于服务降级的领域。还有手动降级的例子,比方大促期间咱们会关掉第三方物流接口,页面上也关掉物流查问性能,防止拖垮本人的服务。这种降级的例子很多。不论什么降级形式,目标都是让零碎可用性更高,容错能力更强,更稳固。对于服务降级详见本文前面的内容。

1.5 缓存要留神什么?

  • 缓存穿透。对于数据库中基本不存在的值,申请缓存时要在缓存记录一个空值,防止每次申请都打到数据库。
  • 缓存雪崩。在某一时间缓存数据集中生效,导致大量申请穿透到数据库,将数据库压垮。能够在初始化数据时,差异化各个 key 的缓存生效工夫,生效工夫 = 一个较大的固定值 + 较小的随机值。
  • 缓存热点。有些热点数据访问量会特地大,单个缓存节点(例如 Redis)无奈撑持这么大的访问量。如果是读申请访问量大,能够思考读写拆散,一主多从的计划,用从节点摊派读流量;如果是写申请访问量大,能够采纳集群分片计划,用分片摊派写流量。以秒杀扣减库存为例,如果秒杀库存是 100,能够分成 5 片,每片存 20 个库存。

1.6 对于隔离的思考

  • 部署隔离:咱们常常会遇到秒杀业务和日常业务依赖同一个服务,以及 C 端服务和外部经营零碎依赖同一个服务的状况,比如说都依赖订单服务。而秒杀零碎的霎时访问量很高,可能会对服务带来微小的压力,甚至压垮服务。外部经营零碎也常常有批量数据导出的操作,同样会给服务带来肯定的压力。这些都是不稳固因素。所以咱们能够将这些独特依赖的服务分组部署,不同的分组服务于不同的业务,防止互相烦扰。
  • 数据隔离:极其状况下还须要缓存隔离,数据库隔离。以秒杀为例,库存和订单的缓存(Redis)和数据库须要独自部署!数据隔离后,秒杀订单和日常订单不在雷同的数据库,之后的订单查问怎么展现?能够采纳相应的数据同步策略。比方,在创立秒杀订单后发音讯到音讯队列,日常订单服务收到音讯后将订单写入日常订单库。留神,要思考数据的一致性,能够应用事务型音讯。
  • 业务隔离:还是以秒杀为例。从业务上把秒杀和日常的售卖辨别开来,把秒杀做为营销流动,要参加秒杀的商品须要提前报名加入流动,这样咱们就能提前晓得哪些商家哪些商品要参加秒杀,能够依据提报的商品提前生成商品详情动态页面并上传到 CDN 预热,提报的商品库存也须要提前预热,能够将商品库存在流动开始前预热到 Redis,防止秒杀开始后大量拜访穿透到数据库。

1.7 CI 测试 & 性能测试

  • CI 测试,继续集成测试,在咱们每次提交代码到公布分支前主动构建我的项目并执行所有测试用例,如果有测试用例执行失败,回绝将代码合并到公布分支,本次集成失败。CI 测试能够保障上线品质,实用于用例不会常常变动的稳固业务。
  • 性能测试,为了保障上线性能,所有用户侧性能须要进行性能测试。上线前要保障性能测试通过。而且要定期做全链路压测,有性能问题能够及时发现。

1.8 监控

咱们须要一套欠缺的监控零碎,零碎出问题时可能疾速告警,最好是零碎出问题前能提前预警。包含系统监控(CPU,内存,网络 IO,带宽等监控),数据库监控(QPS,TPS,慢查问,大后果集等监控),缓存中间件监控(如 Redis),JVM 监控(堆内存,GC,线程等监控),全链路监控(pinpoint,skywaking,cat 等),各种接口监控(QPS,TPS 等)

1.9 CDN

能够充分利用 CDN。除了进步用户访问速度之外,页面动态化之后寄存到 CDN,用 CDN 扛流量,能够大幅缩小零碎(源站)的拜访压力。同时也缩小了网站带宽压力。对系统稳定性十分有益处。

1.10 防止单点问题

除了服务要多点部署外,网关,数据库,缓存也要防止单点问题,至多要有一个 Backup,而且要能够主动发现上线节点和主动摘除下线和故障节点。

1.11 网络带宽

防止带宽成为瓶颈,促销和秒杀开始前提前申请带宽。不光要思考外网带宽,还要思考内网带宽,有些旧服务器网口是千兆网口,访问量高时很可能会打满。

此外,一套欠缺的灰度公布零碎,能够让上线更加平滑,防止上线大面积故障。DevOps 工具,CI,CD 对系统稳定性也有很大意义。

二、对于服务降级

提起服务降级,预计很多人都据说过,然而又因为亲身经历不多,所以可能不是很了解。上面联合具体实例从多方面具体论述服务降级。

互联网分布式系统中,常常会有一些异样情况导致服务器压力剧增,比方促销流动时访问量会暴增,为了保障系统核心性能的稳定性和可用性,咱们须要一些应答策略。这些应答策略也就是所谓的服务降级。上面依据笔者的理论经验,跟大家聊聊服务降级那些事儿。心愿对大家有所启发!

2.1 敞开主要性能

在服务压力过大时,敞开非核心性能,防止外围性能被拖垮。

例如,电商平台根本都反对物流查问性能,而物流查问往往要依赖第三方物流公司的零碎接口。物流公司的零碎性能往往不会太好。所以咱们常常会在双 11 这种大型促销流动期间把物流接口屏蔽掉,在页面上也关掉物流查问性能。这样就防止了咱们本人的服务被拖垮,也保障了重要性能的失常运行。

2.2 升高一致性之读降级

对于读一致性要求不高的场景。在服务和数据库压力过大时,能够不读数据库,降级为只读缓存数据。以这种形式来减小数据库压力,进步服务的吞吐量。

例如,咱们会把商品评论评估信息缓存在 Redis 中。在服务和数据库压力过大时,只读缓存中的评论评估数据,不在缓存中的数据不展现给用户。当然评论评估这种不是很重要的数据能够思考用 NOSQL 数据库存储,不过咱们已经的确用 Mysql 数据库存储过评论评估数据。

2.3 升高一致性之写入降级

在服务压力过大时,能够将同步调用改为异步音讯队列形式,来减小服务压力并进步吞吐量。既然把同步改成了异步也就意味着升高了数据一致性,保证数据最终统一即可。

例如,秒杀场景霎时生成订单量很高。咱们能够采取异步批量写数据库的形式,来缩小数据库拜访频次,进而升高数据库的写入压力。具体步骤:后端服务接到下单申请,间接放进音讯队列,生产端服务取出订单音讯后,先将订单信息写入 Redis,每隔 100ms 或者积攒 100 条订单,批量写入数据库一次。前端页面下单后定时向后端拉取订单信息,获取到订单信息后跳转到领取页面。

用这种异步批量写入数据库的形式大幅缩小了数据库写入频次,从而明显降低了订单数据库写入压力。不过,因为订单是异步写入数据库的,就会存在数据库订单和相应库存数据临时不统一的状况,以及用户下单后不能及时查到订单的状况。因为是降级计划,能够适当升高用户体验,所以咱们保证数据最终统一即可。

流程如下图:

2.4 屏蔽写入

很多高并发场景下,查问申请都会走缓存,这时数据库的压力次要是写入压力。所以对于某些不重要的服务,在服务和数据库压力过大时,能够敞开写入性能,只保留查问性能。这样能够显著减小数据库压力。

例如,商品的评论评估性能。为了减小压力,大促前能够敞开评论评估性能,敞开写接口,用户只能查看评论评估。而大部分查问申请走查问缓存,从而大幅减小数据库和服务的拜访压力。

2.5 数据冗余

服务调用者能够冗余它所依赖服务的数据。当依赖的服务故障时,服务调用者能够间接应用冗余数据。

例如,我之前在某家自营电商公司。过后的商品服务依赖于价格服务,获取商品信息时,商品服务要调用价格服务获取商品价格。因为是自营电商,商品和 SKU 数量都不太多,一两万的样子。所以咱们在商品服务冗余了价格数据。当价格服务故障后,商品服务还能够从本人冗余的数据中取到价格。

当然这样做价格有可能不是最新的,但毕竟这是降级计划,就义一些数据准确性,换来零碎的可用性还是很有意义的!(注:因为一个商品会有多个价格,比方一般价,会员价,促销直提价,促销满减价,所以咱们把价格做成了独自的服务)

数据冗余能够联合熔断一起应用,实现主动降级。上面的熔断局部会具体阐明。

2.6 熔断和 Fallback

熔断是一种主动降级伎俩。当服务不可用时,用来防止连锁故障,雪崩效应。产生在服务调用的时候,在调用方做熔断解决。熔断的意义在于,调用方疾速失败(Fail Fast),防止申请大量阻塞。并且爱护被调用方。

具体解释一下,假如 A 服务调用 B 服务,B 产生故障后,A 开启熔断:

  • 对于调用方 A:申请在 A 间接疾速返回,疾速失败,不再发送到 B。防止因为 B 故障,导致 A 的申请线程继续期待,进而导致线程池线程和 CPU 资源耗尽,最终导致 A 无奈响应,甚至整条调用链故障。
  • 对于被调用方 B:熔断后,申请被 A 拦挡,不再发送到 B,B 压力失去缓解,防止了仍旧存活的 B 被压垮,B 失去了爱护。

还是以电商的商品和价格服务为例。获取商品信息时,商品服务要调用价格服务获取商品价格。为了进步零碎稳定性,咱们要求各个服务要尽量自保。所以咱们在商品服务加了熔断,当价格服务故障时,商品服务申请可能疾速失败返回,保障商品服务不被拖垮,进而防止连锁故障。

看到这,可能有读者会问,疾速失败后价格怎么返回呢?因为是自营电商,商品和 SKU 数量都不太多,一两万的样子。所以咱们做了数据冗余,在商品服务冗余了价格数据。这样咱们在熔断后获取价格的 fallback 计划就变成了从商品服务冗余的数据去取价格。下图为商品服务熔断敞开和开启的比照图。

开源熔断组件:Hystrix,Resilience4j 等

2.7 限流

说起服务降级,就不可避免的要聊到限流。咱们先思考一个场景,例如电商平台要搞促销流动,咱们依照预估的峰值访问量,筹备了 30 台机器。然而流动开始后,理论加入的人数比预估的人数翻了 5 倍,这就远远超出了咱们的服务解决能力,给后端服务、缓存、数据库等带来微小的压力。

随着拜访申请的一直涌入,最终很可能造成平台零碎解体。对于这种突发流量,咱们能够通过限流来爱护后端服务。因为促销流动流量来自于用户,用户的申请会先通过网关层再到后端服务,所以网关层是最合适的限流地位,如下图。

另外,思考到用户体验问题,咱们还须要相应的限流页面。当某些用户的申请被限流拦挡后,把限流页面返回给用户。页面如下图。

另外一个场景,如果有一个外围服务,有几十个服务都调用他。如果其中一个服务调用者出了 Bug,频繁调用这个外围服务,可能给这个外围服务造成十分大的压力,甚至导致这个外围服务无奈响应。同时也会影响到调用他的几十个服务。所以每个服务也要依据本人的解决能力对调用者进行限度。

对于服务层的限流,咱们个别能够利用 spring AOP,以拦截器的形式做限流解决。这种做法尽管能够解决问题,然而问题也比拟多。比方一个服务中有 100 个接口须要限流,咱们就要写 100 个拦截器。而且限流阈值常常须要调整,又波及到动静批改的问题。为了应答这些问题,很多公司会有专门的限流平台,新增限流接口和阈值变动能够间接在限流平台上配置。

对于限流,还有很多细节须要思考,比方限流算法、毛刺景象等。篇幅起因,这些问题就不在本文探讨了。

开源网关组件:Nginx,Zuul,Gateway,阿里 Sentinel 等

2.8 服务降级总结和思考

下面咱们联合具体案例解释了多种降级形式。实际上,对于服务降级的形式和策略,并没有什么定式,也没有规范可言。下面的降级形式也没有涵盖所有的状况。不同公司不同平台的做法也不齐全一样。不过,所有的降级计划都要以满足业务需要为前提,都是为了进步零碎的可用性,保障外围性能失常运行。

2.9 降级分类

个别咱们能够把服务降级分为手动和主动两类。手动降级利用较多,能够通过开关的形式开启或敞开降级。主动降级,比方熔断和限流等属于主动降级的领域。大多手动降级也能够做成主动的形式,能够依据各种零碎指标设定正当阈值,在相应指标达到阈值下限主动开启降级。在很多场景下,因为业务过于简单,须要参考的指标太多,主动降级实现起来难度会比拟大,而且也很容易出错。所以在思考做主动降级之前肯定要充沛做好评估,相应的主动降级计划也要思考周全。

2.10 大规模分布式系统如何降级?

在大规模分布式系统中,常常会有成千盈百的服务。在大促前往往会依据业务的重要水平和业务间的关系批量降级。这就须要技术和产品提前对业务和零碎进行梳理,依据梳理后果确定哪些服务能够降级,哪些服务不能够降级,降级策略是什么,降级程序怎么样。大型互联网公司根本都会有本人的降级平台,大部分降级都在平台上操作,比方手动降级开关,批量降级程序治理,熔断阈值动静设置,限流阈值动静设置等等。

本节的次要目标是通过具体实例,让大家理解服务降级,并提供一些降级的思路。具体的降级形式和计划还是要取决于理论的业务场景和零碎情况。

三、微服务架构下数据一致性问题

服务化后单体零碎被拆分成多个服务,各服务拜访本人的数据库。而咱们的一次申请操作很可能要跨多个服务,同时要操作多个数据库的数据,咱们发现以前用的数据库事务不好用了。那么基于微服务的架构如何保证数据一致性呢?

好,咱们这次就盘一盘分布式事务,最终统一,弥补机制,事务型音讯!

提起这些,大家可能会想到两阶段提交,XA,TCC,Saga,还有最近阿里开源的 Seata(Fescar),这些概念网上一大堆文章,不过都太泛泛,不接地气,让人看了云里雾里。

咱们以 TCC 分布式事务和 RocketMQ 事务型音讯为例,做具体分享!这个弄明确了,也就分明分布式事务,最终统一,弥补机制这些概念啦!

3.1 TCC 分布式事务

TCC(Try-Confirm-Cancel)是分布式事务的一种模式,能够保障不同服务的数据最终统一。目前有不少 TCC 开源框架,比方 Hmily,ByteTCC,TCC-Transaction(咱们之前用过 Hmily 和公司架构组自研组件)

上面以电商下单流程为例对 TCC 做具体论述

流程图如下:

根本步骤如下:

  • 批改订单状态为“已领取”
  • 扣减库存
  • 扣减优惠券
  • 告诉 WMS(仓储管理系统)捡货出库(异步音讯)

咱们先看扣减库存,更新订单状态和扣减优惠券这三步同步调用,告诉 WMS 的异步音讯会在前面的“基于音讯的最终统一”局部具体论述!

上面是伪代码。不同公司的产品逻辑会有差别,相干代码逻辑也可能会有不同,大家不必纠结代码逻辑正确性。

public void makePayment() {orderService.updateStatus(OrderStatus.Payed); // 订单服务更新订单为已领取状态
   inventoryService.decrStock(); // 库存服务扣减库存
   couponService.updateStatus(couponStatus.Used); // 卡券服务更新优惠券为已应用状态      
}

看完这段代码,大家可能感觉很简略!那么有什么问题吗?

答案是必定的。没法保证数据一致性,也就是说不能保障这几步操作全副胜利或者全副失败!因为这几步操作是在分布式环境下进行的,每个操作散布在不同的服务中,不同的服务又对应不同的数据库,本地事务曾经用不上了!如果第一步更新订单为“已领取”胜利了,第二步扣减库存时,库存服务挂了或者网络出问题了,导致扣减库存失败。你通知用户领取胜利了,然而库存没扣减。这怎么能行!

接下来,咱们来看看 TCC 是如何帮咱们解决这个问题并保证数据最终统一的。
TCC 分为两个阶段:

  • 阶段一:Try(预留解冻相干业务资源,设置长期状态,为下个阶段做筹备)
  • 阶段二:Confirm 或者 Cancel(Confirm:对资源进行最终操作,Cancel:勾销资源)

第一阶段:Try

  • 更新订单状态:此时因为还没真正实现整个流程,订单状态不能间接改成已领取状态。能够加一个长期状态 Paying,表明订单正在领取中,领取后果临时还不分明!
  • 解冻库存:假如当初可销售库存 stock 是 10,这单扣减 1 个库存,别间接把库存减掉,而是在表中加一个解冻字段 locked\_stock,locked\_stock 加 1,再给 stock 减 1,这样就相当于解冻了 1 个库存。两个操作放在一个本地事务里。
  • 更新优惠券状态:优惠券加一个长期状态 Inuse,表明优惠券正在应用中,具体有没有失常被应用临时还不分明!

第二阶段:Confirm

如果第一阶段几个 try 操作都胜利了!既然第一阶段曾经预留了库存,而且订单状态和优惠券状态也设置了长期状态,第二阶段的确认提交从业务上来说应该没什么问题了。

Confirm 阶段咱们须要做上面三件事:

  • 先将订单状态从 Paying 改为已领取 Payed,订单状态也实现了。
  • 再将解冻的库存复原 locked\_stock 减 1,stock 第一阶段曾经减掉 1 是 9 了,到此扣减库存就真正实现了。
  • 再将优惠券状态从 Inuse 改为 Used,表明优惠券曾经被失常应用。

第二阶段:Cancel

如果第一阶段失败了,

  • 先将订单状态从 Paying 复原为待领取 UnPayed。
  • 再将解冻的库存还回到可销售库存中,stock 加 1 复原成 10,locked\_stock 减 1,能够放在一个本地事务实现。
  • 再将优惠券状态从 Inuse 复原为未应用 Unused。

基于 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(下面代码有防悬挂解决)。有人可能留神到还有些小纰漏,对,加锁,分布式环境下,咱们能够思考对第二阶段执行记录的查问和插入加上分布式锁,确保十拿九稳。

3.2 基于音讯的最终统一

还是以下面的电商下单流程为例

上图,下单流程最初一步,告诉 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 发送事务型音讯的流程:

  • 发送半音讯(所有事务型音讯都要经验确认过程,从而确定最终提交或回滚(摈弃音讯),未被确认的音讯称为“半音讯”或者“准备音讯”,“待确认音讯”)。
  • 半音讯发送胜利并响应给发送方。
  • 执行本地事务,依据本地事务执行后果,发送提交或回滚的确认音讯。
  • 如果确认音讯失落(网络问题或者生产者故障等问题),MQ 向发送方回查执行后果。
  • 依据上一步骤回查后果,确定提交或者回滚(摈弃音讯)。

看完事务型音讯发送流程,有些读者可能没有齐全了解,不要紧,咱们来剖析一下!

疑难?

问题 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 应答上述问题。

5.1 拓扑图

微服务化后,服务数量变多,服务间调用关系也变得更简单,以至于很多工程师不分明服务间的依赖和调用关系,给系统维护带来很多艰难。通过拓扑图咱们能够清晰地看到服务与服务,服务与数据库,服务与缓存中间件的调用和依赖关系。对服务关系一目了然之后,也能够防止服务间循依赖、循环调用的问题。

5.2 申请调用栈(Call Stack)监控

微服务化后,服务变多,调用链路变长,跨多个服务排查问题会更加艰难。上图是一个申请的调用栈,咱们能够清晰看到一次申请调用了哪些服务和办法、各个环节的耗时以及产生在哪个服节点。上图的申请耗时过长,依据监控(红框局部)咱们能够看到工夫次要耗费在数据库 SQL 语句上。点击数据库图表还能够看具体 sql 语句,如下图:

如果产生谬误,会显示为红色,谬误起因也会间接显示进去。如下图:

相似性能问题和谬误的线上排查。咱们如果通过查日志的传统方法,可能会消耗大量的工夫。然而通过 APM 工具分分钟就能够搞定了!

5.3 申请 Server Map

Server Map 是 Pinpoint 另一个比拟重要的性能。如上图,咱们岂但能清晰地看到一个申请的拜访链路,而且还能看到每个节点的拜访次数,为系统优化提供了无力的根据。如果一次申请拜访了屡次数据库,阐明代码逻辑可能有必要优化了!

5.4 JVM 监控

此外,Pinpoint 还反对堆内存,沉闷线程,CPU,文件描述符等监控。

起源:二马读书
申明:文章取得作者受权在 IDCF 社区公众号(devopshub)转发。优质内容共享给思否平台的技术伙伴,如原作者有其余思考请分割小编删除,致谢。

IDCF【冬哥有话说】收费直播,关注公众号回复“冬哥”获取地址

  • 10 月 21 日(周四)晚 8 点,丰志强分享《组织级麻利转型》
  • 10 月 28 日(周四)晚 8 点,钱勇分享《toB SaaS 从策略到产品经营的天龙八步》
正文完
 0