数据库-GaussDB-基础篇-会话 ACTIVE 状态下 SQL 中断机制研究
一、核心概念区分
1. 语句级取消(本文研究对象):只终止当前正在执行的 SQL,TCP 长连接保留,会话不销毁,执行中断后会话由active → idle。
2. 会话级终止(pg_terminate_backend):直接销毁整个 backend 会话,TCP 连接断开,会话消失,不属于本次研究范围。
- 两种语句级取消入口:
-
> - 方式 A: 客户端侧触发(DBeaver 点击取消,PG Wire 协议 CancelRequest 信号)
-
> - 方式 B: 数据库服务端 DBA 触发(
select pg_cancel_backend(pid);系统函数)
二、两种取消方式对比表
对比项目
客户端点击 Cancel(DBeaver/JDBC)
DBA 执行 pg_cancel_backend (pid)
触发入口
PG Wire 协议,客户端主动发起
数据库内置系统函数,SQL 调用执行
TCP 连接行为
✅新建独立 TCP 短连接,不走原有执行 SQL 的长连接;发送完 16 字节报文后,新连接直接关闭
✅复用 DBA 当前已存在的数据库长连接,作为普通 Query 报文下发,不新建连接
网络报文特征
固定 16 字节CancelRequest专用协议报文
- 报文结构:长度 (4B)+ 魔数 (4B)+32 位代理 backendPID (4B)+secret 密钥 (4B)
配套握手阶段 K 报文(BackendKeyData)提前下发代理 ID 与 secret
普通 PG Query 报文,传输 SQL 文本select pg_cancel_backend(xxx);
不会产生 CancelRequest 报文,无 K 报文交互
ID 参数
使用握手BackendKeyData(K报文)返回的32 位协议代理 ID(该 ID 不会出现在系统视图,仅存在 CN 内存、网络报文)
入参填写pgxc_stat_activity视图里64 位集群全局 session PID
内核处理入口
CN postmaster 进程接收网络报文,查表映射到内部 sessionid,打上中断标记
内核调用misc.cpp内的系统函数,内存中查找目标 session,打上中断标记
业务侧返回报错
ERROR: canceling statement due to user request
ERROR: canceling statement due to user request(对外报错文本完全一致)
CN 内核日志标识
日志无misc.cpp标记,代表本次取消来自网络协议报文
日志打印call: misc.cpp,可作为区分服务端函数触发的诊断标记
内核底层逻辑
-
完全相同:给目标 backend 设置中断标记;后端算子在安全断点检测标记,停止计算、回滚当前语句,会话 active→idle
-
完全相同:给目标 backend 设置中断标记;后端算子在安全断点检测标记,停止计算、回滚当前语句,会话 active→idle
适用场景
应用客户端主动取消正在执行的 SQL;独立信号通道,不占用原有查询连接
DBA 运维场景,登录数据库手动终止长耗时 SQL
三、抓包与日志实验证据
配图 1:K 报文 BackendKeyData(握手阶段)
- 说明: 连接建立握手时,CN 主动下发 K 报文,返回32 位代理 backendPID + secret 密钥。

- > 关键点: 客户端后续取消 SQL,就是复用这一组 ID 和密钥组装 CancelRequest 报文。该代理 ID 无法在数据库系统视图查询。
配图 2:客户端 Cancel 抓包(16 字节 CancelRequest 报文)
-
报文样例:
00 00 00 10 04 d2 16 2e 00 00 13 70 1c c1 84 2c![https://oscimg.oschina.net//AiCreationDetail/up-97c1f089d602bb676d9315f26aaed0ee.png]()
说明:
1. 这条报文在全新端口的 TCP 连接传输,不是执行 SQL 的原有长连接;
2. 这是 PG 协议专用的 Cancel 信号,不带任何 SQL 文本;
3. 报文内的 32 位代理 ID、secret,和 K 报文握手拿到的值完全一致。
DBA 执行 pg_cancel_backend 抓包

1. 复用 DBA 已登录的数据库长连接;
2. 本质是普通 SELECT 查询报文,不存在 16 字节 CancelRequest 报文;
3. 内核在数据库内部完成会话标记,不会向外发送协议 Cancel 信号。
配图 4:CN 内核日志对比截图
-
截图: CN 日志,两行报错记录
-
17:55:客户端 DBeaver 点击取消,日志报错canceling statement due to user request,无misc.cpp标记 -
19:31:DBA 执行 pg_cancel_backend,同样报错文本,日志携带call: misc.cpp
- > 核心证据: 业务侧报错文字一模一样,但是内核日志调用栈标记可以区分取消来源。单纯看应用返回的错误信息,无法分辨是客户端触发还是 DBA 手动取消。
四、完整时序流程
场景 A:客户端 DBeaver 点击取消
1. 客户端与 CN 建立业务长连接;握手阶段 CN 返回BackendKeyData(K报文),下发 32 位代理 backendPID + secret 密钥;
2. 提交大笛卡尔积 SQL,会话进入active;CN 内存维护映射关系:32位代理ID → 集群64位sessionid;
3. 用户点击 DBeaver【取消】:JDBC新开独立 TCP 连接,组装 16 字节 CancelRequest 报文,填入握手拿到的代理 ID、secret;
4. CN postmaster 收到独立连接的 Cancel 报文,校验 secret 合法性,查表定位目标 session,设置中断 flag;
5. DN/CN 上执行算子,在安全断点轮询中断标记;一旦检测标记置位,停止 SQL 计算;
6. 会话状态由active转为idle,TCP 业务长连接保留;CN 向原始客户端返回报错:canceling statement due to user request;
7. 发送 Cancel 信号的临时 TCP 短连接直接关闭。
- > 重点: 代理 32 位 backendPID仅存在网络报文、CN 内存映射表,不会出现在 pgxc_stat_activity 等系统视图。
场景 B:DBA 执行 pg_cancel_backend
1. DBA 登录数据库,在自己的会话执行 SQL:select pg_cancel_backend(64位集群PID);;
2. 这条 SQL 通过 DBA 已有的长连接,以普通 Query 报文下发至 CN;
3. CN 解析识别系统函数,进入misc.cpp内代码,在内存中根据 64 位 PID 找到目标 active 会话,直接设置中断标记(无网络 Cancel 报文);
4. 后端算子同样在安全断点检测中断标记,停止 SQL,会话 active→idle;
5. CN 向执行大 SQL 的原始客户端返回相同报错;CN 日志打印call: misc.cpp,标记本次取消来自服务端函数调用。
五、实验关键现象与踩坑点
1. **中断是协作式,非抢占式
- 打上中断标记后,SQL 不会立刻停止。如果算子长时间没有走到中断检查点(例如超大笛卡尔积),会出现:已经发起取消,但是数据库侧会话依然长时间保持 active,直到算子到达安全断点才退出。
2. **前端报错文字一致,内核日志才能区分来源
两种方式给业务客户端返回的错误文本一模一样,仅依靠应用侧日志无法区分是 DBA 手动取消,还是客户端点击取消;CN 内核日志的misc.cpp标记是区分依据。
3. 协议层几乎兼容原生 PG,GaussDB 改动很小
- Wire 协议 Cancel 报文格式完全继承 PG;仅在内核增加一层 ID 映射:原生 PG 报文 PID 是 OS 进程 PID,GaussDB 替换为 CN 维护的 32 位代理 ID,映射到分布式集群 sessionid。
4. 两种取消均只终止单条 SQL,不会断开 TCP 连接,和 pg_terminate_backend(销毁会话、断连接)是完全不同能力。
六、源码参考(OpenGauss 开源内核)
- > 说明: 取自开源 OpenGauss 内核,仅摘录关键主干代码,剔除无关边界校验、日志、异常处理;用来佐证本次实验结论。
1. Postmaster 接收 CancelRequest 报文(客户端协议取消)
- 文件:
src/postmaster/postmaster.c
/*
* 处理独立TCP连接发来的CancelRequest报文
* 客户端新建短连接,发送16字节固定报文:长度、magic、proxy backend id、secret
*/
static void ProcessCancelRequest(Port *port)
{
CancelRequestKey cancel_key;
int nbytes;
// 读取16字节CancelRequest报文
nbytes = read(port->sock, &cancel_key, sizeof(CancelRequestKey));
if (nbytes != sizeof(CancelRequestKey))
{
return;
}
// 网络字节序转主机序
cancel_key.be_pid = ntohl(cancel_key.be_pid);
cancel_key.be_secret = ntohl(cancel_key.be_secret);
/*
* 重点:
* be_pid 就是我们抓包看到的32位代理backendPID,不是OS真实PID
* postmaster 在共享内存哈希表查表,把代理ID映射到真实backend会话
*/
if (SendCancelSignal(cancel_key.be_pid, cancel_key.be_secret))
{
// 校验secret成功,向目标backend设置中断标记
}
pq_close(port); // 这个独立TCP短连接直接关闭
}
-
SendCancelSignal内部逻辑: 根据传入的代理 ID,查找 backend,校验 secret,设置QueryCancelPending中断标记。 -
> ✅ 对应实验现象: 客户端取消走独立 TCP 连接,解析 32 位代理 ID、secret,不走 misc.cpp。
2. pg_cancel_backend 系统函数(misc.cpp)
- 文件:
src/backend/utils/adt/misc.cpp
这就是日志里
call: misc.cpp的来源!DBA 执行pg_cancel_backend进入这个函数
/*
* pg_cancel_backend(pid bigint)
* 语句级取消,只终止当前SQL,保留会话连接
* 入参:pgxc_stat_activity视图中64位集群backend pid
*/
Datum pg_cancel_backend(PG_FUNCTION_ARGS)
{
int64 pid = PG_GETARG_INT64(0);
bool result = false;
// 在共享内存中,根据集群64位PID查找backend
Backend *backend = GetBackendByPid(pid);
if (backend != NULL)
{
// 直接给backend打上查询中断标记 QueryCancelPending
SetQueryCancelPending(backend);
result = true;
}
PG_RETURN_BOOL(result);
}
- > 关键: 这里完全没有网络报文处理,直接在服务端内存找到 backend,设置中断标记。
不会产生 16 字节 CancelRequest 报文,所以抓包看不到 Cancel 包。
3. 公共底层:中断标记检测(两套取消路径共用)
- 文件:
src/backend/postgres.c
// 后端算子循环执行时,在安全断点检测中断标记
void ProcessInterrupts(void)
{
if (QueryCancelPending)
{
QueryCancelPending = false;
ereport(ERROR,
(errcode(ERRCODE_QUERY_CANCELED),
errmsg("canceling statement due to user request")));
}
}
✅ 这就是为什么两种取消方式,返回给客户端的报错文本完全一模一样!
不管是客户端 Cancel 报文,还是 pg_cancel_backend 函数,最终都会触发QueryCancelPending标记,走到同一段报错代码。
源码关键点小结:
协议里的 32 位 backendPID,是 CN 维护的代理 ID,用于 Wire 协议兼容,和视图 64 位集群 PID 不是同一个;
两套入口,最终中断逻辑同源;
misc.cpp仅属于pg_cancel_backend函数入口,是日志区分两条路径的标记。
七、总结
本次实验原本目标是验证在ACTIVE 状态下语句级取消的两套实现路径:
-
客户端 Cancel 依靠独立 TCP 短连接 + 专用 16 字节协议信号;
-
pg_cancel_backend 是内核函数调用,复用现有连接,无 Cancel 协议报文;
-
二者内核中断底层逻辑同源,差异仅在触发入口。




浙公网安备 33010602011771号