什么是注册中心
注册中心主要有三种角色:
服务提供者(RPC Server):在启动时,向 Registry 注册自身服务,并向 Registry 定期发送心跳汇报存活状态。
服务消费者(RPC Client):在启动时,向 Registry 订阅服务,把 Registry 返回的服务节点列表缓存在本地内存中,并与 RPC Sever 建立连接。
服务注册中心(Registry):用于保存 RPC Server 的注册信息,当 RPC Server 节点发生变更时,Registry 会同步变更,RPC Client 感知后会刷新本地 内存中缓存的服务节点列表。
最后,RPC Client 从本地缓存的服务节点列表中,基于负载均衡算法选择一台 RPC Sever 发起调用。
注册中心需要实现功能:
Zookeeper注册中心原理
Zookeeper可以充当一个服务注册表(Service Registry),让多个服务提供者形成一个集群,让服务消费者通过服务注册表获取具体的服务访问地址(Ip+端口)去访问具体的服务提供者。如下图所示:
Zookeeper注册流程
每当一个服务提供者部署后都要将自己的服务注册到zookeeper的某一路径上: /{service}/{version}/{ip:port} 。
比如我们的HelloWorldService部署到两台机器,那么Zookeeper上就会创建两条目录:
/HelloWorldService/1.0.0/100.19.20.01:16888
/HelloWorldService/1.0.0/100.19.20.02:16888
这么描述有点不好理解,我们以dubbo为例(具体示例可以参考文章【Dubbo系列1】Dubbo与Spring的集成),下图更直观:
在zookeeper中,进行服务注册,实际上就是在zookeeper中创建了一个znode节点,该节点存储了该服务的IP、端口、调用方式(协议、序列化方式)等。该节点承担着最重要的职责,它由服务提供者发布服务时创建,以供服务消费者获取节点中的信息,从而定位到服务提供者真正网络拓扑位置以及得知如何调用。
RPC服务注册/发现过程简述如下:
1.服务提供者启动时,会将其服务名称,ip地址注册到配置中心。
2.服务消费者在第一次调用服务时,会通过注册中心找到相应的服务的IP地址列表,并缓存到本地,以供后续使用。当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从IP列表中取一个服务提供者的服务器调用服务。
3.当服务提供者的某台服务器宕机或下线时,相应的ip会从服务提供者IP列表中移除。同时,注册中心会将新的服务IP地址列表发送给服务消费者机器,缓存在消费者本机。
4.当某个服务的所有服务器都下线了,那么这个服务也就下线了。
5.同样,当服务提供者的某台服务器上线时,注册中心会将新的服务IP地址列表发送给服务消费者机器,缓存在消费者本机。
6.服务提供方可以根据服务消费者的数量来作为服务下线的依据。
Zookeeper的心跳检测
问题:第3步中“当服务提供者的某台服务器宕机或下线时”,zookeeper如何感知到呢?
zookeeper提供了“心跳检测”功能,它会定时向各个服务提供者发送一个请求(实际上建立的是一个 socket 长连接),如果长期没有响应,服务中心就认为该服务提供者已经“挂了”,并将其剔除。
比如100.100.0.237这台机器如果宕机了,那么zookeeper上的路径就会只剩/HelloWorldService/1.0.0/100.100.0.238:16888。
EPHEMERAL-临时节点
临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与zookeeper连接断开不一定会话失效),那么这个客户端创建的所有临时节点都会被移除。
Zookeeper的Watch机制
问题:第3步和第5步中“注册中心会将新的服务IP地址列表发送给服务消费者机器”,这步是如何实现的呢?
这个问题也是经典的生产者-消费者问题,解决的方式有两种:
主动拉取策略:服务的消费者定期调用注册中心提供的服务获取接口获取最新的服务列表并更新本地缓存,经典案例就是Eureka。
发布-订阅模式:服务消费者能够实时监控服务更新状态,通常采用监听器以及回调机制。
Zookeeper使用的是“发布-订阅模式”,这里就要提到Zookeeper的Watch机制,整体流程如下:
客户端先向ZooKeeper服务端成功注册想要监听的节点状态,同时客户端本地会存储该监听器相关的信息在WatchManager中;
当ZooKeeper服务端监听的数据状态发生变化时,ZooKeeper就会主动通知发送相应事件信息给相关会话客户端,客户端就会在本地响应式的回调相关Watcher的Handler。
上面讲的有点抽象,大白话解读一下,Zookeeper的Watch机制其实就是一种推拉结合的模式:
服务消费者会去监听相应路径(/HelloWorldService/1.0.0),一旦路径上的数据有任务变化(增加或减少),Zookeeper只会发送一个事件类型和节点信息给关注的客户端,而不会包括具体的变更内容,所以事件本身是轻量级的,这就是推的部分。
收到变更通知的客户端需要自己去拉变更的数据,这就是拉的部分。
1.什么是Watcher监听机制
Watcher 监听机制是 Zookeeper 中非常重要的特性,我们基于 zookeeper 上创建的节点,可以对这些节点绑定监听事件,比如可以监听节点数据变更、节点删除、子节点状态变更等事件,通过这个事件机制,可以基于 zookeeper实现分布式锁,发布订阅(多个订阅者同时监听某一个主题对象,当这个主题对象自身状态发生变化时,会通知所有订阅者)等功能。
Watcher 特性:当数据发生变化的时候, zookeeper 会产生一个 watcher 事件,并且会发送到客户端。但是客户端只会收到一次通知。如果后续这个节点再次发生变化,那么之前设置 watcher 的客户端不会再次收到消息。(Watcher 是一次性的操作)。 当然,可以通过循环监听去达到永久监听效果。
如何注册事件机制:
ZooKeeper 的 Watcher 机制,总的来说可以分为三个过程:客户端注册 Watcher、服务器处理 Watcher 和客户端回调 Watcher客户端。注册 watcher 有 3 种方式,getData、exists、getChildren;
ZK的所有读操作都可以设置watch监视点: getData, getChildren, exists. 写操作则是不能设置监视点的。
监视有两种类型:数据监视点和子节点监视点。创建、删除或者设置znode都会触发这些监视点。exists,getData 可以设置数据监视点。getChildren 可以设置子节点变化。
而可能监测的事件类型有: None,NodeCreated, NodeDataChanged, NodeDeleted, NodeChildrenChanged.
None // 客户端连接状态发生变化的时候 会收到None事件
NodeCreated // 节点创建事件
NodeDeleted // 节点删除事件
NodeDataChanged // 节点数据变化
NodeChildrenChanged // 子节点被创建 删除触发该事件
ZK 可以做到,只要数据一发生变化,就会通知相应地注册了监听的客户端。那么,它是怎么做到的呢?
其实原理应该是很简单的,四个步骤:
- 客户端注册Watcher到服务端;
- 服务端发生数据变更;
- 服务端通知客户端数据变更;
- 客户端回调Watcher处理变更应对逻辑;
2.Java API实现
package com.ydt.zookeeper.watcher;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
public class WatcherTest {
ZooKeeper zk;
@Before
public void init() throws IOException, KeeperException, InterruptedException {
zk= new ZooKeeper("ydt1:2181,ydt2:2181,ydt3:2181"
, Integer.MAX_VALUE,new Watcher() {
//全局监听
public void process(WatchedEvent watchedEvent) {
//客户端回调Watcher
System.out.println("-----------------------------------------");
System.out.println("connect state:" + watchedEvent.getState());
System.out.println("event type:" + watchedEvent.getType());
System.out.println("znode path:" + watchedEvent.getPath());
System.out.println("-----------------------------------------");
}
}
);
}
/**
* exists监听事件:
* NodeCreated:节点创建
* NodeDeleted:节点删除
* NodeDataChanged:节点内容
* @throws IOException
* @throws KeeperException
* @throws InterruptedException
*/
@Test
public void test1() throws KeeperException, InterruptedException {
//exists注册监听
zk.exists("/watcher-exists", new Watcher() {
public void process(WatchedEvent watchedEvent) {
System.out.println("-----------------------------------------");
System.out.println("connect state:" + watchedEvent.getState());
System.out.println("event type:" + watchedEvent.getType());
System.out.println("znode path:" + watchedEvent.getPath());
System.out.println("-----------------------------------------");
try {
zk.exists("/watcher-exists",this);
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//不开启ACL,以持久化自动生成序列方式创建
zk.create("/watcher-exists", "watcher-exists".getBytes()
, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
//通过修改的事务类型操作来触发监听事件
zk.setData("/watcher-exists", "watcher-exists2".getBytes(), -1);
//删除节点看看能否触发监听事件
zk.delete("/watcher-exists", -1);
}
/**
* getData监听事件:
* NodeDeleted:节点删除
* NodeDataChange:节点内容发生变化
* @throws IOException
* @throws KeeperException
* @throws InterruptedException
*/
@Test
public void test2() throws IOException, KeeperException, InterruptedException {
//不开启ACL,以持久化自动生成序列方式创建
zk.create("/watcher-getData", "watcher-getData".getBytes()
, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
//getData注册监听
zk.getData("/watcher-getData", new Watcher() {
public void process(WatchedEvent watchedEvent) {
System.out.println("-----------------------------------------");
System.out.println("connect state:" + watchedEvent.getState());
System.out.println("event type:" + watchedEvent.getType());
System.out.println("znode path:" + watchedEvent.getPath());
System.out.println("-----------------------------------------");
try {
zk.getData("/watcher-getData",this,null);
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},null);
//通过修改的事务类型操作来触发监听事件
zk.setData("/watcher-getData", "watcher-getData2".getBytes(), -1);
//删除节点看看能否触发监听事件
zk.delete("/watcher-getData", -1);
}
/**
* getChildren监听事件:
* NodeChildrenChanged:子节点发生变化
* NodeDeleted:节点删除
* @throws IOException
* @throws KeeperException
* @throws InterruptedException
*/
@Test
public void test3() throws IOException, KeeperException, InterruptedException {
zk.create("/watcher-getChildren",null,
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
zk.create("/watcher-getChildren/watcher-getChildren01","watcher-getChildren01".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
//getChildren注册监听
zk.getChildren("/watcher-getChildren", new Watcher() {
public void process(WatchedEvent watchedEvent) {
System.out.println("-----------------------------------------");
System.out.println("connect state:" + watchedEvent.getState());
System.out.println("event type:" + watchedEvent.getType());
System.out.println("znode path:" + watchedEvent.getPath());
System.out.println("-----------------------------------------");
try {
zk.getChildren("/watcher-getChildren",this);
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
zk.setData("/watcher-getChildren/watcher-getChildren01","watcher-getChildren02".getBytes(), -1);//修改子节点
zk.delete("/watcher-getChildren/watcher-getChildren01", -1);//删除子节点
zk.delete("/watcher-getChildren", -1);//删除根节点
}
}
3.源码解析
客户端首先将 Watcher注册到服务端,同时将Watcher对象保存到客户端的Watcher管理器中。当Zookeeper服务端监听的数据状态发生变化时,服务端会主动通知客户端,接着客户端的 Watcher管理器会触发相关 Watcher来回调相应处理逻辑,从而完成整体的数据发布/订阅流程
3.1 Watcher接口
public interface Watcher {
/**
* Event的状态
*/
public interface Event {
/**
* 在事件发生时,ZooKeeper的状态
*/
public enum KeeperState {
@Deprecated
Unknown (-1),
Disconnected (0),
@Deprecated
NoSyncConnected (1),
SyncConnected (3),
AuthFailed (4),
ConnectedReadOnly (5),
SaslAuthenticated(6),
Expired (-112);
private final int intValue;
KeeperState( int intValue) {
this.intValue = intValue;
}
......
}
/**
* ZooKeeper中的事件
*/
public enum EventType {
None (-1),
NodeCreated (1),
NodeDeleted (2),
NodeDataChanged (3),
NodeChildrenChanged (4);
private final int intValue; // Integer representation of value
// for sending over wire
EventType( int intValue) {
this.intValue = intValue;
}
......
}
}
//Watcher的回调方法
abstract public void process(WatchedEvent event);
}
3.2 注册全局监听器
//-------------------------------------ZooKeeper.java----------------------------------------
//初始化zookeeper客户端时会将默认的监听器设置到监听器管理中
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher,
boolean canBeReadOnly)
throws IOException
{
LOG.info("Initiating client connection, connectString=" + connectString
+ " sessionTimeout=" + sessionTimeout + " watcher=" + watcher);
//设置默认监听器
watchManager.defaultWatcher = watcher;
ConnectStringParser connectStringParser = new ConnectStringParser(
connectString);
HostProvider hostProvider = new StaticHostProvider(
connectStringParser.getServerAddresses());
cnxn = new ClientCnxn(connectStringParser.getChrootPath(),
hostProvider, sessionTimeout, this, watchManager,
getClientCnxnSocket(), canBeReadOnly);
//事件信息的传输,通知处理,可以从下面方法可以看出来
cnxn.start();
}
------->
public void start() {
//负责客户端和服务器端的数据通信, 也包括事件信息的传输和心跳
sendThread.start();
//主要在客户端回调注册的 Watchers 进行通知处理
eventThread.start();
}
3.3注册监听器(getChildren)
public List<String> getChildren(final String path, Watcher watcher)
throws KeeperException, InterruptedException
{
final String clientPath = path;
PathUtils. validatePath(clientPath);
WatchRegistration wcb = null;
//如果watcher不等于null, 构建WatchRegistration对象,执行回调的时候会用到
//该对象描述了watcher和path之间的关系
if (watcher != null) {
wcb = new ChildWatchRegistration(watcher, clientPath);
}
//在传入的path加上root path前缀,构成服务器端的绝对路径
final String serverPath = prependChroot(clientPath);
//构建RequestHeader对象
RequestHeader h = new RequestHeader();
//设置操作类型为OpCode. getChildren
h.setType(ZooDefs.OpCode. getChildren);
//构建GetChildrenRequest对象
GetChildrenRequest request = new GetChildrenRequest();
//设置path
request.setPath(serverPath);
//设置是否使用watcher
request.setWatch(watcher != null);
//构建GetChildrenResponse对象
GetChildrenResponse response = new GetChildrenResponse();
//提交请求,通过客户端的网络处理类去提交请求,并阻塞等待结果
ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
if (r.getErr() != 0) {
throw KeeperException.create(KeeperException.Code. get(r.getErr()),
clientPath);
}
return response.getChildren();
}
3.4请求包入列并发送
public ReplyHeader submitRequest(RequestHeader h, Record request,
Record response, WatchRegistration watchRegistration)
throws InterruptedException {
ReplyHeader r = new ReplyHeader();//响应头
//组装请求入队
Packet packet = queuePacket(h, r, request, response, null, null, null,
null, watchRegistration);
synchronized (packet) {
//阻塞等待请求结果
while (!packet.finished) {
packet.wait();
}
}
return r;
}
Packet queuePacket(RequestHeader h, ReplyHeader r, Record request,
Record response, AsyncCallback cb, String clientPath,
String serverPath, Object ctx, WatchRegistration watchRegistration)
{
Packet packet = null;
// Note that we do not generate the Xid for the packet yet. It is
// generated later at send-time, by an implementation of ClientCnxnSocket::doIO(),
// where the packet is actually sent.
// 这个队列就是存放我们请求的队列,注意,我们还没有为包生成Xid。
// 它是在发送时生成,通过实现ClientCnxnSocket::doIO(),数据包实际发送的地方
synchronized (outgoingQueue) {
packet = new Packet(h, r, request, response, watchRegistration);
packet.cb = cb;
packet.ctx = ctx;
packet.clientPath = clientPath;
packet.serverPath = serverPath;
if (!state.isAlive() || closing) {
conLossPacket(packet);
} else {
// If the client is asking to close the session then
// mark as closing
if (h.getType() == OpCode.closeSession) {
closing = true;
}
//请求包入队
outgoingQueue.add(packet);
}
}
//请求包入列后,唤醒客户发送线程,将请求包发送出去,进入到发送线程SendThread.run()方法中查看
sendThread.getClientCnxnSocket().wakeupCnxn();
return packet;
}
3.5 服务器端循环监听
//...................................NIOServerCnxnFactory.java.................................
public void run() {
// socket不是关闭状态就进入阻塞
while (!ss.socket().isClosed()) {
try {
selector.select(1000);
Set<SelectionKey> selected;
synchronized (this) {
//获取事件键列表
selected = selector.selectedKeys();
}
ArrayList<SelectionKey> selectedList = new ArrayList<SelectionKey>(
selected);
Collections.shuffle(selectedList);
//遍历事件keys
for (SelectionKey k : selectedList) {
//就绪等待连接事件那就先创建连接
if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0) {
SocketChannel sc = ((ServerSocketChannel) k
.channel()).accept();
InetAddress ia = sc.socket().getInetAddress();
int cnxncount = getClientCnxnCount(ia);
if (maxClientCnxns > 0 && cnxncount >= maxClientCnxns){
LOG.warn("Too many connections from " + ia
+ " - max is " + maxClientCnxns );
sc.close();
} else {
LOG.info("Accepted socket connection from "
+ sc.socket().getRemoteSocketAddress());
sc.configureBlocking(false);
SelectionKey sk = sc.register(selector,
SelectionKey.OP_READ);
NIOServerCnxn cnxn = createConnection(sc, sk);
sk.attach(cnxn);
addCnxn(cnxn);
}
} else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
// 就绪读写事件,开始执行(包括处理消息,返回监听)
NIOServerCnxn c = (NIOServerCnxn) k.attachment();
c.doIO(k);
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Unexpected ops in select "
+ k.readyOps());
}
}
}
selected.clear();
} catch (RuntimeException e) {
LOG.warn("Ignoring unexpected runtime exception", e);
} catch (Exception e) {
LOG.warn("Ignoring exception", e);
}
}
closeAll();
LOG.info("NIOServerCnxn factory exited run method");
}
3.6 触发Watcher(setData)
在Zookeeper二阶段提交的COMMIT阶段。当Follower从Leader那接收到一个写请求的Leader.COMMIT数据包,会调用FinalRequestProcessor.processRequest()方法。Leader本身在发送完Leader.COMMIT数据包,也会调用FinalRequestProcessor.processRequest()方法。
如果是setData修改数据请求,那么FinalRequestProcessor.processRequest()方法最终会调用到DataTree.setData方法将txn应用到指定znode上,同时触发Watcher,并发送notification给Client端。
其关SetData请求的时序图如下:
3.6.1 DataTree.setData()方法
根据上面的时序图,一路跟踪到DataTree.setData方法:
public Stat setData(String path, byte data[], int version, long zxid,
long time) throws KeeperException.NoNodeException {
Stat s = new Stat();
//根据path, 获得DataNode对象n
DataNode n = nodes.get(path);
//如果n为null, 则抛出NoNodeException异常
if (n == null) {
throw new KeeperException.NoNodeException();
}
byte lastdata[] = null;
synchronized (n) {
lastdata = n.data;
n.data = data;
n.stat.setMtime(time);
n.stat.setMzxid(zxid);
n.stat.setVersion(version);
n.copyStat(s);
}
// now update if the path is in a quota subtree.
// 更新数据
String lastPrefix;
if((lastPrefix = getMaxPrefixWithQuota(path)) != null) {
this.updateBytes(lastPrefix, (data == null ? 0 : data.length)
- (lastdata == null ? 0 : lastdata.length));
}
//触发Watcher
dataWatches.triggerWatch(path, EventType.NodeDataChanged);
return s;
}
3.6.2 WatchManager触发Watcher
public Set<Watcher> triggerWatch(String path, EventType type, Set<Watcher> supress) {
WatchedEvent e = new WatchedEvent(type,
KeeperState.SyncConnected, path);
HashSet<Watcher> watchers;
synchronized (this) {
//从watchTable删除掉path对应的watcher
watchers = watchTable.remove(path);
if (watchers == null || watchers.isEmpty()) {
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG,
ZooTrace.EVENT_DELIVERY_TRACE_MASK,
"No watchers for " + path);
}
return null;
}
for (Watcher w : watchers) {
HashSet<String> paths = watch2Paths.get(w);
if (paths != null) {
paths.remove(path);
}
}
}
//循环处理所有关于path的Watcher, 这里Watcher对象实际上就是ServerCnxn类型对象
for (Watcher w : watchers) {
if (supress != null && supress.contains(w)) {
continue;
}
//ServerCnxn执行监控事件对象
w.process(e);
}
return watchers;
}
4.6.3 ServerCnxn发送notification
包括NIOServerCnxn和NettyServerCnxn,其实代码逻辑都一样
//NIO
synchronized public void process(WatchedEvent event) {
ReplyHeader h = new ReplyHeader(-1, -1L, 0);
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG, ZooTrace.EVENT_DELIVERY_TRACE_MASK,
"Deliver event " + event + " to 0x"
+ Long.toHexString(this.sessionId)
+ " through " + this);
}
// Convert WatchedEvent to a type that can be sent over the wire
WatcherEvent e = event.getWrapper();
//发送notification给Client端
sendResponse(h, e, "notification");
}
//Netty
public void process(WatchedEvent event) {
ReplyHeader h = new ReplyHeader(-1, -1L, 0);
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG, ZooTrace.EVENT_DELIVERY_TRACE_MASK,
"Deliver event " + event + " to 0x"
+ Long.toHexString(this.sessionId)
+ " through " + this);
}
// Convert WatchedEvent to a type that can be sent over the wire
WatcherEvent e = event.getWrapper();
try {
//发送notification给Client端
sendResponse(h, e, "notification");
} catch (IOException e1) {
if (LOG.isDebugEnabled()) {
LOG.debug("Problem sending to " + getRemoteSocketAddress(), e1);
}
close();
}
}
protected void internalSendBuffer(ByteBuffer bb) {
if (bb != ServerCnxnFactory.closeConn) {
// We check if write interest here because if it is NOT set,
// nothing is queued, so we can try to send the buffer right
// away without waking up the selector
if(sk.isValid() &&
((sk.interestOps() & SelectionKey.OP_WRITE) == 0)) {
try {
sock.write(bb);//通过socket向客户端写入字节流,调度process
} catch (IOException e) {
// we are just doing best effort right now
}
}
// if there is nothing left to send, we are done
if (bb.remaining() == 0) {
packetSent();
return;
}
}
synchronized(this.factory){
sk.selector().wakeup();
if (LOG.isTraceEnabled()) {
LOG.trace("Add a buffer to outgoingBuffers, sk " + sk
+ " is valid: " + sk.isValid());
}
outgoingBuffers.add(bb);
if (sk.isValid()) {
sk.interestOps(sk.interestOps() | SelectionKey.OP_WRITE);
}
}
}
//至于远程客户端地址端口怎么知道,初始化ServerCnxn的时候就设置了
public NIOServerCnxn(ZooKeeperServer zk, SocketChannel sock,
SelectionKey sk, NIOServerCnxnFactory factory) throws IOException {
this.zkServer = zk;
this.sock = sock;//保存客户端sock对象
this.sk = sk;
this.factory = factory;
if (this.factory.login != null) {
this.zooKeeperSaslServer = new ZooKeeperSaslServer(factory.login);
}
if (zk != null) {
outstandingLimit = zk.getGlobalOutstandingLimit();
}
sock.socket().setTcpNoDelay(true);
/* set socket linger to false, so that socket close does not
* block */
sock.socket().setSoLinger(false, -1);
InetAddress addr = ((InetSocketAddress) sock.socket()
.getRemoteSocketAddress()).getAddress();
authInfo.add(new Id("ip", addr.getHostAddress()));
sk.interestOps(SelectionKey.OP_READ);
}
原文链接:
Zookeeper是否适合作为注册中心
探讨这个问题前,我们一定需要知道什么是CAP理论。
CAP理论
CAP理论是分布式架构中重要理论:
一致性(Consistency):所有节点在同一时间具有相同的数据;
可用性(Availability) :保证每个请求不管成功或者失败都有响应;
分隔容忍(Partition tolerance) :系统中任意信息的丢失或失败不会影响系统的继续运作。
关于 P 的理解,我觉得是在整个系统中某个部分,挂掉了,或者宕机了,并不影响整个系统的运作或者说使用,而可用性是,某个系统的某个节点挂了,但是并不影响系统的接受或者发出请求。
CAP 不可能都取,只能取其中2个的原因如下:
如果C是第一需求的话,那么会影响A的性能,因为要数据同步,不然请求结果会有差异,但是数据同步会消耗时间,期间可用性就会降低。
如果A是第一需求,那么只要有一个服务在,就能正常接受请求,但是对与返回结果变不能保证,原因是,在分布式部署的时候,数据一致的过程不可能想切线路那么快。
再如果,同时满足一致性和可用性,那么分区容错就很难保证了,也就是单点,也是分布式的基本核心。
Zookeeper作为注册中心探讨
作为一个分布式协同服务,ZooKeeper非常好,但是对于Service发现服务来说就不合适了,因为对于Service发现服务来说就算是返回了包含不实的信息的结果也比什么都不返回要好。所以当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接down掉不可用。
但是zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30 ~ 120s, 且选举期间整个zk集群都是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。
所以说,作为注册中心,可用性的要求要高于一致性!
在 CAP 模型中,Zookeeper整体遵循一致性(CP)原则,即在任何时候对 Zookeeper 的访问请求能得到一致的数据结果,但是当机器下线或者宕机时,不能保证服务可用性。
那为什么Zookeeper不使用最终一致性(AP)模型呢?因为这个依赖Zookeeper的核心算法是ZAB,所有设计都是为了强一致性。这个对于分布式协调系统,完全没没有毛病,但是你如果将Zookeeper为分布式协调服务所做的一致性保障,用在注册中心,或者说服务发现场景,这个其实就不合适。
小节
我们对Zookeeper的注册中心总结如下:
Zookeeper的心跳检测,可以自动探测服务提供者机器的宕机或下线;
Zookeeper的Watch机制,可以将变更的注册列表推给服务消费者;
Zookeeper是CP模型,不太适合作为注册中心。