引言
记得几年前团队在招聘时,要求候选人有分布式/微服务相关项目的实际经验,当时问了不少候选人一个问题:我们可以通过具体地址直接调用其他服务的接口,可为什么微服务项目里要搞一个注册中心呢?
大多数候选人面对这个问题时,往往会短暂地愣住,随后陷入沉思。然而,当尝试回答时,却好像遭遇了“语言短路”,言辞变得含糊不清,难以表达清晰。其实,里面很多人都能立刻明白提问的核心,也清楚期待听到的答案方向,但往往难以将思绪转化为流畅的语言,总有一种“说不清道不明”的尴尬。
这种现象并非个例,包括我自己也曾深受其扰。究其根本,还是在于对技术的理解不够深入和透彻。虽然我们在脑海中模糊地知道答案的轮廓,但难以用清晰、简洁的语言表达出来,因此,一旦开口,便给人一种“含糊其辞”的印象。许多具备分布式/微服务经验的小伙伴,也常遇到这样的困扰,尽管大家在实际工作中使用过各种组件,也知道大概怎么回事,但真正被问及细节时,就显得有些力不从心。正因如此,本文将深入探讨微服务中各个组件的必要性,以此帮助各位更好地加深对分布式系统的掌握度。
一、注册中心
为什么需要注册中心?这个问题并不难,想清楚两点就够了,一是没有它会怎么样?二是它能给系统带来什么好处?想明白这两个问题后,再回答最开始提出的问题就能做到紧紧有条。当然,为了更好的讲述,我们先来看个例子:
这是一个电商平台的业务体系图,最初是“大锅烩”般的单体架构,随着业务越来越复杂,且团队规模越来越大,从而造成功能迭代缓慢、代码臃肿且边界模糊、系统内部耦合性过高……一系列问题出现。也正是为了解决这些问题,不得不对整个系统的架构重新调整,即根据业务特性拆分出不同的子系统,以此达到分化解耦的目的。
PS:这里不做具体业务拆分,姑且认为拆分的结果,就是图中的每个模块都拆成了一个独立的子系统。
好了,经过这次架构演进后,系统从集中式的单体模式,调整为了分布式架构,可这时会出现一个问题:假设订单子系统的某些业务场景,需要商品、库存、支付、优惠券、会员……等多个子系统支撑,该怎么办?
在原先的架构中,一个模块需要依赖于其他模块的功能,只需将对应模块的Service
实例直接注入即可。在分布式系统里,由于各模块(子系统)都是独立部署的,面对前面所述的场景,只能基于网络来调用对方暴露的接口。既然需要用到对方的接口,怎么调用呢?按以往对接外部系统的经验,我们可以写死对端的IP
地址完成调用。
但这种方式会存在额外致命的缺陷,因为依赖的其他子系统,其地址都会以硬编码形式维护,一旦其他子系统发生迁移或扩容,当前子系统就必须得修改源码并重启。其次,如果某个子系统以集群方式部署,当前子系统还需自己实现一套请求分发策略,否则无法解析出每次请求要去往的具体地址。最后,如果其中某个子系统出现故障,导致无法继续处理业务请求,那么上游子系统的请求都会被阻塞,这就会造成大量请求堆积从而拖垮整个系统!
再站在整个系统的上帝视角来看,拆分出的每个子系统除开上述问题外,还有一个特别麻烦的事情,所有子系统都是点对点通信,这意味着消费者(调用方)需要维护其依赖的提供者(被调用方)配置信息。可上面的例子中,拆分出的子系统数量众多,而且每个子系统都会依赖另外的多个子系统。这时,各个子系统维护的配置信息会特别复杂,并且子系统之间的依赖关系会跟一团麻一样格外混乱。
1.1、注册中心的诞生
综上所述,尽管我们对原先的单体系统进行了拆分,可各子系统间直接以P2P
方式进行通信,仍然会给系统带来极强的耦合性,这违背了最初拆分的初衷,带来弊端远大于好处,显然无法投入生产使用。可是假设我们是时代的“先驱者”,当一个大系统被拆分为多个子系统后,该怎么跃过面临的重重难关呢?我们来认真分析下拆分后、各子系统真正的诉求,然后再看待提出的这个问题。
首先,每个子系统肯定不能以硬编码的形式,写死依赖的其他系统IP
地址,否则对应的子系统一旦迁移,又需要修改源码……。有啥好办法?可以为每个子系统分配一个域名,其他子系统通过域名调用接口即可。这种方式有两个好处,一是降低耦合度,就算对应的子系统迁移机器,只要域名不换,依赖它的其他系统就无需修改源码;二是提高维护性,每个依赖它的子系统,也不需要自己实现请求分发/负载均衡策略,可以直接在解析域名时统一配置。
其次,子系统之间通信时,要能够动态检测出其他系统节点的健康状态,比如订单系统依赖的支付系统,对应着A、B、C
三个节点。当订单系统在调用接口时,如果B
节点出现故障无法处理请求,要能第一时间感知到,并自动将分发到B
的请求,转移到健康的A、C
节点,从而保证两个系统间的通信不会受阻。当然,实现这点也不难,存在依赖关系的系统做个心跳检测机制即可。
最后,每个子系统还需要支持动态伸缩,传统的Nginx
集群方案,扩容也好,缩容也罢,但凡涉及到集群成员变更,就需要重启Nginx
才能更新集群配置,如果类似的方案放在拆分后的分布式系统,这无疑是致命的。比如某个子系统的性能跟不上业务需求时,想要增加一个集群节点来提升吞吐量,为了使得依赖它的其他子系统感知到,就需要重启当前子系统的接入层,而在未真正完成重启的这个窗口期内,整个系统有可能陷入瘫痪,导致无法继续受理用户请求。
当两个存在交互关系的子系统,做好上述三条后,才能保证拆分出的分布式系统具备可用性,但大家回过头来思考一下,上面提到的几种机制,是不是每个子系统都需要?如果每个子系统都单独实现一套逻辑,这未免太过冗余……。好,既然这些是所有子系统的共性机制,那能否沉淀出一个公共组件呢?答案是当然可以,而这个组件在如今就被称为:注册中心!
1.2、注册中心的优势
在分布式系统中引入注册中心,可以很大程度上提升系统的灵活性、可拓展性及容错能力,具体优势如下:
- ①服务注册:节点启动会自动登记自身的详细信息,最终在注册中心形成可用的服务地址清单;
- ②服务发现:当某个服务新增/剔除一个节点后,消费者能立马动态感知到节点变更的信息;
- ③系统解耦:不再依赖硬编码的地址通信,各系统之间无需关心服务提供者的具体
IP
、端口; - ④灵活性:基于服务注册与发现机制可以随时动态伸缩,这使得系统能应对各种突发状态与弹性负载;
- ⑤中心化管控:注册中心可以统一维护所有服务实例,并对节点进行健康探测,自动剔除故障节点等;
- ⑥拓展性:注册中心可以为动态路由、故障恢复机制、智能化负载均衡等高级功能提供底层支撑;
- ⑦……
注册中心逐一解决了系统拆分带来的种种问题,它不仅是不同服务之间通信的桥梁,也是实现服务治理和动态性的关键基础设施。为此,注册中心是分布式/微服务系统的“中枢神经”,通过它能让节点数量众多、依赖关系复杂、调用关系混乱的分布式系统更加便于治理与维护。
当然,微服务架构中,前面所说的子系统称为“服务”,具体运行服务的节点,被称为“服务实例”;同时,一个服务依赖另一个服务的功能时,如
A
依赖于B
,其中A
被叫做服务消费者,B
被叫做服务提供者,不过这仅是概念层次的区分,实际项目中,一个服务可以同时具备消费者、提供者两种身份。
现如今,微服务技术已经十分成熟,也随之诞生了许多优秀的开源注册中心,这使得大家在设计或拆分系统时有了诸多选择,如早期的Zookeeper、Eureka、Consul
,到如今主流的Etcd、Nacos
,甚至服务网格的云原生产物Istio
等等。不过这里不对如何选型做展开,毕竟这偏离了本文的主题,大家感兴趣可以自行研究~
二、远程调用
抽象出注册中心组件,这使得不同服务间的跨进程交互更加便捷,可是真的便捷吗?实则不然,注册中心的出现,虽然能让系统中的每个服务消费者,不必再费心去维护服务提供者的配置信息,但来仔细观察不同服务之间的通信过程:
这是一个简化的提交订单业务,如果是单体项目,订单需要其他模块的功能提供支撑时,直接调用对应Service
层的方法即可。换到分布式系统,因为各个业务都是独立部署,图中的箭头指明的各类操作就得走网络通信完成,如果对接过外部系统的小伙伴,可以回顾下整个对接流程,大致如下:
- ①根据接口文档定义好请求地址、请求结构体、响应结构体等;
- ②按照要求构建并组装请求报文(请求方式、请求头信息、请求参数等);
- ③使用网络类库,并设置好超时时间、报文格式等,然后真正发出网络请求;
- ④接收并校验接口返回的响应报文,视情况对返回的数据进行处理(验签、解密、清洗等);
如果以对接外部第三方接口的形式来做分布式系统开发,每对接一个其他服务的接口,至少需要经过上述四步,这显然特别繁琐,并且增加了许多工作量。以上图的案例来看,因为各服务拆分粒度较细,提交订单这个操作可能依赖十多个服务,这种情况下,光对接接口的成本都会高于实现业务逻辑……
PS:
SOA
架构中,子系统之间通信就采用这种方式,而且不是轻量级的Http+Json
,而是更折磨人的WebService+XML
~
除了消费者调用接口麻烦之外,作为被调用方的服务提供者也麻烦,比如之前的单体架构中,订单模块需要用到库存模块的某个功能,直接调用Service/Dao
层的方法即可;到了分布式架构里,Service/Dao
层的方法不一定对外暴露了接口,当消费者需要的功能没有提供接口时,还需要挨个重新定义并对外暴露才行。
所以,注册中心仅仅只是单体迈向分布式的第一步,现在又面临了新的挑战,因为分布式/微服务系统里,跨服务通信是常事,又该如何简化服务之间的交互过程呢?
同样的道理,虽然不同服务之间调用的接口有所差异,但调用的过程却极其类似,那么同样可以抽象出可复用的远程调用公共组件。当然,在如今成熟的微服务生态下,你可以选择基于HTTP
的RestTemplate、OpenFeign
,也可以选择性能更优的RPC
组件,如Dubbo、gRPC
等,使用这类组件的优势在于:
- ①我们无需关心底层的
HTTP
细节,如请求构建、响应解析、错误处理等,简化了服务通信过程; - ②允许使用类似于本地方法调用的方式来调用远程服务,进一步提高了代码的可读性可维护性;
- ③能与注册中心紧密配合,在服务注册与发现的基础上,可以动态感知服务提供者的地址并完成调用;
- ④服务之间的交互属于系统内部通信,可以去掉许多身份鉴权、参数校验机制,性能方面更佳。
当然,上述几点只是针对OpenFeign
这种远程调用组件,对于Dubbo、gRPC
这种专业的远程调用组件来说,因为它们使用更底层的网络协议通信,也会采用性能更强的序列化算法,如Hessian2、ProtoBuf
,所以通信时的开销更小、性能更强悍!
三、服务网关
有了注册中心、远程调用组件后,这为我们解决了许多问题,可是再来看一个现象:
虽然系统内部可以基于注册中心的服务名+远程调用组件,完成不同服务间的数据交互,可是当浏览器、小程序、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
处理动态路由和微服务相关的功能,以此设计出更强大、灵活的接入层,示意图如下:
四、负载均衡器
经过不断推演,分布式系统已发展至上述架构,目前内部服务治理问题、外部终端接入问题都已得到解决,那么系统目前是否还存在问题呢?存在,即上述一直反复提及,但一直未作展开的问题:负载均衡!
众所周知,解决高并发最简单粗暴的手段是加机器,使用更多的节点组成集群,从而达到提升吞吐量的目标。在分布式系统中,这个手段同样适用,比如作为电商平台核心之一的商品服务,运行期间会面临较大的访问压力,这时单个节点扛不住,就可以选择部署集群,但问题来了:假设订单服务依赖于商品服务,这时商品服务有五个节点,在做远程调用时,究竟该请求哪个节点呢?
听到这个问题,大多数人心中就有了答案:调用时根据特定的载均衡策略分发请求即可。同样的道理,系统内每个服务都有可能与其他服务通信,并且任意服务都有可能做集群部署,这意味着“负载均衡”也是个公共需求,这时也可以封装出一个公共组件,不过微服务架构中,这个组件被称为:客户端负载均衡器。
4.1、客户端负载均衡与服务端负载均衡的区别
负载均衡这个概念并不陌生,就算单体项目亦可通过Nginx
搭建集群,并基于Nginx
的负载均衡机制分发客户端的请求,那么微服务中的客户端负载均衡,与传统的负载均衡技术有何区别?为何微服务中不复用Nginx
这类组件?
答案很简单,Nginx
这种组件对于微服务之间通信来说太重了,并且会增加系统的风险,因为服务端负载均衡的本质是代理,所有请求都连接代理器(负载均衡器),而后再由代理器将请求分发给具体的节点,一旦代理器出现故障,就有可能造成系统瘫痪。
正因如此,站在客户端角度打造的负载均衡组件,则能完美解决上述问题,所谓的客户端负载均衡,即是指每个调用接口的客户端(服务消费者),都具备请求分发的能力,如下:
微服务中能实现客户端负载均衡,这得益于注册中心,因为每个服务都可以通过服务名称,从注册中心里换取到指定服务的可用节点列表,拿到节点列表后,这时不管哪个节点都具备分发请求的能力。同时,这种能力对每个服务来说特别轻量级,也不会增加系统风险。
4.2、客户端负载均衡的优势
客户端负载均衡,除开能用于服务之间通信的场景外,针对外部入站的用户请求,同样可以实现负载均衡效果,Why
?因为网关也是一种特殊的服务,它同样会注册到注册中心,而所有入站流量都会经过网关,路由时它自然能根据负载均衡策略实现用户请求的分发,这里简单总结下优势:
- ①通过负载均衡可以让单一节点的服务部署集群,使得每个服务都具备超强性能的潜质;
- ②客户端负载均衡器与注册中心配合,可以使任意服务都能支持运行期间的弹性伸缩机制;
- ③客户端负载均衡器让每个消费者都具备请求分发能力,在分布式系统中具备更高的容错性;
不过相较于服务端负载均衡来说,客户端负载均衡分发请求会造成一定的倾斜,比如A
服务依赖B
服务的接口,B
服务由五个节点组成,A
服务由三个节点组成,假设这时的负载均衡是轮询算法,A
服务的三个节点在轮询分发请求时,节点之间无法相互感知,就会出现A1
刚给B1
分发一个请求后,A2
又给B1
分发了一个请求……这种现象。
相较于服务端负载均衡来说,虽然客户端负载均衡会造成请求分发出现倾斜,但这种倾斜度不会太大,总体上利远大于弊。
对于选型来说,如果是SpringCloud
体系,主要有Ribbon、LoadBalancer
两个开源组件可选,目前一般会选后者;如果是其他体系的分布式项目,例如纯Dubbo
的项目,通常会自带客户负载均衡功能;如果全面拥抱云原生环境,也可以使用自带的云基础组件实现负载均衡的效果~
五、服务保护
通过引入负载均衡组件,入站流量的路由也好,服务间远程调用也罢,都可以具备了运行期间动态伸缩节点,并自动将请求分发到健康节点的兼容能力,这种模式能极大程度上满足如今高并发冲击下,弹性伸缩的需求。可是话说回来,高并发带来的问题不仅仅只靠弹性伸缩就能解决,毕竟硬件资源总会达到上限。
假设此时遇到这么个场景,系统出现特别大的并发流量,从而触发弹性扩容机制,并在短时间内耗尽所有空闲的待分配资源,可系统仍然扛不住持续增加的业务请求,在这种空闲资源支持继续扩容情况下,又或者脱离云环境部署的分布式系统中,面对来势汹汹的高并发冲击怎么办?
在有限的硬件资源下,系统能处理的并发请求总会有上限,可业务流量还在持续攀升,面对这种情况,系统中的所有节点只有一个应对方式,那就是“我死给你看”,不过“死”的多种多样,如CPU过载、内存溢出、进程崩溃……。更为重要的是,一旦某个节点发生故障,那这种情况很可能会在短时间内蔓延至整个系统,从而造成整个系统瘫痪,来看例子:
假设这是下单的请求链路,并且整个链路以串行化执行(实际会将部分操作异步化),当图中的库存服务率先宕机后,那位于它上游的订单、商品、优惠券、会员服务,也会在不久的将来陷入宕机状态,为什么呢?因为库存服务发生故障,代表所有请求会被阻塞堆积在会员服务,在很短时间内,源源不断到来的请求会将会员服务拖垮,接着就是优惠券服务……,而这种故障逐步蔓延直至扩散到整个分布式系统的现象,则被称之为:服务雪崩!
5.1、服务保护组件的优势
在前面所说的背景下,必须得弄套“盔甲”保护脆弱的系统,以此提高整体的容错率,而这套盔甲对应的组件就是服务保护,其主要作用就是:保障整套系统在故障不可控的环境下稳定运行,下面具体说说服务保护组件在分布式系统中的作用。
①防止服务故障的蔓延:分布式系统里每个服务通过远程调用进行通信,一旦某个被调用的下游服务发生故障,那么依赖它的上游服务也可能发生故障,最终造成故障蔓延至整个系统。服务保护组件通过断路器实现熔断机制,可以在某个服务发生故障的时候,给调用方返回一个错误响应,而不是长时间的阻塞等待,从而防止故障在服务之间的进一步传播。通过这种及时切断链路的机制,能有效地控制故障范围,避免单点故障引发整个系统崩溃的灾难性后果。
②实现服务降级与限流:在服务保护组件的支持下,当某个服务不可用时,系统可以自动降级到备选服务或备用方案,以保证核心业务功能的正常运行。其次,当某个服务的负荷达到预设的瓶颈值时,服务保护组件可以基于计数器、令牌桶、漏桶、滑动窗口等算法,适量拒绝后续源源不断到来的请求,以此将单个服务的负荷保持在可控范围内,避免激增的脉冲流量将系统打垮。服务降级和限流策略,能够在很大程度上提高系统的容错率和可用性。
③提供资源隔离:先举个例子,比如A服务的不同业务分别依赖B、C服务,如果这时B服务宕机,A服务调用B服务就会造成线程阻塞,最终耗尽A服务有限的线程资源,导致正常的A→C链路也不可用。为了避免这种“由于某个服务的故障,导致整个服务的线程资源被耗尽”的问题,服务保护组件通常会提供类似于沙箱的隔离机制,即隔离每个下游服务的线程资源。这样,即使某个服务的调用线程被阻塞,也不会影响其他服务的远程调用。
④提供请求缓存、合并、重试和监控机制:服务保护组件还可以提供请求缓存、请求合并等功能,以减少对下游服务的调用次数和减轻其负载压力,比如两个几乎连续的并发操作,需要发起相同的远程调用请求(如查询商品详情数据),这就可以合并成一个请求来调用。同时,服务保护组件还具备强大的监控能力,能实时监控服务的运行状态和性能指标,为系统运维和故障排查提供有力支持。
单从历史性角度出发,最初的Hystrix
无疑开创了服务保护组件的先河,它是服务隔离、熔断、降级等理念的传播者,后面许多相同赛道的开源组件,或多或少都借鉴自该组件。不过可惜的是,Hystrix
开源版已宣布停止更新并进入了低维护的阶段,如果你现在需要一款服务保护组件,那Sentinel、Resilience4J
是不错的选择,当然,Hystrix
最后的稳定版也足以满足绝大多数需求。
六、配置中心
引入服务保护组件给脆弱的系统穿上盔甲后,系统健壮性得到了质的飞跃,可随着服务数量越来越多,系统的配置信息将会变得难以管理,尤其是套入多版本、多环境的配置会更加混乱,有时发版往往一个配置项忘记改了,就会造成启动失败等现象。同时,如果发现某个配置项未更新,改成新值后必须得重新构建、打包、发版才能生效,这无疑降低了开发效率。
约定大于配置、配置大于编码,配置信息是分布式系统的核心,为此,系统急需一种将所有服务配置信息聚合起来、并且便于维护的技术手段,也就是在这个背景下,分布式领域出现了新的组件:配置中心,它能给分布式系统带来许多好处:
- 集中管理:可以将所有服务的配置信息集中起来统一管理,使得各服务配置项维护起来更加便捷;
- 降低冗余:各业务服务会存在一定量的重复配置项,在配置中心里可以抽出全局的配置降低冗余;
- 动态刷新:配置中心支持动态更新配置项,并实时推送至各微服务,并无需重启服务即可生效;
- 版本管理:当配置信息发生变更时,配置中心会自动保存历史版本,可以随时回滚支持多版本机制;
- 多环境支持:配u之中心可以通过命名空间等隔离方式,区分出不同的环境配置,使得配置项更加清晰;
除上述列出的优势外,配置中心还具备权限管理、灰度发布、监控与告警等功能,这能为系统的稳定运行和高效管理提供有力支持。通常而言,注册中心都附带配置中心的能力,如ZooKeeper、Consul、Etcd
是天然的K-V
存储组件,自然可以用于存储配置信息,Nacos
甚至专门有设计配置中心模块。当然,市面上同样有许多优秀的开源配置中心,如Apollo、SpringCloud-Config、XDiamond……
。
七、可持续集成/持续部署(CI/CD)
到目前为止,前面提到的注册中心、远程调用、服务网关、负载均衡器、服务保护、配置中心这六大组件,奠定了分布式/微服务系统的基架结构,这六大组件属于分布式/微服务项目必不可少的基础设施。但除开这六大基础组件外,实际分布式架构落地过程中,往往还需要更多的技术组件做支撑,继续来往下看。
在以前,一个系统开发完成需要部署时,都会从各大厂商手中租赁或购买硬件服务器,而后将源码手动打包并人肉部署,这是单体时代的主流部署方案。可到了分布式时代,随着业务拆分越来越细致,分化出的服务数量会越来越多,系统依赖的组件也越来越多,同时还要考虑高可用,为核心组件/服务搭建集群,部署所需的硬件资源呈直线上升。
其次,除开费用高到令人发指的硬件成本外,另一个难题是难以运维!几十上百种服务/组件相互交错,代码管理、源码打包、环境部署、版本发布、版本回退、故障恢复……,如果还以人工形式部署,稍微搞错一步,就会发现整个系统跑不起来,这种体验简直令人生无可恋!
如果再以单体时代的模式来部署分布式系统,就算你将业务需求开发完了,可能光部署就得再耗费一周,怎么办?为了解决硬件成本、降低运维成本,虚拟机容器+自动化运维技术被推上了时代风口,通过Git+Jenkins+Docker+K8S
这类技术,构建出完善的CI/CD
(可持续集成/持续部署)流程,能在极大程度上减轻运维成本、提升开发效率:
为了节省篇幅,相对详细的过程可参考《漫谈分布式专栏的开篇:《架构演进篇-容器化时代》章节,这里总结下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时也很难排查,因为目前的日志是从各个节点聚合而来,这时,去查看日志就会发现日志根本不连贯,你很难将这么多节点的日志,按请求调用链路的顺序先后串联起来,排查时只能从从最下游开始向上排查,还得挨个翻日志,极其影响开发效率~
正因如此,为了方便日常排查功能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
、多维度订单指标、各业务榜单、营销转化率、业务风控策略等;
综上,想搭建一套全面的监控体系绝非易事,但监控体系在大型分布式系统中至关重要,监控系统能够实时收集和分析系统各项监控指标。从技术角度来看,完善的监控体系,能及时发现潜在问题、故障及性能问题,并达到预设的阈值时自动告警,通知技术人员快速响应并进行故障排除,从而减少系统停机时间和用户体验下降的风险。从业务角度出发,还能基于监控系统收集的业务指标和用户行为数据,为业务运营和战略决策提供重要依据,以此帮助优化产品和服务,提升用户体验等。
不过可惜的是,虽然市面上有着许多监控相关的开源组件,如Prometheus+Grafana、Zabbix、ELK-Stack、Nagios
等,但这些组件都只聚焦于“服务器资源监控”这个方面,而对于其他方面的监控,要么得花钱买对应的监控产品,要么就自研监控系统。相对而言,如果规模够大,后者是个不错的选择,尽管耗时耗力,但能根据实际需求来定制监控指标,从而使搭建出的监控体系更贴近业务系统。
十一、分布式事务
就目前为止,一个成熟的分布式系统,该有的基础设施组件就介绍完了,不过实际的分布式项目中,在不同的业务和技术背景下,往往还有更多的困难等待我们去克服。比如在之前的单体项目中,如果一组操作要保证事务,只需要在对应的业务方法上加一个@Transactional
注解即可,这样就会将事务托管给Spring
来负责,而Spring
又间接依赖于关系型数据库提供的事务机制,执行过程如下:
- 在该方法执行时,
Spring
会首先向数据库发送一条begin
开启事务的命令; - 如果执行过程中出现异常,
Spring
会向数据库发送一条rollback
回滚事务的命令; - 如果执行一切正常,
Spring
会向数据库发送一条commit
提交事务的命令。
这种事务管理机制,在单体架构中显然十分好用,可到了分布式系统中,不仅根据业务拆分了应用服务,数据库也会根据业务拆分出不同的独立库。这时,就无法依赖传统关系型数据库提供的事务机制来保证一致性,示例如下:
分布式系统的不同业务模块之间,只能通过远程调用的方式调用对方所提供的API
接口,假设这里在库存服务本地的「扣减库存」方法上加一个@Transactional
注解,同时在订单服务本地的「新增订单」方法也加一个@Transactional
注解,Spring
内部的处理逻辑如下:
- 下单业务远程调用「减库存」接口时,
Spring
会先向库存DB
发送一个begin
命令开启事务; - 当扣减库存的业务执行完成后,
Spring
会直接向库存DB
发送一个commit
命令提交事务; - 下单业务调用本地的「新增订单」方法时,
Spring
又会向订单DB
发送begin
命令开启事务; - 当新增订单执行出现异常时,
Spring
会向订单DB
发送一个rollback
命令回滚事务。
此时分析如上场景,下单业务理论上应该属于同一个事务,但之前《MySQL事务篇》聊到过,InnoDB
的事务机制是基于Undo-log
日志实现的,那么减库存产生的回滚记录会记录到库存DB
的Undo-log
中,而新增订单产生的回滚记录则会记录到订单DB
的Undo-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
注解就能解决之前令人困扰的分布式事务问题~
十二、总结
看到这里,代表本篇走进了尾声,其实大家会发现,架构演进带来好处的同时,还会产生一系列问题,当那些伟大的技术先驱者持续探索,并在解决这些棘手的疑难杂症过程中,就形成了分布式领域里一个个热门的概念;当问题被真正解决并分享出来后,就成为了如今热门的开源组件。这些分布式/微服务组件,就是某一类特定问题的解决方案,也正是因为不断出现新的开源组件,才铸就了更加繁荣的分布式/微服务生态。
先人栽树,后人乘凉,作为后人的我们是时代的受益者,接触分布式/微服务技术栈时,一开始学习的就是这些已经成型的开源组件。而很多资料、课程在讲解只阐述了这些组件的作用,大家在学习时,潜意识下就忽略了这些组件诞生的原因,最终造就了“知其然而不知其所以然”的情况出现,学,我会;用,我也会!可是这些组件为啥需要,我却说不清……。因此,大家在学习/使用一项技术时,一定要多加思考,这样才能带来真正的技术成长,而不是因为工作需才去“搬砖”的工具人。
当然,分布式/微服务要面临的问题,远不仅本文提到的这些,如并发安全需要分布式锁、定时任务需要分布式调度、数据检索需要搜索引擎、海量数据需要大数据生态……。不过这些已经逐渐偏离了本文的核心主题,所以会放在后面的章节中单独阐述,毕竟提到的任何一条,一旦深究都是比较庞大的话题~
所有文章已开始陆续同步至公众号:竹子爱熊猫,想在微信上便捷阅读的小伙伴可搜索关注~