🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# Zookeeper 概述: 1. ZooKeeper 是 Apache 软件基金会的一个软件项目,它为大型分布式计算提供开源的`分布式配置服务`、同步服务和命名注册。其架构通过冗余服务实现高可用性。 2. Zookeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。 3. Zookeeper的数据保存在内存中,性能高,可以实现高吞吐量和低延迟量。 4. ZooKeeper可以用于解决分布式数据一致性的问题,常用在数据发布/订阅、负载均衡、命名服务、集群管理、Master选举、`分布式锁`、`分布式配置服务`和分布式队列等功能中。 **总之:Zookeeper提供分布式协调服务。** &nbsp; ## 理论补充 **CAP理论** CAP 理论指出对于一个分布式计算系统来说,不可能同时满足以下三点: * **一致性**:Consistency 在分布式环境中,一致性是指数据在多个副本之间能够保持一致的特性,等同于所有节点访问`同一份最新的`数据副本。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处于一致的状态。 * **可用性**:Availability 每次请求都能获取到正确的响应,但是不保证获取的数据为最新数据。 * **分区容错性**:Partition Tolerance 分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。 一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。 在这三个基本需求中,最多只能同时满足其中的两项,P 是必须的,因此只能在 CP 和 AP 中选择,`zookeeper 保证的是 CP`,更准确讲ZooKeeper会实现最终的一致性,但是并不能保证强一致性;对比 spring cloud 系统中的注册中心 `eruka 实现的是 AP`。 ![img](https://www.runoob.com/wp-content/uploads/2020/09/cap-theorem-diagram.png) &nbsp; **BASE理论** BASE 是 Basically Available(基本可用)、Soft-state(软状态) 和 Eventually Consistent(最终一致性) 三个短语的缩写。 * **基本可用**:在分布式系统出现故障,允许损失部分可用性(服务降级、页面降级)。 * **软状态**:允许分布式系统出现中间状态。而且中间状态不影响系统的可用性。这里的中间状态是指不同的 data replication(数据备份节点)之间的数据更新可以出现延时的最终一致性。 * **最终一致性**:data replications 经过一段时间达到一致性。 BASE 理论是对 CAP 中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:`我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。`ZooKeeper可以实现系统的最终一致性。 &nbsp; ## 安装 ## 安装 1. Linux的话去[官网](https://zookeeper.apache.org/releases.html)找到最新的稳定版本的下载路径 :-: ![](https://img.kancloud.cn/67/6f/676f462f7fe264a4ddd015523f4dbacc_1081x666.png) 注意下载下来的要带有bin命名的才是打包好的。 2. Linux中创建文件夹下载安装包 ~~~  wget https://dlcdn.apache.org/zookeeper/zookeeper-3.6.3/apache-zookeeper-3.6.3-bin.tar.gz ~~~ 在下载目录中解压 ~~~  tar -zxvf apache-zookeeper-3.6.3-bin.tar.gz ~~~ 3. 复制配置文件,在安装目录的conf目录下 ~~~  cp zoo_camp.cfg zoo.cfg ~~~ zoo.cfg是zookeeper的默认启动配置文件 3. 配置bin环境 ~~~  vim /etc/profile  ​  # 添加上如下内容  export ZOOKEEPER_HOME=/下载目录/apache-zookeeper-3.6.3-bin  export PATH=$ZOOKEEPER_HOME/bin:$PATH    # 使其生效  source /etc/profile ~~~ 最好是保证JDK在profile中配置了! 4. 打开Zookeeper服务 :-: ![](https://img.kancloud.cn/70/ea/70ea1bbddbf9f845e864acd19f6b83d8_771x89.png) 如果不能正确启动成功,有很大的可能是JDK的问题,注意查看JDK是否已经配置了环境变量了。 &nbsp; ### 集群搭建 ZooKeeper一般都会搭建集群来使用,当然使用单机也行,但是这样就不能很好的发挥其高可用的特性了。ZooKeeper需要至少3台服务器形成一个集群。 1. 准备至少三台服务器(由于ZooKeeper的Leader选举至少要过半才能通过,因此ZooKeeper的集群至少要三台服务器才行)。 2. 在各个服务器中安装相同的ZooKeeper版本。并修改`/etc/profile`,将ZooKeeper的相关命令添加到环境变量中。同时要确保服务器中至少安装了JDK1.8+。 :-: ![](https://img.kancloud.cn/2a/d8/2ad86261fe1dbef35620c9b818cb43bb_510x65.png) 3. 修改每台服务器的ZooKeeper的配置文件,在配置文件的末尾添加上类似如下内容: :-: ![](https://img.kancloud.cn/47/a4/47a43962ef301ca7a83ded18516cb262_459x117.png) 及其书写格式为: ~~~  server.myid=ip:2888:3888 ~~~ 2888集群内各台机器之间通信使用,3888用于投票选举时使用。 4. 每台服务器创建myid文件,需要创建在zoo.cfg配置文件`dataDir`目录下的;假如这里dataDir=/var/zookeeper,则可以通过如下命令创建myid文件,设置每台服务器的myid ~~~  echo 1 > /tmp/zookeeper/myid ~~~ '1'表示的就是该服务器的myid,需要和配置文件中server.myid配置的相对应。 5. 启动各台服务器的zkServer.sh。 ~~~ # 前台启动 zkServer.sh start-foreground # 后台启动 zkServer.sh start ~~~ 可以使用命令`zkServer.sh status`查看哪台服务器是Leader节点。 &nbsp; ## 相关元素 1. 架构 ![ZooKeeper的架构](https://atts.w3cschool.cn/attachments/day_161229/201612291344222238.jpg) * Client 客户端,分布式应用集群中的一个节点,从ZooKeeper服务器访问信息。客户端和ZooKeeper服务器之间会维持心跳连接。 * Server 服务器,ZooKeeper集群中的一个节点,为客户端提供所有的服务。在客户端连接的时候,客户端发送确认码以告知服务器是活跃的。 * Ensemble ZooKeeper服务器组。形成ensemble所需的最小节点数为3。 * Leader Zookeeper是主从集群,和Reids类似。Leader表示中心节点,在集群启动的时候被选举。如果集群中Leader服务宕机了,则执行自动选举过程。注意增删改只能发送在Leader中,查询可以发生在其他节点中。 当Leader 节点宕机的时候,官方的压测给出了大约在200ms左右可以选举一个新的节点作为leader节点。即从不可用状态恢复到可用状态大约需要200ms。 * Follower ZooKeeper集群的节点之一,属于Leader节点的从节点,可以给客户端提供查询功能;每个Follower节点在Leader宕机的时候都有机会被选择成新的Leader的。 &nbsp; 2. 层次命名空间 下图描述了用于内存表示的ZooKeeper文件系统的`树结构`。ZooKeeper节点称为 **znode** 。每个znode由一个名称标识,并用路径(/)序列分隔。 * 在图中,首先有一个由“/”分隔的znode。在根目录下,两个逻辑命名空间 **config** 和 **workers** 。 * **config** 命名空间用于集中式配置管理,**workers** 命名空间用于命名。 * 在 **config** 命名空间下,每个znode最多可存储1MB的数据。这种结构的主要目的是存储同步数据并描述znode的元数据。此结构称为 **ZooKeeper数据模型**。 ![分层命名空间](https://atts.w3cschool.cn/attachments/day_161229/201612291345162031.jpg) Znode兼具文件和目录两种特点。既像文件一样维护着*数据长度、元信息、ACL、时间戳*等数据结构,又像目录一样可以作为路径标识的一部分。每个Znode由三个部分组成: * **stat**:此为状态信息,描述该Znode版本、权限等信息。 * **data**:与该Znode关联的数据。 * c**hildren**:该Znode下的节点。 其他信息: * **版本号** - 每个znode都有版本号,这意味着每当与znode相关联的数据发生变化时,其对应的版本号也会增加。当多个zookeeper客户端尝试在同一znode上执行操作时,版本号的使用就很重要。 * **操作控制列表(ACL)** - ACL基本上是访问znode的认证机制。管理所有znode读取和写入操作。 * **时间戳** - 时间戳表示创建和修改znode所经过的时间。通常以毫秒为单位。ZooKeeper从“事务ID"(zxid)标识znode的每个更改。**Zxid** 是唯一的,并且为每个事务保留时间,可以轻松地确定从一个请求到另一个请求所经过的时间。 * **数据长度** - 存储在znode中的数据总量是数据长度。最多可以存储1MB的数据。 &nbsp; 3. Znode的类型 Znode分为持久(persistent)节点,顺序(sequential)节点和临时(ephemeral)节点。 * **持久节点** 即使在创建该特定znode的客户端断开连接后,持久节点仍然存在。默认情况下,除非另有说明,否则所有znode都是持久的。大小为1MB。 * **临时节点** 客户端活跃时,临时节点就是有效的。当客户端与ZooKeeper集群的连接断开时,临时节点会自动删除。因此,只有临时节点不允许有子节点。如果临时节点被删除,则下一个合适的节点将填充其位置。临时节点在leader选举中起着重要作用。也可以用于Redis中的分布式锁。 * **顺序节点** 顺序节点可以是持久的或临时的。当一个新的znode被创建为一个顺序节点时,ZooKeeper通过将10位的序列号附加到原始名称来设置znode的路径。 例如,如果将具有路径 **/myapp** 的znode创建为顺序节点,则ZooKeeper会将路径更改为 **/myapp0000000001** ,并将下一个序列号设置为0000000002。如果两个顺序节点是同时创建的,那么ZooKeeper不会对每个znode使用相同的数字。顺序节点在锁定和同步中起重要作用。 ![](https://img.kancloud.cn/8e/e3/8ee38623b75d56b7d9ef346613161d9a_698x271.png) 4. Sessions(会话) 会话对于ZooKeeper的操作非常重要。会话中的请求按FIFO顺序执行。一旦客户端连接到服务器,将建立会话并向客户端分配**会话ID** 。 客户端以特定的时间间隔发送**心跳**以保持会话有效。如果ZooKeeper集合在超过服务器开启时指定的期间(会话超时)都没有从客户端接收到心跳,则它会判定客户端死机。 会话超时通常以毫秒为单位。当会话由于任何原因结束时,在该会话期间创建的临时节点也会被删除。 5. Watches(监视) 监视是一种简单的机制,使客户端收到关于ZooKeeper中znode的更改的通知。客户端可以在读取特定znode时设置Watches。Watches会向注册的客户端发送任何znode(客户端注册表)更改的通知。 Znode更改是与znode相关的数据的修改或znode的子项中的更改。只触发一次watches。如果客户端想要再次通知,则必须通过另一个读取操作来完成。当连接会话过期时,客户端将与服务器断开连接,相关的watches也将被删除。 &nbsp; ## 客户端使用 ### 常用命令 1. 查看目录下的zNode ~~~  ls [-s] [-w] [-R] path ~~~ 2. 创建节点 ~~~  create [-s] [-e] path data  ​  # 例如:创建aabb节点  create /aabb "hello world" ~~~ 选项说明: * \-s:创建序列化节点,节点名称相同时数据不会覆盖,会自动添加个版本号。可以使每个客户端都有自己的一个同名节点。序列化数字由leader维护,会一直递增。可以用于并发的场景中。 例如: ~~~  [zk: localhost:2181(CONNECTED) 9] create /aabb -s "01"  Created /aabb0000000001  [zk: localhost:2181(CONNECTED) 10] create /aabb -s "02"  Created /aabb0000000002  [zk: localhost:2181(CONNECTED) 11] ls /  [aabb0000000001, aabb0000000002, test, zookeeper] ~~~ * \-e:创建临时节点。 3. 获取节点的数据 ~~~  get [-s] [-w] path ~~~ 选项说明: * \-s:获取节点的详细数据,包括元数据 ~~~  [zk: localhost:2181(CONNECTED) 15] get -s /test  hello world  # data内容  cZxid = 0x2  ctime = Mon Jan 17 11:05:13 HKT 2022  mZxid = 0x2  mtime = Mon Jan 17 11:05:13 HKT 2022  pZxid = 0x2  cversion = 0  dataVersion = 0  aclVersion = 0  ephemeralOwner = 0x0  dataLength = 11  numChildren = 0 ~~~ cZxid:创建事务id,64位,由Leader节点维护。 mZxid:修改的事务id。 pZxid:最后一个节点创建的事务id。 ephemeralOwner:session id,用于临时节点。创建节点的时候可以使用-e选项。 * \-w:只获取data的内容。 &nbsp; ### API使用 1. 导入依赖 ~~~  <dependency>      <groupId>org.apache.zookeeper</groupId>      <artifactId>zookeeper</artifactId>      <version>3.6.3</version>  </dependency> ~~~ 注意要和集群的版本一致。 2. 创建ZooKeeper对象 ~~~  final CountDownLatch countDownLatch = new CountDownLatch(1);  ZooKeeper zk = new ZooKeeper("103.118.42.131:2181", 3000, watchedEvent -> {      System.out.println("new zk watch:" + watchedEvent);      Watcher.Event.KeeperState state = watchedEvent.getState();      Watcher.Event.EventType type = watchedEvent.getType();      String path = watchedEvent.getPath();      System.out.println("path:" + path);      // 连接状态的监控      switch (state) {          case Unknown:          case NoSyncConnected:              break;          case Disconnected:              System.out.println("disconnected...");              break;          case SyncConnected:              System.out.println("SyncConnected...");              countDownLatch.countDown();              break;          case AuthFailed:              break;          case ConnectedReadOnly:              break;          case SaslAuthenticated:              break;          case Expired:              break;          case Closed:              System.out.println("closed...");              break;          default:              break;     }  ​      // 节点的相关状态      switch (type) {          case None:              break;          case NodeCreated:              System.out.println("NodeCreated...");              break;          case NodeDeleted:              System.out.println("NodeDeleted...");              break;          case NodeDataChanged:              System.out.println("NodeDataChanged...");              break;          case NodeChildrenChanged:              System.out.println("NodeChildrenChanged...");              break;          case DataWatchRemoved:              System.out.println("DataWatchRemoved...");              break;          case ChildWatchRemoved:              System.out.println("ChildWatchRemoved...");              break;          case PersistentWatchRemoved:              System.out.println("PersistentWatchRemoved...");              break;          default:              break;     }  });  countDownLatch.await(); ~~~ 由于是异步连接的,所以要用CountDownLatch进行等待。 2. 创建节点 ~~~  String path = zk.create("/test1", "hello world".getBytes(StandardCharsets.UTF_8), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); ~~~ 3. 设置数据 ~~~  Stat stat = zk.setData(path, "hello test".getBytes(StandardCharsets.UTF_8), -1); ~~~ \-1表示自动匹配数据的版本,否则要和集群中的znode节点一致,不然的话会报错。 4. 获取数据,同时设置watch ~~~  byte[] data = zk.getData("/test", new Watcher() {      @Override      public void process(WatchedEvent watchedEvent) {          System.out.println("getData watch:" + watchedEvent);          try {              //可以重新注册回调              zk.getData("/test", this, new Stat());         } catch (KeeperException e) {              e.printStackTrace();         } catch (InterruptedException e) {              e.printStackTrace();         }     }  }, new Stat()); ~~~ 在getData中设置的Watch会在给znode设置数据或者删除节点的时候触发,注意一个watch只能触发一次,所以一般都会重复设置。 异步回调方式: ~~~  zk.getData(path, false, new AsyncCallback.DataCallback() {      @Override      public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {          System.out.println("rc:" + rc);          System.out.println("-------async call back----------");          System.out.println(ctx.toString());          System.out.println(new String(data));          countDownLatch1.countDown();     }  }, "abc"); ~~~ 这种方式会在读取到数据的时候触发。