设计您的架构

本页面包含有关 Cloud Bigtable 架构设计的信息。在阅读本页内容之前,您应先熟悉 Bigtable 概览。本页面包括以下主题:

  • 一般概念:设计架构时应牢记的基本概念。
  • 最佳做法:适用于大多数使用场景的设计准则,按表组件细分。
  • 特殊使用场景:针对某些特定使用场景和数据模式的建议。

一般概念

Bigtable 架构设计与关系型数据库架构设计差异很大。在 Bigtable 中,架构是表的蓝图或模型,包括以下表组件的结构:

  • 行键
  • 列族,包括其垃圾回收政策

以下一般概念适用于 Bigtable 架构设计:

  • Bigtable 是键/值对存储区,而不是关系存储区。它不支持联接,并且事务仅在单个行内受支持。
  • 每个表只有一个索引(即行键)。没有二级索引。 每个行键必须是唯一的。
  • 行按行键的字典顺序排列,即从最低字节字符串到最高字节字符串。行键以 big-endian 字节顺序(有时称为网络字节顺序)排序,这是字母顺序的二进制版本。
  • 列族不以任何特定顺序存储
  • 列按列族分组,并在列族中按字典顺序排序。例如,在名为 SysMonitor 的列族中,列限定符为 ProcessNameUser%CPUIDMemoryDiskReadPriority,Bigtable 按以下顺序存储各列:
SysMonitor
%CPU DiskRead ID 内存 优先级 ProcessName 用户
  • 一行与一列的交叉可以包含多个带时间戳的单元。每个单元包含相应行和列的带时间戳的唯一版本数据。
  • 所有操作在行级层都是原子化的。这意味着,操作要么影响整行,要么不影响行的任何部分。
  • 理想情况下,读取和写入操作都应均匀分布于表行空间中。

  • Bigtable 表属于稀疏表。列在不使用该列的行中不占用任何空间。

最佳做法

一个设计良好的架构会带来出色的性能和可扩缩性,而一个设计不良的架构则可能会导致系统性能不佳。每个使用场景都是不同的,并且需要一些独特的设计,但以下最佳做法适用于大多数使用场景。例外情况也会加以说明。

以下各部分介绍架构设计的最佳做法,从表级别开始一直到行键级别:

在设计所有表元素(尤其是行键)时都应将计划的读取请求考虑在内。请查看配额和限制,了解所有表元素的建议大小和硬性限制。

将具有相同架构的数据集存储在同一个表中,而不是不同的表中。

在其他数据库系统中,您可能会根据主题和列数选择将数据存储在多个表中。但在 Bigtable 中,通常最好将所有数据存储在一个大型表中。您可以为每个数据集指定一个唯一的行键前缀,以便 Bigtable 将相关数据存储在连续的行中,然后您可以按行键前缀查询。

Bigtable 将每个实例的表数量限制为 1000 个,但在大多数情况下,需要使用的表数量应该远远低于这一限制。在 Bigtable 中,创建许多小型表属于一项反模式操作,原因如下:

  • 向许多不同表发送请求会增加后端连接开销,导致尾延迟增加。
  • 拥有多个不同大小的表可能会破坏后台负载平衡,使得 Bigtable 无法良好地运行。

您可能有充分的理由使用另外的表来处理需要完全不同架构的不同用例,但不为对类似的数据使用各自的表。例如,您不应只是为了新的一年或新的客户而创建新表。

列族

将相关列放入同一列族中。如果一个行包含多个彼此相关的值,那么最好将包含这些值的列分组到同一列族中。尽可能将数据紧密地分组,以避免需要设计复杂的过滤条件,这样您就可以通过您最常用的读取请求来仅获取所需的信息。

每个表最多创建大约 100 个列族。 创建 100 个以上的列族可能会导致性能下降。

为列族选择简短但有意义的名称。名称包含在为每个请求转移的数据中。

将具有不同数据保留需求的列放入不同列族中。如果您想限制存储费用,这一点很重要。垃圾回收政策是在列族级(而不是列级)设置的。例如,如果您只需保留特定数据的最新版本,请不要将其存储在设置为存储数据的 1000 个版本的列族中。否则,您将需要为存储您不需要的 999 个单元的数据付费。

将列限定符视为数据。由于您必须为每个列存储列限定符,因此可以通过使用值命名列来节省空间。例如,假设一个表存储关于好友关系的数据,其中每行代表一个人及其所有好友。每个列限定符可以是好友的 ID,该列在该行中的值可以是好友所在的社交圈子。在本示例中,行可能如下所示:

Jose Fred:book-club Gabriel:work Hiroshi:tennis
索非亚 Hiroshi:work Seo Yoon:school Jakob:chess-club

将此架构与存储相同数据并且不将列限定符用作数据的架构进行比较:

Jose#1 Friend:Fred Circle:book-club
Jose#2 Friend:Gabriel Circle:work
Jose#3 Friend:Hiroshi Circle:tennis
Sofia#1 Friend:Hiroshi Circle:work
Sofia#2 Friend:Seo Yoon Circle:school
Sofia# Friend:Jakob Circle:chess-club

第二种架构设计会使表增长得非常快。

如果您不使用列限定符来存储数据,并且想减少每个请求传输的数据量,请使用简短而有意义的列限定符名称。最大 16 KB。

在表中创建您需要的任何数量的列。Bigtable 表属于稀疏表,行中未使用的列不会占用任何空间。只要没有行超过每行 256 MB 的最大限制,您就可以在表中拥有数百万列。

避免在同一行中使用过多列虽然表可以有数百万个列,但行不应该这样。这一最佳做法的原因如下:

  • Bigtable 需要一些时间来处理一行中的每个单元格。
  • 每个单元也会对存储在表中并通过网络发送的数据量增加一些开销。例如,若要存储 1 KB(1024 字节)的数据,将这些数据存储在 1 个单元中,比起将数据分布在 1024 个单元中,每个单元存储 1 个字节的空间效率要高得多。

如果数据集在逻辑上每行需要的列数太多,导致 Bigtable 无法高效处理,请考虑在一列中将数据存储为 protobuf

不要在一行中存储超过 100 MB 的数据。 超出此限制的行可能会导致读取性能降低。

将一个实体的所有信息保存在一行中。对于大多数使用场景,请避免将需要以原子方式读取或一次性读取的数据存储在多个行中,以避免不一致。例如,如果您对表中的两个行进行更新,那么有可能其中一行成功更新,而另一行的更新失败。请确保架构不需要同时更新多个行,以保证相关数据准确无误。这样可以确保如果写入请求部分失败或需要再次发送,数据不会处于暂时不完整的状态。

例外情况:如果将一个实体保存在一行中会使行大小为数百 MB,则应该将数据拆分为多行。

将相关实体存储在相邻行中,以提高读取效率。

单元格

不要在一个单元中存储超过 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#okinawa
        asia#japan#sapporo
        southamerica#bolivia#cochabamba
        southamerica#bolivia#lapaz
        southamerica#chile#santiago
        southamerica#chile#temuco

需避免使用的行键

某些类型的行键可能会使数据查询变得困难或导致性能不良。本部分介绍在 Bigtable 中应避免使用的一些行键类型。

以时间戳开头的行键。这会导致顺序写入被推送到单个节点上,形成热点。如果要将时间戳放在行键中,则需要在前面加上高基数值(如用户 ID),以避免出现 hotspotting 问题。

导致相关数据不分组到一起的行键。避免使用将相关数据存储在非连续的行范围中的行键,因为这样会使同时读取这些数据的效率很低。

顺序数字 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 作为行键前缀。 一个租户的所有数据都会存储在同一个表的连续行中,您可以使用行键前缀进行查询或过滤。此后,如果某家公司不再是您的客户,并且您需要删除为该公司存储的购买记录数据,您可以删除使用该客户的行键前缀的一系列行

例如,如果您要为客户 altostratexamplepetstore 存储手机数据,则可以创建如下行键。此后,如果 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) 或用户数据,除非您的使用场景有此要求。行键和列族既是数据又是元数据,将它们用作元数据的应用(如加密或日志记录)可能会无意中将其暴露给无权访问私密数据的用户。

域名

众多域名

如果您要存储的实体相关数据可以用域名来表示,请考虑使用反向域名(例如 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

在第二个示例中,对相关行自动排序后,就可以轻松地在某个行范围内检索它们。

少量域名

如果您只需要存储一个或少量域名的大量数据,请考虑为行键使用其他值。否则,写入可能会被推送到集群中的单个节点,从而导致 hotspotting 问题或行变得太大。

变化或不确定的查询

如果您并非总是对数据运行相同的查询,或者不确定将会使用什么查询,则可以将一行中的所有数据存储到一个而不是多个列中。如果采用这种方法,您将使用可以在稍后轻松提取各个值的格式,例如协议缓冲区二进制格式或 JSON。

您仍然需要精心设计行键以确保可以检索所需的数据,但每行通常只有一个列,所有数据包含在一个 protobuf 中。

将数据以 protobuf 消息形式存储在一列,而不是将数据分布到多个列中,这种做法有优点也有缺点。优点包括:

  • 数据占用的空间较少,节省存储费用。
  • 无需考虑列族和列限定符,具有一定的灵活性。
  • 读取应用不需要“知道”表架构。

缺点包括:

  • 从 Bigtable 读取 protobuf 消息后,必须进行反序列化。
  • 无法使用过滤条件查询 protobuf 消息中的数据。
  • 从 Bigtable 读取 protobuf 消息后,无法使用 BigQuery 对其中的字段运行联合查询。

后续步骤