接上文 手把手带你撸zookeeper源码-从源码角度分析zookeeper启动时都做了什么?
先说点题外话,因为我想着把整个zookeeper源码分析作为系列文章来写,所以每一篇文章只会分析一部分源码,而不是长篇大论去粘贴源码,然后哪个方法进入哪个方法,然后一带而过。而是抓住主线,然后去分析主要方法,用我的理解,用通俗的语言通过书面的形式表达出来,希望对想学习zookeeper源码的你有帮助
先抛出个目标,本篇文章主要写一下zookeeper集群之间是如何建立连接的?
先贴一下上文最后的一段代码,有一部分代码没有讲解清除,需要再深入说一下,也对本篇文章还是比较重要的
protected Election createElectionAlgorithm(int electionAlgorithm){
Election le=null;
//TODO: use a factory rather than a switch
//0 1 2 已经废弃
//electionAlgorithm = 3
switch (electionAlgorithm) {
case 0:
le = new LeaderElection(this);
break;
case 1:
le = new AuthFastLeaderElection(this);
break;
case 2:
le = new AuthFastLeaderElection(this, true);
break;
case 3:
//zk节点网络通信的组件
qcm = createCnxnManager();
QuorumCnxManager.Listener listener = qcm.listener;
if(listener != null){
// 启动一个listener监听,用于监听其他机器发送过来的请求
listener.start();
le = new FastLeaderElection(this, qcm);
} else {
LOG.error("Null listener when initializing cnx manager");
}
break;
default:
assert false;
}
return le;
}
这个方法主要是创建一个选举算法实例对象,就是FastLeaderElection, 在最后返回之前,我们需要先关注一下createCnxnManager()这个方法,我们看看它做了什么事,看下代码
public QuorumCnxManager(final long mySid,
Map<Long,QuorumPeer.QuorumServer> view,
QuorumAuthServer authServer,
QuorumAuthLearner authLearner,
int socketTimeout,
boolean listenOnAllIPs,
int quorumCnxnThreadsSize,
boolean quorumSaslAuthEnabled,
ConcurrentHashMap<Long, SendWorker> senderWorkerMap) {
this.senderWorkerMap = senderWorkerMap;
//创建一个recvQueue,返回响应的数据会放到这个队列里面,肯定有其他地方会取出这个队列里面的数据,然后进行处理的
this.recvQueue = new ArrayBlockingQueue<Message>(RECV_CAPACITY);
//发送队列的一个map, key: myid, value: 要发送的数据集合
this.queueSendMap = new ConcurrentHashMap<Long, ArrayBlockingQueue<ByteBuffer>>();
// 最新发送出的数据集合
this.lastMessageSent = new ConcurrentHashMap<Long, ByteBuffer>();
String cnxToValue = System.getProperty("zookeeper.cnxTimeout");
if(cnxToValue != null){
this.cnxTO = Integer.parseInt(cnxToValue);
}
this.mySid = mySid;
this.socketTimeout = socketTimeout;
this.view = view;
this.listenOnAllIPs = listenOnAllIPs;
initializeAuth(mySid, authServer, authLearner, quorumCnxnThreadsSize,
quorumSaslAuthEnabled);
// Starts listener thread that waits for connection requests
// 这个比较关键
listener = new Listener();
}
在代码里面添加了一部分注释,在这个方法里面主要关注点在于最后的一个Listener,这个Listener是干什么的?首先Listerner是一个线程,那么肯定会在某一个地方会启动这个线程(上上面的代码中大家可以看到有一个listener.start()来启动线程),然后在线程中会创建一个ServerSocket,然后调用accept方法接受其他客户端的连接请求,具体代码我们等会再贴出来再详细看,这里我们可以先大概知道这个方法里面是用来干什么的就行,具体用的时候再进去具体分析,千万不要钻牛角尖,否则进去可能一头雾水出不来了。
然后回到上文中创建选举算法实例的方法里面,如下代码
QuorumCnxManager.Listener listener = qcm.listener;
if(listener != null){
// 启动一个listener监听,用于监听其他机器发送过来的请求
listener.start();
le = new FastLeaderElection(this, qcm);
} else {
LOG.error("Null listener when initializing cnx manager");
}
然后把listener给启动了,会调用Listener中的run方法,接下来就是去实例化FastLeaderElection对象了,然后调用starter方法
private void starter(QuorumPeer self, QuorumCnxManager manager) {
this.self = self;
proposedLeader = -1;
proposedZxid = -1;
//创建一个发送数据队列
sendqueue = new LinkedBlockingQueue<ToSend>();
// 接受响应通知 队列
recvqueue = new LinkedBlockingQueue<Notification>();
//实例化一个messager
this.messenger = new Messenger(manager);
}
大概就是创建两个队列,然后用来存放发送数据的队列,和接受响应通知的队列
关键点是在new Messager里面
Messenger(QuorumCnxManager manager) {
//创建一个发送数据线程
this.ws = new WorkerSender(manager);
Thread t = new Thread(this.ws,
"WorkerSender[myid=" + self.getId() + "]");
t.setDaemon(true);
t.start();
//接受数据线程
this.wr = new WorkerReceiver(manager);
t = new Thread(this.wr,
"WorkerReceiver[myid=" + self.getId() + "]");
t.setDaemon(true);
t.start();
}
这里创建了两个线程,一个用来发送数据,一个用来接受数据,并同时启动了这两个线程
可以大概看一下
WorkerSender.run()
public void run() {
while (!stop) {
try {
ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS);
if(m == null) continue;
process(m);
} catch (InterruptedException e) {
break;
}
}
LOG.info("WorkerSender is down");
}
从sendQueue里面获取数据,然后调用process去处理
WorkerReceiver.run()
这个方法代码稍微有点长,我不再粘贴出来了,大家可以自己看一下,其实就是从recvQueue里面获取数据,然后进行一系列的处理,我们接下来主要先关注一下,这两个队列里面在什么时候会放入数据,然后再回来看他们是怎么处理的
我们先继续往下看,看看还会有什么操作
发现调用完startLeaderElection方法之后,直接调用的就是super.start(), QuorumPeer也是一个线程,直接启动线程,然后找这个类中的run方法即可
刚才我们在startLeaderElection方法中看到先启动了Listener线程,我们先来分析一下Listener中的run方法
然后再分析QuorumPeer中的run方法
QuorumCnxnManager.Listener
@Override
public void run() {
int numRetries = 0;
InetSocketAddress addr;
while((!shutdown) && (numRetries < 3)){
ss = new ServerSocket();
ss.setReuseAddress(true);
if (listenOnAllIPs) {
int port = view.get(QuorumCnxManager.this.mySid)
.electionAddr.getPort();
addr = new InetSocketAddress(port);
} else {
addr = view.get(QuorumCnxManager.this.mySid)
.electionAddr;
}
LOG.info("My election bind port: " + addr.toString());
setName(view.get(QuorumCnxManager.this.mySid)
.electionAddr.toString());
ss.bind(addr);
while (!shutdown) {
Socket client = ss.accept();//bio
setSockOpts(client);
if (quorumSaslAuthEnabled) {
receiveConnectionAsync(client);
} else {
receiveConnection(client);
}
numRetries = 0;
}
}
}
把注释和异常给删除掉了,其他代码都在shutdown默认为false, 另外shutdown变量是一个被volatile修饰的变量,对线程是可见的,我们可以手动去设置shutdown为true,那么此时就不会再监听其他客户端的连接了, numRetries<3,说明最终只会重试三次,超过三次之后直接异常不再重连。对volatile不熟悉的小伙伴可以自行百度
对java socket熟悉的小伙伴一眼就能看出while中创建了一个ServerSocket,然后绑定一个端口,通过accept阻塞住,等待客户端连接。(上篇末尾说会写两篇有关java socket和java nio的文章,对不熟悉的这方面的小伙伴做个知识点的扩充,这块在这周末去写吧。大家对这块不熟悉的也可以去百度一下,然后自己手动敲个demo先看一下,对这块有个大概的了解)
我们假设现在有一个客户端连接进来,接下来代码将接入到 receiveConnection中去接受连接请求的处理。再说补充一点,就是很多开源框架,大家可以看一下他们的类命名、方法命名、变量名,其实都是很有讲究的,很多时候我们光看名字都可以猜想出来它干了什么事的,大家要多锻炼"连蒙带猜"的能力
private void handleConnection(Socket sock, DataInputStream din)
throws IOException {
Long sid = null;
sid = din.readLong();
if (sid < 0) { // this is not a server id but a protocol version (see ZOOKEEPER-1633)
sid = din.readLong();
int num_remaining_bytes = din.readInt();
if (num_remaining_bytes < 0 || num_remaining_bytes > maxBuffer) {
LOG.error("Unreasonable buffer length: {}", num_remaining_bytes);
closeSocket(sock);
return;
}
byte[] b = new byte[num_remaining_bytes];
// remove the remainder of the message from din
int num_read = din.read(b);
}
if (sid == QuorumPeer.OBSERVER_ID) {
sid = observerCounter.getAndDecrement();
LOG.info("Setting arbitrary identifier to observer: " + sid);
}
authServer.authenticate(sock, din);
//If wins the challenge, then close the new connection.
if (sid < this.mySid) {
SendWorker sw = senderWorkerMap.get(sid);
if (sw != null) {
sw.finish();
}
LOG.debug("Create new connection to server: " + sid);
closeSocket(sock);
connectOne(sid);
} else {
SendWorker sw = new SendWorker(sock, sid);
RecvWorker rw = new RecvWorker(sock, din, sid, sw);
sw.setRecv(rw);
SendWorker vsw = senderWorkerMap.get(sid);
if(vsw != null)
vsw.finish();
senderWorkerMap.put(sid, sw);
queueSendMap.putIfAbsent(sid, new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY));
sw.start();
rw.start();
return;
}
}
我把不需要的代码删了一部分,分析一下这个方法,主要都干了什么事?
1、在接收到连接之后会读取创建连接的服务器的sid给读出来
2、接下来有个分支就是判断读取出来的sid和我自己的sid做比较,现在我们先考虑进入else的部分,先看看都做了什么事
创建了两个线程,SenderWorker和RecvWorker,并把二者给关联起来,并启动,它们两个应该是负责发送和接受选举数据的
3、我们看一下sid < mySid的部分(这部分今天先说一下,后面还会提到)
我们先来简单看一下这张图,打个比方,zookeeper01启动,然后阻塞等待有人连接进来,然后zookeeper02也启动,然后也会等待其他人连接进来。之后呢,zookeeper01会遍历zoo.cfg里面所有的server.x(里面的服务器,然后循环建立连接)即,zookeeper01和zookeeper02建立连接,同样的zookeeper02也会和zookeeper01建立连接。
是不是感觉很奇怪,zookeeper01和zookeeper02其实只需要建立一个连接,然后进行Leader选举进行通信就可以了,而现在却相互建立连接,造成资源浪费
而sid < mySid就是为了处理这个问题的,zookeeper在集群中相互创建连接时,会判断当前哪个sid和我建立连接,如果是比我小的sid和我建立连接然后直接把连接进行关闭了,如果是比我大的sid,则允许建立连接,就是zookeeper只允许sid大的服务器向sid小的服务器发起连接,而小的sid不能向sid大的发起连接,发起之后会直接关闭掉
if (sid < this.mySid) {
SendWorker sw = senderWorkerMap.get(sid);
if (sw != null) {
sw.finish();
}
//关掉socket
closeSocket(sock);
connectOne(sid);
}
换个角度思考一下这个问题,其实我们完全在发起连接的时候去先判断一下sid的大小(通过server.x肯定是可以知道我要准备向哪台zookeeper发起连接的),然后再去创建连接,而不是说我先去建立连接,然后再判断,再关闭连接
另外至于zookeeper之间发送连接的代码,我们接下来再去深入分析,每篇文章不会太长,但是会尽可能的分析全面,不会追求长篇大论,只是用最通俗的话来讲解,一次一个点即可
觉得对你有帮助的小伙伴,请点个赞