标签 Apache 下的文章

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

也谈Commit log

版本控制工具大行其道的今天,作为程序员,势必要每天与各种版本控制系统(比如SubversionGitMercurial等)打交道, 每天不commit几次代码都不好意思说自己是专业程序员^_^。不过commit代码可不止敲入commit命令这么简单,对于一个专业程序员 来说,我们还要关注每次commit所携带的背景信息,这里暂且称之为“commit context”。在每次commit时,这些上下文信息只能通过commit log来体现。

一、Commit Context

今日的软件复杂度日益增加,软件开发模式也早已从单打独斗的英雄模式变成了团队协作模式了,而在团队模式下,版本控制系统发挥着至关重要的作用, 它让开发过程变得有序,将冲突解决的成本尽可能地降低到最低。但版本控制系统毕竟不是智能的,它只是机械地记录着每次提交前后的内容的raw差 异,至于这个差异究竟代表了什么,版本管理系统是不得而知的,这就需要我们开发者们来提供,这就算是产生commit context的动机吧。即便是一个人开发维护的项目,个人的记忆也是有时效性的,时间久了,以前的代码变更context势必也就淡忘了,良好且规范的 commit context有助于更好的维护项目,追踪历史思路和行为,甚至在查找bug时也是能帮得上大忙的,比如确认bug引入的时段边界、代码范围等。

前面说了,commit context最终是以commit log形式提供的,这才是我在这篇文章中真正要说的内容^_^。评价一个项目的好坏,无论是商业项目,还是开源项目,代码本身质量是一个重要的方面,代码 维护的规范性则是另外不可忽略的一个重要因素,而在代码维护规范性方面,commit log的规范是一项重要内容。做了这么多年Coding工作,到目前为止部门内部还没有哪一个项目在commit log规范方面是让我满意和欣赏的。另外本人在亲为commit log方面也是不能让自己满意的,这也是促使我思考commit log这块内容的一个初衷。

commit log承载着每次commit动作的context。一般来说context中至少要有一项内容,那就是此次代码变更的summary,这是最基本的要 求。如果你的commit log还是空着的,那你真该反思反思了,那是对自己和他人的不负责任。但无论是商业公司内部开发还是开源项目,commit context涉及到的因素往往不止一个,很多情况下commit context还与项目过程、质量保证流程以及项目使用的一些工具系统有 关联。我们来看两个知名开源项目的commit log样例吧。

[example1 - Linux Kernel]

audit: catch possible NULL audit buffers
It's possible for audit_log_start() to return NULL.  Handle it in the
various callers.

Signed-off-by: Kees Cook <keescook@chromium.org>
Cc: Al Viro <viro@zeniv.linux.org.uk>
Cc: Eric Paris <eparis@redhat.com>
Cc: Jeff Layton <jlayton@redhat.com>
Cc: "Eric W. Biederman" <ebiederm@xmission.com>
Cc: Julien Tinnes <jln@google.com>
Cc: Will Drewry <wad@google.com>
Cc: Steve Grubb <sgrubb@redhat.com>
Cc: Andrea Arcangeli <aarcange@redhat.com>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>

这是Linux Kernel项目的一个commit log的内容。从这个log携带的context信息来看,我们能够清楚地了解如下一些内容:

- 修改的内核模块范围audit
- 修改的原因summary: to catch possible NULL audit buffers
- 这个patch从诞生到被merge到trunk过程中涉及到的相关的人员列表
- 这个patch由Who sign-off的。

将mail list放入到commit log中,这是Linux Kernel开发过程规范所要求的,同样也是质量保证的一个方法。在《如何加入Linux内核开发社区》系列文章中你可以了解到一些有关Linux Kernel开发过程的内容。从这个例子中我们主要可以看出commit context与Project过程、质量保证链条方面的相关性。

[example2 - Apache Subversion]

Fix issue #3498 – Subversion password stores freeze Eclipse

* subversion/libsvn_auth_gnome_keyring/gnome_keyring.c
  (simple_gnome_keyring_first_creds, simple_gnome_keyring_save_creds,
   ssl_client_cert_pw_gnome_keyring_first_creds,
   ssl_client_cert_pw_gnome_keyring_save_creds): If the keyring is locked
    and we are in interactive mode but have no unlock prompt function, don't
    throw a "GNOME Keyring is locked and we are non-interactive" error;
    instead, continue without unlocking it, so that the unlocking may be
    handled by the default GNOME Keyring unlock dialog box.

这是Apache Subversion项目的一个commit log的内容。同样从这个log携带的context信息来看,我们能够清楚地了解如下一些内容:

- 修改的代码范围subversion/libsvn_auth_gnome_keyring/gnome_keyring.c,包括括号中的函数名列表, 这个显然更为细致。
- 修改的原因summary: Fix issue #3498 – Subversion password stores freeze Eclipse
- 这个patch与问题跟踪系统的关联性 -issue #3498

通过这个commit log,我们可以快速找到此patch对应的问题跟踪系统中的条目#3498,这样可以查看到一些更为细致的context信息。从这个例子我们主要能够 看出commit context与项目所使用的一些工具系统的关联。

综合以上可以看出良好的commit log是可以清楚全面反映commit context的。这里的“全面”是project-dependent的,是需要能够体现出涉及project的一切必要信息的:过程的、质量的、工具 的。

二、Commit log格式

Commit log没有放之四海而皆准的统一格式,而是project-dependent的。就我个人而言,我会在下面的几个问题上有纠结。

* 语言

不得不承认在创造编程语言方面,西方文化占了主导,语言中的关键字也多取自英语。虽然目前主流的语言以及新兴的语言都号称源码原生支持utf8或 unicode其他字符集格式,但却是很少见到在源文件中使用非英语命名变量或函数的,这也影响了我在commit log中对语言的选择 – 我基本上都是用英文编写commit log的。目前主流的版本控制工具都是支持unicode字符集的,你用中文提交也是没有任何问题的,尤其是在国内商业项目中,使用中文描述起来,理解上快且歧义少。我是不反对用中文写commit log的,但反感的是中英文混合写commit log(有些人用中文,有些人用英文)。每当批量看commit log时,中英文混在一起,一点美感都没有了。

commit log不是给最终用户看的,而是给开发维护人员看的。因此选择语言种类时要看这种语言是否能给开发维护人员的工作带来便利,精确全面地传达context。即便 应用是要发布给非洲人民,但若开发人员都是中国人,一样可以用中文编写commit log。

* 地道

说到“地道”,主要是针对你选择外语(大多数情况是英语)作为你commit log的承载语言时。就像生活在国外要用外国人熟悉的语言习惯与人交流似的,我们在用英语编写commit log时也要学会选用“地道”的词汇,远离Chinglish。当然想立即做到“地道”也不是那么容易,毕竟我们一直以来就按照Chinglish的思维去学 习英语的,一个比较好的方式就是多看看知名开源项目(比如linux kernel)的commit log,看看人家是如何选择词汇和组织句子的。其实Commit log中用到的词汇和句型很少,看多了也就找猫画虎的学会了。

* 规范

“没有规矩,不成方圆”,无论是商业软件项目,还是大型开源项目,莫不如此。如果要想很好的传达commit context,一个设计规范,内容全面的commit log格式是必不可少的。我们无需从头做起,很多开源项目在这方面都已经有一些良好的实践,比如上面提到的linux kernel的commit log convention,再比如这里有Apache Subversion的Commit log要求。TYPO3和FLOW3也有自己详细的Commit log说明

制定规范时总体来说,注意以下几点:
– 格式简明扼要,只保留必要的项;
– 注意与项目过程、质量保证流程的结合,以及与第三方工具的关联(注意序号或ID的唯一性);
– 对于规模较大的系统,可以考虑在log中体现影响的涉及的“子模块”或“子目录”名字或者逻辑功能的名字(比如前面linux kernel例子中的audit),这样便于快速定位本地commit的影响范畴。

三、Commit模板

如果像linux kernel或subversion那样涉及到过程、质量控制以及第三方工具的集成(比如问题跟踪系统、代码评审系统等)时,建议设置Commit log template(模板)以简化开发者commit log编写的工作。

* Subversion命令行客户端支持commit log模板

Subversion在命令行客户端侧暂无对模板的支持。不过可以通过一些trick模拟实现这个功能:

- 创建commit log模板log.tmpl,放在特定目录下,本例中放在用户的$HOME目录下
- 添加并导出环境变量SVN_EDITOR
         export SVN_EDITOR="rm svn-commit.tmp && cp ~/log.tmpl svn-commit.tmp && vi "

svn commit时,svn客户端会在当前路径下会执行类似$SVN_EDITOR svn-commit.tmp的命令,而svn-commit.tmp文件已经被替换为我们的模板文件,开发者只需按模板填写内容,并保存退出即可。如果 commit成功,svn客户端会删除当前目录下的svn-commit.tmp,否则svn-commit.tmp不会被删除,这将导致下次再提交 时,svn客户端检测到svn-commit.tmp的存在,从而新建立一个svn-commit.2.tmp的新文件,导致模板失效,这也是这个方法的 一个瑕疵。

* Git命令行支持commit log模板

Git是目前very hot的分布式版本管理工具,起步晚,但起点高,因此已经内置了对模板的支持,只需将模板文件配置一下即可。
         git config –global commit.template ~/log.tmpl

四、良好格式commit log的实施

即便有了良好格式的commit log的模板定义,但就我经验而言,实施起来也还会遇到诸多问题。commit行为是客户端发起的,要让所有开发者都能很好的使用模板并主动按模板提交需 要一些流程以及工具支持。比如在server段部署pre-commit hook,对提交的log格式进行检查,不符合模板格式的予以拒绝等。

对于与问题跟踪系统有关联的log格式,还要注意保持问题跟踪系统id或序号的唯一性,这显然是管理和过程方面的工作。

对于开源项目,一般merge到trunk需要owner的检查,所以反倒实施起来容易了些,只要有一篇内容丰富的 developer/community guide或convention之类的文档即可,多数知名的opensource project(比如linux kernel、subversion、apache httpd server、python等)都是有这类文档的,为这些project提交patch前是要好好阅读这些文档的,不能坏了规矩^_^。     
 

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