├── src ├── main │ ├── resources │ │ ├── application.properties │ │ └── log4j.properties │ └── java │ │ └── com │ │ └── hbase │ │ └── easy │ │ ├── index │ │ └── HbaseSolrIndexCoprocesser.java │ │ └── solr │ │ └── SolrIndexTools.java └── test │ └── java │ └── com │ └── hbase │ └── easy │ └── test │ └── ConfigTest.java ├── LICENSE ├── README.md └── pom.xml /src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qindongliang/hbase-increment-index/HEAD/src/main/resources/application.properties -------------------------------------------------------------------------------- /src/test/java/com/hbase/easy/test/ConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.hbase.easy.test; 2 | 3 | import com.typesafe.config.Config; 4 | import com.typesafe.config.ConfigFactory; 5 | 6 | /** 7 | * Created by qindongliang on 2016/2/15. 8 | */ 9 | public class ConfigTest { 10 | 11 | public static void main(String[] args) { 12 | Config config= ConfigFactory.load("application.properties"); 13 | 14 | 15 | 16 | 17 | 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # Logging level 2 | log4j.rootLogger=WARN 3 | 4 | log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender 5 | 6 | log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout 7 | log4j.appender.CONSOLE.layout.ConversionPattern=%-4r [%t] %-5p %c %x \u2013 %m%n 8 | 9 | #- size rotation with log cleanup. 10 | log4j.appender.file=org.apache.log4j.RollingFileAppender 11 | log4j.appender.file.MaxFileSize=4MB 12 | log4j.appender.file.MaxBackupIndex=9 13 | 14 | #- File to log to and log format 15 | log4j.appender.file.File=logs/hbase-index.log 16 | log4j.appender.file.layout=org.apache.log4j.PatternLayout 17 | log4j.appender.file.layout.ConversionPattern=%-5p - %d{yyyy-MM-dd HH\:mm\:ss.SSS}; %C; %m\n 18 | 19 | log4j.logger.org.apache.zookeeper=WARN 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 qindongliang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/com/hbase/easy/index/HbaseSolrIndexCoprocesser.java: -------------------------------------------------------------------------------- 1 | package com.hbase.easy.index; 2 | 3 | import com.hbase.easy.solr.SolrIndexTools; 4 | import com.typesafe.config.Config; 5 | import com.typesafe.config.ConfigFactory; 6 | import org.apache.hadoop.hbase.Cell; 7 | import org.apache.hadoop.hbase.CellUtil; 8 | import org.apache.hadoop.hbase.client.Delete; 9 | import org.apache.hadoop.hbase.client.Durability; 10 | import org.apache.hadoop.hbase.client.Put; 11 | import org.apache.hadoop.hbase.coprocessor.BaseRegionObserver; 12 | import org.apache.hadoop.hbase.coprocessor.ObserverContext; 13 | import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment; 14 | import org.apache.hadoop.hbase.regionserver.wal.WALEdit; 15 | import org.apache.hadoop.hbase.util.Bytes; 16 | import org.apache.solr.common.SolrInputDocument; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | import java.io.IOException; 21 | import java.util.List; 22 | 23 | /** 24 | * Created by qindongliang on 2016/2/15. 25 | * 26 | * 为hbase提供二级索引的协处理器 Coprocesser 27 | * 28 | */ 29 | public class HbaseSolrIndexCoprocesser extends BaseRegionObserver { 30 | //加载配置文件属性 31 | static Config config=ConfigFactory.load("application.properties"); 32 | 33 | //log记录 34 | private static final Logger logger = LoggerFactory.getLogger(HbaseSolrIndexCoprocesser.class); 35 | 36 | 37 | @Override 38 | public void postPut(ObserverContext e, Put put, WALEdit edit, Durability durability) throws IOException { 39 | String rowkey = Bytes.toString(put.getRow());//得到rowkey 40 | SolrInputDocument doc =new SolrInputDocument();//实例化索引Doc 41 | doc.addField(config.getString("solr_hbase_rowkey_name"),rowkey);//添加主键 42 | for(String cf:config.getString("hbase_column_family").split(",")) {//遍历所有的列簇 43 | List cells = put.getFamilyCellMap().get(Bytes.toBytes(cf)); 44 | if(cells==null||cells.isEmpty()) continue; // 跳过取值为空或null的数据 45 | for (Cell kv : cells ) { 46 | String name=Bytes.toString(CellUtil.cloneQualifier(kv));//获取列名 47 | String value=Bytes.toString(kv.getValueArray());//获取列值 or CellUtil.cloneValue(kv) 48 | doc.addField(name,value);//添加到索引doc里面 49 | } 50 | } 51 | //发送数据到本地缓存 52 | SolrIndexTools.addDoc(doc); 53 | } 54 | 55 | @Override 56 | public void postDelete(ObserverContext e, Delete delete, WALEdit edit, Durability durability) throws IOException { 57 | //得到rowkey 58 | String rowkey = Bytes.toString(delete.getRow()); 59 | //发送数据本地缓存 60 | SolrIndexTools.delDoc(rowkey); 61 | } 62 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hbase-increment-index 2 | hbase+solr实现hbase的二级索引小例子 3 | 4 | ### 背景需求 5 | 现有一张Hbase的表,数据量千万级+,而且不断有新的数据插入,或者无效数据删除,每日新增大概几百万数据,现在已经有离线的hive映射hbase 6 | 提供离线查询,但是由于性能比较低,且不支持全文检索,所以想提供一种OLAP实时在线分析的查询,并且支持常规的聚合统计和全文检索,性能在秒级别可接受 7 | 8 | ### 需求分析 9 | hbase的目前的二级索引种类非常多,但大多数都不太稳定或成熟,基于Lucene的全文检索服务SolrCloud集群和ElasticSearch集群是二种比较可靠的方案,无论需求 10 | 还是性能都能满足,而且支持容错,副本,扩容等功能,但是需要二次开发和定制。 11 | 12 | ### 架构拓扑 13 | ![架构拓扑](http://dl2.iteye.com/upload/attachment/0115/1660/15ae08c4-3b32-3ce6-9fe3-f0cae1f6993f.png) 14 | ### 性能分析 15 | 当前的版本的拓扑架构并不是最优的:
16 | 17 | 从可靠性上看:
18 | 19 | 它并不是高可靠的,因为这个版本仅仅是一个初级的版本,虽然优化了批处理的方式向索引提交数据,但是它使用的 20 | jdk的容器类,所有的数据都会临时存在内存中,如果regionserver某一刻宕机,那么不能保证不会有数据丢失。 21 | 22 | 从性能上看:
23 | 24 | 它的吞吐性能比较低,因为考虑上索引批处理提交完,会清空临时缓存数据,而这一动作是需要加锁的,因为这个版本中,有两种自动提交索引的方式 25 |
第一种是达到某个阈值时提交
26 | 第二种是每间隔一定秒数提交
27 | 从而保证所有数据在常量时间内,肯定会被推送到索引中,当然前提是没有宕机或者其他的故障发生时,需要注意的是,这两个提交的Action发生后,都会清空缓存数据,以确保数据不会被重复提交,为了达到这个目的,在提交索引时,对方法进行了加锁和通过信号量控制线程协作,从而确保任何时候只有一个提交动作发生,产生了同步之后,在大批量插入数据时,性能会大幅度降低。 28 |
如何优化?
29 | 使用异步方式提交数据到一个队列中,如kakfa,然后索引数据时,从队列中读取,这样以来通过队列来中转保证数据的可靠性,索引线程不再需要加锁,对性能和吞吐也会比较大的提升。 有需要的朋友可以仿照这种思路扩展改进一下。 30 | 31 | 32 | ### 技术实现步骤 33 | (1) 搭建一套solr或者es集群,并且提前定制好schemal,本例中用的是solr单节点存储索引, 34 | 如果不知道怎么搭建solrcloud集群或者elasticsearch集群,请参考博客:
35 | [solrcloud集群搭建](http://qindongliang.iteye.com/blog/2275990)
36 | [elasticsearch集群搭建](http://qindongliang.iteye.com/blog/2250776)
37 | (2) 开发自定义的协处理器
38 | (3) 打包代码成一个main.jar
39 | (4) 安装依赖jar给各个Hbase节点,可以拷贝到hbase的lib目录,也可以在hbase.env.sh里面配置CLASSPATH
40 | ```java 41 | config-1.2.1.jar 42 | httpclient-4.3.1.jar 43 | httpcore-4.3.jar 44 | httpmime-4.3.1.jar 45 | noggit-0.6.jar 46 | solr-solrj-5.1.0.jar 47 | ``` 48 | (5) 上传main.jar至HDFS目录
49 | (6) 建表: create 'c', NAME=>'cf'
50 | (7) 禁用表 disable 'c'
51 | (8) 添加协处理器的jar:
52 | ```java 53 | alter 'c', METHOD => 'table_att', 'coprocessor'=>'hdfs:///user/hbase_solr/hbase-increment-index.jar|com.hbase.easy.index.HbaseSolrIndexCoprocesser|1001|' 54 | ``` 55 |
(9)激活表 enable 'c'
56 | (10)启动solr或者es集群, 然后在hbase shell或者 hbase java client进行put数据,然后等待查看索引里面是否正确添加数据,如果添加失败,查看hbase的regionserver的log,并根据提示解决
57 | (11)如何卸载?
58 | ``` 59 | alter 'c',METHOD => 'table_att_unset',NAME =>'coprocessor$1' 60 | ``` 61 | 卸载,完成之后,激活表 62 | 63 | ### 典型异常 64 | hbase的http-client组件与本例中用的最新的solr的http-client组件版本不一致导致,添加索引报错。
65 | 解决办法:
66 | 使用solr的
67 | httpclient-4.3.1.jar
68 | httpcore-4.3.jar
69 | 替换所有节点hbase/lib下的
70 | 低版本的httpclient组件包,即可!
71 | 72 | ### 温馨提示 73 | 本项目主要所用技术有关hbasae协处理器,和solr或者elasticsearch集群的基本知识,如有不不熟悉者, 74 | 可以先从散仙的博客入门一下: 75 | [我的Iteye博客](http://qindongliang.iteye.com/)
76 | 77 | 78 | ## 博客相关 79 | 80 | (1)[个人站点(2018之后,同步更新)](http://8090nixi.com/) 81 | 82 | (2)[iteye博客]() 83 | 84 | 85 | 86 | 87 | 88 | 89 | ## 我的公众号(woshigcs) 90 | 91 | 有问题可关注我的公众号留言咨询 92 | 93 | ![image](https://github.com/qindongliang/answer_sheet_scan/blob/master/imgs/gcs.jpg) 94 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.bizbook.product 8 | hbase-increment-index 9 | 1.0.0-SNAPSHOT 10 | 11 | 12 | 13 | 14 | 15 | 1.7.2 16 | 17 | UTF-8 18 | 19 | 0.98.12-hadoop2 20 | 21 | 22 | 9.8.0 23 | 24 | 2.7.1 25 | 26 | 1.2.1 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | org.apache.hbase 35 | hbase-client 36 | ${hbase.version} 37 | 38 | 39 | hadoop-common 40 | org.apache.hadoop 41 | 42 | 43 | hadoop-common 44 | org.apache.hadoop 45 | 46 | 47 | hadoop-hdfs 48 | org.apache.hadoop 49 | 50 | 51 | hadoop-mapreduce-client-core 52 | org.apache.hadoop 53 | 54 | 55 | slf4j-log4j12 56 | org.slf4j 57 | 58 | 59 | provided 60 | 61 | 62 | 63 | 64 | org.apache.hbase 65 | hbase-server 66 | 0.98.12-hadoop2 67 | provided 68 | 69 | 70 | slf4j-log4j12 71 | org.slf4j 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | com.typesafe 80 | config 81 | ${config.version} 82 | 83 | 84 | 85 | 86 | 87 | 88 | org.apache.solr 89 | solr-core 90 | ${lucene-solr.version} 91 | 92 | 93 | slf4j-api 94 | org.slf4j 95 | 96 | 97 | 98 | 99 | 100 | org.apache.solr 101 | solr-solrj 102 | ${lucene-solr.version} 103 | 104 | 105 | slf4j-api 106 | org.slf4j 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /src/main/java/com/hbase/easy/solr/SolrIndexTools.java: -------------------------------------------------------------------------------- 1 | package com.hbase.easy.solr; 2 | 3 | import com.typesafe.config.Config; 4 | import com.typesafe.config.ConfigFactory; 5 | import org.apache.solr.client.solrj.SolrClient; 6 | import org.apache.solr.client.solrj.impl.CloudSolrClient; 7 | import org.apache.solr.client.solrj.impl.HttpSolrClient; 8 | import org.apache.solr.common.SolrInputDocument; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.Timer; 15 | import java.util.TimerTask; 16 | import java.util.concurrent.Semaphore; 17 | 18 | /** 19 | * Created by qindongliang on 2016/2/15. 20 | * solr索引处理客户端 21 | * 注意问题,并发提交时,需要线程协作资源 22 | */ 23 | public class SolrIndexTools { 24 | //加载配置文件属性 25 | static Config config= ConfigFactory.load("application.properties"); 26 | //log记录 27 | private static final Logger logger = LoggerFactory.getLogger(SolrIndexTools.class); 28 | //实例化solr的client,如果用的是cloud模式, 29 | static SolrClient client=null; 30 | //添加批处理阈值 31 | static int add_batchCount=config.getInt("add_batchCount"); 32 | //删除的批处理阈值 33 | static int del_batchCount=config.getInt("del_batchCount"); 34 | //添加的集合缓冲 35 | static List add_docs=new ArrayList(); 36 | //删除的集合缓冲 37 | static List del_docs=new ArrayList(); 38 | 39 | static { 40 | logger.info("初始化索引调度........"); 41 | if(config.getBoolean("is_solrcloud")){ 42 | client=new CloudSolrClient(config.getString("solr_url"));//cloud模式 43 | }else{ 44 | client=new HttpSolrClient(config.getString("solr_url"));//单机模式 45 | } 46 | //启动定时任务,第一次延迟1s执行,之后每隔指定时间30S执行一次 47 | Timer timer = new Timer(); 48 | timer.schedule(new SolrCommit(), config.getInt("first_delay") * 1000, config.getInt("interval_commit_index") * 1000); 49 | } 50 | 51 | public static class SolrCommit extends TimerTask{ 52 | @Override 53 | public void run() { 54 | 55 | logger.info("索引线程运行中........"); 56 | //只有等于true时才执行下面的提交代码 57 | try { 58 | semp.acquire();//获取信号量 59 | if (add_docs.size() > 0) { 60 | client.add(add_docs);//添加 61 | } 62 | if (del_docs.size() > 0) { 63 | client.deleteById(del_docs);//删除 64 | } 65 | //确保都有数据才提交 66 | if (add_docs.size() > 0 || del_docs.size() > 0) { 67 | client.commit();//共用一个提交策略 68 | //清空缓冲区的添加和删除数据 69 | add_docs.clear(); 70 | del_docs.clear(); 71 | } else { 72 | logger.info("暂无索引数据,跳过commit,继续监听......"); 73 | } 74 | } catch (Exception e) { 75 | logger.error("间隔提交索引数据出错!", e); 76 | }finally { 77 | semp.release();//释放信号量 78 | } 79 | 80 | 81 | } 82 | } 83 | 84 | 85 | 86 | 87 | /** 88 | * 添加数据到临时存储中,如果 89 | * 大于等于batchCount时,就提交一次, 90 | * 再清空集合,其他情况下走对应的时间间隔提交 91 | * @param doc 单个document对象 92 | * */ 93 | public static void addDoc(SolrInputDocument doc){ 94 | commitIndex(add_docs,add_batchCount,doc,true); 95 | } 96 | 97 | 98 | 99 | /*** 100 | * 删除的数据添加到临时存储中,如果大于 101 | * 对应的批处理就直接提交,再清空集合, 102 | * 其他情况下走对应的时间间隔提交 103 | * @param rowkey 删除的rowkey 104 | */ 105 | public static void delDoc(String rowkey){ 106 | commitIndex(del_docs,del_batchCount,rowkey,false); 107 | } 108 | 109 | // 任何时候,保证只能有一个线程在提交索引,并清空集合 110 | final static Semaphore semp = new Semaphore(1); 111 | 112 | 113 | /*** 114 | * 此方法需要加锁,并且提交索引时,与时间间隔提交是互斥的 115 | * 百分百确保不会丢失数据 116 | * @param datas 用来提交的数据集合 117 | * @param count 对应的集合提交数量 118 | * @param doc 添加的单个doc 119 | * @param isAdd 是否为添加动作 120 | */ 121 | public synchronized static void commitIndex(List datas,int count,Object doc,boolean isAdd){ 122 | try { 123 | semp.acquire();//获取信号量 124 | if (datas.size() >= count) { 125 | 126 | if (isAdd) { 127 | client.add(datas);//添加数据到服务端中 128 | } else { 129 | client.deleteById(datas);//删除数据 130 | } 131 | client.commit();//提交数据 132 | 133 | datas.clear();//清空临时集合 134 | 135 | 136 | } 137 | }catch (Exception e){ 138 | logger.error("按阈值"+(isAdd==true?"添加":"删除")+"操作索引数据出错!",e); 139 | }finally { 140 | datas.add(doc);//添加单条数据 141 | semp.release();//释放信号量 142 | } 143 | 144 | } 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | } 153 | --------------------------------------------------------------------------------