前言
后端开发人员都对“分布式”这3个字既熟悉又陌生。说对它熟悉,是因为自己无时无刻的不身在其中,比如spring mvc、spring boot 、spring cloud、dubbo、spring cloud alibaba、web容器、消息中间件、数据库中间件、redis、nginx等等,日常开发可能或多或少的跟它们打过交道。说陌生,是因为分布式本身就是一个及其复杂且的领域,要把它彻底的说清楚需要去了解CAP理论,BASE理论,以及相关的分布式事务,一致性理论,高可用,通讯节点与网络等等,这些理论和算法,以及为解决分布式带来的各种问题所诞生的各种中间件和方案…你整明白了几个?你敢说你跟它熟?
![](https://i-blog.csdnimg.cn/blog_migrate/200cd1c0a807175258348f0398c9c7a1.gif)
前世与今生
有很多小伙伴,刚开始工作就在已经在一个庞大的分布式系统中进行开发了,但是自己可能浑然不知。每天被if/else,crud和各种业务按在地上反复摩擦,对着键盘ctr+c+v疯狂输出(不接受反驳,不信你自己看看键盘上那几个键是不是已经模糊不清了😄),也来不及(懒得)去思考,系统为何会分成如此多的微服务,这样做的好处是什么,带来的问题是什么,业界有哪些应对方案和开源组件,我们又该如何实践?本文将从宏观和微观的角度,去浅析(走过场)这些问题,抛砖引玉,有不妥的点,还请不吝指教。
为了弄清楚分布式架构,我们有必要通过它的演进历程来去理解。下面就用一个简单地电商系统示例,来展开我们今天的话题
一、单体架构
-
在业务发展初期,用户量,业务量都很少,为了快速迭代,通常会是用简单的架构和部署。
![](https://i-blog.csdnimg.cn/blog_migrate/07ddeba39a725b05122e01697ed02d2b.png)
-
随着业务方发展,用户也来越多,流量也越来越大。一些数据读写压力直接传导在数据库。导致数据库出现性能瓶颈。这个时候我们首先想到的是用缓存来过渡。这个阶段使用的缓存是内存缓存。(内存缓存在分布式环境下会成为一个问题点)。那么架构演变成下图所示。
![](https://i-blog.csdnimg.cn/blog_migrate/2046b3a30cdd2518c65c16da707a7671.png)
慢慢的客户群体增多,日益增多的请求,及高并发的请求,导致单机的服务器开始支撑不住。这时候单机计算机的处理能力达到上限,无论如何调整,单机机器的性能,都无法突破该瓶颈。备注:这里瓶颈主要是指,CPU资源,内存资源,磁盘IO,网络IO,线程资源等等。这些问题的出现,让我们不得不思考新的方案解决该类问题。集群架构就此诞生。
二、集群架构
-
既然单机无法支持大量的并发,那么使用多台机器来分摊请求压力不就OK了。怎样才能达到该目的呢?nginx做反向代理,配置合适的路由策略就能达到该目的。
![](https://i-blog.csdnimg.cn/blog_migrate/419842a35b13ed7e201ef6436dce9a33.png)
集群架构可以一定程度上的缓解应用服务器的压力,多台机器理论上就可以承载更多的并发请求了。这里开始已经有一点点分布式的味道在里面了。
大家有没有注意点其中的问题点:
-
内存缓存的问题,各个节点很容易出现缓存不一致。
-
如果大量扩展应用服务器机器节点,大量的读写透过缓存,直接落到数据库,会造成数据库的瓶颈。
为解决上述问题,架构再一次演进。
-
分布式缓存+数据库读写分离
![](https://i-blog.csdnimg.cn/blog_migrate/3ff97025e3ba7940c186913f56fd98b0.png)
大家可能还会注意到,上面的集群架构仍然有演进的空间,随着请求量和业务量的增加有几个地方仍然很容易产生瓶颈,比如:
-
Redis缓存瓶颈,可以演进成集群架构。
-
Mysql主从架构瓶颈,可以考虑分库分表。
-
Nginx代理转发瓶颈,当一台nginx服务器的资源利用达到上线时,就必须考虑nginx集群,那么在nginx集群之上又有谁来做负载转发呢?这里就得考虑LVS/F5 + Nginx集群解决。(至于啥是LVS和F5这个还请大家自行去了解,本文篇幅有限就不展开了)
![](https://i-blog.csdnimg.cn/blog_migrate/19f6198bb900215549c6f5a6f507eddf.png)
本次演进(解决了Nginx瓶颈,缓存瓶颈,及数据库瓶颈),集群模式下算是一个相对终极的版本了,它虽然可以支持大量客户请求和服务,但是如此庞大的集群,和巨大的代码库给开发和运维带来了相当大的挑战。
到目前为止,仍然没有做出对业务做拆分及分布式架构的调整,为什么不提早做微服务的拆分和分布式架构呢?真相只有一个:一切不以业务导向的架构都是耍流氓…。其实还有另外一个原因:一旦拆分成分布式架构体系,必然带来各种服务间的协同和治理问题,为了保证数据一致和结构的稳定,在分布式环境下需要解决各种复杂的问题,为解决这样的问题带来的各种学习成本,软硬件成本,运维成本等等都是未知且巨大的,没有足够的经验很难把控。这也是为什么很多公司及团队,不在万不得已的情况下不去考虑服务拆分做分布式的架构。
前面讲了这么多,真正分布式才开始。
在开始分布式架构前,我们需要考虑为什么做,怎么做,做了有哪些问题要解决,如何解决等等,这都是演进过程中需要谈论的?
庞大集群面临的问题:
-
集群节点伸缩,配置问题,当新增一个节点或删除一个节点,需要更改nginx或LVS的各种配置,可见的运维成本不小。
-
代码工程集中,开发成本曲线逐渐陡峭。试想一下,所有业务功能模块都在一个代码库,当开发人员成千上百的时候,代码版本控制都成问题,更别说快速的开发满足日益变化的业务需求。
-
对于持续集成,交付,部署的问题,代码编译->打包->部署->UT->CT->部署…,巨多的业务模块耦合在一起,会导致牵一发而动全身,打包出来的部署包可能成百上千M大小。
-
扩展太不灵活,往往由于某个业务的处理瓶颈,需要把整个节点都扩展出来,造成其他处理要求小的模块也不得不一起扩展,产生资源浪费。
随着上述问题的出现,业务快速发展跟日益剧增笨重的开发、测试及运维流程出现了不可调和的矛盾,迫使开发者、架构者们另谋出路。于是SOA、微服务、分布式架构等等开始进入大家的视野。
其实其本质的思想还是拆分与分治。将原有的庞大的应用,按照业务功能划分,拆分出或大或小的独立应用服务,以较小且独立的单元实现快速迭代和交付。
理想和丰满,现实却很残酷
-
如何拆分应用?
有不少业务或团队在迈出这第一步的时候,就已经“望而却步”不了,做业务开发的同学应该都明白,平时写代码如果从来都考虑解耦和扩展的事情,十年如一日的CRUD,想必在这一步可能九死一生。
将一个庞大且凌乱的系统剥离开,是一件极具挑战的事情,得整体的去规划业务,划分好领域边界…这里就不展开说了(主要是本人才疏学浅,想必也说不清楚,😝)。这里姑且就假设咱们的开发都很NB,把解耦和扩展都考虑的淋漓尽致,让业务架构师和技术架构师们保住那所剩无几黑发…,顺利的拆完模块,并拆出来了对应的业务表,拆解后每一个独立的单元都能提供业务能力。看下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/a698f09d34268357f851c5f0347971ca.png)
2. 拆分后如何调用?
以前在同一个应用内,各个模块间的调用通过service接口方法就能轻松完成,但现在,它们都被拆分到不同的应用,也意味着是一个独立的web-server(JVM进程)服务,要完成一个完整的业务流程,必然会跨服务调用。拆分后面临的第一个浅显且必须解决的问题摆在面前:远程调用!
有些老铁可能会来杠精了…这算啥子问题嘛?直接上spring cloud 不就完事了?啥?dubbo它不香吗?
额,嗯… ,稍安勿躁,说好的谈演进过程呢?一下子就飞机火箭,一步登天了,我还演进啥?老铁们手下留情,给个面子,舞台留给博主好吗…
言归正传,对于远程调用(RPC),其实有各种各样解决方案,但是万变不离其宗,我这里也总结了一下,要解决RPC问题,有一些必要的步骤:
-
建立通讯通道:既然是不同的应用,他们之间的通讯只能通过网络来进行了。TCP?UDP?长链接?短连接?
-
各个服务的寻址:调用者服务要能唯一的确定调被调用服务的“地址”。
-
通讯协议:调用方和被调用方需要有达成共识,能通过“共识”解析出数据。私有协议:比如定义一个只有你公司或业务特有的协议。公共协议:http协议。
-
序列化和反序列化:对要在通道内传输的数据进行编码的过程,该过程有一些业界比较有名的工具,比如: thrift, probuff等等。
做好以上的几件事,我们就能进行正常的RPC了。其实业界RPC框架五花八门,除了dubbo还有
Motan,
Dubbox,
Thrift,
gRPC等等。它们都很好的实现。从广义上来讲,以httpclient为首的http访问客户端也可认为是RPC框架,只不过它是对http协议的定制框架而已。而恰好业务都是用的是tomcat作为web容器,http协议通讯RPC变得十分普遍,以至于我们忽略了它。
![](https://i-blog.csdnimg.cn/blog_migrate/958467526860936c44e9ce81c13dcc86.png)
3. 服务节点伸缩该如何应对
解决了“调用问题”,仅仅只能算是解决温饱。服务拆分了,业务团队职责也可以随之划分的更清晰,从此代码和迭代的版本控制就可以隔离了。但是另一个噩梦即将开始。大家有没有发现一个问题,这种简单的RPC调用,还只能做到点对点。但是当某个模块(服务)本身就是热点,比如:用户服务,订单服务,它们需要承载着大量的请求,特别是在某些特殊场景下需要增加节点来提高整体的处理能力。那么这个时候该怎么办。下面拿用户服务和订单服务来举例:
有的大佬,可能又有话要讲了:扩展节点很简单呀,把用户服务、订单服务多部署几台,使用nginx做反向代理分分钟解决问题啊?我竟无言以对…
的确,这是一种可行的方案,但是却忽略了一个很重要的问题,使用nginx这大前提就是咱必须使用web容器,必须使用HTTP协议进行通讯了呗?试想一下,我如果使用dubbo-rpc框架,又或者使用gRPC框架,甚至可能是大神自己造轮子实现的RPC使用私有协议通讯。你一上来就整个nginx。这让我们这些小码弟如何是好?
其实从这里开始,就体现出RPC框架选型的重要性了。如果一开始咱们就选择web容器,然后使用http协议进行通讯,那么这一步做起来可能就如大佬所说,搞个nginx反向代理便能轻松搞定。但是如果你要使用开源的RPC框架,那么就得考虑它有没有配套扩展能力配置了。如果你要造轮子,那恭喜你,这个问题点只是分布式环境下要考虑问题的冰山一角。
不管你采用什么方式你都逃避不了一个事实:随着节点的扩张或者减少,都需要我们去更改配置。比如:nginx需要去实时修改反向代理配置,dubbo-rpc需要去修改服务消费者reference的地址等等。当服务较少的时候,手动改改还没啥,如果有成百上千个服务要同时修改配置,你想像一下这个工作量,及出错的概率???
![](https://i-blog.csdnimg.cn/blog_migrate/6f9951f82c9fa4cc43dde80a0d457a75.png)
-
多节点的协调问题
随着解决问题的逐步深入,我们发现不同服务的节点间的协调(比如:订单服务调用用户服务,感知用户服务状态),相同服务不同节点的协作(比如:订单服务节点1和节点2共同对外负载提供服务),各个服务自身的高可用保障,故障自动转移和恢复等等问题开始浮出水面。
演进到3阶段(节点伸缩)的时候,用nginx做反向代理,其实就隐式的解决部分问题(比如:相同服务不同节点间的负载均衡(协作问题),利用探针可以剔除不可用的节点)。表面上看起来,除了频繁修改配置较繁琐外,一切都还好。但是在要求支持高并发低延迟的场景下,你会发现单机的nginx很容易就成为瓶颈,它的网络宽流量也会承受巨大压力,一旦单点故障,就会导致大面积服务不可用…,总结起来在第3阶段的架构有如下几点短板:
-
任何节点的伸缩,都会引发nginx配置的修改。在大规模的微服务场景下,这种运维成本可想而知。
-
nginx单点故障问题。
-
围绕nginx的服务器网络带宽极有可能成为机器性能的短板效应的缺口。
-
(可能还有其他因素,本人不才,还望大佬指教,后续我好改正更新)
那么针对这些短板,也有一些简单且直接的理论对策,如下:
-
伸缩节点时能否做到让调用方节点自动感知,这样就不需要人工配置了。(运维:此处应该有掌声...)
-
那nginx有单点故障问题,那直接去掉它呢? 还是对nginx做集群扩展 (运维:一脸黑线…) 。[ 此情此景选择哪种方案,就不言而喻了吧 ]
-
流量太集中,那么咱们就尽量多路,多点对多点,,防止流量扎堆。(这不扯犊子么?又回到点对点的难以扩展的问题上了吗?)
有了理论和对策,那就实现起来,一言不合。大名鼎鼎的分布式组件从此就诞生了,它就是:注册中心。它有两个核心概念:服务注册和服务发现。关于注册中心是如何解决这些问题的,请读者自行去了解。业界也有不少注册中心开源组件,比如:nacos,dubbo+zk,
consul,eureka等。
到此为止咱么的架构就演进如下:
![](https://i-blog.csdnimg.cn/blog_migrate/2064346a2fe3d5fa16aa631385cb5318.png)
5.服务治理
经历了上面一些列的演进,我们分布式体系下,微服务协协作日趋完善。但是要想架构更优秀跟完善咱们还得考虑更多关于服务治理相关的问题。比如:
-
服务的优雅上下线,生命周期管理
-
服务间流量调配
-
服务性能驾考
-
服务发布环境隔离
要解决这些问题,优秀的框架一般都提供这些能力,比如:dubbo,spring cloud,spring cloud alibaba。团队还可以根据自己的需要去扩展,定制。至于具体功能点,请大家自行脑补。
6.
问题追踪
以前是单体服务,所有的业务关键日志都记录在同一台机器上,出问题,查询日志都比较方便,当拆分成微服务做分布式架构,一次请求的日志会散落在各个服务器上,如果还使用传统查log文件的方式,那将是一件令人痛苦的事情。这个时候就需要有一个日志中心来统筹这件事了。业界传统的解决方案无外乎就是分散采集,集中存储,可视化查询。
其中有一个很重要的概念:分布式调用链追踪。这里也不展开了。感兴趣的小伙伴可以去学习。
业界比较常用的解决方案是:ELK = ES + logstash + kibana。其实在日志采集,分发,缓冲,消费,存储各个环节都是有可替代和扩展方案的。详细的可以去看
Beats相关文档
。
7.分布式事务
当
单体服务拆分成多个服务之后,将会面临一个很常见的问题,那就是分布式事务,这也是分布式架构体系下一个老生常谈的问题,至于解决方案,业界也有很过种,并且有不少开源的中间件。常用解决方案概要如下:
一、强一致性
XA协议:(2pc,3pc)
为了统一标准减少行业内不必要的对接成本,需要制定标准化的处理模型及接口标准,国际开放标准组织Open Group定义分布式事务处理模型DTP(Distributed Transaction Processing Reference Model)
XA是X/Open组织 提出的分布式事务处理规范。XA则规范了TM(事务管理器)与RM(资源管理器)之间的通信接口,在TM与多个RM之间形成一个双向通信桥梁,从而在多个数据库资源下保证ACID四个特性。目前知名的数据库,如Oracle, DB2,mysql等,都是实现了XA接口的,都可以作为RM。所以说XA是数据库的分布式事务实现强一致性的标准。
DTP模型定义如下角色 :
- AP(Application Program) : 既应用程序,可以理解为使用DTP分布式事务的程序。
- RM(Resource Manager) : 即资源管理器,可以理解为事务的参与者,一般情况下是指一个数据库实例,通过资源管理器对该数据库进行控制,资源管理器控制着分支事务。
- TM(Transaction Manager) : 事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理事务生命周期,并协调各个RM。全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一个工作,这个工作即是一个全局事务。
- DTP模型定义TM和RM之间通讯的接口规范叫XA,简单理解为数据库提供的2PC接口协议,基于数据库的XA协议来实现2PC又称为XA方案。
二、最终一致性
-
本地事务表 + 轮询补偿
-
本地事务表 + 事务消息(MQ)
-
TCC(Try-Commit-Cancel)
三、弱一致性
-
最大努力通知 + 消息重试控制
-
监控报警 + 人工干预
具体的逻辑和实践,这里也不多说了,网上资料一大堆。(这块感兴趣的可以一起交流)
总结
通过系统架构的演变,这里简单聊了一下分布架构及其问题,从宏观到微观的都走马观花的梳理了业界相应的解决方案。不管是什么问题,在如今这个互联网大环境下,都有对应的解决方案,就对应的开源框架实现,只要我们把握住其核心思想,在做架构和做框架选型时,才可能游刃有余。