每日学习笔记(25)

一, 上一篇中第4点里,我遇到了一个solr集群中各个节点在进行master-slave身份切换时困扰的问题,详情见上一篇。今天顺利地解决了这个问题,下面记录下完整的解决方案。

     0) 值得特别注意的一点,在创建Solr核的时候,不要指定dataDir,而只提供必需的instanceDir即可,即创建命令如下:   

 http://localhost:8983/solr/admin/cores?action=CREATE&name=coreX&instanceDir=path_to_instance_directory

  这是因为后面reload相应的solr核时,系统讲index目录变成index.20120307213400这样带后缀的索引目录,若创建solr核时指定死了dataDir,那么会发现新建立的mater/slava之间无法正常进行索引复制,根本原因在于slave节点会一直认为mater的索引目录在index下面,而实际是没有的,所以不去指定,让系统来处理,反而可以顺利进行replication.

1) 首先,为每个solr节点依次分配优先顺序,在zookeeper集群中同一个ZNode下注册为一个EPHEMERAL节点,节点名称就是每个Solr节点的机器名。

2) 这样在Zookeeper回调的Watcher里就可以拿到当前最新已经注册的solr节点列表,为了简单,我们采用“序号最小的节点为整个集群的master节点这个master选举策略。

3) 这里有一种特殊情况需要考虑,若序号不是最小的Solr节点抢先注册,那么它会在真正的master节点注册之前窃取到master身份,并且导致在此之前集群里其他solr节点也误认为它是真正的master节点。等到真正最小序号的master节点在zookeeper中注册后,就需要前面窃取master身份的节点将master身份交还给真正序号最小的节点,自己重新变为slave节点,相应的replication配置片段也要从master的配置变为并且其他slave节点需要重新指向新的master节点。

4) 这之后,还有一种情况需要考虑,若集群的master节点突然挂了,那么此时序号紧挨在它之后的slave节点就自动被选举为master节点,它的replication配置片段要从slave片段变为master配置,并且重新reload相应的solr核,而其他slave节点就需要将replicationmaserUrl指向这个新选举出来的master节点,并且重新reload相应的solr核。

5) 再之后,若原先挂掉的master节点经过抢修后,又重新回到集群中来,那么由于它的序号会是最小的,就会从上次被选举出的master节点手中夺回master的位置,这就又回到了步骤2)的过程了。这里特别要注意的一点,重新拿回master节点的位置,需要强制性提交一个commit请求,以启动master/slave之间的索引复制过程


整个解决方案具体的实现代码如下:

     /**
     * 实际的重新加载solr核动作
     * 
@param  coreName
     
*/
     private  void doReloadSolrCore(String coreName) {
         boolean isLoadOk =  false;
        HttpClient client =  new HttpClient();

        String createCMD = "http://" + Constants.LOCAL_ADDRESS + ":" + jbossPort + "/admin/cores?action=RELOAD&core=" + coreName;
        HttpMethod method =  new GetMethod(createCMD);
        method.getParams().setContentCharset("GBK");
        method.getParams().setHttpElementCharset("GBK");
        method.getParams().setCredentialCharset("GBK");
        logger.error("重新加载核命令: " + createCMD);
         //  execute the method.
         try {
            client.executeMethod(method);
        }  catch (Exception e) {
            logger.error("重新加载solr核失败, 核名称: " + coreName, e);
             throw  new RuntimeException("重新加载solr核失败, 核名称: " + coreName, e);
        }

    }
    
     /**
     * 启动一个线程重新加载solr核
     * 
@param  groupName
     * 
@param  coreName
     
*/
     private  void reloadSolrCore( final String coreName) {
         // http://localhost :8983/solr/admin/cores?action=RELOAD&core=core0
         new Thread() {
            @Override
             public  void run() {
                 try {
                    doReloadSolrCore(coreName);
                }  catch (Exception e) {
                    logger.error("重新加载solr核失败, 核名称: " + coreName, e);
                     throw  new RuntimeException("重新加载solr核失败, 核名称: " + coreName, e);
                }
            }
        }.start();
    }
    
     /**
     * 本机从slave成为master,修改对应的配置文件
     * 
@param  solrConfigPath solr配置文件路径
     
*/
     private  void changeConfigAsMaster(String solrConfigPath) {
        SAXReader reader =  new SAXReader();  
        File file =  new File(solrConfigPath);
         try {
            Document doc = reader.read(file);
             // 首先移除以前slave的配置片段
            Node node = doc.selectSingleNode("/config/requestHandler[@name=\"/replication\"]");
            node.detach();
             // 然后加入master的配置片段
            Element rootElement = doc.getRootElement(); // 拿到<config/>元素
            Element oneLevelElement = rootElement.addElement("requestHandler").addAttribute("name", "/replication").addAttribute("class", "solr.ReplicationHandler");
            Element twoLevelElement = oneLevelElement.addElement("lst").addAttribute("name", "master");
            twoLevelElement.addElement("str").addAttribute("name", "replicateAfter").addText("startup");
            twoLevelElement.addElement("str").addAttribute("name", "replicateAfter").addText("commit");
            twoLevelElement.addElement("str").addAttribute("name", "commitReserveDuration").addText("00:00:10");
            XMLWriter writer =  new XMLWriter( new FileWriter(solrConfigPath));
            writer.write(doc);
            writer.close();
        }  catch (DocumentException e) {
            logger.error("写入master节点配置片段失败, 配置文件路径: " + solrConfigPath);
             throw  new RuntimeException("写入master节点配置片段失败, 配置文件路径: " + solrConfigPath, e);
        } catch (IOException e) {
            logger.error("写入master节点配置片段失败, 配置文件路径: " + solrConfigPath);
             throw  new RuntimeException("写入master节点配置片段失败, 配置文件路径: " + solrConfigPath, e);
        }
    }
     /**
     * 修改slave配置片段,指向新的mater节点,如果我原先是master,那么先去掉master的配置,因为我身份将转为slave,若原先是slave,只需要简单修改masterUrl即可
     * 
@param  solrConfigPath solr配置文件路径
     
*/
     private  void changeConfigForSlave(String solrConfigPath, String coreName, String newMasterHost) {
        SAXReader reader =  new SAXReader();  
        File file =  new File(solrConfigPath);
        String masterUrl = "";
         if (StringUtils.isNotBlank(newMasterHost)) {
            masterUrl = "http://" + newMasterHost + ":" + jbossPort + "/" + coreName + "/replication";
             try {
                Document doc = reader.read(file);
                
                Node node = doc.selectSingleNode("/config/requestHandler[@name=\"/replication\"]");
                 if ( null != node) { // 如果我原先是master,那么先去掉master的配置,
                    node.detach();
                }
                Element rootElement = doc.getRootElement(); // 拿到<config/>元素
                Element oneLevelElement = rootElement.addElement("requestHandler").addAttribute("name", "/replication").addAttribute("class", "solr.ReplicationHandler");
                Element twoLevelElement = oneLevelElement.addElement("lst").addAttribute("name", "slave");
                twoLevelElement.addElement("str").addAttribute("name", "masterUrl").addText(masterUrl);
                twoLevelElement.addElement("str").addAttribute("name", "pollInterval").addText("00:02:00");
                twoLevelElement.addElement("str").addAttribute("name", "compression").addText("internal");
                twoLevelElement.addElement("str").addAttribute("name", "httpConnTimeout").addText("5000");
                twoLevelElement.addElement("str").addAttribute("name", "httpReadTimeout").addText("10000");
                XMLWriter writer =  new XMLWriter( new FileWriter(solrConfigPath));
                writer.write(doc);
                writer.close();
            }  catch (DocumentException e) {
                logger.error("写入slave节点配置片段失败, 配置文件路径: " + solrConfigPath);
                 throw  new RuntimeException("写入master节点配置片段失败, 配置文件路径: " + solrConfigPath, e);
            } catch (IOException e) {
                logger.error("写入slave节点配置片段失败, 配置文件路径: " + solrConfigPath);
                 throw  new RuntimeException("写入master节点配置片段失败, 配置文件路径: " + solrConfigPath, e);
            }
        }
    }
    
     /**
     * 本机新身份是master,是从slave转来的,调整指定核的配置文件,重新reload
     * 
@param  coreName
     
*/
     private  void adjustSolrCoreAsMaster(String coreName) {
         if ( this.mapCoreConfigInfo.containsKey(coreName)) {
            String configFilePath =  this.mapCoreConfigInfo.get(coreName); // 目标配置文件路径
             if (StringUtils.isNotBlank(configFilePath)) {
                 // 修改配置文件replication判断为master的片段
                 this.changeConfigAsMaster(configFilePath);
                 this.reloadSolrCore(coreName);
            }
        }
    }
    
     /**
     * 本机新的身份是slave,可能是master转来的,或者原先还是slave,无论哪种,都需要调整指定核的配置文件,指向新的master节点, 重新reload
     * 
@param  coreName solr核名称
     
*/
     private  void adjustSolrCoreAsSlave(String coreName, String newMasterHost) {
         if ( this.mapCoreConfigInfo.containsKey(coreName)) {
            String configFilePath =  this.mapCoreConfigInfo.get(coreName); // 目标配置文件路径
             if (StringUtils.isNotBlank(configFilePath)) {
                 // 修改配置文件replication, 指向新的master地址
                 this.changeConfigForSlave(configFilePath, coreName, newMasterHost);
                 this.reloadSolrCore(coreName);
            }
        }
    }
    
     /**
     * 为了m/s同步,master节点强制性commit一下
     
*/
     private  void commitMasterIndex(String masterHost, String port, String coreName) {
        String targerUrl = "http://" + masterHost + ":" + port + "/" + coreName;
        logger.error("master节点强制性commit一下,url: " + targerUrl);
        CommonsHttpSolrServer httpSolrServer =  null;
         try {
            httpSolrServer =  new CommonsHttpSolrServer(targerUrl);
        }  catch (MalformedURLException e1) {
             throw  new RuntimeException("连接solr服务器失败, targetURL: " + targerUrl, e1);
        }
        httpSolrServer.setSoTimeout(Constants.soTimeOut);
        httpSolrServer.setConnectionTimeout(Constants.connectionTimeOut);
        httpSolrServer.setDefaultMaxConnectionsPerHost(Constants.maxConnectionsPerHost);
        httpSolrServer.setMaxTotalConnections(Constants.maxTotalConnections);
        httpSolrServer.setFollowRedirects( false);
        httpSolrServer.setAllowCompression( true);
        httpSolrServer.setMaxRetries(Constants.maxRetries);

         try {
            httpSolrServer.commit();
        }  catch (Exception e) {
        }
    }
    
     /**
     * 指定核下指定组的节点发生变化
     * 
@param  coreName solr核名称
     * 
@param  groupName 机器分组名称
     * 
@param  newNodeNameList 最新的组内各个节点
     
*/
     private  void masterSlaveNodeChanged(String coreName, String groupName, List<String> newNodeHostList) {
        logger.error("===========masterSlaveNodeChanged============, solr核名称: " + coreName + ", 组节点名称: " + groupName + ", node节点IP列表: " + newNodeHostList);
        logger.error("此时全局数据信息: " + mapGroupNodeStatus);

        String newMasterHost = getNewestMasterHost(newNodeHostList); // 当前最新的master节点IP
        String oldMasterHost =  this.getOldMasterHost(coreName, groupName); // 老的master节点地址
         if (StringUtils.isBlank(oldMasterHost)) {
             // 若本地缓存中记录的master节点没有,则说明是第一次取master节点,
             if (StringUtils.isNotBlank(newMasterHost)) {
                logger.error("maser节点地址: " + newMasterHost);
                 this.setMasterNode(coreName, groupName, newMasterHost);
            }
             if (newMasterHost.equalsIgnoreCase(Constants.LOCAL_ADDRESS) && MineNotifyProducer.isAuctionEntityGroup(coreName)) {
                 this.commitMasterIndex(Constants.LOCAL_ADDRESS, jbossPort, coreName); //若这一次是本机重新夺回master节点,强制性commit一下,避免出现replication过程没有启动
                logger.error("本机是master节点,可以启动相应的消费者线程");
            }
        }  else  if (!oldMasterHost.equalsIgnoreCase(newMasterHost)) {
             // 若本地缓存有记录,却与最新的master节点不一致,则说明原来的master挂了,取最新的master节点
             if (StringUtils.isNotBlank(newMasterHost)) {
                 this.setMasterNode(coreName, groupName, newMasterHost);
                 if (oldMasterHost.equalsIgnoreCase(Constants.LOCAL_ADDRESS)) {
                     // 本机是老的master,集群却又有了新的master,说明是本机提前窃取了master身份,现在需要归还出master身份,自己重新成为slave
                    
// 结束掉相应的处理线程
                    logger.error("本机是老的master,集群却又有了新的master: " + newMasterHost + ", 说明是发布启动阶段, 本机提前窃取了master身份,结束掉相应的处理线程");

                     this.adjustSolrCoreAsSlave(coreName, newMasterHost); //身份成为slave, 写入slave的配置,指向当前最新的master地址,并reload对应的solr核
                }  else {
                     if (newMasterHost.equalsIgnoreCase(Constants.LOCAL_ADDRESS)) {
                         // 若本机是新的mater节点,则需要转换身份,从slave转为maser, 修改replciation配置,重载solr核,
                         this.adjustSolrCoreAsMaster(coreName);
                    }  else {
                         // 本机原本就是slave节点,需要修改replication配置,指向新的master,重载solr核
                         this.adjustSolrCoreAsSlave(coreName, newMasterHost);
                    }
                }
            }
        }
        
         // 首先从节点列表中去掉master节点
        newNodeHostList.remove(newMasterHost);
         // 设置新的slave列表
         if (newNodeHostList.size() > 0) {
             this.setSlaveNodeList(coreName, groupName, newNodeHostList);
        }  else { // slave节点全都挂了,只剩下master节点了,
             this.clearSlaveNodeList(coreName, groupName);
        }

   }  

 

二, HSF服务的测试步骤:

1) 首先在pom文件中加入依赖项 

< dependency >             

   <groupId>com.taobao.hsf</groupId>
   <artifactId>hsfunit</artifactId>
   <version>1.0.2-SNAPSHOT</version>
   <scope>test</scope>
</dependency>

2)  启动HSF
     static {
        HSFEasyStarter.startFromPath("/hsf/release");
    }
3)  Junit 测试脚本中加入下述语句,每次都等待 HSF 服务准备完毕,才在 test** 方法中去使用服务。
    @Before
     public  void preDate()  throws SolrServerException, IOException{        
        ServiceUtil.waitServiceReady(queryService);
    }
 

你可能感兴趣的:(每日学习笔记(25))