分块统计信息

本页介绍了如何检测和调试数据库中的热点。您可以通过 GoogleSQL 和 PostgreSQL 访问有关分块热点的统计信息。

Spanner 会将您的数据存储为连续的键空间,并按表和索引的主键对其进行排序。分块是一组表或索引中的一系列行。分块的起始位置称为“分块起始位置”。分块限制用于设置分块结束位置。分块包括分块起始位置,但不包括分块限制。

在 Spanner 中,热点是指向同一服务器发送的请求过多,导致服务器资源饱和,并可能导致延迟时间较长的情况。受热点影响的分块称为热分块或温分块。

分块热点统计信息(在系统中标识为 CPU_USAGE_SCORE)是受服务器上可用资源限制的分块负载的度量值。此衡量结果以百分比表示。如果分块负载的 50% 以上受可用资源的限制,则该分块会被视为温分块。如果某个分块上的负载 100% 都受到限制,则该分块会被视为热分块

Spanner 使用基于负载的拆分来跨实例的服务器平均分配数据负载。温分块和热分块可以跨服务器移动以进行负载均衡,也可以拆分为更小的分块。但是,由于应用中的反模式,即使在多次尝试拆分后,Spanner 也可能无法均衡负载。因此,持续时间至少为 10 分钟的热点可能需要进一步的排障和潜在的应用更改。

Spanner 热分块统计信息可帮助您确定热点出现的分块。然后,您可以根据需要更改应用或架构。您可以使用 SQL 语句从 SPANNER_SYS.SPLIT_STATS_TOP_MINUTE 系统表中检索这些统计信息。

热分块统计信息的可用性

Spanner 在 SPANNER_SYS 架构中提供热分块统计信息。SPANNER_SYS 数据只能通过 GoogleSQL 和 PostgreSQL 接口获得。您可以通过以下方式访问这些数据:

Spanner 单次读取 API 不支持 SPANNER_SYS

热分块统计信息

您可以使用以下表格来跟踪热分块:

  • SPANNER_SYS.SPLIT_STATS_TOP_MINUTE:显示 1 分钟时间段内热度较高的分块。

这些表具有以下属性:

  • 每个表包含表名指定的非重叠时间间隔时长内的数据。
  • 间隔基于时钟时间:

    • 1 分钟间隔结束于分钟。
  • 在每个间隔后,Spanner 都会从所有服务器收集数据,然后稍后在 SPANNER_SYS 表中提供这些数据。

    例如,在上午 11:59:30,SQL 查询可用的最近时间段为:

    • 1 分钟:上午 11:58:00-11:58:59
  • Spanner 会按分块对统计信息进行分组。

  • 每行包含一个百分比,用于指示 Spanner 在指定时间段内捕获统计信息的每个分块的热度或温度。

  • 如果分块负载中受可用资源限制的部分小于 50%,Spanner 不会捕获统计信息。如果 Spanner 无法在指定时间间隔内存储所有热分块,则系统会优先存储在该时间间隔内 CPU_USAGE_SCORE 百分比最高的分块。如果未返回任何分块,则表示没有任何热点。

表架构

下表显示了以下统计信息的表架构:

  • SPANNER_SYS.SPLIT_STATS_TOP_MINUTE
列名 类型 说明
INTERVAL_END TIMESTAMP 分块处于热状态的时间间隔的结束时间
SPLIT_START STRING 分块中行范围的起始键。分块起始位置也可能是 <begin>,表示键空间的起始位置
SPLIT_LIMIT STRING 分块中行范围的限制键。限制键也可能是 <end>,表示键空间的结束位置。
CPU_USAGE_SCORE INT64 分块的CPU_USAGE_SCORE百分比。CPU_USAGE_SCORE 百分比为 50% 表示存在温分块或热分块。
AFFECTED_TABLES STRING ARRAY 可能有行位于分块中的表

分块起始位置和分块限制键

分块是数据库的连续行范围,由其起始位置限制键定义。分块可以是单行、狭窄的行范围或宽行范围,并且可以包含多个表或索引。

SPLIT_STARTSPLIT_LIMIT 列标识温分块或热分块的主键。

示例架构

以下架构是本页中主题的示例表格。

GoogleSQL

CREATE TABLE Users (
  UserId INT64 NOT NULL,
  FirstName STRING(MAX),
  LastName STRING(MAX),
) PRIMARY KEY(UserId);

CREATE INDEX UsersByFirstName ON Users(FirstName DESC);

CREATE TABLE Threads (
  UserId INT64 NOT NULL,
  ThreadId INT64 NOT NULL,
  Starred BOOL,
) PRIMARY KEY(UserId, ThreadId),
  INTERLEAVE IN PARENT Users ON DELETE CASCADE;

CREATE TABLE Messages (
  UserId INT64 NOT NULL,
  ThreadId INT64 NOT NULL,
  MessageId INT64 NOT NULL,
  Subject STRING(MAX),
  Body STRING(MAX),
) PRIMARY KEY(UserId, ThreadId, MessageId),
  INTERLEAVE IN PARENT Threads ON DELETE CASCADE;

CREATE INDEX MessagesIdx ON Messages(UserId, ThreadId, Subject),
INTERLEAVE IN Threads;

PostgreSQL

CREATE TABLE users
(
   userid    BIGINT NOT NULL PRIMARY KEY,-- INT64 to BIGINT
   firstname VARCHAR(max),-- STRING(MAX) to VARCHAR(MAX)
   lastname  VARCHAR(max)
);

CREATE INDEX usersbyfirstname
  ON users(firstname DESC);

CREATE TABLE threads
  (
    userid   BIGINT NOT NULL,
    threadid BIGINT NOT NULL,
    starred  BOOLEAN, -- BOOL to BOOLEAN
    PRIMARY KEY (userid, threadid),
    CONSTRAINT fk_threads_user FOREIGN KEY (userid) REFERENCES users(userid) ON
    DELETE CASCADE -- Interleave to Foreign Key constraint
  );

CREATE TABLE messages
  (
    userid    BIGINT NOT NULL,
    threadid  BIGINT NOT NULL,
    messageid BIGINT NOT NULL PRIMARY KEY,
    subject   VARCHAR(max),
    body      VARCHAR(max),
    CONSTRAINT fk_messages_thread FOREIGN KEY (userid, threadid) REFERENCES
    threads(userid, threadid) ON DELETE CASCADE
  -- Interleave to Foreign Key constraint
  );

CREATE INDEX messagesidx ON messages(userid, threadid, subject), REFERENCES
threads(userid, threadid);

假设您的键空间如下所示:

主键
<begin>
Users()
Threads()
Users(2)
Users(3)
Threads(3)
Threads(3,"a")
Messages(3,"a",1)
Messages(3,"a",2)
Threads(3, "aa")
Users(9)
Users(10)
Threads(10)
UsersByFirstName("abc")
UsersByFirstName("abcd")
<end>

分块示例

以下示例显示了一些分块,可帮助您了解分块的外观。

SPLIT_STARTSPLIT_LIMIT 可能表示表或索引的行,也可能是 <begin><end>,表示数据库键空间的边界。SPLIT_STARTSPLIT_LIMIT 也可能包含截断键,即表中任何完整键之前的键。例如,Threads(10)Users(10) 中交织的任何 Threads 行的前缀。

SPLIT_START SPLIT_LIMIT AFFECTED_TABLES 解释
Users(3) Users(10) UsersByFirstNameUsersThreadsMessagesMessagesIdx 分块从包含 UserId=3 的行开始,并在包含 UserId = 10 行的前一行结束。分块包含 Users 表行及其 UserId=3 到 10 的所有交织表行。
Messages(3,"a",1) Threads(3,"aa") ThreadsMessagesMessagesIdx 分块从包含 UserId=3ThreadId="a"MessageId=1 的行开始,并在包含 UserId=3ThreadsId = "aa" 键的行之前结束。分块包含 Messages(3,"a",1)Threads(3,"aa") 之间的所有表。由于 split_startsplit_limit 在同一顶级表行中交织,因此分块包含起始位置和限制之间的交织表行。如需了解交织表是如何共存的,请参阅架构概览
Messages(3,"a",1) <end> UsersByFirstNameUsersThreadsMessagesMessagesIdx 分块从消息表中键为 UserId=3ThreadId="a"MessageId=1 的行开始。分块包含从 split_start<end>(数据库键空间的末尾)的所有行。在 split_start 后面的表(如 Users(4))的所有行都包含在分块中。
<begin> Users(9) UsersByFirstNameUsersThreadsMessagesMessagesIdx 分块从 <begin>(数据库键空间的起始位置)开始,并在包含 UserId=9Users 行之前结束。因此,分块包含 Users 之前的所有表行,以及 UserId=9 之前的所有 Users 表行及其交织表的行。
Messages(3,"a",1) Threads(10) UsersByFirstNameUsersThreadsMessagesMessagesIdx 分块从 Users(3) 中交织的 Messages(3,"a", 1) 开始,并在 Threads(10) 前面的行结束。Threads(10) 是截断的分块键,该键是 Users(10) 中交织的“Threads”表的任何键的前缀。
Users() <end> UsersByFirstNameUsersThreadsMessagesMessagesIdx 分块从截断的分块键 Users() 开始,该键位于 Users 表的任何完整键之前。分块会一直延伸到数据库中可能的键空间的末尾。因此,affected_tables 涵盖了 Users 表、其交织表和索引,以及用户之后可能显示的所有表。
Threads(10) UsersByFirstName("abc") UsersByFirstNameUsersThreadsMessagesMessagesIdx 分块从包含 UserId = 10Threads 行开始,并在 "abc" 前面的键的 UsersByFirstName 索引处结束。

用于查找热分块的查询示例

以下示例展示了一个可用于检索热分块统计信息的 SQL 语句。您可以使用客户端库、gcloud 或 Google Cloud 控制台运行这些 SQL 语句。

GoogleSQL

SELECT t.split_start,
       t.split_limit,
       t.cpu_usage_score,
       t.affected_tables,
FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE  t.interval_end =
  (SELECT MAX(interval_end)
  FROM    SPANNER_SYS.SPLIT_STATS_TOP_MINUTE)
ORDER BY  t.cpu_usage_score DESC;

PostgreSQL

SELECT t.split_start,
       t.split_limit,
       t.cpu_usage_score,
       t.affected_tables
FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE  t.interval_end = (
  SELECT MAX(interval_end)
  FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE
)
ORDER BY t.cpu_usage_score DESC;

查询输出如下所示:

SPLIT_START SPLIT_LIMIT CPU_USAGE_SCORE AFFECTED_TABLES
Users(13) Users(76) 82 Messages,Users,Threads
Users(101) Users(102) 90 Messages,Users,Threads
Threads(10, "a") Threads(10, "aa") 100 Messages,Threads
Messages(631, "abc", 1) Messages(631, "abc", 3) 100 Messages
Threads(12, "zebra") Users(14) 76 Messages,Users,Threads
Users(620) <end> 100 Messages,Users,Threads

热分块统计信息的数据保留

Spanner 至少会为每个表保留以下时间段内的数据:

  • SPANNER_SYS.SPLIT_STATS_TOP_MINUTE:涵盖前 6 个小时的间隔。

使用热分块统计信息排查热点问题

本部分介绍如何检测热点并排查相关问题。

选择要调查的时间段

检查 Spanner 数据库的延迟时间指标,以查找应用出现高延迟和高 CPU 使用率的时间段。例如,它可能会显示某个问题大约在 2024 年 5 月 18 日下午 10:50 开始出现。

查找持久性 hotspotting

由于 Spanner 会使用基于负载的拆分来均衡您的负载,因此我们建议您调查hotspotting是否持续超过 10 分钟。您可以通过查询 SPANNER_SYS.SPLIT_STATS_TOP_MINUTE 表来实现此目的,如以下示例所示:

GoogleSQL

SELECT Count(DISTINCT t.interval_end)
FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE  t.utilization >= 50
  AND  t.interval_end >= "interval_end_date_time"
  AND  t.interval_end <= "interval_end_date_time";

interval_end_date_time 替换为该时间段的日期和时间,使用 2024-05-18T17:40:00Z 格式。

PostgreSQL

SELECT COUNT(DISTINCT t.interval_end)
FROM   SPLIT_STATS_TOP_MINUTE t
WHERE  t.utilization >= 50
  AND  t.interval_end >= 'interval_end_date_time'::timestamptz
  AND  t.interval_end <= 'interval_end_date_time'::timestamptz;

interval_end_date_time 替换为该时间段的日期和时间,使用 2024-05-18T17:40:00Z 格式。

如果上一个查询结果等于 10,则表示您的数据库存在hotspotting问题,可能需要进一步调试。

查找 CPU_USAGE_SCORE 级别最高的分块

在此示例中,我们运行以下 SQL 以查找具有最高 CPU_USAGE_SCORE 级别的行范围:

GoogleSQL

SELECT t.split_start,
       t.split_limit,
       t.affected_tables,
       t.cpu_usage_score
FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE  t.cpu_usage_score >= 50
  AND  t.interval_end = "interval_end_date_time";

interval_end_date_time 替换为该时间段的日期和时间,使用 2024-05-18T17:40:00Z 格式。

PostgreSQL

SELECT t.split_start,
       t.split_limit,
       t.affected_tables,
       t.cpu_usage_score
FROM   SPLIT_STATS_TOP_MINUTE t
WHERE  t.cpu_usage_score = 100
  AND  t.interval_end = 'interval_end_date_time'::timestamptz;

interval_end_date_time 替换为该时间段的日期和时间,使用 2024-05-18T17:40:00Z 格式。

上述 SQL 会输出以下内容:

SPLIT_START SPLIT_LIMIT CPU_USAGE_SCORE AFFECTED_TABLES
Users(180) <end> 85 Messages,Users,Threads
Users(24) Users(76) 76 Messages,Users,Threads

从这个结果表中,我们可以看到热点出现在两个分块上。Spanner 基于负载的拆分可能会尝试解析这些分块上的热点。不过,如果架构或工作负载中存在有问题的模式,则可能无法做到这一点。为了检测是否存在需要您干预的分块,我们建议您至少跟踪分块 10 分钟。例如,以下 SQL 会跟踪过去 10 分钟内的第一个分块。

GoogleSQL

SELECT t.interval_end,
       t.split_start,
       t.split_limit,
       t.cpu_usage_score
FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE  t.split_start = "users(180)"
  AND  t.split_limit = "<end>"
  AND  t.interval_end >= "interval_end_date_time"
  AND  t.interval_end <= "interval_end_date_time";

interval_end_date_time 替换为该时间段的日期和时间,使用 2024-05-18T17:40:00Z 格式。

PostgreSQL

SELECT t.interval_end,
       t.split_start,
       t.split_limit,
       t.cpu_usage_score
FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE  t.split_start = 'users(180)'
  AND  t.split_limit = ''
  AND  t.interval_end >= 'interval_end_date_time'::timestamptz
  AND  t.interval_end <= 'interval_end_date_time'::timestamptz;

interval_end_date_time 替换为该时间段的日期和时间,使用 2024-05-18T17:40:00Z 格式。

上述 SQL 会输出以下内容:

INTERVAL_END SPLIT_START SPLIT_LIMIT CPU_USAGE_SCORE
2024-05-18T17:46:00Z Users(180) <end> 85
2024-05-18T17:47:00Z Users(180) <end> 85
2024-05-18T17:48:00Z Users(180) <end> 85
2024-05-18T17:49:00Z Users(180) <end> 85
2024-05-18T17:50:00Z Users(180) <end> 85

过去几分钟,分块似乎一直很热。您可以观察更长时间的分块,以确定基于负载的 Spanner 拆分是否会缓解热点问题。在某些情况下,Spanner 无法再进行负载均衡。

例如,查询 SPANNER_SYS.SPLIT_STATS_TOP_MINUTE 表。请参阅以下示例场景。

GoogleSQL

SELECT t.interval_end,
      t.split_start,
      t.split_limit,
      t.cpu_usage_score
FROM  SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE t.interval_end >= "interval_end_date_time"
      AND t.interval_end <= "interval_end_date_time";

interval_end_date_time 替换为该时间段的日期和时间,使用 2024-05-18T17:40:00Z 格式。

PostgreSQL

SELECT t.interval_end,
       t.split_start,
       t.split_limit,
       t._cpu_usage
FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE  t.interval_end >= 'interval_end_date_time'::timestamptz
  AND  t.interval_end <= 'interval_end_date_time'::timestamptz;

interval_end_date_time 替换为该时间段的日期和时间,使用 2024-05-18T17:40:00Z 格式。

单个热行

在以下示例中,Threads(10,"spanner") 似乎位于单行分块中,并且在 10 分钟内一直很热。如果热门行存在持续负载,可能会发生这种情况。

INTERVAL_END SPLIT_START SPLIT_LIMIT CPU_USAGE_SCORE
2024-05-16T20:40:00Z Threads(10,"spanner") Threads(10,"spanner1") 62
2024-05-16T20:41:00Z Threads(10,"spanner") Threads(10,"spanner1") 62
2024-05-16T20:42:00Z Threads(10,"spanner") Threads(10,"spanner1") 62
2024-05-16T20:43:00Z Threads(10,"spanner") Threads(10,"spanner1") 62
2024-05-16T20:44:00Z Threads(10,"spanner") Threads(10,"spanner1") 62
2024-05-16T20:45:00Z Threads(10,"spanner") Threads(10,"spanner1") 62
2024-05-16T20:46:00Z Threads(10,"spanner") Threads(10,"spanner1") 80
2024-05-16T20:47:00Z Threads(10,"spanner") Threads(10,"spanner1") 80
2024-05-16T20:48:00Z Threads(10,"spanner") Threads(10,"spanner1") 80
2024-05-16T20:49:00Z Threads(10,"spanner") Threads(10,"spanner1") 100
2024-05-16T20:50:00Z Threads(10,"spanner") Threads(10,"spanner1") 100

Spanner 无法均衡此单个键的负载,因为它无法进一步拆分。

移动热点

在以下示例中,负载会随着时间的推移通过连续的分块移动,并在时间间隔内移动到新的分块。

INTERVAL_END SPLIT_START SPLIT_LIMIT CPU_USAGE_SCORE
2024-05-16T20:40:00Z Threads(1,"a") Threads(1,"aa") 100
2024-05-16T20:41:00Z Threads(1,"aa") Threads(1,"ab") 100
2024-05-16T20:42:00Z Threads(1,"ab") Threads(1,"c") 100
2024-05-16T20:43:00Z Threads(1,"c") Threads(1,"ca") 100

例如,这可能是因为工作负载按单调递增顺序读取或写入键。Spanner 无法均衡负载以缓解此应用行为的影响。

常规负载均衡

Spanner 会尝试通过添加更多分块或移动分块来均衡负载。以下示例展示了这种情况的可能情形。

INTERVAL_END SPLIT_START SPLIT_LIMIT CPU_USAGE_SCORE
2024-05-16T20:40:00Z Threads(1000,"zebra") <end> 82
2024-05-16T20:41:00Z Threads(1000,"zebra") <end> 90
2024-05-16T20:42:00Z Threads(1000,"zebra") <end> 100
2024-05-16T20:43:00Z Threads(1000,"zebra") Threads(2000,"spanner") 100
2024-05-16T20:44:00Z Threads(1200,"c") Threads(2000) 92
2024-05-16T20:45:00Z Threads(1500,"c") Threads(1700,"zach") 76
2024-05-16T20:46:00Z Threads(1700) Threads(1700,"c") 76
2024-05-16T20:47:00Z Threads(1700) Threads(1700,"c") 50
2024-05-16T20:48:00Z Threads(1700) Threads(1700,"c") 39

在此示例中,2024-05-16T17:40:00Z 的较大分块进一步拆分为较小的分块,因此 CPU_USAGE_SCORE 统计信息有所下降。Spanner 可能不会将分块创建为单独的行。分块会反映导致 CPU_USAGE_SCORE 统计信息偏高的负载。

如果您观察到持续存在的热分块超过 10 分钟,请参阅缓解热点问题的最佳实践

缓解热点问题的最佳实践

如果负载均衡无法缩短延迟时间,则下一步需要确定热点的原因。之后,您可以选择减少热点分析工作负载,或优化应用架构和逻辑以避免热点。

确定原因

  • 使用锁定和事务分析洞见查找锁定等待时间较长的事务,其中行范围起始键位于热分块内。

  • 使用查询分析查找从包含热分块的表中读取数据且延迟时间最近增加的查询,或延迟时间与 CPU 的比例较高的查询。

  • 使用最早的活跃查询查找从包含热分块的表中读取数据且延迟时间高于预期的查询。

需要注意的一些特殊情况:

  • 检查是否最近启用了存留时间 (TTL)。如果旧数据有大量分块,则 TTL 可能会在批量删除期间提高 CPU_USAGE_SCORE 级别。在这种情况下,初始删除完成后,问题应该会自行解决。

优化工作负载

  • 遵循 SQL 最佳实践。考虑过时数据读取、不先执行读取的写入或添加索引。
  • 遵循架构最佳实践。确保您的架构设计可处理负载均衡并避免热点。

后续步骤