本页介绍了如何检测和调试数据库中的热点。您可以使用 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 接口获得。您可以通过以下方式访问此类数据:
- Google Cloud 控制台中的数据库 Spanner Studio 页面
gcloud spanner databases execute-sql
命令executeQuery
API
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
|
分块中行范围的限制键。limit: 键也可能是 <end> ,表示键空间的结束位置| |
CPU_USAGE_SCORE
|
INT64
|
拆分项的 CPU_USAGE_SCORE 百分比。CPU_USAGE_SCORE 百分比为 50% 表示存在温分块或热分块 | |
AFFECTED_TABLES |
STRING ARRAY |
可能有行位于分块中的表 |
分块起始键和分块限制键
分块是数据库中的连续行范围,由其起始键和上限键定义。分块可以是单个行、狭窄的行范围或宽的行范围,并且分块可以包含多个表或索引。
SPLIT_START
和 SPLIT_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_START
和 SPLIT_LIMIT
可能表示表或索引的行,也可能是 <begin>
和 <end>
,表示数据库键空间的边界。SPLIT_START
和 SPLIT_LIMIT
可能还包含经过截断的键,即表中任何完整键之前的键。例如,Threads(10)
是 Users(10)
中交错的任何 Threads
行的前缀。
SPLIT_START | SPLIT_LIMIT | AFFECTED_TABLES | 说明 |
---|---|---|---|
Users(3) |
Users(10) |
UsersByFirstName 、Users 、Threads 、Messages 、MessagesIdx |
拆分从包含 UserId=3 的行开始,到包含 UserId = 10 的行之前的行结束。分块包含 Users 表行以及 UserId=3 到 10 的所有交错表行。 |
Messages(3,"a",1) |
Threads(3,"aa") |
Threads 、Messages 、MessagesIdx |
分块从包含 UserId=3 、ThreadId="a" 和 MessageId=1 的行开始,到包含 UserId=3 和 ThreadsId = "aa" 键的行之前的行结束。该分块包含 Messages(3,"a",1) 和 Threads(3,"aa") 之间的所有表。由于 split_start 和 split_limit 交错在同一顶级表行中,因此分块包含 start 和 limit 之间的交错表行。如需了解交错表如何共存,请参阅架构概览。 |
Messages(3,"a",1) |
<end> |
UsersByFirstName 、Users 、Threads 、Messages 、MessagesIdx |
拆分从消息表中键值为 UserId=3 、ThreadId="a" 和 MessageId=1 的行开始。分块包含从 split_start 到 <end> (数据库键空间的结束位置)的所有行。split_start 后面的表中的所有行(例如 Users(4) )都包含在分块中。 |
<begin> |
Users(9) |
UsersByFirstName 、Users 、Threads 、Messages 、MessagesIdx |
分块从数据库键空间的起始位置 <begin> 开始,到包含 UserId=9 的 Users 行之前的行结束。因此,该分块包含 Users 之前的所有表行、UserId=9 之前的 Users 表的所有行,以及其交错表的行。 |
Messages(3,"a",1) |
Threads(10) |
UsersByFirstName 、Users 、Threads 、Messages 、MessagesIdx |
分块从 Users(3) 中交错的 Messages(3,"a", 1) 开始,到 Threads(10) 前面的行结束。Threads(10) 是一个经过截断的分块键,是 Users(10) 中交错的“线程”表的任何键的前缀。 |
Users() |
<end> |
UsersByFirstName 、Users 、Threads 、Messages 、MessagesIdx |
分块从 Users() 的截断分块键开始,该键位于 Users 表的任何完整键之前。分块会延伸到数据库中可能的键空间的末尾。因此,affected_tables 涵盖 Users 表、其交错表和索引,以及用户之后可能会出现的所有表。 |
Threads(10) |
UsersByFirstName("abc") |
UsersByFirstName 、Users 、Threads 、Messages 、MessagesIdx |
分块从包含 UserId = 10 的 Threads 行开始,到 "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 左右开始出现。
查找持续热点
由于 Spanner 会通过基于负载的分片来平衡负载,因此我们建议您调查热点是否已持续超过 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,则表示您的数据库存在热点问题,可能需要进一步调试。
查找 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 分钟以上的热分块,请参阅减少热点的最佳实践。
减少热点的最佳实践
如果负载均衡无法缩短延迟时间,下一步是确定热点的原因。之后,您可以选择减少热点工作负载,或优化应用架构和逻辑以避免热点。
找出原因
使用锁定和事务数据分析查找锁定等待时间较长且行范围起始键位于热点分块内的事务。
使用 Query Insights 查找从包含热分块的表中读取且最近延迟时间增加或延迟时间与 CPU 比率较高的查询。
使用最早的活跃查询查找从包含热分块的表中读取且延迟时间超出预期的查询。
需要注意的一些特殊情况:
- 检查是否最近启用了存留时间 (TTL)。如果旧数据中有大量分块,则 TTL 可能会在批量删除期间提高
CPU_USAGE_SCORE
级别。在这种情况下,初始删除操作完成后,问题应该会自行解决。
优化工作负载
后续步骤
- 了解架构设计最佳实践。
- 了解 Key Visualizer。
- 查看架构设计的示例。
- 了解如何使用“分屏数据分析”信息中心检测热点14