企业级分布式实时搜索模型研究与实现SorlCloud+HBase+Flume-ng


通常我们在电信项目中采用HBase来存储用户终端明细数据等,供前台页面即时查询。HBase无可置疑拥有其优势,但其本身只对rowkey支持毫秒级的快速检索,对于多字段的组合查询却无能为力。针对HBase的多条件查询也有多种方案,实时搜索已成为信息检索领域的热点问题之一。但是有些方案要么太复杂,要么效率太低,本文将对基于SolrCloud的HBase多条件查询方案进行测试和验证,同时结合SparkStreaming对SolrCloud的索引进行定期维护操作。

SolrCloud+HBase 组合的原理:

基于Solr的HBase多条件查询原理很简单,将HBase表中涉及条件过滤的字段和rowkey在Solr中建立索引,通过Solr的多条件查询快速获得符合过滤条件的rowkey值,拿到这些rowkey之后在HBASE中通过指定rowkey进行查询。

1.    操作系统:centos6.5(64位)

2.    应用软件:CDH5.6.0(HDFS、Yarn、Zookeeper、Kafka、HBase、Flume-ng、SolrCloud)

3.    编程环境:oracle-j2sdk1.7-1.7.0+update67-1.x86_64




三、  Flume程序设计

数据是从消息中间件(kafka)中读取,由flume担任消费者(comsumer),同时将数据实时sink到hbase和solr cloud。用flume为hbase和solr cloud搭建起桥梁。代码如下:

public class MMSAsyncHbaseEventSerializer implements AsyncHbaseEventSerializer {

    private byte[] table;
    private byte[] cf;
    private byte[][] payload;
    private byte[][] payloadColumn;
    private final String payloadColumnSplit = "\\|";
    private String SOLR_URL="";

    @Override
    public void configure(Context context) {
        String pCol = context.getString("payloadColumn","pCol");
        SOLR_URL=context.getString("solrurl");
        if(SOLR_URL.isEmpty()){
            //取solrURL地址,且不能为空
            System.out.println("Solr Url地址不能为空!");
            System.exit(-1);
        }
        if(pCol != null && !pCol.isEmpty()) {
            // 从配置文件中读出column。
            String[] pCols = pCol.replace(" ", "").split(",");
            payloadColumn = new byte[pCols.length][];
            for (int i = 0; i < pCols.length; i++) {
                // 列名转为小写
                payloadColumn[i] = pCols[i].toLowerCase().getBytes(Charsets.UTF_8);
            }
        }
    }

    @Override
    public void configure(ComponentConfiguration context) {
    }

    @Override
    public void initialize(byte[] table, byte[] cf) {
        this.table = table;
        this.cf = cf;
    }

    @Override
    public void setEvent(Event event) {
        String strBody = new String(event.getBody());
        String[] subBody = strBody.split(this.payloadColumnSplit);
        if (subBody.length == this.payloadColumn.length){
            this.payload = new byte[subBody.length][];
            for (int i = 0; i < subBody.length; i++){
                this.payload[i] = subBody[i].getBytes(Charsets.UTF_8);
            }
        }
    }

    @Override
    public List<PutRequest> getActions() {
        List<PutRequest> actions = new ArrayList<PutRequest>();
        List<SolrInputDocument> docs = new ArrayList<SolrInputDocument>();
        if (payloadColumn != null) {
            try {
                // for 循环,提交所有列和对于数据的put请求。
                String keys = HashRowKeyGenerator.getRowKey().toString();
                String rowkey="";
                for (int i = 0; i < this.payload.length; i++) {
                    //HBase操作
                    rowkey = new String(payload[0], "UTF8") + keys;
                    PutRequest putRequest = new PutRequest(table,rowkey.getBytes("UTF8"), cf, payloadColumn[i],payload[i]);
                    actions.add(putRequest);
                }
                //solr操作
                SolrInputDocument doc1 = new SolrInputDocument();
                if(!rowkey.isEmpty()){
                    doc1.addField("id", rowkey, 1.0f);
                    doc1.addField("productName", new String(payload[6], "UTF8"),1.0f);
                    docs.add(doc1);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            try {
                CloudSolrServer solrserver = new CloudSolrServer(urlSolr);
                solrserver.setDefaultCollection(defaultCollection);
                solrserver.setZkClientTimeout(zkClientTimeOut);
                solrserver.setZkConnectTimeout(zkConnectTimeOut);           server.add(docs.iterator());
                server.commit();
            } catch (Exception e) {
                System.out.println(e);
            }
        }
        return actions;
    }

    @Override
    public List<AtomicIncrementRequest> getIncrements() {
        List<AtomicIncrementRequest> actions = new ArrayList<AtomicIncrementRequest>();
        return actions;
    }

    @Override
    public void cleanUp() {
    }
}

Flume 配置文件:

a1.sources = r1
a1.sinks = k1
a1.channels = c1

# Describe/configure the source
a1.sources.r1.type = org.apache.flume.plugins.KafkaSource
a1.sources.r1.zookeeper.connect=Master:2181,Slave1:2181,Slave2:2181
a1.sources.r1.group.id=test-consumer-group
a1.sources.r1.zookeeper.session.timeout.ms=400
a1.sources.r1.zookeeper.sync.time.ms=200
a1.sources.r1.auto.commit.interval.ms=1000
a1.sources.r1.custom.topic.name=test2
a1.sources.r1.custom.thread.per.consumer=4
a1.sources.r1.batchSize = 10000000

a1.sinks.k1.type  = org.apache.flume.sink.hbase.AsyncHBaseSink
a1.sinks.k1.table = test11
a1.sinks.k1.columnFamily = cf
a1.sinks.k1.serializer.pp = 11
a1.sinks.k1.serializer.payloadColumn=msg_id,service_id,msg_fmt,src_id,dest_phone,msg_length,msg_content_tmp,timestr,result
a1.sinks.k1.serializer = org.apache.flume.sink.hbase.MMSAsyncHbaseEventSerializer

# Use a channel which buffers events in memory
a1.channels.c1.type = memory
a1.channels.c1.capacity = 100000000
a1.channels.c1.transactionCapacity = 1000000

# Bind the source and sink to the channel
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1

 

四、HBase的RowKey设计

当处理由连续事件得到的数据时,即时间上连续的数据。它们显著的特点就是rowkey中含有事件发生时间。带来的一个问题便是HBase对于row的不均衡分布,它们被存储在一个唯一的rowkey区间中,被称为region,区间的范围被称为Start KeyEnd Key

对于单调递增的时间类型数据,很容易被散列到同一个Region中,这样它们会被存储在同一个服务器上,从而所有的访问和更新操作都会集中到这一台服务器上,从而在集群中形成一个hot spot,从而不能将集群的整体性能发挥出来。

要解决这个问题是非常容易的,只需要将所有的数据散列到全部的Region上即可。这是可以做到的,比如,在rowkey前面加上一个非线程序列,本案例的做法如下:

 HBase RowKey使用msg_id+ MD5Hash.getMD5AsHex(Bytes.toBytes(currentId)).substring(0,8).getBytes(),同时TTL保留24小时数据,轮训删除

COLUMNFAMILIES DESCRIPTION                                                                                                                                                                   

{NAME=> 'cf', DATA_BLOCK_ENCODING => 'NONE', BLOOMFILTER => 'ROW',REPLICATION_SCOPE => '0', COMPRESSION => 'NONE', VERSIONS => '5', TTL=> '86400 SECONDS', MIN_VERSIONS => '0', KEEP_DELETED_CE

LLS=> 'FALSE', BLOCKSIZE => '65536', IN_MEMORY => 'false', BLOCKCACHE=> 'true'}  

public class HashRowKeyGenerator {

   
public static String getRowKey(){
       
long currentId= 100L;
        byte
[]rowkey=Bytes.add(MD5Hash.getMD5AsHex(Bytes.toBytes(currentId)).substring(0, 8).getBytes(),Bytes.toBytes(currentId));
        return
rowkey.toString();
   
}
}

 

五、Cloudera Manger管理Solr Cloud

由于现在solrcloud上是没有collection的,我们先创建一个collection,命名为collection1,在创建collection1之前,首先要将配置信息上传到zookeeper上,我们这里就将solr自带的examples里面的那个solr目录下面的collection1的配置上传,对应cm安装的集群,这个目录在/etc/solr/conf/collection1

sudo-u solr solrctl instancedir --create collection1 /etc/solr/conf/collection1

SolrCloud提供近实时搜索,索引持久化,同时SolrCloud也能被配置成分片(shard)索引,这样集群主机可以把索引数据分布到各个主机上,多个shard可以组成一个Collection.

SolrCloud集群的所有的配置存储在ZooKeeper. 一旦一个SolrCloud节点启动, 该节点的配置信息将发送到ZooKeeper上存储.
Shard Replica除了作为容灾备份存在, 另外一个作用就是分散查询请求, 提高整个集群的查询能力.

创建solr中心库product

 

public static void query(){
    try{
        HttpSolrServer server=new HttpSolrServer(SOLR_URL);
        SolrQuery query=new SolrQuery("productName:10元/");
        QueryResponse res=server.query(query);
        SolrDocumentList list=res.getResults();
        for(int i=0;i<list.size();i++){
            System.out.println(">>>>>>>>>>>>>>>>>"+list.get(i).get("rowkey"));
        }
    }catch(Exception e){
        e.printStackTrace();
    }
}

 通过solr cloud提供的rowkey进行filter查询

// 获取指定rowkey数据集
public static void getRows(String tableName, List<String> rows) throws Exception {
    table = connection.getTable(tableName);
    for(String row:rows){
        Get get = new Get(Bytes.toBytes(row));
        Result result = table.get(get);
        // 输出结果,raw方法返回所有keyvalue数组
        for (Cell cell : result.rawCells()) {
            System.out.print("行名:" + new String(CellUtil.cloneRow(cell)) + " ");
            System.out.print("时间戳:" + cell.getTimestamp()+ " ");
            System.out.print("列族名:" + new String(CellUtil.cloneFamily(cell)) + " ");
            System.out.print("列名:" + new String(CellUtil.cloneQualifier(cell)) + " ");
            System.out.println("值:" + new String(CellUtil.cloneValue(cell)));
        }
    }
}

 中文分词(Jcseg)设计

由于SolrCloud对中文分词的支持不是很好,经过对多个中文分词(IKAnalysis)、ansj_seg、mmseg4j等进行比较,要么对中文标点符号和特殊符号支持不佳,要么分词效率比较低。经多方面考虑采用Jcseg。

jcseg是使用Java开发的一款开源的中文分词器, 使用mmseg算法. 分词准确率高达

98.4%,支持中文人名识别, 同义词匹配, 停止词过滤..., 详情请查看jcseg官方首页.

官方首页:  https://code.google.com/p/jcseg/

下载地址: https://code.google.com/p/jcseg/downloads/list

 git   源码: http://git.oschina.net/lionsoul/jcseg

Jcseg详细功能介绍:(可以略过, 方便查看新版本功能变化)

1。目前最高版本:jcseg-1.9.5。兼容最高版本 lucene, solr, elasticsearch

2。三种切分模式:

(1).简易模式:FMM算法,适合速度要求场合。

(2).复杂模式-MMSEG四种过滤算法,具有较高的岐义去除,分词准确率达到了98.41%。

(3).检测模式:只返回词库中已有的词条,很适合某些应用场合。(1.9.4开始)

3。智能中文人名识别。中文人名识别正确率达94%以上。

4.。支持自定义词库。在lexicon文件夹下,可以随便添加/删除/更改词库和词库内容,并且对词库进行了分类。

具体参考官方文档。

操作步骤如下

1.在solrconfig.xml中配置引用的中文分词的jar包目录:

<lib dir="目录前缀/solr/product/lib/"regex=".*\.jar" />

1.在shema.xml中配置

<field name="rowkey" type="string" indexed="true" stored="true" required="true" multiValued="false" />
<field name="Content" type="textComplex" indexed="true" stored="false" omitNorms="true" />

 

<!-- 复杂模式分词: -->
<fieldtype name="textComplex" class="solr.TextField">
<analyzer>
<tokenizer class="org.lionsoul.jcseg.solr.JcsegTokenizerFactory" mode="complex"/>
</analyzer>
</fieldtype>
<!-- 简易模式分词: -->
<fieldtype name="textSimple" class="solr.TextField">
<analyzer>
<tokenizer class="org.lionsoul.jcseg.solr.JcsegTokenizerFactory" mode="simple"/>
</analyzer>
</fieldtype>
<!-- 检测模式分词: -->
<fieldtype name="textSimple1" class="solr.TextField">
<analyzer>
<tokenizer class="org.lionsoul.jcseg.solr.JcsegTokenizerFactory" mode="detect"/>
</analyzer>
</fieldtype>

 

将编辑好的文件传到主机上,并提交Cloud Manager集群主机: sudo -u solr solrctl instancedir --create product/etc/solr/conf/product

 

 

、SolrCloud增量索引-删除业务

SolrCloud索引数据保存24小时内的数据,所以借住webapplication的listener监听器,做定时任务,每隔1分钟或者10秒,删除时间戳范围数据。

public class SolrSchedulJob implements ServletContextListener{

   
private Timertimer;
    private
MyTask task;

   
@Override
   
public void contextDestroyed(ServletContextEventarg0) {
       
timer.cancel();
       
System.out.println("定时器已销毁");
   
}

    @Override
   
public void contextInitialized(ServletContextEvent event) {
        timer =
new java.util.Timer(true);
       
task = new MyTask();
       
System.out.println("定时器已启动");
        
timer.schedule(task, 0, 10 * 1000);
       
System.out.println("已经添加任务调度表");
   
}

}

 

Task任务定时删除时间戳索引:

public class SolrRemoveIndexTask extends TimerTask {

   
private static final String SOLR_URL= "http://Master:8983/Solr/product";

   
@Override
   
public void run() {
       
long start= System.currentTimeMillis();
       
Date beginDate = new Date();
       
Calendar date = Calendar.getInstance();
       
date.setTime(beginDate);
       
date.set(Calendar.DATE, date.get(Calendar.DATE)- 1);
        long
end = date.getTime().getTime();
       
HttpSolrServer server = new HttpSolrServer(SOLR_URL);
       
SolrQuery query = new SolrQuery("createtime:[" + start + " to " + end + "]");
       
QueryResponse res;
        try
{
            res = server.query(query)
;
           
SolrDocumentList list = res.getResults();
           
List<String> ids = new ArrayList<String>();
            for
(int i = 0; i < list.size(); i++){
                ids.add(list.get(i).get(
"rowkey").toString());
            
}
            server.deleteById(ids)
;
           
server.commit();
       
} catch(Exception e) {
            e.printStackTrace()
;
       
}
        System.
out.println("timeelapsed(ms):" + (System.currentTimeMillis()- start));
   
}
}

 

在web.xml中配置的listener监听器:

<listener>
<listener-class>servlet
.SolrSchedulJob</listener-class>

</listener>

 

基于Solr的HBase多条件查询原理很简单,将HBase表中涉及条件过滤的字段和rowkey在Solr中建立索引,通过Solr的多条件查询快速获得符合过滤条件的rowkey值,拿到这些rowkey之后在HBASE中通过指定rowkey进行查询。当然实现rowkey共享机制还有很多种方法。此案例开发过程中也存在诸多问题。欢迎指正。谢谢!


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值