数据库-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 密钥。

https://oscimg.oschina.net//AiCreationDetail/up-00b469de77da7cb7ce1c8ff94f4da1f5.png

  • > 关键点: 客户端后续取消 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 抓包

https://oscimg.oschina.net//AiCreationDetail/up-d574b19e39b3031ae6f10b2222a5be33.png

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

https://oscimg.oschina.net//AiCreationDetail/up-24082d295f12930ab4107efc06d0e326.png

  • > 核心证据: 业务侧报错文字一模一样,但是内核日志调用栈标记可以区分取消来源。单纯看应用返回的错误信息,无法分辨是客户端触发还是 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 短连接直接关闭。

https://oscimg.oschina.net//AiCreationDetail/up-96de5e5d565f4c06f95c993c4fae8245.png

  • > 重点: 代理 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 协议报文;

  • 二者内核中断底层逻辑同源,差异仅在触发入口。

posted @ 2026-09-27 12:50  打印helloworld  阅读(2)  评论(1)    收藏  举报