參考資料
<<從PAXOS到ZOOKEEPER分佈式一致性原理與實踐>>
zookeeper-3.0.0
Zookeeper概述
Zookeeper是一個分佈式的,開放源碼的分佈式應用程序協調服務。致力於提供一個高性能、高可用,具有嚴格的順序訪問控制能力(寫操作嚴格順序)的分佈式協調服務。
Zookeeper集羣啓動
集羣啓動方法與配置文件
查看目錄bin下的zkServer.sh內容;
ZOOBIN=`readlink -f "$0"`
ZOOBINDIR=`dirname "$ZOOBIN"`
. $ZOOBINDIR/zkEnv.sh # 設置運行的環境變量
case $1 in
start)
echo -n "Starting zookeeper ... "
java "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" \
-cp $CLASSPATH $JVMFLAGS org.apache.zookeeper.server.quorum.QuorumPeerMain $ZOOCFG & # 啓動 zookeeper 啓動類爲 QuorumPeerMain
echo STARTED
;;
stop)
echo -n "Stopping zookeeper ... "
echo kill | nc localhost $(grep clientPort $ZOOCFG | sed -e 's/.*=//') # 殺死進程
echo STOPPED
;;
upgrade)
shift
echo "upgrading the servers to 3.*"
java "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" \
-cp $CLASSPATH $JVMFLAGS org.apache.zookeeper.server.upgrade.UpgradeMain ${@}
echo "Upgrading ... "
;;
restart)
shift
$0 stop ${@}
sleep 3
$0 start ${@} # 重啓就是先殺死進程 然後再啓動
;;
status)
STAT=`echo stat | nc localhost $(grep clientPort $ZOOCFG | sed -e 's/.*=//') 2> /dev/null| grep Mode`
if [ "x$STAT" = "x" ]
then
echo "Error contacting service. It is probably not running." # 檢查狀態
else
echo $STAT
fi
;;
*)
echo "Usage: $0 {start|stop|restart|status}" >&2
esac
在中斷中輸入zkServer.sh start就可以啓動Zookeeper集羣,啓動的配置文件爲默認的zoo_sample.cfg,如果是集羣啓動,需要修改該配置文件,文件中需要加入多臺集羣的IP信息,並且集羣啓動的時候的配置文件需要相同。參考配置文件如下;
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
dataDir=/export/crawlspace/mahadev/zookeeper/server1/data
# the port at which the clients will connect
clientPort=2181
#設置集羣信息
server.1=192.168.0.1:2888:3888
server.2=192.168.0.2:2888:3888
server.3=192.168.0.3:2888:3888
在啓動的參數解析過程中可以依次查看各個參數的用途。
Zookeeper集羣啓動流程
public class QuorumPeerMain {
private static final Logger LOG = Logger.getLogger(QuorumPeerMain.class);
/**
* To start the replicated server specify the configuration file name on the
* command line.
* @param args command line
*/
public static void main(String[] args) {
if (args.length == 2) {
ZooKeeperServerMain.main(args); // 如果是參數啓動則直接啓動 默認爲單節點啓動
return;
}
QuorumPeerConfig.parse(args);
if (!QuorumPeerConfig.isStandalone()) {
runPeer(new QuorumPeer.Factory() { // 繼承自QuorumPeer.Factory 並實現了其中的接口方法 create 和 createConnectionFactory
public QuorumPeer create(NIOServerCnxn.Factory cnxnFactory) throws IOException {
QuorumPeer peer = new QuorumPeer(); // 生成實例
peer.setClientPort(ServerConfig.getClientPort()); // 獲取實例監聽的客戶端端口
peer.setTxnFactory(new FileTxnSnapLog(
new File(QuorumPeerConfig.getDataLogDir()),
new File(QuorumPeerConfig.getDataDir())));
peer.setQuorumPeers(QuorumPeerConfig.getServers()); // 設置Servers配置信息
peer.setElectionType(QuorumPeerConfig.getElectionAlg()); // 設置選舉類型
peer.setMyid(QuorumPeerConfig.getServerId()); // 設置Serverid
peer.setTickTime(QuorumPeerConfig.getTickTime());
peer.setInitLimit(QuorumPeerConfig.getInitLimit());
peer.setSyncLimit(QuorumPeerConfig.getSyncLimit());
peer.setCnxnFactory(cnxnFactory); // 設置網絡客戶端請求處理的框架
return peer;
}
public NIOServerCnxn.Factory createConnectionFactory() throws IOException {
return new NIOServerCnxn.Factory(getClientPort()); // 找到IO複用的工廠方法
}
});
}else{
// there is only server in the quorum -- run as standalone
ZooKeeperServerMain.main(args);
}
}
public static void runPeer(QuorumPeer.Factory qpFactory) {
try {
QuorumStats.registerAsConcrete();
QuorumPeer self = qpFactory.create(qpFactory.createConnectionFactory()); // 創建實例
self.start(); // 啓動線程執行
self.join(); // 阻塞直到線程退出
} catch (Exception e) {
LOG.fatal("Unexpected exception",e);
}
System.exit(2);
}
}
啓動的簡單邏輯流程就是,首先判斷是否是集羣模式啓動,如果是集羣模式啓動,則首先調用QuorumPeerConfig解析配置參數,通過解析參數來判斷是否在配置文件中是否是集羣模式,如果配置中是集羣模式,則調用runPeer方法,該方法主要就是接受一個QuorumPeer.Factory參數,然後調用create方法,然後就調用start方法啓動線程並阻塞等待經常結束。
配置文件類QuorumPeerConfig
public class QuorumPeerConfig extends ServerConfig { // 繼承自ServerConfig 該類實現了一個配置實例單例模式
private static final Logger LOG = Logger.getLogger(QuorumPeerConfig.class);
private int tickTime;
private int initLimit;
private int syncLimit;
private int electionAlg;
private int electionPort;
private HashMap<Long,QuorumServer> servers = null;
private long serverId;
private QuorumPeerConfig(int port, String dataDir, String dataLogDir) { // 調用父類的構造方法
super(port, dataDir, dataLogDir);
}
public static void parse(String[] args) { // 解析配置文件參數
if(instance!=null)
return;
try {
if (args.length != 1) { // 確保輸入的唯一參數就是配置文件的文件路徑
System.err.println("USAGE: configFile");
System.exit(2);
}
File zooCfgFile = new File(args[0]); // 生成配置文件類型
if (!zooCfgFile.exists()) { // 檢查輸入的配置文件是否存在
LOG.error(zooCfgFile.toString() + " file is missing");
System.exit(2);
}
Properties cfg = new Properties();
FileInputStream zooCfgStream = new FileInputStream(zooCfgFile); // 讀文件
try {
cfg.load(zooCfgStream);
} finally {
zooCfgStream.close();
}
HashMap<Long,QuorumServer> servers = new HashMap<Long,QuorumServer>(); // 保存集羣機器的信息
String dataDir = null;
String dataLogDir = null;
int clientPort = 0;
int tickTime = 0;
int initLimit = 0;
int syncLimit = 0;
int electionAlg = 3;
int electionPort = 2182;
for (Entry<Object, Object> entry : cfg.entrySet()) { // 獲取解析的文件的配置參數
String key = entry.getKey().toString(); // 轉爲string類型
String value = entry.getValue().toString();
if (key.equals("dataDir")) { // 文件目錄
dataDir = value;
} else if (key.equals("dataLogDir")) { // 日誌目錄
dataLogDir = value;
} else if (key.equals("clientPort")) { // 客戶端連接端口
clientPort = Integer.parseInt(value);
} else if (key.equals("tickTime")) { // 基本時間間隔
tickTime = Integer.parseInt(value);
} else if (key.equals("initLimit")) { // 配置多少個心跳間隔
initLimit = Integer.parseInt(value);
} else if (key.equals("syncLimit")) { // 表示主從之間最長不能超過多少個基本時間間隔
syncLimit = Integer.parseInt(value);
} else if (key.equals("electionAlg")) { // 選舉類型 有幾個選舉的策略可供選擇
electionAlg = Integer.parseInt(value);
} else if (key.startsWith("server.")) { // 解析配置集羣IP端口信息
int dot = key.indexOf('.');
long sid = Long.parseLong(key.substring(dot + 1)); // 獲取server配置的第一個id
String parts[] = value.split(":"); // 獲取 ip port
if ((parts.length != 2) &&
(parts.length != 3)){
LOG.error(value
+ " does not have the form host:port or host:port:port");
}
InetSocketAddress addr = new InetSocketAddress(parts[0],
Integer.parseInt(parts[1])); // 配置IP Port
if(parts.length == 2)
servers.put(Long.valueOf(sid), new QuorumServer(sid, addr));
else if(parts.length == 3){
InetSocketAddress electionAddr = new InetSocketAddress(parts[0],
Integer.parseInt(parts[2])); // 通信接口監聽
servers.put(Long.valueOf(sid), new QuorumServer(sid, addr, electionAddr)); // 壓入server
}
} else {
System.setProperty("zookeeper." + key, value); // 其他屬性直接設置到類上
}
}
if (dataDir == null) { // 檢查參數 是否爲空 如果爲空 則報錯
LOG.error("dataDir is not set");
System.exit(2);
}
if (dataLogDir == null) {
dataLogDir = dataDir;
} else {
if (!new File(dataLogDir).isDirectory()) {
LOG.error("dataLogDir " + dataLogDir+ " is missing.");
System.exit(2);
}
}
if (clientPort == 0) {
LOG.error("clientPort is not set");
System.exit(2);
}
if (tickTime == 0) {
LOG.error("tickTime is not set");
System.exit(2);
}
if (servers.size() > 1 && initLimit == 0) {
LOG.error("initLimit is not set");
System.exit(2);
}
if (servers.size() > 1 && syncLimit == 0) {
LOG.error("syncLimit is not set");
System.exit(2);
}
QuorumPeerConfig conf = new QuorumPeerConfig(clientPort, dataDir,
dataLogDir); // 生成一個實例 並設置參數
conf.tickTime = tickTime;
conf.initLimit = initLimit;
conf.syncLimit = syncLimit;
conf.electionAlg = electionAlg;
conf.servers = servers;
if (servers.size() > 1) { // 如果是多個server
/*
* If using FLE, then every server requires a separate election port.
*/
if(electionAlg != 0){
for(QuorumServer s : servers.values()){
if(s.electionAddr == null)
LOG.error("Missing election port for server: " + s.id);
}
}
File myIdFile = new File(dataDir, "myid"); // 檢查myid文件是否存在 該文件包含該實例的id信息
if (!myIdFile.exists()) {
LOG.error(myIdFile.toString() + " file is missing");
System.exit(2);
}
BufferedReader br = new BufferedReader(new FileReader(myIdFile)); // 獲取server id
String myIdString;
try {
myIdString = br.readLine();
} finally {
br.close();
}
try {
conf.serverId = Long.parseLong(myIdString);
} catch (NumberFormatException e) {
LOG.error(myIdString + " is not a number");
System.exit(2);
}
}
instance=conf; // 將解析好的數據設置到instance 中, 後續的實例信息都是從該實例獲取
} catch (Exception e) {
LOG.error("FIXMSG",e);
System.exit(2);
}
}
...
}
繼承的父類就是ServerConfig,主要查看該類的parse方法。
public static int getClientPort(){
assert instance!=null;
return instance.clientPort;
}
public static String getDataDir(){
assert instance!=null;
return instance.dataDir;
}
public static String getDataLogDir(){
assert instance!=null;
return instance.dataLogDir;
}
public static boolean isStandalone(){
assert instance!=null;
return instance.isStandaloneServer();
}
protected static ServerConfig instance=null;
public static void parse(String[] args) { // 解析的時候生成單例
if(instance!=null)
return;
if (args.length != 2) { // 如果輸入參數長度不爲2
System.err.println("USAGE: ZooKeeperServer port datadir\n");
System.exit(2);
}
try {
instance=new ServerConfig(Integer.parseInt(args[0]),args[1],args[1]);
} catch (NumberFormatException e) {
System.err.println(args[0] + " is not a valid port number");
System.exit(2);
}
}
QuorumPeer執行流程
由於QuorumPeer類繼承自Thread,所以調用start方法時,最終會調用QuorumPeer的start方法,然後該方法會執行run函數啓動線程執行。
@Override
public synchronized void start() {
startLeaderElection(); // 啓動選舉流程
super.start(); // 調用Thread的start方法,即最終會調用該類的run方法
}
此時就調用了startLeaderElection方法來啓動集羣的選舉。
synchronized public void startLeaderElection() {
currentVote = new Vote(myid, getLastLoggedZxid()); // 獲取最後的zxid 並首先投一票給自己
for (QuorumServer p : quorumPeers.values()) { // 獲取當前自己的id
if (p.id == myid) {
myQuorumAddr = p.addr; // 獲取當前的地址
break;
}
}
if (myQuorumAddr == null) { // 如果沒找到則報錯
throw new RuntimeException("My id " + myid + " not in the peer list");
}
if (electionType == 0) { // 如果選擇策略爲0
try {
udpSocket = new DatagramSocket(myQuorumAddr.getPort()); // 獲取端口 使用UDP進行選舉
responder = new ResponderThread(); // 開啓線程 執行
responder.start();
} catch (SocketException e) {
throw new RuntimeException(e);
}
}
this.electionAlg = createElectionAlgorithm(electionType); // 獲取當前的選舉算法
}
此時開始選舉的使用了UDP來進行選舉。
class ResponderThread extends Thread {
ResponderThread() {
super("ResponderThread");
}
volatile boolean running = true;
@Override
public void run() {
try {
byte b[] = new byte[36];
ByteBuffer responseBuffer = ByteBuffer.wrap(b);
DatagramPacket packet = new DatagramPacket(b, b.length);
while (running) {
udpSocket.receive(packet); // 接受數據包
if (packet.getLength() != 4) {
LOG.warn("Got more than just an xid! Len = "
+ packet.getLength());
} else {
responseBuffer.clear();
responseBuffer.getInt(); // Skip the xid // 跳過 xid
responseBuffer.putLong(myid);
Vote current = getCurrentVote(); // 獲取當前選票
switch (getPeerState()) {
case LOOKING: // 如果是競選狀態
responseBuffer.putLong(current.id); // 壓入id 和 zxid
responseBuffer.putLong(current.zxid);
break;
case LEADING:
responseBuffer.putLong(myid); // 如果是主 則返回當前主的服務器id
try {
responseBuffer.putLong(leader.lastProposed); // 壓入主 最後一次提交的事物
} catch (NullPointerException npe) {
// This can happen in state transitions,
// just ignore the request
}
break;
case FOLLOWING:
responseBuffer.putLong(current.id); // 壓入當前的id
try {
responseBuffer.putLong(follower.getZxid()); // 壓入 zxid
} catch (NullPointerException npe) {
// This can happen in state transitions,
// just ignore the request
}
}
packet.setData(b);
udpSocket.send(packet); // 將數據發送出去
}
packet.setLength(b.length);
}
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally {
LOG.warn("QuorumPeer responder thread exited");
}
}
}
根據當前的角色進行不同的操作,選舉過程中會傳輸當前的id和事物id來進行數據的統一,有關選舉的詳細內容後文再詳細分析。
開始執行
@Override
public void run() {
setName("QuorumPeer:" + cnxnFactory.getLocalAddress()); // 設置當前的名稱 該名稱以監聽客戶端的端口結尾
/*
* Main loop
*/
while (running) {
switch (getPeerState()) { // 獲取當前的狀態
case LOOKING:
try {
LOG.info("LOOKING");
setCurrentVote(makeLEStrategy().lookForLeader()); // 設置投票並選擇leader
} catch (Exception e) {
LOG.warn("Unexpected exception",e); // 如果出錯則設置爲LOOKING狀態
setPeerState(ServerState.LOOKING);
}
break;
case FOLLOWING:
try {
LOG.info("FOLLOWING");
setFollower(makeFollower(logFactory)); // 如果是FOLLOWING狀態則轉換成follower 跟隨主
follower.followLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally {
follower.shutdown();
setFollower(null);
setPeerState(ServerState.LOOKING);
}
break;
case LEADING:
LOG.info("LEADING");
try {
setLeader(makeLeader(logFactory)); // 設置成主狀態
leader.lead(); // 接聽所有事件請求
setLeader(null); // 如果失去當前主 則將主設置爲空
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally {
if (leader != null) { // 設置爲空並重置狀態
leader.shutdown("Forcing shutdown");
setLeader(null);
}
setPeerState(ServerState.LOOKING);
}
break;
}
}
LOG.warn("QuorumPeer main thread exited");
}
根據狀態來執行不同的操作,如果是主則接受從的連接,並處理從發送上來的信息。從則會轉發創建信息等到主節點進行處理。後續會詳細的描述整個過程。
客戶端服務器啓動
在創建的過程中也需要創建給客戶端連接請求的服務端口,創建過程就是初始化過程中執行;
new NIOServerCnxn.Factory(getClientPort())
該方法如下;
public Factory(int port) throws IOException {
super("NIOServerCxn.Factory:" + port); // 獲取服務端連接端口
setDaemon(true);
this.ss = ServerSocketChannel.open(); // 打開連接
ss.socket().bind(new InetSocketAddress(port)); // 監聽端口
ss.configureBlocking(false); // 設置成非阻塞
ss.register(selector, SelectionKey.OP_ACCEPT); // 設置該描述符爲接受請求
start(); // 開始執行
}
應該該類也是繼承自Thread,調用start就是執行了該類重寫的run方法。
public void run() {
while (!ss.socket().isClosed()) { // 檢查連接是否關閉
try {
selector.select(1000); // IO複用
Set<SelectionKey> selected;
synchronized (this) {
selected = selector.selectedKeys(); // 加鎖 獲取 當前的觸發事件描述符
}
ArrayList<SelectionKey> selectedList = new ArrayList<SelectionKey>(
selected);
Collections.shuffle(selectedList);
for (SelectionKey k : selectedList) { // 遍歷 該列表
if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0) { // 如果是新的請求進來
SocketChannel sc = ((ServerSocketChannel) k
.channel()).accept(); // 接受新連接
sc.configureBlocking(false); // 設置非阻塞
SelectionKey sk = sc.register(selector,
SelectionKey.OP_READ); // 註冊讀事件
NIOServerCnxn cnxn = createConnection(sc, sk); // 初始化一個NIOServerCnxn類
sk.attach(cnxn); // 添加到列表中
addCnxn(cnxn);
} else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) { // 如果是讀事件或者寫事件則獲取觸發內容
NIOServerCnxn c = (NIOServerCnxn) k.attachment();
c.doIO(k); // 回調執行處理該事件
}
}
selected.clear(); // 清空
} catch (Exception e) {
LOG.error("FIXMSG",e); // 如果報錯則打印錯誤日誌
}
}
ZooTrace.logTraceMessage(LOG, ZooTrace.getTextTraceLevel(),
"NIOServerCnxn factory exitedloop.");
clear();
LOG.error("=====> Goodbye cruel world <======");
// System.exit(0);
}
通過查看run函數的執行流程可知,該函數處理過程是一個典型的IO複用的處理過程,客戶端新入的請求都是通過該服務來進行處理的,後續會詳細分析該處理的詳細流程。
總結
本文主要是簡單的概述了一下Zookeeper集羣模式的啓動流程,很粗略的描述了啓動過程中執行的主要內容,首先會開啓一個線程接受客戶端請求的處理,然後打開一個選舉端口進行選舉,接着就會打開一個集羣之間數據處理同步的端口,至此三個端口都提供了不同的服務,完成了主要的Zookeeper集羣的啓動。由於本人才疏學淺,如有錯誤請批評指正。