分布式专题——15 ZooKeeper特性与节点数据类型详解

1 简介

  • ZooKeeper 是 Apache Hadoop 的子项目,作为开源分布式协调框架 ,旨在解决分布式集群应用系统的一致性问题。它把复杂易错的分布式一致性服务封装成高效可靠的原语集,通过简单易用接口供用户使用;

  • 官网:Apache ZooKeeper

  • 从本质看,ZooKeeper 是文件系统 + 监听机制 的分布式小文件存储系统,以类似文件系统目录树的方式存储数据,能有效管理树中节点,维护和监控数据状态变化,进而实现基于数据的集群管理、统一命名服务、分布式配置管理、分布式消息队列、分布式锁、分布式协调等功能;

  • 从设计模式角度,ZooKeeper 是一个基于观察者模式设计的分布式服务管理框架,存储管理大家关心的数据。ZooKeeper 接受观察者的注册,当数据状态变化时,会通知已注册的观察者做出反应;

  • 在实际应用流程中,ZooKeeper 集群的作用:

    • 服务启动时(如 Server1、Server2、Server3),会注册信息(创建临时节点)到集群;
    • Client(如 Client1、Client2、Client3)获取当前在线 server 列表并注册监听;
    • 当服务节点下线(比如某一 server 出现故障等情况),ZooKeeper 集群会向相关 Client 发送 server 节点下线通知;
    • 之后 Client 会重新获取 server 列表并注册监听,以保证能获取最新的服务节点信息;

2 快速入门

2.1 ZooKeeper 安装

  • 下载地址:Apache ZooKeeper

  • 解压安装包后进入conf目录,复制zoo_sample.cfg,修改文件名为zoo.cfg

    cmd 复制代码
    cp zoo_sample.cfg zoo.cfg
  • 修改zoo.cfg配置文件,将dataDir=/tmp/zookeeper修改为指定的 data 目录:

    properties 复制代码
    # zookeeper时间配置中的基本单位(毫秒)
    tickTime=2000
    # 允许follower初始化连接到leader最大时长,它表示tickTime时间倍数 即:initLimit*tickTime
    initLimit=10
    # 允许follower与leader数据同步最大时长,它表示tickTime时间倍数
    syncLimit=5
    # zookeeper 数据存储目录及日志保存目录(如果没有指明dataLogDir,则日志也保存在这个文件中)
    dataDir=/tmp/zookeeper
    # 对客户端提供的端口号
    clientPort=2181
    # 单个客户端与zookeeper最大并发连接数
    maxClientCnxns=60
    # 保存的快照数量,之外的将会被清除
    autopurge.snapRetainCount=3
    # 自动触发清除任务时间间隔,小时为单位。默认为0,表示不自动清除。
    autopurge.purgeInterval=1
  • 启动 zookeeper server:

    cmd 复制代码
    # 可以通过 bin/zkServer.sh 来查看都支持哪些参数 
    # 默认加载配置路径conf/zoo.cfg
    bin/zkServer.sh start
    bin/zkServer.sh start conf/my_zoo.cfg
    
    # 查看zookeeper状态
    bin/zkServer.sh status
  • 启动 zookeeper client 连接 zookeeper server:

    cmd 复制代码
    bin/zkCli.sh
    # 连接远程的zookeeper server
    bin/zkCli.sh -server ip:port

2.2 客户命令行操作

  • 通过命令help查看 zookeeper 支持的所有命令:

  • 常见 cli 命令:ZooKeeper: Because Coordinating Distributed Systems is a Zoo

    命令基本语法 功能描述
    help 显示所有操作命令
    ls -s -w -R path 使用 ls 命令来查看当前 znode 的子节点可监听 -w:监听子节点变化 -s:节点状态信息(时间戳、版本号、数据大小等) -R:表示递归的获取
    create -s -e -c -t ttl path data acl 创建节点 -s:创建有序节点 -e:创建临时节点 -c:创建一个容器节点 ttl:创建一个 TTL 节点,-t 时间(单位毫秒) data:节点的数据,可选,如果不使用时,节点数据就为 null acl:访问控制
    get -s -w path 获取节点数据信息 -s:节点状态信息(时间戳、版本号、数据大小等) -w:监听节点变化
    set -s -v version path data 设置节点数据 -s:表示节点为顺序节点 -v:指定版本号
    getAcl -s path 获取节点的访问控制信息 -s:节点状态信息(时间戳、版本号、数据大小等)
    setAcl -s -v version -R path acl 设置节点的访问控制列表 -s:节点状态信息(时间戳、版本号、数据大小等) -v:指定版本号 -R:递归的设置
    stat -w path 查看节点状态信息
    delete -v version path 删除某一节点,只能删除无子节点的节点 -v:表示节点版本号
    deleteall path 递归的删除某一节点及其子节点
    setquota -n|-b val path 对节点增加限制 n:表示子节点的最大个数 b:数据值的最大长度,-1表示无限制

2.3 GUI 工具

3 ZooKeeper 数据结构

3.1 整体概述

  • ZooKeeper 数据模型结构类似 Unix 文件系统,整体可看作一棵树,每个节点称为 ZNode;

  • 其数据模型是层次模型,层次模型常见于文件系统,层次模型和 key-value 模型是两种主流数据模型。ZooKeeper 采用文件系统模型主要基于两点考虑:

    • 一是文件系统的树形结构便于表达数据之间的层次关系
    • 二是便于为不同应用分配独立的命名空间(namespace)
  • ZooKeeper 的层次模型称作 Data Tree ,Data Tree 的每个节点叫 ZNode。和文件系统不同,每个 ZNode 都能保存数据,默认每个 ZNode 可存储 1MB 数据,且每个 ZNode 能通过自身路径唯一标识,每个节点还有版本(version),版本从 0 开始计数;

  • 从代码层面看:

    • DataTree 类中用 ConcurrentHashMap<String, DataNode> 来存储节点,还有 dataWatcheschildWatches 用于管理监听;
    • DataNode 类实现了 Record 接口,包含存储数据的 byte 数组、访问控制相关的 Long 类型 acl、持久化状态 StatPersisted 以及子节点集合 children 等成员;
    java 复制代码
    public class DataTree {
        private final ConcurrentHashMap<String, DataNode> nodes =
            new ConcurrentHashMap<String, DataNode>();
        
        private final WatchManager dataWatches = new WatchManager();
        private final WatchManager childWatches = new WatchManager();
    }
    
    public class DataNode implements Record {
        byte data[];
        Long acl;
        public StatPersisted stat;
        private Set<String> children = null;
     }       

3.2 ZNode

3.2.1 介绍

  • ZooKeeper 存在多种节点(ZNode)类型,各有不同生命周期:

    类型 生命周期 创建示例
    持久节点(persistent node) 一直存在,一直存储在 ZooKeeper 服务器上,即使创建该节点的客户端与服务端的会话关闭了,该节点依然不会被删除 create /locks
    临时节点(ephemeral node) 当创建该临时节点的客户端会话因超时或发生异常而关闭时,该节点也相应在 ZooKeeper 服务器上被删除 create -e /locks/DBLock
    有序节点(sequential node) 并不算是一种单独种类的节点,而是在持久节点和临时节点特性的基础上,增加了一个节点有序的性质。在我们创建有序节点的时候会自动使用一个单调递增的数字作为后缀 create -e -s /jobs/job(临时有序节点)
    容器节点(container node) 当一个容器节点的最后一个子节点被删除后,容器节点也会被删除 create -c /work
    TTL节点(ttl node) 当一个TTL节点在 TTL 内没有被修改并且没有子节点,会被删除。注意:默认此功能不开启,需要修改配置文件extendedTypesEnabled=true create -t 3000 /ttl_node
  • 此外,还有几种组合类型节点:

    • 持久节点(PERSISTENT):这样的节点在创建后,即使 ZooKeeper 集群或客户端宕机也不会丢失;

    • 临时节点(EPHEMERAL):客户端宕机或在指定超时时间内未给 ZooKeeper 集群发消息,节点就会消失;

    • 持久顺序节点(PERSISTENT_SEQUENTIAL):除了具备持久节点的特点,节点名还具备顺序性;

    • 临时顺序节点(EPHEMERAL_SEQUENTIAL):除了具备临时节点的特点,节点名也具备顺序性;

  • ZooKeeper 主要用到上述 4 种节点,另外 3.5.3 版本新增 Container 容器节点

    • 当容器节点无子节点时,会被 ZooKeeper 定期(定时任务默认 60s 检查一次)删除;
    • 与持久节点的区别是:ZooKeeper 服务端启动后,有有一个单独的线程去扫描所有容器节点,当子节点数为 0 时会自动删除该容器节点,可用于 Leader 选举或锁场景(例如,当锁释放或领导者退出时,相关节点被删除后,容器节点会自动清理);
    • 注意:容器节点不能有子容器节点(即不能嵌套容器节点);
  • TTL 节点 :带过期时间节点,默认禁用,需要在zoo.cfg中添加extendedTypesEnabled=true开启。 注意:TTL 不能用于临时节点;

  • 不同节点的创建命令示例:

    cmd 复制代码
    # 创建持久节点
    create /servers  xxx
    # 创建临时节点
    create -e /servers/host  xxx
    # 创建临时有序节点
    create -e -s /servers/host  xxx
    # 创建容器节点
    create -c /container xxx
    # 创建TTL节点
    create -t 10 /ttl

3.2.2 示例:实现分布式锁

  • 分布式锁需要在锁的持有者出现异常(宕机)时能释放锁,而 ZooKeeper 的临时节点(ephemeral 节点)就具备类似于分布式锁这样的特性:当创建临时节点的客户端会话结束(比如客户端宕机,会话关闭),临时节点会被自动删除;

    • 终端 1

      cmd 复制代码
      # 启动 ZooKeeper 命令行客户端
      zkCli.sh
      # 创建一个临时节点 /lock,此时终端 1 持有这个"锁"(对应临时节点)
      create --e /lock
      # 退出客户端,会话结束,临时节点 /lock 会被自动删除,锁释放
      quit
    • 终端 2

      cmd 复制代码
      # 同样先启动客户端
      zkCli.sh
      # 因为终端 1 已创建了 /lock 节点,所以下面的创建会失败
      create --e /lock
      # 对 /lock 节点设置监听,当节点状态变化(比如终端 1 退出后节点被删除)时,会收到通知
      stat --w /lock
      # 当终端 1 退出,/lock 节点被删除后,终端 2 就能成功创建临时节点 /lock,获取到锁
      create --e /lock
  • 节点状态信息:ZooKeeper 的节点类似树状结构,可存储信息和属性,通过 stat 命令查看节点状态:

    • cZxid:节点创建时的事务 ID,用于标识节点创建这个操作的唯一性;

    • ctime:节点创建的时间戳,记录节点创建的时间;

    • mZxid:节点最后一次被修改时的事务 ID,每次对节点的修改(包括数据修改等)都会更新该值;

      对于 ZooKeeper 来说,每次的变化都会产生一个唯一的事务 id,即 Zxid(ZooKeeper Transaction Id);

      • 通过 Zxid,可以确定更新操作的先后顺序。例如,如果 Zxid1 小于 Zxid2 ,说明 Zxid1 操作先于 Zxid2 发生;
      • Zxid 对于整个 ZooKeeper 都是唯一的,即使操作的是不同的 ZNode;
    • mtime:节点最后一次被修改的时间戳;

    • pZxid:节点的子节点列表最后一次被修改的事务 ID;

      • 添加或删除子节点操作才会变更 pZxid,对于子节点内容的修改不影响;
      • 换句话说:只有子节点列表变更了才会变更pzxid,子节点内容变更不会影响pzxid
    • cversion:子节点的版本号,子节点有变化(添加、删除)时,版本号加 1;

    • dataVersion:节点数据 的版本号,每次对节点数据进行 set 操作(即使数据相同),版本号加 1,可避免数据更新的先后顺序问题;

    • aclVersion:节点访问控制列表(ACL)的版本号;

    • ephemeralOwner:若该节点为临时节点,该值是与节点绑定的会话 ID;若为持久节点,值为 0(持久节点);

      客户端和 ZooKeeper 服务端通信前要建立会话(session),会话因连接超时、授权失败或显式关闭等情况结束时,临时节点会被删除;

    • dataLength:节点存储数据的长度;

    • numChildren:节点直接子节点的数量。

3.2.3 示例:ZooKeeper 乐观锁删除

悲观锁 :假定会冲突,因此先加锁再访问,独占资源;
乐观锁:假定不冲突,因此直接访问,只在提交更新时检测是否发生冲突;

  • 先删除再创建节点

    • 执行 delete /test2 命令删除 /test2 节点;
    • 再用 create /test2 重新创建该节点;
    • 这一步是为后续基于版本号的操作做准备,重新初始化节点;
  • 尝试按指定版本删除节点(失败)

    • 执行 delete -v 1 /test2,试图删除版本号(cversion)为 1 的 /test2 节点。但操作失败,提示"version No is not valid : /test2";

      在 ZooKeeper 的 delete -v 命令中,版本号指的是 dataVersion

    • 接着用 get -s /test2 获取节点详细信息,发现此时节点的 cversion 为 0,这就解释了为何按版本 1 删除会失败------当前节点版本并非 1;

  • 修改节点数据

    • 执行 set /test2 abc 命令,修改 /test2 节点的数据为 abc。然后再次用 get -s /test2 查询节点信息;

    • 此时可以看到,节点的 dataVersion 变为 1(因为数据被修改,版本号递增),而我们关注的 cversion 仍为 0(cversion 主要和子节点相关,这里没有子节点操作,所以未变化);

  • 按正确版本删除节点(成功)

    • 执行 delete -v 1 /test2,这次删除成功;
    • 因为经过前面修改节点数据等操作后,节点相关版本变化,此时按版本 1 删除能匹配上节点的版本状态,所以删除操作生效;
  • ZooKeeper 中基于版本号的操作(如这里的 delete -v),是乐观锁的一种体现。客户端在操作节点时,会携带期望的版本号,服务端会检查节点当前版本是否与期望版本一致:

    • 若一致,操作(删除、修改等)成功,且版本号递增;

    • 若不一致,操作失败,以此来避免分布式环境下的并发冲突问题。

3.3 watch 机制

3.3.1 介绍

  • ZooKeeper 的 watch 机制是一种监听机制,需要客户端先向服务端注册监听,当对应的事件发生时,服务端会通知客户端;

  • 监听的对象是事件,支持的事件类型如下:

    • None:连接建立事件;

    • NodeCreated:节点创建事件;

    • NodeDeleted:节点删除事件;

    • NodeDataChanged:节点数据变化事件;

    • NodeChildrenChanged:子节点列表变化事件;

    • DataWatchRemoved:节点监听被移除事件;

    • ChildWatchRemoved:子节点监听被移除事件;

  • 相关命令:

    cmd 复制代码
    # 监听节点数据的变化
    get -w path # 获取节点数据并设置监听
    stat -w path # 获取节点状态并设置监听
    # 监听子节点增减的变化 
    ls -w path # 列出节点子节点并设置监听
  • 监听特性:

    • 一次性触发:watch 是一次性的,触发后就会被移除,若要再次使用需要重新注册;

    • 客户端顺序回调:watch 回调是顺序串行执行的,只有回调完成后客户端才能看到最新的数据状态,且一个 watcher 的回调逻辑不宜过多,否则会影响其他 watch 执行;

    • 轻量级:WatchEvent 是最小的通信单位,仅包含通知状态、事件类型和节点路径,不会告知数据节点变化前后的具体内容;

    • 时效性:watcher 只有在当前 session 彻底失效时才会无效,如果在 session 有效期内快速重连成功,watcher 依然存在,还能接收通知;

  • 永久性 Watch :在 ZooKeeper 3.6.0 版本新增了永久性 Watch 功能,被触发后仍然保留,可继续监听 ZNode 上的变更。通过 addWatch [-m mode] path 命令为指定节点添加事件监听,支持两种模式:

    • PERSISTENT:持久化订阅,针对当前节点的修改和删除事件,以及当前节点的子节点的删除和新增事件;

    • PERSISTENT_RECURSIVE:持久化递归订阅(默认),在 PERSISTENT 的基础上,增加了子节点修改的事件触发,子节点的子节点的数据变化也会触发相关事件(满足递归订阅特性)。

3.3.2 示例:协同服务

  • 设计一个 master-worker 的组成员管理系统,要求:

    • 保证只有一个 master
    • master 监控 worker 状态
  • **保证只有一个 master **。要确保系统中只有一个 master,可以利用 ZooKeeper 临时节点(ephemeral 节点)的特性:临时节点会在创建它的客户端会话结束(如客户端宕机)时自动删除

    cmd 复制代码
    # master1 创建了临时节点 /master,此时 master1 成为系统中的 master
    create -e /master "m1:2223"
    
    # 由于 /master 节点已存在,所以创建失败,master2 无法成为 master
    create -e /master "m2:2223"
    Node already exists: /master
    
    # master2 对 /master 节点设置监听,这样当 /master 节点状态变化(比如 master1 宕机,节点被删除)时,master2 能收到通知
    stat -w /master
    
    # 当 master2 收到 /master 节点删除的通知后,再次执行 create -e /master "m2:2223"
    # 此时若成功创建,master2 就成为新的 master。
    create -e /master "m2:2223"
    • 这种方式也可用于 master-slave 选举:
  • master 实时获取 worker 的情况,借助 ZooKeeper 的节点监听机制以及临时节点特性;

    cmd 复制代码
    # master服务
    create /workers  # 创建/workers节点作为所有worker节点的父节点,用于统一管理worker
    
    # 让master服务监控/workers下的子节点
    ls -w /workers  # 对/workers节点设置子节点变化监听,当子节点增删时会收到通知
    
    # worker1
    create -e /workers/w1 "w1:2224"  # worker1创建临时子节点,数据为自身标识和端口;由于master设置了监听,会收到NodeChildrenChanged通知,得知有新的 worker 加入
    
    # master服务
    ls -w /workers  # 监听是一次性的,收到通知后需重新注册监听,以继续监控后续子节点变化
    
    # worker2
    create -e /workers/w2 "w2:2224"  # worker2创建临时子节点;master因重新注册了监听,会再次收到子节点变化通知,得知有新的 worker 加入
    
    # master服务
    ls -w /workers  # 再次重新注册监听,确保持续监控
    
    # worker2
    quit  # worker2退出导致会话结束,其创建的临时节点/w2被自动删除;master收到子节点删除的通知,感知到worker2下线

3.3.3 示例:条件更新

  • 用 ZooKeeper 的 ZNode(下图中的 /c)实现一个计数器(counter),通过 set 命令完成自增操作。在分布式环境下,多个客户端可能同时操作这个计数器,若不加以控制,会出现基于过期数据更新的问题,而条件更新能解决此问题;

  • 过程:

    • 初始状态 :ZooKeeper 中的 /c 节点数据版本为 0;

    • 客户端 1 首次操作 :客户端 1 执行 get -s -w /c。获取 /c 节点数据(此时为 0)和版本号(version=0),同时设置监听(-w);

    • 客户端 2 操作

      • 客户端 2 执行 set -s -v 0 /c 1,基于版本号 0,将 /c 节点数据更新为 1,此时节点版本号变为 1;
      • 客户端 1 因之前设置了监听,会收到节点数据变化的通知;
    • 客户端 1 再次获取并更新

      • 客户端 1 收到通知后,再次执行 get -s -w /c,获取到 /c 节点数据为 1,版本号为 1;
      • 然后执行 set -s -v 1 /c 2,基于版本号 1,将 /c 节点数据更新为 2,此时节点版本号变为 2(这一步下图中未体现);
    • 错误情况演示(若用无条件更新)

      • 如果客户端 1 不知道 /c 已被客户端 2 更新(即没有收到通知 ),还用过时的版本 1 去更新,若采用无条件更新,会错误地将 /c 数据更新为 2;
      • 但其实此时正确的后续更新应基于版本 1 之后的状态,条件更新通过版本号校验,能避免这种错误(当版本不匹配时,更新失败);
  • ZooKeeper 的条件更新(set 命令带 -v 指定版本号),要求更新操作时的版本号与节点当前版本号一致,才会执行更新。这样就保证了客户端是基于最新的节点状态进行操作,避免了分布式环境下多个客户端因并发操作,基于过期数据更新而导致的数据不一致问题。

3.4 节点特性总结

  • 同一级节点 key 名称是唯一的

    • 在 ZooKeeper 中,同一父节点下的子节点,其名称(key)是唯一的;
    • 例:先创建了 /lock 节点(create /lock),然后再次创建 /lock 节点(create /lock)时,会提示"Node already exists: /lock",即节点已存在,创建失败;
  • 创建 ZooKeeper 节点时,需要指定从根节点(/)开始的完整路径,不能只写相对路径

  • session 关闭时,临时节点清除

    • 临时节点(通过 create -e 创建的节点)与客户端的 session 相关联;
    • 当客户端的 session 关闭(比如客户端断开连接)时,临时节点会被自动清除;
  • 自动创建顺序节点

    • 可以创建带顺序的节点(通过 create -screate -e -s,后者是临时且顺序的节点)。ZooKeeper 会为顺序节点自动生成唯一的顺序编号,保证节点创建的顺序性;
    • 例:创建 /queue/host1/queue/host2 等临时顺序节点时,节点名称后自动添加了如 1000000000020000000001 等顺序编号,通过 ls -R /queue 可以看到这些带顺序编号的节点;
  • watch 机制,监听节点变化

    • ZooKeeper 的 watch 机制类似于观察者模式。客户端向服务端的某个节点路径注册一个 watcher,同时客户端存储特定的 watcher;
    • 当节点数据或子节点发生变化时,服务端通知客户端,客户端进行回调处理;
    • 需要注意的是,监听事件是单次触发的,事件触发后,该 watcher 就失效了,如果还需要监听,需要重新注册。
  • delete 命令只能一层一层删除 。使用 delete 命令删除节点时,只能删除没有子节点的节点,需要一层一层地删除。不过新版本的 ZooKeeper 可以通过 deleteall 命令递归删除节点及其所有子节点。

3.5 应用场景详解

  • ZooKeeper 适用于存储和协同相关的关键数据,但不适合大数据量存储,因为其设计更侧重于分布式协同而非大规模数据承载;

  • 基于 ZooKeeper 具备的诸多节点特性(如节点唯一性、临时节点机制、watch 监听机制、顺序节点等),能支持多种经典应用场景:

    • 注册中心:服务可在 ZooKeeper 注册自身信息,其他服务能方便地发现和调用这些已注册的服务;

    • 数据发布/订阅(常用于实现配置中心):可将配置数据发布到 ZooKeeper 节点,订阅的应用能实时获取配置变更,实现配置的集中管理与动态更新;

    • 负载均衡:结合服务注册等,可根据服务节点的状态等信息,将请求合理分发到不同服务节点,平衡各节点负载;

    • 命名服务:为分布式系统中的资源(如服务、节点等)提供统一的命名规则,方便识别和管理;

    • 分布式协调/通知:在分布式环境下,各节点可通过 ZooKeeper 进行协调操作,或接收节点状态变化等通知;

    • 集群管理:能监控集群中节点的状态(如在线、离线等),实现集群的统一管理;

    • Master 选举:在分布式系统中,通过 ZooKeeper 选出一个主节点(Master),负责协调或管理其他节点,保证系统中只有一个有效主节点;

    • 分布式锁:利用 ZooKeeper 的节点特性,实现分布式环境下的锁机制,保证多个节点对共享资源的互斥访问;

    • 分布式队列:基于 ZooKeeper 的顺序节点等,可实现分布式队列,支持多节点间的有序任务调度等。

3.5.1 统一命名服务

  • 在分布式环境中,为了方便识别应用服务,常需要对服务进行统一命名。比如 IP 地址难以记忆,而域名更容易记住;

  • 通过 ZooKeeper 可以实现这种统一命名服务。如图所示:

    • ZooKeeper 以树状结构组织节点,根节点(/)下有 services 节点,services 下又有 www.baidu.com 节点,www.baidu.com 节点下关联了多个 IP 地址(183.232.231.172180.97.34.9439.156.66.18 等);
    • 客户端(Client1Client2)可以通过访问 www.baidu.com 这个易记的域名节点,来获取对应的 IP 地址等服务信息,无需直接记忆复杂的 IP;
  • 利用 ZooKeeper 顺序节点的特性,能够制作分布式的序列号生成器(也叫 ID 生成器);

    • 在分布式环境下,需要为数据生成唯一 ID,UUID 虽然能保证唯一性,但没有规律,不利于理解和管理。而 ZooKeeper 生成的顺序节点可以产生有顺序、易理解且支持分布式环境的编号;

    • 例:/order 节点下有多个顺序节点,如 /order - date1 - 00000000000001/order - date2 - 00000000000002 等,这些节点带有顺序编号,可作为分布式环境下数据的唯一 ID,既保证了唯一性,又有顺序性,方便使用和管理;

      复制代码
      /
      └── /order
          ├── /order-date1-000000000000001
          ├── /order-date2-000000000000002
          ├── /order-date3-000000000000003
          ├── /order-date4-000000000000004
          └── /order-date5-000000000000005

3.5.2 数据发布/订阅

  • 数据发布/订阅的常见场景是配置中心:发布者将数据发布到 ZooKeeper 的一个或一系列节点上,订阅者对这些节点进行数据订阅,从而实现动态获取数据的目的;

  • 配置信息通常具有以下特点:

    • 数据量小的 KV:配置数据一般是键值对(Key-Value)形式,且数据量不大;

    • 数据内容在运行时会发生动态变化:比如系统运行过程中,配置可能需要调整,需要能动态更新;

    • 集群机器共享,配置一致:集群中的多台机器需要共享相同的配置,保证配置的一致性;

  • ZooKeeper 采用"推 + 拉"结合的方式来实现数据发布/订阅:用 及时通知客户端数据变化,通过让客户端获取到最新的完整数据

    • :服务端(ZooKeeper 服务)会向注册了监控节点的客户端推送 Watcher 事件通知。当 ZooKeeper 中存储配置数据的节点发生变化时,服务端会主动通知订阅了该节点的客户端;

    • :客户端获得通知后,会主动到服务端拉取最新的数据;

    • 下图中:

      • ZooKeeper 中有 /configuration 节点存储配置数据,Client1Client2Client3 等客户端通过 watch(监听)机制关注该节点;
      • /configuration 节点数据变化时,ZooKeeper 服务端会推送通知给客户端,客户端再主动拉取最新配置数据;

3.5.3 统一集群管理

  • 在分布式环境里,实时掌握每个节点的状态十分必要,这样能依据节点的实时状态做出相应调整。ZooKeeper 可实现对节点状态变化的实时监控,具体方式为:

    • 把节点信息写入 ZooKeeper 上的一个 ZNode(ZooKeeper 的数据节点);

    • 监听这个 ZNode,从而获取它的实时状态变化;

  • 如下图:

    • ZooKeeper 中有 /GroupManager 节点,其下有 /client1/client2/client3 等子节点,分别对应集群中的 Client1Client2Client3
    • 客户端(Client1Client2Client3)会向对应的 ZNode 进行注册并设置监听;
    • 当节点状态发生变化时,ZooKeeper 能及时将变化通知给监听的客户端,使得集群管理者或其他相关组件可以实时了解各节点的状态,进而进行如负载均衡调整、故障节点处理等操作;

3.5.4 负载均衡

  • 在 ZooKeeper 中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求,以此实现负载均衡;

  • 如下图:

    • ZooKeeper 以树状结构组织节点,根节点(/)下有 services 节点,services 下又有 orderService 节点,orderService 节点下关联了多个服务器(以 ip:port 标识),并且记录了每台服务器的访问数(如访问数 100、50、80 等);
    • Client1Client2 等客户端发起访问请求时,系统会参考 ZooKeeper 中记录的各服务器访问数,将请求导向当前访问数最少的服务器,从而平衡各服务器的负载,避免部分服务器因请求过多而压力过大,部分服务器又过于空闲的情况;

3.5.5 Master-Worker 架构

  • Master-Worker 是一种广泛应用的分布式架构,在该架构中,有一个 master 负责监控 worker 的状态,并为 worker 分配任务;

    • 单一 active master

      • 在任何时刻,系统中最多只能有一个处于 active(活跃)状态的 master
      • 如果出现多个 master 共存的情况,会导致"脑裂"问题(即多个 master 各自为战,对系统状态和任务分配产生冲突,破坏系统一致性);
    • 备份 master

      • 系统中除了处于 active 状态的 master,还会有一个 backup master(备份主节点);
      • active master 失败时,backup master 可以快速进入 active 状态,保证系统服务的连续性;
    • worker 状态监控与任务重分配

      • master 会实时监控 worker 的状态,能够及时收到 worker 成员变化(如 worker 上线、下线、故障等)的通知;
      • master 收到 worker 成员变化的通知时,通常会重新进行任务的分配,以适应 worker 资源的变化,确保任务能高效执行;
  • 如下图:

    • 左侧的 cluster(集群)中包含 master 以及 worker1worker2worker3
    • 右侧的树状结构展示了 ZooKeeper 中的节点组织,/ 为根节点,下有 masterworkers 节点,workers 下又有 w1w2w3 等子节点,分别对应各个 worker
    • master 通过 ZooKeeper 的节点机制来监控 worker 的状态变化,从而实现对 worker 的管理和任务分配;

3.6 ACL 权限控制

  • ZooKeeper 的 ACL(Access Control List,访问控制列表)权限在生产环境至关重要,可对节点设置读写等权限,保障数据安全。

3.6.1 ACL 构成

  • ZooKeeper 的 ACL 通过 [scheme:id:permissions] 构成权限列表:

    • scheme :授权模式,有 world(授权所有客户端)、auth(用添加认证的用户)、digest(用户:密码方式)、ip(IP 地址认证)、super(超级用户)几种;

    • id :授权对象,若为 IP 模式,是 IP 地址或网段;Digest/Super 模式对应用户名;World 模式则是所有用户;

    • permissions :授权权限,由 cdrwa 组成,分别代表创建(create)、删除(delete)、读(read)、写(write)、管理(admin)权限;

  • 授权模式说明

    模式 描述
    world 授权对象为 anyone,所有登录服务器的客户端都能对节点执行对应权限操作
    auth 使用添加认证的用户进行认证
    digest 使用 用户:密码 方式验证
    ip 对连接的客户端用 IP 地址认证
  • 权限类型说明

    权限类型 ACL 简写 描述
    read r 读取节点及显示子节点列表的权限
    write w 设置节点数据的权限
    create c 创建子节点的权限
    delete d 删除子节点的权限
    admin a 设置该节点 ACL 权限的权限
  • 授权命令说明

    授权命令 用法 描述
    getAcl getAcl path 读取节点的 ACL
    setAcl setAcl path acl 设置节点的 ACL
    create create path data acl 创建节点时设置 ACL
    addAuth addAuth scheme auth 添加认证用户,类似登录操作
  • ACL 权限控制测试:

    • 取消节点读权限

      • 先创建 /name 节点,用 getAcl /name 查看,默认是 world:anyone:cdrwa(所有权限);

      • 执行 setAcl /name world:anyone:cdwa,取消读(r)权限;

      • 再用 get /name 读取节点,提示 Insufficient permission : /name,即因无读权限,读取操作失败;

    • 取消节点删除子节点权限

      • 创建 /name/fox 子节点;

      • 执行 setAcl /name world:anyone:cwa,取消删除(d)权限;

      • 尝试 delete /name/fox 删除子节点,提示 Insufficient permission : /name/fox,因无删除子节点权限,删除操作失败;

3.6.2 auth授权模式

  • 创建用户:

    cmd 复制代码
    addauth digest shisan:123456
    • 添加一个采用 digest 认证模式的用户,用户名为 shisan,密码为 123456
  • 设置权限:

    • /name 节点设置 ACL 权限,指定只有用户 shisan(密码 123456)拥有创建(c)、删除(d)、读(r)、写(w)、管理(a)的权限;

      cmd 复制代码
      setAcl /name auth:shisan:123456:cdrwa
    • shisan:123456 进行 SHA - 1 加密并做 Base64 编码,会返回一串加密后的字符串;

      cmd 复制代码
      echo -n shisan:123456 | openssl dgst -binary -sha1 | openssl base64
    • 用加密后的信息(ZSwmgmtnTnxIusRfIvoHFJAYGQU)来设置节点权限,这样更安全:

      cmd 复制代码
      setAcl /name auth:shisan:ZSwmgmtnTnxIusRfIvoHFJAYGQU=:cdrwa
  • 权限验证:

    • 退出客户端后重新连接,执行 get /name 命令尝试读取 /name 节点,此时提示 Insufficient permission : /name,即没有足够权限,因为重新连接后,客户端未携带之前添加的认证用户信息;

    • 接着执行 addauth digest shisan:123456 命令,重新添加认证用户 shisan,之后再执行 get /name 命令,就能够成功读取节点内容。这验证了只有通过认证的用户才能访问被设置了对应权限的节点。

3.6.3 digest授权模式

  • digest 授权模式是一种基于用户名:密码的认证方式,密码会经过加密处理;

    • 设置权限 :为 /lost/shisan 节点设置 digest 模式的 ACL 权限,指定用户 shisan(密码经加密后为 ZSwmgmtnTnxIusRF1voHFJAYGQU=)拥有创建(c)、删除(d)、读(r)、写(w)、管理(a)的权限;

      cmd 复制代码
      setAcl /lost/shisan digest:shisan:ZSwmgmtnTnxIusRF1voHFJAYGQU=:cdrwa
    • 操作示例

      • 先创建 /lost/lost/shisan 节点,初始时 /lost/shisan 节点的 ACL 是 world:anyone:cdrwa(所有客户端都有全部权限,通过getAcl /lost/shisan可以获取);

        cmd 复制代码
        create /lost
        create /lost/shisan
      • 执行上方设置权限的 setAcl 命令修改权限后,直接执行 getAcl /lost/shisan 会提示 Insufficient permission : /lost/shisan(权限不足);

      • 执行 addauth digest shisan:123456 添加认证用户后,再执行 getAcl /lost/shisan,就能成功获取到该节点的 ACL 信息,显示为 digest,'shisan:ZSwmgmtnTnxIusRF1voHFJAYGQU=:cdrwa,这就验证了只有通过认证的用户才能访问设置了 digest 权限的节点。

3.6.4 IP 授权模式

  • IP 授权模式是基于客户端 IP 地址进行权限控制;

    • 设置权限

      cmd 复制代码
      # 为 /node-ip 节点设置 ACL 权限,指定 IP 为 192.168.109.128 的客户端拥有创建、删除、读、写、管理的权限
      setAcl /node-ip ip:192.168.109.128:cdrwa
      # 创建 /node-ip 节点时就设置好 IP 授权的 ACL
      create /node-ip data ip:192.168.109.128:cdrwa
    • 多 IP 支持 :多个指定 IP 可以通过逗号分隔,例如 setAcl /node-ip ip:IP1:rw,ip:IP2:a,这样 IP 为 IP1 的客户端有读和写权限,IP 为 IP2 的客户端有管理权限。

3.6.5 Super 超级管理员模式

  • Super 是一种特殊的 Digest 模式,在该模式下,超级管理员用户可以对 ZooKeeper 上的所有节点进行任何操作;

  • 开启方式:需要在 ZooKeeper 的启动脚本中添加 JVM 参数来开启,参数为:

    复制代码
    -Dzookeeper.DigestAuthenticationProvider.superDigest=admin:<base64encoded(SHA1(123456))
    • admin 是用户名,123456 是密码,base64encoded(SHA1(123456)) 是对 admin:123456 进行 SHA - 1 加密后再做 Base64 编码的结果。

3.6.6 可插拔身份验证接口

  • ZooKeeper 提供了权限扩展机制,允许用户实现自定义的权限控制方式;

  • 实现方式 :要实现自定义的权限控制机制,需要继承 AuthenticationProvider 接口,用户通过该接口可以实现自定义的权限控制逻辑;

    java 复制代码
    public interface AuthenticationProvider {
        
    	// 返回标识插件的字符串
        String getScheme();
        // 将用户和验证信息关联起来
        KeeperException.Code handleAuthentication(ServerCnxn cnxn, byte authData[]);
        // 验证 ID 格式是否有效
        boolean isValid(String id);
        // 将认证信息与 ACL 进行匹配,判断是否命中权限规则
        boolean matches(String id, String aclExpr);
        // 判断是否已授权
        boolean isAuthenticated();
    }

4 ZooKeeper 集群架构

4.1 集群角色

  • Leader(领导者)

    • 是事务请求(写操作,如 createsetDatadelete 等)的唯一调度者和处理者,保证集群事务处理的顺序性;
    • 同时也是集群内部各个服务器的调度者,对于写操作请求,会统一转发给 Leader 处理,Leader 需要决定编号、执行操作,这个过程称为事务;
  • Follower(跟随者)

    • 处理客户端的非事务(读操作)请求,这类请求可以直接响应;
    • 对于事务请求,会转发给 Leader;
    • 参与集群 Leader 的选举投票;
  • Observer(观察者)

    • 对于非事务请求可以独立处理(读操作),事务性请求会转发给 Leader 处理;

    • Observer 节点接收来自 Leader 的 inform 信息,更新自己的本地存储,但不参与提交和选举投票;

    • 作用是在不影响集群事务处理能力的前提下,提升集群的非事务处理能力;

    • 配置示例 :配置一个 ID 为 3 的观察者节点,其中 :observer 标识该节点为 Observer 角色

      cmd 复制代码
      server.3=192.168.0.3:2888:3888:observer
    • 应用场景

      • 提升集群读性能:因为 Observer 不参与提交和选举的投票过程,所以往集群里添加 Observer 节点可以提高整个集群的读性能;
      • 跨数据中心部署:例如需要部署一个北京和香港两地都可使用的 ZooKeeper 集群服务,且要求北京和香港客户的读请求延迟都很低。解决方案是把香港的节点都设置为 Observer,这样香港的读请求可由本地 Observer 节点处理,降低延迟。

4.2 集群架构

  • ZooKeeper 集群由 Leader 节点和 Follower 节点组成:

    • Leader 节点(以 Server2 为例):可以处理读写请求,是集群中事务(写操作)的唯一调度者和处理者,保证集群事务处理的顺序性;

    • Follower 节点(以 Server1、Server3 为例):只能处理读请求,当接收到写请求时,会把写请求转发给 Leader 来处理;

    • 客户端与节点交互

      • 客户端可以向 Leader 节点发起读或写请求,也可以向 Follower 节点发起读请求;
      • 若向 Follower 节点发起写请求,Follower 会将其转发给 Leader 处理;
  • ZooKeeper 通过以下方式保证数据一致性:

    • 全局可线性化(Linearizable)写入:先到达 Leader 的写请求会被先处理,Leader 负责决定写请求的执行顺序,确保所有客户端对写操作的顺序感知是一致的;

    • 客户端 FIFO 顺序:来自给定客户端的请求按照发送顺序执行,保证了单个客户端请求的有序性。

4.3 三节点 ZooKeeper 集群搭建

4.3.1 环境准备

  • 需要三台虚拟机,IP 分别为:

    复制代码
    192.168.65.163
    192.168.65.184
    192.168.65.186
  • 如果条件有限,也可以在一台虚拟机上搭建 ZooKeeper 伪集群。

4.3.2 集群搭建步骤

  • 修改 zoo.cfg 配置,添加 server 节点配置

    • 首先修改数据存储目录,设置 dataDir=/data/zookeeper

    • 然后在三台虚拟机的 zoo.cfg 文件末尾添加如下配置:

      • server.1=192.168.65.163:2888:3888
      • server.2=192.168.65.184:2888:3888
      • server.3=192.168.65.186:2888:3888

      server.A=B:C:D 中:

      • A 是服务器编号,集群模式下需在 dataDir 目录下创建 myid 文件(就是下一步所做的工作),文件内容为 A 的值,ZooKeeper 启动时读取该文件,与 zoo.cfg 中的配置比较来判断是哪个服务器;
      • B 是服务器的地址;
      • C 是该服务器(Follower)与集群中 Leader 服务器交换信息的端口;
      • D 是当集群中的 Leader 服务器宕机时,用于重新选举新 Leader 的通信端口;
  • 创建 myid 文件,配置服务器编号

    • dataDir 对应的目录(即 /data/zookeeper)下创建 myid 文件,文件内容为对应 IP 的 ZooKeeper 服务器编号(注意文件中不要有空格和空行);

      • 对于 zoo.cfg 中配置的 server.1=192.168.65.163:2888:3888,这台服务器(IP 为 192.168.65.163)的 myid 文件里就填写 1
      • 对于 server.2=192.168.65.184:2888:3888,对应的服务器(IP 为 192.168.65.184)的 myid 文件里填写 2
      • 对于 server.3=192.168.65.186:2888:3888,对应的服务器(IP 为 192.168.65.186)的 myid 文件里填写 3
    • 注意:myid 文件一定要在 Linux 系统中创建,在 Notepad++ 等 Windows 编辑器中创建很可能出现乱码问题;

  • 启动 ZooKeeper server 集群

    • 启动前需要关闭防火墙(生产环境需要打开对应端口);

    • 分别在三个节点执行 bin/zkServer.sh start 命令启动 ZooKeeper server;

    • 执行 bin/zkServer.sh status 命令查看集群状态,若显示某节点的模式为 leader,说明该节点成为了集群的 Leader 节点。

4.3.3 常见问题及解决

  • 如果服务启动出现java.net.NoRouteToHostException: 没有到主机的路由 (Host unreachable)的异常;
  • 原因
    • zoo.cfg 配置错误;
    • 防火墙未关闭;
  • 解决方法
    • 检查 zoo.cfg 配置是否正确;
    • 关闭防火墙,在 CentOS 7 系统中,可通过以下命令:
      • systemctl status firewalld:检查防火墙状态;
      • systemctl stop firewalld:关闭防火墙;
      • systemctl disable firewalld:禁止防火墙开机启动。

4.4 四字命令

  • 用户可以使用 ZooKeeper 四字命令获取 ZooKeeper 服务的当前状态及相关信息,在客户端通过 nc(netcat)向 ZooKeeper 提交相应命令。首先需要安装 nc 命令,在 CentOS 系统中,可通过 yum install nc 进行安装;

  • 四字命令格式为:

    cmd 复制代码
    echo [command] | nc [ip] [port]
    • 即通过 echo 输出四字命令,再通过管道(|)传递给 nc 命令,由 nc 连接到指定 IP 和端口的 ZooKeeper 服务来执行命令;
  • 常用四字命令及功能

    • conf:3.3.0 版本引入,打印出服务相关配置的详细信息;

    • cons:3.3.0 版本引入,列出所有连接到这台服务器的客户端全部连接/会话详细信息,包括"接受/发送"的包数量、会话 ID、操作延迟、最后的操作执行等信息;

    • crst:3.3.0 版本引入,重置所有连接的连接和会话统计信息;

    • dump:列出那些比较重要的会话和临时节点,这个命令只能在 leader 节点上有用;

    • envi:打印出服务环境的详细信息;

    • reqs:列出未经处理的请求;

    • ruok:测试服务是否处于正确状态,如果确实如此,服务返回"imok",否则不做任何相应;

    • stat:输出关于性能和连接的客户端的列表;

    • srst:重置服务器的统计;

    • srvr:3.3.0 版本引入,列出连接服务器的详细信息;

    • wchs:3.3.0 版本引入,列出服务 watch 的详细信息;

    • wchc:3.3.0 版本引入,通过 session 列出服务器 watch 的详细信息,输出是一个与 watch 相关的会话的列表;

    • wchp:3.3.0 版本引入,通过路径列出服务器 watch 的详细信息,输出一个与 session 相关的路径;

    • mntr:3.4.0 版本引入,输出可用于检测集群健康状态的变量列表;

  • 开启四字命令的方法

    • 方法 1 :在 zoo.cfg 文件里加入配置项 4lw.commands.whitelist=*,让这些指令放行;

    • 方法 2 :在 ZK 的启动脚本 zkServer.sh 中新增放行指令

      cmd 复制代码
      # 添加 JVM 环境变量 -Dzookeeper.4lw.commands.whitelist=*
      ZOOMAIN="-Dzookeeper.4lw.commands.whitelist=* ${ZOOMAIN}"
  • 具体命令示例

    • stat 命令:用于查看 ZK 的状态信息,例:

      cmd 复制代码
      echo stat | nc 192.168.65.186 2181
    • ruok 命令 :用于查看当前 ZK server 是否启动,若返回 imok 表示正常,例:

      cmd 复制代码
      echo ruok | nc 192.168.65.186 2181

4.5 ZooKeeper选举原理

  • ZooKeeper 的 Leader 选举过程基于投票和对比规则,确保集群中选出具有最高优先级的服务器作为 Leader 处理客户端请求。以服务启动期间的选举 为例,选票格式为 vote=(myid,ZXID),通过多轮投票,依据特定规则确定 Leader;

  • 选票格式为vote=(myid, ZXID),其中myid是节点的唯一标识,ZXID是事务ID,用于标识数据的新旧程度,ZXID越大,代表该节点的数据越新;

    • 第一轮投票:
      • myid=1的节点
        • 投出选票(1, 0)自己投自己);
        • 收到myid=2节点投出的(2, 0)选票后,对比自己投出的选票;
        • 因为ZXID相同(都为0),按照规则,默认选择myid大的节点作为Leader,所以推荐(2, 0)成为Leader,后续投出的选票变为(2, 0)
      • myid=2的节点
        • 投出选票(2, 0)自己投自己);
        • 收到myid=1节点投出的(1, 0)选票后,由于自己的ZXID与对方相同,但myid更大,所以仍然推荐(2, 0)成为Leader;
    • 第二轮投票:
      • myid=1的节点
        • 投出选票(2, 0),同时收到的选票也是(2, 0)
        • 此时,投给(2, 0)的票数已经超过集群节点数的半数(集群有3个节点,这里已有2个节点支持(2, 0)),选举结束,确定(2, 0)对应的节点为Leader;
      • myid=2的节点 :投出选票(2, 0),收到的选票同样是(2, 0),因此确定自己为Leader;
    • myid=3的节点启动时,发现集群已经选举出Leader(myid=2对应的节点),于是自己成为Follower(跟随者),接受Leader的管理,同步数据等。
  • 源码:

    • totalOrderPredicate 方法用于判断新的候选(newIdnewZxidnewEpoch)是否比当前投票(curIdcurZxidcurEpoch)更优,返回 true 表示新候选更优,否则为 false

    • 判断逻辑与上述投票对比规则一致:

      • newEpoch > curEpoch,新候选更优;

      • newEpoch == curEpoch,再看 newZxidcurZxid

        • newZxid > curZxid,新候选更优;
        • newZxid == curZxid,则比较 newIdcurIdnewId > curId 时新候选更优;
    java 复制代码
    /**
     * Check if a pair (server id, zxid) succeeds our
     * current vote.
     *
     */
    protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
        LOG.debug(
            "id: {}, proposed id: {}, zxid: 0x{}, proposed zxid: 0x{}",
            newId,
            curId,
            Long.toHexString(newZxid),
            Long.toHexString(curZxid));
    
        if (self.getQuorumVerifier().getWeight(newId) == 0) {
            return false;
        }
    
        /*
         * We return true if one of the following three cases hold:
         * 1- New epoch is higher
         * 2- New epoch is the same as current epoch, but new zxid is higher
         * 3- New epoch is the same as current epoch, new zxid is the same
         *  as current zxid, but server id is higher.
         */
    
        return ((newEpoch > curEpoch)
                || ((newEpoch == curEpoch)
                    && ((newZxid > curZxid)
                        || ((newZxid == curZxid)
                            && (newId > curId)))));
    }
  • zxid 是一个 64 位的整数,由高 32 位的 epoch 和低 32 位的 counter 组成:

    • epoch:表示 ZooKeeper 服务器的逻辑时期(logical epoch),用于区分不同的 Leader 选举周期,每次新的 Leader 选举周期会更新 epoch

    • counter:是一个在每个 epoch 内递增的计数器,用于标识事务的顺序,保证同一 epoch 内事务的有序性;

    • 工具类 ZxidUtils 提供了从 zxid 中获取 epochcounter、根据 epochcounter 生成 zxid 以及将 zxid 转换为字符串等方法,方便对 zxid 进行操作和解析;

    java 复制代码
    public class ZxidUtils {
    
        public static long getEpochFromZxid(long zxid) {
            return zxid >> 32L;
        }
        public static long getCounterFromZxid(long zxid) {
            return zxid & 0xffffffffL;
        }
        public static long makeZxid(long epoch, long counter) {
            return (epoch << 32L) | (counter & 0xffffffffL);
        }
        public static String zxidToString(long zxid) {
            return Long.toHexString(zxid);
        }
    
    }
相关推荐
禅思院4 小时前
前端部署“三层漏斗”完全指南:从CI/CD到自动回滚的工程化实战【开题】
前端·架构·前端框架
plainGeekDev5 小时前
单例模式 → object 声明
android·java·kotlin
用户298698530146 小时前
Java 实现 Word 文档文本与图片提取的方法
java·后端
Java之美6 小时前
一次k8s升级引发的DevicePlugin注册失败
云原生·kubernetes
秋播6 小时前
nerdctl推送rancher本地镜像到harbor
云原生
SimonKing7 小时前
铁子,IntelliJ IDEA 2026.1.3来了,升不升?
java·后端·程序员
咖啡八杯18 小时前
GoF设计模式——策略模式
java·后端·spring·设计模式
用户128526116021 天前
我把祖传Java项目重构后,接口响应从3s砍到了200ms,只改了这几行代码
java