利用ZooKeeper开发服务器上下线感知程序

What is ZooKeeper

ZooKeeper是一个分布式的分布式应用程序协调服务。简单地来说,就是用于协调管理多个分布式应用程序的一个工具,扮演着一个第三方管理者的角色。

问题背景分析

假设现在有10个应用程序(App#0 - App#9),运行在由10台服务器(Server#0 - Server#9)组成的集群上(假设平均分配,每台服务器上运行一个程序)。此时由于某个热门线上活动的开始(如抢票or低价秒杀等),突然间有数以百万计的用户访问服务器上的资源,等待服务器处理并应答(如下图所示)。

server.png

很不幸10台服务器中有K台受不住负载压力,导致服务器崩溃。在这种情况下,如果客户端无法感知服务器的状态(在线/离线),部分向已经崩溃的服务器发送请求的客户端将会有长时间无法获得应答,它们只能一直重复地向已经崩溃的服务器地址重发请求,无法切换至另外(10-K)台完好的服务器进行交互。

breakdown.png

其实在这种场景下,如果客户端能够及时地感知到集群中哪些节点已经崩溃,哪些节点仍然完好,是可以切换至完好的节点并向其发送请求的。理论上只要集群中仍有1个节点是完好的,它即能向客户端提供服务。

所以整个问题的症结就在于,如何让客户端感知到服务器上下线状态,以便切换请求发送的地址。

zk.png

重新参考ZooKeeper的功能描述,ZooKeeper可以用来协调管理多个分布式应用程序,那其实可以用于管理我们的分布式机器集群。如上图所示,在用户和服务器集群中间可设置ZooKeeper层,让ZooKeeper实时感知每一个节点的状态,然后客户端并不直接向具体节点发起请求,而应先向ZooKeeper询问当前仍然存活的服务器节点,然后再从中挑选一个负载较低的服务器节点进行交互。由于ZooKeeper本身的高可用性(本身也可拓展为分布式架构),所以就能大大地提高整个系统的可用性。

ZooKeeper数据结构

ZooKeeper数据结构采用了树状结构(在文件系统中被广泛使用),且不是简单的二叉树,而是多叉树。在ZooKeeper的树结构中,每一个节点被称为znode,可通过控制台命令或者Java的SDK对内部数据进行管理。

znode的类型有2*2=4种,分别是:

  • PERSISTENT
  • PERSISTENT_SEQUENTIAL
  • EPHEMERAL
  • EPHEMERAL_SEQUENTIAL

其中PERSISTENTEPHEMERAL的区别正如其名,在无外力影响下PERSISTENT节点不会被改变和删除,而EPHEMERAL节点在创建节点的session结束后会自动从树中删除。至于SEQUENTIAL非SEQUENTIAL则影响了节点id自增,SEQUENTIAL节点的id会自动遵循父节点下的自增规则进行命名。

zkds.png

如图所示,在本问题中我们可以把一台服务器看作树中的一个节点,我们可以利用EPHEMERAL节点的这一特性进行服务器状态的监听。服务器上线时创建与zk之间的session并向zk注册节点,只要服务器不崩溃,session便不会结束,即EPHEMERAL节点会一直存在,可被客户端感知;当服务器崩溃时,其与zk之间保持的session自然也会结束,EPHEMERAL节点会自动被删除,客户端查询服务器列表时绝对无法获得已删除的节点信息。

Demo程序

  • Server.java (服务器端代码)
package my.bigdata.zk;

import org.apache.zookeeper.*;

public class Server {

    private static final String HOST_ADDRESS = "localhost:2181";
    private static final int DEFAULT_TIMEOUT = 2000;
    private static final String DEFAULT_SERVER_PARENT = "/servers";

    private ZooKeeper zkConnect = null;

    /**
     * 连接至ZooKeeper
     * @throws Exception
     */
    public void connect() throws Exception{
        zkConnect = new ZooKeeper(HOST_ADDRESS, DEFAULT_TIMEOUT, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                System.out.println("Type:" + watchedEvent.getType()
                        + " Path:" + watchedEvent.getPath());
            }
        });
    }

    /**
     * 向ZooKeeper注册本服务器节点
     * @param data 服务器信息
     * @throws Exception
     */
    public void register(String data) throws Exception{
        String create = zkConnect.create(DEFAULT_SERVER_PARENT + "/server",
                                            data.getBytes(),
                                            ZooDefs.Ids.OPEN_ACL_UNSAFE,
                                            CreateMode.EPHEMERAL_SEQUENTIAL);   // 注册成ephemeral节点以便自动在zk上注销
        System.out.println(create + " is registered!");
    }

    /**
     * 通过sleep模拟服务器在线
     */
    public void sleep() {
        try {
            Thread.sleep(20000);
        } catch (Exception e) {
            System.out.println(e.toString());
        }
    }


    public static void main(String[] args) throws Exception {

        //连接至zk
        Server server = new Server();
        server.connect();

        //向zk注册服务器信息
        String data = args[0];
        server.register(data);

        server.sleep();
    }
}

服务器端的重点在于,程序启动时向ZooKeeper的指定节点下注册服务器信息,相当于通知ZooKeeper这个第三方:“服务器已上线”。其次,注册的节点类型必须是ephemeral节点,为了实现节点id自增(auto-increment)还可以使用ephemeral_sequential节点。

  • Client.java (客户端代码)
package my.bigdata.zk;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Client {

    private static final String HOST_ADDRESS = "localhost:2181";
    private static final int DEFAULT_TIMEOUT = 2000;
    private static final String DEFAULT_SERVER_PARENT = "/servers";

    private ZooKeeper zkConnect = null;
    private List<String> availableServers;

    /**
     * 连接至ZooKeeper
     * @throws Exception
     */
    public void connect() throws Exception {
        zkConnect = new ZooKeeper(HOST_ADDRESS, DEFAULT_TIMEOUT, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                try {
                    updateServerCondition();    // 重复注册
                } catch (Exception e) {
                    System.out.println(e.toString());
                }
            }
        });
    }

    /**
     * 向zk查询服务器情况, 并update本地服务器列表
     * @throws Exception
     */
    public void updateServerCondition() throws Exception {
        List<String> children = zkConnect.getChildren(DEFAULT_SERVER_PARENT, true);
        List<String> servers = new ArrayList<>();
        for(String child : children) {
            byte[] data = zkConnect.getData(DEFAULT_SERVER_PARENT + "/" + child,
                                        false,
                                        null);
            servers.add(new String(data));
        }
        availableServers = servers;
        System.out.println(Arrays.toString(servers.toArray(new String[0])));
    }

    /**
     * 通过sleep让客户端持续运行,模拟"监听"
     */
    public void sleep() throws Exception{
        System.out.println("client is working");
        Thread.sleep(Long.MAX_VALUE);
    }

    public static void main(String[] args) throws Exception {

        // 连接zk
        Client client = new Client();
        client.connect();

        // 获取servers节点信息(并监听),从中获取服务器信息列表
        client.updateServerCondition();

        client.sleep();
    }
}

客户端的重点在于,它不断地向ZooKeeper某个特定节点(此处是servers节点)注册了一个Watcher,那么一旦该节点下的结构发生改变,ZooKeeper会向注册了Watcher的客户端发送“状态变化”的消息,那么客户端即可动态地从ZooKeeper中获取最新的服务器节点信息,甚至无需“主动”询问。

当然,ZooKeeper的应用场景还有很多,考虑到它本身也可拓展为一个分布式应用,在这种高可用性保证下它简直就是多个分布式应用的万能管家和协调者😊。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容