HBase
最佳实践
避免数据热点
加盐的过程本质上是对原有的主键加上一个字节的前缀,
如下面公式所示:
new_row_key = (++index % BUCKETS_NUMBER) + original_key
BUCKETS_NUMBER 为桶的个数。主键的分布决定了数据的分布,把主键
打散也就意味着数据打散。具体使用时,一般建议桶的数目等于 RegionServer 的
数目。
多租户
我们开发了 DHS 系统(Didi HBase Service)进行项目管理,并且在 HBase 上通过 Namespace、RS Group 等技术来分割用户的资源、数据和权限。通过计算开销并计费的方法来管控资源分配
HBase 自带的 jxm 信息会汇总到 Region 和 RegionServer 级别的数据,管理员会经常用到,但是用户却很少关注这个级别。根据这种情况我们开发了 HBase 表级别的监控,并且会有权限控制,让业务 RD 只能看到和自己相关的表,清楚自己项目表的吞吐及存储占用情况
RegionServer Group,实现细节可以参照 HBase HBASE-6721 这个 Patch。滴滴在这个基础上作了一些分配策略上的优化,以便适合滴滴业务场景的修改。RS Group简单概括是指通过分配一批指定的RegionServer列表,成为一个RS Group,每个 Group 可以按需挂载不同的表,并且当 Group 内的表发生异常后,Region不会迁移到其他的 Group。这样,每个 Group 就相当于一个逻辑上的子集群,通过这种方式达到资源隔离的效果,降低管理成本,不必为每个高 SLA 的业务线单独搭集群。
HDFS中文件的存储
Browsing HDFS for HBase Objects
1 | /hbase/data/default/<table_name>/ab0ba9f7e0d2898a0f61aa687d3d4886/<column_family>/<HFile_name> |
如
1 | /hbase/data/default/my_table/d177ff7276d2e46d69dc50f67f92643a/cf/ |
HFile每bulkload一次,新增加的HFile序列号增加1
1 | ./hadoop fs -ls /hbase/data/default/recommend_result/d177ff7276d2e46d69dc50f67f92643a/cf |
如上面的seqId_1,代表序列号为1,在major compact时,将合并成一个文件
如果存在多个HFile,就需要同时查询多个列族。
- 一个region只保存在一个region server中,不会跨越region server,由hdfs保证高可用
client写入
通过client写入数据时,首先会把数据写入到memstore和WAL,数据到达一定的阈值,才会溢写到StoreFile,StoreFile也就是HFile
zk存储
hbase meta
1 | hbase(main):053:0> list_namespace_tables 'hbase' |
GET的过程
Coprocessor
Phoenix
在没有 Phoenix 之前,用户如果需要分析 HBase 数据,只能从 HBase 拖出去或者绕过 HBase 直接读取 HDFS 上面的HFile 的方式进行,前者浪费了大量的网络IO,后者用不到 HBase 本身做的大量缓存和索引优化。Phoenix 使用 HBase 的协处理器机制,直接在 RegionServer 上执行算子逻辑,然后将算子的结果返回即可,也就是大数据中的“Move Operator to Data”理念。比如,用户执行“ select count(*) from mytable where mytime > timestamp’2018-11-11 0:00:00’ ”,在实际执行中,会先找到符合过滤条件的region,然后在RegionServer本地计算count,最后再对各个 Region 上的结果进行汇总。
索引是传统数据库中常见的技术,HBase 表中可以指定主键,对主键使用 LSM 算
法构建索引。Phoenix 中,用户在创建表的时候,可以指定索引列,可以是单列,
也可以是组合列。很多时候仅有主键索引是不够的,特别是对于列特别多的宽表,
为此,Phoenix 提供二级索引功能,用户能够对表中非主键的列添加索引,并可
以加入其它相关列到索引表中,如果某个查询涉及到的列全部在索引表中,直接
查询索引表即可,无需访问原数据表。索引表跟原表做到实时同步更新,以保证
数据一致性。
@Controller
@SessionAttributes({ “credentials”, “user” })
shell
HFile文件的元数据
1 | hdfs dfs -du -h /hbase/data/default/dal_hx_lct_700_800/999fdd6a2ed426fcb33d202195fdd7d2/i/ |
WAL
写入模型优化
WAL写入模型
了解了HLog的结构之后,我们就开始研究HLog的写入模型。HLog的写入可以分为三个阶段,首先将数据对<HLogKey,WALEdit>写入本地缓存,然后再将本地缓存写入文件系统,最后执行sync操作同步到磁盘。在以前老的写入模型中,上述三步都由工作线程独自完成,如下图所示:

上图中,本地缓存写入文件系统那个步骤工作线程需要持有updateLock执行,不同工作线程之间必然会恶性竞争;不仅如此,在Sync HDFS这步中,工作线程之间需要抢占flushLock,因为Sync操作是一个耗时操作,抢占这个锁会导致写入性能大幅降低。
所幸的是,来自中国(准确的来说,是来自小米,鼓掌)的3位工程师意识到了这个问题,进而提出了一种新的写入模型并被官方采纳。根据官方测试,新写入模型的吞吐量比之前提升3倍多,单台RS写入吞吐量介于12150~31520,5台RS组成的集群写入吞吐量介于22000~70000(见HBASE-8755)。下图是小米官方给出来的对比测试结果:

在新写入模型中,本地缓存写入文件系统以及Sync HDFS都交给了新的独立线程完成,并引入一个Notify线程通知工作线程是否已经Sync成功,采用这种机制消除上述锁竞争,具体如下图所示:

1. 上文中提到工作线程在写入WALEdit之后并没有进行Sync,而是等到释放行锁阻塞在syncedTillHere变量上,等待AsyncNotifier线程唤醒。
2. 工作线程将WALEdit写入本地Buffer之后,会生成一个自增变量txid,携带此txid唤醒AsyncWriter线程
3. AsyncWriter线程会取出本地Buffer中的所有WALEdit,写入HDFS。注意该线程会比较传入的txid和已经写入的最大txid(writtenTxid),如果传入的txid小于writteTxid,表示该txid对应的WALEdit已经写入,直接跳过
4. AsyncWriter线程将所有WALEdit写入HDFS之后携带maxTxid唤醒AsyncFlusher线程
5. AsyncFlusher线程将所有写入文件系统的WALEdit统一Sync刷新到磁盘
6. 数据全部落盘之后调用setFlushedTxid方法唤醒AyncNotifier线程
7. AyncNotifier线程会唤醒所有阻塞在变量syncedTillHere的工作线程,工作线程被唤醒之后表示WAL写入完成,后面再执行MVCC结束写事务,推进全局读取点,本次更新才会对用户可见
通过上述过程的梳理可以知道,新写入模型采取了多线程模式独立完成写文件系统、sync磁盘操作,避免了之前多工作线程恶性抢占锁的问题。同时,工作线程在将WALEdit写入本地Buffer之后并没有马上阻塞,而是释放行锁之后阻塞等待WALEdit落盘,这样可以尽可能地避免行锁竞争,提高写入性能。
wal写入数据写入经历三步:
- 写入本地缓存
- 写入hdfs
- flush hdfs
旧的写入模型,工作线程自己写入hdfs和flush,为了避免一致性问题,需要分别竞争updateLock和flushLock, 工作线程在单个线程中完成,写了hdfs,立即直接flush
新的写入模型,将写hdfs和flush hdfs分别由两个单独的线程(AsyncWriter和AsyncFlusher)处理,落盘后回调工作线程;
为了避免多个线程同时写入冲突,给每个要写入的文件生成一个自增的txid,写hdfs的线程会把所有的文件全部写入到hdfs,
如果工作线程要写的txid小于max txid,代表已经写过了。
memstore
MSLAB
LAB 对 GC 的优化
ConcurrentSkipListMap
CompactingMemStore
HBase clonesnapshot 限流实现原理
scanner
数据存储压缩
org.apache.hadoop.hbase.util.Bytes
#data-compress
#byte
构建一个打印字节数组的工具方法:
1 |
|