HBASE简介(一)
关系型数据库的不足
不擅长的处理:
1. 大量数据的写入处理
2. 为有数据更新的表做索引或表结构(schema)变更
3. 字段不固定时应用
4. 对简单查询需要快速返回结果的处理
NoSQL数据库
NoSQL数据库通常不支持Join处理,各个数据都是独立设计的,很容易把数据分散在多个服务器上,故减少了每个服务器上的数据量,即使要处理大量数据的写入,也变得更加容易,数据的读入操作当然也同样容易。
典型的NoSQL数据库
临时性键值存储(memcached、Redis)、永久性键值存储(ROMA、Redis)、面向文档的数据库(MongoDB、CouchDB)、面向列的数据库(Cassandra、HBase)
HBASE定义
Apache HBase是Hadoop数据库,是一个分布式,可扩展的大数据存储。
当您需要随机,实时读取/写入您的大数据时可以使用Apache HBase。 该项目的目标是在集群上托管非常大的表 - 数十亿行×数百万列。 Apache HBase是一个开源的,分布式的,版本化的非关系数据库,其模型是由Chang等人在Google的Bigtable之后建模的。Bigtable是一种用于结构化数据的分布式存储系统。 就像Bigtable利用Google文件系统提供的分布式数据存储一样,Apache HBase在Hadoop和HDFS之上提供了类似的功能。
特点
HBASE有以下特点:
- 线性和模块化的可扩展性
- 严格一致性的读取和写入
- 自动和可配置的表分片
- 区域服务器之间的自动容灾
- 为使用Apache HBase表支持Hadoop MapReduce作业提供方便的基础类
- 易于使用Java API进行客户端访问
- 提供块缓存和布隆过滤器进行实时查询
- 通过服务器端过滤器实现查询预测
- Thrift网关和支持XML的REST-ful Web service,Protobuf和二进制数据编码
- 支持基于jruby(JIRB)的脚本
- 监控支持Ganglia可视化和导出到文件
HBASE和HDFS
HDFS | HBASE |
---|---|
分布式的文件存储系统 | j建立在HDFS之上的数据库 |
HDFS不支持快速单个的记录查询 | 支持快速单个的记录查询 |
支持高延迟的批处理 | 单行访问低延迟 |
顺序访问 | 内部采用hash-tables和随机访问,存储的数据索引在HDFS上 |
HBASE数据结构
HBase 表结构
HBase以表的形式存储数据。表有行和列组成。列划分为若干个列族/列簇(column family)。如下图所示:
具体名字包括:
Row Key(行键)
与nosql数据库们类似,row key是用来检索记录的主键。访问hbase table中的行,包括三种方式:
- 通过单个row key访问
- 通过row key的range
- 全表扫描
Rowkey行键 (Row key)可以是任意字符串(最大长度是 64KB,实际应用中长度一般为 10-100bytes),在hbase内部,row key保存为字节数组。
存储时,数据按照Row key的字典序(byte order)排序存储。设计key时,要充分排序存储这个特性,将经常一起读取的行存储放到一起。
column family(列族)
hbase表中的每个列,都归属与某个列族。列族是表的chema的一部分(而列不是),必须在使用表之前定义。列名都以列族作为前缀。
访问控制、磁盘和内存的使用统计都是在列族层面进行的。
实际应用中,列族上的控制权限能帮助我们管理不同类型的应用:
我们允许一些应用可以添加新的基本数据、一些应用可以读取基本数据并创建继承的列族、一些应用则只允许浏览数据(甚至可能因为隐私的原因不能浏览所有数据)。
Cell
HBase中通过row和columns确定的为一个存贮单元称为cell。由{row key, column( =<family> + <label>), version} 唯一确定的单元。cell中的数据是没有类型的,全部是字节码形式存贮。
timestamp
每个cell都保存着同一份数据的多个版本。版本通过时间戳来索引。
为了避免数据存在过多版本造成的的管理 (包括存贮和索引)负担,hbase提供了两种数据版本回收方式。一是保存数据的最后n个版本,二是保存最近一段时间内的版本(比如最近七天)。用户可以针对每个列族进行设置。
HBASE存储结构
HBase的部署架构通常如下图所示:
HBase存储结构如下图所示:
-
Client:
- Client 包含访问HBase的接口,维护cache加快对HBase的访问
-
HMaster:
- 为 HRegion 分配HRegion Server;
- 负责 HRegion Server的负载均衡;
- 发现失效的HRegion Server,并重新分配其上的Region;
- 处理Schema的更新处理请求
- 管理功能:提供创建、删除、更新表的接口
-
Zookeeper:
- Zookeeper保证集群中只有一个HMaser;
- 监控HRegion Server的状态,及时将上线下线的HRegion Server信息通知HMaster;
- 记录所有HRegion的寻址入口
- 存储Schema信息,包括有哪些表,表有哪些column family
-
Region Server
- 处理region的IO请求(所以Client访问HBase的数据不需要HMaster的参与。从Zookeeper中寻址,然后到HRegion Server上进行读写操作)
- 对到达阈值的HRegion进行分裂
- WAL
-
即Write Ahead Log, 是HDFS上一个文件,早期版本中称为HLog,用以存储尚未进行持久化的数据。
所有写操作都会先保证将数据写入这个Log文件后,才会真正更新MemStore,最后写入HFile中。采用这种模式,可以保证HRegionServer宕机后,我们依然可以从该Log文件中读取数据,Replay所有的操作,而不至于数据丢失。
-
-
BlockCache
- BlockCache是一个读缓存。在内存中存放经常读的数据,提升读性能。当缓存满的时候,最近最少使用的数据(Least Recently Used data )被踢出去。
-
MemStore
- 是一个写缓存。存储尚未写到磁盘中的数据。在写到磁盘之前,数据是经过排序的。在Region中每个column family对应一个HStore,每个HStore有一个MemStore和0到n个HFile。相同列族的数据存放在一个文件中。
- 是一个写缓存。存储尚未写到磁盘中的数据。在写到磁盘之前,数据是经过排序的。在Region中每个column family对应一个HStore,每个HStore有一个MemStore和0到n个HFile。相同列族的数据存放在一个文件中。
-
Hfiles
-
在磁盘上,用于存储排序后的数据行(KeyValues.)
读写过程
读取过程:
- 集群启动时,HMaster负责将所有的Region分配到每个HRegionServer中。
- zookeeper中存储了meta表的region信息,所以先从zookeeper中找到meta表regionServer的位置
- 读取meta表中的数据,namespace、表名和rowkey,找到对应region server。
- 联系对应的region server,查找对应的region。
- 先从MemStore找数据,如果没有,再到StoreFile上读
- 如果再次读取,客户端将使用缓存来获取META 的位置及之前的行健。这样时间久了,客户端不需要查询META表,
除非Region 移动所导致的丢失,这样的话,则将会重新查询更新缓存。
写入过程:
- 集群启动时,HMaster负责将所有的Region分配到每个HRegionServer中。
- zookeeper中存储了meta表的region信息,所以先从zookeeper中找到meta表regionServer的位置
- 读取meta表中的数据,namespace、表名和rowkey,找到对应region server。
- 联系对应的region server,查找对应的region,把数据先后写入到WAL和MemStore中。MemStore达到一个阈值后则把数据刷成一个StoreFile文件。若MemStore中的数据有丢失,则可以总HLog上恢复
-
当多个StoreFile文件达到一定的大小后,会触发Compact合并操作,合并为一个StoreFile,这里同时进行版本的合并和数据删除。
当Compact后,逐步形成越来越大的StoreFIle后,会触发Split操作,把当前的StoreFile分成两个,这里相当于把一个大的region分割成两个region。
cache机制
HBase上RegionServer的cache主要分为两个部分,分别是memstore&blockcache,其中memstore主要用于写缓存,而blockcache用于读缓存。
当数据写入hbase时,会先写入memstore,RegionServer会给每个region提供一个memstore,memstore中的数据达到系统设置的水位值后,会触发flush将memstore中的数据刷写到磁盘。
客户的读请求会先到memstore中查数据,若查不到就到blockcache中查,再查不到就会从磁盘上读,并把读入的数据同时放入blockcahce。
一个RegionServer上有一个BlockCache和N个Memstore。Cache在HBase中所处的位置如下图中所示:
集群的高可用
Hbase集群的高可用通过zookeeper维护:
Zookeeper一般在分布式系统中的成员之间协调共享的状态信息,Region Server和活跃的HMaster通过会话连接到Zookeeper,ZooKeeper维护短暂的阶段,通过心跳机制用于活跃的会话。
每个Region Server创建一个短暂的节点,HMaster监控这些节点发现可用的Region Server,同时HMaster 也监控这些节点的服务器故障。HMaster 通过创建一个临时的节点,Zookeeper决定其中一个HMaster作为活跃的。活跃的HMaster 给ZooKeeper发送心跳信息,不活跃的HMaster在活跃的HMaster出现故障时,接受通知。
如果一个Region Server或是一个活跃的HMaster在发送心跳信息时失败或是出现了故障,则会话过期,相应的临时节点将被删除,监听器将因这些删除的节点更新通知信息,活跃的HMaster将监听Region Server,并且将会恢复出现故障的Region Server,不活跃的HMaster 监听活跃的HMaster故障,如果一个活跃的HMaster出现故障,则不活跃的HMaster将会变得活跃。
存储目录结构:
- 表数据的存储目录结构构成:hdfs://{域名}/hbase/data/${名字空间}/${表名}/${区域名称}/${列族名称}/${文件名}
- WAL目录结构构成:hdfs://{域名}/hbase/WALs/${区域服务器名称,主机名,端口号,时间戳}/
HBASE客户端操作
HBase shell操作
hbase shell操作如下
- hbase shell: 进入shell
- help:帮助,常见命令和组:
-
- Group name: general(常规组)
Commands: status, table_help, version, whoami - Group name: ddl(数据定义语言组)
Commands: alter, alter_async, alter_status, create, describe, disable, disable_all, drop, drop_all, enable, enable_all, exists, get_table, is_disabled, is_enabled, list, locate_region, show_filters - Group name: namespace(命名空间组)
Commands: alter_namespace, create_namespace, describe_namespace, drop_namespace, list_namespace, list_namespace_tables - Group name: dml(数据操作语言组)
Commands: append, count, delete, deleteall, get, get_counter, get_splits, incr, put, scan, truncate, truncate_preserve - Group name: tools
Commands: assign, balance_switch, balancer, balancer_enabled, catalogjanitor_enabled, catalogjanitor_run, catalogjanitor_switch, close_region, compact, compact_rs, flush, major_compact, merge_region, move, normalize, normalizer_enabled, normalizer_switch, split, trace, unassign, wal_roll, zk_dump - Group name: replication
Commands: add_peer, append_peer_tableCFs, disable_peer, disable_table_replication, enable_peer, enable_table_replication, list_peers, list_replicated_tables, remove_peer, remove_peer_tableCFs, set_peer_tableCFs, show_peer_tableCFs - Group name: snapshots
Commands: clone_snapshot, delete_all_snapshot, delete_snapshot, list_snapshots, restore_snapshot, snapshot - Group name: configuration
Commands: update_all_config, update_config - Group name: quotas
Commands: list_quotas, set_quota - Group name: security
Commands: grant, list_security_capabilities, revoke, user_permission - Group name: procedures
Commands: abort_procedure, list_procedures - Group name: visibility labels
Commands: add_labels, clear_auths, get_auths, list_labels, set_auths, set_visibility
- Group name: general(常规组)
基本api操作
创建命名空间和表
用户需要首先创建命名空间和表,shell命令行:
hbase(main):024:0> create_namespace 'ns1' hbase(main):025:0> create 'ns1:t1','f1' |
java api代码示例:
@Test public void createNameSpace() throws Exception { Configuration conf = HBaseConfiguration.create(); Connection conn = ConnectionFactory.createConnection(conf); Admin admin = conn.getAdmin(); //创建名字空间描述符 NamespaceDescriptor nsd = NamespaceDescriptor.create("ns2").build(); admin.createNamespace(nsd); NamespaceDescriptor[] ns = admin.listNamespaceDescriptors(); for (NamespaceDescriptor n : ns) { System.out.println(n.getName()); } } @Test public void listNameSpaces() throws Exception { Configuration conf = HBaseConfiguration.create(); Connection conn = ConnectionFactory.createConnection(conf); Admin admin = conn.getAdmin(); NamespaceDescriptor[] ns = admin.listNamespaceDescriptors(); for (NamespaceDescriptor n : ns) { System.out.println(n.getName()); } } @Test public void createTable() throws Exception { Configuration conf = HBaseConfiguration.create(); Connection conn = ConnectionFactory.createConnection(conf); Admin admin = conn.getAdmin(); //创建表名对象 TableName tableName = TableName.valueOf("ns2:t2"); //创建表描述符对象 HTableDescriptor tbl = new HTableDescriptor(tableName); //创建列族描述符 HColumnDescriptor col = new HColumnDescriptor("f1"); tbl.addFamily(col); admin.createTable(tbl); System.out.println("over"); } |
put操作
将数据插入表中,shell命令行:
hbase(main):016:0> put 'ns1:t1','row1','f1:id',100 |
java api代码示例:
@Test public void put() throws Exception { //创建conf对象 Configuration conf = HBaseConfiguration.create(); //通过连接工厂创建连接对象 Connection conn = ConnectionFactory.createConnection(conf); //通过连接查询tableName对象 TableName tname = TableName.valueOf("ns1:t1"); //获得table Table table = conn.getTable(tname); //通过bytes工具类创建字节数组(将字符串) byte[] rowid = Bytes.toBytes("row1"); //创建put对象 Put put = new Put(rowid); byte[] f1 = Bytes.toBytes("f1"); byte[] id = Bytes.toBytes("name"); byte[] value = Bytes.toBytes("sam"); put.addColumn(f1, id, value); //执行插入 table.put(put); } |
get操作
从表中获取数据,shell命令行:
hbase(main):017:0> get 'ns1:t1','row1' |
java api代码示例:
@Test public void get() throws Exception { //创建conf对象 Configuration conf = HBaseConfiguration.create(); //通过连接工厂创建连接对象 Connection conn = ConnectionFactory.createConnection(conf); //通过连接查询tableName对象 TableName tname = TableName.valueOf("ns1:t1"); //获得table Table table = conn.getTable(tname); //通过bytes工具类创建字节数组(将字符串) byte[] rowid = Bytes.toBytes("row1"); Get get = new Get(Bytes.toBytes("row1")); Result r = table.get(get); byte[] idvalue = r.getValue(Bytes.toBytes("f1"), Bytes.toBytes("id")); System.out.println(Bytes.toInt(idvalue)); } |
delete操作
删除数据,对应时间戳之前的标记为删除,shell命令行:
$hbase>delete 'nd1:t3','row1','f1:name',148989875645 |
java api代码示例:
@Test public void deleteData() throws IOException { Configuration conf = HBaseConfiguration.create(); Connection conn = ConnectionFactory.createConnection(conf); TableName tname = TableName.valueOf("ns1:t1"); Table table = conn.getTable(tname); Delete del = new Delete(Bytes.toBytes("row0001")); del.addColumn(Bytes.toBytes("f1"), Bytes.toBytes("id")); del.addColumn(Bytes.toBytes("f1"), Bytes.toBytes("name")); table.delete(del); System.out.println("over"); } |
TTL
设置存活时间:
$hbase>create 'ns1:tx' , {NAME=>'f1',TTL=>10,VERSIONS} |
删除表操作
删除表前需要先将表置微不可用后才能删除
hbase(main):020:0> disable 'ns1:t1' hbase(main):021:0> drop 'ns1:t1' |
java api代码示例:
@Test public void disableTable() throws Exception { Configuration conf = HBaseConfiguration.create(); Connection conn = ConnectionFactory.createConnection(conf); Admin admin = conn.getAdmin(); //禁用表 enable(...) disableTable(...) admin.deleteTable(TableName.valueOf("ns2:t2")); } |
scan操作
从表中获取数据,shell命令行:
hbase(main):018:0> scan 'ns1:t1' hbase(main):026:0> scan 'hbase:meta' |
@Test public void getScanCache() throws IOException { Configuration conf = HBaseConfiguration.create(); Connection conn = ConnectionFactory.createConnection(conf); TableName tname = TableName.valueOf("ns1:t1"); Scan scan = new Scan(); // scan.setCaching(5000); Table t = conn.getTable(tname); ResultScanner rs = t.getScanner(scan); long start = System.currentTimeMillis() ; Iterator<Result> it = rs.iterator(); while(it.hasNext()){ Result r = it.next(); System.out.println(r.getColumnLatestCell(Bytes.toBytes("f1"), Bytes.toBytes("name"))); } System.out.println(System.currentTimeMillis() - start); } @Test public void bigInsert() throws Exception { DecimalFormat format = new DecimalFormat(); format.applyPattern("0000"); long start = System.currentTimeMillis(); Configuration conf = HBaseConfiguration.create(); Connection conn = ConnectionFactory.createConnection(conf); TableName tname = TableName.valueOf("ns1:t1"); HTable table = (HTable) conn.getTable(tname); //不要自动清理缓冲区 table.setAutoFlushTo(false); for (int i = 1; i < 10000; i++) { Put put = new Put(Bytes.toBytes("row" + format.format(i))); //关闭写前日志 put.setWriteToWAL(false); put.addColumn(Bytes.toBytes("f1"), Bytes.toBytes("id"), Bytes.toBytes(i)); put.addColumn(Bytes.toBytes("f1"), Bytes.toBytes("name"), Bytes.toBytes("tom" + i)); put.addColumn(Bytes.toBytes("f1"), Bytes.toBytes("age"), Bytes.toBytes(i % 100)); table.put(put); if (i % 2000 == 0) { table.flushCommits(); } } // table.flushCommits(); System.out.println(System.currentTimeMillis() - start); } @Test public void scan() throws IOException { Configuration conf = HBaseConfiguration.create(); Connection conn = ConnectionFactory.createConnection(conf); TableName tname = TableName.valueOf("ns1:t1"); Table table = conn.getTable(tname); Scan scan = new Scan(); scan.setCaching(1000); scan.setStartRow(Bytes.toBytes("row5000")); scan.setStopRow(Bytes.toBytes("row8000")); ResultScanner rs = table.getScanner(scan); StopWatch watch = new StopWatch(); watch.start(); Iterator<Result> it = rs.iterator(); while (it.hasNext()) { Result r = it.next(); byte[] name = r.getValue(Bytes.toBytes("f1"), Bytes.toBytes("name")); // System.out.println(Bytes.toString(name)); } watch.stop(); System.out.println("处理时间(ms):" + watch.getTime()); } |
高级用法
缓存和批处理
为降低客户端与服务器端的请求次数,提高查询效率可使用缓存和批处理。开启缓存可以使一次RPC请求获取多行数据。缓存可以在表层面和当前扫描实例层面做。
如果行数据量很大,这样对内存的占用很大,可以使用批处理,批量可以控制每次next函数返回的列的个数。
java代码示例如下:
/** * 测试缓存和批处理 */ @Test public void testBatchAndCaching() throws IOException { Configuration conf = HBaseConfiguration.create(); Connection conn = ConnectionFactory.createConnection(conf); TableName tname = TableName.valueOf("ns1:t7"); Scan scan = new Scan(); scan.setCaching(2); scan.setBatch(4); Table t = conn.getTable(tname); ResultScanner rs = t.getScanner(scan); Iterator<Result> it = rs.iterator(); while (it.hasNext()) { Result r = it.next(); System.out.println("========================================"); //得到一行的所有map,key=f1,value=Map<Col,Map<Timestamp,value>> NavigableMap<byte[], NavigableMap<byte[], NavigableMap<Long, byte[]>>> map = r.getMap(); // for (Map.Entry<byte[], NavigableMap<byte[], NavigableMap<Long, byte[]>>> entry : map.entrySet()) { //得到列族 String f = Bytes.toString(entry.getKey()); Map<byte[], NavigableMap<Long, byte[]>> colDataMap = entry.getValue(); for (Map.Entry<byte[], NavigableMap<Long, byte[]>> ets : colDataMap.entrySet()) { String c = Bytes.toString(ets.getKey()); Map<Long, byte[]> tsValueMap = ets.getValue(); for (Map.Entry<Long, byte[]> e : tsValueMap.entrySet()) { Long ts = e.getKey(); String value = Bytes.toString(e.getValue()); System.out.print(f + "/" + c + "/" + ts + "=" + value + ","); } } } System.out.println(); } } |
过滤器
还可以通过在客户端增加过滤器实现提高查询表数据的效果,hbase提供了多种类型的过滤器。
常用的过滤器如下:
- RowFilter
- FamilyFilter
- QualifierFilter
- ValueFilter
- SingleColumValueFilter
- ...
如下
select * from t7 where ((age <= 13) and (name like '%t')
or (age > 13) and (name like 't%'))
java 实现如下:
@Test public void testComboFilter() throws IOException { Configuration conf = HBaseConfiguration.create(); Connection conn = ConnectionFactory.createConnection(conf); TableName tname = TableName.valueOf("ns1:t5"); Scan scan = new Scan(); //where ... f2:age <= 13 SingleColumnValueFilter ftl = new SingleColumnValueFilter( Bytes.toBytes("f2"), Bytes.toBytes("age"), CompareFilter.CompareOp.LESS_OR_EQUAL, new BinaryComparator(Bytes.toBytes("13")) ); //where ... f2:name like %t SingleColumnValueFilter ftr = new SingleColumnValueFilter( Bytes.toBytes("f2"), Bytes.toBytes("name"), CompareFilter.CompareOp.EQUAL, new RegexStringComparator("^t") ); //ft FilterList ft = new FilterList(FilterList.Operator.MUST_PASS_ALL); ft.addFilter(ftl); ft.addFilter(ftr); //where ... f2:age > 13 SingleColumnValueFilter fbl = new SingleColumnValueFilter( Bytes.toBytes("f2"), Bytes.toBytes("age"), CompareFilter.CompareOp.GREATER, new BinaryComparator(Bytes.toBytes("13")) ); //where ... f2:name like %t SingleColumnValueFilter fbr = new SingleColumnValueFilter( Bytes.toBytes("f2"), Bytes.toBytes("name"), CompareFilter.CompareOp.EQUAL, new RegexStringComparator("t$") ); //ft FilterList fb = new FilterList(FilterList.Operator.MUST_PASS_ALL); fb.addFilter(fbl); fb.addFilter(fbr); FilterList fall = new FilterList(FilterList.Operator.MUST_PASS_ONE); fall.addFilter(ft); fall.addFilter(fb); scan.setFilter(fall); Table t = conn.getTable(tname); ResultScanner rs = t.getScanner(scan); Iterator<Result> it = rs.iterator(); while (it.hasNext()) { Result r = it.next(); byte[] f1id = r.getValue(Bytes.toBytes("f1"), Bytes.toBytes("id")); byte[] f2id = r.getValue(Bytes.toBytes("f2"), Bytes.toBytes("id")); byte[] f1name = r.getValue(Bytes.toBytes("f1"), Bytes.toBytes("name")); byte[] f2name = r.getValue(Bytes.toBytes("f2"), Bytes.toBytes("name")); System.out.println(f1id + " : " + f2id + " : " + Bytes.toString(f1name) + " : " + Bytes.toString(f2name)); } } |
计数器
基于一种类似CAS的原理的原子操作
@Test public void testIncr() throws IOException { Configuration conf = HBaseConfiguration.create(); Connection conn = ConnectionFactory.createConnection(conf); TableName tname = TableName.valueOf("ns1:t8"); Table t = conn.getTable(tname); Increment incr = new Increment(Bytes.toBytes("row1")); incr.addColumn(Bytes.toBytes("f1"),Bytes.toBytes("daily"),1); incr.addColumn(Bytes.toBytes("f1"),Bytes.toBytes("weekly"),10); incr.addColumn(Bytes.toBytes("f1"),Bytes.toBytes("monthly"),100); t.increment(incr); } |
协处理器
在服务器端执行的,类似于关系型数据库的存储过程或者触发器。
- observer:基于事件的,发生动作时,回调相应方法。
- RegionObserver: 与表的region相关联,用户可以用这种协处理器处理数据修改事件。
- MasterObserver: 可以被用做管理或DDL操作,Master节点的相关事件。
- WALObserver: 处理WAL事件。
- endpoint: endpoint通过添加一些远程调用来动态拓展RPC协议。
事务性操作
事务的并发控制早期有两种策略:
- rowlock:
通过rowlock进行锁定,但最新版本将rowlock删除了,主要是影响性能,同时安全性也存在死锁的可能。 - checkAndPut:
使用HTable.checkAndPut()等原子性操作。
rowKey设计
- 热点选取:
HBase中的行是按照rowkey的字典顺序排序的,这种设计优化了scan操作,可以将相关的行以及会被一起读取的行存取在临近位置,便于scan。
然而糟糕的rowkey设计是热点的源头。 热点发生在大量的client直接访问集群的一个或极少数个节点(访问可能是读,写或者其他操作)。设计良好的数据访问模式以使集群被充分,均衡的利用。
为了避免写热点,设计rowkey使得不同行在同一个region,但是在更多数据情况下,数据应该被写入集群的多个region,而不是一个。
- 长度原则:
rowkey是一个二进制码流,可以是任意字符串,最大长度 64kb ,实际应用中一般为10-100bytes,以byte[] 形式保存,一般设计成定长。
建议越短越好,不要超过16个字节。
- 散列原则:
如果rowkey按照时间戳的方式递增,不要将时间放在二进制码的前面,
建议将rowkey的高位采用散列字段处理,由程序随机生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer,以实现负载均衡的几率。以防造成热点问题,会降低查询效率。
- 唯一原则:
必须在设计上保证其唯一性,rowkey是按照字典顺序排序存储的。
因此,设计rowkey的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。
- 盐析(salt):
盐析是在rowkey的前面增加随机数,具体就是给rowkey分配一个随机前缀以使得它和之前的rowkey的开头不同。
分配的前缀种类数量应该和你想使用数据分散到不同的region的数量一致。加盐之后的rowkey就会根据随机生成的前缀分散到各个region上,以避免热点。
- 哈希:
哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的rowkey,可以使用get操作准确获取某一个行数据
- 反转:
第三种防止热点的方法反转固定长度或者数字格式的rowkey。这样可以使得rowkey中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机rowkey,但是牺牲了rowkey的有序性。
一个简单话单的业务场景:
1.主叫、被叫、通话时长、通话时间、通话状态
rowkey:
n + callerNumber + callType + timestamp +calledNumber
n = hashcode(callerNumber + date(timestamp)) % 100
xx18612345678+0+timestamp(20180727)+0000...
xx18612345678+0+timestamp(20180728)+0000...
java 代码示例如下:
@Test public void put() throws Exception{ Configuration conf = HBaseConfiguration.create(); //通过连接工厂创建连接对象 Connection conn = ConnectionFactory.createConnection(conf); TableName tname = TableName.valueOf("ns1:callRecord"); Table table = conn.getTable(tname); String callerNumber = "18612345678"; String calledNumber = "18612345678"; SimpleDateFormat sdf = new SimpleDateFormat(); sdf.applyPattern("yyyyMMddHHmmss"); String callTime = sdf.format(new Date()); //0为主叫1为被叫 int callType = 0; int hash = (callerNumber + callTime).hashCode() % 100; DecimalFormat format = new DecimalFormat(); format.applyPattern("00"); String regionNo = sdf.format(hash); String rowKey = regionNo + "," + callerNumber + "," + callType + "," + callTime; byte[] rowid = Bytes.toBytes(rowKey); Put put = new Put(rowid); put.addColumn(Bytes.toBytes("f1"),Bytes.toBytes("status"),Bytes.toBytes("通话成功")); put.addColumn(Bytes.toBytes("f1"),Bytes.toBytes("duration"),Bytes.toBytes(100)); table.put(put); System.out.println("success"); } |
性能优化
- 垃圾回收
- 本地memstore分配缓冲区
- 压缩
- 优化拆分和合并
- 负载均衡
HBase总结
HBase适合场景和不足:
- HBase适合大量插入同时又有读的情况
- 具有搞扩展性,即使数据增加也不会降低相应的处理速度(特别是写入速度)
- HBase的瓶颈是硬盘传输速度,关系型数据库的瓶颈是硬盘寻道时间。
- HBase很适合寻找按照时间排序top n的场景
- 只能做简单的Key value查询,复杂的sql统计做不到。
- 只能在row key上做快速查询
HBase VS ES VS TiDB VS mysql
Mysql | HBase | ES | TiDB | |
---|---|---|---|---|
存储方式 | 行存储 | 列存储 | 索引存储 | 行存储 |
存储引擎/存储容器 | innodb/myisam | hdfs | 硬盘、hdfs、s3 | rocksDB |
开发语言 | C++ | java | java/lucene | TiDB:go TiKV:rust rocksDB:C++ |
写入能力 | 一般 | 极强 | 弱 | 一般 |
查询能力 | 强,但随着数据量的提高有性能瓶颈 | 弱 | 极强 | 强 |
存储拓展性 | 单机,拓展性较差 | 水平拓展 | 水平拓展 | 水平拓展 |
集群高可用 | 不支持集群,支持主从。 master-slave架构主要思路是: master负责业务的读写请求, 然后通过binlog复制到slave节点, 这样如果主库因为不可抗拒因素无法恢复时, 从库可以提供服务。 | Zookeeper一般在分布式系统中的成员之间协调共享的状态信息, Region Server和活跃的HMaster通过会话连接到Zookeeper, ZooKeeper维护短暂的阶段,通过心跳机制用于活跃的会话。 HMaster监控这些节点发现可用的Region Server, 同时HMaster 也监控这些节点的服务器故障。 HMaster 通过创建一个临时的节点,Zookeeper决定其中一个HMaster作为活跃的。 活跃的HMaster 给ZooKeeper发送心跳信息, 不活跃的HMaster在活跃的HMaster出现故障时,接受通知。 心跳信息时失败或是出现了故障,则会话过期, 相应的临时节点将被删除,监听器将因这些删除的节点更新通知信息, 活跃的HMaster将监听Region Server, 并且将会恢复出现故障的Region Server, 不活跃的HMaster 监听活跃的HMaster故障,如果一个活跃的HMaster出现故障,则不活跃的HMaster将会变得活跃 | 一个运行中的 Elasticsearch 实例称为一个 节点, 而集群是由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。 当有节点加入集群中或者从集群中移除节点时, 集群将会重新平均分布所有的数据。 当一个节点被选举成为主节点时, 它将负责管理集群范围内的所有变更, 例如增加、删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作, 所以当集群只拥有一个主节点的情况下, 即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。 | TiDB Server 节点失效,只会影响该节点上的 session, 应用连接失败重试后可通过前端负载均衡中间件 将请求发送到其他正常的 TiDB Server 节点。 Raft 协议保证自身数据一致性)则无影响, 否则会重新选取 leader,期间该无法对外提供服务(约 3 秒钟)。 若 region 非 Raft leader(TiKV Cluster 也通过 Raft 协议保证节点间的数据一致性), 则服务不受影响,否则服务会中断,待重新选举 leader 后恢复。 如果 PD 确认了失效的 TiKV 节点已经不能恢复, 则会自动将该节点的数据调度至其他正常的 TiKV 节点。 |
secordary index | 支持 | 不支持 | 支持 | 支持 |
事务 | 支持 | 不支持 | 不支持 | 支持 |
全文索引 | 支持,鸡肋 | 不支持 | 支持 | 支持,鸡肋 |