标签 学习 下的文章

利用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。

也谈代码行统计

一直在纠结要不要就这个话题写点什么,之前梳理过一些思路,但感觉这个题目似乎没什么大意义。不过将东西憋在肚子里的滋味总是不好受的,最终我还是选择写出来一些,即便它真的没有什么意义^_^。

事情缘于近期领导让我负责的一个内部任务:制定组织内的代码行统计标准并实现标准化的工具。就是这个任务促使了我对代码行统计重新做了一番考量。

对代码行统计的理解

代码行统计这个活动不是软件开发过程中的关键路径活动,它对代码质量、开发进度以及软件价格几乎产生不了什么影响,应该算是个可有可无的东西。

就代码行统计这个活动本身而言,我个人的观点是没有代码行统计不表明不能开发出好软件;有了代码行统计,就一定能开发出高质量软件吗?

不过有一种观点认为:世界的本质是数据。通过数据我们可以发现事物运行的规律。代码行统计则是软件工程中对“数据”要求的产物。过程的好坏需要有数据支 撑,因此代码行统计这个活动成为了人们实现“用数据说话”的一柄利器。在“数据为王”的今天,我们无论如何都不能忽视数据的作用。人们通过数据来反映软件 开发过程中的一些规律性的东西本身也没有什么不妥。另外代码是软件开发过程的最重要成果物,因此围绕着代码的性态,我们用工具做诸多分析,期望从得到的数 据中找寻出一些可以指导和改善我们后续工作的蛛丝马迹。代码行统计提供的多是基础数据,在与其他过程基础数据结合分析后,我们能得到更多的信息。

合理地使用场合

个人觉得下面几个场合对代码行统计的需求是合理的:

* 统计代码总规模
   某个项目、某个模块或又某个版本的代码总规模。

* 代码“成分”统计
   统计空行、注释、代码的行数及占比、重复代码行数及占比等。

* 版本间代码变更差异统计
   两个有关联版本的数据对比统计,获取版本间的有效变更数据情况并作为基础数据提供给后续分析。

一些过程质量指标,诸如千行代码缺陷率等均是以上面这些代码行统计输出的基础数据为基础的。

“误用”

有合理的使用,就有“不合理”的使用 – “误用”。之所以加上引号,是因为至今人们对此见仁见智,尚无定论。以下列举两典型的“误用”。

* 通过代码行统计评估进度

有些组织在项目开始初期,就对成果的规模做了估计,比如10w行代码。然后在过程中使用代码统计工具对项目当前已实现的规模进行统计,并用统计出的数据与 初值的比值作为项目进度的评估参考。个人认为这是种典型的误用。盖茨说过:“用代码行数来衡量编程的进度,就如同用航空器零件的重量来衡量航空飞机的制造 进度一样”。且不提初期的估值有多么的不准确,就代码的行数本身而言,也受到各种因素的影响,比如设计方案、实现者的功力以及编码习惯等。同一个功能,A 实现需要100行代码;换成B就需要10行。

* 通过代码行统计评估程序员绩效

在一些外包公司或外包项目里,尤其是日本人的外包项目里,通过编写代码行的多少来评估程序员绩效的作法是很有市场的。我不能完全否定这种方法的正确性,因 为在日本外包项目中变态的日本人对代码的审核极其严格,并且有着苛刻的编码标准和风格,因此一些胡乱堆砌代码或使用奇技淫巧的代码都会被驳回,因此所有项 目开发者的效率似乎被约束到了一个平均线上。在这个前提下,产出的代码越多,似乎的确表明了这个开发者超出了平均效率,或至少牺牲了不少个人时间来完成项 目中的任务,精神可嘉,绩效被评高似乎也是合情合理的。但除此之外,用代码行多寡来评估程序员绩效显然是不受待见的。

考虑这个“误用”时,我也想模仿盖茨的话做个形象且深刻比喻,最初我写下的是这句话:“用代码行数多少来评估程序员的绩效,就好比用曲子的长短来评估音乐 家的水平,或又好比用画幅的大小来衡量画家的水准,或又好比用电影的时长 来掂量导演的功力!”。但仔细揣摩后发现这句话看起来挺像那么回事,但实际上却是不恰当的。什么是水准、水平或功力,这是衡量人的水平高低的;而绩效则是 一段时间范畴内工作成果的评估; 一个是长期的肯定,一个是阶段性的成绩。我显然是将水平和绩效(阶段性成绩)混为一谈了。高水平的开发者不一定每个周期都会取得高绩效,低水平的开发者也 不是无法取得高绩效的。因此这句话似乎应该改成:“用代码行数多少来评估程序员的绩效,就好比用这首曲子的长短来评估音乐家在这个阶段的水平,或又好比用 画幅的大小来衡量画家的这个阶段水准,或又 好比用电影的时长来掂量导演在这部电影上的功力!”。是不是读起来很别扭啊,反正我是这么觉得的。程序员的成果物是代码,代码好坏优劣对程序员绩效有着直 接影响(虽非充分必要条件),我们不妨替换一下本体来换种说法:“用代码行数多少来评估代码实现的好坏,就好比用曲子的长短来评估曲子的优劣,或又好比用 画幅的大小来衡量画作的高低,或又好比用电影的时长来掂量影片的良莠”!

对用代码行数多少来评估程序员绩效这种事情,我是很反感的,但在国内许多公司里,这种现象却又屡见不鲜。但这种行为背后的动机何在呢?传统工厂中,衡量一 个worker的绩效是相对容易量化,也比较客观的,比如制鞋厂可以用制成鞋子的数量来确定 worker绩效;在汽车组装车间,组装汽车的数量可以作为作为工人们的绩效;在炼钢厂,班组炼出的钢铁的吨数可作为班组成员绩效等等。将代码行数作为程 序员绩效的参考指标也许是一个无奈的方法。之所以想用代码行数,是因为程序员工作中能量化的东西不多,代码行数首当其冲。组织为了尽量减少绩效评定时主观 的成分,增加客观的评价,代码行统计从此被误用了。

代码行统计的高效使用

* 标准统一,工具一致

代码行统计工具有很多,因此执行这个活动时会出现不同人使用的代码行统计工具不一致的情况;并且不同工具对一些指标的定义也许有不同,这会导致收集到的数据存在含义不一致,精确度差的问题。因此高效使用代码行统计工具的一个前提就是(统计)标准统一,工具一致。

* 零干扰

一些传统的代码行统计方法是配置负责人收到统计任务时,将任务分发给各个模块的负责人,由各个模块负责人各自统计,然后反馈给配置负责人汇总。这种方式显 然不那么高效,而且容易引起一些对统计任务的反感情绪。高效的代码行统计最好能做到对开发人员“零干扰”。配置负责人可以通过“自动化”的静默方式收集代 码行数据。当然这需要对一些现成的开源工具做一些包装或二次开发才能做到,个人觉得这种投入是值得的,同时也能避免标准不一,工具不一致的情况。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 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