每日学习笔记(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);
    }
 

posted on 2012-03-07 22:12 Phinecos(洞庭散人) 阅读(...) 评论(...) 编辑 收藏

导航

统计

公告