Apache curator-recipes代码实例

    Apache curator-recipes组件提供了大量已经"生产化"(produced)的特性,极大的简化了使用zk的复杂度.

    1. Cache: 提供了对一个Node持续监听,如果节点数据变更,即可立即得到响应. 开发者无需过度的关注watcher和Event操作.

    2. Queues: 提供了重量级的分布式队列解决方案,比如:权重队列,可延迟队列等.其实Zookeeper并不适合作为数据存储系统,你可以适度的使用它来达成分布式队列的设计要求.

    3. Counters: 全局计数器是分布式设计中很常用的,包括"全局计算器"和"原子自增计数器".

    4. Locks: 分布式锁的设计有很多手段,此组件提供了分布式"读写分写锁"/"共享锁"等.在我们需要控制资源访问的情况下,非常有用.

    5. Barries: 栅栏,需要对分布式环境中,多个操作(进程)进行同步或者协同时,可以考虑使用barries;

    6. Elections: 选举;可以在多个"注册者"之间选举出leader,作为操作调度/任务监控/队列消费的执行者,我们在设计"leader角色选举"/"单点任务执行""分布式队列消费者"等场景时,非常有效.

 

    代码实例

1. 创建Client

CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder();
//fluent style
String namespace = "cluster-worker";
CuratorFramework client = builder.connectString("127.0.0.1:2181")
		.sessionTimeoutMs(30000)
		.connectionTimeoutMs(30000)
		.canBeReadOnly(false)
		.retryPolicy(new ExponentialBackoffRetry(1000, Integer.MAX_VALUE))
		.namespace(namespace)
		.defaultData(null)
		.build();
client.start();
EnsurePath ensure = client.newNamespaceAwareEnsurePath(namespace);
//code for test
Thread.sleep(5000);
//client.close()//

 

2. Caches

    持续watcher节点,并将节点的数据变更即时的在本地反应出来.recpise提供了PathChildrenCache和NodeCache两个API.

public static PathChildrenCache pathChildrenCache(CuratorFramework client, String path, Boolean cacheData) throws Exception {
	final PathChildrenCache cached = new PathChildrenCache(client, path, cacheData);
	cached.getListenable().addListener(new PathChildrenCacheListener() {
		@Override
		public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
			PathChildrenCacheEvent.Type eventType = event.getType();
			switch (eventType) {
				case CONNECTION_RECONNECTED:
					cached.rebuild();
					break;
				case CONNECTION_SUSPENDED:
				case CONNECTION_LOST:
					System.out.println("Connection error,waiting...");
					break;
				default:
					System.out.println("Data:" + event.getData().toString());
			}
		}
	});
	return cached;
}

 

PathChildrenCache cached = pathChildrenCache(client,path,true);
//= start() + rebuild()
//事件操作,将会在额外的线程中执行.
cached.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
List<ChildData> childData = cached.getCurrentData();
if (childData != null) {
	for (ChildData data : childData) {
		System.out.println("Path:" + data.getPath() + ",data" + new String(data.getData(), "utf-8"));
	}
}

//当不在需要关注此节点数据时,需要及时的关闭它.
//因为每个cached,都会额外的消耗一个线程.
//cached.close();////close the watcher,clear the cached Data

    对于PathChildrenCache.getCurrentData()将从获取本地的数据列表,而不是触发一次zookeeper.getChildren(),因此为"Cache".

3. Queues:分布式队列

    分布式队列的基本特性,就是"生产者"或"消费者"跨越多个进程,且在此种环境中需要确保队列的push/poll的有序性.

    zookeeper本身并没有提供分布式队列的实现,只是recipse根据zookeeper的watcher和具有version标记的node,来间接的实现分布式queue..内部机制如下:

    --> 如果是消费者(QueueConsumer),会创建一个类似于PathChildrenCache的实例用于监听queuePath下的子节点变更事件(单独的线程中).同时consumer处于阻塞状态,当有子节点变更事件时会被唤醒(包括创建子节点/删除子节点等);

    --> 此时consumer获取子节点列表,并将每个节点信息封装成Runnable任务单元,提交到线程池中. 

    --> Runnable中并执行QueueConsumer.consumer(.)方法.

    --> 如果是生产者,则发布一个message时recipes将会在queuePath下创建一个PERSISTENT_SEQUENTIAL节点,同时保存message数据. 消费时,也将按照节点的顺序进行.

 

    发布消息并没有太多的问题仅仅是创建一个"有序"节点即可..但是对于消费者,那么需要考虑的因数就很多,比如:1) 多个消费者同时消费时,需要确保消息不能重复且有序 2) 消息消费时,如果网络异常,怎么办?

    对于QistributedQueue中,对上述问题的解决办法也非常粗糙,内部机制如下:

    --> 如果使用了消费担保(即指定了lockPath),在调用consumer方法之前,首先创建一个临时节点(lockPath + 子节点),如果创建此临时节点失败也就意味着此消息被其他消费者,则忽略此消息.

    --> 然后从子节点中获取数据,如果获取失败,意味着此节点已经被其他消费者删除,则忽略此消息.

    --> 然后调用consumer()方法,如果此方法抛出异常,消息将会再次添加到队列中(删除旧的子节点,创建一个新的子节点).如果消费正常,则删除节点.

    --> 无论成败,则删除临时节点(lockPath + 子节点)

 

    --> 如果没有使用消费担保,则首先获取子节点的数据(getData),然后立即删除此子节点

    -->调用consumer()方法.

 

    问题:

    1) 源代码中,在获取子节点的data时(getData)并没有指定version校验,我深度怀疑,当消息被并发的消费时,是否有重复的可能.

    2) 因为zookeeper本身获取一个节点的子节点列表时,将得到所有的子节点,那么就意味任何一个消费者中每次Event触发,都将获取整个childrenList,如果此列表很庞大,性能问题将是非常突出的.

    3) 在消费担保的情况下,每消费一个消息,就会做"创建临时节点""删除临时节点""获取数据"等大量工作,如果有多个消费者同时运行,那么对zk的操作次数将会倍数级增加,性能问题以及数据安全性问题,也是非常值得考虑的.

    4) zookeeper已经限定了每个节点的数据尺寸,以及每个节点下子节点的个数,这对于实现规模性的分布式队列,确实不是良好的选择.

    5) 当队列中,消费者和生产者的速率不均衡时,问题将会更加严重,比如:快速的生产者 + 慢速的消费者;因为此时Event事件将会非常频繁(网络消耗严重),对于消费者而言,线程上线切换锁带来的性能消耗,不可忽视.

 

    最终,我们需要明确使用zookeeper作为分布式队列的场景: 1) 队列深度较小 2) 生产者和消费者的速度都非常的低且消费者消费速度更快,即单位时间内产生的消息很少.  3) 建议只有一个消费者.

    比如: 一个数据分析系统,这个系统中有多个定时任务,当任务即将触发时,向此分布式队列中提交一个消息,消息中包含任务的ID,那么谁消费了此任务ID,那么谁就负责执行此任务.我们间接的实现了分布式任务单点运行的需求.

    还有一种场景,比如实现"排它重入锁"也可以使用DistributedQueue作为底层支撑.

 

    消费者:DistributedQueueConusumer.java

public class DistributedQueueConsumer {

    private DistributedQueue<String> queue;

    public DistributedQueueConsumer(CuratorFramework client, String queuePath) throws Exception {
        QueueBuilder<String> builder = QueueBuilder.builder(client, new StringQueueConsumer(), new StringQueueSerializer(), queuePath);
        queue = builder.lockPath("queue-lock").buildQueue();//消费担保
    }

    public void start() throws Exception {
        queue.start();
    }

    public void close() throws Exception {
        queue.close();
    }

    public static DistributedQueue distributedQueueAsProducer(CuratorFramework client, String path) throws Exception {
        QueueBuilder<String> builder = QueueBuilder.builder(client, null, new StringQueueSerializer(), path);
        builder.maxItems(1024);// 有界队列,最大队列深度,如果深度达到此值,将阻塞"生产者"创建新的节点.
        return builder.buildQueue();
    }

    //utils for producer and consumer
    static class StringQueueSerializer implements QueueSerializer<String> {
        private static final Charset charset = Charset.forName("utf-8");

        //as producer
        @Override
        public byte[] serialize(String item) {
            return item.getBytes(charset);
        }

        //as consumer
        @Override
        public String deserialize(byte[] bytes) {
            return new String(bytes, charset);
        }
    }


    class StringQueueConsumer implements QueueConsumer<String> {
        static final String TAG = "_TAG";
        @Override
        public void consumeMessage(String message) throws Exception {
            System.out.println("Consumer:" + message);
            if (message.equals(TAG)) {
                System.out.println("Tag message...ignore it.");
            }
        }

        @Override
        public void stateChanged(CuratorFramework client, ConnectionState newState) {
            switch (newState) {
                case RECONNECTED:
                    try {
                        //当链接重建之后,需要发送一个TAG消息,用于重新触发本地的watcher,以便获取新的children列表
                        queue.put(TAG);
                    } catch (Exception e) {
                        //
                    }
                    break;
                default:
                    System.out.println(newState.toString());
            }
        }
    }

}

    生产者: DistributedQueueProducer.java

public class DistributedQueueProducer {

    private DistributedQueue<String> queue;

    public DistributedQueueProducer(CuratorFramework client, String queuePath) throws Exception {
        QueueBuilder<String> builder = QueueBuilder.builder(client, null, new StringQueueSerializer(), queuePath);
        queue = builder.lockPath("queue-lock").buildQueue();//消费担保
    }

    public void start() throws Exception {
        queue.start();
    }

    public void close() throws Exception {
        queue.close();
    }

    public void put(String message) throws  Exception{
        queue.put(message);
    }

}

     

    Recipse还提供了其他2个API:

    1) DistributedIdQueue: 内部基于DistributedQueue的所有机制,只是除了指定queue中消息的内容之外,还可以指定一个ID,这个ID作为消息的标记,最终此ID值将作为znode的path后缀.此后可以通过ID去消费(dequeue)一个消息.队列的排序方式是根据ID的字典顺序--正序.

    2) DistributedProrityQueue: 有权重的队列,内部基于DistributedQueue,不过在发布消息时,需要指定此消息的权重数字;队列的排序方式为根据权重排序.

  

    此外DistributedQueue的开发中,必须在QueueConsumer中关注"链接失效"的事件.

 

4. Counters

    计数器,其中SharedCount可以用来监听zookeeper中一个Integer类型的数字变更.此外还有一个更加基础的API: SharedValue,你可以实现任意类型的计数器.

final SharedCount counter = new SharedCount(client,"/counter-vv",0);
counter.addListener(new SharedCountListener(){
	@Override
	public void countHasChanged(SharedCountReader sharedCount, int newCount) throws Exception {
		//
		System.out.println("count changed:" + newCount);
	}

	@Override
	public void stateChanged(CuratorFramework client, ConnectionState newState) {
		switch (newState){
			case RECONNECTED:
				try {
					//当链接重建之后,需要手动fresh
					Integer current = counter.getCount();
					counter.trySetCount(current);//reflush,无论更新成败,都会获取最新的值
				} catch (Exception e) {
					//
				}
				break;
			default:
				System.out.println(newState.toString());
		}
	}
});
counter.start();

//counter.close();//取消watcher

    SharedCount中也需要关注"链接异常"的问题,我们可以通过注册listener的方式,当链接重连成功后,重新获取新的值.

    在"计数器"中,还提供了DistributedAtomicInteger,DistributedAtomicLong两个分布式自增计数器.

DistributedAtomicInteger atomicInteger = new DistributedAtomicInteger(client,"/counter-vv",new RetryNTimes(32,1000));
AtomicValue<Integer> rc = atomicInteger.increment();
System.out.println("success:" + rc.succeeded() + ";before:" + rc.preValue() + ";after:" + rc.postValue());
//atomicInteger.add(1); //++
//atomicInteger.subtract(1); //--

 

5. Locks 

    使用zookeeper作为分布式锁,是一个普遍的需求,如下展示如何设计一个分布式重入锁,其中一个path表示一个锁资源.

public class DistributedLock{

    private InterProcessMutex lock;//重入的,排他的.
    
    private LockHolder _holder;

    private String lockPath;

    private ConnectionStateListener stateListener = new StateListener();
    
    private LockEvent lockEvent;
    
    private Semaphore semaphore = new Semaphore(0);
    private RevocationListener<InterProcessMutex> revocationListener;
    
    public DistributedLock(CuratorFramework client,String path,final LockEvent lockEvent){
        lockPath = path;
        this.lockEvent = lockEvent;
        revocationListener = new RevocationListener<InterProcessMutex>() {
            @Override
            public void revocationRequested(InterProcessMutex forLock) {
                if(!forLock.isAcquiredInThisProcess()){
                    return;
                }
                try{
                	lockEvent.beforeRelease();
                	//只有当前线程才可以释放
                	//其他线程,将会抛出一个错误.
                    forLock.release();
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        };
        lock = createLock(client);
        lock.makeRevocable(revocationListener);
        client.getConnectionStateListenable().addListener(stateListener);
    }
    

    
    public boolean lock(){
        try{
        	if(_holder == null) {
        		_holder = new LockHolder();
            	_holder.setDaemon(true);
        	}
            if(!_holder.isAlive()){
            	_holder.start();
            }
            semaphore.acquire();//直到_holder正常运行
        } catch (Exception e){
            //
        }
        return false;
    }

    public void unlock(){
        if(_holder.isAlive()){
        	_holder.interrupt();
        }
    }
    

    private InterProcessMutex createLock(CuratorFramework client){
        lock = new InterProcessMutex(client,lockPath);
        //协同中断,如果其他线程/进程需要此锁中断时,调用此listener.
        lock.makeRevocable(revocationListener);
        client.getConnectionStateListenable().addListener(stateListener);
        return lock;
    }

    class StateListener implements ConnectionStateListener{
        @Override
        public void stateChanged(CuratorFramework client, ConnectionState newState) {
            switch (newState){
                case LOST:
                    //一旦丢失链接,就意味着zk server端已经删除了锁数据
                    boolean rebuild = lockEvent.lose();
                    _holder.interrupt();//NUll
                    if(rebuild){
                    	lock();
                    }
                    break;
                default:
                    System.out.println(newState.toString());

            }
        }
    }
    
    static interface LockEvent {
    	
    	public void afterAquire();
    	
    	public boolean lose();
    	
    	//释放锁
    	public void beforeRelease();
    }
    
    class LockHolder extends Thread{
		@Override
		public void run() {
			try{
				lock.acquire();
	            semaphore.release();//
	            lockEvent.afterAquire();
	            synchronized (lock) {
					lock.wait();
				}
			}catch(InterruptedException e){
				try{
					lockEvent.beforeRelease();
					lock.release();
				}catch(Exception ie){
					ie.printStackTrace();
				}
			}catch (Exception e) {
				e.printStackTrace();
			} 
			
		}
		
    	
    }
    
    public static void main(String[] args) throws Exception {
        CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder();
        //fluent style
        String namespace = "cluster-worker";
        CuratorFramework client = builder.connectString("127.0.0.1:2181")   //FixedEnsembleProvider
                .sessionTimeoutMs(30000)
                .connectionTimeoutMs(30000)
                .canBeReadOnly(false)   // cant connect to one observer-instance
                .retryPolicy(new ExponentialBackoffRetry(1000, Integer.MAX_VALUE)) //auto reconnect policy
                .namespace(namespace)  //good method,you can specify one fix prefiex-path of all znode.
                .defaultData(null)
                .build();
        client.start();
        EnsurePath ensure = client.newNamespaceAwareEnsurePath(namespace);
        LockEvent le = new LockEvent() {
			
			@Override
			public boolean lose() {
				System.out.println("lose,shoud be waiting or stop workers!");
				return false;
			}
			
			@Override
			public void beforeRelease() {
				System.out.println("Lock would be released!");
				
			}
			
			@Override
			public void afterAquire() {
				System.out.println("Locked success,can be running workers");
				
			}
		};
        DistributedLock distLock = new DistributedLock(client,"/lock",le);
        distLock.lock();
        distLock.unlock();
        Thread.sleep(2000);
        client.close();

    }

}

    底层的机制非常的简单: "获取锁"的操作,就是在zookeeper中创建一个EPHEMERAL_SEQUENTIAL节点,同时对此节点的临近节点注册一个watcher;当"临近节点"被删除时,表示其他进程已经释放了锁,此watcher将会触发,并唤醒当前线程,然后acquire方法返回.."释放锁"的操作,就是删除此临时节点.此时临近的下一个节点将获得锁..

    所谓"重入",就是同一个线程多次获取锁时,如果此线程已经持有了锁(即创建了zk临时节点),事实上将不会再次创建zk的临时节点,而是直接返回.

    因为"重入锁",基于临时节点的特性,因此必须关注client链接重建的问题;粗糙的解决办法,就是每次链接重建(session过期),重新实例化lock对象. 

 

6. Barrier

    栅栏, 可以用来协同分布式环境中的线程.让他们有条件的阻塞,且同时唤醒.

DistributedBarrier barrier = new DistributedBarrier(client,"/barrier");
barrier.setBarrier(); //设置barrier
System.out.println("setBarrier...");
barrier.waitOnBarrier();//等待其他进程移除barrier,此后所有的waitOnBarrier进程都将解除阻塞.
//barrier.removeBarrier(); //移除barrier,解除阻塞.
DistributedDoubleBarrier barrier = new DistributedDoubleBarrier(client,"/d-barrier",12);
System.out.println("enter...");
barrier.enter();//阻塞,直到12个成员加入
System.out.println("running...");
barrier.leave();//阻塞,直到12个成员离开

    其中DistributedDoubleBarrier为双端栅栏,可以让N个线程(进程)同时开始,并且同时退出..

    对于DistributedBarrier内部机制非常简单: setBarrier()方法就是创建"栅栏"节点,removeBarrier()方法就是删除此节点;当执行setBarrier之后,所有的waitOnBarrier()操作都将阻塞,直到删除节点的事件触发.

 

7.LeaderSelector

    在很多场景中,我们需要"leader选举";比如在分布式有很多task,这些task的执行时机需要一个"Leader"去调度.任何时候,同一个leaderPath节点下,只会有一个"leader"..如下展示如何简单的使用LeaderSelector.

public class LeaderSelectorClient {

    private LeaderSelector selector;

    private final Object lock = new Object();
    private boolean isLeader = false;

    public LeaderSelectorClient(CuratorFramework client, String leaderPath) throws Exception {
        LeaderSelectorListener selectorListener = new LeaderSelectorListener() {
            //此方法将会在Selector的线程池中的线程调用
            @Override
            public void takeLeadership(CuratorFramework client) throws Exception {
                System.out.println("I am leader...");
                //如果takeLeadership方法被调用,说明此selector实例已经为leader
                //此方法需要阻塞,直到selector放弃leader角色
                isLeader = true;
                while (isLeader) {
                    synchronized (lock) {
                        lock.wait();
                    }
                }
            }

            //这个方法将会在Zookeeper主线程中调用---watcher响应时
            @Override
            public void stateChanged(CuratorFramework client, ConnectionState newState) {
                System.out.println("Connection state changed...");
                //对于LeaderSelector,底层实现为对leaderPath节点使用了"排他锁",
                //"排他锁"的本质,就是一个"临时节点"
                //如果接收到LOST,说明此selector实例已经丢失了leader信息.
                if (newState == ConnectionState.LOST) {
                    isLeader = false;
                    synchronized (lock) {
                        lock.notifyAll();
                    }
                }
            }
        };
        selector = new LeaderSelector(client, leaderPath, selectorListener);
        //一旦leader释放角色之后,是否继续参与leader的选举
        //此处需要关注CuratorFrameworker.RetryPolicy策略.
        //1) 如果leader是耐久性的,selector实例需要一致关注leader的状态,可以autoRequeue
        //2) 如果leader再行使完任务之后,释放,然后在此后的某个时刻再次选举(比如定时任务),此处可以保持默认值false
        selector.autoRequeue();
    }


    public void start() {
        selector.start();
    }

    public void release() {
        //释放leader角色
        isLeader = false;
        //takeLeadership方法将会中断并返回.
        selector.interruptLeadership();
        synchronized (lock){
            lock.notifyAll();//
        }
    }

    //重新获取leader角色--选举
    public void take() {
        selector.requeue();
    }

    public void close() {
        isLeader = false;
        selector.close();
        synchronized (lock){
            lock.notifyAll();//
        }
    }

    public boolean isLeader() {
        return selector.hasLeadership();
    }

}

    开发者使用LeaderSelector时,需要关注takeLeadership方法的内部逻辑,一旦takeLeadership方法被调用,那么此selector已经是leader角色了,你可以在此方法中增加"事件通知"等来执行一些异步的操作.

    isLeader()方法只能返回当前的状态,有可能返回true之后不久,这个selector实例将不不再是leader,那么就需要我们在listener中更多的关注stateChanged过程.

 

8. LeaderLatch

    上述LeaderSelector开发中,开发者需要关注"链接异常"情况,也需要自己去阻塞leader角色变更,也需要自己去封装"leader角色变更"时的事件处理器....recipse组件已经实现了LeaderLatch来初步解决上述问题,这个类是个便捷的类,如果你还需要更多的处理,恐怕还是需要自己去封装LeaderSelector.

LeaderLatch latch = new LeaderLatch(client,"/task/leader");
//让listener在单独的线程池中运行
Executor executor = Executors.newCachedThreadPool();
//每个listener都用来执行角色变换的事件处理.
LeaderLatchListener latchListener = new LeaderLatchListener() {
	@Override
	public void isLeader() {
		System.out.println("I am leader...");
	}

	@Override
	public void notLeader() {
		System.out.println("I am not leader...");
	}
};
latch.addListener(latchListener,executor);
latch.start();
latch.await();//等待leader角色.
//在await退出之后,你需要通过其他手段继续关注leader状态变更.
System.out.println(latch.hasLeadership());
Thread.sleep(5000);
latch.close();
Thread.sleep(2000);
client.close();

 

  

    到此为止,我们已经把curator-recipse的大部分API都介绍完毕了,希望我们的zookeeper开发之旅更加愉快.

    

你可能感兴趣的:(curator)