(九)漫谈分布式之微服务组件篇:探索分布式环境下各核心组件的必要性!

本文涉及的产品
传统型负载均衡 CLB,每月750个小时 15LCU
网络型负载均衡 NLB,每月750个小时 15LCU
日志服务 SLS,月写入数据量 50GB 1个月
简介: 本文将深入探讨微服务中各个组件的必要性,以此帮助各位更好地加深对分布式系统的掌握度。

引言

记得几年前团队在招聘时,要求候选人有分布式/微服务相关项目的实际经验,当时问了不少候选人一个问题:我们可以通过具体地址直接调用其他服务的接口,可为什么微服务项目里要搞一个注册中心呢

大多数候选人面对这个问题时,往往会短暂地愣住,随后陷入沉思。然而,当尝试回答时,却好像遭遇了“语言短路”,言辞变得含糊不清,难以表达清晰。其实,里面很多人都能立刻明白提问的核心,也清楚期待听到的答案方向,但往往难以将思绪转化为流畅的语言,总有一种“说不清道不明”的尴尬。

这种现象并非个例,包括我自己也曾深受其扰。究其根本,还是在于对技术的理解不够深入和透彻。虽然我们在脑海中模糊地知道答案的轮廓,但难以用清晰、简洁的语言表达出来,因此,一旦开口,便给人一种“含糊其辞”的印象。许多具备分布式/微服务经验的小伙伴,也常遇到这样的困扰,尽管大家在实际工作中使用过各种组件,也知道大概怎么回事,但真正被问及细节时,就显得有些力不从心。正因如此,本文将深入探讨微服务中各个组件的必要性,以此帮助各位更好地加深对分布式系统的掌握度。

一、注册中心

为什么需要注册中心?这个问题并不难,想清楚两点就够了,一是没有它会怎么样?二是它能给系统带来什么好处?想明白这两个问题后,再回答最开始提出的问题就能做到紧紧有条。当然,为了更好的讲述,我们先来看个例子:

1.png

这是一个电商平台的业务体系图,最初是“大锅烩”般的单体架构,随着业务越来越复杂,且团队规模越来越大,从而造成功能迭代缓慢、代码臃肿且边界模糊、系统内部耦合性过高……一系列问题出现。也正是为了解决这些问题,不得不对整个系统的架构重新调整,即根据业务特性拆分出不同的子系统,以此达到分化解耦的目的

PS:这里不做具体业务拆分,姑且认为拆分的结果,就是图中的每个模块都拆成了一个独立的子系统。

好了,经过这次架构演进后,系统从集中式的单体模式,调整为了分布式架构,可这时会出现一个问题:假设订单子系统的某些业务场景,需要商品、库存、支付、优惠券、会员……等多个子系统支撑,该怎么办?

在原先的架构中,一个模块需要依赖于其他模块的功能,只需将对应模块的Service实例直接注入即可。在分布式系统里,由于各模块(子系统)都是独立部署的,面对前面所述的场景,只能基于网络来调用对方暴露的接口。既然需要用到对方的接口,怎么调用呢?按以往对接外部系统的经验,我们可以写死对端的IP地址完成调用

但这种方式会存在额外致命的缺陷,因为依赖的其他子系统,其地址都会以硬编码形式维护,一旦其他子系统发生迁移或扩容,当前子系统就必须得修改源码并重启。其次,如果某个子系统以集群方式部署,当前子系统还需自己实现一套请求分发策略,否则无法解析出每次请求要去往的具体地址。最后,如果其中某个子系统出现故障,导致无法继续处理业务请求,那么上游子系统的请求都会被阻塞,这就会造成大量请求堆积从而拖垮整个系统!

再站在整个系统的上帝视角来看,拆分出的每个子系统除开上述问题外,还有一个特别麻烦的事情,所有子系统都是点对点通信,这意味着消费者(调用方)需要维护其依赖的提供者(被调用方)配置信息。可上面的例子中,拆分出的子系统数量众多,而且每个子系统都会依赖另外的多个子系统。这时,各个子系统维护的配置信息会特别复杂,并且子系统之间的依赖关系会跟一团麻一样格外混乱。

1.1、注册中心的诞生

综上所述,尽管我们对原先的单体系统进行了拆分,可各子系统间直接以P2P方式进行通信,仍然会给系统带来极强的耦合性,这违背了最初拆分的初衷,带来弊端远大于好处,显然无法投入生产使用。可是假设我们是时代的“先驱者”,当一个大系统被拆分为多个子系统后,该怎么跃过面临的重重难关呢?我们来认真分析下拆分后、各子系统真正的诉求,然后再看待提出的这个问题。

首先,每个子系统肯定不能以硬编码的形式,写死依赖的其他系统IP地址,否则对应的子系统一旦迁移,又需要修改源码……。有啥好办法?可以为每个子系统分配一个域名,其他子系统通过域名调用接口即可。这种方式有两个好处,一是降低耦合度,就算对应的子系统迁移机器,只要域名不换,依赖它的其他系统就无需修改源码;二是提高维护性,每个依赖它的子系统,也不需要自己实现请求分发/负载均衡策略,可以直接在解析域名时统一配置。

其次,子系统之间通信时,要能够动态检测出其他系统节点的健康状态,比如订单系统依赖的支付系统,对应着A、B、C三个节点。当订单系统在调用接口时,如果B节点出现故障无法处理请求,要能第一时间感知到,并自动将分发到B的请求,转移到健康的A、C节点,从而保证两个系统间的通信不会受阻。当然,实现这点也不难,存在依赖关系的系统做个心跳检测机制即可。

最后,每个子系统还需要支持动态伸缩,传统的Nginx集群方案,扩容也好,缩容也罢,但凡涉及到集群成员变更,就需要重启Nginx才能更新集群配置,如果类似的方案放在拆分后的分布式系统,这无疑是致命的。比如某个子系统的性能跟不上业务需求时,想要增加一个集群节点来提升吞吐量,为了使得依赖它的其他子系统感知到,就需要重启当前子系统的接入层,而在未真正完成重启的这个窗口期内,整个系统有可能陷入瘫痪,导致无法继续受理用户请求。

当两个存在交互关系的子系统,做好上述三条后,才能保证拆分出的分布式系统具备可用性,但大家回过头来思考一下,上面提到的几种机制,是不是每个子系统都需要?如果每个子系统都单独实现一套逻辑,这未免太过冗余……。好,既然这些是所有子系统的共性机制,那能否沉淀出一个公共组件呢?答案是当然可以,而这个组件在如今就被称为:注册中心

1.2、注册中心的优势

2.png

在分布式系统中引入注册中心,可以很大程度上提升系统的灵活性、可拓展性及容错能力,具体优势如下:

  • ①服务注册:节点启动会自动登记自身的详细信息,最终在注册中心形成可用的服务地址清单;
  • ②服务发现:当某个服务新增/剔除一个节点后,消费者能立马动态感知到节点变更的信息;
  • ③系统解耦:不再依赖硬编码的地址通信,各系统之间无需关心服务提供者的具体IP、端口;
  • ④灵活性:基于服务注册与发现机制可以随时动态伸缩,这使得系统能应对各种突发状态与弹性负载;
  • ⑤中心化管控:注册中心可以统一维护所有服务实例,并对节点进行健康探测,自动剔除故障节点等;
  • ⑥拓展性:注册中心可以为动态路由、故障恢复机制、智能化负载均衡等高级功能提供底层支撑;
  • ⑦……

注册中心逐一解决了系统拆分带来的种种问题,它不仅是不同服务之间通信的桥梁,也是实现服务治理和动态性的关键基础设施。为此,注册中心是分布式/微服务系统的“中枢神经”,通过它能让节点数量众多、依赖关系复杂、调用关系混乱的分布式系统更加便于治理与维护。

当然,微服务架构中,前面所说的子系统称为“服务”,具体运行服务的节点,被称为“服务实例”;同时,一个服务依赖另一个服务的功能时,如A依赖于B,其中A被叫做服务消费者,B被叫做服务提供者,不过这仅是概念层次的区分,实际项目中,一个服务可以同时具备消费者、提供者两种身份。

现如今,微服务技术已经十分成熟,也随之诞生了许多优秀的开源注册中心,这使得大家在设计或拆分系统时有了诸多选择,如早期的Zookeeper、Eureka、Consul,到如今主流的Etcd、Nacos,甚至服务网格的云原生产物Istio等等。不过这里不对如何选型做展开,毕竟这偏离了本文的主题,大家感兴趣可以自行研究~

二、远程调用

抽象出注册中心组件,这使得不同服务间的跨进程交互更加便捷,可是真的便捷吗?实则不然,注册中心的出现,虽然能让系统中的每个服务消费者,不必再费心去维护服务提供者的配置信息,但来仔细观察不同服务之间的通信过程:

3.png

这是一个简化的提交订单业务,如果是单体项目,订单需要其他模块的功能提供支撑时,直接调用对应Service层的方法即可。换到分布式系统,因为各个业务都是独立部署,图中的箭头指明的各类操作就得走网络通信完成,如果对接过外部系统的小伙伴,可以回顾下整个对接流程,大致如下:

  • ①根据接口文档定义好请求地址、请求结构体、响应结构体等;
  • ②按照要求构建并组装请求报文(请求方式、请求头信息、请求参数等);
  • ③使用网络类库,并设置好超时时间、报文格式等,然后真正发出网络请求;
  • ④接收并校验接口返回的响应报文,视情况对返回的数据进行处理(验签、解密、清洗等);

如果以对接外部第三方接口的形式来做分布式系统开发,每对接一个其他服务的接口,至少需要经过上述四步,这显然特别繁琐,并且增加了许多工作量。以上图的案例来看,因为各服务拆分粒度较细,提交订单这个操作可能依赖十多个服务,这种情况下,光对接接口的成本都会高于实现业务逻辑……

PS:SOA架构中,子系统之间通信就采用这种方式,而且不是轻量级的Http+Json,而是更折磨人的WebService+XML~

除了消费者调用接口麻烦之外,作为被调用方的服务提供者也麻烦,比如之前的单体架构中,订单模块需要用到库存模块的某个功能,直接调用Service/Dao层的方法即可;到了分布式架构里,Service/Dao层的方法不一定对外暴露了接口,当消费者需要的功能没有提供接口时,还需要挨个重新定义并对外暴露才行。

所以,注册中心仅仅只是单体迈向分布式的第一步,现在又面临了新的挑战,因为分布式/微服务系统里,跨服务通信是常事,又该如何简化服务之间的交互过程呢?

同样的道理,虽然不同服务之间调用的接口有所差异,但调用的过程却极其类似,那么同样可以抽象出可复用的远程调用公共组件。当然,在如今成熟的微服务生态下,你可以选择基于HTTPRestTemplate、OpenFeign,也可以选择性能更优的RPC组件,如Dubbo、gRPC等,使用这类组件的优势在于:

  • ①我们无需关心底层的HTTP细节,如请求构建、响应解析、错误处理等,简化了服务通信过程;
  • ②允许使用类似于本地方法调用的方式来调用远程服务,进一步提高了代码的可读性可维护性;
  • ③能与注册中心紧密配合,在服务注册与发现的基础上,可以动态感知服务提供者的地址并完成调用;
  • ④服务之间的交互属于系统内部通信,可以去掉许多身份鉴权、参数校验机制,性能方面更佳。

当然,上述几点只是针对OpenFeign这种远程调用组件,对于Dubbo、gRPC这种专业的远程调用组件来说,因为它们使用更底层的网络协议通信,也会采用性能更强的序列化算法,如Hessian2、ProtoBuf,所以通信时的开销更小、性能更强悍!

三、服务网关

有了注册中心、远程调用组件后,这为我们解决了许多问题,可是再来看一个现象:

4.png

虽然系统内部可以基于注册中心的服务名+远程调用组件,完成不同服务间的数据交互,可是当浏览器、小程序、APP……这些外部终端需要接入呢?毕竟拆分出了多个服务,原先的后端接口地址也完全不同,图中有二三十个服务,难道需要配置二三十个域名,然后前端来根据不同业务对接吗?

就算真的可以这么干,那还有新的问题,部分接口需要经过身份鉴权才能调用,之前单体架构因为所有接口都部署在一起,所以通过一个拦截器或过滤器就能搞定,而到了分布式系统呢?接口分散部署在数十个服务,为了防止越权调用,难道给每个微服务都写一套鉴权体系吗?这不太现实。

综上,注册中心+远程调用组件,能缓解系统拆分后内部的大多问题,可是前后端联调时会存在许多问题,如接口路由问题、身份鉴权问题、API管理问题等等,也正是为了解决这一系列问题,分布式系统又出现了一个全新的组件:服务网关

3.1、为什么需要服务网关?

服务网关又被称之为API网关,它是系统拆分后的一个重要组件,它作为整个系统唯一的外部API入口,负责统筹管理所有后端服务。实际开发中,可以将大多数请求需要的通用能力,如路由、认证、限流、监控、日志记录等,在网关处进行统一实现,从而满足不同服务之间的请求管理与控制,下面一起看看网关给系统究竟带来了哪些好处呢?

  • ①请求路由:统一承接外部流量,并根据给定规则进行转发,将请求路由至对应的业务服务;
  • ②安全控制:网关可以对入站的所有外部流量进行安全控制,如统一鉴权、反爬虫校验、验签等;
  • ③响应处理:所有流量出站时同样会经过网关,因此可以在网关对响应结果进行统一的处理;
  • ④协议转换:在多语言异构的分布式系统中,网关可以将不同协议的请求,转换为同一协议再路由;
  • ⑤动态路由:可以与注册中心配合,动态感知业务节点的变更,动态将请求转发至健康节点处理;
  • ⑥API管理:网关可以提供一个统一的API管理界面,允许开发人员查看、测试和管理后端接口;
  • ⑦跨域配置:网关可以处理CORS预检请求,允许来自不同源的客户端访问后面的业务服务集群;
  • ⑧负载均衡:网关可以根据业务服务的节点数量,自动将请求均匀分发至集群内的各个节点;
  • ⑨……

简单来说,服务网关在分布式系统中的作用,就好似你小区门口的保安大爷,在保安亭可以对出入的人员进行统一管控,如验证身份、出入登记、停车收费……。也正是因为这样,注册中心作为分布式系统的“中枢神经”,主要负责系统内部服务之间的治理工作;而服务网关则作为系统的“咽喉要塞”,承担所有外部流量的接入工作,同样是分布式/微服务架构里必不可缺的核心组件!

同时,微服务网关同样有着许多开源组件可供选择,如Zuul、Gateway、Kong、Istio-Envoy、K8S-Ingress。当然,网关还可以实现灰度发布、流量染色/录制/回放、日志审计等高级功能,但大多数网关对这些功能没有直接支持,想要其中的功能需要自行定制开发。

3.2、服务网关与接入层网关的区别

聊完服务网关的必要性后,我们再来看个新问题,其实在之前的系统架构中,也有“网关”的概念,比如常用的Nginx,它同样具备请求转发与路由的功能,那为什么不直接用它作为微服务网关呢?其实也并非不行,但如果直接用Nginx作为微服务网关,那么location规则会特别复杂,并且想要实现一些特定的功能,还得结合Lua脚本实现,这会降低Nginx的性能,并且无法实现较为复杂的功能,如登录鉴权、接口验签……。

为此,Nginx这类被称作为流量网关,主要用于控制和管理网络流量,提供全局性的、与后端业务应用无关的策略配置,如HTTPS证书、请求分发与负载均衡、全局流量监控、黑白名单控制等功能,它通常位于网络接入层的最前端,作为整个系统的网络入口。Gateway这类则被称为业务网关,主要用于连接系统内部服务和外部终端,它提供独立业务域级别的、与后端业务紧耦合策略配置,适用于业务逻辑较为复杂、需要细粒度控制的场景,如精准的请求路由、安全策略、动态分发、协议转换等功能。

流量网关和业务网关,虽然功能上有一定类似,但却有着不同的优势及应用场景,在成熟的分布式/微服务项目中,通常会将两者结合使用,例如可以使用Nginx作为域名请求的入口点,然后将请求分发到Gateway,由 Gateway处理动态路由和微服务相关的功能,以此设计出更强大、灵活的接入层,示意图如下:

5.png

四、负载均衡器

6.png

经过不断推演,分布式系统已发展至上述架构,目前内部服务治理问题、外部终端接入问题都已得到解决,那么系统目前是否还存在问题呢?存在,即上述一直反复提及,但一直未作展开的问题:负载均衡

众所周知,解决高并发最简单粗暴的手段是加机器,使用更多的节点组成集群,从而达到提升吞吐量的目标。在分布式系统中,这个手段同样适用,比如作为电商平台核心之一的商品服务,运行期间会面临较大的访问压力,这时单个节点扛不住,就可以选择部署集群,但问题来了:假设订单服务依赖于商品服务,这时商品服务有五个节点,在做远程调用时,究竟该请求哪个节点呢

听到这个问题,大多数人心中就有了答案:调用时根据特定的载均衡策略分发请求即可。同样的道理,系统内每个服务都有可能与其他服务通信,并且任意服务都有可能做集群部署,这意味着“负载均衡”也是个公共需求,这时也可以封装出一个公共组件,不过微服务架构中,这个组件被称为:客户端负载均衡器

4.1、客户端负载均衡与服务端负载均衡的区别

负载均衡这个概念并不陌生,就算单体项目亦可通过Nginx搭建集群,并基于Nginx的负载均衡机制分发客户端的请求,那么微服务中的客户端负载均衡,与传统的负载均衡技术有何区别?为何微服务中不复用Nginx这类组件?

答案很简单,Nginx这种组件对于微服务之间通信来说太重了,并且会增加系统的风险,因为服务端负载均衡的本质是代理,所有请求都连接代理器(负载均衡器),而后再由代理器将请求分发给具体的节点,一旦代理器出现故障,就有可能造成系统瘫痪。

正因如此,站在客户端角度打造的负载均衡组件,则能完美解决上述问题,所谓的客户端负载均衡,即是指每个调用接口的客户端(服务消费者),都具备请求分发的能力,如下:

7.png

微服务中能实现客户端负载均衡,这得益于注册中心,因为每个服务都可以通过服务名称,从注册中心里换取到指定服务的可用节点列表,拿到节点列表后,这时不管哪个节点都具备分发请求的能力。同时,这种能力对每个服务来说特别轻量级,也不会增加系统风险。

4.2、客户端负载均衡的优势

客户端负载均衡,除开能用于服务之间通信的场景外,针对外部入站的用户请求,同样可以实现负载均衡效果,Why?因为网关也是一种特殊的服务,它同样会注册到注册中心,而所有入站流量都会经过网关,路由时它自然能根据负载均衡策略实现用户请求的分发,这里简单总结下优势:

  • ①通过负载均衡可以让单一节点的服务部署集群,使得每个服务都具备超强性能的潜质;
  • ②客户端负载均衡器与注册中心配合,可以使任意服务都能支持运行期间的弹性伸缩机制;
  • ③客户端负载均衡器让每个消费者都具备请求分发能力,在分布式系统中具备更高的容错性;

不过相较于服务端负载均衡来说,客户端负载均衡分发请求会造成一定的倾斜,比如A服务依赖B服务的接口,B服务由五个节点组成,A服务由三个节点组成,假设这时的负载均衡是轮询算法,A服务的三个节点在轮询分发请求时,节点之间无法相互感知,就会出现A1刚给B1分发一个请求后,A2又给B1分发了一个请求……这种现象。

相较于服务端负载均衡来说,虽然客户端负载均衡会造成请求分发出现倾斜,但这种倾斜度不会太大,总体上利远大于弊。

对于选型来说,如果是SpringCloud体系,主要有Ribbon、LoadBalancer两个开源组件可选,目前一般会选后者;如果是其他体系的分布式项目,例如纯Dubbo的项目,通常会自带客户负载均衡功能;如果全面拥抱云原生环境,也可以使用自带的云基础组件实现负载均衡的效果~

五、服务保护

8.png

通过引入负载均衡组件,入站流量的路由也好,服务间远程调用也罢,都可以具备了运行期间动态伸缩节点,并自动将请求分发到健康节点的兼容能力,这种模式能极大程度上满足如今高并发冲击下,弹性伸缩的需求。可是话说回来,高并发带来的问题不仅仅只靠弹性伸缩就能解决,毕竟硬件资源总会达到上限。

假设此时遇到这么个场景,系统出现特别大的并发流量,从而触发弹性扩容机制,并在短时间内耗尽所有空闲的待分配资源,可系统仍然扛不住持续增加的业务请求,在这种空闲资源支持继续扩容情况下,又或者脱离云环境部署的分布式系统中,面对来势汹汹的高并发冲击怎么办?

在有限的硬件资源下,系统能处理的并发请求总会有上限,可业务流量还在持续攀升,面对这种情况,系统中的所有节点只有一个应对方式,那就是“我死给你看”,不过“死”的多种多样,如CPU过载、内存溢出、进程崩溃……。更为重要的是,一旦某个节点发生故障,那这种情况很可能会在短时间内蔓延至整个系统,从而造成整个系统瘫痪,来看例子:

9.png

假设这是下单的请求链路,并且整个链路以串行化执行(实际会将部分操作异步化),当图中的库存服务率先宕机后,那位于它上游的订单、商品、优惠券、会员服务,也会在不久的将来陷入宕机状态,为什么呢?因为库存服务发生故障,代表所有请求会被阻塞堆积在会员服务,在很短时间内,源源不断到来的请求会将会员服务拖垮,接着就是优惠券服务……,而这种故障逐步蔓延直至扩散到整个分布式系统的现象,则被称之为:服务雪崩

5.1、服务保护组件的优势

在前面所说的背景下,必须得弄套“盔甲”保护脆弱的系统,以此提高整体的容错率,而这套盔甲对应的组件就是服务保护,其主要作用就是:保障整套系统在故障不可控的环境下稳定运行,下面具体说说服务保护组件在分布式系统中的作用。

①防止服务故障的蔓延:分布式系统里每个服务通过远程调用进行通信,一旦某个被调用的下游服务发生故障,那么依赖它的上游服务也可能发生故障,最终造成故障蔓延至整个系统。服务保护组件通过断路器实现熔断机制,可以在某个服务发生故障的时候,给调用方返回一个错误响应,而不是长时间的阻塞等待,从而防止故障在服务之间的进一步传播。通过这种及时切断链路的机制,能有效地控制故障范围,避免单点故障引发整个系统崩溃的灾难性后果。

②实现服务降级与限流:在服务保护组件的支持下,当某个服务不可用时,系统可以自动降级到备选服务或备用方案,以保证核心业务功能的正常运行。其次,当某个服务的负荷达到预设的瓶颈值时,服务保护组件可以基于计数器、令牌桶、漏桶、滑动窗口等算法,适量拒绝后续源源不断到来的请求,以此将单个服务的负荷保持在可控范围内,避免激增的脉冲流量将系统打垮。服务降级和限流策略,能够在很大程度上提高系统的容错率和可用性。

③提供资源隔离:先举个例子,比如A服务的不同业务分别依赖B、C服务,如果这时B服务宕机,A服务调用B服务就会造成线程阻塞,最终耗尽A服务有限的线程资源,导致正常的A→C链路也不可用。为了避免这种“由于某个服务的故障,导致整个服务的线程资源被耗尽”的问题,服务保护组件通常会提供类似于沙箱的隔离机制,即隔离每个下游服务的线程资源。这样,即使某个服务的调用线程被阻塞,也不会影响其他服务的远程调用。

④提供请求缓存、合并、重试和监控机制:服务保护组件还可以提供请求缓存、请求合并等功能,以减少对下游服务的调用次数和减轻其负载压力,比如两个几乎连续的并发操作,需要发起相同的远程调用请求(如查询商品详情数据),这就可以合并成一个请求来调用。同时,服务保护组件还具备强大的监控能力,能实时监控服务的运行状态和性能指标,为系统运维和故障排查提供有力支持。

单从历史性角度出发,最初的Hystrix无疑开创了服务保护组件的先河,它是服务隔离、熔断、降级等理念的传播者,后面许多相同赛道的开源组件,或多或少都借鉴自该组件。不过可惜的是,Hystrix开源版已宣布停止更新并进入了低维护的阶段,如果你现在需要一款服务保护组件,那Sentinel、Resilience4J是不错的选择,当然,Hystrix最后的稳定版也足以满足绝大多数需求。

六、配置中心

10.png

引入服务保护组件给脆弱的系统穿上盔甲后,系统健壮性得到了质的飞跃,可随着服务数量越来越多,系统的配置信息将会变得难以管理,尤其是套入多版本、多环境的配置会更加混乱,有时发版往往一个配置项忘记改了,就会造成启动失败等现象。同时,如果发现某个配置项未更新,改成新值后必须得重新构建、打包、发版才能生效,这无疑降低了开发效率。

约定大于配置、配置大于编码,配置信息是分布式系统的核心,为此,系统急需一种将所有服务配置信息聚合起来、并且便于维护的技术手段,也就是在这个背景下,分布式领域出现了新的组件:配置中心,它能给分布式系统带来许多好处:

  • 集中管理:可以将所有服务的配置信息集中起来统一管理,使得各服务配置项维护起来更加便捷;
  • 降低冗余:各业务服务会存在一定量的重复配置项,在配置中心里可以抽出全局的配置降低冗余;
  • 动态刷新:配置中心支持动态更新配置项,并实时推送至各微服务,并无需重启服务即可生效;
  • 版本管理:当配置信息发生变更时,配置中心会自动保存历史版本,可以随时回滚支持多版本机制;
  • 多环境支持:配u之中心可以通过命名空间等隔离方式,区分出不同的环境配置,使得配置项更加清晰;

除上述列出的优势外,配置中心还具备权限管理、灰度发布、监控与告警等功能,这能为系统的稳定运行和高效管理提供有力支持。通常而言,注册中心都附带配置中心的能力,如ZooKeeper、Consul、Etcd是天然的K-V存储组件,自然可以用于存储配置信息,Nacos甚至专门有设计配置中心模块。当然,市面上同样有许多优秀的开源配置中心,如Apollo、SpringCloud-Config、XDiamond……

七、可持续集成/持续部署(CI/CD)

到目前为止,前面提到的注册中心、远程调用、服务网关、负载均衡器、服务保护、配置中心这六大组件,奠定了分布式/微服务系统的基架结构,这六大组件属于分布式/微服务项目必不可少的基础设施。但除开这六大基础组件外,实际分布式架构落地过程中,往往还需要更多的技术组件做支撑,继续来往下看。

在以前,一个系统开发完成需要部署时,都会从各大厂商手中租赁或购买硬件服务器,而后将源码手动打包并人肉部署,这是单体时代的主流部署方案。可到了分布式时代,随着业务拆分越来越细致,分化出的服务数量会越来越多,系统依赖的组件也越来越多,同时还要考虑高可用,为核心组件/服务搭建集群,部署所需的硬件资源呈直线上升。

其次,除开费用高到令人发指的硬件成本外,另一个难题是难以运维!几十上百种服务/组件相互交错,代码管理、源码打包、环境部署、版本发布、版本回退、故障恢复……,如果还以人工形式部署,稍微搞错一步,就会发现整个系统跑不起来,这种体验简直令人生无可恋!

如果再以单体时代的模式来部署分布式系统,就算你将业务需求开发完了,可能光部署就得再耗费一周,怎么办?为了解决硬件成本、降低运维成本,虚拟机容器+自动化运维技术被推上了时代风口,通过Git+Jenkins+Docker+K8S这类技术,构建出完善的CI/CD(可持续集成/持续部署)流程,能在极大程度上减轻运维成本、提升开发效率:

11.png

为了节省篇幅,相对详细的过程可参考《漫谈分布式专栏的开篇:《架构演进篇-容器化时代》章节,这里总结下CI/CD带来的优势:

  • ①通过Git+Jenkins技术,可以轻易实现代码版本控制、持续集成、自动打包/构建与部署工作;
  • ②通过虚拟化容器,既节省了硬件资源成本,又提升了交付速度,还有类似沙箱的资源隔离机制;
  • ③通过自动化流程减少了运维手动操作时间,降低人工部署出错的风险,提升了运维效率;
  • ④基于容器编排能提供自动伸缩、滚动升级、灰度发布、节点自愈、版本回退等高级功能;
  • ……

总而言之,越是庞大的分布式系统,越依赖于自动化的CI/CD流程,否则动辄几百上千个节点,挨个节点去手动部署,不仅耗时耗力,而且极容易出错,一个不注意或许就得重头再来。不过对于构建CI/CD而言,最主流的方案就是Git+Jenkins+Docker+Kubernetes这个组合,就算有所差异也不会太大~

PS:不过与单体时代不同的是,曾经的开发几乎都身兼运维一职,而如今都会有专门的运维岗来负责该工作,为此这些技术大概了解即可。

八、日志收集

有了CI/CD流程解决系统部署的难题后,还有一个十分常见、但令人容易忽略的问题,即系统日志。系统日志具备极高的价值,对于开发者来说,有助于排查程序Bug、性能问题;对业务人员而言,可从中挖掘出用户行为、各项业务指标等信息。

不过在之前的单体时代,因为只有一个应用,所以通过Log4j、Slf4j、Logback……这类日志框架,很轻易的就能收集并管理程序日志。同时,日志都统一输出到了指定位置,日志也可以在终端通过shell命令查阅。

到了分布式系统中,部署的节点数量众多,日志分散在不同的机器上,这时想要看某个服务的日志,首先登录部署对应服务的节点才能查看,而服务之间调用关系错综复杂,有时为了看一个请求链路的日志,需要切换十多个节点,这无疑降低了排查问题的效率。

为此,如何将整个系统所有节点的日志收集到一处管理,这是分布式系统中要考虑的问题,怎么解决呢?答案是搭建日志收集分析平台,日志收集分析平台带来的优势如下:

  • ①可以将所有节点的日志记录集中存储与统一管理,使得查询、分析和处理日志数据更便捷;
  • ②能承载大型分布式系统的海量日志增长速率,可以根据实际需求调整收集策略、存储容量与方式;
  • ③可以基于收集的日志做数据挖掘,从日志数据中挖掘出用户行为、业务事件、业务指标等;
  • ④能根据各日志级别的数量,结合程序日志埋点,为系统提供接近实时的反馈与异常监控;
  • ⑤……

如果你需要为系统搭建日志收集分析平台,可以选择常见的ELK(Elasticsearch、Logstash、Kibana)方案,也可以选择Graylog、Fluentd这些开源组件,不过在Java生态中,ELK+Kafka组合是最成熟、应用最广的方案。

九、链路追踪

搭建出完善的日志收集分析平台后,虽然能将分散在各个节点的日志,收集到一处统一管理分析,但大型分布式系统的日志增长量极其恐怖,比如一个由数百个节点组成的系统,在业务峰值时,单日产出的日志量可达几百GB、甚至接近TB级。在这种背景下,当你收到某个用户反馈系统Bug、想根据系统日志分析原因时,试图从海量日志中找出对应的日志,难度堪比大海捞针!

同理,就算整个系统只有一二十个节点,每天的日志量也不大,当你遇到Bug时也很难排查,因为目前的日志是从各个节点聚合而来,这时,去查看日志就会发现日志根本不连贯,你很难将这么多节点的日志,按请求调用链路的顺序先后串联起来,排查时只能从从最下游开始向上排查,还得挨个翻日志,极其影响开发效率~

12.png

正因如此,为了方便日常排查功能Bug、性能问题、系统故障……,必须想办法将日志串联起来,而这种技术则被称为链路追踪。所谓的链路追踪很容易理解,以调用接口举例,当客户端调用某个接口发出请求时,系统就会记为日志的开始,而请求在系统内经过的任意节点,产出的所有日志都会被串联起来,直至该请求出站为止。

所以,最基本的链路追踪技术,只需要在请求入站的网关处,分配一个全局唯一的traceId,并在后续服务通信时依旧保持传递,并且记录日志时手动获取该traceId输出,就能将将请求链路上产生的日志串起来,后续排查问题只要拿着唯一标识去搜索,就能轻易得出该请求连续且完整的日志。不过这种方式对代码侵入性高,而且只具备最基本的日志串联功能,这不能完全满足于大型分布式系统。

为了迎合大型分布式系统的需求,市面上涌现了许多无侵入式的链路追踪组件,如Zipkin、SkyWalking、Jaeger、SpringCloud-Sleuth、PinPoint、Elastic、CAT、OpenTelemetry……,这类开源组件的优势如下:

  • ①基于埋点、探针、APM等技术实现,对业务代码基本都是零侵入,接入成本较低;
  • ②强大的链路追踪能力,包括经过的中间件、数据库等,并且能记录各节点的停留耗时等信息;
  • ③提供应用监控能力,如服务器负载、JVM堆使用率、GC次数、程序吞吐量、接口RT等;
  • ④当出现大规模超时、故障、响应缓慢等现象时,还提供了异常告警功能,能提前感知故障;
  • ……

十、系统监控

有了日志收集分析平台和链路追踪后,当系统出现故障或性能问题,开发者能日志+链路追踪快速排查、分析、定位与解决问题。可是,如果光依靠被动感知问题,就只有等到问题发生后才能介入排查,这就有点“亡羊补牢”的感觉,而提前防患于未然才是解决问题的最优解,当问题真正发生前就着手解决,能有效减少系统故障带来的业务损失,那如何提前感知到即将要发生的问题呢?搭建全面的监控体系

上节内容提到过,某些链路追踪组件,具备一定程度的监控告警功能,但不能保证系统所有异常情况都能及时告警,大型分布式系统需要的监控会更全面,往往包含以下几方面:

  • 服务器资源监控:服务器的CPU使用率、内存利用率、磁盘IO频率、空间占用、网络带宽、健康状态等;
  • 流量监控:出入站请求数、并发连接数、数据包体积、状态码统计、PV、UV、QPS、TPS、RPS等;
  • 应用监控:堆空间占用、GC频率与耗时、活跃线程数与死锁、程序异常率、接口RT、断路信息等;
  • 中间件监控:MQ投递/消费速率、MQ消息积压数、DB慢SQL、缓存命中率、热点缓存Key、客户端连接等;
  • 业务监控:CTR、GMV、ROI、多维度订单指标、各业务榜单、营销转化率、业务风控策略等;

综上,想搭建一套全面的监控体系绝非易事,但监控体系在大型分布式系统中至关重要,监控系统能够实时收集和分析系统各项监控指标。从技术角度来看,完善的监控体系,能及时发现潜在问题、故障及性能问题,并达到预设的阈值时自动告警,通知技术人员快速响应并进行故障排除,从而减少系统停机时间和用户体验下降的风险。从业务角度出发,还能基于监控系统收集的业务指标和用户行为数据,为业务运营和战略决策提供重要依据,以此帮助优化产品和服务,提升用户体验等。

13.png

不过可惜的是,虽然市面上有着许多监控相关的开源组件,如Prometheus+Grafana、Zabbix、ELK-Stack、Nagios等,但这些组件都只聚焦于“服务器资源监控”这个方面,而对于其他方面的监控,要么得花钱买对应的监控产品,要么就自研监控系统。相对而言,如果规模够大,后者是个不错的选择,尽管耗时耗力,但能根据实际需求来定制监控指标,从而使搭建出的监控体系更贴近业务系统。

十一、分布式事务

就目前为止,一个成熟的分布式系统,该有的基础设施组件就介绍完了,不过实际的分布式项目中,在不同的业务和技术背景下,往往还有更多的困难等待我们去克服。比如在之前的单体项目中,如果一组操作要保证事务,只需要在对应的业务方法上加一个@Transactional注解即可,这样就会将事务托管给Spring来负责,而Spring又间接依赖于关系型数据库提供的事务机制,执行过程如下:

  • 在该方法执行时,Spring会首先向数据库发送一条begin开启事务的命令;
  • 如果执行过程中出现异常,Spring会向数据库发送一条rollback回滚事务的命令;
  • 如果执行一切正常,Spring会向数据库发送一条commit提交事务的命令。

这种事务管理机制,在单体架构中显然十分好用,可到了分布式系统中,不仅根据业务拆分了应用服务,数据库也会根据业务拆分出不同的独立库。这时,就无法依赖传统关系型数据库提供的事务机制来保证一致性,示例如下:

14.png

分布式系统的不同业务模块之间,只能通过远程调用的方式调用对方所提供的API接口,假设这里在库存服务本地的「扣减库存」方法上加一个@Transactional注解,同时在订单服务本地的「新增订单」方法也加一个@Transactional注解,Spring内部的处理逻辑如下:

  • 下单业务远程调用「减库存」接口时,Spring会先向库存DB发送一个begin命令开启事务;
  • 当扣减库存的业务执行完成后,Spring会直接向库存DB发送一个commit命令提交事务;
  • 下单业务调用本地的「新增订单」方法时,Spring又会向订单DB发送begin命令开启事务;
  • 当新增订单执行出现异常时,Spring会向订单DB发送一个rollback命令回滚事务。

此时分析如上场景,下单业务理论上应该属于同一个事务,但之前《MySQL事务篇》聊到过,InnoDB的事务机制是基于Undo-log日志实现的,那么减库存产生的回滚记录会记录到库存DBUndo-log中,而新增订单产生的回滚记录则会记录到订单DBUndo-log中,此时由于服务不同、库不同,因此相互之间无法感知到对方的事务。

当「新增订单」的操作执行出现异常,Spring框架发送的rollback命令,就只能根据订单DB中的回滚记录去还原订单数据,而此前扣减过的库存数据就无法回滚,因此导致了整体数据出现了不一致性,即商品库存扣掉了,但没有相应的订单产生

PS:具体可参考之前《手写分布式事务框架篇-分布式事务问题演示》

分布式事务在很长一段时间内,是一个令人头疼的疑难问题,而如今分布式大行其道的时代,这个问题出现了多种成熟的解决方案:

  • ①基于Best Efforts 1PC模式解决分布式事务问题。
  • ②基于XA协议的2PC、3PC模式做全局事务控制。
  • ③基于TTC方案做事务补偿。
  • ④基于MQ实现事务的最终一致性。

但上述四种仅是方法论,也就是一些虚无缥缈的理论,想要在项目中真正解决分布式事务问题可以使用现成的落地组件/框架,如Seata、GTS、TX-LCN、 Atomikos、RocketMQ、Sharding-Sphere...,它们都提供了完善的分布式事务支持,目前较为主流的是引入Seata框架解决,其内部提供了AT、TCC、XA、Saga四种模式,主推的是AT模式,只需要添加一个@GlobalTransactional注解就能解决之前令人困扰的分布式事务问题~

十二、总结

看到这里,代表本篇走进了尾声,其实大家会发现,架构演进带来好处的同时,还会产生一系列问题,当那些伟大的技术先驱者持续探索,并在解决这些棘手的疑难杂症过程中,就形成了分布式领域里一个个热门的概念;当问题被真正解决并分享出来后,就成为了如今热门的开源组件。这些分布式/微服务组件,就是某一类特定问题的解决方案,也正是因为不断出现新的开源组件,才铸就了更加繁荣的分布式/微服务生态。

先人栽树,后人乘凉,作为后人的我们是时代的受益者,接触分布式/微服务技术栈时,一开始学习的就是这些已经成型的开源组件。而很多资料、课程在讲解只阐述了这些组件的作用,大家在学习时,潜意识下就忽略了这些组件诞生的原因,最终造就了“知其然而不知其所以然”的情况出现,学,我会;用,我也会!可是这些组件为啥需要,我却说不清……。因此,大家在学习/使用一项技术时,一定要多加思考,这样才能带来真正的技术成长,而不是因为工作需才去“搬砖”的工具人。

当然,分布式/微服务要面临的问题,远不仅本文提到的这些,如并发安全需要分布式锁、定时任务需要分布式调度、数据检索需要搜索引擎、海量数据需要大数据生态……。不过这些已经逐渐偏离了本文的核心主题,所以会放在后面的章节中单独阐述,毕竟提到的任何一条,一旦深究都是比较庞大的话题~

所有文章已开始陆续同步至公众号:竹子爱熊猫,想在微信上便捷阅读的小伙伴可搜索关注~

相关文章
|
2月前
|
Dubbo Java 应用服务中间件
微服务框架Dubbo环境部署实战
微服务框架Dubbo环境部署的实战指南,涵盖了Dubbo的概述、服务部署、以及Dubbo web管理页面的部署,旨在指导读者如何搭建和使用Dubbo框架。
219 17
微服务框架Dubbo环境部署实战
|
2月前
|
SpringCloudAlibaba JavaScript 前端开发
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架
分布式组件、nacos注册配置中心、openfegin远程调用、网关gateway、ES6脚本语言规范、vue、elementUI
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架
|
1月前
|
SQL NoSQL 安全
分布式环境的分布式锁 - Redlock方案
【10月更文挑战第2天】Redlock方案是一种分布式锁实现,通过在多个独立的Redis实例上加锁来提高容错性和可靠性。客户端需从大多数节点成功加锁且总耗时小于锁的过期时间,才能视为加锁成功。然而,该方案受到分布式专家Martin的质疑,指出其在特定异常情况下(如网络延迟、进程暂停、时钟偏移)可能导致锁失效,影响系统的正确性。Martin建议采用fencing token方案,以确保分布式锁的正确性和安全性。
43 0
|
1月前
|
存储 SQL 消息中间件
Hadoop-26 ZooKeeper集群 3台云服务器 基础概念简介与环境的配置使用 架构组成 分布式协调框架 Leader Follower Observer
Hadoop-26 ZooKeeper集群 3台云服务器 基础概念简介与环境的配置使用 架构组成 分布式协调框架 Leader Follower Observer
47 0
|
2月前
|
XML Java 数据库
在微服务架构中,请求常跨越多个服务,涉及多组件交互,问题定位因此变得复杂
【9月更文挑战第8天】在微服务架构中,请求常跨越多个服务,涉及多组件交互,问题定位因此变得复杂。日志作为系统行为的第一手资料,传统记录方式因缺乏全局视角而难以满足跨服务追踪需求。本文通过一个电商系统的案例,介绍如何在Spring Boot应用中手动实现日志链路追踪,提升调试效率。我们生成并传递唯一追踪ID,确保日志记录包含该ID,即使日志分散也能串联。示例代码展示了使用过滤器设置追踪ID,并在日志记录及配置中自动包含该ID。这种方法不仅简化了问题定位,还具有良好的扩展性,适用于各种基于Spring Boot的微服务架构。
50 3
|
3月前
|
负载均衡 监控 Java
SpringCloud常见面试题(一):SpringCloud 5大组件,服务注册和发现,nacos与eureka区别,服务雪崩、服务熔断、服务降级,微服务监控
SpringCloud常见面试题(一):SpringCloud 5大组件,服务注册和发现,nacos与eureka区别,服务雪崩、服务熔断、服务降级,微服务监控
SpringCloud常见面试题(一):SpringCloud 5大组件,服务注册和发现,nacos与eureka区别,服务雪崩、服务熔断、服务降级,微服务监控
|
3月前
|
机器学习/深度学习 分布式计算 PyTorch
大规模数据集管理:DataLoader在分布式环境中的应用
【8月更文第29天】随着大数据时代的到来,如何高效地处理和利用大规模数据集成为了许多领域面临的关键挑战之一。本文将探讨如何在分布式环境中使用`DataLoader`来优化大规模数据集的管理与加载过程,并通过具体的代码示例展示其实现方法。
178 1
|
3月前
|
消息中间件 Java Kafka
如何在Kafka分布式环境中保证消息的顺序消费?深入剖析Kafka机制,带你一探究竟!
【8月更文挑战第24天】Apache Kafka是一款专为实时数据管道和流处理设计的分布式平台,以其高效的消息发布与订阅功能著称。在分布式环境中确保消息按序消费颇具挑战。本文首先介绍了Kafka通过Topic分区实现消息排序的基本机制,随后详细阐述了几种保证消息顺序性的策略,包括使用单分区Topic、消费者组搭配单分区消费、幂等性生产者以及事务支持等技术手段。最后,通过一个Java示例演示了如何利用Kafka消费者确保消息按序消费的具体实现过程。
123 3
|
3月前
|
Java 微服务 Spring
SpringBoot+Vue+Spring Cloud Alibaba 实现大型电商系统【分布式微服务实现】
文章介绍了如何利用Spring Cloud Alibaba快速构建大型电商系统的分布式微服务,包括服务限流降级等主要功能的实现,并通过注解和配置简化了Spring Cloud应用的接入和搭建过程。
SpringBoot+Vue+Spring Cloud Alibaba 实现大型电商系统【分布式微服务实现】
|
3月前
|
资源调度 Java 调度
项目环境测试问题之Schedulerx2.0通过分布式分片任务解决单机计算瓶颈如何解决
项目环境测试问题之Schedulerx2.0通过分布式分片任务解决单机计算瓶颈如何解决
项目环境测试问题之Schedulerx2.0通过分布式分片任务解决单机计算瓶颈如何解决

热门文章

最新文章