在这里简单整理下hbase hbck的用法。 用法: opts通用可选项 -help 展示help信息; -detail 展示所有Region的详情; -timelag <秒级时间> 处理在过去的指定时间内没有发生过元数据更新的region; -sleepBeforeRerun <秒级时间> 在执行-fix指令后时睡眠指定的时间后再检查fix是否生效; -summary 只打印表和状态的概要信息; -metaonly 只检查hbase:meta表的状态; -sidelineDir <hdfs://> 备份当前的元数据到HDFS上; -boundaries 校验META表和StoreFiles的Region边界是否一致; 元数据修复选项 在不确定的情况下,慎用以下指令。 -fix 尝试修复Region的分配,通常用于向后兼容; -fixAssignments 尝试修复Region的分配,用来替换-fix指令; -fixMeta 尝试修复元数据问题;这里假设HDFS上的region信息是正确的; -noHdfsChecking 不从HDFS加载/检查Region信息;这里假设hbase:meta表中的Region信息是正确的,不会在检查或修复任何HDFS相关的问题,如黑洞(hole)、孤岛(orphan)或是重叠(overlap); -fixHdfsHoles 尝试修复HDFS中的Region黑洞; -fixHdfsOrphans 尝试修复hdfs中没有.regioninfo文件的region目录; -fixTableOrphans 尝试修复hdfs中没有.tableinfo文件的table目录(只支持在线模式); -fixHdfsOverlaps 尝试修复hdfs中region重叠的现象; -fixVersionFile 尝试修复hdfs中hbase.version文件缺失的问题; -maxMerge <n> 在修复region重叠的现时,允许merge最多<n>个region(默认n等于5); -sidelineBigOverlaps 在修复region重叠问题时,允许暂时搁置重叠量较大的部分; -maxOverlapsToSideline <n> 在修复region重叠问题时,允许一组里暂时搁置最多n个region不处理(默认n等于2); -fixSplitParents 尝试强制将下线的split parents上线; -ignorePreCheckPermission 在执行检查时忽略文件系统权限; -fixReferencesFiles 尝试下线引用断开(lingering reference)的StoreFile; -fixEmptyMetaCells 尝试修复hbase:meta表中没有引用到任何region的entry(REGIONINFO_QUALIFIER为空的行)。 Datafile修复选项 专业命令,慎用。 -checkCorruptHFiles 检查所有HFile —— 通过逐一打开所有的HFile来确定其是否可用; -sidelineCorruptHFiles 隔离损坏的HFile。该指令中包含-checkCorruptHFiles操作。 Meta修复快捷指令 -repair 是以下指令的简写:-fixAssignments -fixMeta -fixHdfsHoles -fixHdfsOrphans -fixHdfsOverlaps -fixVersionFile -sidelineBigOverlaps -fixReferenceFiles -fixTableLocks -fixOrphanedTableZnodes; -repairHoles 是以下指令的简写:-fixAssignments -fixMeta -fixHdfsHoles。 Table lock选项 -fixTableLocks 删除已持有超过很长时间的table lock((hbase.table.lock.expire.ms配置项,默认值为10分钟)。 Table Znode选项 -fixOrphanedTableZnodes 如果表不存在,则将其在zookeeper中ZNode状态设置为disabled。 其他 之前遇到过一篇文,整理了一些HBase的维护案例:http://blackproof.iteye.com/blog/2052898 ########
[阅读更多...]-
hbase hbck用法
-
HBase连接异常:KeeperErrorCode = OperationTimeout
手上的一个HBase相关的服务在重启后开始报错(重启前运行良好),错误信息如下: 错误信息提示连接zookeeper时间超长。经检查是因为有一个zookeeper节点已经停止运行(zk3.com),和运维商量后将zookeeper故障节点删除,并移掉了另一个节点,剩下了3个节点。修改配置文件并重启后服务恢复。 #######
[阅读更多...] -
使用HBase Coprocessor
HBase的Coprocessor是模仿谷歌BigTable的Coprocessor模型实现的。 Coprocessor提供了一种机制可以让开发者直接在RegionServer上运行自定义代码来管理数据。 首先必须要指明使用Coprocessor还是存在一些风险的。Coprocessor是HBase的高级功能,本来是只为HBase系统开发人员准备的。因为Coprocessor的代码直接在RegionServer上运行,并直接接触数据,这样就带来了数据破坏的风险,比如“中间人攻击(Man-in-the-MiddleAttack,简称“MITM攻击”,见百度词条)”以及其他类型的恶意入侵。目前还没有任何机制来屏蔽Coprocessor导致的数据破坏。此外,因为没有资源隔离,一个即使不是恶意设计的但表现不佳的Coprocessor也会严重影响集群的性能和稳定性。 通常我们访问HBase的方式是使用scan或get获取数据,使用Filter过滤掉不需要的部分,最后在获取到的数据上进行业务运算。但是在数据量非常大的时候,比如一个有上亿行及十万个列的数据集,再按常用的方式移动获取数据就会在网络层面遇到瓶颈。客户端也需要有强大的计算能力以及足够的内存来处理这么多的数据。此外,这也会使客户端的代码变得庞大而复杂。 这种场景正是Coprocessor可以发挥作用的地方。我们可以将业务运算代码封装到Coprocessor中并在RegionServer上运行,即在数据实际存储位置执行,最后将运算结果返回到客户端。 如下的一些理论可以帮助我们理解Coprocessor是如何发挥作用的: 触发器和存储过程:一个Observer Coprocessor有些类似于关系型数据库中的触发器,通过它我们可以在一些事件(如Get或是Scan)发生前后执行特定的代码。Endpoint Coprocessor则类似于关系型数据库中的存储过程,因为它允许我们在RegionServer上直接对它存储的数据进行运算,而非是在客户端完成运算。 MapReduce:MapReduce的原则就是将运算移动到数据所处的节点。Coprocessor也是按照相同的原则去工作的。 AOP:如果熟悉AOP的概念的话,可以将Coprocessor的执行过程视为在传递请求的过程中对请求进行了拦截,并执行了一些自定义代码。 Coprocessor类型 Coprocessor可以分为两大类:Observer Coprocessors(观察者)和EndPoint Coprocessor(终端)。 Observer Coprocessors Observer Coprocessor在一个特定的事件发生前或发生后触发。在事件发生前触发的Coprocessor需要重写以pre作为前缀的方法,比如prePut。在事件发生后触发的Coprocessor使用方法以post作为前缀,比如postPut。 Observer Coprocessor的使用场景如下: 安全性:在执行Get或Put操作前,通过preGet或prePut方法检查是否允许该操作; 引用完整性约束:HBase并不直接支持关系型数据库中的引用完整性约束概念,即通常所说的外键。但是我们可以使用Coprocessor增强这种约束。比如根据业务需要,我们每次写入user表的同时也要向user_daily_attendance表中插入一条相应的记录,此时我们可以实现一个Coprocessor,在prePut方法中添加相应的代码实现这种业务需求。 二级索引:可以使用Coprocessor来维持一个二级索引。这里暂不展开,有时间会单独说明。 根据作用的对象,Observer Coprocessor有如下几种:RegionObserver、RegionServerObserver、MasterObserver和WalObserver。我们可以通过这些Observer来处理其观察的对象的操作,比如可以通过RegionObserver处理Region相关的事件,如Get和Put操作。 Endpoint Coprocessor Endpoint Coprocessor可以让开发者在数据本地执行运算。一个典型的案例:一个table有几百个Region,需要计算它的运行平均值或者总和。 Observer Coprocessor中代码的执行是相对透明的,而对于Endpoint Coprocessor,则需要显式的调用Table, HTableInterface或者HTable中的CoprocessorService()方法才能使之执行。 从0.96版本开始,HBase开始使用Google的protobuff。这对Endpoint Coprocessor的开发多少有一些影响。Endpoint Coprocessor不应该使用HBase内部成员,尽量只使用公共的API,最理想的情况应该是只依赖接口和数据结构。这样可以使开发的Endpoint Coprocessor更加健壮,不会受到HBase内核演进的干扰。注释为private或evolving的HBase内部API在删除前不必遵守关于deprecate的语义版本规则或相关的一般java规则。而使用protobuff生成的文件不会受到这些注释的影响,因为这些文件是用protoc工具自动生成的。在生成时这些文件时,protoc不知道也不会考虑HBase是如何工作的。 装载和卸载Coprocessor 要使用Coprocessor,就需要先完成对其的装载。这可以静态实现(通过HBase配置文件),也可以动态完成(通过shell或Java API)。 静态装载和卸载Coprocessor 按以下如下步骤可以静态装载自定义的Coprocessor。需要注意的是,如果一个Coprocessor是静态装载的,要卸载它就需要重启HBase。 静态装载步骤如下: 1. 在hbase-site.xml中使用<property>标签定义一个Coprocessor。<property>的子元素<name>的值只能从下面三个中选一个: hbase.coprocessor.region.classes 对应 RegionObservers和Endpoints; hbase.coprocessor.wal.classes 对应 WALObservers; hbase.coprocessor.master.classes 对应MasterObservers。 而<value>标签的内容则是自定义Coprocessor的全限定类名。 下面演示了如何装载一个自定义Coprocessor(这里是在SumEndPoint.java中实现的),需要在每个RegionServer的hbase-site.xml中创建如下的记录: 如果要装载多个类,类名需要以逗号分隔。HBase会使用默认的类加载器加载配置中的这些类,因此需要将相应的jar文件上传到HBase服务端的类路径下。 使用这种方式加载的Coprocessor将会作用在HBase所有表的全部Region上,因此这样加载的Coprocessor又被称为系统Coprocessor。在Coprocessor列表中第一个Coprocessor的优先级值为Coprocessor.Priority.SYSTEM,其后的每个Coprocessor的值将会按序加一(这意味着优先级会减降低,因为优先级是按整数的自然顺序降序排列的)。 当调用配置的Observer Coprocessor时,HBase将会按照优先级顺序依次调用它们的回调方法。 2. 将代码放到HBase的类路径下。一个简单的方法是将封装好的jar(包括代码和依赖)放到HBase安装路径下的/lib目录中。 3. 重启HBase。 静态卸载的步骤如下: 1. 移除在hbase-site.xml中的配置。 2. 重启HBase。 3. 这一步是可选的,将上传到HBase类路径下的jar包移除。 动态装载Coprocessor 动态装载Coprocessor的一个优势就是不需要重启HBase。不过动态装载的Coprocessor只是针对某个表有效。因此,动态装载的Coprocessor又被称为表级Coprocessor。 此外,动态装载Coprocessor是对表的一次schema级别的调整,因此在动态装载Coprocessor时,目标表需要离线。 动态装载Coprocessor有两种方式:通过HBase Shell和通过Java API。 在下面介绍关于动态装载的部分,假设已经封装好了一个coprocessor.jar的包,里面包含实现代码及所有的依赖,并且已经将这个jar上传到了HDFS中。 通过HBase Shell动态装载和卸载 装载步骤如下 1. 在HBase Shell中disable 掉目标表 2. 使用类似如下的命令加载Coprocessor 简单解释下这个命令。这条命令在一个表的table_att中添加了一个新的属性“Coprocessor”。使用的时候Coprocessor会尝试从这个表的table_attr中读取这个属性的信息。这个属性的值用管道符“|”分成了四部分: 文件路径:文件路径中需要包含Coprocessor的实现,并且对所有的RegionServer都是可达的。这个路径可以是每个RegionServer的本地磁盘路径,也可以是HDFS上的一个路径。通常建议是将Coprocessor实现存储到HDFS。HBASE-14548允许使用一个路径中包含的所有的jar,或者是在路径中使用通配符来指定某些jar,比如:hdfs://<namenode>:<port>/user/<hadoop-user>/ 或者 hdfs://<namenode>:<port>/user/<hadoop-user>/*.jar。需要注意的是如果是用路径来指定要加载的Coprocessor,这个路径下的所有jar文件都会被加载,不过该路径下的子目录中的jar不会被加载。另外,如果要用路径指定Coprocessor时,就不要再使用通配符了。这些特性在Java API中也得到了支持。 类名:Coprocessor的全限定类名。 优先级:一个整数。HBase将会使用优先级来决定在同一个位置配置的所有Observer Coprocessor的执行顺序。这个位置可以留白,这样HBase将会分配一个默认的优先级。 参数(可选的):这些值会被传递给要使用的Coprocessor实现。这个项是可选的。 3. enable这个表 4. 检验Coprocessor是否被加载 Coprocessor可以在TABLE_ATTRIBUTES中找到。 加载步骤就是这样。 卸载步骤如下 1. disbale目标表 2. 使用alter命令移除掉Coprocessor 3. enable目标表 使用Java API动态装载和卸载 装载方式如下 针对不同版本的HBase会有不同的JavaAPI。幸运的是有一个全版本的Java API。下面的代码演示了是如何使用Java API来装载Coprocessor的: 0.96及更高版本的HBase还有另一套API。在这套API里,HTableDescriptor的addCoprocessor()方法提供了一种更简单的方式来动态加载Coprocessor: 卸载方式如下: 卸载方式就是重新加载表定义信息。重新加载的时候就不需要再使用setValue()方法或者是addCoprocessor()方法设置表的Coprocessor信息了: 对于0.96及更高版本的HBase,可以使用HTableDescriptor类的removeCoprocessor()方法。 Coprocessor示例程序
[阅读更多...] -
使用BufferedMutator
org.apache.hadoop.hbase.client.BufferedMutator主要用来对HBase的单个表进行操作。它和Put类的作用差不多,但是主要用来实现批量的异步写操作。 BufferedMutator替换了HTable的setAutoFlush(false)的作用。 可以从Connection的实例中获取BufferedMutator的实例。在使用完成后需要调用close()方法关闭连接。对BufferedMutator进行配置需要通过BufferedMutatorParams完成。 MapReduce Job的是BufferedMutator使用的典型场景。MapReduce作业需要批量写入,但是无法找到恰当的点执行flush。BufferedMutator接收MapReduce作业发送来的Put数据后,会根据某些因素(比如接收的Put数据的总量)启发式地执行Batch Put操作,且会异步的提交Batch Put请求,这样MapReduce作业的执行也不会被打断。 BufferedMutator也可以用在一些特殊的情况上。MapReduce作业的每个线程将会拥有一个独立的BufferedMutator对象。一个独立的BufferedMutator也可以用在大容量的在线系统上来执行批量Put操作,但是这时需要注意一些极端情况比如JVM异常或机器故障,此时有可能造成数据丢失。 如下是几个使用BufferedMutator的实例。 实例一: 这段代码演示了创建BufferedMutator的一般过程。如前文所说,BufferedMutator的配置通常通过BufferedMutatorParams完成。获取BufferedMutator实例则是通过Connection对象。在这里还调整了一个writeBuffer的设置,这里等于是覆盖了HBase配置文件中的“hbase.client.write.buffer”设置。 实例二: 这段代码只看前半部分即可。与实例一的不同在于这里新建了一个ExceptionListener接口实现,并添加到BufferedMutatorParams中替换了BufferedMutatorParams的默认ExceptionListener实现。 实例三: 这里是一段BufferedMutator测试代码。 实例四: 实例五: 如有必要,稍后抽时间对代码做些简单说明。 ######
[阅读更多...] -
译文:HBase File Locality in HDFS
Hadoop中一个不明确的内容就是Block复制:它自动完成,通常不需要用户关心。HBase将数据保存到HDFS,并完全相信它的安全性。正是因为HDFS的Block复制对HBase来说是完全透明的,就产生了一个问题:HBase的效率会受到多大的影响?当我们开始写MapReduce作业访问HBase和Hadoop的时候,就难免会想到这个问题。尤为关键的是,当HBase中存储的数据很多的时候,它怎样使数据距离需要的地方更近一些?这就涉及到HBase的数据是怎样在HDFS上存储了。 首先,我们看看Hadoop是如何处理这个问题的。在MapReduce文档中说明了task通常运行在距离其处理的数据比较近的位置。这主要是通过将HDFS中的数据拆分成小块实现的,这些小块的数据被称为Block。HDFS中的Block的大小要比文件系统的Block大得多:默认是64M,但是通常会被设置为128M(这个值还可以设置得更大一些——如果确认所有文件的size都大于单个Block)。每个Block对应一个map task。这也就意味着Block的size设置得越大,Block的数量就越少,需要的map task的数量就越少。Hadoop知道每个Block的存储位置,它会在存储Block的节点上直接运行map task。每个Block都有两到三个副本。事实上,hadoop选择的节点通常是存储Block副本的节点。这样Hadoop保证了MapReduce作业总是在本地处理数据。 现在回到HBase。既然已经知道Hadoop是如何让它的每个map task处理本地数据的,就可以再进一步思考下HBase是如何做到Data Locality的。要是读过关于HBase存储结构的文章的话,就知道HBase是将相关的文件保存在HDFS上。这些文件包括数据文件(HFile)和日志文件(WAL)。在源码里也能看到HBase是调用了FileSystem.create(Path path)方法来创建这些文件的。现在可以想想两种常见的访问模型:1.直接随机访问;2.使用MapReduce扫描全表。我们会很好奇HBase是怎样就近读取HDFS上的Block,从而提升这两种访问方式的效率的。 话说回来,如果Hadoop和Hbase没有在同一个集群上,而是分隔开的,就不要再想Data Locality了——这根本不可能做到。这就如同运行一个独立的MapReduce集群,而它根本无法在DataNode上执行task。所以要实现Data Locality就必须让相关的服务在一个同一个集群上,包括Hadoop(或者说是HDFS)、MapReduce和HBase。就是这样。 好了,是不是弄清了所有的服务都在同一个集群(希望是一个比较大的集群)上?弄清了,那就继续。在HBase访问数据的时候,Hadoop是怎样找到数据在哪儿呢?这样又回到了前面提到的两种访问模型上。这涉及到一个概念:RegionServer。不管是直接随机访问还是扫描全表,都是通过相同的API实现的。正如之前所提到的,HBase仅仅是执行文件的保存,而文件的分布和Block的复制是通过HDFS的DatNode来实现的。假设一个场景:在向HBase中存储了大量的数据后停止HBase服务并连续地对其进行重启。RegionServer在重启后会被分配随机数量的Regions。在这个时候,HBase的Data Locality是无法保证的。为什么呢? 最重要的是HBase不会频繁重启,并且会定期进行内部维护。随着时间的推移,数据不停地增加,HBase会执行compact来重写文件。由于各种原因,文件一旦写入HDFS就是不可变的。因此,数据会不停地写入新的文件。随着数据文件越来越多,HBase会将这些数据文件压缩(compact)合并成另一组新的文件。这里是最让人惊奇的地方:HDFS足够聪明,它知道将数据放到被需要的地方。这是怎么做到的呢?我们需要深入Hadoop的源码来看看HBase调用的FileSystem.create(Path path)方法是怎么工作的。因为我们选择的HBase存储方案是HDFS,所以我们实际上调用的是DistributedFileSystem.create(Path path)方法,该方法的代码如下: 这个方法返回了一个FSDataOutputStream类型的实例。create这个实例的方法如下: 这个方法中使用了一个DFSClient实例作为纽带来连接client和NameNode: 最终返回的是一个DFSClient.DFSOutputStream实例。数据不停地写入到DFSOutputStream,DFSClient会将之收集起来打成包作为一个Block写入到DataNode。这个过程是由DFSClient.DFSOutputStream.DataStreamer实现的,它以守护进程的形式在后台运行。接下来我们一步一步的推断出这个过程具体是怎么实现的。首先我们看一下DataStreamer后台线程的run()方法,它获取了存储数据的DataNode的列表: 而这个方法又调用了如下的代码: 接下来看看locateFollowingBlocks()又调用了哪个方法: 这里就是关键了。这里使用namenode对象添加了一个新的Block,方法中使用的src参数表示要写入的文件,clientName表示DFSClient实例的名称。接下来跳过部分不太重要的内容,直接看看稍后一些关键的步骤: 最后也是最为核心的代码,就是replicator.chooseTarget() 方法的详情了: 接回上面的内容,我们已经启动了一个DFSClient实例并创建了一个写满数据的序列化文件。当要将数据文件存储为Block的时候,上面的代码首先检查是否可以将Block保存在client所在的本地主机(代码中的“writer”)——这是程序中“case 0”的内容。而“case 1”的部分表示程序尝试在另一个机架上保存一个远程备份。后面“case 2”优先选择一个与“case 0”中相同的机架,然后才考虑选择不同的机架。如果还需要更多的备份,则会随机选择一个机架的任意节点保存。 这就表示,在完成一次对所有表的major compaction(可以是手动触发也可以在配置文件中配置)以后,RegionServer运行的时间越长,它将数据保存在本地节点的概率就越大。和RegionServer在相同物理主机上的DataNode会保留一份这个RegionServer需要的所有数据的拷贝。这样可以保证在执行scan、get或其他任何操作时有最好的性能。 最后,要想全面地了解HDFS的设计和数据冗余备份的内容请移步Hadoop官网:HDFS Architecture。也请注意到HBase团队一直在重新设计HMaster分配Region给RegionServer的方案。新的方案将会把Regions分配给拥有Block最多的RegionServer。这调整对改善HBase重启后的DataLocality特别有用。 原文: HBase File Locality in HDFS:http://www.larsgeorge.com/2010/05/hbase-file-locality-in-hdfs.html #####
[阅读更多...] -
HBase Region Locality
因为DataNode和RegionServer通常会部署在相同的机器上,所以会产生Locality这样的概念。 HBase的Locality是通过HDFS的Block复制实现的。在复制Block时,HBase是这样选择副本的位置的: 第一个副本写到本地节点上; 第二个副本写到另一个机架的随机节点上; 第三个副本写到相同机架的一个随机选择的其他节点上; 如果还有更多的副本,这些副本将会写到集群上的随机节点上。 就是这样,在flush或compact后,HBase的Region实现了Locality。 当一个RegionServer处在failover的情况下(rebalance或重启)时,可能会分配到一些没有本地StoreFiles的Region(因为此时没有可用的本地副本)。然而,有新数据再写入这些Region的时候,或者是对表进行compact的时候,StoreFiles将会被重写,这些Region也会再次变成RegionServer的“local”Region。 有一个相关的指标“data locality”,即Region保存在本地的StoreFile的百分比。这个指标影响了major compact的执行。 #########
[阅读更多...] -
使用HBase总结
前段时间我们在项目中使用了HBase,在这里记一下使用经历或者说踩过的坑。 RowKey设计 我们读取数据的方式主要是批量查询,因此在最初的设计中就将大部分查询字段放在了RowKey上,目的是利用RowKey作为索引的特性。 关于RowKey的设计,通常有要求唯一性、散列性以及尽量短。我们在设计RowKey时也尽量地按照这三个原则去做。为了保证唯一性,也想过直接将一个32个字符的原始记录ID(UUID)放到RowKey里,但这样必然会导致行键特别长,所以我们选择使用“查询字段(3个)+服务器标记+机器时间(秒级)+顺序号”这样的行键。为了进一步压缩RowKey,又对“机器时间+顺序号”的数字组合做了32进制的换算。加上为了实现预分区而添加的分区提示符,最终得到的RowKey方案是“分区提示符+查询字段+服务器标记+机器时间(秒级)+顺序号”这样的组合,总长度是31位。 不过这样的设计是存在一个弊端的:因为没有将原始记录ID添加到RowKey里,所以无法在重写数据时保证唯一性,只好在查询完成后再做排重处理。如果直接使用原始记录ID作为RowKey,就无法再利用RowKey作为索引的能力。曾经考虑过再新建一个表专门用来维护RowKey和原始记录ID的关系,也就是作为二级索引。可是这样增加了写入数据的复杂度,后来就放弃了。 另一个问题就是这样设计出来的RowKey仍然是太长了,间接导致了后来的一个非常严重的问题。 再补充一点:假设查询字段为“用户ID+事件时间+标签ID”的组合。因为数据老化的速度比较快,可以考虑将第二个查询字段设置为(Long.MAX_VALUE – 事件时间)。因为RowKey是按字典顺序排序,这样做可以使同一用户的最新记录排在前面。 建表 我们的数据的特点是量非常大,老化速度比较快,需要长期保存。根据这个特点我们进行了按月分表。并在建表的同时做了预分区。 关于预分区,如何选择SplitKey是一个问题。根据一开始的RowKey设计方案无法计算出SplitKey,所以又在RowKey前面添了一个字符作为分区引导符(如1-9,A-Z,a-z)。分区引导符是用原始记录ID的hashcode做求余运算后得到的数值(注意:这样并不能做到很好的散列)。SplitKey自然就是这些分区引导符了。然而很快就发现使用一个字符做分区引导符有点不够用,在运行了一天后,开始出现了新的SplitKey,只好又为分区引导符添加了一个字符。 如果分区数量持续增加到两个字符的分区引导符也不够用的情况,就考虑将两个字符换成一个short型的值(最大为32767),虽然不是可视字符但能省下一些空间。 此外,为了减少空间占用,在建表时对所有列族设置了SNAPPY压缩。 列设计 现在使用的列设计方案是:将数据的字段分成三大类对应三个列族,列族名称是:cf1111,cf222222,cf33333(名称随手写的,但长度是对的)。而后每个字段作为一个qualifier,其中一个表有22个列(qualifier)。列名就是字段的原始名称。 看到这里有经验的朋友应该会指出问题了:列族和列的名称太长了。另外我这里的列也有些多,再加上RowKey的长度,然后想想HFile的结构——最终导致的问题是数据占用的空间膨胀了大约8倍(相对SequenceFile存储),如果再运行一个月就极有可能耗尽HBase的存储空间。 遇到这个问题后,开始重新考虑该如何设计了。我们的需求就是存储日志,并按要求查询导出日志。如果仅从这个需求出发,实在没必要分出这许多列族和列。更彻底一些的做法是:既然将查询条件都放到了RowKey上,存储数据的时候只需要使用单列族单列保存原始的记录即可。只不过这样的做法也太短视了一些。 写数据 为了提升写数据的效率,在写数据时做了如下操作: 关闭了auto flush,设置恰当的WriteBufferSize; 执行批量put; 做了预分区,避免写数据时产生split(split会造成region短暂的停止); 关闭定期major compact的功能,仅在必要的时候使用Java API手动执行major compact; 使用BulkLoad导入历史数据。 查询 为了提升查询效率做了如下事情: 优化RowKey设计,在查询时尽量做到面向行键查询,尽量利用StartRow和StopRow; 在scan时指明要查询的列; 将经常要一起查询的字段放到同一个列族里; 使用恰当的FIlter; 调大查询服务的xmx; 根据需求合理设置ScanCaching,通过这个参数决定client和HBase每次交互的数据量; 关闭BlockCache,我们的查询主要是用来导出数据,不存在反复查询某些记录的情况,所以可以考虑关闭BlockCache。 另外,在查询实现上做了一件极蠢的事情,就是设计实现了一个自定义Filter。不过这个自定义的Filter压根就没有发挥作用,因为不可能为了上传自定义Filter而重启生产环境的HBase。 ——— 好了,现就这样。以后想到了再继续写。 ##########
[阅读更多...] -
HBase Bulk Load
概述 BulkLoad是一种高效写入HBase的方式,适用于将数据批量迁移到HBase。 BulkLoad使用MapReduce作业直接生成HBase的StoreFile,并将生成的StoreFile直接装载入正在运行的HBase集群。较之使用HBase的API,使用BulkLoad耗费的CPU和网络资源都相对较少。 因为BulkLoad绕过了正常写数据的路径(WAL、MemStore、flush),尤其是WAL,通过WAL进行的Cluster Replication就不会处理BulkLoad装载的数据。这很像是调用HBase API时使用了Put.setDurability(SKIP_WAL)。一个解决方式是将原始文件或HFile移到Replication集群上再做其他处理。《Bulk Loaded HFile Replication》对这个问题做了讨论。 步骤 Bulk Load分成两步完成。 通过MapReduce作业准备数据 BulkLoad的第一步是在MapReduce作业中使用HFileOutputFormat2类生成HBase数据文件(StoreFile)。 为了使最终生成的每个HFile都能对应一个Region,需要在MapReduce作业中使用TotalOrderPartitioner类对map的输出结果进行partition,使之与Region的RowKey范围达到一致。幸运的是HFileOutputFormat2类的configureIncrementalLoad()已经做了这个工作,它会根据HBase表中现有的Region边界自动配置TotalOrderPartitioner。 载入数据到HBase集群 在准备好数据文件后,可以在命令行中使用completebulkload工具完成BulkLoad,命令如下: 也可以直接调用LoadIncrementalHFiles实例的doBulkLoad方法完成BulkLoad,: doBulkLoad方法也是completebulkload工具最终调用的方法。不同的是completebulkload工具会检查要写入的表是否存在,不存在的话会主动创建该表。直接调用doBulkLoad方法则需要手动做这些事情。 doBulkLoad方法会遍历MapReduce作业生成的每个数据文件,并决定将其分配给哪一个Region,随后联系接收数据的HRegionServer,将数据移动到HRegionServer上的存储目录,都做完后再通知client数据可用了。 如果在准备数据的时候或者是在装载数据到HBase集群的过程中,Region的边界发生了变化,LoadIncrementalHFiles会自动对数据文件进行split,并发送split后的文件到不同的Region。但是这样会影响导入数据的效率,尤其是在还有其他客户端同时写数据的时候。在执行BulkLoad的时候应当尽量避免这种情况发生。 ###########
[阅读更多...] -
HBase自定义Filter
必需要提前说明下:不建议使用自定义的Filter。所有的Filter都是在服务端生效:就是说需要将自定义的Filter封装为jar,上传到HBase的类路径下,并重启HBase使之生效。对于生产环境的HBase来说,重启通常是不能接受的。 Filter的设置是在客户端完成的,而Filter的逻辑是在HBase的服务端完成的,中间需要一次序列化。我试过几种序列化方案,不过protobuffer以外的其他几种效果不算好。HBase自带的Filter也是用protobuffer进行的序列化,因此使用protobuffer还可以少传几个包。 需要提前说明的已经说完了,开始进入正题。这次从一个案例开始说起:在HBase中存储着用户行为记录,行键设计为“uid(6位)+etime(时间戳/1000)+tid(7位)+顺序号(8位)”。其中uid为用户ID、etime为事件时间、tid为行为标签。目标是检索出某个用户在指定时间范围内的几种行为数据。 针对这个案例我们自定义一个CustomRowKeyFilter,并将一个用户ID、事件起止时间以及多个行为ID作为CustomRowKeyFilter的成员变量。 代码中继承了FilterBase类,可以减少一些结构性的代码工作。至于Filter是如何工作的,在网上找到的这张图应该描述得很清楚了: 前面的代码只是实现了Filter的处理逻辑。要想使用这个Filter还需要做一些序列化处理。如前面所说序列化方案选择的是protobuffer,这里需要先定义一个描述文件CustomRowKeyFilterProto.proto,内容如下: 定义完成后,执行protoc命令: 其中“-I”指定了proto描述文件的父目录, “—java_out”指定了java类的类路径,具体请根据自己的情况进行设置。执行命令后会在包com.zhyea.dev.hbase.filter.proto下生成序列化工具类CustomRowKeyFilterProto.java。 接下来在CustomRowKeyFilter中重写Filter类的toByteArray()方法和parseFrom()方法: 这样自定义Filter就完成了。剩下的事情就是将之打包并上传到HBase(每个RegionServer)的类路径下。然后就可以在程序中使用了。 现在再仔细想想这个程序,是否一定需要一个自定义Filter呢!我们已经将查询需要的所有元素都定义在行键里了。那么可以使用“uid+起始时间”作为startRow,“uid+结束时间”作为stopRow完成时间范围的匹配,使用RegexStringComparator来处理tid的匹配,这样直接使用HBase提供的RowFilter就能解决问题了。唯一需要注意的事情就是在设计表时多花些心思在行键上罢了。 就是这样。 参考文档 HBase Filter介绍及执行流程:https://my.oschina.net/cloudcoder/blog/289649
[阅读更多...] -
HBase RowKey设计
热点现象 HBase中的记录行按行键的字典顺序进行排序。这种设计有利于扫描(scan)记录。因此我们可以合理的设计行键,将相关的行或者需要一起读取的行放得靠近一些。不过设计得不好的行键也是热点现象的常见来源。当大量客户端流量指向集群中一个或少数几个节点时就容易产生热点现象。这里说的流量指的是读、写或其他操作。这些客户端流量可能会压倒负责托管某个区域(region)的机器,并因此导致性能下降甚至该区域的不可用。因为无法响应请求,这台主机上托管的其他region也可能会受到不良影响。因此如何设计数据访问模式以使集群全面均衡地发挥作用就很重要了。 为防止写数据的热点出现,就需要调整行键设计。原本有些数据非常需要写入同一个region,但是从更高的角度上来看,这些数据就应该分发到集群的多个region上,而非是一次性写入某一个region。下面会介绍一些常用的避免热点的方案,以及这些方案的优点和缺点。 盐化 这里说的盐与加密盐并无关系,只是将一串随机数添加到行键的头部。这里说的盐化指的是将一串随机分配的前缀添加到行键以使其按与原本不同的方式进行排序。不同前缀的数量应该和设计的region的数量一致。如果在均匀分布的行模式中反复出现了少量热点行模式,此时使用盐化的方式是很有用的。看一下下面的实例,在实例中演示了是如何使用盐化将数据分散到不同的regionServer中的,以及盐化对读取数据的影响。 假设我们有如下行键列表。存储记录的表按字母表给每个字母分配一个region,比如前缀“a”是一个region,前缀“b”就是另一个region。在表中所有以“f”开头的行键的记录就会被写入到相同的region中。在这个例子中我们有如下这样的一批行键: 现在我们想把这些行键对应的记录分发到不同的region中。我们将使用4个不同的盐a、b、c和d作为前缀。这样,包含盐前缀的记录就会被写到对应的region上。使用盐以后,我们会得到下面这样的行键。因为现在是将记录写到四个不同的region,相比只写入到一个region理论上会有四倍的吞吐量。 如果要添加一行新的记录,在现有的行键上随机填上四个盐中的一个作为前缀。 因为这种分配是随机的,同一条记录有可能会分配到不同的前缀,这样要想按字典顺序检索出记录就得做更多的工作。 使用盐化的方式扩大了写入的吞吐量,却也增加了读取数据的开销。 哈希 要替代随机分配盐的方案,可以使用单向哈希。这使用单向哈希每个行都会分配到一个固定的前缀。这样记录仍可以被写到不同的region上,不过在读的时候可以保证做到精确读取。使用确定性哈希允许客户端重建完整的行键并使用Get操作准确检索到记录。 仍然是前面盐化方案中的例子,可以使用单向哈希给“foo0003”这条记录始终分配一个可以预知的前缀“a”。这样在检索这一行记录的时候就可以就可以准确计算出行键。基于这个方案我们可以对行键做一些优化,比如让特定的一对行键总是被分配到相同的region中。 反转行键 第三个常见的防止热点的技巧是反转行键。也就是将固定宽度或数值类型的行键反转,这样行键中变化最频繁的部分(最低有效位)就会排到第一位。这有效地对行键进行了随机化,不过却牺牲了行排序属性。 单调递增行键或时间序列数据 在使用HBase时要警惕一个现象:所有的客户端集中锁定访问表的一个region(也就是hbase的一个节点),然后又全部移动向下一个region,就这样推进并形成循环。在使用单调递增的行键(比如使用时间戳)时就容易发生这样的情况。在BigTable类的数据存储中使用单调递增的行键是一个坏的选择。这里有一幅漫画描述了为什么单调递增是不好的。虽然可以采用随机化的方式打乱输入记录的排序顺序来减轻单调递增行键造成的单个region的访问热点的问题,但是通常也应尽量避免使用时间戳或者一个自增序列(比如1、2、3)作为行键。 如果确实需要上传时间序列数据到HBase,可以学习一个成功案例:OpenTSDB。这个项目有一个页面专门描述了它在HBase中使用的schema。OpenTSDB实际上使用的行键格式是:[metric_type][event_timestamp]。乍一看这和我们前面说的不建议使用时间戳作为行键是冲突的。差别就在于这里没有将时间戳置于行键的前导位置,并且在设计中假设存在几十几百个(或者更多)不同的metric_type。因此,尽管有不间断的输入数据流,相关的Puts操作也会被分发向不同的region。 尝试最小化行键和列的大小 在HBase中,每一个值都有与其对应的坐标。比如HBase系统中的一个cell value,它的坐标就是行键名、列名还有时间戳。如果行键名和列名很大——尤其是与cell value一起比较的时候——就可能会遇到一些有趣得情况。在HBASE-3551中Marc Limotte就描述了这样的一种情形。这里描述的问题是保存在HBase存储文件(HFile)上以便于随机访问的索引占用了大部分分配给HBase的RAM,原因就是cell value的坐标太大了。刚才引用的这个问题,在评论中有人建议调大blocksize,这样为HBase存储记录新建索引的时间间隔就会大一些,或者是修改表设计使用更小的列名和行键名。此外,压缩也能导致索引变大,在这里提到了一个相关的案例。 大多数时候这种小的低效率并没有大的影响。不幸的是它们确实有发生的契机,尤其是在需要访问几十亿条数据的时候。 行键长度 行键长度固然是越短越好,但是应尽量保证行键有意义,以便于对数据进行访问(包括Get和scan)。一个短的行键并不比一个虽然长却有利于scan或get的行键更好。因此在设计行键时需要反复权衡。 关于列族 列族名称应该尽量的小,最好是1个字母,比如使用d来替换default或data。 关于属性名称 尽管冗长的属性名称(比如:myVeryImportantAttribute)更容易理解,但是保存在HBase中的属性名称应该是越短越好(比如使用via)。 字节模式 一个长整型值的长度是8个字节。在这8个字节里我们可以保存一个最大为18,446,744,073,709,551,615的整数值。如果我们将这个整数值保存在字符串里——假设使用一个单词代表一个字节——我们将需要3倍的字节数。 不相信?下面是一段示例代码,可以自己运行一下试试看。 使用字节代表一个类型有一个缺点:将会使数据在代码以外的地方很难阅读。下面这个例子演示了在hbase shell中对一个值执行increase操作的结果: hbase shell会尽最大努力打印一个字符串。在这种情况下,它只会打印16进制的值。同样的情况也会发生在region内的行键上。通常如果知道正在写入的数据是什么是最好的,不过写入cell内的可以是任何内容,包括可读的和不可读的。这点在设计时需要仔细衡量。 反向时间戳 数据库中一个常见的问题是如何快速的找到一个值的最新版本。在某些特殊情况下使用反向时间戳技术作为行键的一部分可以很好地处理这个问题。反向时间戳即(Long.MAX_VALUE – timestamp)。通常是将反向时间戳追加在行键末尾,即[行键][reverse_timestamp]。这样,在通过一个原始的行键执行scan搜索时检索到的第一个值就是这个原始行键所对应的最新的值。 使用反向时间戳替换HBase的Versions管理的目的是永久保留一些值的所有版本,并在使用相同的Scan方案的时候仍然能够保持快速访问其它任何版本的能力。 行键和列族 行键的作用域是列族。因此一个表中的列族可以有相同的行键而不冲突。 行键的不变性 行键是不可变的。一个表中的行键可以被“改变”的方式就是将记录删除后再重新插入。 行键和region split的关系 如果要对表进行预分区,那么弄清楚region边界处的行键分布就很重要了。下面的例子说明了为什么这很重要,在实例中使用了可视化十六进制字符作为行键(比如”0000000000000000″ 到 “ffffffffffffffff”)。使用Bytes.split(这也是用Admin.createTable(byte[] startKey, byte[] endKey, numRegions)方法创建分区时使用的split策略)处理这个范围内的行键,计划创建10个分区,目前有如下的splits数组作为splitKey: (注意:右侧的注释说明了前导字节的值)。如上面所示的,第一个split字节是字符‘0’,最后一个split字节是字符‘f’。看起来就是这样,没毛病,是不是?先别着急下定论。 问题是所有的数据将会堆积在前两个region以及最后一个region处,并因此产生lumpy region(也有可能是hot region)问题。要理解为什么,可以先参考ASCII表。字符‘0’的byte值是48,字符‘f’的byte值是102。但是在字节值中有一个巨大的间隙(字节58 到 96)永远不会出现在行键空间中,因为有效值是[0-9]和[a-f]。因此中间部分的region将永远不会出现。要使示例的行键设计在预分区工作中生效,需要使用一个自定义的split策略(而不仅仅是依赖内置的split方法)。 从刚才描述的问题,我们能够获得如下两条经验: 经验一:预分区通常是一个不错的方案,但是需要确保预分区得到的所有regions是对行键可达的。在这个例子里演示了使用十六进制字符作为行键前缀造成的问题,类似的问题也可能发生在其他类型的行键设计上。因此首先要做的一件事情就是充分了解要写入HBase的数据。 经验二:尽管通常不建议,使用十六进制字节(通常建议使用可视化数据)作为预分区表的行键前缀也是可行的——只要创建的所有分区对于设计的行键是可达的即可。 为了好好结束这个例子,下面提供了一种适合十六进制行键的预分区split生成方案: 参考文档 Configuring The Blocksize For HBase ##################
[阅读更多...]