程序的入口QuorumPeerMain
#
public static void main(String[] args) { // QuorumPeerMain main = new QuorumPeerMain(); try { // 初始化服务端,并运行服务端 // todo 跟进去看他如何处理 服务端的配置文件,以及根据服务端的配置文件做出来那些动作 main.initializeAndRun(args);
初始化和启动总览#
跟进initializeAndRun()
方法 , 这个方法中主要做了如下三件事
- 从
args[0]
解析出配置文件的位置,创建QuorumPeerConfig
配置类对象(可以把这个对象理解成单个ZK server的配置对象),然后将配置文件中的内容加载进内存,并完成对java配置类的属性的赋值 - 开启,启动并清除计划任务的逻辑
- 根据从内存中读取配置文件实例化好的配置类,启动ZKserver
protected void initializeAndRun(String[] args) throws ConfigException, IOException { // todo 这个类是关联配置文件的类, 我们在配置文件中输入的各种配置都是他的属性 QuorumPeerConfig config = new QuorumPeerConfig(); if (args.length == 1) { // todo config.parse(args[0]); } // Start and schedule the the purge task // todo 启动并清除计划任务 DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config .getDataDir(), config.getDataLogDir(), config .getSnapRetainCount(), config.getPurgeInterval()); purgeMgr.start(); // todo config.servers.size() > 0 说明添加了关于集群的配置 if (args.length == 1 && config.servers.size() > 0) { // todo 根据配置启动服务器, 跟进去, 就在下面 runFromConfig(config); } else { // todo 没添加集群的配置 LOG.warn("Either no config or no quorum defined in config, running " + " in standalone mode"); // there is only server in the quorum -- run as standalone // todo 启动单机 ZooKeeperServerMain.main(args); } }
读取配置文件#
下面跟进parse
, 这个方法的目的是将磁盘上的配置信息读取到文件中,完成对QuorumPeerConfig
的初始化主要做了如下两件事
- 因为ZK的配置文件是
.properties
结尾的,因此呢选择了Properties.java
(格式是 key=value)来解析读取配置文件 parseProperties()
方法,对解析出来的配置文件进行进一步的处理
public void parse(String path) throws ConfigException { File configFile = new File(path); LOG.info("Reading configuration from: " + configFile); try { if (!configFile.exists()) { throw new IllegalArgumentException(configFile.toString() + " file is missing"); } Properties cfg = new Properties(); FileInputStream in = new FileInputStream(configFile); try { // todo 使用 Properties 按行读取出配置文件内容 cfg.load(in); } finally { in.close(); } // todo 将按行读取处理出来的进行分隔处理, 对当前的配置类进行赋值 parseProperties(cfg); } catch (IOException e) { throw new ConfigException("Error processing " + path, e); } catch (IllegalArgumentException e) { throw new ConfigException("Error processing " + path, e); } }
解析配置文件#
看一看,他是如何处理已经被加载到内存的配置文件的,
- 首先看一下上图中我截取的配置文件的截图,可以看到通过下面的if-else分支语句将配置文件的中的信息一对一的读取出来,完成对当前配置类的初始化
- if (value.toLowerCase().equals("observer")) {..}这个分支就是判断当前的配置文件是不是Observer的配置文件,比较推荐的observer的配置,就是添加一条配置写
peerType=observer
,但是这是为了人们查看方便设计的,换句话说,一个普通的Follower的配置文件,即便是添加上了这条配置文件,它同样不是observer,后续还会有进一步的检验,因为zk集群的配置文件大同小异,一开始即便是我们不添加这个配置,observer角色的server依然会成为observer,但是对于人们来说,就不用点开dataDir中的myid文件查看究竟当前的server是不是Observer了 else if (key.startsWith("server."))
标记着配置文件中有关集群的配置信息开始了,它根据不同的配置信息,将不同身份的server存放进两个map中,就像下面那样,如果是Observer类型的,就存放在observers
中,如果是Follower类型的就添加进servers
map中
- 它这样做是为了下一步实现ZAB协议,过半检查. 而设计的, 什么是过半检查机制呢? 首先是集群中的server存在一半以上健康时,集群才可用
- 其次是,Leader发起的决议,需要有一半的Follower同意决议才能通过,注意这里是Follower,而不是OBserver+Follower,因为OBserver不参加投票,因此在这个半数协议中,它不作数, 所以再看他现在的做法,就是创建过半检查机制封装类
QuorumVerifer
时,使用servers
的容量
- 合并servers和observers, 虽然后者不参加决议投票,但是它同样需要提供服务
- 读取myid文件,最终确定不同的server的身份划分,哪个是myid配置文件呢? 它是我们在配置集群信息时在dataDir中创建的, 里面仅仅存放一个数据,这个数字不是乱写的,对应的是配置文件的server.n中的n, 启动时会读取这个文件,拿到里面的数据与 zoo.cfg 里面的配置信息比较从而判断到底是那个server,只是一个标识作用。
public void parseProperties(Properties zkProp) throws IOException, ConfigException { int clientPort = 0; String clientPortAddress = null; for (Entry<Object, Object> entry : zkProp.entrySet()) { String key = entry.getKey().toString().trim(); String value = entry.getValue().toString().trim(); 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("clientPortAddress")) { clientPortAddress = value.trim(); } else if (key.equals("tickTime")) { . . . . } else if (key.equals("peerType")) { if (value.toLowerCase().equals("observer")) { // todo 这是推荐配置做法在 observer 的配置文件中配置上添加 peerType=observer //todo 但是如果给一台不是observer的机器加上了这个配置, 它也不会是observer. 在这个函数的最后会有校验 peerType = LearnerType.OBSERVER; } else if (value.toLowerCase().equals("participant")) { peerType = LearnerType.PARTICIPANT; } else { throw new ConfigException("Unrecognised peertype: " + value); } . . . } else if (key.startsWith("server.")) { // todo 全部以server.开头的配置全部放到了 servers int dot = key.indexOf('.'); long sid = Long.parseLong(key.substring(dot + 1)); String parts[] = splitWithLeadingHostname(value); if ((parts.length != 2) && (parts.length != 3) && (parts.length !=4)) { LOG.error(value + " does not have the form host:port or host:port:port " + " or host:por . . . // todo 不论是普通节点,还是观察者节点,都是 QuorumServer, 只不过添加进到不同的容器 if (type == LearnerType.OBSERVER){ // todo 如果不是观察者的话,就不会放在 servers, // todo server.1=localhost:2181:3887 // todo server.2=localhost:2182:3888 // todo server.3=localhost:2183:3889 // todo port是对外提供服务的端口 electionPort是用于选举的port // todo 查看zk的数据一致性我们使用的端口是 port observers.put(Long.valueOf(sid), new QuorumServer(sid, hostname, port, electionPort, type)); } else { // todo 其他的普通节点放在 servers servers.put(Long.valueOf(sid), new QuorumServer(sid, hostname, port, electionPort, type)); } . . . . /* * Default of quorum config is majority */ if(serverGroup.size() > 0){ if(servers.size() != serverGroup.size()) throw new ConfigException("Every server must be in exactly one group"); /* * The deafult weight of a server is 1 */ for(QuorumServer s : servers.values()){ if(!serverWeight.containsKey(s.id)) serverWeight.put(s.id, (long) 1); } /* * Set the quorumVerifier to be QuorumHierarchical */ quorumVerifier = new QuorumHierarchical(numGroups, serverWeight, serverGroup); } else { /* * The default QuorumVerifier is QuorumMaj */ // todo 默认的仲裁方式, 过半机制中,是不包含 observer 的数量的 LOG.info("Defaulting to majority quorums"); quorumVerifier = new QuorumMaj(servers.size()); } // Now add observers to servers, once the quorums have been // figured out // todo 最后还是将 Observers 添加进了 servers servers.putAll(observers); /** * todo 当时搭建伪集群时,在每一个节点的dataDir文件中都添加进去了一个 myid文件 * 分别在zk、zk2、zk3、的dataDir中新建myid文件, 写入一个数字, 该数字表示这是第几号server. * 该数字必须和zoo.cfg文件中的server.X中的X一一对应. * myid的值是zoo.cfg文件里定义的server.A项A的值, * Zookeeper 启动时会读取这个文件,拿到里面的数据与 zoo.cfg 里面的配置信息比较从而判断到底是那个server,只是一个标识作用。 * */ // todo 找到当前节点的dataDir 下面的 myid文件 File myIdFile = new File(dataDir, "myid"); if (!myIdFile.exists()) { throw new IllegalArgumentException(myIdFile.toString() + " file is missing"); } BufferedReader br = new BufferedReader(new FileReader(myIdFile)); String myIdString; try { // todo 读取出myid里面的内容 myIdString = br.readLine(); } finally { br.close(); } try { // todo myid文件中存到的数据就是 配置文件中server.N 中的 N这个数字 serverId = Long.parseLong(myIdString); MDC.put("myid", myIdString); } catch (NumberFormatException e) { throw new IllegalArgumentException("serverid " + myIdString + " is not a number"); } // todo 通过检查上面的Observers map 中是否存在 serverId, 这个serverId其实就是myid, 对应上了后,就将它的 // Warn about inconsistent peer type LearnerType roleByServersList = observers.containsKey(serverId) ? LearnerType.OBSERVER : LearnerType.PARTICIPANT; if (roleByServersList != peerType) { LOG.warn("Peer type from servers list (" + roleByServersList + ") doesn't match peerType (" + peerType + "). Defaulting to servers list."); peerType = roleByServersList; }
根据配置文件启动ZKServer#
在一开始的QuorumPeerMain.java
类中的Initializer()
方法中,存在如下的逻辑,判断是单机版本启动还是集群的启动
if (args.length == 1 && config.servers.size() > 0) { // todo 根据配置启动服务器, 跟进去, 就在下面 runFromConfig(config); } else { // todo 没添加集群的配置 LOG.warn("Either no config or no quorum defined in config, running " + " in standalone mode"); // there is only server in the quorum -- run as standalone // todo 启动单机 ZooKeeperServerMain.main(args); }
如果是单机版本的话,会进入else块从此构建ZookeeperServerMain
对象, 可以把这个ZooKeeperServerMain
理解成一个辅助类,经过它,初始化并启动一个ZooKeeperServer.java的对象
继续跟进
public static void main(String[] args) { // todo 使用无参的构造方法实例化服务端, 单机模式 ZooKeeperServerMain main = new ZooKeeperServerMain(); try { // todo 跟进去看他如何解析配置文件 main.initializeAndRun(args);
继续跟进
protected void initializeAndRun(String[] args) throws ConfigException, IOException { try { ManagedUtil.registerLog4jMBeans(); } catch (JMException e) { LOG.warn("Unable to register log4j JMX control", e); } // todo 这个配置类, 对应着单机模式的配置类 , 里面的配置信息很少 ServerConfig config = new ServerConfig(); if (args.length == 1) { config.parse(args[0]); } else { // todo 单机版本 config.parse(args); } // todo 读取配置,启动单机节点 runFromConfig(config); }