架构设计最佳实践

Spanner 的分布式架构使您可以设计自己的架构以避免热点。热点是指这样的情况:向同一服务器发送的请求过多,导致服务器资源利用率达到饱和状态,并可能导致延迟时间较长。

本页面介绍了架构设计的最佳实践,可以避免产生热点。避免热点的一种方法是调整架构设计,使 Spanner 能够将数据拆分并分布到多个服务器。将数据分布到多个服务器有助于 Spanner 数据库高效地运行,特别是在执行批量数据插入操作时。

Spanner 会自动检测应用架构设计最佳实践的机会。如果您的数据库有建议,您可以在其 Spanner Studio 页面上查看这些建议。如需了解详情,请参阅查看架构设计最佳实践建议

选择一个主键以避免生成热点

为避免在数据库中生成热点,请在架构设计期间仔细选择主键

热点的一个常见原因是使用单调递增或递减的键(例如时间戳)。单调键会导致所有新条目都写入键空间的同一范围。由于 Spanner 使用键范围在服务器之间分配数据,因此单调键会将所有插入流量定向到单个服务器,从而造成瓶颈。

例如,假设您想维护 UserAccessLogs 表行上的上次访问时间戳列。以下表定义使用基于时间戳的主键作为键的第一个部分。如果表的插入速率很高,我们不建议这样做:

GoogleSQL

CREATE TABLE UserAccessLogs (
  LastAccess TIMESTAMP NOT NULL,
  UserId STRING(1024),
  ...
) PRIMARY KEY (LastAccess, UserId);

PostgreSQL

CREATE TABLE useraccesslogs (
  lastaccess timestamptz NOT NULL,
  userid text,
  ...
PRIMARY KEY (lastaccess, userid)
);

这里的问题在于,行按照上次访问时间戳的顺序写入此表,但是因为上次访问时间戳总是不断递增,因此它们总是写入表的末尾。由于单个 Spanner 服务器接收所有写入操作,这将使该服务器超载,从而生成热点。

下图演示了这个陷阱:

按时间戳排序且带有相应热点的 UserAccessLog 表

之前的 UserAccessLogs 表包含五个示例数据行,它们分别表示五个执行某种用户操作的不同用户,各操作之间的间隔大约为一毫秒。该图还注释了 Spanner 插入这些行的顺序(带有标签的箭头表示每行的写入顺序)。由于插入按时间戳排序,并且时间戳值始终递增,因此 Spanner 始终会将插入添加到表的末尾并指向相同的分块。(正如在架构和数据模型中所讨论的,分块是来自一个或多个相关表的一系列行,Spanner 按行键顺序存储这些行。)

这可能会引发问题,因为 Spanner 以分块为单位将工作分配给不同的服务器,因此分配给此特定分块的服务器最终会处理所有插入请求。随着用户访问事件频率的增加,向相应的服务器插入请求的频率也会增加。然后,服务器便易于成为热点,如上图的红色边框和背景所示。在此简化图示中,每个服务器最多处理一个分块,但 Spanner 可为每个服务器分配多个分块。

基于负载的拆分中所述,当 Spanner 向表中添加更多行时,分块会增大,当其大小达到大约 8 GiB 时,Spanner 会创建另一个分块。Spanner 会将后续的新行添加到这个新的分块中,分配给该分块的服务器将成为新的潜在热点。

当热点出现时,您可能会发现插入变得缓慢,同一台服务器上的其他工作的速度也会下降。将 LastAccess 列的顺序改为升序并不能解决这个问题,因为这样会使所有写入都插入到表的顶部,所有插入仍然会被传递到单个服务器。

架构设计最佳做法 #1:不要选择其值单调递增或递减的列作为高写入速率表的首个键部分。

使用通用唯一标识符 (UUID)

您可以使用由 RFC 4122 定义的通用唯一标识符 (UUID) 作为主键。建议使用 UUID 版本 4,因为它使用位序列中的随机值。不建议使用版本 1 UUID,因为它们将时间戳存储在高位中。

以下几种方法可以将 UUID 存储为主键:

  • STRING(36) 列中。
  • 在一对 INT64 列中。
  • BYTES(16) 列中。

对于 STRING(36) 列,您可以使用 Spanner GENERATE_UUID() 函数(GoogleSQLPostgreSQL)作为列的默认值,以便让 Spanner 自动生成 UUID 值。

例如,对于下表:

GoogleSQL

CREATE TABLE UserAccessLogs (
  LogEntryId STRING(36) NOT NULL