1,rowkey的长度原则
rowkey是一个二进制码流,可以是任意字符串,最大长度64kb,实际应用中一般为10-100bytes,以byte[]形式保存,一般设计成定长。
建议越短越好,不要超过16个字节,原因如下:
- 数据的持久化文件HFile中是按照KeyValue存储的,如果rowkey过长,比如超过100字节,1000w行数据,光rowkey就要占用100*1000w=10亿个字节,将近1G数据,这样会极大影响HFile的存储效率;
- MemStore将缓存部分数据到内存,如果rowkey字段过长,内存的有效利用率就会降低,系统不能缓存更多的数据,这样会降低检索效率。
2,rowkey的散列原则
如果rowkey按照时间戳的方式递增,不要将时间放在二进制码的前面,建议将rowkey的高位作为散列字段,由程序随机生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer,以实现负载均衡的几率。如果没有散列字段,首字段直接是时间信息,所有的数据都会集中在一个RegionServer上,这样在数据检索的时候负载会集中在个别的RegionServer上,造成热点问题,会降低查询效率。
3,rowkey的唯一原则
必须在设计上保证其唯一性,rowkey是按照字典顺序排序存储的,因此,设计rowkey的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。
什么是热点?
Hbase中的行是按照rowkey的字典顺序排序的,这种设计优化了scan操作,可以将相关的行以及会被一起读取的行存取在临近位置,便于scan。然而糟糕的rowkey设计是热点的源头。
热点发生在大量的client直接访问集群的一个或极少数个节点(访问可能是读,写或者其他操作)。大量访问会使热点region所在的单个机器超出自身承受能力,引起性能下降甚至region不可用,这也会影响同一个RegionServer上的其他region,因为主机无法服务其他region的请求。
数据倾斜: Hbase可以被划分为多个Region,但是默认创建时只有一个Region分布在集群的一个节点上,数据一开始时都集中在这个Region,也就是集中在这一个节点上,就算region存储达到临界值时被划分,数据也是存储在少数节点上。这就是数据倾斜。
Hbase中,表会被划分为1...n个Region,被托管在RegionServer中。Region有二个重要的属性:StartKey与EndKey表示这个Region维护的rowKey范围,当我们要读/写数据时,如果rowKey落在某个start-end key范围内,那么就会定位到目标region并且读/写到相关的数据。
默认的情况下,创建一张表是,只有1个region,start-end key没有边界,所有数据都在这个region里装,然而,当数据越来越多,region的size越来越大时,大到一定的阀值,hbase认为再往这个region里塞数据已经不合适了,就会找到一个midKey将region一分为二,成为2个region,这个过程称为分裂(region-split)。而midKey则为这二个region的临界(这个中间值这里不作讨论是如何被选取的)。
此时,我们假设假设rowkey小于midKey则为阴被塞到1区,大于等于midKey则会被塞到2区,如果 rowkey还是顺序增大的,那数据就总会往2区里面写数据,而1区现在处于一个被冷落的状态,而且是半满的。2区的数据满了会被再次分裂成2个区,如此不断产生被冷落而且不满的Region,当然,这些region有提供数据查询的功能。
可以建表时进行预分区,让表的数据可以均衡的分散在集群中,而不是默认只有一个region分布在集群的一个节点上。预分区后每个region的rowkey还是有序的。
下面是一些常见的避免热点的方法以及它们的优缺点:
1 加盐
这里所说的加盐不是密码学中的加盐,而是在rowkey的前面增加随机数,具体就是给rowkey分配一个随机前缀以使得它和之前的rowkey的开头不同。分配的前缀种类数量应该和你想使用数据分散到不同的region的数量一致。加盐之后的rowkey就会根据随机生成的前缀分散到各个region上,以避免热点。
2 哈希
哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的rowkey,可以使用get操作准确获取某一个行数据。
3 反转
第三种防止热点的方法时反转固定长度或者数字格式的rowkey。这样可以使得rowkey中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机rowkey,但是牺牲了rowkey的有序性。
反转rowkey的例子以手机号为rowkey,可以将手机号反转后的字符串作为rowkey,这样的就避免了以手机号那样比较固定开头导致热点问题
3 时间戳反转
一个常见的数据处理问题是快速获取数据的最近版本,使用反转的时间戳作为rowkey的一部分对这个问题十分有用,可以用 Long.Max_Value - timestamp 追加到key的末尾,例如 [key][reverse_timestamp] , [key] 的最新值可以通过scan [key]获得[key]的第一条记录,因为Hbase中rowkey是有序的,第一条记录是最后录入的数据。
其他一些建议:
尽量减少行键和列族的大小在Hbase中,value永远和它的key一起传输的。当具体的值在系统间传输时,它的rowkey,列名,时间戳也会一起传输。如果你的rowkey和列名很大,这个时候它们将会占用大量的存储空间。
列族尽可能越短越好,最好是一个字符。
冗长的属性名虽然可读性好,但是更短的属性名存储在Hbase中会更好。
Hbase的基本架构
主从的架构,Hbase强依赖zookeeper和hadoop
主节点:HMaster , 负责维护管理整个Hbase集。由HMaster把分裂出来的region分配给不同的服务器,实现负载均衡。
从节点:HRegionServer 存储数据,存储集群当中的数据。当region的大小达到一定阈值时(根据storeFile的大小来判定),会split操作,把region分割成两个,并由HMaster进行负载均衡。
数据是以HFlie的形式存在HDFS中
最终是以key-value的形式存储。
开始是两个固定长度的数值,分别表示Key的长度和Value的长度。紧接着是Key,开始是固定长度的数值,表示RowKey的长度,紧接着是 RowKey,然后是固定长度的数值,表示Family的长度,然后是Family,接着是Qualifier,然后是两个固定长度的数值,表示Time Stamp和Key Type(Put/Delete)。Value部分没有这么复杂的结构,就是纯粹的二进制数据了(没有类型,全部都是字节码形式存储)。
Hbase的插入和删除等操作都是新插入一条数据,Key Type标识操作类型,TimeStamp更新为最新的,这样下次读取时就能读到正确的值。在compact时会合并这些修改。
JavaAPI:
Cell中包含rowKey, 列族,列名,列值等所有信息
KeyValue是Cell的实现类,所有数据都装在一个字节数组里面
protected byte [] bytes = null; // an immutable byte array that contains the KV protected int offset = 0; // offset into bytes buffer KV starts at protected int length = 0; // length of the KV starting from offset.
通过bytes与offset的组合可以取到rowKey, 列族,列名,列值等数据。
例如,取keyLength,长度固定为4B。则从byte[]数组bytes中位置offset开始,获取一个int,也就是4B。源码片段如下:
Bytes.toInt(this.bytes, this.offset);
取ValueLength也是一样,只不过offset要加4,即 Bytes.toInt(this.bytes, this.offset + 4);
RowLength的长度只有2B,应从bytes下标为8的位置取2个byte,返回一个长度为2的字节数组({bytes[8],bytes[9]})。
源码片段:
Bytes.toShort(this.bytes, getKeyOffset());
public int getKeyOffset() {
return this.offset + ROW_OFFSET;
}
// How far into the key the row starts at. First thing to read is the short
// that says how long the row is.
public static final int ROW_OFFSET =
Bytes.SIZEOF_INT +
Bytes.SIZEOF_INT ;
// Bytes.SIZEOF_INT 即 java中int类型的字节数(4)
当RowLength的值获得后,就可以得到rowKey的长度,而起始下标也可得出为4+4+2=10,从bytes中下标为10的位置开始,连续取RowLength - 1个元素所组成的字节数组即为RowKey的字节数组。
依此类推,列族,列名,列值等数据都可取得。注意,因为keyLength的值已得出,列的长度可以倒推出,所以Hbase的Cell就没有额外保存 ‘列长度’ 这个信息。
常用API:
@Test
public void searchData() throws IOException {
Get get = new Get(Bytes.toBytes("0003"));
Result result = myuser.get(get);
//默认获取的就是最近一个版本的cell
Cell[] cells = result.rawCells();
printValue(cells);
}
private void printValue(Cell[] cells) throws IOException {
//获取所有列的值
for (Cell cell:cells){
//获取单元格当中的值
byte[] value = CellUtil.clonevalue(cell);
//获取数据值是在哪一个列里面
byte[] bytes = CellUtil.cloneQualifier(cell);
//列名
String cloumnName = new String(bytes);
//列族名
byte[] family = CellUtil.cloneFamily(cell);
//rowKey
byte[] rowKey = CellUtil.cloneRow(cell);
System.out.println(new String(rowKey) + ":"+new String(family) + ":"+cloumnName);
if ("age".equals(cloumnName) || "id".equals(cloumnName)){
System.out.println(Bytes.toInt(value));
}else{
//注意如果使用new String的方式打印数据,只能打印字符串的数据
System.out.println(new String(value));
}
}
myuser.close();
}
@Test
public void scanRowKey() throws IOException {
Scan scan = new Scan();
scan.setStartRow("0004".getBytes());
scan.setStopRow("0006".getBytes());
ResultScanner resultScanner = myuser.getScanner(scan);
for (Result result:resultScanner){
//获取rowKey
System.out.println(Bytes.toString(result.getRow()));
//遍历获取所有得到的列族以及所有的列名
Cell[] cells = result.rawCells();
printValue(cells);
}
}



