读《从Paxos到Zookeeper 分布式一致性原理与实践》笔记之系统模型

1.    Zookeeper技术内幕

1.1. 系统模型

1.1.1.  数据模型

        树

zookeeper名字空间由节点znode构成,其组织方式类似文件系统,其中各个节点相当于目录和文件,通过路径作为唯一标识。

事务ID

        在传统数据库中事务具有所谓的ACID特性:即原子性、一致性、隔离性、和持久性。

        在Zookeeper中,事务是指能够改变Zookeeper服务器状态的操作,称为事务操作或者更新操作。一般包括数据节点的创建与删除,数据节点内容更新,和客户端会话创建与失效等操作。对于每一个事务的请求,Zookeeper都会为其分配一个全局唯一的事务ID(ZXID),通常是一个64位的数字。每一个ZXID对应一次更新操作,Zookeeper根据这些全局唯一的ZXID请求来处理更新请求的全局顺序。

1.1.2.  节点特性

节点类型

        节点类型可以分为持久节点(PERSISTENT )、临时节点(EPHEMERAL),以及时序节点(SEQUENTIAL ),具体在节点创建过程中,一般是组合使用,可以生成以下 4 种节点类型。

        持久节点(PERSISTENT)

        所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。

        持久顺序节点(PERSISTENT_SEQUENTIAL)

        在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。

        临时节点(EPHEMERAL)

        和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点,注意是根据Session会话的失效时间来设定的。

        临时顺序节点(EPHEMERAL_SEQUENTIAL)

        临时顺序节点的特性和临时节点一致,同时是在临时节点的基础上,添加了顺序的特性。

状态信息

        在Zookeeper维护数据节点的同时,每个节点除了存储数据内容之外,还存储了数据节点本身一些状态信息,

[zk: 127.0.0.1:2181(CONNECTED) 1] get /zookeeper/test

/zookeeper/test

cZxid = 0x600000014

ctime = Sat Nov 05 16:56:53 CST 2016

mZxid = 0x600000014

mtime = Sat Nov 05 16:56:53 CST 2016

pZxid = 0x600000014

cversion = 0

dataVersion = 0

aclVersion = 0

ephemeralOwner = 0x0

dataLength = 15

numChildren = 0

状态信息解释:

czxid:节点创建时的事物ID

mzxid:节点最新一次更新时的事物ID

ctime:节点创建时的时间戳

mtime:节点最新一次更新发生时的时间戳

pZxid:该节点的子节点列表最后一次被修改时的事物ID,如果没有子节点,则为当前节点的cZxid

cversion:其子节点的更新次数

dataVersion:节点数据的更新次数

aclVersion:节点ACL(授权信息)的更新次数

ephemeralOwner:创建临时节点的会话的sessionID. 如果该节点是持久节点,该值为0

dataLength:节点数据的字节数

numChildren:当前节点子节点个数

1.1.3.  版本

        每个数据节点都具有三种类型的版本信息, 对数据节点的任何更新操作都会引起版本号的变化。

版本类型

说明

dataVersion

当前数据节点数据内容的版本号

cversion

当前数据节点子节点的版本号

aversion

当前数据节点ACL变更版本号

        dataVersion是表示对数据节点数据内容的变更次数,强调的是变更次数,因此就算数据内容的值没有发生变化,version的值也会递增。

        版本用来干什么?

        乐观锁控制的事务分成如下三个阶段:数据读取、写入校验和数裾写入。在写入校验阶段.事务会检查数据在读取阶段后是否有其他事物对数据进行过更新,以确保数裾更新的一致性,那么,如何来进行写人校验呢?我们首先可以来看JDK中最典型的乐观锁实现——CAS。简单地讲就是“对于值V,毎次更新前都会比对其值是否是预期值A,只有符合预期,才会将V原子化地更 新到新值B“,其中是否符合预期便是乐观锁中的“写入校验”阶段。

        在ZooKeeper中, version属性正是用来实现乐观锁机制中的“写人校验”的。在这里我们看看ZooKeeper的内部实现,在ZooKeeper服务器的PrepRequestProcessor处理器类中,在处理毎一个数椐更新<setDataRequest>请求时,会进行如下所示的版本检查。

private static int checkAndIncVersion(int currentVersion, int expectedVersion, String path)
        throws KeeperException.BadVersionException {
    if (expectedVersion != -1 && expectedVersion != currentVersion) {
        throw new KeeperException.BadVersionException(path);
    }
    return currentVersion + 1;
}

        在进行一次修改数据的请求处理时,首先进行了版本检查:Zookeeper会从请求中获取到当前请求的版本expectedVersion,同时从内存的数据记录中获取到当前服务器上该数据的最新版本currentVersion,如果expectedVersion为“-1”,那么说明客户端并不要求使用乐观锁,可以忽略版本比对,如果 version不是“-1”,那么就比对expectedVersion和currentVersion,如果两个版本不匹配,那么将会抛出BadVersionException异常。

1.1.4. Watcher机制

        Zookeeper的Watcher机制主要包括客户端线程、客户端WatchManager和Zookeeper服务器三部分。


        在具体的流程上,客户端向Zookeeper服务器注册Watcher事件监听的同时,会将Watcher对象存储在 客户端WatchManager中。当Zookeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象执行回调逻辑。

Watcher接口

        Watcher接口类用于表示一个标准的事件处理器,定义事件通知的相关逻辑,包含KeeperState和EventType两个枚举,分别代表通知状态和事件类型,同时定义事件的回调方法:process方法。

public interface Watcher {
  
    public interface Event {
        public enum KeeperState {
            Unknown (-1),
            NoSyncConnected (1),
            SyncConnected (3),
            AuthFailed (4),
            ConnectedReadOnly (5),
            SaslAuthenticated(6),
            Expired (-112);
            private final int intValue;
  
            KeeperState(int intValue) {
                this.intValue = intValue;
            }
  
            public int getIntValue() {
                return intValue;
            }
        }
  
        public enum EventType {
            None (-1),
            NodeCreated (1),
            NodeDeleted (2),
            NodeDataChanged (3),
            NodeChildrenChanged (4),
            DataWatchRemoved (5),
            ChildWatchRemoved (6);
            private final int intValue;     // Integer representation of value

            EventType(int intValue) {
                this.intValue = intValue;
            }
  
            public int getIntValue() {
                return intValue;
            }    
        }
    }
    abstract public void process(WatchedEvent event);
}

Watcher事件

        同一个事件类型在不同的通知状态中代表的含义有所不同。如下常见的通知状态和事件类型。


回调方法process()

        process方法是 Watcher 接口中的一个回调方法,当 ZooKeeper 向客户端发送一个 Watcher 事件通知时,客户端就会对相应的 process 方法进行回调,从而实现对事件的处理。

        process 方法包含 WatcherEvent 类型的参数,WatcherEvent包含每一事件的三种基本属性:通知状态、事件类型、节点路径。Zookeeper使用Watcher对象来封装服务器端事件,并传递给Watcher,从而方便回调方法process对服务器事件进行处理。

public class WatchedEvent {
    final private KeeperState keeperState;
    final private EventType eventType;
    private String path;
}
        注:ZookeeperWatcher的一个重要特性:客户端无法直接从WatchedEvent事件中获取到对应数据节点的原始数据内容,以及变更后的数据内容,而是客户端再次主动去重新获取数据。

事件注册


事件响应

 

一次性Watcher几个特性

        无论是服务端还是客户端,一旦一个 Watcher 被触发,ZooKeeper 都会将其从相应的存储中移除。因此,开发人员在 Watcher 的使用上要记住的一点是需要反复注册。这样的设计有效地减轻了服务端的压力。如果注册一个 Watcher 之后一直有效,那么针对那些更新非常频繁的节点,服务端会不断地向客户端发送事件通知,这无论对于网络还是服务端性能的影响都非常大。

客户端串行执行

        客户端Watcher 回调的过程是一个串行同步的过程,这为我们保证了顺序,同时,需要开发人员注意的一点是,千万不要因为一个 Watcher 的处理逻辑影响了整个客户端的 Watcher 回调。

轻量

        WatchedEvent 是 ZooKeeper 整个 Watcher 通知机制的最小通知单元,这个数据结构中只包含三部分的内容:通知状态、事件类型和节点路径。也就是说,Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。另外,客户端向服务端注册 Watcher 的时候,并不会把客户端真实的 Watcher 对象传递到服务端,仅仅只是在客户端请求中使用 boolean 类型属性进行了标记,同时服务端也仅仅只是保存了当前连接的ServerCnxn 对象。这样轻量级的 Watcher 机制设计,在网络开销和服务端内存开销上都是非常廉价的。

1.1.5. ACL权限控制

        Zookeeper提供一套ACL权限控制机制来保证数据的安全。

        从三个方面来理解ACL机制,分别是:权限模式(Scheme)、授权对象(ID)和权限 (Permission),通常使用 “scheme:id:permission”来标识一个有效的 ACL信息。

权限模式:Scheme

        权限模式用来确定权限验证过程中使用的检验策略。在ZooKeeper中,开发人员使用最多的就是以下四种权限模式。

IP

        通过IP地址粒度来进行权限控制,例如配置了“ip:192.168.0.110”,即表示权限控制都针对这IP地址的。同时IP模式也支持按照网段的方式进行配置, 例如“ip:192.168.0.1/24”表示针对192.168.0.*这个网段进行权限控制。

Digest

        Digest是最常用的权限控制模式,也史符合我们对于权限控制的认识,其以类似于“username:password”形式的权限标识来进行权限配置,便于区分不同应用来进行权限控制。

World

        World是一种最开放的权限控制模式,从其名字中也以看出,事实上这种权限控制方式几乎没有任何作用,数据节点的访问权限对所存用户开放,即所有用户都可以在不进行任何权限校验的情况下操作ZooKeeper的数据。另外,World模式也可以看作一种特殊的Digest模式,它只有一个权限标识,即“world:anyone”。

Super

        Super模式,顾名思义就是超级用户的意思,也是—种特殊的Digest模式。在Super模式下,超级用户可以对任意ZooKeeper上的数据节点进行任何操作。

授权对象:ID

        权限赋予的用户或一个指定的实体,例如IP地址或是机器等。在不同的权限模式下,授权对象是不同的。

权限模式

授权对象

IP

通常是一个IP地址或是IP段,例如“192.168.0.123”或“192.168.0.1/24”

Digest

自定义,通常是

username:BASE64(SHA-l(usemame:password))”,

例如”foo: kWN6aNSbjcKWPqjiV7cgON24raU=”

World

只有一个ID:“anyone”

Super

与Digest模式一致

    权限

        权限指的是那些通过权限检查后可以被允许执行的操作。数据权限分为五大类:

        CREATE (C):数据节点的创建权限,允许授权对象在该数据节点下创建子节点。

        DELETE (D):子节点的刪除权限,允许授权对象刪除该数椐节点的子节点。

        READ (K):数据节点的读取权限,允许授权对象访问该数据节点并读取其数据内容或子节点列表等。

        WRITE (W):数据节点的更新权限,允许授权对象对该数据节点进行更新操作。

        ADMIN (A):数椐节点的管理权限,允许授权对象对该数据节点进行ACL相关的设置操作,

权限扩展

        启动Zookeeper的时候,通过-Dzookeeper.authProvider.$n=$classname的方式,$n只要保证不重复即可,没有实际的意义,通常用数字1,2,3等

ACL管理

        略过

数据结构

DataTree中的数据节点
DataNode{
      byte data[];
    Long acl;
    public StatPersisted stat;
    private Set<String> children = null;
}
DataTree{
      Map<Long, List<ACL>> longKeyMap;
      ConcurrentHashMap<String, DataNode> nodes
}

例子

public class ZookeeperTest {
  
    public static void main(String[] args) {
        try {
            String lockPath = "/zookeeper/test5";
            ZookeeperTest test = new ZookeeperTest();
            test.create(lockPath);
            test.setData(lockPath);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  
    private void setData(String lockPath) throws IOException, KeeperException, InterruptedException {
        ZooKeeper zk1 = new ZooKeeper("192.168.99.171:2181", 100000, new Watcher() {
            // 监控所有被触发的事件
            public void process(WatchedEvent event) {
                System.out.println("已经触发了" + event.getType() + "事件!事件名字:" + event.getState().name());
            }
        });
        zk1.addAuthInfo("digest", "lihyRead:passRead".getBytes());
        System.out.println("修改前,读取数据:"+new String(zk1.getData(lockPath,false,null)));
        zk1.addAuthInfo("digest", "lihyWrite:passWrite".getBytes());
        zk1.setData(lockPath,"createContent_1".getBytes(),-1);
        System.out.println("修改后,读取数据:"+new String(zk1.getData(lockPath,false,null)));
    }
  
    private void create(String path) throws IOException, KeeperException, InterruptedException, NoSuchAlgorithmException {
        ZooKeeper zk = new ZooKeeper("192.168.99.171:2181", 100000, new Watcher() {
            // 监控所有被触发的事件
            public void process(WatchedEvent event) {
                System.out.println("已经触发了" + event.getType() + "事件!事件名字:" + event.getState().name());
            }
        });
        ArrayList<ACL> acls = new ArrayList<ACL>();
        ACL writeAcl = new ACL(ZooDefs.Perms.WRITE,new Id("digest",DigestAuthenticationProvider.generateDigest("lihyWrite:passWrite")));
        ACL readAcl = new ACL(ZooDefs.Perms.READ,new Id("digest",DigestAuthenticationProvider.generateDigest("lihyRead:passRead")));
        acls.add(writeAcl);
        acls.add(readAcl);
        String seq = zk.create(path, "createContent".getBytes(), acls, CreateMode.PERSISTENT);
    }
}

猜你喜欢

转载自blog.csdn.net/lihuayong/article/details/53704197