<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; ZooKeeper</title>
	<atom:link href="http://tonybai.com/tag/zookeeper/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Fri, 17 Apr 2026 00:21:29 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>使用consul实现分布式服务注册和发现</title>
		<link>https://tonybai.com/2015/07/06/implement-distributed-services-registery-and-discovery-by-consul/</link>
		<comments>https://tonybai.com/2015/07/06/implement-distributed-services-registery-and-discovery-by-consul/#comments</comments>
		<pubDate>Mon, 06 Jul 2015 07:37:38 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Airbnb]]></category>
		<category><![CDATA[cluster]]></category>
		<category><![CDATA[consul]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[haproxy]]></category>
		<category><![CDATA[hashicorp]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[raft]]></category>
		<category><![CDATA[SmartStack]]></category>
		<category><![CDATA[ZooKeeper]]></category>
		<category><![CDATA[分布式系统]]></category>
		<category><![CDATA[强一致性]]></category>
		<category><![CDATA[服务发现]]></category>
		<category><![CDATA[服务注册]]></category>
		<category><![CDATA[选主]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1758</guid>
		<description><![CDATA[Consul是HashiCorp公司推出的开源工具，用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案，比如 Airbnb的SmartStack等相比，Consul的方案更&#8220;一站式&#8221;，内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案，不再需要依赖其他工具（比如ZooKeeper等）。使用起来也较 为简单。Consul用Golang实现，因此具有天然可移植性(支持Linux、windows和Mac OS X)；安装包仅包含一个可执行文件，方便部署，与Docker等轻量级容器可无缝配合。 本文是Consul的入门介绍，并用一些例子说明如何使用Consul实现服务的注册和发现。 一、建立Consul Cluster 要想利用Consul提供的服务实现服务的注册与发现，我们需要建立Consul Cluster。在Consul方案中，每个提供服务的节点上都要部署和运行Consul的agent，所有运行Consul agent节点的集合构成Consul Cluster。Consul agent有两种运行模式：Server和Client。这里的Server和Client只是Consul集群层面的区分，与搭建在Cluster之上 的应用服务无关。以Server模式运行的Consul agent节点用于维护Consul集群的状态，官方建议每个Consul Cluster至少有3个或以上的运行在Server mode的Agent，Client节点不限。 每个数据中心的Consul Cluster都会在运行于server模式下的agent节点中选出一个Leader节点，这个选举过程通过Consul实现的raft协议保证，多个 server节点上的Consul数据信息是强一致的。处于client mode的Consul agent节点比较简单，无状态，仅仅负责将请求转发给Server agent节点。 下面我们就来搭建一个实验Consul Cluster。 实验环境和节点角色如下： n1(Ubuntu 14.04 x86_64): 10.10.105.71&#160; server mode n2(Ubuntu 12.04 x86_64): 10.10.126.101 server mode&#160;&#160;&#160; with Consul Web UI n3(Ubuntu 9.04 i386): 10.10.126.187&#160;&#160;&#160; client mode 在三台主机上分别下载和安装Consul包，安装包很简单，只是包含一个可执行文件consul。在n2主机上还要下载一份Consul Web UI包，支持图形化展示Consul cluster中的节点状态和服务状态。 Consul Cluster的启动过程如下： [...]]]></description>
			<content:encoded><![CDATA[<p><a href="https://github.com/hashicorp/consul">Consul</a>是<a href="https://www.hashicorp.com">HashiCorp</a>公司推出的开源工具，用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案，比如 <a href="https://www.airbnb.com/">Airbnb</a>的<a href="http://nerds.airbnb.com /smartstack-service-discovery-cloud/">SmartStack</a>等相比，Consul的方案更&ldquo;一站式&rdquo;，内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案，不再需要依赖其他工具（比如<a href="http://tonybai.com/tag/zookeeper">ZooKeeper</a>等）。使用起来也较 为简单。Consul用<a href="http://tonybai.com/tag/go">Golang</a>实现，因此具有天然可移植性(支持Linux、windows和Mac OS X)；安装包仅包含一个可执行文件，方便部署，与<a href="http://tonybai.com/tag/docker">Docker</a>等轻量级容器可<b>无缝配合</b>。</p>
<p>本文是Consul的入门介绍，并用一些例子说明如何使用Consul实现服务的注册和发现。</p>
<p><b>一、建立Consul Cluster</b></p>
<p>要想利用Consul提供的服务实现服务的注册与发现，我们需要建立Consul Cluster。在Consul方案中，每个提供服务的节点上都要部署和运行Consul的agent，所有运行Consul agent节点的集合构成Consul Cluster。Consul agent有两种运行模式：Server和Client。这里的Server和Client只是Consul集群层面的区分，与搭建在Cluster之上 的应用服务无关。以Server模式运行的Consul agent节点用于维护Consul集群的状态，官方建议每个Consul Cluster至少有3个或以上的运行在Server mode的Agent，Client节点不限。</p>
<p><img alt="" src="/wp-content/uploads/consul-arch.png" style="width: 520px; height: 538px;" /></p>
<p>每个数据中心的Consul Cluster都会在运行于server模式下的agent节点中选出一个Leader节点，这个选举过程通过Consul实现的raft协议保证，多个 server节点上的Consul数据信息是强一致的。处于client mode的Consul agent节点比较简单，无状态，仅仅负责将请求转发给Server agent节点。</p>
<p>下面我们就来搭建一个实验Consul Cluster。</p>
<p>实验环境和节点角色如下：</p>
<p><font face="Courier New">n1(Ubuntu 14.04 x86_64): 10.10.105.71&nbsp; server mode<br />
	n2(Ubuntu 12.04 x86_64): 10.10.126.101 server mode&nbsp;&nbsp;&nbsp; with Consul Web UI<br />
	n3(Ubuntu 9.04 i386): 10.10.126.187&nbsp;&nbsp;&nbsp; client mode</font></p>
<p>在三台主机上分别下载和安装Consul包，安装包很简单，只是包含一个可执行文件consul。在n2主机上还要下载一份Consul Web UI包，支持图形化展示Consul cluster中的节点状态和服务状态。</p>
<p>Consul Cluster的启动过程如下：</p>
<p><b>n1主机：</b></p>
<p><font face="Courier New">$ consul agent -server -bootstrap-expect 2 -data-dir /tmp/consul -node=n1 -bind=10.10.105.71 -dc=dc1<br />
	==&gt; WARNING: Expect Mode enabled, expecting 2 servers<br />
	==&gt; WARNING: It is highly recommended to set GOMAXPROCS higher than 1<br />
	==&gt; Starting Consul agent&#8230;<br />
	==&gt; Starting Consul agent RPC&#8230;<br />
	==&gt; Consul agent running!<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Node name: &#39;n1&#39;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Datacenter: &#39;dc1&#39;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Server: true (bootstrap: false)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Client Addr: 127.0.0.1 (HTTP: 8500, HTTPS: -1, DNS: 8600, RPC: 8400)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Cluster Addr: 10.10.105.71 (LAN: 8301, WAN: 8302)<br />
	&nbsp;&nbsp;&nbsp; Gossip encrypt: false, RPC-TLS: false, TLS-Incoming: false<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Atlas: &lt;disabled&gt;</font></p>
<p><font face="Courier New">==&gt; Log data will now stream in as it occurs:</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; 2015/07/03 09:18:25 [INFO] serf: EventMemberJoin: n1 10.10.105.71<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 09:18:25 [INFO] serf: EventMemberJoin: n1.dc1 10.10.105.71<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 09:18:25 [INFO] raft: Node at 10.10.105.71:8300 [Follower] entering Follower state<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 09:18:25 [INFO] consul: adding server n1 (Addr: 10.10.105.71:8300) (DC: dc1)<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 09:18:25 [INFO] consul: adding server n1.dc1 (Addr: 10.10.105.71:8300) (DC: dc1)<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 09:18:25 [ERR] agent: failed to sync remote state: No cluster leader<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 09:18:26 [WARN] raft: EnableSingleNode disabled, and no known peers. Aborting election.1</font></p>
<p><b>n2主机：</b></p>
<p><font face="Courier New">$ consul agent -server -bootstrap-expect 2 -data-dir /tmp/consul -node=n2 -bind=10.10.126.101 <b>-ui-dir ./dist</b>&nbsp; -dc=dc1<br />
	==&gt; WARNING: Expect Mode enabled, expecting 2 servers<br />
	==&gt; WARNING: It is highly recommended to set GOMAXPROCS higher than 1<br />
	==&gt; Starting Consul agent&#8230;<br />
	==&gt; Starting Consul agent RPC&#8230;<br />
	==&gt; Consul agent running!<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Node name: &#39;n2&#39;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Datacenter: &#39;dc1&#39;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Server: true (bootstrap: false)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Client Addr: 127.0.0.1 (HTTP: 8500, HTTPS: -1, DNS: 8600, RPC: 8400)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Cluster Addr: 10.10.126.101 (LAN: 8301, WAN: 8302)<br />
	&nbsp;&nbsp;&nbsp; Gossip encrypt: false, RPC-TLS: false, TLS-Incoming: false<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Atlas: &lt;disabled&gt;</font></p>
<p><font face="Courier New">==&gt; Log data will now stream in as it occurs:</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; 2015/07/03 11:30:32 [INFO] serf: EventMemberJoin: n2 10.10.126.101<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:30:32 [INFO] serf: EventMemberJoin: n2.dc1 10.10.126.101<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:30:32 [INFO] raft: Node at 10.10.126.101:8300 [Follower] entering Follower state<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:30:32 [INFO] consul: adding server n2 (Addr: 10.10.126.101:8300) (DC: dc1)<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:30:32 [INFO] consul: adding server n2.dc1 (Addr: 10.10.126.101:8300) (DC: dc1)<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:30:32 [ERR] agent: failed to sync remote state: No cluster leader<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:30:33 [WARN] raft: EnableSingleNode disabled, and no known peers. Aborting election.</font></p>
<p>从两个server agent的启动日志可以看出，n1、n2启动后并不知道集群其他节点的存在。以n1为例，通过consul members和consul info查看当前agent状态：</p>
<p><font face="Courier New">$ consul members<br />
	Node&nbsp; Address&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Status&nbsp; Type&nbsp;&nbsp;&nbsp; Build&nbsp; Protocol&nbsp; DC<br />
	n1&nbsp;&nbsp;&nbsp; 10.10.105.71:8301&nbsp; alive&nbsp;&nbsp; server&nbsp; 0.5.2&nbsp; 2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dc1</font></p>
<p><font face="Courier New">$ consul info<br />
	&#8230; &#8230;<br />
	consul:<br />
	&nbsp;&nbsp;&nbsp; bootstrap = false<br />
	&nbsp;&nbsp;&nbsp; known_datacenters = 1<br />
	&nbsp;&nbsp;&nbsp; leader = false<br />
	&nbsp;&nbsp;&nbsp; server = true<br />
	raft:<br />
	&nbsp;&nbsp;&nbsp; applied_index = 0<br />
	&nbsp;&nbsp;&nbsp; commit_index = 0<br />
	&nbsp;&nbsp;&nbsp; fsm_pending = 0<br />
	&nbsp;&nbsp;&nbsp; last_contact = never<br />
	&nbsp;&nbsp;&nbsp; last_log_index = 0<br />
	&nbsp;&nbsp;&nbsp; last_log_term = 0<br />
	&nbsp;&nbsp;&nbsp; last_snapshot_index = 0<br />
	&nbsp;&nbsp;&nbsp; last_snapshot_term = 0<br />
	&nbsp;&nbsp;&nbsp; num_peers = 0<br />
	&nbsp;&nbsp;&nbsp; state = <b>Follower</b><br />
	&nbsp;&nbsp;&nbsp; term = 0<br />
	&#8230; &#8230;</font></p>
<p>可以看出，n1上的agent当前状态是Follower，bootstrap = false；n2同样也是这个情况。整个Cluster并未完成Bootstrap过程。</p>
<p>我们用consul join命令触发Cluster bootstrap过程，我们在n1上执行如下命令：</p>
<p><font face="Courier New">$ consul join 10.10.126.101<br />
	Successfully joined cluster by contacting 1 nodes.</font></p>
<p>我们通过consul join子命令将当前节点加入包含成员10.10.126.101（也就是n2)的集群中去。命令执行结果通过n1和n2的日志可以观察到：</p>
<p><b>n1主机:</b></p>
<p><font face="Courier New">2015/07/03 09:29:48 [INFO] agent: (LAN) joining: [10.10.126.101]<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 09:29:48 [INFO] serf: EventMemberJoin: n2 10.10.126.101<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 09:29:48 [INFO] agent: (LAN) joined: 1 Err: &lt;nil&gt;<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 09:29:48 [INFO] consul: adding server n2 (Addr: 10.10.126.101:8300) (DC: dc1)<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 09:29:48 [INFO] consul: Attempting bootstrap with nodes: [10.10.126.101:8300 10.10.105.71:8300]<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 09:29:49 [INFO] consul: New leader elected: n2<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 09:29:50 [INFO] agent: Synced service &#39;consul&#39;</font></p>
<p><b>n2主机:</b></p>
<p><font face="Courier New">2015/07/03 11:40:53 [INFO] serf: EventMemberJoin: n1 10.10.105.71<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:40:53 [INFO] consul: adding server n1 (Addr: 10.10.105.71:8300) (DC: dc1)<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:40:53 [INFO] consul: Attempting bootstrap with nodes: [10.10.126.101:8300 10.10.105.71:8300]<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:40:54 [WARN] raft: Heartbeat timeout reached, starting election<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:40:54 [INFO] raft: Node at 10.10.126.101:8300 [Candidate] entering Candidate state<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:40:54 [INFO] raft: Election won. Tally: 2<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:40:54 [INFO] raft: Node at 10.10.126.101:8300 [Leader] entering Leader state<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:40:54 [INFO] consul: cluster leadership acquired<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:40:54 [INFO] consul: New leader elected: n2<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:40:54 [INFO] raft: pipelining replication to peer 10.10.105.71:8300<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:40:54 [INFO] consul: member &#39;n2&#39; joined, marking health alive<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:40:54 [INFO] consul: member &#39;n1&#39; joined, marking health alive<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 11:40:55 [INFO] agent: Synced service &#39;consul&#39;</font></p>
<p>join后，两台主机互相知道了对方，并进行了leader election过程，n2被选举为Leader。</p>
<p>在n2主机上通过consul info确认一下n2 agent的状态：</p>
<p><font face="Courier New">$consul info<br />
	&#8230; &#8230;<br />
	consul:<br />
	&nbsp;&nbsp;&nbsp; bootstrap = false<br />
	&nbsp;&nbsp;&nbsp; known_datacenters = 1<br />
	&nbsp;&nbsp;&nbsp; leader = true<br />
	&nbsp;&nbsp;&nbsp; server = true<br />
	raft:<br />
	&nbsp;&nbsp;&nbsp; applied_index = 10<br />
	&nbsp;&nbsp;&nbsp; commit_index = 10<br />
	&nbsp;&nbsp;&nbsp; fsm_pending = 0<br />
	&nbsp;&nbsp;&nbsp; last_contact = never<br />
	&nbsp;&nbsp;&nbsp; last_log_index = 10<br />
	&nbsp;&nbsp;&nbsp; last_log_term = 1<br />
	&nbsp;&nbsp;&nbsp; last_snapshot_index = 0<br />
	&nbsp;&nbsp;&nbsp; last_snapshot_term = 0<br />
	&nbsp;&nbsp;&nbsp; num_peers = 1<br />
	&nbsp;&nbsp;&nbsp; state = <b>Leader</b><br />
	&nbsp;&nbsp;&nbsp; term = 1<br />
	&#8230; &#8230;</font></p>
<p><font face="Courier New">$ consul members<br />
	Node&nbsp; Address&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Status&nbsp; Type&nbsp;&nbsp;&nbsp; Build&nbsp; Protocol&nbsp; DC<br />
	n2&nbsp;&nbsp;&nbsp; 10.10.126.101:8301&nbsp; alive&nbsp;&nbsp; server&nbsp; 0.5.2&nbsp; 2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dc1<br />
	n1&nbsp;&nbsp;&nbsp; 10.10.105.71:8301&nbsp;&nbsp; alive&nbsp;&nbsp; server&nbsp; 0.5.2&nbsp; 2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dc1</font></p>
<p>可以看到n2的state已经为Leader了，n1的state依旧是Follower。</p>
<p>到这里，n1和n2就成为了dc1这个数据中心Consul Cluster的两个节点，而且是用来维护集群状态的Server node。n2被选举为Leader，n1是Folllower。</p>
<p>如果作为Leader的n2退出集群，我们来看看集群状态会发生怎样变化。在n2上，我们通过consul leave命令告诉n2上的agent离开集群并退出：</p>
<p><font face="Courier New">$ consul leave<br />
	Graceful leave complete</font></p>
<p>n2上Agent的日志：</p>
<p><font face="Courier New">2015/07/03 14:04:40 [INFO] agent.rpc: Accepted client: 127.0.0.1:35853<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:04:40 [INFO] agent.rpc: Graceful leave triggered<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:04:40 [INFO] consul: server starting leave<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:04:40 [INFO] raft: Removed peer 10.10.105.71:8300, stopping replication (Index: 7)<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:04:40 [INFO] raft: Removed ourself, transitioning to follower<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:04:40 [INFO] raft: Node at 10.10.126.101:8300 [Follower] entering Follower state<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:04:40 [INFO] serf: EventMemberLeave: n2.dc1 10.10.126.101<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:04:40 [INFO] consul: cluster leadership lost<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:04:40 [INFO] raft: aborting pipeline replication to peer 10.10.105.71:8300<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:04:40 [INFO] consul: removing server n2.dc1 (Addr: 10.10.126.101:8300) (DC: dc1)<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:04:41 [INFO] serf: EventMemberLeave: n2 10.10.126.101<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:04:41 [INFO] consul: removing server n2 (Addr: 10.10.126.101:8300) (DC: dc1)<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:04:41 [INFO] agent: requesting shutdown<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:04:41 [INFO] consul: shutting down server<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:04:42 [INFO] agent: shutdown complete</font></p>
<p>n1上的日志：</p>
<p><font face="Courier New">2015/07/03 11:53:36 [INFO] serf: EventMemberLeave: n2 10.10.126.101<br />
	2015/07/03 11:53:36 [INFO] consul: removing server n2 (Addr: 10.10.126.101:8300) (DC: dc1)<br />
	2015/07/03 11:55:15 [ERR] agent: failed to sync remote state: No cluster leader</font></p>
<p>这个时候我们在n1上通过consul info查看，n1的状态依旧是Follower，也就是说在双server节点的集群下，一个server退出，将产生无Leader状态。在三 server节点集群里，Leader退出，其余两个会再协商选出一个新Leader，但一旦再退出一个节点，同样集群就不会再有Leader了。 当然，如果是单节点bootstrap的集群( -bootstrap-expect 1 )，集群只有一个server节点，那这个server节点自然当选Leader。</p>
<p>现在我们在n1上通过consul members查看集群状态：</p>
<p><font face="Courier New">$ consul members<br />
	Node&nbsp; Address&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Status&nbsp; Type&nbsp;&nbsp;&nbsp; Build&nbsp; Protocol&nbsp; DC<br />
	n1&nbsp;&nbsp;&nbsp; 10.10.105.71:8301&nbsp;&nbsp; alive&nbsp;&nbsp; server&nbsp; 0.5.2&nbsp; 2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dc1<br />
	n2&nbsp;&nbsp;&nbsp; 10.10.126.101:8301&nbsp; <b>left&nbsp;</b>&nbsp;&nbsp; server&nbsp; 0.5.2&nbsp; 2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dc1</font></p>
<p>执行结果显示：n2是Left状态。我们重新启动n2，再来看看集群的状态变化。</p>
<p><font face="Courier New">$ consul agent -server -bootstrap-expect 2 -data-dir /tmp/consul -node=n2 -bind=10.10.126.101 -ui-dir ./dist&nbsp; -dc=dc1<br />
	&#8230; &#8230;<br />
	==&gt; Log data will now stream in as it occurs:</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; 2015/07/03 14:13:46 [INFO] serf: EventMemberJoin: n2 10.10.126.101<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:13:46 [INFO] raft: Node at 10.10.126.101:8300 [Follower] entering Follower state<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:13:46 [INFO] consul: adding server n2 (Addr: 10.10.126.101:8300) (DC: dc1)<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:13:46 [INFO] serf: EventMemberJoin: n2.dc1 10.10.126.101<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:13:46 [INFO] consul: adding server n2.dc1 (Addr: 10.10.126.101:8300) (DC: dc1)<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:13:46 [ERR] agent: failed to sync remote state: No cluster leader<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:13:48 [WARN] raft: EnableSingleNode disabled, and no known peers. Aborting election.<br />
	&#8230; &#8230;</font></p>
<p>n2启动后，并未自动加入之前的cluster，而是依旧如第一次启动那样，看不到peers，孤立运行。</p>
<p>我们再来在n1上join一下：<font face="Courier New">consul join 10.10.126.101</font></p>
<p>n1的日志变为：</p>
<p><font face="Courier New">2015/07/03 12:04:55 [INFO] consul: adding server n2 (Addr: 10.10.126.101:8300) (DC: dc1)<br />
	2015/07/03 12:04:56 [ERR] agent: failed to sync remote state: No cluster leader</font></p>
<p>n2的日志变为：</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; 2015/07/03 14:16:00 [INFO] serf: EventMemberJoin: n1 10.10.105.71<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:16:00 [INFO] consul: adding server n1 (Addr: 10.10.105.71:8300) (DC: dc1)<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:16:00 [INFO] consul: New leader elected: n2<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:16:01 [ERR] agent: failed to sync remote state: No cluster leader</font></p>
<p>n1和n2无法再选出Leader，通过info命令看，两个节点都变成了Follower，集群仍然处于无Leader状态。</p>
<p>这个问题在consul的github repositroy issues中被多人多次提及，但作者似乎不将此作为bug。产生这个问题的原因是当n2退出时，consul会将/tmp/consul/raft /peers.json的内容由：</p>
<p><font face="Courier New">["10.10.105.71:8300", "10.10.126.101:8300"]</font></p>
<p>改为</p>
<p><font face="Courier New">null</font></p>
<p>n2重启后，该文件并未改变，依旧为null，n2启动就不会重新自动join到n1的cluster中。</p>
<p>关于这个问题的cluster恢复方法，官方在<a href="https://www.consul.io/docs/guides/outage.html">Outage Recovery</a>一文中有明确说明。我们来测试一下：</p>
<p>我们打开n1和n2的/tmp/consul/raft/peers.json，将其内容统一修改为：</p>
<p><font face="Courier New">["10.10.126.101:8300","10.10.105.71:8300"]</font></p>
<p>然后重启n2，但加上-rejoin命令：</p>
<p><font face="Courier New">$ consul agent -server -bootstrap-expect 2 -data-dir /tmp/consul -node=n2 -bind=10.10.126.101 -ui-dir ./dist&nbsp; -dc=dc1 <b>-rejoin</b></font></p>
<p>&#8230;. &#8230;</p>
<p>&nbsp;<font face="Courier New">&nbsp;&nbsp; 2015/07/03 14:56:02 [WARN] raft: Election timeout reached, restarting election<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:56:02 [INFO] raft: Node at 10.10.126.101:8300 [Candidate] entering Candidate state<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:56:02 [INFO] raft: Election won. Tally: 2<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:56:02 [INFO] raft: Node at 10.10.126.101:8300 [Leader] entering Leader state<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:56:02 [INFO] consul: cluster leadership acquired<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:56:02 [INFO] consul: New leader elected: n2</font><br />
	&#8230;&#8230;.</p>
<p>n1上的日志：</p>
<p><font face="Courier New">2015/07/03 12:44:52 [INFO] serf: EventMemberJoin: n2 10.10.126.101<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 12:44:52 [INFO] consul: adding server n2 (Addr: 10.10.126.101:8300) (DC: dc1)<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 12:44:54 [INFO] consul: New leader elected: n2<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 12:44:55 [WARN] raft: Rejecting vote from 10.10.126.101:8300 since we have a leader: 10.10.126.101:8300<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 12:44:56 [WARN] raft: Heartbeat timeout reached, starting election<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 12:44:56 [INFO] raft: Node at 10.10.105.71:8300 [Candidate] entering Candidate state<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 12:44:56 [ERR] raft: Failed to make RequestVote RPC to 10.10.126.101:8300: EOF<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 12:44:57 [INFO] raft: Node at 10.10.105.71:8300 [Follower] entering Follower state<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 12:44:57 [INFO] consul: New leader elected: n2</font></p>
<p>这回集群的Leader重新选举成功，集群状态恢复。</p>
<p>接下来我们启动n3上的client mode agent：</p>
<p><font face="Courier New">$ consul agent&nbsp; -data-dir /tmp/consul -node=n3 -bind=10.10.126.187&nbsp; -dc=dc1<br />
	==&gt; WARNING: It is highly recommended to set GOMAXPROCS higher than 1<br />
	==&gt; Starting Consul agent&#8230;<br />
	==&gt; Starting Consul agent RPC&#8230;<br />
	==&gt; Consul agent running!<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Node name: &#39;n3&#39;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Datacenter: &#39;dc1&#39;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Server: false (bootstrap: false)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Client Addr: 127.0.0.1 (HTTP: 8500, HTTPS: -1, DNS: 8600, RPC: 8400)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Cluster Addr: 10.10.126.187 (LAN: 8301, WAN: 8302)<br />
	&nbsp;&nbsp;&nbsp; Gossip encrypt: false, RPC-TLS: false, TLS-Incoming: false<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Atlas: &lt;disabled&gt;</font></p>
<p><font face="Courier New">==&gt; Log data will now stream in as it occurs:</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; 2015/07/03 14:55:17 [INFO] serf: EventMemberJoin: n3 10.10.126.187<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:55:17 [ERR] agent: failed to sync remote state: No known Consul servers</font></p>
<p>在n3上join n1后，n3的日志输出如下：</p>
<p><font face="Courier New">&nbsp;&nbsp; 2015/07/03 14:59:31 [INFO] agent: (LAN) joining: [10.10.105.71]<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:59:31 [INFO] serf: EventMemberJoin: n2 10.10.126.101<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:59:31 [INFO] serf: EventMemberJoin: n1 10.10.105.71<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:59:31 [INFO] agent: (LAN) joined: 1 Err: &lt;nil&gt;<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:59:31 [INFO] consul: adding server n2 (Addr: 10.10.126.101:8300) (DC: dc1)<br />
	&nbsp;&nbsp;&nbsp; 2015/07/03 14:59:31 [INFO] consul: adding server n1 (Addr: 10.10.105.71:8300) (DC: dc1)</font></p>
<p>n3上consul members可以查看到如下内容：</p>
<p><font face="Courier New">$ consul members<br />
	Node&nbsp; Address&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Status&nbsp; Type&nbsp;&nbsp;&nbsp; Build&nbsp; Protocol&nbsp; DC<br />
	n1&nbsp;&nbsp;&nbsp; 10.10.105.71:8301&nbsp;&nbsp; alive&nbsp;&nbsp; server&nbsp; 0.5.2&nbsp; 2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dc1<br />
	n3&nbsp;&nbsp;&nbsp; 10.10.126.187:8301&nbsp; alive&nbsp;&nbsp; client&nbsp; 0.5.2&nbsp; 2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dc1<br />
	n2&nbsp;&nbsp;&nbsp; 10.10.126.101:8301&nbsp; alive&nbsp;&nbsp; server&nbsp; 0.5.2&nbsp; 2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dc1</font></p>
<p>处于client mode的agent可以自由退出和启动，不会出现server mode下agent的问题。</p>
<p><b>二、服务</b><b>注册与发现</b></p>
<p>我们建立Consul Cluster是为了实现服务的注册和发现。Consul支持两种服务注册的方式，一种是通过Consul的服务注册HTTP API，由服务自身在启动后调用API注册自己，另外一种则是通过在配置文件中定义服务的方式进行注册。Consul文档中建议使用后面一种方式来做服务 配置和服务注册。</p>
<p>我们还是用例子来说明一下如何做服务配置。前面我们已经建立了Consul Cluster，Cluster里包含了三个Node：两个Server mode node，一个Client mode Node。我们计划在n2、n3上部署一类服务web3，于是我们需要分别在n2、n3上增加Consul agent的配置文件。</p>
<p>Consul agent在启动时可以通过<font face="Courier New">-config-dir</font>来指定配置文件所在目录，比如以n3为例，我们可以如此启动n3：</p>
<p><font face="Courier New">consul agent -data-dir /tmp/consul -node=n3 -bind=10.10.126.187 -dc=dc1 -config-dir=./conf</font></p>
<p>这样在<font face="Courier New">./conf</font>下的所有文件扩展为<font face="Courier New">.json</font>的文件都会被Consul agent作为配置文件读取。</p>
<p>我们以n3为例，我们在n3的consul agent的配置文件目录下创建web3.json文件：</p>
<p><font face="Courier New">//web3.json<br />
	{<br />
	&nbsp; &quot;service&quot;: {<br />
	&nbsp;&nbsp;&nbsp; &quot;name&quot;: &quot;web3&quot;,<br />
	&nbsp;&nbsp;&nbsp; &quot;tags&quot;: ["master"],<br />
	&nbsp;&nbsp;&nbsp; &quot;address&quot;: &quot;127.0.0.1&quot;,<br />
	&nbsp;&nbsp;&nbsp; &quot;port&quot;: 10000,<br />
	&nbsp;&nbsp;&nbsp; &quot;checks&quot;: [<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;http&quot;: &quot;http://localhost:10000/health&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;interval&quot;: &quot;10s&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; ]<br />
	&nbsp; }<br />
	}</font></p>
<p>这个配置就是我们在n3节点上为web3这个服务做的服务定义，定义中包含服务的name、address、port等，还包含一个服务检测的配置，这里 我们每隔10s对服务进行一次健康检查，这要求服务增加对/health的处理逻辑。同理，我们在n2上也建立同样配置文件（n2需重启，并带上 -config-dir命令行选项），<b>服务注册</b>就这么简单。</p>
<p>在重启后的n2、n3日志中，我们能发现如下的错误内容：</p>
<p><font face="Courier New">2015/07/06 13:48:11 [WARN] agent: http request failed &#39;http://localhost:10000/health&#39; : Get http://localhost:10000/health: dial tcp 127.0.0.1:10000: connect failed&quot;</font></p>
<p>这就是agent对定义的服务的check日志。为了避免这个错误日志刷屏，我们在n2、n3上各部署一个web3服务实例。以n3上的web3为例，其源码如下：</p>
<p><font face="Courier New">//web3.go<br />
	package main</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp; &quot;fmt&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;net/http&quot;<br />
	)</font></p>
<p><font face="Courier New">func handler(w http.ResponseWriter, r *http.Request) {<br />
	&nbsp;&nbsp;&nbsp; fmt.Println(&quot;hello Web3! This is n3&quot;)<br />
	&nbsp;&nbsp;&nbsp; fmt.Fprintf(w, &quot;Hello Web3! This is n3&quot;)<br />
	}</font></p>
<p><font face="Courier New">func healthHandler(w http.ResponseWriter, r *http.Request) {<br />
	&nbsp;&nbsp;&nbsp; fmt.Println(&quot;health check!&quot;)<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; http.HandleFunc(&quot;/&quot;, handler)<br />
	&nbsp;&nbsp;&nbsp; http.HandleFunc(&quot;/health&quot;, healthHandler)<br />
	&nbsp;&nbsp;&nbsp; http.ListenAndServe(&quot;:10000&quot;, nil)<br />
	}</font></p>
<p>一旦n2、n3上的web3服务实例启动，我们就可以尝试发现这些服务了。</p>
<p>Consul提供了两种发现服务的方式，一种是通过HTTP API查看存在哪些服务；另外一种是通过consul agent内置的DNS服务来做。两者的差别在于后者可以根据服务check的实时状态动态调整available服务节点列表。我们这里也着重说明适用 DNS方式进行服务发现的具体步骤。</p>
<p>在配置和部署完web3服务后，我们就可以通过DNS命令来查询服务的具体信息了。consul为服务编排的内置域名为 &ldquo;NAME.service.consul&quot;，这样我们的web3的域名为:web3.service.consul。我们在n1通过dig工具来查看一 下，注意是在n1上，n1上并未定义和部署web3服务，但集群中服务的信息已经被同步到n1上了，信息是一致的：</p>
<p><font face="Courier New">$ dig @127.0.0.1 -p 8600 web3.service.consul SRV</font></p>
<p><font face="Courier New">; &lt;&lt;&gt;&gt; DiG 9.9.5-3-Ubuntu &lt;&lt;&gt;&gt; @127.0.0.1 -p 8600 web3.service.consul SRV<br />
	; (1 server found)<br />
	;; global options: +cmd<br />
	;; Got answer:<br />
	;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: NOERROR, id: 6713<br />
	;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 2<br />
	;; WARNING: recursion requested but not available</font></p>
<p><font face="Courier New">;; QUESTION SECTION:<br />
	;web3.service.consul.&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; IN&nbsp;&nbsp;&nbsp; SRV</font></p>
<p><font face="Courier New">;; ANSWER SECTION:<br />
	web3.service.consul.&nbsp;&nbsp;&nbsp; 0&nbsp;&nbsp;&nbsp; IN&nbsp;&nbsp;&nbsp; SRV&nbsp;&nbsp;&nbsp; 1 1 10000 n2.node.dc1.consul.<br />
	web3.service.consul.&nbsp;&nbsp;&nbsp; 0&nbsp;&nbsp;&nbsp; IN&nbsp;&nbsp;&nbsp; SRV&nbsp;&nbsp;&nbsp; 1 1 10000 n3.node.dc1.consul.</font></p>
<p><font face="Courier New">;; ADDITIONAL SECTION:<br />
	n2.node.dc1.consul.&nbsp;&nbsp;&nbsp; 0&nbsp;&nbsp;&nbsp; IN&nbsp;&nbsp;&nbsp; A&nbsp;&nbsp;&nbsp; 127.0.0.1<br />
	n3.node.dc1.consul.&nbsp;&nbsp;&nbsp; 0&nbsp;&nbsp;&nbsp; IN&nbsp;&nbsp;&nbsp; A&nbsp;&nbsp;&nbsp; 127.0.0.1</font></p>
<p><font face="Courier New">;; Query time: 2 msec<br />
	;; SERVER: 127.0.0.1#8600(127.0.0.1)<br />
	;; WHEN: Mon Jul 06 12:12:53 CST 2015<br />
	;; MSG SIZE&nbsp; rcvd: 219</font></p>
<p>可以看到在ANSWER SECTION中，我们得到了两个结果：n2和n3上各有一个web3的服务。在dig命令中我们用了SRV标志，那是因为我们需要的服务信息不仅有ip地址，还需要有端口号。</p>
<p>现在我们停掉n2上的web3服务，10s后，我们再来查一下：</p>
<p><font face="Courier New">$ dig @127.0.0.1 -p 8600 web3.service.consul SRV</font></p>
<p><font face="Courier New">; &lt;&lt;&gt;&gt; DiG 9.9.5-3-Ubuntu &lt;&lt;&gt;&gt; @127.0.0.1 -p 8600 web3.service.consul SRV<br />
	; (1 server found)<br />
	;; global options: +cmd<br />
	;; Got answer:<br />
	;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: NOERROR, id: 25136<br />
	;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1<br />
	;; WARNING: recursion requested but not available</font></p>
<p><font face="Courier New">;; QUESTION SECTION:<br />
	;web3.service.consul.&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; IN&nbsp;&nbsp;&nbsp; SRV</font></p>
<p><font face="Courier New">;; ANSWER SECTION:<br />
	web3.service.consul.&nbsp;&nbsp;&nbsp; 0&nbsp;&nbsp;&nbsp; IN&nbsp;&nbsp;&nbsp; SRV&nbsp;&nbsp;&nbsp; 1 1 10000 n3.node.dc1.consul.</font></p>
<p><font face="Courier New">;; ADDITIONAL SECTION:<br />
	n3.node.dc1.consul.&nbsp;&nbsp;&nbsp; 0&nbsp;&nbsp;&nbsp; IN&nbsp;&nbsp;&nbsp; A&nbsp;&nbsp;&nbsp; 127.0.0.1</font></p>
<p><font face="Courier New">;; Query time: 3 msec<br />
	;; SERVER: 127.0.0.1#8600(127.0.0.1)<br />
	;; WHEN: Mon Jul 06 12:16:39 CST 2015<br />
	;; MSG SIZE&nbsp; rcvd: 128</font></p>
<p>结果显示，只有n3上这一个web3服务可用了。通过下面Consul Agent日志：</p>
<p><font face="Courier New">dns: node &#39;n2&#39; failing health check &#39;service web3&#39; check&#39;, dropping from service &#39;web3&#39;</font></p>
<p>我们可以看到consul agent将health check失败的web3从结果列表中剔除了，这样web3服务的客户端在服务发现过程中就只能获取到当前可用的web3服务节点了，这个好处是在实际应 用中大大降低了客户端实现&rdquo;服务发现&ldquo;时的难度。另外consul agent DNS在返回查询结果时也支持DNS Server常见的策略，至少是支持轮询。你可以多次执行dig命令，可以看到n2和n3的排列顺序是不同的。还有一点值得注意的是：由于考虑DNS cache对consul agent查询结果的影响，默认情况下所有由consul agent返回的结果TTL值均设为0，也就是说不支持dns结果缓存。</p>
<p>接下来，我们使用golang实现一个demo级别的服务发现的客户端，这里会用到第三方dns client库&quot;<font face="Courier New">github.com/miekg/dns</font>&quot;。</p>
<p><font face="Courier New">// servicediscovery.go<br />
	package main</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp; &quot;fmt&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;log&quot;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; &quot;github.com/miekg/dns&quot;<br />
	)</font></p>
<p><font face="Courier New">const (<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; srvName = &quot;web3.service.consul&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; agentAddr = &quot;127.0.0.1:8600&quot;<br />
	)</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; c := new(dns.Client)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; m := new(dns.Msg)<br />
	&nbsp;&nbsp;&nbsp; m.SetQuestion(dns.Fqdn(srvName), dns.TypeSRV)<br />
	&nbsp;&nbsp;&nbsp; m.RecursionDesired = true</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; r, _, err := c.Exchange(m, agentAddr)<br />
	&nbsp;&nbsp;&nbsp; if r == nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Fatalf(&quot;dns query error: %s\n&quot;, err.Error())<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; if r.Rcode != dns.RcodeSuccess {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Fatalf(&quot;dns query error: %v\n&quot;, r.Rcode)<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp; for _, a := range r.Answer {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b, ok := a.(*dns.SRV)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ok {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m.SetQuestion(dns.Fqdn(b.Target), dns.TypeA)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r1, _, err := c.Exchange(m, agentAddr)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if r1 == nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Fatalf(&quot;dns query error: %v, %v\n&quot;, r1.Rcode, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for _, a1 := range r1.Answer {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c, ok := a1.(*dns.A)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ok {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%s &#8211; %s:%d\n&quot;, b.Target, c.A, b.Port)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>我们执行该程序：<br />
	<font face="Courier New">$ go run servicediscovery.go<br />
	n2.node.dc1.consul. &#8211; 10.10.126.101:10000<br />
	n3.node.dc1.consul. &#8211; 10.10.126.187:10000</font></p>
<p>注意各个node上的服务check是由其node上的agent上进行的，一旦那个node上的agent出现问题，则位于那个node上的所有 service也将会被置为unavailable状态。比如我们停掉n3上的agent，那么我们在进行web3服务节点查询时，就只能获取到n2这一 个节点上有可用的web3服务了。</p>
<p>在真实的程序中，我们可以像上面demo中那样，每Request都做一次DNS查询，不过这样的代价也很高。稍复杂些，我们可以结合dns结果本地缓存+定期查询+每遇到Failed查询的方式来综合考量服务的发现方法或利用Consul提供的watch命令等。</p>
<p>以上仅仅是Consul的一个入门。真实场景中，理想的方案需要考虑的事情还有很多。Consul自身目前演进到0.5.2版本，还有不完善之处，但它已 经被很多公司用于production环境。Consul不是孤立的，要充分发挥出Consul的优势，在真实方案中，我们还要考虑与 <a href="https://www.docker.com/">Docker</a>，HAProxy，Mesos等工具的结合。</p>
<p style='text-align:left'>&copy; 2015, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2015/07/06/implement-distributed-services-registery-and-discovery-by-consul/feed/</wfw:commentRss>
		<slash:comments>11</slash:comments>
		</item>
		<item>
		<title>2013小结</title>
		<link>https://tonybai.com/2014/01/04/my-summary-of-2013/</link>
		<comments>https://tonybai.com/2014/01/04/my-summary-of-2013/#comments</comments>
		<pubDate>Fri, 03 Jan 2014 23:54:46 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[生活簿]]></category>
		<category><![CDATA[2013]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Kindle]]></category>
		<category><![CDATA[Memcached]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[ZooKeeper]]></category>
		<category><![CDATA[个人总结]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[咨询]]></category>
		<category><![CDATA[女儿]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[工作总结]]></category>
		<category><![CDATA[年终总结]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[感悟]]></category>
		<category><![CDATA[生活]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[读书]]></category>
		<category><![CDATA[豆瓣]]></category>
		<category><![CDATA[跑步]]></category>
		<category><![CDATA[转型]]></category>
		<category><![CDATA[香港]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1470</guid>
		<description><![CDATA[2013年的个人年终总结比以往来得晚了一些，至于原因，我也说不清楚，拖延症也罢，其他原因也罢，总之是晚了。 写年终小结已经有小几年了，风格一直如一，无非是老三样：工作得失、生活酸甜以及新年展望，今年也不利外。 * 工作篇 我们部门在所在行业里已经摸爬滚打了10多年了，经 历和见证了这个行业从诞生、增长、成熟到如今的衰退的整个过程。也正是由于处于行业的衰退期，2013年部门的运营十分艰难。十年对于任何一个行业来说， 可能都已经过了其巅峰期，真心不能再期望这个行业还能会有下一个高峰了，对于个人来说也是如此。转型、业务突破变成了领导常挂在嘴边的词汇，但做起来又何 其艰难。 2013年，我们的业务转型依旧是围绕着我们的&#8220;金主&#8221;，虽然他们的业务营收也受到了微信等OTT业务的极大影响，传统业务投资也在缩减。对于个人而言， 除了负责传统产品线，转型、业务突破也成了我的绩效目标的一部分。于是在2013年，写文档比写代码多了一点，出差比常年多了一点，周六周日的连续加班也 多出了一点。在这些尝试中，以5月份某运营商某信的重构项目最为让人印象深刻。为了这个合同额几个亿的项目，我们近30人连续奋战了一个多月编写技术建议 书和投标方案，过程辛苦但却颇感充实，最终我们拿到了两个第二、两个第三的成绩。也许这个结果对于公司来说算是一种失败，但对于我个人来说，我获得了些许 转型的信心，以至于在后续的几次投标资料编写过程中，面对较新的领域，我也可以镇定自若。 掐指算来，这一年我以咨询顾问的临时角色参与了8个大大小小项目的前期交流以及投标支持工作，其中六个标以失败或不了了之而告终，还有两个标尚未有最终结 果。对于这样的结果，我也只能表示无奈。虽然我心里也十分清楚，对于国内这类解决方案项目的投标，技术往往不是最重要的，况且对于这些新领域，我们的技术 储备还不够系统，积累较为浅薄，落地的也的确较少。但面对这样的局面，我们还能怎么做呢？我也期待新一年能得到一个新的答案。 当然2013的工作中不全是遗憾，年末之前新系统的上线算是为我的2013划上了一个还算不错的句号，毕竟这是我两年来为之付出最多，也是最重要的一个工作目标。另外2013年继续整理和总结自己的一些管理经验和工作原则。在过程方面继续深入改善，尤其在代码质量方面。 在技术精深方面，今年没有太多进步。年初的时候曾探讨过如何在现有项目中使用一些成熟的开源技术和产品，比如memcached、zookeeper等， 为了保持手热，还尝试做些算法类的编码，这个在experiments库中有体现。在其他方面，可谓是&#8220;三无状态&#8221;：无技术书籍翻译、无技术杂志投稿、无 新开源项目发起。另外今年没有尝试去学习什么新语言，理由在此。 在年末的绩效评审时，观察到一些现象：那些绩效最末尾的人，往往并非是自身不够努力，而是领导赋予的目标不明确，这会给下属带来更多的不安，多数下属也会因为工作目标的不明确，而表现出更为糟糕的绩效。 * 生活篇 我个人十分注重工作和生活的平衡，不知道这种理念对于一个革命尚未成功的人来说算不算正确。 今年写了56篇博文，只完成了计划值的3/4，算是可接受范围，博文质量有所提升，访问次数和评论反馈也多了许多。文章以技术理解偏多，深入的偏少。技术攻关还是留给年轻人去吧。另外就是经验总结和感悟偏多，这也许与工作年头多有关系吧。 读书方面，据豆瓣不完全统计一共读了61本，这照比去年要多出不少，想必是有了Kindle PaperWhite的缘故吧，使得碎片时间得到充分利用。技术、商业书籍依旧占较大比例，小说尤其是科幻小说也不少。同样是因为电子书，今年纸质书籍购 买减少了（痛定思痛后的决定），双十一、双十二以及圣诞促销均没有出手。不过豆瓣上想读的书单依旧还有上百本^_^，任重道远啊。 今年爱上了跑步，坚持到11月末，因出差和天气转深寒等原因，决定暂停一段时间，等春节后气温回升时再拾起这个好习惯，相信不是大问题。跑步的确让我的身体状况大为好转，至少感冒次数大为下降。 &#160; &#160;&#160;&#160;&#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; 今年的一些家庭目标也多已实现，比如和老婆一起去香港、带孩子去海边玩等。数码装备也更新了一圈。 果果这个小家伙那叫一个茁壮成长啊。年中给她换了一个大的幼儿园，她也变得十分喜欢和小朋友在一起玩了，有时候还觉得在家里没有意思。每周果果还要上一节 她最喜欢的舞蹈课，我们的初衷就是让她多与小朋友老师接触，也不指望她能学出什么样子来，不过她学得倒是有模有样，十分认真。现在的果果简直就是一个小大 人，每天从早到晚说个不停，精力那叫一个充沛，有时候不得不强迫她去睡觉^_^。 * 新年展望 感觉这一年的进步有些差强人意，心底真心感觉自己的努力还是太少了，于是立下了&#8220;少睡觉，多干活&#8221;的目标。 新的一年，无论是个人还是工作，都要更多的思考如何将知识、技能和经验转化为更多价值，如何将业务经验、技术积累转化为合同。 新的一年，要主动适应转型，无论是工作上的还是个人方向上的，争取在这一年里能找到正确的方向，并成功入门。最好给自己做一个三年到五年的布局。 新的一年，尝试继续保持生活与工作的平衡，也许这将变成一种奢侈的期望。 新的一年，还有什么比全家健康快乐更重要的呢。 &#169; 2014, bigwhite. 版权所有.]]></description>
			<content:encoded><![CDATA[<p>2013年的个人年终总结比以往来得晚了一些，至于原因，我也说不清楚，拖延症也罢，其他原因也罢，总之是晚了。</p>
<p>写年终小结已经有小几年了，风格一直如一，无非是老三样：工作得失、生活酸甜以及新年展望，今年也不利外。</p>
<p><b>* 工作篇</b></p>
<p>我们部门在所在行业里已经摸爬滚打了10多年了，经 历和见证了这个行业从诞生、增长、成熟到如今的衰退的整个过程。也正是由于处于行业的衰退期，2013年部门的运营十分艰难。十年对于任何一个行业来说， 可能都已经过了其巅峰期，真心不能再期望这个行业还能会有下一个高峰了，对于个人来说也是如此。转型、业务突破变成了领导常挂在嘴边的词汇，但做起来又何 其艰难。</p>
<p>2013年，我们的业务转型依旧是围绕着我们的&ldquo;金主&rdquo;，虽然他们的业务营收也受到了微信等OTT业务的极大影响，传统业务投资也在缩减。对于个人而言， 除了负责传统产品线，转型、业务突破也成了我的绩效目标的一部分。于是在2013年，写文档比写代码多了一点，出差比常年多了一点，周六周日的连续加班也 多出了一点。在这些尝试中，以5月份某运营商某信的重构项目最为让人印象深刻。为了这个合同额几个亿的项目，我们近30人连续奋战了一个多月编写技术建议 书和投标方案，过程辛苦但却颇感充实，最终我们拿到了两个第二、两个第三的成绩。也许这个结果对于公司来说算是一种失败，但对于我个人来说，我获得了些许 转型的信心，以至于在后续的几次投标资料编写过程中，面对较新的领域，我也可以镇定自若。</p>
<p>掐指算来，这一年我以咨询顾问的临时角色参与了8个大大小小项目的前期交流以及投标支持工作，其中六个标以失败或不了了之而告终，还有两个标尚未有最终结 果。对于这样的结果，我也只能表示无奈。虽然我心里也十分清楚，对于国内这类解决方案项目的投标，技术往往不是最重要的，况且对于这些新领域，我们的技术 储备还不够系统，积累较为浅薄，落地的也的确较少。但面对这样的局面，我们还能怎么做呢？我也期待新一年能得到一个新的答案。</p>
<p>当然2013的工作中不全是遗憾，年末之前<a href="http://tonybai.com/2013/12/26/just-for-being-relieved/">新系统的上线</a>算是为我的2013划上了一个还算不错的句号，毕竟这是我两年来为之付出最多，也是最重要的一个工作目标。另外2013年继续整理和总结自己的一些<a href="http://tonybai.com/2013/08/04/more-thoughts-on-improving-efficiency/">管理经验</a>和<a href="http://tonybai.com/2013/08/19/my-personal-work-principles/">工作原则</a>。在过程方面继续深入改善，尤其在<a href="http://tonybai.com/2013/07/08/code-review-from-rule-of-man-to-rule-of-law/">代码质量</a>方面。</p>
<p>在技术精深方面，今年没有太多进步。年初的时候曾探讨过如何在现有项目中使用一些成熟的开源技术和产品，比如<a href="http://tonybai.com/2013/11/01/a-case-of-applying-memcached-cas/">memcached</a>、<a href="http://tonybai.com/2013/08/28/implement-config-sync-for-distributed-system-with-zookeeper-services/">zookeeper</a>等， 为了保持手热，还尝试做些算法类的编码，这个在<a href="https://github.com/bigwhite/experiments">experiments库</a>中有体现。在其他方面，可谓是&ldquo;三无状态&rdquo;：无技术书籍翻译、无技术杂志投稿、无 新开源项目发起。另外今年没有尝试去学习什么新语言，理由<a href="http://tonybai.com/2013/10/22/some-experience-about-learning-programming-language/">在此</a>。</p>
<p>在年末的绩效评审时，观察到一些现象：那些绩效最末尾的人，往往并非是自身不够努力，而是领导赋予的目标不明确，这会给下属带来更多的不安，多数下属也会因为工作目标的不明确，而表现出更为糟糕的绩效。</p>
<p><b>* 生活篇</b></p>
<p>我个人十分注重工作和生活的平衡，不知道这种理念对于一个革命尚未成功的人来说算不算正确。</p>
<p>今年写了56篇博文，只完成了计划值的3/4，算是可接受范围，博文质量有所提升，访问次数和评论反馈也多了许多。文章以技术理解偏多，深入的偏少。技术攻关还是留给年轻人去吧。另外就是经验总结和感悟偏多，这也许与工作年头多有关系吧。</p>
<p>读书方面，据豆瓣不完全统计一共读了61本，这照比去年要多出不少，想必是有了Kindle PaperWhite的缘故吧，使得碎片时间得到充分利用。技术、商业书籍依旧占较大比例，小说尤其是科幻小说也不少。同样是因为电子书，今年纸质书籍购 买减少了（痛定思痛后的决定），双十一、双十二以及圣诞促销均没有出手。不过豆瓣上想读的书单依旧还有上百本^_^，任重道远啊。</p>
<p>今年<a href="http://tonybai.com/2013/10/09/love-running/">爱上了跑步</a>，坚持到11月末，因出差和天气转深寒等原因，决定暂停一段时间，等春节后气温回升时再拾起这个好习惯，相信不是大问题。跑步的确让我的身体状况大为好转，至少感冒次数大为下降。 &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;</p>
<p>今年的一些家庭目标也多已实现，比如<a href="http://tonybai.com/2013/06/18/a-hongkong-macau-trip/">和老婆一起去香港</a>、带孩子去海边玩等。数码装备也更新了一圈。</p>
<p>果果这个小家伙那叫一个茁壮成长啊。年中给她换了一个大的幼儿园，她也变得十分喜欢和小朋友在一起玩了，有时候还觉得在家里没有意思。每周果果还要上一节 她最喜欢的舞蹈课，我们的初衷就是让她多与小朋友老师接触，也不指望她能学出什么样子来，不过她学得倒是有模有样，十分认真。现在的果果简直就是一个小大 人，每天从早到晚说个不停，精力那叫一个充沛，有时候不得不强迫她去睡觉^_^。</p>
<p><b>* 新年展望</b></p>
<p>感觉这一年的进步有些差强人意，心底真心感觉自己的努力还是太少了，于是立下了&ldquo;少睡觉，多干活&rdquo;的目标。</p>
<p>新的一年，无论是个人还是工作，都要更多的思考如何将知识、技能和经验转化为更多价值，如何将业务经验、技术积累转化为合同。</p>
<p>新的一年，要主动适应转型，无论是工作上的还是个人方向上的，争取在这一年里能找到正确的方向，并成功入门。最好给自己做一个三年到五年的布局。</p>
<p>新的一年，尝试继续保持生活与工作的平衡，也许这将变成一种奢侈的期望。</p>
<p>新的一年，还有什么比全家健康快乐更重要的呢。</p>
<p style='text-align:left'>&copy; 2014, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/01/04/my-summary-of-2013/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>利用ZooKeeper服务实现分布式系统的配置数据同步</title>
		<link>https://tonybai.com/2013/08/28/implement-config-sync-for-distributed-system-with-zookeeper-services/</link>
		<comments>https://tonybai.com/2013/08/28/implement-config-sync-for-distributed-system-with-zookeeper-services/#comments</comments>
		<pubDate>Wed, 28 Aug 2013 11:32:14 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[leader-election]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Oracle]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[trigger]]></category>
		<category><![CDATA[ZooKeeper]]></category>
		<category><![CDATA[分布式系统]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[学习]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[触发器]]></category>
		<category><![CDATA[选主算法]]></category>
		<category><![CDATA[配置同步]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1383</guid>
		<description><![CDATA[很多时候，一旦习惯了某些事情，也就习惯了它们的恶劣，习惯了它们的丑陋，习惯了它们&#8220;赋予&#8221;你的各种痛苦。 &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#8211; Tony Bai 一、痼疾难解 曾几何时，在那个还没有集群化，没有分布式的时代，它还是一个不错的方案，至少在线上没有暴露出太多问题，它也不在我们关注的重点范围之内。但随 着集群化、分布式的新版本的到来，那一大坨遗留的代码就变得格外让人不顺眼，同时问题也随之在线上暴露开来了。 [...]]]></description>
			<content:encoded><![CDATA[<p><i>很多时候，一旦习惯了某些事情，也就习惯了它们的恶劣，习惯了它们的丑陋，习惯了它们&ldquo;赋予&rdquo;你的各种痛苦。<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &#8211; Tony Bai</i></p>
<p><b>一、痼疾难解</b></p>
<p>曾几何时，在那个还没有集群化，没有分布式的时代，它还是一个不错的方案，至少在线上没有暴露出太多问题，它也不在我们关注的重点范围之内。但随 着集群化、分布式的新版本的到来，那一大坨遗留的代码就变得格外让人不顺眼，同时问题也随之在线上暴露开来了。</p>
<p>这里的&ldquo;它&rdquo;指的就是我们目前的业务配置数据同步方案。简单描述这个方案如下：</p>
<p>* 方案涉及两个角色 &#8211; 数据库(DB)与应用节点（app_node)；<br />
	* 所有的业务配置数据均统一存储在DB中；<br />
	* 应用节点在启动后从DB中读取最新业务配置数据；<br />
	* 应用节点运行过程中，如果DB中的业务配置数据发生变更（增/删/改），DB中的触发器(trigger)将会执行。在触发器的脚本中，触发器将会【串 行】地与每个应用节点建立TCP链接，并将业务配置表的变更信息发给各个应用节点。 应用节点会接收并【解析】触发器发过来变更数据包，并同步到自己的本地内存中。这样就达到了运行时更新配置的目的。</p>
<p>上面我用【】标记了两个关键词：&ldquo;串行&rdquo;和&ldquo;解析&rdquo;。这两个词隐含有这个方案的两个主要问题。</p>
<p>&ldquo;串行&rdquo; &#8211; 意味着每一次DB的业务配置数据变更，trigger脚本都要逐个与应用节点建立链接并收发数据。当应用节点逐渐增多时，每一次业务数据同步都会相当地耗 时。尤其是当某个应用节点所在主机出现问题时，到该节点链接建立的过程会阻塞，导致整个业务配置数据同步的时间达到无法忍受的地步。</p>
<p>&ldquo;解析&rdquo; &#8211; 我们自定义了trigger与应用节点之间的协议包。协议包中包含了每次变更的详细信息，比如在某个表添加一条记录，trigger会将这个记录的每个字 段信息排成一行打包发给应用节点。应用节点收到这个包后，会根据已有的表字段信息对该包进行解析。看得出这是一个很强的耦合：表字段一旦修 改，trigger脚本要修改，应用节点的解析函数要修改，还要考虑协议包中表字段的排序。如果应用节点解析时与trigger脚本打包时的字段 顺序不同的话，那就可能出现严重错误，而且这种错误有时难于校验并难于发现。</p>
<p><b>二、曾经的努力</b></p>
<p>针对这个方案的不足，我们曾经也做过改进，但主要针对的是解决&ldquo;串行&rdquo;这个问题上。</p>
<p>第一次改进：同步的发起能否并行做？trigger脚本能否并行发起对各个应用节点的链接建立请求？</p>
<p>Java组同事对trigger脚本做了改进。让trigger脚本调用function，而function中又调用了写好的Java方 法，Java代码由DB加载到环境中。在Java方法中创建多个同步线程，并发与各应用节点建立链接并发送数据。这个方法的确可以变&ldquo;串行&rdquo;为 &ldquo;并行&rdquo;，但不知为何生产环境中实际运行时偶尔会出现异常，该异常发生在DB中，影响很大。有时还会导致DB的一些异常现象。至今原因尚未明确， 我们无奈退回到以前的方案。</p>
<p>第二次改进：从Push模式到Pull模式</p>
<p>在之前部门新规划的一个产品中，开发人员对数据同步的机制做了重新的设计，将原来的Push模式改为了Pull模式。大致方案是：<br />
	&nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp; * 业务数据变更时，trigger直接将变更内容（以老方案中那个协议包的打包格式）写到一个&ldquo;变更日志表&rdquo;中，每条记录有一个唯一的序号，序号递增。<br />
	&nbsp;&nbsp;&nbsp; * 应用节点启动后，从DB加载最新配置信息，查询&ldquo;变更日志表&rdquo;，得到该表内最新的一条记录的序号n。<br />
	&nbsp;&nbsp;&nbsp; * 应用节点以&ldquo;轮询&rdquo;的方式定期查询&ldquo;变更日志表&rdquo;，并读取和解析那些序号比序号n更新的记录；更新完后，将继续保存最新的一条记录序号。<br />
	&nbsp;&nbsp;&nbsp; * 数据库中有job定期对&ldquo;变更日志表&rdquo;中的记录进行过期删除处理。</p>
<p>个人感觉第二个方案应该是理想方案的一个雏形，虽然目前它的同步更新可能不是那么及时，与DB交互过多（方案细节中每个应用节点在处理完一条记录 后还要更新记录的状态）。该方案设计者也完全也可以放弃那个导致耦合的协议包设计，但他最终还是选择保留了原有协议包解析函数。目前该方案在产品 环境下运行还算良好，并未暴露出什么问题。这算是一次有效的改进，也为本文中要提到的方案提供了一些思路启示。</p>
<p><b>三、与时俱进</b></p>
<p><a href="http://zookeeper.apache.org">ZooKeeper</a>生来就具备解决分布式系统的配置分发和同步的能力。利用ZooKeeper服务实现分布式系统的统一配置中心已经不是那么新鲜 的话题了。最简单的模型莫过于将配置数据存储在ZooKeeper上的路径节点上，然后应用节点在这些配置节点上添加watch。当配置数据变更 时，每个应用节点都可以及时得到通知，同步到最新数据。这种模型对于一些量少简单的系统配置来说较为合适。对于我们每个表动辄上万条配置的情形似 乎不那么适合，想象一下每个应用节点要添加上万个watch，这对ZooKeeper而言也是压力山大啊。因此用ZooKeeper提供的诸多服 务如何来优化我们上面提到的两个主要问题呢？这里提出一种方案仅供参考。</p>
<p>方案示意图：</p>
<p>DB&nbsp; &#8212;-&gt; <b>Config Center Services</b>(css_agent + ZooKeeper)&nbsp; &#8212;&gt; App Node</p>
<p>在新方案中，我们要：<br />
	&nbsp;&nbsp;&nbsp; 保留 &#8211; 保留trigger脚本，作为业务数据变更的唯一的触发起点；<br />
	&nbsp;&nbsp;&nbsp; 摒弃 &#8211; 摒弃那个复杂的带来耦合的协议格式；<br />
	&nbsp;&nbsp;&nbsp; 借鉴 &#8211; 借鉴&ldquo;Push -&gt; Pull&rdquo;的数据获取方式。</p>
<p>新方案中除了DB、应用节点(app_node)外，新增加了一个角色Config Center Services(缩写为ccs），ccs由ZooKeeper + ccs_agent的集群组成。简单起见，每个ZooKeeper节点上部署一个ccs_agent。这些角色之间的数据流和指令流关系，即该方案的原理 如下：</p>
<p>&nbsp;&nbsp;&nbsp; <b>* 初始化</b><br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8211; ZooKeeper集群启动；<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8211; ccs_agent启动，利用ZooKeeper提供的<a href="http://tonybai.com/2013/08/23/leader-election-using-zookeeper/">leader election服务</a>，选出ccs_agent leader。ccs_agent leader启动后负责在ZooKeeper中建立业务配置表node，比如：表employee_info_tab对应的node路径为&ldquo;/ccs /foo_app/employee_info_tab&rdquo;；<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8211; ccs_agent启动后会监听一个端口，用来接受DB trigger向其发起的数据链接；<br />
	&nbsp;&nbsp; &nbsp;&nbsp; &nbsp; &#8211; 应用节点启动，监听ZooKeeper上所有（数量有限的）业务配置表node的child event；<br />
	&nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp; <b>* 数据变更</b><br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8211; DB中某业务表比如employee_info_tab增加了一条id为&quot;1234567&quot;的记录；<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8211; 触发器启动，向ccs_agent cluster中任意一个可用的节点建立链接，并将数据包&ldquo;^employee_info_tab|ADD|1234567$&quot;发送给 ccs_agent；<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8211; ccs_agent收取并解析trigger发来的数据包，在对应的/ccs/foo_app/employee_info_tab下建立<b>ZOO_SEQUENCE</b>类 型节点&ldquo;item-000000000&rdquo;，该节点的值为&ldquo;ADD 1234567&quot;；<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8211; ZooKeeper将/ccs/foo_app/employee_info_tab节点的child事件发给所有watch该节点事件的应用节点；<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8211; 应用节点&ldquo;取出&rdquo;/ccs/foo_app/employee_info_tab节点下的children节点&quot;item-000000000&quot;，并读取 其值，后续到DB的employee_info_tab中将id = 1234567的这条记录select出来，将该条记录更新到本地内存中。应用节点记录下处理过的当下节点id为&quot;item-000000000&quot;；<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; DB业务表employee_info_tab又增加了两条记录，id分别为&quot;7777777&quot;和&quot;8888888&quot;，经过上面描述的流程，/ccs /foo_app/employee_info_tab节点下会增加&quot;item-000000001&quot;和&quot;item-000000002&quot;两项； 应用节点最终会收到child事件通知。应用节点&ldquo;取出&rdquo;/ccs/foo_app/employee_info_tab节点下的所有 children节点并排序。之后，处理那些id号大于&quot;item-000000000&quot;的节点，并将当前节点id记录为&ldquo;item- 000000002&quot;。依次类推。</p>
<p>&nbsp;&nbsp;&nbsp; <b>* 过期处理</b><br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8211; ccs_agent leader负责定期扫描ZooKeeper中/ccs下各个表节点下的子项，对于超出过期时间的item进行删除处理。</p>
<p>&nbsp;&nbsp;&nbsp; <b>* 应用节点重启</b><br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; -&nbsp; 应用节点重启后，会首先从db读取最新信息，并记录启动时间戳；<br />
	&nbsp; &nbsp; &nbsp; &nbsp; -&nbsp; 应用节点重启后，在收到zookeeper的数据变更事件后，会根据当前时间戳与变更表节点下的item创建时间进行比较，并仅处理比启动时间戳新的 item的数据。<br />
	&nbsp;&nbsp;&nbsp;</p>
<p>这个方案主要利用了ZooKeeper提供的leader election服务以及sequence节点的特性，几点好处在于：</p>
<p>&nbsp;&nbsp;&nbsp; &#8211; 串行通知变为并行通知，且通知到达及时；<br />
	&nbsp;&nbsp;&nbsp; &#8211; 变更数据的Push模式为Pull模式，降低了或去除了诸多耦合，包括：<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 1) 去除trigger脚本与表字段及字段顺序的耦合；<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 2) 去除应用节点与表字段顺序的耦合；<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 3) 降低应用节点与表字段构成的耦合。<br />
	&nbsp;&nbsp;&nbsp; &#8211; 应用节点无需复杂的包解析，简化后期维护。</p>
<p>当然为了该方案新增若干网元会给产品部署和维护带来一些复杂性，这算是不足之处吧。</p>
<p><b>四、Demo</b></p>
<p><a href="https://github.com/bigwhite/experiments/tree/master/zookeeper_c_bindings_examples/config_center_services">这里</a>有一个600多行代码的Demo，模拟新方案中几个角色：<br />
	&nbsp;&nbsp;&nbsp; DB &#8211; trigger_sim.py<br />
	&nbsp;&nbsp;&nbsp; 应用节点 &#8211; app.c<br />
	&nbsp;&nbsp;&nbsp; ccs_agent &#8211; ccs_agent.c</p>
<p>模拟的步骤大致如下（单机版）：</p>
<p>a) 启动ZooKeeper<br />
	<font face="Courier New">&nbsp;&nbsp;&nbsp; $&gt; zkServer.sh start<br />
	&nbsp;&nbsp;&nbsp; JMX enabled by default<br />
	&nbsp;&nbsp;&nbsp; Using config: /home1/tonybai/.bin/zookeeper-3.4.5/bin/../conf/zoo.cfg<br />
	&nbsp;&nbsp;&nbsp; Starting zookeeper &#8230; STARTED</font></p>
<p>b) 启动ccs_agent<br />
	<font face="Courier New">&nbsp;&nbsp;&nbsp; $&gt; ccs_agent<br />
	&nbsp;&nbsp;&nbsp; This is [ccs-member0000000037], i am a leader<br />
	&nbsp;&nbsp;&nbsp; /ccs node exists<br />
	&nbsp;&nbsp;&nbsp; /ccs/employee_info_tab node exists<br />
	&nbsp;&nbsp;&nbsp; /ccs/boss_info_tab node exists<br />
	&nbsp;&nbsp;&nbsp; trigger listen thread start up!<br />
	&nbsp;&nbsp;&nbsp; item expire thread start up!</font></p>
<p>c) 启动app</p>
<p>d) 使用trigger_sim.py模拟DB触发trigger<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <font face="Courier New">$&gt; trigger_sim.py employee_info_tab ADD 1234567</font></p>
<p>可以看到ccs_agent输出结果如下：<br />
	<font face="Courier New">&nbsp;&nbsp;&nbsp; table[employee_info_tab], oper_type[ADD], id[1234567]</font></p>
<p>app的输出如下：<br />
	<font face="Courier New">&nbsp;&nbsp;&nbsp; child event happened: type[4]<br />
	&nbsp;&nbsp;&nbsp; item-0000000015<br />
	&nbsp;&nbsp;&nbsp; employee_info_tab: execute [ADD 1234567]</font></p>
<p>大约30s后，ccs_agent会输出如下：<br />
	<font face="Courier New">&nbsp;&nbsp;&nbsp; [expire]: employee_info_tab: expire [item-0000000015]</font></p>
<p>模拟步骤在README里有写。这里仅是Demo代码，存在硬编码以及异常处理考虑不全面的情况，不要拍砖哦。</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/08/28/implement-config-sync-for-distributed-system-with-zookeeper-services/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>利用ZooKeeper服务实现分布式系统的Leader选举</title>
		<link>https://tonybai.com/2013/08/23/leader-election-using-zookeeper/</link>
		<comments>https://tonybai.com/2013/08/23/leader-election-using-zookeeper/#comments</comments>
		<pubDate>Fri, 23 Aug 2013 14:31:02 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[BAT]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[ZooKeeper]]></category>
		<category><![CDATA[分布式系统]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[学习]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[选主算法]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1377</guid>
		<description><![CDATA[每次与Java组的同事们坐下来谈技术、谈理想、谈人生时，Java组的同事总会向我们投来羡慕的眼光：卧槽！又是自己开发的工具，太NB了。这时C程序 员们的脸上就会洋溢出自豪的笑容，然后内心骂道：谁让我们没有现成的呢。另一个空间里的某些&#8220;无C不欢&#8221;们或者某些&#8220;C Guru&#8221;们会骂道：靠，有了也不用，自己写！ 有时候，C程序员真的有一种下意识：不情愿使用其他语言开发的工具、框架或服务，且比其他程序员更爱&#8220;重新发明轮子&#8221;（有利有弊）。也许这是某种 骨子里的自负在搞怪；另外一个极端：今天和我聊天的一个经验丰富的C程序员还在忧虑：如果离职是否有公司会要他:(。 其实这个时代的C程序员一直活得挺纠结^_^。 这个世界，软硬件发展日新月异，越来越多的后端程序用Java等其他语言实现。Java高级选手在这个世界上也甚是吃香，这个你看看各大招聘网站 就知道了。再听听坊间&#8220;BAT&#8221;三巨头给出的高高在上的offer价格，也可以看出Java程序员是多么的有&#8220;钱途&#8221;和受欢迎了。当然拿好offer的前提是你的Java底子不薄。 其实无论用什么编程语言，成为牛人后，钱途也都是杠杠的。 没有什么好的开场白，于是有了上面一些&#8220;胡言乱语&#8221;。我们言归正传。 本文是一篇初级技术博文。讲的是如何使用ZooKeeper C API通过ZooKeeper的服务实现分布式系统的Leader选举。当然这一试验是为了尝试解决我们自己的分布式系统在集中配置数据分发这一环节上的 一个&#8220;固疾&#8221;。还好我还不那么纠结，也没有重新实现ZooKeeper的冲动，于是我就用了ZooKeeper这一Java实现的成熟的分布式 系统的服务框架。 * 搭建ZooKeeper服务环境 &#160;&#160;&#160; &#8211; 下载官方stable release版本 &#8211; ZooKeeper3.4.5。解压后，将$ZooKeeper_INSTALL_PATH/bin加入到PATH变量中（其中ZooKeeper_INSTALL_PATH为解压后ZooKeeper-3.4.5目录的绝对路径）。 &#160;&#160;&#160; &#8211; 试验环境下，最简单的ZooKeeper用法就是使用单机版。 &#160;&#160;&#160; &#160; 进入到$ZooKeeper_INSTALL_PATH/conf下，将zoo_sample.cfg改名为zoo.cfg，即可作为单机版ZooKeeper的配置文件。当然你也可以像我一样随意修改修改： &#160;&#160;&#160; &#160; # The number of milliseconds of each tick &#160;&#160; tickTime=2000 &#160;&#160; # The number of ticks that the initial &#160;&#160; # synchronization phase can [...]]]></description>
			<content:encoded><![CDATA[<p>每次与Java组的同事们坐下来谈技术、谈理想、谈人生时，Java组的同事总会向我们投来羡慕的眼光：卧槽！又是自己开发的工具，太NB了。这时C程序 员们的脸上就会洋溢出自豪的笑容，然后内心骂道：谁让我们没有现成的呢。另一个空间里的某些&ldquo;无C不欢&rdquo;们或者某些&ldquo;C Guru&rdquo;们会骂道：靠，有了也不用，自己写！</p>
<p>有时候，<a href="http://tonybai.com/tag/c">C程序员</a>真的有一种下意识：不情愿使用其他语言开发的工具、框架或服务，且比其他程序员更爱&ldquo;<a href="http://tonybai.com/2012/11/02/treat-reinventing-the-wheel-dialectically">重新发明轮子</a>&rdquo;（有利有弊）。也许这是某种 骨子里的自负在搞怪；另外一个极端：今天和我聊天的一个经验丰富的C程序员还在忧虑：如果离职是否有公司会要他:(。</p>
<p>其实这个时代的C程序员一直活得<b>挺纠结</b>^_^。</p>
<p>这个世界，软硬件发展日新月异，越来越多的后端程序用Java等其他语言实现。Java高级选手在这个世界上也甚是吃香，这个你看看各大招聘网站 就知道了。再听听坊间&ldquo;BAT&rdquo;三巨头给出的高高在上的offer价格，也可以看出Java程序员是多么的有&ldquo;钱途&rdquo;和受欢迎了。当然拿好offer的前提是你的Java底子不薄。</p>
<p>其实无论用什么编程语言，成为牛人后，钱途也都是杠杠的。</p>
<p>没有什么好的开场白，于是有了上面一些&ldquo;胡言乱语&rdquo;。我们言归正传。</p>
<p>本文是一篇初级技术博文。讲的是如何使用<a href="http://zookeeper.apache.org">ZooKeeper</a> C API通过ZooKeeper的服务实现分布式系统的Leader选举。当然这一试验是为了尝试解决我们自己的分布式系统在集中配置数据分发这一环节上的 一个&ldquo;固疾&rdquo;。还好我还不那么纠结，也没有重新实现ZooKeeper的冲动，于是我就用了ZooKeeper这一Java实现的成熟的分布式 系统的服务框架。</p>
<p><b>* 搭建ZooKeeper服务环境</b></p>
<p>&nbsp;&nbsp;&nbsp; &#8211; 下载官方stable release版本 &#8211; ZooKeeper3.4.5。解压后，将$<i>ZooKeeper_INSTALL_PATH</i>/bin加入到PATH变量中（其中ZooKeeper_INSTALL_PATH为解压后ZooKeeper-3.4.5目录的绝对路径）。</p>
<p>&nbsp;&nbsp;&nbsp; &#8211; 试验环境下，最简单的ZooKeeper用法就是使用单机版。<br />
	&nbsp;&nbsp;&nbsp; &nbsp; 进入到$ZooKeeper_INSTALL_PATH/conf下，将zoo_sample.cfg改名为zoo.cfg，即可作为单机版ZooKeeper的配置文件。当然你也可以像我一样随意修改修改：</p>
<p>&nbsp;&nbsp;&nbsp; &nbsp; <font face="Courier New"># The number of milliseconds of each tick<br />
	&nbsp;&nbsp; tickTime=2000<br />
	&nbsp;&nbsp; # The number of ticks that the initial<br />
	&nbsp;&nbsp; # synchronization phase can take<br />
	&nbsp;&nbsp; initLimit=5<br />
	&nbsp;&nbsp; # The number of ticks that can pass between<br />
	&nbsp;&nbsp; # sending a request and getting an acknowledgement<br />
	&nbsp;&nbsp; syncLimit=2</font></p>
<p><font face="Courier New">&nbsp;&nbsp; dataDir=/home/tonybai/proj/myZooKeeper<br />
	&nbsp;&nbsp; # the port at which the clients will connect<br />
	&nbsp;&nbsp; clientPort=2181</font><br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 如果你要体验多机版ZooKeeper服务，那你还要继续动动手脚，以双机版为例，假设有两个ZooKeeper节点(10.0.0.13和10.0.0.14)：</p>
<p>&nbsp;&nbsp;&nbsp; &nbsp; 10.0.0.13上的ZooKeeper节点1的配置文件如下：</p>
<p>&nbsp;&nbsp;&nbsp;<font face="Courier New">&nbsp; # The number of milliseconds of each tick<br />
	&nbsp;&nbsp; tickTime=2000<br />
	&nbsp;&nbsp; # The number of ticks that the initial<br />
	&nbsp;&nbsp; # synchronization phase can take<br />
	&nbsp;&nbsp; initLimit=5<br />
	&nbsp;&nbsp; # The number of ticks that can pass between<br />
	&nbsp;&nbsp; # sending a request and getting an acknowledgement<br />
	&nbsp;&nbsp; syncLimit=2</font></p>
<p><font face="Courier New">&nbsp;&nbsp; dataDir=/home/tonybai/proj/myZooKeeper<br />
	&nbsp;&nbsp; # the port at which the clients will connect<br />
	&nbsp;&nbsp; clientPort=2181</font></p>
<p><font face="Courier New">&nbsp;&nbsp; server.1=10.0.0.13:2888:3888&nbsp;<br />
	&nbsp;&nbsp; server.2=10.0.0.14:2888:3888</font></p>
<p>&nbsp;&nbsp;&nbsp;&nbsp; 10.0.0.14上的ZooKeeper节点2的配置文件如下：</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp; <font face="Courier New"># The number of milliseconds of each tick<br />
	&nbsp;&nbsp; tickTime=2000<br />
	&nbsp;&nbsp; # The number of ticks that the initial<br />
	&nbsp;&nbsp; # synchronization phase can take<br />
	&nbsp;&nbsp; initLimit=5<br />
	&nbsp;&nbsp; # The number of ticks that can pass between<br />
	&nbsp;&nbsp; # sending a request and getting an acknowledgement<br />
	&nbsp;&nbsp; syncLimit=2</font></p>
<p><font face="Courier New">&nbsp;&nbsp; dataDir=/home/tonybai/proj/myZooKeeper<br />
	&nbsp;&nbsp; # the port at which the clients will connect<br />
	&nbsp;&nbsp; clientPort=2181</font></p>
<p><font face="Courier New">&nbsp;&nbsp; server.1=10.0.0.13:2888:3888<br />
	&nbsp;&nbsp; server.2=10.0.0.14:2888:3888</font></p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 别忘了在每个节点的dataDir下分别创建一个myid文件：<br />
	&nbsp;&nbsp;&nbsp; &nbsp; 在10.0.0.13节点1上执行：<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;<font face="Courier New"> $&gt; echo 1 &gt; myid</font></p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 在10.0.0.14节点2上执行：<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
	<font face="Courier New">&nbsp;&nbsp; $&gt; echo 2 &gt; myid</font></p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 启动ZooKeeper执行：<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <font face="Courier New">$&gt; zkServer.sh start</font></p>
<p>&nbsp;&nbsp;&nbsp; &nbsp; 模拟一个客户端连到ZooKeeper服务上：<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <font face="Courier New">$&gt; zkCli.sh</font></p>
<p>&nbsp;&nbsp;&nbsp; &nbsp; 成功链接后，你将进入一个命令行交互界面：<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;<font face="Courier New"> [zk: 10.0.0.13:2181(CONNECTED) 1] help<br />
	&nbsp;&nbsp;&nbsp; ZooKeeper -server host:port cmd args<br />
	&nbsp;&nbsp;&nbsp; connect host:port<br />
	&nbsp;&nbsp;&nbsp; get path [watch]<br />
	&nbsp;&nbsp;&nbsp; ls path [watch]<br />
	&nbsp;&nbsp;&nbsp; set path data [version]<br />
	&nbsp;&nbsp;&nbsp; rmr path<br />
	&nbsp;&nbsp;&nbsp; delquota [-n|-b] path&nbsp; </font><br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;</p>
<p><b>* 选主原理</b></p>
<p>&nbsp;&nbsp; ZooKeeper在选主过程中提供的服务就好比一栋名为&quot;/election&quot;小屋，小屋只有一个门，各节点只能通过这个门逐个进入。每个节点进入后， 都会被分配唯一编号(member-n)，编号n自小到大递增，节点编号最小的自封为Leader，其他节点只能做跟班的（follower) &#8211; 这年头还是小的吃香：原配干不过小三儿，小三儿干不过小四儿，不是么^_^！）。<br />
	&nbsp;&nbsp; 每当一个节点离开，ZooKeeper都会通知屋内的所有节点，屋内节点收到通知后再次判断一下自己是否是屋内剩余节点中编号最小的节点，如果是，则自封为Leader，否则为Follower。</p>
<p>&nbsp;&nbsp; 再用稍正式的语言重述一遍：</p>
<p>&nbsp;&nbsp; 各个子节点同时在某个ZooKeeper数据路径/election下建立&quot;ZOO_SEQUENCE|ZOO_EPHEMERAL&quot;节点 &#8211; member，且各个节点监视(Watch) /election路径的子路径的变更事件。ZooKeeper的sequence节点特性保证节点创建时会被从小到大加上编号。同时节点的 ephemeral特性保证一旦子节点宕机或异常停掉，其对应的member节点会被ZooKeeper自动删除，而其他节点会收到该变更通知，重新判定 自己是leader还是follower以及谁才是真正的leader。</p>
<p><b>* 示例代码</b></p>
<p>关于ZooKeeper的C API的使用资料甚少，但这里就偏偏要用C API举例。</p>
<p>C API的安装方法：进入$ZOOKEEPER_INSTALL_PATH/src/c下面，configure-&gt;make-&gt;make install即可。</p>
<p>ZooKeeper的C API分为同步与异步两种模式，这里简单起见用的都是同步机制。代码不多，索性全贴出来。在<a href="https://github.com/bigwhite/experiments">这里</a>能checkout到全部代码。</p>
<p><font face="Courier New">/* election.c */</font><br />
	<font face="Courier New">#include &lt;stdio.h&gt;<br />
	#include &lt;stdlib.h&gt;<br />
	#include &lt;string.h&gt;<br />
	#include &lt;unistd.h&gt;<br />
	#include &quot;zookeeper.h&quot;</font></p>
<p><font face="Courier New">static int<br />
	is_leader(zhandle_t* zkhandle, char *myid);</font></p>
<p><font face="Courier New">static void<br />
	get_node_name(const char *buf, char *node);</font></p>
<p><font face="Courier New">struct watch_func_para_t {<br />
	&nbsp;&nbsp;&nbsp; zhandle_t *zkhandle;<br />
	&nbsp;&nbsp;&nbsp; char node[64];<br />
	};</font></p>
<p><font face="Courier New">void<br />
	election_children_watcher(zhandle_t* zh, int type, int state,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const char* path, void* watcherCtx)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; int ret = 0;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; struct watch_func_para_t* para= (struct watch_func_para_t*)watcherCtx;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; struct String_vector strings;<br />
	&nbsp;&nbsp;&nbsp; struct Stat stat;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; /* 重新监听 */<br />
	&nbsp;&nbsp;&nbsp; ret = zoo_wget_children2(para-&gt;zkhandle, &quot;/election&quot;, election_children_watcher,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; watcherCtx, &amp;strings, &amp;stat);<br />
	&nbsp;&nbsp;&nbsp; if (ret) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fprintf(stderr, &quot;child: zoo_wget_children2 error [%d]\n&quot;, ret);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; exit(EXIT_FAILURE);<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; /* 判断主从 */<br />
	&nbsp;&nbsp;&nbsp; if (is_leader(para-&gt;zkhandle, para-&gt;node))<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;This is [%s], i am a leader\n&quot;, para-&gt;node);<br />
	&nbsp;&nbsp;&nbsp; else<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;This is [%s], i am a follower\n&quot;, para-&gt;node);</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; return;<br />
	}</font></p>
<p><font face="Courier New">void def_election_watcher(zhandle_t* zh, int type, int state,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const char* path, void* watcherCtx)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;Something happened.\n&quot;);<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;type: %d\n&quot;, type);<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;state: %d\n&quot;, state);<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;path: %s\n&quot;, path);<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;watcherCtx: %s\n&quot;, (char *)watcherCtx);<br />
	}</font></p>
<p><font face="Courier New">int<br />
	main(int argc, const char *argv[])<br />
	{</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; const char* host = &quot;10.0.0.13:2181&quot;;<br />
	&nbsp;&nbsp;&nbsp; zhandle_t* zkhandle;<br />
	&nbsp;&nbsp;&nbsp; int timeout = 5000;<br />
	&nbsp;&nbsp;&nbsp; char buf[512] = {0};<br />
	&nbsp;&nbsp;&nbsp; char node[512] = {0};</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; zoo_set_debug_level(ZOO_LOG_LEVEL_WARN);<br />
	&nbsp;&nbsp;&nbsp; zkhandle = zookeeper_init(host, def_election_watcher, timeout,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0, &quot;Zookeeper examples: election&quot;, 0);<br />
	&nbsp;&nbsp;&nbsp; if (zkhandle == NULL) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fprintf(stderr, &quot;Connecting to zookeeper servers error&#8230;\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; exit(EXIT_FAILURE);<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; /* 在/election下创建member节点 */<br />
	&nbsp;&nbsp;&nbsp; int ret = zoo_create(zkhandle,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/election/member&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;hello&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 5,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &amp;ZOO_OPEN_ACL_UNSAFE,&nbsp; /* a completely open ACL */<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ZOO_SEQUENCE|ZOO_EPHEMERAL,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; buf,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sizeof(buf)-1);<br />
	&nbsp;&nbsp;&nbsp; if (ret) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fprintf(stderr, &quot;zoo_create error [%d]\n&quot;, ret);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; exit(EXIT_FAILURE);<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; get_node_name(buf, node);<br />
	&nbsp;&nbsp;&nbsp; /* 判断当前是否是Leader节点 */<br />
	&nbsp;&nbsp;&nbsp; if (is_leader(zkhandle, node)) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;This is [%s], i am a leader\n&quot;, node);<br />
	&nbsp;&nbsp;&nbsp; } else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;This is [%s], i am a follower\n&quot;, node);<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; struct Stat stat;<br />
	&nbsp;&nbsp;&nbsp; struct String_vector strings;<br />
	&nbsp;&nbsp;&nbsp; struct watch_func_para_t para;<br />
	&nbsp;&nbsp;&nbsp; memset(&amp;para, 0, sizeof(para));<br />
	&nbsp;&nbsp;&nbsp; para.zkhandle = zkhandle;<br />
	&nbsp;&nbsp;&nbsp; strcpy(para.node, node);</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; /* 监视/election的所有子节点事件 */<br />
	&nbsp;&nbsp;&nbsp; ret = zoo_wget_children2(zkhandle, &quot;/election&quot;, election_children_watcher, &amp;para, &amp;strings, &amp;stat);<br />
	&nbsp;&nbsp;&nbsp; if (ret) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fprintf(stderr, &quot;zoo_wget_children2 error [%d]\n&quot;, ret);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; exit(EXIT_FAILURE);<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; /* just wait for experiments*/<br />
	&nbsp;&nbsp;&nbsp; sleep(10000);</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; zookeeper_close(zkhandle);<br />
	}</font></p>
<p><font face="Courier New">static int<br />
	is_leader( zhandle_t* zkhandle, char *myid)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; int ret = 0;<br />
	&nbsp;&nbsp;&nbsp; int flag = 1;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; struct String_vector strings;<br />
	&nbsp;&nbsp;&nbsp; ret = zoo_get_children(zkhandle, &quot;/election&quot;, 0, &amp;strings);<br />
	&nbsp;&nbsp;&nbsp; if (ret) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fprintf(stderr, &quot;Error %d for %s\n&quot;, ret, &quot;get_children&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; exit(EXIT_FAILURE);<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; /* 计数 */<br />
	&nbsp;&nbsp;&nbsp; for (int i = 0;&nbsp; i &lt; strings.count; i++) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (strcmp(myid, strings.data[i]) &gt; 0) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; flag = 0;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; break;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; return flag;<br />
	}</font></p>
<p><font face="Courier New">static void<br />
	get_node_name(const char *buf, char *node)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; const char *p = buf;<br />
	&nbsp;&nbsp;&nbsp; int i;<br />
	&nbsp;&nbsp;&nbsp; for (i = strlen(buf) &#8211; 1; i &gt;= 0; i&#8211;) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (*(p + i) == &#39;/&#39;) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; break;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; strcpy(node, p + i + 1);<br />
	&nbsp;&nbsp;&nbsp; return;<br />
	}</font></p>
<p>编译这个代码：<br />
	<font face="Courier New">$&gt; gcc -g -std=gnu99 -o election election.c -DTHREADED -I/usr/local/include/zookeeper -lzookeeper_mt -lpthread</font></p>
<p>验证时，我们在不同窗口启动三次election程序：</p>
<p>窗口1， election启动：</p>
<p><font face="Courier New">$&gt; election<br />
	Something happened.<br />
	type: -1<br />
	state: 3<br />
	path:<br />
	watcherCtx: Zookeeper examples: election<br />
	This is [member0000000001], i am a leader</font></p>
<p>窗口2，election启动：</p>
<p><font face="Courier New">$&gt; election<br />
	Something happened.<br />
	type: -1<br />
	state: 3<br />
	path:<br />
	watcherCtx: Zookeeper examples: election<br />
	This is [member0000000002], i am a follower</font></p>
<p>此时窗口1中的election也会收到/election的字节点增加事件，并给出响应：</p>
<p><font face="Courier New">This is [member0000000001], i am a leader</font></p>
<p>同理当窗口3中的election启动时，窗口1和2中的election都能收到变动通知，并给予响应。</p>
<p>我们现在停掉窗口1中的election，大约5s后，我们在窗口2中看到：</p>
<p><font face="Courier New">This is [member0000000002], i am a leader</font></p>
<p>在窗口3中看到：</p>
<p><font face="Courier New">This is [member0000000003], i am a follower</font></p>
<p>可以看出窗口2和3中的election程序又做了一次自我选举。结果窗口2中的election由于节点编号最小而被选为Leader。</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/08/23/leader-election-using-zookeeper/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
