深入浅出 HBase 实战|青训营笔记

130 阅读24分钟

这是我参与「第四届青训营 」笔记创作活动的第 8 天!

适用场景

  • “近在线”的海量分布式KV/宽表存储,数据量级达到百TB级以上

  • 写密集型应用,高吞吐,可接受一定的时延抖动

  • 需要按行顺序扫描的能力

  • 接入Hadoop大数据生态

  • 结构化、半结构化数据,可以经常新增/更新列属性

  • 平滑的水平扩展

业务落地场景包括:

-   电商订单数据 抖音电商每日交易订单数据基于 HBase 存储,支持海量数据存储的同时满足稳定低延时的查询需求,并且只需相对很低的存储成本。通过多个列存储订单信息和处理进度,快速查询近期新增/待处理订单列表。同时也可将历史订单数据用于统计、用户行为分析等离线任务。

-   搜索推荐引擎 存储网络爬虫持续不断抓取并处理后的原始网页信息,通过 MapReduce、Flink、Spark 等大数据计算框架分析处理原始数据后产出粗选、精选、排序后的网页索引集,再存储到 HBase 以提供近实时的随机查询能力,为上层的多个字节跳动应用提供通用的搜索和推荐能力。

-   大数据生态 天生融入 Hadoop 大数据生态。对多种大数据组件、框架拥有良好的兼容性,工具链完善,快速打通大数据链路,提高系统构建落地效率,并借助 HDFS 提供可观的成本优势。敏捷平滑的水平扩展能力可以自如地应对数据体量和流量的快速增长。

-   广告数据流 存储广告触达、点击、转化等事件流,为广告分析系统提供快速的随机查询及批量读取能力,助力提升广告效果分析和统计效率。

-   用户交互数据 Facebook 曾使用 HBase 存储用户交互产生的数据,例如聊天、评论、帖子、点赞等数据,并利用 HBase 构建用户内容搜索功能。

-   时序数据引擎 基于 HBase 构建适用于时序数据的存储引擎,例如日志、监控数据存储。例如 OpenTSDB(Open Time Series Database)是一个基于 HBase 的时序存储系统,适用于日志、监控打点数据的存储查询。

-   图存储引擎 基于 HBase 设计图结构的数据模型,如节点、边、属性等概念,作为图存储系统的存储引擎。例如 JanusGraph 可以基于 HBase 存储图数据。

生态

通过在 HBase之上引入各种组件可以使HBase应用场景得到极大扩展,例如监控、车联网、风控、实时推荐、人工智能等场景的需求。

  1. Phoenix

主要提供SQL的方式来查询HBase里面的数据。一般能够在毫秒级别返回,比较适合OLTP以及操作性分析等场景,支持构建二级索引。

  1. Spark

很多企业使用HBase存储海量数据,一种常见的需求就是对这些数据进行离线分析,我们可以使用Spark(Spark SQL) 来实现海量数据的离线分析需求。同时,Spark还支持实时流计算,我们可以使用 HBase+Spark Streaming 解决实时广告推荐等需求。

  1. HGraphDB

分布式图数据库,可以使用其进行图 OLTP查询,同时结合 Spark GraphFrames 可实现图分析需求,帮助金融机构有效识别隐藏在网络中的黑色信息,在团伙欺诈、黑中介识别等。

  1. GeoMesa

目前基于NoSQL数据库的时空数据引擎中功能最丰富、社区贡献人数最多的开源系统。提供高效时空索引,支持点、线、面等空间要素存储,百亿级数据实现毫秒(ms)级响应;提供轨迹查询、区域分布统计、区域查询、密度分析、聚合、OD 分析等常用的时空分析功能;提供基于Spark SQL、REST、GeoJSON、OGC服务等多种操作方式,方便地理信息互操作。

  1. OpenTSDB

基于HBase的分布式的,可伸缩的时间序列数据库,适合做监控系统;比如收集大规模集群(包括网络设备、操作系统、应用程序)的监控数据并进行存储,查询。

  1. Solr

原生的HBase只提供了Rowkey单主键,仅支持对Rowkey进行索引查找。可以使用 Solr来建立二级索引/全文索引来扩展更多查询场景的支持。

功能

Bulkload

大批量向HBase导入数据的功能。使用MapReduce任务直接生成底层存储的HFile文件,并直接移动到HBase存储目录下,节省HBase写路径的开销从而提高写入效率。

Coprocessor

提供一套接口框架,给HBase原生接口添加类似lifecycle hook函数的能力,用来执行用户自定义的功能来扩展HBase的能力,例如二级索引、Observer等功能。

Filter

将用户的过滤逻辑下推到HBase服务端,避免无用数据传输处理开销来提高查询效率。

MOB

Medium Object Storage解决HBase对中等大小对象(10-100MB)的低延迟读写支持,拓宽HBase适用场景。

Snapshot

数据备份功能,将某一时刻的数据以及元数据备份,用于数据恢复、快照读、复制表等功能。

Replication

将一个HBase集群中的数据复制到目标HBase集群,使用WAL将变更记录同步到其他集群。

数据组织方式

HBase是半结构化存储。数据以行(row)组织,每行包括一到多个列簇(column family)。使用列簇前需要通过创建表或更新表操作预先声明column family。

column family是稀疏存储,如果某行数据未使用部分column family则不占用这部分存储空间。

每个column family由一到多个列(column qualifier)组成。column qualifier不需要预先声明,可以使用任意值。

最小数据单元为cell,支持存储多个版本的数据。由rowkey + column family + column qualifier + version指定一个cell。

同一行同一列族的数据物理上连续存储,首先以column qualifier字典序排序,其次以timestamp时间戳倒序排序。

简单起见可以将HBase数据格式理解为如下结构:

// table名格式:"${namespace}:${table}"
// 例如:table = "default:test_table"
[
    "rowKey1": { // rowkey定位一行数据
        "cf1": { // column family需要预先定义到表结构
            "cq_a": { // column qualifier无需定义,使用任意值
                "timestamp3": "value3", 
                // row=rowKey1, column="cf1:cf_a", timestamp=timestamp2
                // 定位一个cell
                "timestamp2": "value2", 
                "timestamp1": "value1"
            },
            "cq_b": {
                "timestamp2": "value2",
                "timestamp1": "value1"
            }
        }, 
        "cf3": {
            "cq_m": {
                "timestamp1": "value1"
            },
            "cq_n": {
                "timestamp1": "value1"
            }
        },
    },   
    "rowKey3": { 
        "cf2": { // 缺省column family不占用存储空间
            "cq_x": {
                "timestamp3": "value3",
                "timestamp2": "value2",
                "timestamp1": "value1"
            },
            "cq_y": {
                "timestamp1": "value1"
            }
        },
    }
]
复制代码

主要数据结构

HDFS目录结构

hdfs://base_dir/hbase_cluster_name/data/default/table_name/region_name(e.g.fffe6d7a8e19490f8770fbe8637a686c)/column_family_name/hfile_name(e.g.7a8b82e197274fd7ade1a7f6b20b9417)
复制代码

MemStore

MemStore数据结构详细介绍可参考 hbasefly.com/2019/10/18/…

MemStore使用SkipList组织KeyValue数据,提供O(logN)的查询/插入/删除操作,支持双向遍历。

具体实现采用JDK标准库的ConcurrentSkipListMap,结构如下图:

图片选自 hbasefly.com/2019/10/18/…

写一个KeyValue到MemStore的顺序如下:

  1. 在JVM堆中为KeyValue对象申请一块内存区域。
  1. 调用ConcurrentSkipListMap的put(K key, V value)方法将这个KeyValue对象作为参数传入。

图片选自 hbasefly.com/2019/10/18/…

HFile

HFile结构介绍详见hbasefly.com/2016/03/25/…

HFile是HBase存储数据的文件组织形式,参考BigTable的SSTable和Hadoop的TFile实现。HFile共经历了三个版本,其中V2在0.92引入,V3在0.98引入。HFileV1版本的在实际使用过程中发现它占用内存多,HFileV2版本针对此进行了优化,HFileV3版本基本和V2版本相同,只是在cell层面添加了Tag数组的支持。这里主要针对V2版本进行分析。

官方文档的HFile结构图如下:

图片选自 hbase.apache.org/book.html#_…

HFlie主要分为4个section

  • Scanned block section:顾名思义,表示顺序扫描HFile时所有的数据块将会被读取,包括Leaf Index Block和Bloom Block。
  • Non-scanned block section:表示在HFile顺序扫描的时候数据不会被读取,主要包括Meta Block和Intermediate Level Data Index Blocks两部分。
  • Load-on-open-section:这部分数据在HBase的region server启动时,需要加载到内存中。包括FileInfo、Bloom filter block、data block index和meta block index。
  • Trailer:这部分主要记录了HFile的基本信息、各个部分的偏移值和寻址信息。

\

一个HFile内的数据会被切分成等大小的block,每个block的大小可以在创建表列簇的时候通过参数blocksize => ‘65535’进行指定,默认为64k,大号的Block有利于顺序Scan,小号Block利于随机查询,需要根据使用场景来权衡block大小。

图片选自 hbasefly.com/2016/03/25/…

HFileBlock

block有多种类型,除了data block还包括index block和bloom block来优化数据读取。但格式统一抽象如下:

图片选自 hbasefly.com/2016/03/25/…

Data Block

HBase中数据存储的最小文件单元。主要存储用户的KeyValue数据。KeyValue是HBase数据表示的最基础单位,每个数据都是以KeyValue结构在HBase中进行存储。KeyValue结构的内存/磁盘表示如下:

图片选自 hbasefly.com/2016/03/25/…

可以看出Row、ColumnFamily、ColumnQualifier是每个KeyValue都会存储,有明显的信息冗余。因此设计名称时最好比较精简,尽量减小重复信息占用的存储。

Block Index

可参见hbasefly.com/2016/04/03/…

索引分为单层和多层两种。单层即root data index,在打开HFile时就全量加载进内存。结构表示如下:

图片选自 hbasefly.com/2016/04/03/…

  • Block Offset 表示索引指向数据块的偏移量,
  • BlockDataSize 表示索引指向数据块在磁盘上的大小,
  • BlockKey 表示索引指向数据块中的第一个key
  • 其他三个字段记录HFile所有Data Block中最中间的一个Data Block,用于在对HFile进行split操作时,快速定位HFile的中间位置

Root Index Entry不是定长的,因此需要在Trail Block中的dataIndexCount记录entry数量才能保证正常加载。

NonRoot Index一般有intermediate和leaf两层,具有相同结构。Index Entry指向leaf索引块或者数据块。NonRoot index通过entry Offset实现索引块内的二分查找来优化查询效率。

图片选自 hbasefly.com/2016/04/03/…

完整的索引流程如下图,rowkey通过多级索引以类似B+树的方式定位到所属的数据块。

图片选自 hbasefly.com/2016/04/03/…

HBase集群架构

Zookeeper

提供分布式一致性的元数据管理服务。HBase使用Zookeeper实现master节点信息登记、master节点选主、RegionServer信息登记、分布式任务管理等功能。

HMaster

元信息管理组件,以及集群调度、保活等功能。通常部署一个主节点和一到多个备节点,通过Zookeeper选主。

RegionServer

提供数据读写服务,每个实例负责一段不重叠的连续rowkey范围内的数据。

ThriftServer

提供一层以Thrift协议访问数据的代理层。

HMaster主要组件

CatalogJanitor

定期扫描元数据hbase:meta的变化,回收无用的region(仅当没有region-in-transition时)。

scan方法执行扫描、回收操作:

  1. 扫描一遍hbase:meta找出可回收的region,检查元数据一致性并生成报告实例
  1. 已完成merge或split的region都可被回收,即子目录/父目录没有reference文件后
  1. 创建GCMultipleMergedRegionsProcedure/GCRegionProcedure异步回收regions

    1. GCMultipleMergedRegionsProcedure步骤:

      1. merge的两个源region分别创建GCRegionProcedure删除region数据
      2. 删除merge生成region元信息的merge qualifiers(info:mergeN)
    2. GCRegionProcedure步骤:

      1. archive要回收region的HFiles
      2. 删除region对应的WAL
    3. assignmentManager删除region state

    4. meta表删除该region记录

    5. masterService、FavoredNodesManager删除该region

AssignmentManager

管理region分配,processAssignQueue方法每当pendingAssignQueue放满RegionStateNode时批量处理。单独启动daemon线程循环处理。

processAssignmentPlans方法雇用LoadBalancer

  1. 首先尝试尽量保持现有分配
  1. 将分配plan的目标server region location更新到对应RegionStateNode中
  1. 将regionNNode的ProcedureEvent放入队列,统一唤醒所有regionNodes的events上等待的procedures
  1. 不保持原有分配的通过loadbalancer的round-robin策略分配(原则是不能降低availability)
  1. 分配失败的塞回pendingAssignQueue下次重新处理

setupRIT方法处理RIT,仅仅是设置RegionStateNode的this.procedure和ritMap

HMaster启动流程

  1. 先将自己加到backup master的ZK目录下,这样抢主失败了active master能感知到backup;
  1. 除非关闭master,无限循环尝试写入active master的ZK目录来抢主,成功的话从backup目录删除自己的znode;失败的话监听ZK上activeMaster的znode,主挂了就再次抢主
  1. 初始化文件系统相关组件

    1. MasterFileSystem - 封装对底层HDFS文件系统的交互
    2. MasterWalManager - 封装master分裂WAL操作,管理WAL文件、目录
    3. TableDescriptor - 管理table信息,预加载所有table的信息
  1. 发布clusterID到ZK,位于/hbase/hbaseid
  1. 加文件锁防止hbck1在hbase2集群执行造成数据损坏
  1. 初始化以下master状态管理组件

    1. ServerManager - 管理regionserver状态(online/dead),处理rs启动、关闭、恢复

    2. SplitWALManager - 代替ZK管理split WAL的procedures,

    3. ProcedureExecutor - 负责调度、执行、恢复procedures,包含以下组件

      1. WALProcedureStore - 持久化记录procedure状态,用于故障恢复未完成的procedure。
      2. MasterProcedureScheduler - 针对procedure类型选择合适的并发控制,例如region是server/namespace/table/region等粒度,大致原理是提供不同等级的queue来跑任务。
      3. 只init procedureExecutor来加载procedures但不开始让workers执行,因为要等DeadServers的ServerCrashProcedures调度执行完成,避免重复执行SCP。
    4. AssignmentManager - 负责调度region,包含以下组件

      1. RegionStates - 管理内存态的region状态,包括online/offline/RIT的region信息 RegionStateNode表示一个region的内存态状态,和meta表保持一致,会关联TransitRegionStateProcedure以保证最多一个RIT在并发
      2. RegionStateStore - 负责更新region state到meta表 AssignmentManager::start()方法从ZK路径meta-region-server加载meta region state,对第一个meta region(hbase:meta,,1.1588230740)上锁后设置region location、state、唤醒等待着meta region online的event
      3. 从procedure列表里找出RIT,即未完成的TransitRegionStateProcedure,由AssignmentManager::setupRIT方法将RIT的procedures绑定到regionStates里对应的region
    5. RegionServerTracker - 监听ZK的rs目录管理online servers,对比ServerCrashProcedure列表、HDFS的WAL目录里的alive/splitting rs记录,delta就是dead servers,并分别调用ServerManager::expireServer安排ServerCrashProcedures;将online rs添加到ServerManager管理。ServerCrashProcedure流程如下:

      1. SERVER_CRASH_START:如果挂了的rs负责meta表,先进入SERVER_CRASH_SPLIT_META_LOGS状态split meta WAL,将rs状态设为*SPLITTING_META* MasterWalManager::splitMetaLog在HDFS找到该rs的WAL目录,移动到-splitting结尾的新路径避免更多WAL写入。 这里支持用ZK或不用ZK两种模式,先介绍用ZK的模式: SplitLogManager::splitLogDistributed方法过滤出该路径下.meta结尾的WAL文件,为每个文件提交一个Task并记录到ZK目录splitWAL下,由ZkSplitLogWorkerCoordination协调online的regionserver认领并通过SplitLogWorker::splitLog方法处理,核心逻辑在WALSplitter::splitLogFile里,依次读取WAL文件每条entry,格式如下:

        1.       对每个WAL entry的region检查lastFlushedSeqID,如果大于WAL entry的seqID就跳过,否则放入buffer,LogRecoveredEditsOutputSink异步批量写到临时目录里对应region的recovered.edits目录下,具体实现在OutputSink::duRun里,调用LogRecoveredEditsOutputSink::append,最终在ProtobufLogWriter::append方法将rowkey和cells追加写到HDFS里tmp目录下对应region的recovered.edits目录;
        2.       等所有log split都结束后设置serverState为SPLITTING_META_DONE。 不用ZK的模式将上述步骤封装成SplitWALProcedure,由HMaster协调全过程。
        3. \
      2. SERVER_CRASH_ASSIGN_META:ZK模式进入此状态assign meta region上线,然后进入SERVER_CRASH_GET_REGIONS状态获取该宕机rs上的region列表,进入SERVER_CRASH_SPLIT_LOGS状态处理用户数据的WAL split,过程类似上述meta WAL split,全部完成后标记rs的ServerState状态OFFLINE。(非ZK模式通过SplitWALProcedure处理)

      3. SERVER_CRASH_ASSIGN状态assign该rs上所有挂掉的region后进入SERVER_CRASH_FINISH状态删除该rs,标记这个dead server处理完成

    6. TableStateManager - meta表确认已online才能启动,负责更新table state到meta表

  1. 初始化一系列ZK相关的trackers

    1. LoadBalancer
    2. RegionNormalizer
    3. LoadBalancerTracker
    4. RegionNormalizerTracker
    5. SplitOrMergeTracker
    6. ReplicationPeerManager
    7. DrainingServerTracker
    8. MetaLocationSyncer + MasterAddressSyncer (client ZK不是observer mode时需要)
    9. SnapshotManager
    10. MasterProcedureManagerHost
  1. InitializationMonitor - zombie master检测 HBASE-21535
  1. 如果是新建集群,安排InitMetaProcedure来初始化元数据,即创建AssignProcedure把meta表region拉起
  1. 初始化以下后台服务

    1. Balancer
    2. CatalogJanitor
    3. ExecutorServices
    4. LogCleaner
    5. HFileCleaner
    6. ReplicationBarrierCleaner
  1. 等待元数据构建完成
  1. 等待足够多RegionServer加入集群,默认至少1个,可配置数量: hbase.master.wait.on.regionservers.maxtostart hbase.master.wait.on.regionservers.mintostart
  1. AssignmentManager::joinCluster扫描meta表每行数据,构建regionStates,wake等待meta加载后执行的任务。
  1. 启动其他Chore服务,如:

    1. RITChore - 定期巡检RIT数量是否过多并打warn日志和打metrics
    2. DeadServerMetricRegionChore - 定期打deadServer相关metrics
  1. TableStateManager::start扫描meta表的table:state column和HDFS上每个table目录下.tabledesc目录下最新的$seqNum.tableinfo文件(即seqNum最大的.tableinfo文件),将meta表里的tableState添加到内存中的tableState,HDFS里不存在则设置为ENABLED
  1. assignmentManager::processOfflineRegions对每个offline region创建AssignProcedure,按round-robin策略分配
  1. 如果开启了favoredNode功能,扫meta创建一个索引多种查询模式(rs->region, region->rs等)的元数据快照来初始化favoredNodesManager
  1. 启动Chore服务

    1. ClusterStatusChore
    2. BalancerChore
    3. RegionNormalizerChore
    4. CatalogJanitor
    5. HbckChore
  1. assignmentManager检查regionserver实例是否存在不同版本,是则移动所有system table到版本最新的rs上以保证兼容性。
  1. 初始化quota
  1. serverManager.clearDeadServersWithSameHostNameAndPortOfOnlineServer清楚deadServer里相同host和port的已经online的rs,因master初始化期间加入的rs未更新状态,见HBASE-5916
  1. 检查ZK上ACL配置
  1. 初始化MobCleaner
  1. 最后刷新下balancer的RegionLocationFinder
  2. 创建master addr tracker,注册到zk,阻塞等待master上线
  1. 等待master实例在zk设置集群状态为已上线
  1. 从zk读取clusterID
  1. 等待active master上线
  1. 初始化RegionServerProcedureManagerHost并加载procedures,包括:

RegionServerSnapshotManager

RegionServerFlushTableProcedureManager

  1. 创建cluster connection和联系master用的rpc client
  1. 创建RegionServerCoprocessorHost
  1. 反复尝试向master rpc通知本region server上线,直到成功

    1. 通过masterAddressTracker.getMasterAddress获取master地址

    2. 创建rpc通道并调用rpc regionServerStartup。 master实现如下:

      1. MasterRpcServices.regionServerStartup确认master启动完成
      2. 调用ServerManager.regionServerStartup检查clock ske,以及发起请求的region server是否被记录为dead,是则返回异常让rs自杀。(每次rs启动会携带递增的start code以唯一标识每次重启)通过检查后将该rs加入ServerManager实例内部的onlineServers中(基于ConcurrentSkipListMap)同时移出rsAdmins(HashMap),这里存着未纳入集群的rs。
      3. response返回master看到的该rs的hostname信息,rs发现不一致的话抛异常
  1. 在zk创建该region server的临时节点并存储infoPort、versionInfo信息
  1. response还可能返回hbase.rootdir让rs以此目录初始化filesystem。把conf里的rootDir更新成匹配hbase.rootdir配置对应的fs类型,以免用错fs。
  1. 初始化WAL和replication。在filesystem创建该region server的wal目录。通过反射创建新replication实例
  1. 启动RegionServerPrecedureManagerHost,启动snapshot handler和其他procesure handlers
  1. 启动quotaManager
  1. 定期向master上报负载信息,调用regionServerReport RPC

Master故障恢复

故障恢复流程即从master实例从ZK监听到主master节点掉了,抢主成功后执行上述启动流程。

RegionServer启动流程

先初始化所有组件但不启动,直到reportForDuty成功注册到Hmaster后才启动所有服务。

  1. 在ctor里初始化(不启动)

    1. 初始化Netty server event loop用于RPC server/client和WAL

    2. 检查Memory limit,HFile version,codecs,初始化UserProvider,配置短路读等

    3. 初始化RSRpcServices,其中是所有RPC接口的实现

    4. 初始化HFileSystem和FSTableDescriptors

    5. 初始化ZK,创建:

      1. ZkCoordinatedStateManager来协调WAL split
      2. MasterAddressTracker管理ZK上当前active master信息,监听主master变化
      3. ClusterStatusTracker管理ZK上的cluster配置信息
    6. 创建ChoreService和ExecutorService

    7. 拉起rs的webUI

  1. reportForDuty成功上报HMaster(启动服务)

    1. 上报前初始化ZK,创建RegionServerProcedureManagerHost并从配置项hbase.procedure.regionserver.classes加载system procedures、RegionServerSnapshotManager和RegionServerFlushTableProcedureManager
    2. 初始化可以短路优化本地请求的ClusterConnection和用于HMaster通信的client
    3. 从ZK获取当前active master地址,发送RegionServerStartupRequest向master注册本rs
    4. 成功收到resp后在handleReportForDutyResponse方法把master返回的KV对加入自身conf,在ZK创建ephemeral znode(默认目录rs/$hostname,$port,$startcode),把rsInfo写入value,PB格式如下:
    5. 如果resp里有hbase.rootdir这个key,需要重新初始化FS更新rootDir
    6. ZNodeClearer::writeMyEphemeralNodeOnDisk把rs的ZK ephemeral节点路径写入FS里HBASE_ZNODE_FILE环境变量设置的路径。server正常退出时会删除该文件,用来识别server是否正常退出,加速恢复
    7. 创建WAL相关目录,并初始化replication的source和sink实例并启动
    8. 唤醒等待rs online的线程
    9. 启动snapshotManager和其他procedureHandlers,quotaManager
    10. 定期调用HMaster的regionServerReport RPC上报server load,报错则尝试重建连接

RegionServer故障恢复

regionserver每次启动的startcode不同,都视为一个新的rs实例,因此走一遍启动流程。

Region上线流程

Region状态转换统一抽象成TransitRegionStateProcedure。RIT即一个region有未完成的TRSP。常见的状态转换包括以下几种:

  1. assign region: GET_ASSIGN_CANDIDATE ------> OPEN -----> CONFIRM_OPENED

    1. 设置region的candidate rs,AssignmentManager异步用balancer选择rs打开region。assign是封装成一个AssignmentProcedureEvent创建在RegionStateNode里,AssignmentManager::processAssignQueue批量获取待assign region的regionStateNodes,acceptPlan方法里依次调用每个AssignmentProcedureEvent的wakeInternal方法,将阻塞等待在event上的procedures保序加入procedureScheduler调度队列恢复执行。

    2. AssignmentManager::regionOpening更新regionNode状态为OPENING,regionStates添加region和对应的rs记录,创建子过程OpenRegionProcedure,这个RemoteProcedure会向目标rs发送executeProcedures RPC,在RSRpcServices::executeOpenRegionProcedures方法里提交AssignRegionHandler异步地通过HRegion.openHRegion做以下操作:

      1. 写RegionInfo到HDFS上region的目录/ns/table/encodedRegionName下的.regioninfo文件用于恢复meta表数据。如果相同文件内容已存在就跳过,否则覆盖写(先写到region目录下.tmp目录,再move)
      2. initializeStores并发初始化每个cf的HStore,创建cf的hdfs目录,配置blocksize、hdfs storagePolicy、dataBlockEncoding、cell comparator、TTL、低峰期等,创建memstore,创建StoreEngine(集成storeFlusher,compactionPolicy,Compactor,storeFileManager的工厂实例),遍历HDFS上该region该cf的HFiles创建StoreFileInfo集合,并发打开reader,从HFIie读取元信息compectedFiles并移动到archive目录下WIP here
  1. unassign region: CLOSE -----> CONFIRM_CLOSED
  1. reopen/move region: CLOSE -----> CONFIRM_CLOSED -----> GET_ASSIGN_CANDIDATE ------> OPEN -----> CONFIRM_OPENED

同一时刻一个region只能最多有一个TRSP。

HRegion

Region name格式:

<tablename>,<startKey>,<regionId>_<replicaId>.<encodedName>.
复制代码

regionId: Usually timestamp from when region was created

encodedName: MD5 hash of the ",,_" part (included only for the new format)

例如:

ycsb_test,user6399,1600830539266_0.3be086687f0a59f0f3fd74d5041be1bd.
复制代码

MD5哈希值(上例中的3be086687f0a59f0f3fd74d5041be1bd)作为hbase:meta表的rowkey用于查对应region信息。建表时的region pre-split过程就是预先在hbase:meta表写入记录切分好region,比如用"0000"到"9999"之间的多个数值分别作为region start_key。

代码见private void locateInMeta(TableName tableName, LocateRequest req)

Region Split 热点切分

hbasefly.com/2017/08/27/…

HBase支持的几种常见region split触发策略如下:

  • ConstantSizeRegionSplitPolicy:0.94版本前默认切分策略。一个region中最大store的大小大于设置阈值之后触发切分。阈值(hbase.hregion.max.filesize)设置较大对大表比较友好,但是小表就有可能不会触发分裂,极端情况下可能就1;设置较小则对小表友好,但一个大表就会在整个集群产生大量的region。
  • IncreasingToUpperBoundRegionSplitPolicy: 0.94版本~2.0版本默认切分策略。总体来看和ConstantSizeRegionSplitPolicy思路相同,一个region中最大store大小大于设置阈值就会触发切分。阈值计算公式 :(#regions) * (#regions) * (#regions) * flush size * 2,最大不超过用户设置的MaxRegionFileSize。这种切分策略很好的弥补了ConstantSizeRegionSplitPolicy的短板,能够自适应大表和小表。
  • SteppingSplitPolicy: 2.0版本默认切分策略。如果region个数等于1,切分阈值为flush size * 2,否则为MaxRegionFileSize。这种切分策略对于大集群中的大表、小表会比IncreasingToUpperBoundRegionSplitPolicy更加友好,小表不会再产生大量的小region,而是适可而止。

Region split 步骤如下:

  1. 找到切分点splitpoint,一般是整个region中最大store中的最大HFile文件中最中心的一个block的首个rowkey。

  1. HBase将整个切分过程包装成了一个事务,意图能够保证切分事务的原子性。整个分裂事务过程分为三个阶段:prepare – execute – (rollback)
  1. prepare阶段:在内存中初始化两个子region,具体是生成两个HRegionInfo对象,包含tableName、regionName、startkey、endkey等。同时会生成一个transaction journal,这个对象用来记录切分的进展
  1. execute阶段:HBase 1.0 版本的 region split 核心操作如下图:

图片选自:blog.cloudera.com/apache-hbas…

  1. regionserver 更改ZK节点 /region-in-transition 中该region的状态为SPLITING。
  1. master通过watch节点/region-in-transition检测到region状态改变,并修改内存中region的状态,在master页面RIT模块就可以看到region执行split的状态信息。
  1. 在父存储目录下新建临时文件夹.split保存split后的daughter region信息。
  1. 关闭parent region:parent region关闭数据写入并触发flush操作,将写入region的数据全部持久化到磁盘。此后短时间内客户端落在父region上的请求都会抛出异常NotServingRegionException。
  1. 核心分裂步骤:在.split文件夹下新建两个子文件夹,称之为daughter A、daughter B,并在文件夹中生成reference文件,分别指向父region中对应文件。生成reference文件的日志如下:
2017-08-12 11:53:38,158 DEBUG [StoreOpener-0155388346c3c919d3f05d7188e885e0-1] regionserver.StoreFileInfo: reference 'hdfs://hdfscluster/hbase-rsgroup/data/default/music/0155388346c3c919d3f05d7188e885e0/cf/d24415c4fb44427b8f698143e5c4d9dc.00bb6239169411e4d0ecb6ddfdbacf66' to region=00bb6239169411e4d0ecb6ddfdbacf66 hfile=d24415c4fb44427b8f698143e5c4d9dc。
复制代码

reference文件名前半段是父region对应的HFile文件名,后半段是父region的目录名。

图片选自 hbasefly.com/2017/08/27/…

reference文件内容包括切分点的rowkey,和一个boolean值表示该子文件夹是上半还是下半部分。

  1. 父region分裂为两个子region后,将daughter A、daughter B拷贝到HBase根目录下,形成两个新的region。
  1. parent region通知修改 hbase.meta 表后下线,不再提供服务。下线后parent region在meta表中的信息并不会马上删除,而是标注split列、offline列为true,并记录两个子region。
  1. 开启daughter A、daughter B两个子region。通知修改 hbase.meta 表,正式对外提供服务。

rollback阶段:如果execute阶段出现异常,则执行rollback操作。为了实现回滚,整个切分过程被分为很多子阶段,回滚程序会根据当前进展到哪个子阶段清理对应的垃圾数据。具体见下图:

图片选自 hbasefly.com/2017/08/27/…

Region split过程不会实际移动文件,而是等到下次compaction时处理。在此之前通过子region的reference文件重定向到原来父region的文件去读数据,流程如下:

图片选自 hbasefly.com/2017/08/27/…

HBase 2.x 版本的 region split 实现去除了对 Zookeeper 的依赖,通过持久化存储的 Procedure 框架在 HMaster 内部闭环管理切分进度,在 HMaster 故障恢复后可恢复进度继续执行或取消。其他部分的实现和 1.0 版本类似。

Region Merge 碎片整合

region merge可以通过HBaseAdmin手动触发,或由normalization触发。merge流程如下:

  1. 调用MasterRpcServices::mergeTableRegions方法,从AssignmentManager查找对应regionInfo并调用HMaster::mergeRegions()方法,检查master已完成初始化且merge功能启用后提交NonceProcedureRunnable(关于Nonce的介绍见zhuanlan.zhihu.com/p/75734938)…
  1. MergeTableRegionsProcedure检查待合并的parent regions是否有重复,是否同table,是否相邻,是否重叠,是否主副本,是否online,集群和table状态是否可以merge。检查通过后排序regions并生成合并后region的RegionInfo(计算合并后的startKey,endKey,regionID取待合并regions最大值+1)
  1. 调用executeFromState,从MERGE_TABLE_REGIONS_PREPARE状态开始,检查table不能正在打snapshot,merge功能必须开启,如果region上有已merge标记需要成功清除并archive对应HDFS文件。具体逻辑是检查region的目录下是否有referenceFile,没有则提交GCMultipleMergedRegionsProcedure清理已merge的regions文件,并清理HMaster内存里对应的region信息。

GCMultipleMergedRegionsProcedure调用MetaTableAccessor.getMergeRegions检查hbase:meta表里该region的cells,过滤出column为"info:merge.*" regex格式的cell,从value解析RegionInfo,这些就是该region merge前的parent regions。

对父母region分别创建一个GCRegionProcedure,具体在GC_REGION_ARCHIVE状态下调用HFileArchiver.archiveRegion移动region文件到archive目录,如果archive目录下已存在同名文件,尝试加时间戳后缀重命名该文件,失败就删除之。move成功后删除region目录。

然后删除该region对应的WAL文件目录(存着seqid等文件),格式:$rootDir/$cluster/WALs/data/$namespace/table/regionName

然后删除AssignmentManager里该region的状态,再删除meta表里该region的info cf下所有信息。ServerManager::removeRegion清楚该region的两个seqID信息:

  • The last flushed sequence id for a store in a region
  • The last flushed sequence id for a region

再清理该region的FavoredNodes。

  1. 从meta表删除子region上对应"info:merge.*" regex格式的cq。至此完成清理父母regions里上次merge留下的元数据。
  1. 设置AssignmentManager里父母region的状态为MERGING
  1. 检查quota是否够,跳过正在执行的normalizer
  1. MERGE_TABLE_REGIONS_CLOSE_REGIONS 关闭父母regions
  1. MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS 检查关闭都完成后
  1. MERGE_TABLE_REGIONS_CREATE_MERGED_REGION移除只读副本,创建merge出的新region。具体步骤:

    1. 在第一个父region目录下创建.merges临时目录
    2. 对父母regions的每个cf分别遍历所有storeFiles,每个storeFile分别创建referenceFile,文件名称格式:${parent_HFile_name_before_merge}.${parent_region_name} 文件内容统一指向原文件的startKey,等效于split里指向上半部分的referenceFile内容。
    3. commitMergedRegion将.merged临时目录下的内容move到合并后的子region目录下
    4. AssignmentManager创建新region的状态为MERGING_NEW
  1. MERGE_TABLE_REGIONS_WRITE_MAX_SEQUENCE_ID_FILE:从父母region的seqid里找到最大值,写入子region的seqid文件
  1. MERGE_TABLE_REGIONS_UPDATE_META:AssignmentManager将子region的状态设为MERGED, 将父母region删除,regionStateStore.mergeRegions方法原子地操作meta表,删除父母region,添加子region。子region的info cf下会写入父母regions的RegionInfo,cq分别是merge0000merge0001 。子region状态为CLOSED防止过早被master调度拉起。 子region需要添加父母regions作为replication barrier,保证父母regions都完成同步再开始同步子region。
  1. 创建AssignProcedure打开子region

Merge过程也只是创建reference等compaction才移动数据,移动前读数据方式如下图:

最佳实践

rowkey设计

  • 最大长度是64KB,实际应用中长度一般为 10 ~ 100bytes。key在保证功能的前提下建议越短越好,因为key是冗余到每个cell存储的,过长的key会占用更多存储、缓存空间。
  • 设计Key时,要充分利用排序存储这个特性,将经常一起读取的行存储到一起。HBase以HFile文件块为单位从HDFS读取数据,一次性读出相邻相关数据可以达到随机读变成顺序读的效果。

但同时要防止出现热key聚焦打爆region server实例。

反例:以时间戳作rowkey前缀,一段时间的请求会全部打到同一regionserver。

Column family数量

过多的cf会影响HBase性能,建议不超过3个。

value大小

建议不要超过1MB。过大的value会影响HBase读写性能。可以将实际value存储在其他对象存储系统,在HBase的value存储其位置信息。

Region数量

一个region的大小最好在10-50GB之间。

mapreduce的应用在低峰期运行

批处理任务建议在业务低峰期运行,并且需要对HBase的访问流量进行一定限制。

尽量复用client实例

新建client实例需要访问Zookeeper和元信息表所在regionserver,频繁操作有打垮服务的风险。

client参数调优

  • nodelay设置true

  • 读较多的应用中gc的新生代不能设置太小

  • 数据版本尽可能少来增加有效缓存容量,提升命中率

  • 避免一次scan过多的row,尽量拆分为多次小规模scan作分页查询