分类 技术志 下的文章

利用ZooKeeper服务实现分布式系统的Leader选举

每次与Java组的同事们坐下来谈技术、谈理想、谈人生时,Java组的同事总会向我们投来羡慕的眼光:卧槽!又是自己开发的工具,太NB了。这时C程序 员们的脸上就会洋溢出自豪的笑容,然后内心骂道:谁让我们没有现成的呢。另一个空间里的某些“无C不欢”们或者某些“C Guru”们会骂道:靠,有了也不用,自己写!

有时候,C程序员真的有一种下意识:不情愿使用其他语言开发的工具、框架或服务,且比其他程序员更爱“重新发明轮子”(有利有弊)。也许这是某种 骨子里的自负在搞怪;另外一个极端:今天和我聊天的一个经验丰富的C程序员还在忧虑:如果离职是否有公司会要他:(。

其实这个时代的C程序员一直活得挺纠结^_^。

这个世界,软硬件发展日新月异,越来越多的后端程序用Java等其他语言实现。Java高级选手在这个世界上也甚是吃香,这个你看看各大招聘网站 就知道了。再听听坊间“BAT”三巨头给出的高高在上的offer价格,也可以看出Java程序员是多么的有“钱途”和受欢迎了。当然拿好offer的前提是你的Java底子不薄。

其实无论用什么编程语言,成为牛人后,钱途也都是杠杠的。

没有什么好的开场白,于是有了上面一些“胡言乱语”。我们言归正传。

本文是一篇初级技术博文。讲的是如何使用ZooKeeper C API通过ZooKeeper的服务实现分布式系统的Leader选举。当然这一试验是为了尝试解决我们自己的分布式系统在集中配置数据分发这一环节上的 一个“固疾”。还好我还不那么纠结,也没有重新实现ZooKeeper的冲动,于是我就用了ZooKeeper这一Java实现的成熟的分布式 系统的服务框架。

* 搭建ZooKeeper服务环境

    – 下载官方stable release版本 – ZooKeeper3.4.5。解压后,将$ZooKeeper_INSTALL_PATH/bin加入到PATH变量中(其中ZooKeeper_INSTALL_PATH为解压后ZooKeeper-3.4.5目录的绝对路径)。

    – 试验环境下,最简单的ZooKeeper用法就是使用单机版。
      进入到$ZooKeeper_INSTALL_PATH/conf下,将zoo_sample.cfg改名为zoo.cfg,即可作为单机版ZooKeeper的配置文件。当然你也可以像我一样随意修改修改:

      # The number of milliseconds of each tick
   tickTime=2000
   # The number of ticks that the initial
   # synchronization phase can take
   initLimit=5
   # The number of ticks that can pass between
   # sending a request and getting an acknowledgement
   syncLimit=2

   dataDir=/home/tonybai/proj/myZooKeeper
   # the port at which the clients will connect
   clientPort=2181

       
      如果你要体验多机版ZooKeeper服务,那你还要继续动动手脚,以双机版为例,假设有两个ZooKeeper节点(10.0.0.13和10.0.0.14):

      10.0.0.13上的ZooKeeper节点1的配置文件如下:

     # The number of milliseconds of each tick
   tickTime=2000
   # The number of ticks that the initial
   # synchronization phase can take
   initLimit=5
   # The number of ticks that can pass between
   # sending a request and getting an acknowledgement
   syncLimit=2

   dataDir=/home/tonybai/proj/myZooKeeper
   # the port at which the clients will connect
   clientPort=2181

   server.1=10.0.0.13:2888:3888 
   server.2=10.0.0.14:2888:3888

     10.0.0.14上的ZooKeeper节点2的配置文件如下:

     # The number of milliseconds of each tick
   tickTime=2000
   # The number of ticks that the initial
   # synchronization phase can take
   initLimit=5
   # The number of ticks that can pass between
   # sending a request and getting an acknowledgement
   syncLimit=2

   dataDir=/home/tonybai/proj/myZooKeeper
   # the port at which the clients will connect
   clientPort=2181

   server.1=10.0.0.13:2888:3888
   server.2=10.0.0.14:2888:3888

      别忘了在每个节点的dataDir下分别创建一个myid文件:
      在10.0.0.13节点1上执行:
      
     $> echo 1 > myid

      在10.0.0.14节点2上执行:
     
   $> echo 2 > myid

      启动ZooKeeper执行:
      $> zkServer.sh start

      模拟一个客户端连到ZooKeeper服务上:
      $> zkCli.sh

      成功链接后,你将进入一个命令行交互界面:
       [zk: 10.0.0.13:2181(CONNECTED) 1] help
    ZooKeeper -server host:port cmd args
    connect host:port
    get path [watch]
    ls path [watch]
    set path data [version]
    rmr path
    delquota [-n|-b] path 

        … …

* 选主原理

   ZooKeeper在选主过程中提供的服务就好比一栋名为"/election"小屋,小屋只有一个门,各节点只能通过这个门逐个进入。每个节点进入后, 都会被分配唯一编号(member-n),编号n自小到大递增,节点编号最小的自封为Leader,其他节点只能做跟班的(follower) – 这年头还是小的吃香:原配干不过小三儿,小三儿干不过小四儿,不是么^_^!)。
   每当一个节点离开,ZooKeeper都会通知屋内的所有节点,屋内节点收到通知后再次判断一下自己是否是屋内剩余节点中编号最小的节点,如果是,则自封为Leader,否则为Follower。

   再用稍正式的语言重述一遍:

   各个子节点同时在某个ZooKeeper数据路径/election下建立"ZOO_SEQUENCE|ZOO_EPHEMERAL"节点 – member,且各个节点监视(Watch) /election路径的子路径的变更事件。ZooKeeper的sequence节点特性保证节点创建时会被从小到大加上编号。同时节点的 ephemeral特性保证一旦子节点宕机或异常停掉,其对应的member节点会被ZooKeeper自动删除,而其他节点会收到该变更通知,重新判定 自己是leader还是follower以及谁才是真正的leader。

* 示例代码

关于ZooKeeper的C API的使用资料甚少,但这里就偏偏要用C API举例。

C API的安装方法:进入$ZOOKEEPER_INSTALL_PATH/src/c下面,configure->make->make install即可。

ZooKeeper的C API分为同步与异步两种模式,这里简单起见用的都是同步机制。代码不多,索性全贴出来。在这里能checkout到全部代码。

/* election.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "zookeeper.h"

static int
is_leader(zhandle_t* zkhandle, char *myid);

static void
get_node_name(const char *buf, char *node);

struct watch_func_para_t {
    zhandle_t *zkhandle;
    char node[64];
};

void
election_children_watcher(zhandle_t* zh, int type, int state,
                      const char* path, void* watcherCtx)
{
    int ret = 0;

    struct watch_func_para_t* para= (struct watch_func_para_t*)watcherCtx;

    struct String_vector strings;
    struct Stat stat;

    /* 重新监听 */
    ret = zoo_wget_children2(para->zkhandle, "/election", election_children_watcher,
                             watcherCtx, &strings, &stat);
    if (ret) {
        fprintf(stderr, "child: zoo_wget_children2 error [%d]\n", ret);
        exit(EXIT_FAILURE);
    }

    /* 判断主从 */
    if (is_leader(para->zkhandle, para->node))
        printf("This is [%s], i am a leader\n", para->node);
    else
        printf("This is [%s], i am a follower\n", para->node);

    return;
}

void def_election_watcher(zhandle_t* zh, int type, int state,
        const char* path, void* watcherCtx)
{
    printf("Something happened.\n");
    printf("type: %d\n", type);
    printf("state: %d\n", state);
    printf("path: %s\n", path);
    printf("watcherCtx: %s\n", (char *)watcherCtx);
}

int
main(int argc, const char *argv[])
{

    const char* host = "10.0.0.13:2181";
    zhandle_t* zkhandle;
    int timeout = 5000;
    char buf[512] = {0};
    char node[512] = {0};

    zoo_set_debug_level(ZOO_LOG_LEVEL_WARN);
    zkhandle = zookeeper_init(host, def_election_watcher, timeout,
                              0, "Zookeeper examples: election", 0);
    if (zkhandle == NULL) {
        fprintf(stderr, "Connecting to zookeeper servers error…\n");
        exit(EXIT_FAILURE);
    }

    /* 在/election下创建member节点 */
    int ret = zoo_create(zkhandle,
                        "/election/member",
                        "hello",
                        5,
                        &ZOO_OPEN_ACL_UNSAFE,  /* a completely open ACL */
                        ZOO_SEQUENCE|ZOO_EPHEMERAL,
                        buf,
                        sizeof(buf)-1);
    if (ret) {
        fprintf(stderr, "zoo_create error [%d]\n", ret);
        exit(EXIT_FAILURE);
    }

    get_node_name(buf, node);
    /* 判断当前是否是Leader节点 */
    if (is_leader(zkhandle, node)) {
        printf("This is [%s], i am a leader\n", node);
    } else {
        printf("This is [%s], i am a follower\n", node);
    }

    struct Stat stat;
    struct String_vector strings;
    struct watch_func_para_t para;
    memset(&para, 0, sizeof(para));
    para.zkhandle = zkhandle;
    strcpy(para.node, node);

    /* 监视/election的所有子节点事件 */
    ret = zoo_wget_children2(zkhandle, "/election", election_children_watcher, &para, &strings, &stat);
    if (ret) {
        fprintf(stderr, "zoo_wget_children2 error [%d]\n", ret);
        exit(EXIT_FAILURE);
    }

    /* just wait for experiments*/
    sleep(10000);

    zookeeper_close(zkhandle);
}

static int
is_leader( zhandle_t* zkhandle, char *myid)
{
    int ret = 0;
    int flag = 1;

    struct String_vector strings;
    ret = zoo_get_children(zkhandle, "/election", 0, &strings);
    if (ret) {
        fprintf(stderr, "Error %d for %s\n", ret, "get_children");
        exit(EXIT_FAILURE);
    }

    /* 计数 */
    for (int i = 0;  i < strings.count; i++) {
        if (strcmp(myid, strings.data[i]) > 0) {
            flag = 0;
            break;
        }
    }

    return flag;
}

static void
get_node_name(const char *buf, char *node)
{
    const char *p = buf;
    int i;
    for (i = strlen(buf) – 1; i >= 0; i–) {
        if (*(p + i) == '/') {
            break;
        }
    }

    strcpy(node, p + i + 1);
    return;
}

编译这个代码:
$> gcc -g -std=gnu99 -o election election.c -DTHREADED -I/usr/local/include/zookeeper -lzookeeper_mt -lpthread

验证时,我们在不同窗口启动三次election程序:

窗口1, election启动:

$> election
Something happened.
type: -1
state: 3
path:
watcherCtx: Zookeeper examples: election
This is [member0000000001], i am a leader

窗口2,election启动:

$> election
Something happened.
type: -1
state: 3
path:
watcherCtx: Zookeeper examples: election
This is [member0000000002], i am a follower

此时窗口1中的election也会收到/election的字节点增加事件,并给出响应:

This is [member0000000001], i am a leader

同理当窗口3中的election启动时,窗口1和2中的election都能收到变动通知,并给予响应。

我们现在停掉窗口1中的election,大约5s后,我们在窗口2中看到:

This is [member0000000002], i am a leader

在窗口3中看到:

This is [member0000000003], i am a follower

可以看出窗口2和3中的election程序又做了一次自我选举。结果窗口2中的election由于节点编号最小而被选为Leader。

Ubuntu 12.04修复记

今天一早发现Ubuntu 12.04坏掉了,于是用了大半天对其做了修复,修复过程十分坎坷,但结果还不错,遂记之以备忘。

* 毁掉Ubuntu

Ubuntu坏掉完全是由于我的错误决策。昨天一天Ubuntu桌面右上方的状态拦一直有一个红色的错误提示符,提示系统包冲突,建议执行sudo apt-get install -f解决。apt-get也提示索引冲突,无法卸载和安装任何包。于是执行了sudo apt-get install -f,虽然我不知道这个命令对系统做了哪些更改。但结果是那个错误提示符的确不见了。

不过等到晚上回家启动电脑后才发现笔记本的快捷键都不好用了。比如无法通过fn+f6 or f7对屏幕亮度进行调节(默认启动时是最大亮度,太刺眼,每次都要调)。更要命的是声音快捷键居然不好用了,而且其为关闭状态。并且状态栏上到小喇叭也无 法点击,“系统设置->声音”也根本打不开。没有声音,如何听歌看电影啊,于是乎想到了upgrade。

执行upgrade,有400多M的包要升级,于是让电脑自己升级,我去睡觉去了。今天早上起来发现Ubuntu upgrade ok了。重启、引导,似乎一切似乎很正常。但输入密码登录后,画面就始终停留在墙纸背景上了。啥都没有出现。快捷键依旧无法使用,反复重启几次均如此,超 级杯具了!

* 重装Ubuntu

上班后,试图用livecd引导修复Ubuntu,但ubuntu没有修复菜单选项,要么删除当前已经安装的ubuntu 12.04.2并重新安装,丢弃HOME路径下的数据;要么就是保持现有版本OS不动,新安装一个OS,原OS HOME路径下的数据不会有损失。我只能选择后者。这时我才发现,livecd在我的笔记本中发现的已有OS版本居然变成了ubuntu 12.10!靠,upgrade居然直接将12.04.2升级到了12.10。

原12.04.2安装在/dev/sda1分区,livecd将该分区拆分成两个分区,有点类似于Win7高级磁盘分区工具中对大分区的压缩,压缩后变成安装了老系统的/dev/sda1和新分区/dev/sda10,livecd在/dev/sda10上面安装新系统。

新Ubuntu很快就安装好了,重启后顺利的进入了桌面,一切正常。接下来又是老一套,恢复数据+装软件。

* 自动挂接各分区

由于采用的是默认安装,没有自定义挂接点,于是需要手工编写/etc/fstab文件,将诸多分区做自定义挂接,使之能在系统启动时自动挂接。
首先执行sudo blkid,查看各分区信息:

$> sudo blkid
/dev/sda1: UUID="d0d1424b-e3a8-43d9-887a-1c58c64ecff3" TYPE="ext3"
/dev/sda5: UUID="8bda8d60-b5cb-43aa-b408-dd6ce4957923" TYPE="ext3"
/dev/sda6: UUID="c415cf1c-624c-42ce-a8a6-6c072b5ee232" TYPE="ext3"
/dev/sda7: UUID="b8f6c810-bbb0-458c-8306-7b4a834ad726" TYPE="swap"
/dev/sda8: UUID="E208-E865" TYPE="vfat"
/dev/sda9: UUID="6BB3-FA39" TYPE="vfat"
/dev/sda10: UUID="1477776e-fe68-40f6-9804-c752b5efb149" TYPE="ext4"

接下来编辑/etc/fstab,该文件中swap分区以及前面的分区是系统安装时就设置好的。后面三个是我自己设置的:

# <file system> <mount point>   <type>  <options>       <dump>  <pass>
proc            /proc           proc    nodev,noexec,nosuid 0       0
# / was on /dev/sda10 during installation
UUID=1477776e-fe68-40f6-9804-c752b5efb149 /               ext4    errors=remount-ro 0       1
# swap was on /dev/sda7 during installation
UUID=b8f6c810-bbb0-458c-8306-7b4a834ad726 none            swap    sw              0       0
UUID=8bda8d60-b5cb-43aa-b408-dd6ce4957923 /home1          ext3    defaults        0       0
UUID=c415cf1c-624c-42ce-a8a6-6c072b5ee232 /home2          ext3    defaults        0       0
UUID=d0d1424b-e3a8-43d9-887a-1c58c64ecff3 /oldlinux       ext3    defaults        0       0

重启后,就会发现,根目录下自动挂载了/home1、/home2和/oldlinux三个分区。别忘了对这几个挂载点做一下chown操作,这样你的用户才能对这些路径有写权限。

* 恢复用户数据

主要是迁移原home目录下的数据。在原系统中,我单独将一个分区挂接到/home路径上,其中的/home/tonybai设置为HOME路径。重装 os后,系统在/dev/sda10分区建立了/home/tonybai作为HOME目录。而之前的那个存放HOME路径的数据分区被我映射为 /home1了,但其中的数据完好无损。我于是打开/etc/passwd,将我的用户到home路径由/home/tonybai改为/home1 /tonybai,这样重新登录后,我又回到了熟悉的HOME环境中了。不过一些原先为/home/tonybai路径的配置需要修改为/home1 /tonybai了。

剩下的就是安装各种软件了。

* 问题再现,有惊无险

经过大半天的折腾,工作环境基本得以恢复。晚上回到家里,打算再补一些软件。结果刚进入Ubuntu就发现了异常:触控板失灵、无线网卡失灵、静音并无法 调节、指点杆失灵、所有快捷键失灵等。并且总是弹出对话框,提示系统错误,建议重启。重启若干次依旧是老样子。靠!这不又回到了最初的问题状态了吗。难道 还得推倒重来?

死马当活马医。试着执行一下sudo apt-get install -f,居然提示:用"sudo dpkg –configure -a"可以解决。遂按照后面的命令执行了一下。命令的效果是系统在重新配置包 – 所有包。执行完毕后,注销登录,发现大不相同了。重启后再看一下,一切恢复正常。估计又是我装了什么软件导致包依赖异常导致的。如果早知道dpkg –configure -a可以解决问题,我这大半天时间就可以专注于其他事情了,唉。

生命也许就在于折腾^_^!!!

再次提醒:用Ubuntu的童鞋apt-get update/install要谨慎,upgrade尽量就不要做了,成功率低得很!

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 AI原生开发工作流实战 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats