架构设计最佳做法
本页面包含有关 Bigtable 架构设计的信息。在阅读本页面内容之前,您应先熟悉 Bigtable 概览。本页面包括以下主题:
一般概念
Bigtable 架构设计与关系型数据库架构设计差异很大。Bigtable 架构由应用逻辑定义,而不是由架构定义对象或文件定义。您可以在创建或更新表时向表中添加列族,但列和行键模式由您写入表中的数据定义。
在 Bigtable 中,架构是表的蓝图或模型,包括以下表组件的结构:
- 行键
- 列族,包括其垃圾回收政策
- 列
在 Bigtable 中,架构设计主要由您计划发送到表中的查询或读取请求驱动。由于读取行范围是读取 Bigtable 数据的最快方式,因此本页中的建议旨在帮助您针对行范围读取进行优化。在大多数情况下,这意味着发送基于行键前缀的查询。
次要考虑因素是避免热点。为防止热点,您需要考虑写入模式以及如何避免在短时间内访问小键空间。
以下一般概念适用于 Bigtable 架构设计:
- Bigtable 是键/值对存储区,而不是关系存储区。它不支持联接,并且事务仅在单个行内受支持。
- 每个表只有一个索引(即行键)。没有二级索引。每个行键必须是唯一的。
- 行按行键的字典顺序排列,即从最低字节字符串到最高字节字符串。行键以 big-endian 字节顺序(有时称为网络字节顺序)排序,这是字母顺序的二进制版本。
- 列族不以任何特定顺序存储。
- 列按列族分组,并在列族中按字典顺序排序。例如,在名为
SysMonitor
的列族中,列限定符为ProcessName
、User
、%CPU
、ID
、Memory
、DiskRead
和Priority
,Bigtable 按以下顺序存储各列:
SysMonitor | ||||||
---|---|---|---|---|---|---|
%CPU | DiskRead | ID | 内存 | 优先级 | ProcessName | 用户 |
- 一行与一列的交叉可以包含多个带时间戳的单元。每个单元包含相应行和列的带时间戳的唯一版本数据。
- 汇总列族包含汇总单元格。您可以创建仅包含汇总单元的列族。借助汇总函数,您可以将新数据与单元格中已有的数据合并。
- 所有操作在行级层都是原子化的。操作要么影响整行,要么不影响行的任何部分。
- 理想情况下,读取和写入的数据都应均匀分布于表行空间中。
- Bigtable 表属于稀疏表。列在不使用该列的行中不占用任何空间。
最佳实践
一个设计良好的架构会带来出色的性能和可扩缩性,而一个设计不良的架构则可能会导致系统性能不佳。每个使用场景都是不同的,并且需要一些独特的设计,但以下最佳做法适用于大多数使用场景。例外情况也会加以说明。
以下各部分介绍架构设计的最佳做法,从表级别开始一直到行键级别:
在设计所有表元素(尤其是行键)时都应将计划的读取请求考虑在内。请查看配额和限制,了解所有表元素的建议大小和硬性限制。
由于实例中的所有表都存储在相同的数据片中,因此导致一个表中出现热点问题的架构设计可能也会影响同一实例中其他表的延迟表现。 热点是由于在短时间内频繁访问表的一部分而引起的。
Tables
将架构类似的数据集存储在同一个表中,而不是不同的表中。
在其他数据库系统中,您可能会根据主题和列数选择将数据存储在多个表中。但在 Bigtable 中,通常最好将所有数据存储在一个表中。您可以为每个数据集指定一个唯一的行键前缀,以便 Bigtable 将相关数据存储在连续的行中,然后您可以按行键前缀查询。
Bigtable 将每个实例的表数量限制为 1,000 个,但我们建议您避免创建大量表,原因如下:
- 向许多不同表发送请求会增加后端连接开销,导致尾延迟增加。
- 创建更多表不会改善负载均衡,反而会增加管理开销。
您可能有充分的理由使用单独的表来处理需要不同架构的不同应用场景,但不应对类似的数据使用单独的表。例如,您不应只是为了新的一年或新的客户而创建新表。
列族
将相关列放入同一列族中。如果一个行包含多个彼此相关的值,那么最好将包含这些值的列分组到同一列族中。尽可能将数据紧密地分组,以避免需要设计复杂的过滤条件,这样您就可以通过您最常用的读取请求来仅获取所需的信息。
每个表最多创建大约 100 个列族。 创建 100 个以上的列族可能会导致性能下降。
为列族选择简短名称。名称包含在为每个请求转移的数据中。
将具有不同数据保留需求的列放入不同列族中。如果您想限制存储费用,这一做法很重要。垃圾回收政策是在列族级(而不是列级)设置的。例如,如果您只需保留特定部分数据的最新版本,请不要将其存储在设置为存储数据的 1,000 个版本的列族中。否则,您需要为存储您不需要的 999 个单元的数据付费。
Columns
在表中创建您需要的任何数量的列。Bigtable 表属于稀疏表,行中未使用的列不会占用任何空间。只要没有行超过每行 256 MB 的最大限制,您就可以在表中拥有数百万列。
避免在同一行中使用过多列。虽然表可以有数百万个列,但行不应该这样。这一最佳实践的因素如下:
- Bigtable 需要一些时间来处理一行中的每个单元格。
- 每个单元会对存储在表中并通过网络发送的数据量增加一些开销。例如,如果您要存储 1 KB(1,024 字节)的数据,则将这些数据存储在一个单元中,要比起将数据分布在 1,024 个均包含一个字节的单元中更具空间效率。
如果数据集在逻辑上每行需要的列数太多,导致 Bigtable 无法高效处理,请考虑在一列中将数据存储为 protobuf。
您可以视需要将列限定符视为数据。由于您必须为每个列存储列限定符,因此可以通过使用值命名列来节省空间。例如,假设一个表将有关好友关系的数据存储在 Friends
列族中。每一行代表一个人及其所有好友关系。每个列限定符可以是好友的 ID。然后,该行中每列的值可以是好友所在的社交圈子。在本示例中,行可能如下所示:
行键 | Fred | Gabriel | Hiroshi | Seo Yoon | Jakob |
---|---|---|---|---|---|
Jose | book-club | 工作 | 网球 | ||
索非亚 | 工作 | 学校 | chess-club |
将此架构与存储相同数据的架构进行比较,后者不会将列限定符视为数据,而是在每行中具有相同的列:
行键 | 朋友 | 圆形 |
---|---|---|
Jose#1 | Fred | book-club |
Jose#2 | Gabriel | 工作 |
Jose#3 | Hiroshi | 网球 |
Sofia#1 | Hiroshi | 工作 |
Sofia#2 | Seo Yoon | 学校 |
Sofia#3 | Jakob | chess-club |
第二种架构设计会使表增长得非常快。
如果您使用列限定符来存储数据,请使用简短而有意义的列限定符名称。此方法可减少为每个请求传输的数据量。最大大小为 16 KB。
行
将单行中的所有值的大小保持在 100 MB 以下。确保单行中的数据不超过 256 MB。超出此限制的行可能会导致读取性能降低。
将一个实体的所有信息保存在一行中。对于大多数应用场景,请避免将必须以原子方式读取或一次性全部读取的数据存储在多个行中,以避免不一致。例如,如果您对表中的两个行进行更新,那么有可能其中一行成功更新,而另一行的更新失败。请确保架构不需要同时更新多个行,以保证相关数据准确无误。此做法可以确保如果写入请求部分失败或必须再次发送,这部分数据不会处于暂时不完整状态。
例外情况:如果将一个实体保存在一行中会使行大小为数百 MB,则应该将数据拆分为多行。
将相关实体存储在相邻行中,以提高读取效率。
Cells
不要将 10 MB 以上的数据存储在一个单元中。回想一下,单元是为具有唯一时间戳的给定行和列存储的数据,并且该行和列的交叉处可以存储多个单元。一个列中保留的单元数量取决于您为包含该列的列族设置的垃圾回收政策。
使用汇总单元格存储和更新汇总数据。如果您只关心实体事件的汇总价值(例如零售店每位员工的月销售额总和),则可以使用汇总。如需了解详情,请参阅在写入时汇总值。
行键
根据您将用于检索数据的查询设计行键。设计良好的行键可让 Bigtable 发挥最佳性能。最高效的 Bigtable 查询基于以下元素之一来检索数据:
- 行键
- 行键前缀
- 通过开始和结束行键定义的行范围
其他类型的查询会触发全表扫描,使效率显著降低。一开始就选择正确的行键,就可以避免日后进行痛苦的数据迁移。
使用较短的行键。行键不得超过 4 KB。冗长的行键会占用额外的内存和存储空间,而且还会增加从 Bigtable 服务器获取响应所需的时间。
在每个行键中存储多个分隔的值。高效查询 Bigtable 的最佳方法是使用行键,因此在行键中包含多个标识符通常会很有用。如果您的行键包含多个值,清楚自己如何使用数据尤为重要。
行键片段通常由分隔符(例如冒号、斜杠或井号)分隔。第一个片段或一组连续的片段是行键前缀,而最后一个片段或一组连续的片段是行键后缀。
有了精心设计的行键前缀,您将可以利用 Bigtable 内置的排序顺序将相关数据存储在一系列连续行中。如此一来,您便可通过一定范围的行来访问相关数据,而无需运行低效的表扫描。
如果您的数据包含要按数字顺序存储或排序的整数,请用前导零填充整数。Bigtable 以字典顺序存储数据。例如,按字典顺序,3 > 20,但 20 > 03。用前导零填充 3,可确保按数字顺序对数字进行排序。在使用基于范围的查询时,此策略对时间戳十分重要。
务必创建使查询能够检索明确定义的行范围的行键。否则,您的查询需要执行表扫描,而此操作要比检索特定行的速度慢得多。
例如,如果您的应用跟踪移动设备数据,则您可以创建由设备类型、设备 ID 以及记录数据的日期组成的行键。该数据的行键可能如下所示:
phone#4c410523#20200501
phone#4c410523#20200502
tablet#a0b81f74#20200501
tablet#a0b81f74#20200502
通过此行键设计,您可以使用单个请求根据以下元素检索数据:
- 设备类型
- 设备类型和设备 ID 的组合
如果您想要检索某个给定日期的所有数据,则此行键设计不是最佳选择。由于日期存储在第三个片段或行键后缀中,您无法根据行键的后缀或中间片段来仅请求某个范围内的行。您必须发送具有过滤条件的读取请求,该请求将扫描整个表来查找日期值。
尽可能在行键中使用直观易懂的字符串值。这样,您就可以使用 Key Visualizer 工具更轻松地排查 Bigtable 问题。
通常,您应该设计以泛化值开头并以细分值结尾的行键。例如,如果行键包含大洲、国家/地区和城市,则您可以创建如下所示的行键,以便先按照基数较低的值自动进行排序:
asia#india#bangalore
asia#india#mumbai
asia#japan#osaka
asia#japan#sapporo
southamerica#bolivia#cochabamba
southamerica#bolivia#lapaz
southamerica#chile#santiago
southamerica#chile#temuco
需避免使用的行键
某些类型的行键可能会使数据查询变得困难或导致性能不良。本部分介绍在 Bigtable 中应避免使用的一些行键类型。
以时间戳开头的行键。此模式会导致顺序写入被推送到单个节点上,从而形成热点。如果要将时间戳放在行键中,请在它前面加上高基数值(如用户 ID),以避免热点问题。
导致相关数据不分组到一起的行键。避免使用将相关数据存储在非连续的行范围中的行键,因为这样会使同时读取这些数据的效率很低。
顺序数字 ID假设您的系统为应用的每个用户都分配了一个数字 ID。您可能想要使用用户的数字 ID 作为表的行键。但是,由于新用户更可能成为活跃用户,因此这种方法可能会将大部分流量推送到少数节点。
一种更安全的方法是使用用户数字 ID 的反向版本,这样可以将流量更均匀地分布到 Bigtable 表的所有节点中。
频繁更新的标识符。避免使用单个行键来标识必须频繁更新的值。例如,如果您为多个设备每秒存储一次内存用量数据,请不要为每个设备使用由设备 ID 和要存储的指标组成的单一行键(如 4c410523#memusage
),并重复更新该行。这类操作会使存储常用行的片发生过载,也可能导致行超过其大小限制,因为列的先前值会占用空间,直到垃圾回收期间移除单元。
您应该将每次新读取存储在一个新的行中。如果以内存用量为例,每个行键可以包含设备 ID、指标类型和时间戳,因此行键类似于 4c410523#memusage#1423523569918
。这种策略非常高效,因为在 Bigtable 中,创建新行不会比创建新单元花费更多的时间。此外,该策略可以计算适当的开始键和结束键,可让您快速读取特定日期范围内的数据。
对于变化频繁的值(例如每分钟更新数百次的计数器),最好将数据保留在应用层的内存中,并定期向 Bigtable 写入新行。
哈希值。对行键进行哈希处理后,您就无法再利用 Bigtable 的自然排序顺序,也就无法以最适合查询的方式存储行。出于同样的原因,哈希值使得使用 Key Visualizer 工具排查 Bigtable 问题变得困难。使用直观易懂的值,而不是经过哈希处理的行键。
以原始字节表示的值(而非直观易懂的字符串)。原始字节对于列值没有问题,但为了方便阅读和问题排查,请在行键中使用字符串值。
特殊使用场景
您可能有一个独特的数据集,在设计架构以将其存储在 Bigtable 中时,需要进行一些特别的考虑。本部分介绍部分(而非全部)不同类型的 Bigtable 数据,以及以最佳方式存储此类数据的一些建议策略。
基于时间的数据
如果您经常根据数据的记录时间来检索数据,则可以将时间戳纳入行键中。
例如,您的应用可能会每秒记录一次许多机器的性能相关数据(如 CPU 和内存用量)。对于这种数据,您可以组合使用机器的标识符与数据的时间戳作为行键(例如 machine_4223421#1425330757685
)。请记住,行键按字典顺序排序。
如果您要在行键中添加时间戳,请勿仅使用时间戳,也不要将时间戳放在行键的开头。此模式会导致顺序写入被推送到单个节点上,从而形成热点。
如果您通常在查询中先检索最近的记录,则可以考虑在行键中使用倒序时间戳。这种模式会导致行按从最近到最早的顺序排列,因此表格中的最新数据位于前面。与任何时间戳一样,请避免以反向时间戳开头来构建行键,以免造成热点。
您可以从所用编程语言的最大长整数值(在 Java 中为 java.lang.Long.MAX_VALUE
)中减去相应时间戳,以获取倒序时间戳。
如需专门了解如何使用时序数据,请参阅时序数据的架构设计。
多租户
行键前缀为“多租户”使用场景提供了可扩缩的解决方案,让您可以代表多位客户使用相同的数据模型来存储类似的数据。对所有租户使用同一个表是存储和访问多租户数据的最高效方式。
例如,假设您代表多个公司存储和跟踪交易记录。您可以使用每家公司的专属 ID 作为行键前缀。一个租户的所有数据都会存储在同一个表的连续行中,您可以使用行键前缀进行查询或过滤。之后,如果某家公司不再是您的客户,并且您需要删除为该公司存储的交易记录数据,您可以丢弃使用该客户的行键前缀的一系列行。
例如,如果您要为客户 altostrat
和 examplepetstore
存储移动设备数据,则可以创建如下行键。此后,如果 altostrat
不再是您的客户,您可以删除所有行键前缀为 altostrat
行。
altostrat#phone#4c410523#20190501
altostrat#phone#4c410523#20190502
altostrat#tablet#a0b41f74#20190501
examplepetstore#phone#4c410523#20190502
examplepetstore#tablet#a6b81f79#20190501
examplepetstore#tablet#a0b81f79#20190502
相比之下,如果将代表每家公司的数据存储在其各自的表中,那么您将有可能遇到性能和可扩缩性问题,也更有可能会无意中达到 Bigtable 的限制(每个实例 1000 个表)。当某个实例达到此限制后,Bigtable 会阻止您在该实例中创建更多表。
隐私权
请避免在行键或列族 ID 中使用个人身份信息 (PII) 或用户数据,除非您的应用场景有此要求。行键和列族中的值都是客户数据和服务数据,使用这些数据的应用(例如加密或日志记录)可能会无意中将其暴露给无权访问私有数据的用户。
如需详细了解服务数据的处理方式,请参阅 Google Cloud 隐私权声明。
域名
您可以将域名存储为 Bigtable 数据。
众多域名
如果您要存储的实体相关数据可以用域名来表示,请考虑使用反向域名(例如 com.company.product
)作为行键。如果每行的数据很可能与相邻行重叠,使用反向域名就特别理想。在这种情况下,Bigtable 可以更有效地压缩您的数据。
相反,未反向的标准域名会使系统在排序行时无法将相关数据分组在一起,从而导致压缩和读取效率低下。
当您的数据分布在许多不同的反向域名中时,此方法效果最佳。
为了说明这一点,请考虑以下域名,这些域名由 Bigtable 自动按字典顺序排序:
drive.google.com
en.wikipedia.org
maps.google.com
如果您要查询所有的 google.com
行,这种做法就不太可取。作为对比,将相同的行反向显示:
com.google.drive
com.google.maps
org.wikipedia.en
在第二个示例中,对相关行自动排序后,就可以轻松地在某个行范围内检索它们。
少量域名
如果您只需要存储一个或少量域名的大量数据,请考虑为行键使用其他值。否则,写入数据可能会被推送到集群中的单个节点,从而导致热点或使相关行变得过大。
变化或不确定的查询
如果您并非总是对数据运行相同的查询,或者不确定查询将是什么情况,则可以将一行中的所有数据存储在一个而不是多个列中。如果采用这种方法,您将使用可以在稍后更轻松地提取各个值的格式,例如协议缓冲区二进制格式或 JSON 文件。
您仍然需要精心设计行键以确保可以检索所需的数据,但每行通常只有一个列,所有数据包含在一个 protobuf 中。
将数据以 protobuf 消息形式存储在一列,而不是将数据分布到多个列中,这种做法有优点也有缺点。优点包括:
- 数据占用的空间较少,节省存储费用。
- 无需考虑列族和列限定符,具有一定的灵活性。
- 读取应用不需要“知道”表架构。
缺点包括:
- 从 Bigtable 读取 protobuf 消息后,必须进行反序列化。
- 无法使用过滤条件查询 protobuf 消息中的数据。
- 从 Bigtable 读取 protobuf 消息后,无法使用 BigQuery 对其中的字段运行联合查询。