下面将介绍 HBase 的一些典型数据摄取用例,以及如何处理 rowkey 设计和构造。注意:这只是潜在方法的说明,而不是详尽的清单。了解您的数据,了解您的处理要求。
在阅读这些案例研究之前,强烈建议您先阅读 HBase 和 Schema Design 的其余部分。
描述了以下案例研究:
记录数据/时间序列数据
在类固醇上记录数据/时间序列
顾客订单
高/宽/中图式设计
列表数据
假设正在收集以下数据元素。
主机名
时间戳
记录事件
值/消息
我们可以将它们存储在名为 LOG_DATA 的 HBase 表中,但是 rowkey 是什么?从这些属性中,rowkey 将是 hostname,timestamp 和 log-event 的某种组合 - 但具体是什么?
rowkey [timestamp][hostname][log-event]
遭受单调递增行键/时间序列数据中描述的单调递增的 rowkey 问题。
通过对时间戳执行 mod 操作,在 dist-lists 中经常提到的关于“bucketing”时间戳的另一种模式。如果面向时间的扫描很重要,这可能是一种有用的方法。必须注意桶的数量,因为这将需要相同数量的扫描才能返回结果。
long bucket = timestamp % numBuckets;
构建:
[bucket][timestamp][hostname][log-event]
如上所述,要选择特定时间范围的数据,需要为每个存储桶执行扫描。例如,100 个存储桶将在密钥空间中提供广泛的分布,但是需要 100 个扫描才能获得单个时间戳的数据,因此需要权衡利弊。
如果存在大量主机以在密钥空间上传播写入和读取,则 rowkey [hostname][log-event][timestamp]
是候选者。如果按主机名扫描是优先事项,则此方法很有用。
如果最重要的访问路径是拉取最近的事件,那么将时间戳存储为反向时间戳(例如,timestamp = Long.MAX_VALUE – timestamp
)将创建能够在[hostname][log-event]
上执行扫描以获取最近捕获的事件的属性。
这两种方法都不对,它只取决于最适合这种情况的方法。
Reverse Scan AP
HBASE-4811 implements an API to scan a table or a range within a table in reverse, reducing the need to optimize your schema for forward or reverse scanning. This feature is available in HBase 0.98 and later. See Scan.setReversed() for more information.
重要的是要记住,在 HBase 的每一列上都标记了 rowkeys。如果主机名是a
且事件类型是e1
,则生成的 rowkey 将非常小。但是,如果摄取的主机名是myserver1.mycompany.com
而事件类型是com.package1.subpackage2.subsubpackage3.ImportantService
怎么办?
在 rowkey 中使用一些替换可能是有意义的。至少有两种方法:散列和数字。在 Rowkey Lead Position 示例中的 Hostname 中,它可能如下所示:
带哈希的复合 Rowkey:
[主机名的 MD5 哈希值] = 16 个字节
[事件类型的 MD5 哈希] = 16 个字节
[timestamp] = 8 个字节
具有数字替换的复合 Rowkey:
对于这种方法,除了 LOG_DATA 之外,还需要另一个查找表,称为 LOG_TYPES。 LOG_TYPES 的 rowkey 是:
[type]
(例如,表示主机名与事件类型的字节)
原始主机名或事件类型的[bytes]
可变长度字节。
此 rowkey 的列可以是带有指定编号的 long,可以通过使用 HBase 计数器获得
因此生成的复合 rowkey 将是:
[取代主机名长] = 8 个字节
[代替事件类型的长度] = 8 个字节
[timestamp] = 8 bytes
在散列或数字替换方法中,hostname 和 event-type 的原始值可以存储为列。
这实际上是 OpenTSDB 方法。 OpenTSDB 所做的是重写数据并将行打包到特定时间段的列中。有关详细说明,请参阅: http://opentsdb.net/schema.html 和来自 HBaseCon2012 的 OpenTSDB 的经验教训。
但这就是一般概念的工作原理:例如以这种方式摄取数据......
[hostname][log-event][timestamp1]
[hostname][log-event][timestamp2]
[hostname][log-event][timestamp3]
每个详细事件都有单独的 rowkeys,但是会像这样重写...
[hostname][log-event][timerange]
并且每个上述事件被转换成以相对于开始时间范围的时间偏移(例如,每 5 分钟)存储的列。这显然是一种非常先进的处理技术,但 HBase 使这成为可能。
假设 HBase 用于存储客户和订单信息。摄取了两种核心记录类型:客户记录类型和订单记录类型。
客户记录类型将包括您通常期望的所有内容:
顾客号码
顾客姓名
地址(例如,城市,州,邮编)
电话号码等
订单记录类型包括以下内容:
Customer number
订单号
销售日期
用于装运位置和项目的一系列嵌套对象(有关详细信息,请参阅订单对象设计)
假设客户编号和销售订单的组合唯一地标识订单,这两个属性将组成 rowkey,特别是组合键,例如:
[customer number][order number]
对于 ORDER 表。但是,还有更多的设计决策要做: raw 值是 rowkeys 的最佳选择吗?
日志数据用例中的相同设计问题在这里面对我们。什么是客户编号的密钥空间,以及格式是什么(例如,数字?字母数字?)因为在 HBase 中使用固定长度密钥是有利的,以及可以支持密钥空间中合理传播的密钥,类似选项出现:
Composite Rowkey With Hashes:
[客户编号的 MD5] = 16 个字节
[订货号的 MD5] = 16 个字节
复合数字/哈希组合 Rowkey:
[代替客户编号] = 8 个字节
[MD5 of order number] = 16 bytes
传统的设计方法将为 CUSTOMER 和 SALES 提供单独的表。另一种选择是将多种记录类型打包到一个表中(例如,CUSTOMER ++)。
客户记录类型 Rowkey:
[顾客 ID]
[type] =表示客户记录类型为“1”的类型
订单记录类型 Rowkey:
[customer-id]
[type] =表示订单记录类型的“2”的类型
[订购]
这种特殊的 CUSTOMER ++方法的优点是可以按客户 ID 组织许多不同的记录类型(例如,单次扫描可以获得有关该客户的所有信息)。缺点是扫描特定记录类型并不容易。
现在我们需要解决如何为 Order 对象建模。假设类结构如下:
订购
(订单可以有多个 ShippingLocations
的 LineItem
(ShippingLocation 可以有多个 LineItems
存储此数据有多种选择。
使用这种方法,ORDER,SHIPPING_LOCATION 和 LINE_ITEM 会有单独的表。
ORDER 表的 rowkey 如上所述: schema.casestudies.custorder
SHIPPING_LOCATION 的复合 rowkey 是这样的:
[order-rowkey]
[shipping location number]
(例如,第 1 位,第 2 位等)
LINE_ITEM 表的复合 rowkey 将是这样的:
[order-rowkey]
[shipping location number]
(e.g., 1st location, 2nd, etc.)
[line item number]
(例如,第 1 个 lineitem,第 2 个等)
这样的规范化模型很可能是使用 RDBMS 的方法,但这不是您使用 HBase 的唯一选择。这种方法的缺点是,要检索有关任何订单的信息,您将需要:
获取订单的 ORDER 表
在 SHIPPING_LOCATION 表上扫描该订单以获取 ShippingLocation 实例
在 LINE_ITEM 上扫描每个 ShippingLocation
无论如何,这就是 RDBMS 所做的事情,但由于 HBase 中没有连接,你只是更加意识到这一事实。
使用这种方法,将存在一个包含的单个表 ORDER
Order rowkey 如上所述: schema.casestudies.custorder
[order-rowkey]
[ORDER record type]
ShippingLocation 复合 rowkey 将是这样的:
[order-rowkey]
[SHIPPING record type]
[shipping location number]
(e.g., 1st location, 2nd, etc.)
LineItem 复合 rowkey 将是这样的:
[order-rowkey]
[LINE record type]
[shipping location number]
(e.g., 1st location, 2nd, etc.)
[line item number]
(e.g., 1st lineitem, 2nd, etc.)
具有记录类型的单表方法的变体是对一些对象层次结构进行非规范化和展平,例如将 ShippingLocation 属性折叠到每个 LineItem 实例上。
The LineItem composite rowkey would be something like this:
[order-rowkey]
[LINE record type]
[line item number]
(例如,第 1 个 lineitem,第 2 个等,必须注意整个订单中有唯一性)
而 LineItem 列将是这样的:
项目编号
数量
价钱
shipToLine1(从 ShippingLocation 非规范化)
shipToLine2(从 ShippingLocation 非规范化)
shipToCity(从 ShippingLocation 非规范化)
shipToState(从 ShippingLocation 非规范化)
shipToZip(从 ShippingLocation 非规范化)
这种方法的优点包括不太复杂的对象层次结构,但其中一个缺点是,如果任何此类信息发生更改,更新会变得更加复杂。
使用这种方法,整个 Order 对象图以某种方式被视为 BLOB。例如,ORDER 表的 rowkey 如上所述: schema.casestudies.custorder ,一个名为“order”的列将包含一个可以反序列化的对象,该对象包含容器 Order,ShippingLocations 和 LineItems。
这里有很多选项:JSON,XML,Java Serialization,Avro,Hadoop Writables 等。所有这些都是相同方法的变体:将对象图编码为字节数组。在对象模型发生变化的情况下,应该注意这种方法以确保向后兼容性,以便仍然可以从 HBase 中读回较旧的持久性结构。
专业人员能够以最少的 I / O 管理复杂的对象图(例如,在这个例子中,单个 HBase Get per Order),但是缺点包括前面提到的关于序列化的向后兼容性,序列化的语言依赖性的警告(例如,Java 序列化)只适用于 Java 客户端),事实上你必须反序列化整个对象以获取 BLOB 中的任何信息,并且难以获得像 Hive 这样的框架来处理像这样的自定义对象。
本节将介绍出现在 dist 列表中的其他架构设计问题,特别是有关高表和宽表的问题。这些是一般准则,而不是法律 - 每个应用程序必须考虑自己的需求。
一个常见的问题是,是否应该选择行或 HBase 的内置版本。上下文通常是要保留行的“很多”版本的地方(例如,它明显高于 HBase 默认的 1 个最大版本)。行方法需要在 rowkey 的某些部分中存储时间戳,以便它们不会在每次连续更新时覆盖。
偏好:行(一般来说)。
另一个常见问题是,是否应该更喜欢行或列。上下文通常在宽表的极端情况下,例如具有 1 行具有 100 万个属性,或者 1 百万行,每行 1 列。
偏好:行(一般来说)。需要明确的是,本指南在非常广泛的情况下,而不是在需要存储几十或一百列的标准用例中。但是这两个选项之间也有一条中间路径,那就是“Rows as Columns”。
行与列之间的中间路径是打包数据,对于某些行,这些数据将成为列中的单独行。 OpenTSDB 是这种情况的最佳示例,其中单行表示定义的时间范围,然后将离散事件视为列。这种方法通常更复杂,可能需要重新编写数据的额外复杂性,但具有 I / O 效率的优势。有关此方法的概述,请参见 schema.casestudies.log-steroids 。
以下是来自用户 dist-list 的关于一个相当常见的问题的交换:如何处理 Apache HBase 中的每用户列表数据。
我们正在研究如何在 HBase 中存储大量(每用户)列表数据,并且我们试图找出哪种访问模式最有意义。一种选择是将大部分数据存储在密钥中,因此我们可以使用以下内容:
<FixedWidthUserName><FixedWidthValueId1>:"" (no value)
<FixedWidthUserName><FixedWidthValueId2>:"" (no value)
<FixedWidthUserName><FixedWidthValueId3>:"" (no value)
我们的另一个选择是完全使用:
<FixedWidthUserName><FixedWidthPageNum0>:<FixedWidthLength><FixedIdNextPageNum><ValueId1><ValueId2><ValueId3>...
<FixedWidthUserName><FixedWidthPageNum1>:<FixedWidthLength><FixedIdNextPageNum><ValueId1><ValueId2><ValueId3>...
其中每行包含多个值。所以在一个案例中,读取前 30 个值将是:
scan { STARTROW => 'FixedWidthUsername' LIMIT => 30}
在第二种情况下,它将是
get 'FixedWidthUserName\x00\x00\x00\x00'
一般使用模式是只读取这些列表的前 30 个值,不经常访问读入更深入列表。有些用户在这些列表中总共有 30 个值,有些用户会有数百万(即幂律分布)
单值格式似乎会占用 HBase 上更多的空间,但会提供一些改进的检索/分页灵活性。是否可以通过获取与分页扫描分页来获得显着的性能优势?
我最初的理解是,如果我们的分页大小未知(并且适当地设置了缓存),那么扫描应该更快,但如果我们总是需要相同的页面大小,则应该更快。我最后听到不同的人告诉我有关表现的事情。我假设页面大小相对一致,因此对于大多数用例,我们可以保证在固定页面长度的情况下我们只需要一页数据。我还假设我们不经常更新,但可能会插入这些列表的中间(意味着我们需要更新所有后续行)。
感谢您的帮助/建议/后续问题。
如果我理解正确,你最终会尝试以“user,valueid,value”的形式存储三元组,对吧?例如,类似于:
"user123, firstname, Paul",
"user234, lastname, Smith"
(但用户名是固定宽度,valueids 是固定宽度)。
并且,您的访问模式如下:“对于用户 X,列出接下来的 30 个值,从 valueid Y 开始”。是对的吗?这些值应该按 valueid 返回?
tl; dr 版本是你可能应该为每个用户+值一行,而不是自己构建一个复杂的行内分页方案,除非你真的确定它是需要的。
您的两个选项反映了人们在设计 HBase 模式时遇到的常见问题:我应该“高”还是“宽”?您的第一个架构是“高”:每行代表一个用户的一个值,因此每个用户的表中有很多行;行键是 user + valueid,并且(可能)是单列限定符,表示“值”。如果你想按行排序按行排序扫描行(这是我上面的问题,关于这些 id 是否正确排序),这是很好的。您可以在任何用户+ valueid 处开始扫描,阅读下一个 30,然后完成。您放弃的是能够为一个用户的所有行提供事务保证,但听起来并不像您需要的那样。通常建议这样做(参见 https://hbase.apache.org/book.html#schema.smackdown )。
您的第二个选项是“宽”:您使用不同的限定符(其中限定符是 valueid)将一堆值存储在一行中。这样做的简单方法是将一个用户的所有值存储在一行中。我猜你跳到了“分页”版本,因为你假设在一行中存储数百万列对性能有害,这可能是也可能不是真的;只要你不是在单个请求中尝试做太多,或者做一些事情,比如扫描并返回行中的所有单元格,它就不应该从根本上变得更糟。客户端具有允许您获取特定切片列的方法。
请注意,这两种情况都不会从根本上占用更多的磁盘空间;您只是将标识信息的一部分“移动”到左侧(进入行键,选项 1)或右侧(进入选项 2 中的列限定符)。在封面下,每个键/值仍然存储整个行键和列族名称。 (如果这有点令人困惑,请花一个小时观看 Lars George 关于理解 HBase 架构设计的精彩视频: http://www.youtube.com/watch?v=_HLoH_PgrLk )。
正如您所注意到的那样,手动分页版本具有更多复杂性,例如必须跟踪每个页面中有多少内容,如果插入新值则重新进行混洗等。这看起来要复杂得多。它可能在极高的吞吐量下具有一些轻微的速度优势(或缺点!),并且真正了解它的唯一方法是尝试它。如果你没有时间来构建它并进行比较,我的建议是从最简单的选项开始(每个用户一行+值)。开始简单并迭代! :)