数据模型znodeZookeeper技术内幕


zookeeper添加用户认证后kafka连接不上_子节点


zk 数据存储结构与标准的 Unix 文件系统非常相似,都是在根节点下挂很多子节点。zk中没有引入传统文件系统中目录与文件的概念,而是使用了称为 znode 的数据节点概念。znode 是 zk 中数据的最小单元,每个 znode 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。

节点类型

  • 持久节点:zk 中最常见的节点,节点一旦被创建,只要不删除,其就会一直存在于 zk中。
  • 持久顺序节点:一个父节点可以为其直接子节点维护一份顺序,用于记录子节点创建的先后顺序。在创建子节点时,会自动在指定的节点名称后添加数字后辍,用为该子节点的完整名称。序号由 10 位数字组成,从 0 开始计数。
  • 临时节点:临时节点的生命周期与客户端的会话绑定在一起,会话消失则该节点也就会消失。临时节点只能做叶子节点,不能创建子节点。
  • 临时顺序节点:添加了创建序号的临时节点

节点状态

  • cZxid:Created Zxid,表示当前 znode 被创建时的事务 ID。
  • ctime:Created Time,表示当前 znode 被创建的时间。
  • mZxid:Modified Zxid,表示当前 znode 最后一次被修改时的事务 ID。
  • mtime:Modified Time,表示当前 znode 最后一次被修改时的时间。
  • pZxid:表示当前 znode 的子节点列表最后一次被修改时的事务 ID。注意,只能是其子节点列表变更了才会引起 pZxid 的变更,子节点内容的修改不会影响 pZxid。
  • cversion:Children Version,表示子节点的版本号。该版本号用于充当乐观锁。
  • dataVersion:表示当前 znode 数据的版本号。该版本号用于充当乐观锁。
  • aclVersion:表示当前 znode 的权限 ACL 的版本号。该版本号用于充当乐观锁。
  • ephemeralOwner:如果节点是持久节点值为0,如果是临时节点值就为临时节点的会话sessionid。当会话消失后,会根据sessionid来删除这个临时节点。
  • dataLength:当前 znode 中存放的数据的长度。
  • numChildren:当前 znode 所包含的子节点的个数。

会话

会话是 zk 中最重要的概念之一,客户端与服务端之间的任何交互操作都与会话相关。ZooKeeper 客户端启动时,首先会与 zk 服务器建立一个 TCP 长连接。连接一旦建立,客户端会话的生命周期也就开始了。

会话状态

常见的会话状态有三种:

CONNECTING:连接中。Client 要创建一个连接,其首先会在本地创建一个 zk 对象,用于表示其所连接上的Server。从zk对象被创建开始,会话状态就进入到了CONNECTING,同时 Client 会从 Server 服务列表中通过轮询方式逐个尝试连接,直到连接成功。注意,在轮询之前,其首先会将列表进行随机打散,然后再在打散的列表基础上进行轮询。

CONNECTED:已连接。连接成功后,该连接的各种临时性数据会被初始化到 zk 对象中。

CLOSED:已关闭。连接关闭后,这个代表 Server 的 zk 对象会被删除。

会话连接超时管理—客户端维护

我们这里的会话连接超时管理指的是,客户端所发起的服务端连接时间记录,是从客户端当前会话第一次发起服务端连接的时间开始计时。

会话空闲超时管理—服务端维护

服务器为每一个客户端的会话都记录着上一次交互后空闲的时长,及从上一次交互结束开始会话空闲超时的时间点。一旦空闲时长超时,服务端就会将该会话的 SessionId 从服务端清除。这也就是为什么客户端在空闲时需要定时向服务端发送心跳,就是为了维护这个会话长连接的。服务器是通过空闲超时管理来判断会话是否发生中断的。

服务端对于会话空闲超时管理,采用了一种特殊的方式——分桶策略。

  1. 分桶策略

分桶策略是指,将空闲超时时间相近的会话放到同一个桶中来进行管理,以减少管理的复杂度。在检查超时时,只需要检查桶中剩下的会话即可,因为没有超时的会话已经被移出了桶,而桶中存在的会话就是超时的会话。

zk 对于会话空闲的超时管理并非是精确的管理,即并非是一超时马上就执行相关的超时操作。


zookeeper添加用户认证后kafka连接不上_客户端_02


  1. 分桶依据

java //(当前时间 / 会话超时默认时间 + 1) * 会话默认超时时间 BucketTime = (ExpirationTime/ExpirationInterval + 1) * ExpirationInterval

  1. touchSession()*调用关系*


zookeeper添加用户认证后kafka连接不上_数据_03


源码阅读

客户端连接ZK服务端时


zookeeper添加用户认证后kafka连接不上_子节点_04


zookeeper添加用户认证后kafka连接不上_客户端_05


zookeeper添加用户认证后kafka连接不上_子节点_06


zookeeper添加用户认证后kafka连接不上_客户端_07


zookeeper添加用户认证后kafka连接不上_子节点_08


zookeeper添加用户认证后kafka连接不上_子节点_09


处理客户端连接请求


zookeeper添加用户认证后kafka连接不上_zookeeper 认证_10


zookeeper添加用户认证后kafka连接不上_子节点_11


zookeeper添加用户认证后kafka连接不上_客户端_12


zookeeper添加用户认证后kafka连接不上_数据_13


zookeeper添加用户认证后kafka连接不上_子节点_14


如果不是重连情况,那就创建新会话


zookeeper添加用户认证后kafka连接不上_客户端_15


删除会话


zookeeper添加用户认证后kafka连接不上_客户端_16


会话连接事件

客户端与服务端的长连接失效后,客户端将进行重连。在重连过程中客户端会产生三种会话连接事件:

CONNECTION_LOSS:连接丢失。因为网络抖动等原因导致客户端长时间收不到服务端的心跳回复,客户端就会引发“连接丢失事件”。该事件会触发当前客 户端重连服务端,直到重连成功,或重连超时。

SESSION_MOVED:会话转移。当发生“连接丢失事件”后,若客户端在连接超时时限内重连服务端成功,此时当前会话的 id 是没有发生变化的。若服务器检 测到同一个SessionId 的会话,两次连接到的不是同一个 zk 主机,那么服务端就会引发“会话转移异常”,客户端会引发“会话转移事 件”。该事件会触发当前客户端使用第二次连接上的主机的 IP 来与 Server 进行交互。

SESSION_EXPIRED:会话失效。若服务端发现某客户端的会话空闲时间超时,那么服务器就会将该客户端会话进行清除。对于客户端来说,其长时间没有 收到服务端的心跳回复,则会引发“连接丢失事件”,然后进行重连,直到连接成功或超时。但在会话已经从服务端清除完毕,而重连又 未超时的一个很短暂的时间缝隙中,客户端与服务连接成功了。此时客户端就会引发“会话失效事件”。该事件会触发客户端取消该连 接,并使客户端重新实例化 zk 对象,即重新使用新的 SessionId 进行重连。

ACL

ACL 简介

ACL 全称为 Access Control List(访问控制列表),是一种细粒度的权限管理策略,可以针对任意用户与组进行细粒度的权限控制。zk 利用 ACL 控制 znode 节点的访问权限,如节点数据读写、节点创建、节点删除、读取子节点列表、设置节点权限等。

UGO,User、Group、Others,是一种粗粒度的权限管理策略。

zk 的 ACL 维度

Zookeeper 的 ACL 分为三个维度:

  • 授权策略 scheme(采用何种方式授权)
  • world:默认方式,相当于全部都能访问
  • auth:代表已经认证通过的用户(cli中可以通过addauth digest user:pwd 来添加当前上下文中的授权用户)
  • digest:即用户名:密码这种方式认证,这也是业务系统中最常用的。用 username:password 字符串来产生一个MD5串,然后该串被用来作为ACL ID。认证是通过明文发送username:password 来进行的,当用在ACL时,表达式为username:base64
  • ip:使用客户端的主机IP作为ACL ID 。这个ACL表达式的格式为addr/bits
  • 授权对象 ID(给谁授予权限)
  • 授权对象ID是指,权限赋予的用户或者一个实体,例如:IP 地址或者机器。授权模式 schema 与 授权对象 ID 之间
  • 用户权限 permission(授予什么权限)

CREATE、READ、WRITE、DELETE、ADMIN 也就是 增、删、改、查、管理权限,这5种权限简写为crwda

  1. CREATE: c 可以创建子节点
  2. DELETE: d 可以删除子节点(仅下一级节点)
  3. READ:
  4. WRITE:
  5. ADMIN:

子 znode 不会继承父 znode 的权限。

Watcher 机制

zk 通过 Watcher 机制实现了发布/订阅模式

watcher 工作原理


zookeeper添加用户认证后kafka连接不上_子节点_17


watcher 事件

对于同一个事件类型,在不同的通知状态中代表的含义是不同的。


zookeeper添加用户认证后kafka连接不上_客户端_18


watcher 特性

zk 的 watcher 机制具有以下几个特性

  • 一次性:一旦一个 watcher 被触发,zk 就会将其从客户端的 WatcherManager 中删除,服务端中也会删除该 watcher。zk 的 watcher 机制不适合监听变化非常频繁的场景。
  • 串行性:对同一个节点的相同事件类型的 watcher 回调方法的执行是串行的。
  • 轻量级:真正传递给 Server 的是一个简易版的 watcher。回调逻辑存放在客户端,没有在服务端。

客户端操作

下载zk客户端工具

https://issues.apache.org/jira/secure/attachment/12436620/ZooInspector.zip

连接上zk服务


zookeeper添加用户认证后kafka连接不上_数据_19


新建ZK节点


zookeeper添加用户认证后kafka连接不上_客户端_20


新建节点默认为持久节点


zookeeper添加用户认证后kafka连接不上_数据_21


ZKClient 客户端

ZkClient 是一个开源客户端,在 Zookeeper 原生 API 接口的基础上进行了包装,更便于开发人员使用。内部实现了 Session 超时重连,Watcher 反复注册等功能。像 dubbo 等框架对其也进行了集成使用。

API 介绍

创建会话


zookeeper添加用户认证后kafka连接不上_子节点_22


查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:

| 参数名 | 作用 | | :-------------------: | :----------------------------------------------------------: | | zkServers | 指定 zk 服务器列表,由英文状态逗号分开的 host:port 字符串组成 | | connectionTimeout | 设置连接创建超时时间,单位毫秒。在此时间内无法创建与 zk 的连接,则直接放弃连接,并抛出异常 | | sessionTimeout | 设置会话超时时间,单位毫秒 | | zkSerializer | 为会话指定序列化器。zk 节点内容仅支持字节数组(byte[])类型,且 zk 不负责序列化。在创建 zkClient 时需要指定所要使用的序列化器,例如 Hessian 或 Kryo。默认使用 Java 自带的序列化方式进行对象的序列化。当为会话指定了序列化器后,客户端在进行读写操作时就会自动进行序列化与反序列化。 | | connection | IZkConnection 接口对象,是对 zk 原生 API 的最直接包装,是和 zk最直接的交互层,包含了增删改查等一系列方法。该接口最常用的实现类是 zkClient 默认的实现类 ZkConnection,其可以完成绝大部分的业务需求。 | | operationRetryTimeout | 设置重试超时时间,单位毫秒 |

创建节点


zookeeper添加用户认证后kafka连接不上_zookeeper 认证_23


| 参数名 | 作用 | | :-----------: | :----------------------------------------------------------: | | path | 要创建的节点完整路径 | | data | 节点的初始数据内容,可以传入 Object 类型及 null。zk 原生 API中只允许向节点传入 byte[]数据作为数据内容,但 zkClient 中具有自定义序列化器,所以可以传入各种类型对象。 | | mode | 节点类型,CreateMode 枚举常量,常用的有四种类型。PERSISTENT:持久型。PERSISTENT_SEQUENTIAL:持久顺序型。EPHEMERAL:临时型。EPHEMERAL_SEQUENTIAL:临时顺序型 | | acl | 节点的 ACL 策略 | | callback | 回调接口 | | context | 执行回调时可以使用的上下文对象 | | createParents | 是否级递归创建节点。zk 原生 API 中要创建的节点路径必须存在,即要创建子节点,父节点必须存在。但 zkClient 解决了这个问题,可以做递归节点创建。没有父节点,可以先自动创建了父节点,然后再在其下创建子节点。 |

删除节点


zookeeper添加用户认证后kafka连接不上_数据_24


| 参数名 | 作用 | | :-----: | :--------------------------: | | path | 要删除的节点的完整路径 | | version | 要删除的节点中包含的数据版本 |

更新数据


zookeeper添加用户认证后kafka连接不上_数据_25


| 参数名 | 意义 | | :-------------: | :--------------------------: | | path | 要更新的节点的完整路径 | | data | 要采用的新的数据值 | | expectedVersion | 数据更新后要采用的数据版本号 |

检测节点是否存在


zookeeper添加用户认证后kafka连接不上_客户端_26


| 参数名 | 作用 | | :----: | :-----------------------------------------------: | | path | 要判断存在性节点的完整路径 | | watch | 要判断存在性节点及其子孙节点是否具有 watcher 监听 |

获取节点数据内容


zookeeper添加用户认证后kafka连接不上_客户端_27


| 参数名 | 作用 | | :-----------------------: | :----------------------------------------------------------: | | path | 要读取数据内容的节点的完整路径 | | watch | 指定节点及其子孙节点是否具有 watcher 监听 | | returnNullIfPathNotExists | 这是个 boolean 值。默认情况下若指定的节点不存在,则会抛出 KeeperException$NoNodeException为 true,若指定节点不存在,则直接返回null 而不再抛出异常 | | stat | 指定当前节点的状态信息。不过,执行过后该 stat 值会被最新获取到的 stat 值给替换。 |

获取子节点列表


zookeeper添加用户认证后kafka连接不上_子节点_28


| 参数名 | 意义 | | :----: | :-----------------------------------------------------: | | path | 要获取子节点列表的节点的完整路径 | | watch | 要获取子节点列表的节点及其子孙节点是否具有 watcher 监听 |

watcher 注册

ZkClient 采用 Listener 来实现 Watcher 监听。客户端可以通过注册相关监听器来实现对zk 服务端事件的订阅。

可以通过 subscribeXxx()方法实现 watcher 注册,即相关事件订阅;通过 unsubscribeXxx()方法取消相关事件的订阅。


zookeeper添加用户认证后kafka连接不上_子节点_29


| 参数名 | 意义 | | :--------------: | :----------------------------------------------------------: | | path | 要操作节点的完整路径 | | IZkChildListener | 子节点数量变化监听器 | | IZkDataListener | 数据内容变化监听器 | | IZkStateListener | 客户端与zk的会话连接状态变化监听器,可以监听新会话的创建、会话创建出错、连接状态改变。连接状态是系统定义好的枚举类型 Event.KeeperState 的常量 |

代码演示

依赖


<!--zkClient依赖-->
<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.10</version>
</dependency>


代码


import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.serialize.SerializableSerializer;
import org.apache.zookeeper.CreateMode;

public class ZKClientTest {
    // 指定zk集群
    private static final String CLUSTER = "zkOS1:2181,zkOS2:2181,zkOS3:2181,zkOS4:2181";
    // private static final String CLUSTER = "zkOS:2181";
    // 指定节点名称
    private static final String PATH = "/mylogtest";

    public static void main(String[] args) {
        // ---------------- 创建会话 -----------
        // 创建zkClient
        ZkClient zkClient = new ZkClient(CLUSTER);
        // 为zkClient指定序列化器
        zkClient.setZkSerializer(new SerializableSerializer());

        // ---------------- 创建节点 -----------
        // 指定创建持久节点
        CreateMode mode = CreateMode.PERSISTENT;
        // 指定节点数据内容
        String data = "first log";
        // 创建节点
        String nodeName = zkClient.create(PATH, data, mode);
        System.out.println("新创建的节点名称为:" + nodeName);

        // ---------------- 获取数据内容 -----------
        Object readData = zkClient.readData(PATH);
        System.out.println("节点的数据内容为:" + readData);

        // ---------------- 注册watcher -----------
        zkClient.subscribeDataChanges(PATH, new IZkDataListener() {
            @Override
            public void handleDataChange(String dataPath, Object data) throws Exception {
                System.out.print("节点" + dataPath);
                System.out.println("的数据已经更新为了" + data);
            }

            @Override
            public void handleDataDeleted(String dataPath) throws Exception {
                System.out.println(dataPath + "的数据内容被删除");
            }
        });

        // ---------------- 更新数据内容 -----------
        zkClient.writeData(PATH, "second log");
        String updatedData = zkClient.readData(PATH);
        System.out.println("更新过的数据内容为:" + updatedData);

        // ---------------- 删除节点 -----------
        zkClient.delete(PATH);

        // ---------------- 判断节点存在性 -----------
        boolean isExists = zkClient.exists(PATH);
        System.out.println(PATH + "节点仍存在吗?" + isExists);
    }


ZK连接是的shuffle源码阅读


zookeeper添加用户认证后kafka连接不上_zookeeper 认证_30



zookeeper添加用户认证后kafka连接不上_客户端_31


zookeeper添加用户认证后kafka连接不上_子节点_32


zookeeper添加用户认证后kafka连接不上_zookeeper 认证_33


zookeeper添加用户认证后kafka连接不上_zookeeper 认证_34


zookeeper添加用户认证后kafka连接不上_数据_35


zookeeper添加用户认证后kafka连接不上_数据_36


zookeeper添加用户认证后kafka连接不上_zookeeper 认证_37


zookeeper添加用户认证后kafka连接不上_客户端_38


总结

  • 【Q-01】一个 zk 客户端到底连接的是 zk 列表中的哪台 Server?请谈一下你的看法。

答:

  • 首先,在连接服务的时候会把连接zk的地址解析成一个ArrayList列表。
  • 解析成列表后会使用Collections.shuffle(resultList)将解除好的列表打撒(随机排序里面的数据)-》此时的数据是:主机名+端口
  • 然后再把列表里面的数据依次解析成IP+端口,解析完后会返回一个列表,然后再次将这个列表打散一次。
  • 服务节点连接zk的时候就拿第二次打散列表里面的第一条数据。
  • 【Q-02】(追问)为什么要打散?请谈一下你的看法。

答:

如果不打散,所有的服务节点都总是会连接到第一台zk上,这会导致第一台部署zk的服务器压力过大。

  • 【Q-03】(再追问,看来是打破砂锅了 )zk 客户端指定的要连接的 zk 集群地址,会被 shuffle几次?请谈一下你的看法。

答:

如果连接字符串是主机名:会被shuffle一次,如果是ip:会再次被shuffle

第一次shuffle的是主机名+端口,第二次shuffle是解析主机名后获取到主机的IP,IP+端口

第一次shuffle:所有的服务节点都总是会连接到第一台zk上,这会导致第一台部署zk的服务器压力过大。

第二次shuffle:解析完主机名的IP+端口。一个主机可以映射出多个虚拟IP。所以再次打散,避免单台服务器压力过大。

  • 【Q-04】第一个客户端将 zk 列表打散后,在打散的列表上采取轮询方式尝试连接。那么,第二个客户端又来连接 zk 集群,其是在前面打散的基础上采用轮询方 式选择 Server,还是又重新打散后再进行轮询连接尝试?

答:

肯定是又重新打散,因为不同的客户端去连接zk时,会重新执行连接zk的代码,所以只要代码被执行一次,那么集群的主机又被再一次被打散。

  • 【Q-09】zk 客户端维护着会话超时管理,请谈一下你对此的认识。

答:

在服务节点连接上zk后,zk会初始化一个会话超时时间,该时间是从jvm中获取的(即使改了系统时间也不会受影响),该时间的开始时间就是服务节点连上zk的时间。

  • 【Q-10】zk 时 CP 的,zk 集群在数据同步或 leader 选举时是不对外提供服务的,那岂不是用户体验非常不好?请谈一下你对此的看法

答:

其实也不完全是用户体验不好,在分布式系统中,CAP定理只能遵循CP或者AP。zk遵守CP的优点就在于虽然服务是短暂性的不可用,但是最终的数据是一定正确的。当然其实在执行leader选举也是非常快的。在leader选举的过程中,客户端几乎是感知不到的。

像Eureka的话就是遵循的AP,当集群里面的部分节点挂了之后,是有缓存的,仍然可以提供服务。但是数据正确性就不能保证了。

  • 【Q-11】zk Client 在连接 zk 时会发生连接丢失事件,什么是连接丢失?请谈一下你的认识。

答:

这个会发生在网络不稳定情况下,就是Clinet给Server发送心跳。其实Server是活着的,只不过是短暂的网络不稳定情况导致Server没有接收到Clinet的心跳。而此时Clinet没有收到Server的心跳,则会认为Server端挂了。就导致了连接丢失。

  • 【Q-12】zk Client 在连接 zk 时会发生会话转移事件,什么是会话转移?请谈一下你的认识。

答:

会话转移就是再会话过期时间内。从当时时间计算超时时间,如果算出来的超时时候在另外一个桶,那么当前会话就会从一个桶换到另外一个新桶。

  • 【Q-13】zk Client 在连接 zk 时会发生会话失效事件,什么是会话失效?请谈一下你的认识。

答:

会话失效将会被删除,指的是在指定时间内没有通信过了,然后桶的过时时间也达到边界值,此时客户端还未与客户端成功通信,那么此时桶将会被删除,随之而然的桶中的会话也会被删除。

  • 【Q-14】zk 中的会话空闲超时管理采用的是分桶策略。请谈一下你对分桶策略的认识。

答:


zookeeper添加用户认证后kafka连接不上_客户端_39


  • 【Q-15】zk 中的会话空闲超时管理采用的是分桶策略。什么是会话桶?里面存放的是什么?从源码角度请谈一下你的认识。

答:

简单说就是一个Map集合,将会话的sesssion作为key。会话的超时时间作为value。每次发生会话后,计算出session的超时时间,如果超时时间没有超出当前所在桶的边界值,那么就不需要换桶,如果超过了就需要将该session移动到另外一个过期时间边界值的桶里面。

  • 【Q-16】zk 中的会话空闲超时管理采用的是分桶策略。会话桶中的会话会发生换桶,什么时候会进行换桶?如何换桶呢?从源码角度请谈一下你的认识

答:

  • 当桶的边界值不然容忍当前会话过期时间的时候会进行换桶。
  • 会话进行换桶的时候首先会对session进行校验,看session是否存在,session的过期时间是否小于当前桶(如果当前会话的过期时间超过了桶的边界值,那明显就是错误的。)
  • 校验通过后
  • 把会话从原来的桶中删除
  • 获取新的会话桶ID,如果不存在则创建桶
  • 创建后将会话放入桶中
  • 【Q-17】zk 中的会话空闲超时管理采用的是分桶策略。该分桶策略中会话空闲超时判断发生在哪里?超时发生后的处理发生在哪里?都做了哪些处理呢?请谈一 下你的认识。

答:

其实就是有一个异步线程,run方法里面是个while循环。只要服务端是运行状态,它就是一直循环,它里监听的是桶的边界值是否大于当前时间,如果没有大于那么就会等待(使用了object的wait方法),直到这个桶过期然后把它删除。


zookeeper添加用户认证后kafka连接不上_客户端_16