Skip to content

ZK分布式系统

更新: 2025/2/24 字数: 0 字 时长: 0 分钟

分布式系统

**分布式系统(distributed system)是建立在网络之上的软件系统。**正是因为软件的特性,所以分布式系统具有高度的内聚性和透明性。因此,网络和分布式系统之间的区别更多的在于高层软件(特别是操作系统),而不是硬件。

简单的来说,分布式系统是通过软件来连接的一组计算机系统一起工作,在终端用户看来,就像一台计算机在工作一样。这组一起工作的计算机,拥有共享的状态,他们同时运行,独立机器的故障不会影响整个系统的正常运行。

系统扩展

提到分布式系统,我们首先需要明白两个概念:横向扩展、纵向扩展

横向扩展:指通过增加更多的机器来提升整个系统的性能。

纵向扩展:指通过靠升级单台计算机的硬件来提升整个系统的性能。

优劣比较:

  1. 纵向扩展有很强的局限性,即使达到最新硬件的能力以后,还是无法满足中等或者大型工作负载的技术要求。
  2. 横向扩展则没有这个限制,它没有上限,每当性能下降的时候,你就需要增加一台机器,这样理论上讲可以达到无限大的工作负载支持。
  3. 横向扩展的容错性更强,某个节点出现错误以后,并不会导致整个系统的瘫痪,而单机系统出错后,可能会导致整个系统的崩溃。
  4. 横向扩展的延迟性低,在不同的物理位置部署不同的机器,通过就近获取的原则降低访问的延迟时间。
  5. 从价格上来说,横向扩展相比纵向扩展更容易控制。

举例应用

举个例子,传统的数据库是存储在一台机器的文件系统上的。每当我们取出或者插入信息的时候,我们直接和那台机器进行交互。那么现在我们把这个传统的数据库设计成分布式数据库。假设我们使用了三台机器来构建这台分布式数据库,我们追求的结果是,在机器1上插入一条记录,需要在机器1、2、3上都可以返回那条记录。

因此使用分布式系统最大的好处就是能够让你横向的扩展系统。

以上面提到的单一数据库为例,处理更多流量的唯一方式就是升级数据库运行的硬件,这就是纵向扩展。然而纵向扩展的是有局限性的,当到了一定程度以后,即使最好的硬件,也不能够满足当前流量的需求。如果使用分布式来横向扩展系统,可以让整个系统的容错性更强,且延迟更低。

设计推演

假设一个场景,有一个网络应用变得越来越流行,服务的人数也越来越多,应用程序每秒收到的请求远远超过能够正常处理的数量,导致应用程序性能下降明显。那我们下面就来扩展一下应用程序来满足更高的要求:

一般来说,读取信息的频率要远远超过插入或修改信息的频率。

主从复制策略来实现扩展系统:我们可以创建两个新的数据库服务器,他们与主服务器同步。用户业务对这两个新的数据库只能读取。每次当向主数据库插入和修改信息时,都会异步的通知副本数据库进行更新变化。这让我们有了三倍于原来系统读取数据的性能支持,但是这里有一个问题,在数据库事务的设计当中遵循ACID原则,在主数据库对其他两个数据库进行数据更新的时候,我们有一个时间窗口失去了一致性原则。如果在这个时间窗口内对两个新的数据库进行查询,可能查不到数据。且在这个时候由于同步这三个数据库的数据,会影响到写操作的性能,这是我们在设计分布式系统的时候,不得不承受的一些代价

上面的主从复制策略解决了用户读取性能方面的需求,但是当数据量达到一定程度,一台机子上无法存放的时候,我们可以**使用分区技术扩展写操作性能。分区技术是指根据特定的算法,比如用户名a到z作为不同的分区,分别指向不同的数据库写入,每个写入数据库会有若干读取的从数据库进行同步提升读取性能。**当然,这样使得整套系统变得更加复杂,最重要的难点是分区算法。试想一下,如果c开头的用户名比其他开头的用户名要多很多,这会导致c区的数据量非常庞大,相应地,对于c区的请求也会远远大于其他区。此时c区成为热点。要避免热点,需要对c区进行拆分。此时要进行共享数据就会变得非常昂贵,甚至可能导致停机

如果一切都很理想,那我们就拥有了n倍的写入流量,n是分区的数目。当然这里也存在一个陷阱,我们进行数据分区以后,导致除了分区键以外的查询都变得非常低效,尤其是对于sql语句如join查询就变得非常之糟糕,导致一些复杂的查询根本无法使用。

CAP定理

CAP定理是指一个分布式系统不能同时具有一致性,可用性和分区容忍性

  • 一致性(Consistency):即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。

  • 可用性(Availability):服务一直可用,而且是正常响应时间。

  • 分区容忍(Partition tolerant):即当某个节点或网络分区故障时,仍然能够对外提供满足一致性或可用性的服务。简单说,就是避免单点故障,就要进行冗余部署,冗余部署相当于是服务的分区,这样的分区就具备了容错性。

对于任何分布式系统来说,分区容忍是一个必要条件,如果没有这一点,就不可能做到一致性和可用性。试想如果两个节点链接断掉了,他们如何能够做到既可用又一致?最后你只能选择在网络分区情况下,你的系统要么强一致,要么高可用

QQ截图20220121105353

BASE理论

BASE理论是对CAP理论的延伸,核心思想就是即使无法做到强一致性(CAP的一致性就是强一致性),但应用可以采用合适的方式达到最终一致性。

  • 基本可用:指分布式系统在出现故障的时候,允许损失部分可用性,只保证核心可用。例如,电商大促时,为了应对访问量激增,部分用户可能被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。
  • 软状态:指允许系统存在中间状态,而中间状态不会影响系统整体可用性。
  • 最终一致性:指系统中所有数据副本经过一定时间后,最终能够达到一致的状态。

这些系统提供了BASE属性,这是相对于传统数据库的ACID来讲的。 也就是(Basically available)基本上是可用的,系统总会返回一个响应。(Soft state)软状态, 系统可以随着时间的推移而变化,甚至在没有输入的情况下也可以变化, 如保持最终的一致性的同步。(Eventual consistency)最终的一致性, 在没有输入的情况下,数据迟早会传播到每一个节点上,从而变得一致。

追求高可用的分布式数据库例子有Cassandra;看重强一致性的数据库有HBase,Redis;而zookeeper在数据同步时,最求的并不是强一致性,而是顺序一致性(事务id的单调递增)。

常用架构类型

客户端服务器:在这个类型中,分布式系统架构有一个服务器作为共享资源。比如打印机或者网络服务器,它有多个客户机,这些客户机决定何时使用共享资源,如何使用和显示改变数据,并将其送回服务器,像git这样的代码仓,这是一个很好的例子。

三层架构:这种架构把系统分为表现层,逻辑层和数据层,这简化了应用程序的部署,大部分早期的网络应用都是三层的

多层架构:上面的三层架构是多层架构的一种特殊形式。一般会把上面的三层进行更详细的划分,比如说以业务的形式进行分层

点对点架构:在这种架构中,没有专门的机器提供服务或管理网络资源。而是**将责任统一分配给所有的机器,成为对等机,对等机既可以作为客户机,也可以作为服务器。**这种架构的例子,包括bittorrent和区块链。

以数据库为中心:这种架构是指用一个共享的数据库,使分布式的各个节点在不需要任何形式直接通信的情况下,进行协同工作的架构。

分布式优缺点

分布式系统优点如下:

  1. 分布式系统中的所有节点都是相互连接的,所以节点之间可以很容易地共享数据。

  2. 更多的节点可以很容易地添加到分布式系统中,即可以根据需要进行扩展。

  3. 一个节点的故障不会导致整个分布式系统的失败,其他节点仍然可以相互通信。

  4. 硬件资源可以与多个节点共享,而不是只限于一个节点。

分布式系统缺点如下:

  1. 在分布式系统中很难提供足够的安全,因为节点以及连接都需要安全。

  2. 一些消息和数据在从一个节点转移到另一个节点时,可能会在网络中丢失

  3. 与单用户系统相比,连接到分布式系统的数据库是相当复杂和难以处理的

  4. 如果分布式系统的所有节点都试图同时发送数据,网络中可能会出现过载现象

小结

最后谈一下分布式系统与集群的关联,因为分布式系统是通过多个节点的集群来完成一个任务,让外界看起来是跟一套系统作为一个整体打交道。

一套分布式系统可以有多个集群,这些集群可以业务进行划分,也可以物理区域进行划分。每一个集群可以作为这个分布式系统的一个节点。这些集群节点组成的分布式系统,又可以作为单个的节点与其他的节点组成一个集群。

Zookeeper简介

ZooKeeper 是 Apache 的一个顶级项目,为分布式应用提供高效、高可用的分布式协调服务,提供了诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知和分布式锁等分布式基础服务。ZooKeeper的优点就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户,由于其便捷的使用方式、卓越的性能和良好的稳定性,被广泛地应用于诸如 Hadoop、HBase、Kafka 和 Dubbo 等大型分布式系统中。

?> 为了简化表达,后期常用ZK代表zookeeper。

运行模式

Zookeeper 有三种运行模式:单机模式、伪集群模式和集群模式。

**单机模式:**这种模式一般适用于开发测试环境,一方面我们没有那么多机器资源,另外就是平时的开发调试并不需要极好的稳定性。

**集群模式:**一个 ZooKeeper 集群通常由一组机器组成,一般3台以上就可以组成一个可用的 ZooKeeper 集群了。组成 ZooKeeper 集群的每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都会互相保持通信。

**伪集群模式:**这是一种特殊的集群模式,即集群的所有服务器都部署在一台机器上。当你手头上有一台比较好的机器,如果作为单机模式进行部署,就会浪费资源,这种情况下,ZooKeeper 允许你在一台机器上通过启动不同的端口来启动多个 ZooKeeper 服务实例,以此来以集群的特性来对外服务。

数据持久化

Zookeeper的数据是运行在内存当中的,zk提供了两种持久化机制:

  • 数据快照:zk会在一定时间间隔内做一次内存数据的快照,把该时刻的内存数据保存在快照文件中。

  • 事务日志:zk把执行的命令以日志的形式保存在dataLogDir指定的路径文件中(没有指定dataLogDir,就按dataDir指定路径)。

?> 提示:如果Zookeeper发生错误,将快照文件中的数据恢复到内存中,再用日志文件数据做增量恢复,这样速度最快。

!> 注意:Zookeeper虽然可以保存数据,但不能把它当作数据库来使用。

集群搭建

这里讲解如何搭建Zookeeper集群,为了方便演示,这里选择搭建伪集群模式,另外的模式搭建都大同小异。

下载安装包

先准备安装包,这里我推荐在Apache官网下载(地址:https://zookeeper.apache.org/releases.html)。选择要安装的zookeeper版本:

QQ截图20220105145131

点击进行下载:

QQ截图20220105145328

配置文件内容

讲安装包解压到自己指定的目录,进入zookeeper的conf目录:

QQ截图20220105145832

打开 zoo_sample.cfg 文件(模板配置文件),里面的配置内容有许多是 # 开头注释的省略不看,最终只有下面5行配置生效,其作用如下:

# 2000毫秒,这个时间是作为Zookeeper服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个时间间隔就会发送一个心跳。
tickTime=2000
# 集群中的follower服务器(F)与leader服务器(L)之间初始连接时能容忍的最多心跳数(tickTime的数量)。
initLimit=10
# 集群中的follower服务器(F)与leader服务器(L)之间 请求和应答 之间能容忍的最多心跳数(tickTime的数量)。
syncLimit=5
# Zookeeper保存数据的默认目录,包括日志文件默认也保存在这个目录里。
dataDir=/tmp/zookeeper
# 连接Zookeeper服务器的端口,Zookeeper会监听这个端口接受客户端的访问请求。
clientPort=2181

zoo_sample.cfg 文件,复制三份,重命名为 zoo_1.cfgzoo_2.cfgzoo_3.cfg 文件:

QQ截图20220105150236

zoo_1.cfg 文件内容添加修改如下:

tickTime=2000
initLimit=10
syncLimit=5
# 1号服务存放数据路径
dataDir=D:/chenzhuo/apache-zookeeper-3.7.0-bin/data/1
# 1号服务通信连接端口
clientPort=2181
# 服务器列表
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890

zoo_2.cfg 文件内容添加修改如下:

tickTime=2000
initLimit=10
syncLimit=5
# 2号服务存放数据路径
dataDir=D:/chenzhuo/apache-zookeeper-3.7.0-bin/data/2
# 2号服务通信连接端口
clientPort=2182
# 服务器列表
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890

zoo_3.cfg 文件内容添加修改如下:

tickTime=2000
initLimit=10
syncLimit=5
# 3号服务存放数据路径
dataDir=D:/chenzhuo/apache-zookeeper-3.7.0-bin/data/3
# 3号服务通信连接端口
clientPort=2183
# 服务器列表
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890

这里说明一下服务列表配置,共有3个server说明共有3个服务器,分别是server.1、server.2、server.3,每个服务器都有可能是领导者或跟随者。每个服务器开启两个端口,第一列端口2888、2889、2890是集群之间通信用和数据同步用的,第二列端口3888、3889、3890是用来进行选举投票用的。

?> 如果再增加一台服务器指定为观察者(observer),服务器列表就添加一行 server.4=127.0.0.1:2891:3891:observer

?> 因为搭建是伪集群模式,即一台机器开多个服务来模拟集群,因此每个服务的配置文件的数据存放路径和所占通信端口都不一样。如果是多台机器,则 zoo.cfg 配置文件内容是一样的。

建立存放目录

根据存放数据的路径,新建data文件夹,在data里面新建名称为 123 的文件夹:

QQ截图20220105153329

分别在 123 文件夹里面新建一个名称为 myid 文件(标识文件),其内容分别为 123

QQ截图20220105154214

?> myid的号码必须一一对应配置内容中的“服务列表”,一个myid号码对应一个服务server后的编号。

!> 不管有多少机器或多少服务,每个服务的myid都是不一样的,一样的myid在集群内会产生冲突。

启动集群

进入bin目录,用编辑文件打开 zkEnv.cmd 文件,当中有一行内容如下,这个就是读取服务配置文件的内容:

set ZOOCFG=%ZOOCFGDIR%\zoo.cfg

QQ截图20220105164801

我们将 zkEnv.cmd 文件内容修改如下并保存,点击运行 zkServer.cmd 文件,启动第一个服务:

set ZOOCFG=%ZOOCFGDIR%\zoo_1.cfg

QQ截图20220105165358

需要注意的是,在上面的配置文件里面说明了共有3个服务,当启动第一个服务时会一直报错,因为运行服务不到整个服务数量的一半,当启动第二个服务后,第一个服务才不会报错。以此类推,如果共有9个服务,那么需要启动5个服务才不会报错。

接下来,继续修改 zkEnv.cmd 文件内容修改如下并保存,点击运行 zkServer.cmd 文件,启动第二个服务:

set ZOOCFG=%ZOOCFGDIR%\zoo_2.cfg

接下来,继续修改 zkEnv.cmd 文件内容修改如下并保存,点击运行 zkServer.cmd 文件,启动第三个服务:

set ZOOCFG=%ZOOCFGDIR%\zoo_3.cfg

到此,一个zookeeper伪集群模式就搭建起来了,根据 myid 来从上到下分别是第一个、第二个、第三个服务:

QQ截图20220105170628

在Linux系统中常用的集群命令如下:

首先还是要将con中的zoo_sample.cfg重命名为zoo.cfg
启动zk服务器:./bin/zkServer.sh start ../conf/zoo.cfg
查看zk服务器:./bin/zkServer.sh status ../conf/zoo.cfg
停止zk服务器:./bin/zkServer.sh stop ../conf/zoo.cfg

测试连接

双击运行 zkCli.cmd 测试与集群的连接,出现如图欢迎字样则连接成功!

QQ截图20220105172054

在Linux系统中连接集群命令如下:

连接到zk服务器:./bin/zkCli.sh -server server1的IP:server1通信端口,server2的IP:server2通信端口,server3的IP:server3通信端口

应用场景

ZooKeeper 是一个高可用的分布式数据管理与系统协调框架。基于对 Paxos 算法的实现,使该框架保证了分布式环境中数据的强一致性,也正是基于这样的特性,使得 ZooKeeper 解决很多分布式问题。

值得注意的是,ZooKeeper 并非天生就是为这些应用场景设计的,都是后来众多开发者根据其框架的特性,利用其提供的一系列 API 接口(或者称为原语集),摸索出来的典型使用方法。

分布式协调组件

有A、B两台服务器里面的 flag 属性都为 True,用户进入到A服务器修改 flag 属性为 Flase,这个时候zookeeper分布式协调组件就会监听到A服务器属性发生了变化,就发送消息给B服务器要求改变其 flag 属性为 Flase,这样就保证了数据的强一致性。

分布式锁

分布式锁主要得益于 ZooKeeper 为我们保证了数据的强一致性。锁服务可以分为两类:一类是保持独占,另一类是控制时序。

所谓保持独占,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。通常的做法是把 ZooKeeper 上的一个 Znode 看作是一把锁,通过 create znode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。

控制时序,就是所有视图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。做法和上面基本类似,只是这里 /distribute_lock 已经预先存在,客户端在它下面创建临时有序节点(这个可以通过节点的属性控制:CreateMode.EPHEMERAL_SEQUENTIAL 来指定)。ZooKeeper 的父节点(/distribute_lock)维持一份 sequence,保证子节点创建的时序性,从而也形成了每个客户端的全局时序。

1.由于同一节点下子节点名称不能相同,所以只要在某个节点下创建 Znode,创建成功即表明加锁成功。注册监听器监听此 Znode,只要删除此 Znode 就通知其他客户端来加锁。

2.创建临时顺序节点:在某个节点下创建节点,来一个请求则创建一个节点,由于是顺序的,所以序号最小的获得锁,当释放锁时,通知下一序号获得锁。

分布式队列

队列方面,简单来说有两种:一种是常规的先进先出队列,另一种是等队列的队员聚齐以后才按照顺序执行。对于第一种的队列和上面讲的分布式锁服务中控制时序的场景基本原理一致,这里就不赘述了。

第二种队列其实是在 FIFO 队列的基础上作了一个增强。通常可以在 /queue 这个 Znode 下预先建立一个 /queue/num 节点,并且赋值为 n(或者直接给 /queue 赋值 n)表示队列大小。之后每次有队列成员加入后,就判断下是否已经到达队列大小,决定是否可以开始执行了。

这种用法的典型场景是:分布式环境中,一个大任务 Task A,需要在很多子任务完成(或条件就绪)情况下才能进行。这个时候,凡是其中一个子任务完成(就绪),那么就去 /taskList 下建立自己的临时时序节点(CreateMode.EPHEMERAL_SEQUENTIAL)。当 /taskList 发现自己下面的子节点满足指定个数,就可以进行下一步按序进行处理了。

负载均衡

这里说的负载均衡是指软负载均衡。在分布式环境中,为了保证高可用性,通常同一个应用或同一个服务的提供方都会部署多份,达到对等服务。而消费者就须要在这些对等的服务器中选择一个来执行相关的业务逻辑,其中比较典型的是消息中间件中的生产者,消费者负载均衡。