当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。
这句话反过来就是,当单个节点有能力时,最好不要引入分布式系统,因为分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。
记得2016年刚读研一的时候有个刚来学校任教的清河毕业的计算机老师就在班里的课上提到过谁有兴趣和他一起搞分布式系统,当时报了名,但是后来也没有很积极的去学习,唉,现在想起来甚是后悔啊。好在还不晚,去年自学了Kafka、ElasticSearch以及Redis这些分布式实践的系统后,今天就着要学习的Dubbo再来重温一遍,深入理解一遍到底什么是分布式,怎么实现。
分布式理论
分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据,也可以简单理解为:分布式系统(distributed system)是建立在网络之上的软件系统
分布式系统设计思路
从设计思路上来讲,分布式系统可以分为中心化和去中心化,核心区别就是有没有人为规定一个机器领导,中心化是人为指定的,而去中心化是自动选举的。
中心化设计
中心化的设计有如下要素:
- 两个角色: 中心化的设计思想很简单,分布式集群中的节点机器按照角色分工,大体上氛围两种角色: “领导” 和 “干活的”
- 角色职责: “领导”通常负责分发任务并监督“干活的”,发现谁太闲了,就想发设法地给其安排新任务,确保没有一个“干活的”能够偷懒,如果“领导”发现某个“干活的”因为劳累过度而病倒了,则是不会考虑先尝试“医治”他的,而是一脚踢出去,然后把他的任务分给其他人。其中微服务架构 Kubernetes 就恰好采用了这一设计思路。
- 中心化设计的问题:中心化的设计存在的最大问题是“领导”的安危问题,如果“领导”出了问题,则群龙无首,整个集群就奔溃了。但我们难以同时安排两个“领导”以避免单点问题;中心化设计还存在另外一个潜在的问题,既“领导”的能力问题:可以领导10个人高效工作并不意味着可以领导100个人高效工作,所以如果系统设计和实现得不好,问题就会卡在“领导”身上。
- 领导安危问题的解决办法: 大多数中心化系统都采用了主备两个“领导”的设计方案,可以是热备或者冷备,也可以是自动切换或者手动切换,而且越来越多的新系统都开始具备自动选举切换“领导”的能力,以提升系统的可用性。
目前了解到的中心化设计的实践好像只有Kubernetes
去中心化设计
去中心化的设计有如下要素:
- 终生地位平等: 在去中心化的设计里,通常没有“领导”和“干活的”这两种角色的区分,大家的角色都是一样的,地位是平等的,全球互联网就是一个典型的去中心化的分布式系统,联网的任意节点设备宕机,都只会影响很小范围的功能。
- 去中心化不是不要中心,而是由节点来自由选择中心: 集群的成员会自发的举行“会议”选举新的“领导”主持工作。最典型的案例就是ZooKeeper及Redis、Kafka、ElasticSearch
- 去中心化设计的问题: 去中心化设计里最难解决的一个问题是 脑裂问题 ,最典型的例如ElasticSearch,这种情况的发生概率很低,但影响很大。脑裂问题,这种情况的发生概率很低,但影响很大。脑裂指一个集群犹豫网络的故障,被分为至少两个彼此无法通信的单独集群,此时如果两个集群都各自工作,则可能会产生眼中的数据冲突何错误。一般的设计思路是,当集群半段发声了脑裂问题是,规模较小的集群就“自杀”或者拒绝服务。
目前了解到的去中心化设计的实践好像包括Redis、Kafka、ElasticSearch,可以看的出去中心化似乎更流行一些
分布式系统设计定理
业界主要存在CAP定理和BASE定理
CAP定理
在理论计算机科学中,CAP定理(CAP theorem),又被称作布鲁尔定理(Brewer’s theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点:
- 强一致性(Consistence) : 系统在执行过某项操作后仍然处于一致的状态。在分布式系统中,更新操作执行成功后所有的用户都应该读到最新的值,这样的系统被认为是具有强一致性的。 等同于所有节点访问同一份最新的数据副本
- 可用性(Availability):每一个操作总是能够在一定的时间内返回结果,这里需要注意的是"一定时间内"和"返回结果"。一定时间指的是,在可以容忍的范围内返回结果,结果可以是成功或者失败。 对数据更新具备高可用性
- 分区容错性(Partition tolerance) :理解为在存在网络分区的情况下,仍然可以接受请求(满足一致性和可用性)。这里的网络分区是指由于某种原因,网络被分成若干个孤立的区域,而区域之间互不相通。还有一些人将分区容错性理解为系统对节点动态加入和离开的能力,因为节点的加入和离开可以认为是集群内部的网络分区
不可能同时满足指的是:当发生网络分区故障的时候,如果我们要继续服务,那么强一致性和可用性只能2选1。也就是说当网络分区故障之后P是前提,决定了P之后才有C和A的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。CAP仅适用于原子读写的NOSQL场景中,并不适合数据库系统。关于CAP的证明
BASE理论
BASE 是 Basically Available(基本可用) 、Soft-state(软状态) 和 Eventually Consistent(最终一致性) 三个短语的缩写。
- 最终一致性,最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性
- 基本可用性,基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。
- 响应时间上的损失:正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒
- 系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面
- 软状态,软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时
BASE理论是对CAP中强一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的,它大大降低了我们对系统的要求
架构演变历史
理解架构演变前先理解下什么是分布式和集群。分布式是指通过网络连接的多个组件,通过交换信息协作而形成的系统。而集群,是指同一种组件的多个实例,形成的逻辑上的整体,也就是说,分布式是把多种任务分给多个业务组件,而集群是一堆互联的机器干同一件事,这两个概念其实不冲突,例如我们的系统需要Kafka做消息通信框架任务,Redis做缓存任务,Kafka和Redis就各自是一个分布式的任务组件,但是他们各自又需要多个机器构成集群来提高工作效率。所以以下提到的每一种架构其实都可以以集群的形式呈现
1 单一应用架构
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。
适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。缺点是:性能扩展比较难,协同开发问题,不利于升级维护
2 垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键
通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性,但是缺点就是公用模块无法重复利用,开发资源的浪费,各自重复造轮子。
3 分布式服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的**分布式服务框架(RPC)**是关键
4 流动计算架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心是关键
RPC理论
RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
RPC基本原理
两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数
在上述图中,通过1-10的步骤图解的形式,说明了RPC每一步的调用过程。
客户端发起网络调用
- 客户端想要发起一个远程过程调用,首先通过调用本地客户端Stub程序的方式调用想要使用的功能方法名;
- 客户端Stub程序接收到了客户端的功能调用请求,将客户端请求调用的方法名,携带的参数等信息做序列化操作,并打包成数据包。
网络传递数据
- 客户端Stub查找到远程服务器程序的IP地址,调用Socket通信协议,通过网络发送给服务端。
服务端接收到调用:
- 服务端Stub程序接收到客户端发送的数据包信息,并通过约定好的协议将数据进行反序列化,得到请求的方法名和请求参数等信息。
- 服务端Stub程序准备相关数据,调用本地Server对应的功能方法进行,并传入相应的参数,进行业务处理。
服务端生成调用结果:
- 服务端程序根据已有业务逻辑执行调用过程,待业务执行结束,将执行结果返回给服务端Stub程序。
- 服务端Stub程序将程序调用结果按照约定的协议进行序列化,并通过网络发送回客户端Stub程序。
网络传递数据
- 服务端Stub查找到远程客户端程序的IP地址,调用Socket通信协议,通过网络发送给客户端。
客户端收到响应
- 客户端Stub程序接收到服务端Stub发送的返回数据,对数据进行反序列化操作,并将调用返回的数据传递给客户端请求发起者。
- 客户端请求发起者得到调用结果,整个RPC调用过程结束
从RPC调用中我们发现至少需要生产者(服务提供者)和消费者(服务调用者)两个角色。
从实现技术上分为三层:代理层、序列化层和网络层,对于消费者:
- 代理层:消费者将对应的接口,通过RPC框架的代理来生成一个对象到Spring容器中。代理层将代理接口生成该接口的对象,该对象处理调用时传过来的对象、方法、参数,通过序列化层封装好,调用网络层。
- 序列化层:将请求的参数序列化成报文;将返回的报文反序列化成对象;
- 网络层: 将报文与服务端通信;接收返回结果
对于生产者而言:
- 代理层:一个应用提供服务,必须由一个网络监听的模块,这个模块大多有开源的容器来处理网络上的监听;服务需要注册,只有注册了的服务才可以被调用;注册的服务需要被我们发射调用到,来进行相应的处理。
- 序列化层: 就是相应的做请求的反序列化和结果的序列化。
- 网络层:接收客户端报文;将序列化的结果返回给客户端
所以从上述描述中我们发现,完整的流程其实还需要监听器和注册中心
Dubbo基本架构
依据RPC的基本概念原理和执行过程,我们可以定义成如下4个角色,并且依据分工确认Dubbo的行为规范
Dubbo的四种角色说明如下:
- 服务生产者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
- 监控中心(Monitor):在内存中统计累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
调用关系说明如下:
- 服务容器负责启动,加载,运行服务提供者。服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 监控中心在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
Dubbo一般使用zookeeper作为注册中心,接下来我们就使用SpringBoot集成Dubbo和Zookeeper做一个实践。
SpringBoot整合Dubbo&Zookeeper环境搭建
我们按照如下的步骤进行环境搭建和做必要的准备工作:
1 安装zookeeper
首先我们需要下载一个zookeeper,下载地址为:zookeeper下载地址,这里我们选择现在(20211024)的最新版本3.6.3进行下载。
下载好后为了防止闪退,运行/bin/zkServer.cmd
,初次运行会报闪退,编辑zkServer.cmd
文件末尾添加pause 。这样运行出错就不会退出,会提示错误信息,方便找到原因
初次运行会报错,没有zoo.cfg配置文件,将conf文件夹下面的zoo_sample.cfg复制一份改名为zoo.cfg即可:
zoo.cfg
# The number of milliseconds of each tick tickTime=2000 # The number of ticks that the initial # synchronization phase can take initLimit=10 # The number of ticks that can pass between # sending a request and getting an acknowledgement syncLimit=5 # the directory where the snapshot is stored. # do not use /tmp for storage, /tmp here is just # example sakes. dataDir=/tmp/zookeeper # the port at which the clients will connect clientPort=2181 # the maximum number of client connections. # increase this if you need to handle more clients #maxClientCnxns=60 # # Be sure to read the maintenance section of the # administrator guide before turning on autopurge. # # http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance # # The number of snapshots to retain in dataDir #autopurge.snapRetainCount=3 # Purge task interval in hours # Set to "0" to disable auto purge feature #autopurge.purgeInterval=1 ## Metrics Providers # # https://prometheus.io Metrics Exporter #metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider #metricsProvider.httpPort=7000 #metricsProvider.exportJvmInfo=true
正常运行后Zookeeper启动:
可以做一个简单测试,创建一个KV键值对:
2 安装dubbo-admin
dubbo本身并不是一个服务软件。它其实就是一个jar包,能够帮你的java程序连接到zookeeper,并利用zookeeper消费、提供服务。但是为了让用户更好的管理监控众多的dubbo服务,官方提供了一个可视化的监控程序dubbo-admin,不过这个监控即使不装也不影响使用,我们从这里下载:dubbo-admin下载地址,同样也下载最新版
为了避免端口冲突,修改配置文件如下:
//新增一个端口配置 server.port=7001 admin.registry.address=zookeeper://127.0.0.1:2181 admin.config-center=zookeeper://127.0.0.1:2181 admin.metadata-report.address=zookeeper://127.0.0.1:2181 admin.root.user.name=root admin.root.user.password=root admin.check.sessionTimeoutMilli=3600000 server.compression.enabled=true server.compression.mime-types=text/css,text/javascript,application/javascript server.compression.min-response-size=10240
修改好后按照步骤执行命令