必需要提前说明下:不建议使用自定义的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自定义Filter
-
通过HA访问Hdfs获取ActiveNode
通过HA访问Hdfs的时候如何获取到活跃节点是一个稍稍有些麻烦的事情。 目前使用过两种方案:一是通过webhdfs接口逐一访问测试,找到状态为可用的节点;一是在zookeeper上直接获取当前活跃的节点。 简单说下第二种方案。ha的ActiveNode在zookeeper上的存储节点为:/hadoop-ha/dcnameservice/ActiveStandbyElectorLock。只需要通过ZooKeeper的API监听获取这个节点的信息即可。不过这个节点保存的信息不能当做字符串来读取,它是一个序列化后的对象,需要反序列化才能使用。 实现的代码如下: 就这样。
[阅读更多...] -
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 ##################
[阅读更多...] -
魔术师or建筑师
前段时间(额,至少是六个月前)接手了一个应用。看了一圈代码心里满是郁闷。应用要处理的事情很简单,但是代码一点儿也不简单。给人的感觉就是为了要使用java8的一个特性而生生将程序扭曲成了一个奇怪的东西。当时就有心吐槽一番,不过拖延症发作才等到了今天。这也是一件好事情,因为在这段时间里多少经过了一些思考和反思——满是负面情绪吐槽只是一时痛快了,思考和反思却是对以后有益的事情。 回到问题上来。可以将一个开发者设计开发应用的心态分成两种:魔术师和建筑师。魔术师是想法最多的那一批人,他们会搬出各种令人眼花缭乱的技术,随口说出各种专业术语,拿出的方案有各种理论的支持。建筑师则相对保守,他们通常是从曾经使用的方案上衍生出方案,没有新意却有经验的支撑。需要在这两者间选择的时候该怎么办呢?可以从三个方面考虑:目标、一致性还有简洁性。 在设计和开发的时候,至少要思量两个目标:怎样完成任务需求以及这个任务对自己有哪些提升?这两个目标一个是对职业的负责、一个是对自己的负责。对职业负责,就是尽量将一切做好,做到问心无愧。对自己负责就是在工作中学习更多的东西、掌握更多的技术、开拓自己的人脉。最好的做法当然是将这两个目标拧到一个方向上。最不好的做法就是一心偷懒敷衍任务。还有一种做法是将任务做成一块试验田,试验田的意思就是更重视过程而不计收成。魔术师最容易犯这个毛病,虽然有时候不是有意的,但是经常会走到为技术而开发的路子上。 在团队开发中需要考虑到一致性。这里的一致性指的是在使用的技术、方案以致编码风格上做到统一。这样做有如下好处:降低风险,减少学习成本和沟通成本。如果不是有明确的需要,就不要打破这种一致性。建筑师很容易加入到一致的队伍中。魔术师则可能会对这种一致性产生反感。 要避免和魔术师的冲突只需要用事实说话。这里有一种思路:列出应用中需要商榷的部分,逐一进行验证,应该保留的就保留,可以简化的就简化、可以删除的就删除。这样最后剩下来的部分就是最该留下来的部分。最后保留的部分也成了一致性的构成。 这里说的魔术师和建筑师的衡量是从技术追求和业务需求两个方向进行的衡量。可能是因为之前想要吐槽的缘故,所以前面多少显得对技术追求型做法有些不认同。不过作为开发就得凭技术说话,对新技术的学习永远也不该停止。但是否要以及怎样将一项技术引入到工作中,就得全面考虑了。 就这样。
[阅读更多...]