在一套分布式的online services系統中,各service通常不會放在一台服務器上,而是通過Zookeeper這樣的東西,將自己的service信息注冊到上面,service的使用者通過Zookeeper來發現各service的信息,從而可以將request發送到不同的service上去處理。

如上圖所示,兩個Service Provider 1和2分別在192.168.1.5和192.168.1.6這兩台服務器的2688端口上提供服務,服務的地址和端口注冊到了Zookeeper中。Service User通過查詢Zookeeper,可得知這些服務的信息。通常,Service User與Service Provider之間的通信,是通過connection pool實現的,因為Service User不可能假定在第一次查詢到所有Service Provider的信息之后,它們就是一直存活的,假如某個Service Provider因為程序問題死掉了,向它發送request只會造成大量的失敗結果,因此通常會實現一個connection pool來保證實時更新節點的信息,當有一個Service Provider從Zookeeper上消失之后,從connection pool中取出的connection總是可用的(即:總能通過它把request發送到一個有效的Service Provider那里)。
文章來源:http://www.codelast.com/
這里,我們保證了當一個Service Provider從Zookeeper上退出后,我們一定不會再用它,但是,我們如何保證一個Service Provider還存活的情況下,都能處於可服務的狀態呢?例如,Service Provider 1的程序工作一直很穩定,但是某天由於ISP的原因,它和Zookeeper之間的網絡中斷了5分鍾,於是Zookeeper會無法向它發送心跳包,最終Zookeeper會認為session expired了,從而會把它注冊的臨時節點給移除掉(必須是注冊臨時節點啊,要不然當Service Provider掛了之后節點還在,豈不出亂子了)。移除掉之后,問題就來了:5分鍾后中斷的網絡恢復正常了,Service Provider 1的程序也一直沒有死掉,它又可以serving了,但是由於它在Zookeeper中注冊的節點沒了,所以Service User通過connection pool是永遠也無法向Service Provider 1發送request的,於是所有的request還是都發到了Service Provider 2那里,造成它的負載一直很大,系統的處理能力減弱。
因此,為了避免這種問題,我們需要在Service Provider中提供Zookeeper掉線自動重新注冊的功能。
【1】用Java怎么注冊Zookeeper
Curator庫是一個絕好的選擇。它是Netflix開發的一套開源軟件。
什么?沒聽說過Netflix?全美三分之一的帶寬都是被這家公司占用的你不知道?
好吧,那么火爆得一塌糊塗的美劇《紙牌屋》你總聽說過吧?就是這家公司花錢制作的。如果這也沒聽說過的話,那么你只能去Google啦。
題外話:科技和生活娛樂息息相關啊!
如果要問為什么不直接用Zookeeper官方的API,而是使用包裝過的Curator,那只能用一句話來解釋:Curator很好很強大很方便。
文章來源:http://www.codelast.com/
【2】代碼
『A』使用Curator注冊Zookeeper的代碼
1
2
3
4
5
6
7
8
|
CuratorFramework curator = CuratorFrameworkFactory.newClient(
"zookeeper.codelast.com:2181"
,
5000
,
3000
,
new
RetryNTimes(
5
,
1000
));
curator.start();
String regContent =
"192.168.1.5:2688"
;
String zkRegPathPrefix =
"/codelast/service-provider-"
;
//TODO:
curator.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(zkRegPathPrefix, regContent.getBytes(
"UTF-8"
));
|
解釋一下:
zookeeper.codelast.com:2181 是你的Zookeeper server的host:port。
192.168.1.5:2688 是注冊的Zookeeper節點的內容,表示Service Provider 1在1912.168.1.5的2688端口提供服務。當你用Zookeeper客戶端zkCli.sh的get命令時,獲取的返回值的第一行就是這個內容。
CreateMode.EPHEMERAL_SEQUENTIAL 表示注冊的節點是臨時的,並且其命名是順序增加的。
/codelast/service-provider- 是把service注冊到Zookeeper中的哪個路徑下。上面代碼的效果就是,注冊的節點的完整路徑會類似於 /codelast/service-provider-0000000001
文章來源:http://www.codelast.com/
『B』上面的代碼在session expired的情況下,是無法自動重新在Zookeeper中注冊的。要實現這種功能,需要添加一個實現了ConnectionStateListener接口的類,並應用到CuratorFramework對象上。我們需要在上面代碼中的“TODO”處添加如下代碼:
1
2
|
MyConnectionStateListener stateListener =
new
MyConnectionStateListener(zkRegPathPrefix, regContent);
curator.getConnectionStateListenable().addListener(stateListener);
|
然后再實現MyConnectionStateListener類:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
/**
* A class to monitor connection state & re-register to Zookeeper when connection lost.
*
* @author Darran Zhang @ codelast.com
*/
public
class
MyConnectionStateListener
implements
ConnectionStateListener {
private
String zkRegPathPrefix;
private
String regContent;
public
MyConnectionStateListener(String zkRegPathPrefix, String regContent) {
this
.zkRegPathPrefix = zkRegPathPrefix;
this
.regContent = regContent;
}
@Override
public
void
stateChanged(CuratorFramework curatorFramework, ConnectionState connectionState) {
if
(connectionState == ConnectionState.LOST) {
while
(
true
) {
try
{
if
(curatorFramework.getZookeeperClient().blockUntilConnectedOrTimedOut()) {
curatorFramework.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(zkRegPathPrefix, regContent.getBytes(
"UTF-8"
));
break
;
}
}
catch
(InterruptedException e) {
//TODO: log something
break
;
}
catch
(Exception e) {
//TODO: log something
}
}
}
}
}
|
文章來源:http://www.codelast.com/
此類負責監聽connection的狀態,並在檢測到LOST狀態時(此時client已經從Zookeeper中掉線)重新注冊。
注意,在檢測到LOST狀態后,上面的代碼用了一個while (true) 死循環來不斷嘗試重新連接Zookeeper server,連不上不罷休。
【3】測試
如何測試?當然要創造出一種session expired的情況,讓Zookeeper server把它認為已經掉線的節點的注冊信息給移除掉。怎么讓session expire呢?官方網站上有這么一段話:
Is there an easy way to expire a session for testing?Yes, a ZooKeeper handle can take a session id and password. This constructor is used to recover a session after total application failure. For example, an application can connect to ZooKeeper, save the session id and password to a file, terminate, restart, read the session id and password, and reconnect to ZooKeeper without loosing the session and the corresponding ephemeral nodes. It is up to the programmer to ensure that the session id and password isn't passed around to multiple instances of an application, otherwise problems can result.In the case of testing we want to cause a problem, so to explicitly expire a session an application connects to ZooKeeper, saves the session id and password, creates another ZooKeeper handle with that id and password, and then closes the new handle. Since both handles reference the same session, the close on second handle will invalidate the session causing a SESSION_EXPIRED on the first handle.
文章來源:http://www.codelast.com/
但是需要這么麻煩嗎?直接通過OS的防火牆就可以做到。例如,在RedHat上,通過如下命令:
1
|
iptables -A INPUT -d codelast.com -p tcp --sport 2181 -j DROP
|
你將可以阻止來自codelast.com這個域名的所有輸入(INPUT)的流量。這會導致Zookeeper server和client之間的心跳失效,互相認為對方已經掉線了。對Zookeeper server來說,當它認為client掉線時,就會把client節點從Zookeeper中移除。這個時候,如果client程序沒有重新注冊的能力,那么當網絡恢復后,client程序雖然是能正常運行的,但是也失去了提供service的能力——因為service的使用者已經無法通過Zookeeper發現它了。
我們先啟動Service Provider,然后利用執行上面的命令(如果不是RedHat,請自行Google)阻斷網絡連接一段時間,你最終將會觀察到Zookeeper中注冊的節點已經沒了,也就是說我們讓session expired了。
然后,執行如下命令:
1
|
iptables -F
|
文章來源:http://www.codelast.com/
該命令可以恢復防火牆的設置。此時,網絡恢復連接,你會看到Curator打印出來很多信息,提示已經重新連接上了Zookeeper server。但是注意,如果你沒有像前面的代碼一樣提供自動重新注冊的能力,那么,先前在Zookeeper中注冊的節點並不會出現!也就是說,連上了也沒用,它已經不能被發現了。
在添加了自動重新注冊的功能后,Zookeeper中注冊的節點就會自動重新被創建出來了。這就達到了我們要的效果。