在使用数据库过程中,我们难免要终止一些正在执行的查询等语句,比如不合理的超长大事物,对数据库性能有影响的偶发性查询。一般在pg中使用以下两个函数终止相关查询,这里不在详述两个函数的区别。

直接给出官方文档解释:

在这里插入图片描述

pg_cancel_backend 调用系统信号 SIGINT 对应信号2
pg_terminate_backend 调用系统信号 SIGTERM 对应信号15

--定义在/usr/include/asm/signal.h
#define SIGHUP           1
#define SIGINT           2
#define SIGQUIT          3
#define SIGILL           4
#define SIGTRAP          5
#define SIGABRT          6
#define SIGIOT           6
#define SIGBUS           7
#define SIGFPE           8
#define SIGKILL          9
#define SIGUSR1         10
#define SIGSEGV         11
#define SIGUSR2         12
#define SIGPIPE         13
#define SIGALRM         14
#define SIGTERM         15
#define SIGSTKFLT       16
#define SIGCHLD         17
#define SIGCONT         18
#define SIGSTOP         19
#define SIGTSTP         20
#define SIGTTIN         21
#define SIGTTOU         22
#define SIGURG          23
#define SIGXCPU         24
#define SIGXFSZ         25
#define SIGVTALRM       26
#define SIGPROF         27
#define SIGWINCH        28
#define SIGIO           29
#define SIGPOLL         SIGIO
/*
#define SIGLOST         29
*/
#define SIGPWR          30
#define SIGSYS          31
#define SIGUNUSED       31

或者kill -l也可以查看
在这里插入图片描述

没安装man-pages的话,执行man 7 signal会报错No manual entry for signal in section 7
安装man-pages

yum install -y man-pages

看下POSIX.1-1990标准中描述的信号
在这里插入图片描述
postgresql的pg_ctl可以通过kill对相应进程发送以下几种信号

pg_ctl kill SIGNALNAME pid
SIGNALNAME可以是 ABRT HUP INT KILL QUIT TERM USR1 USR2中的一个

举例:
查找到postgres主进程号为4209
在这里插入图片描述
发送INT信号给4209进行
在这里插入图片描述
在这里插入图片描述
有了以上的介绍,我们描述下postgresql中是如何取消查询等语句的,PostgreSQL协议对中断正在运行的语句是有一定准则的。它通过打开一个新连接并发送一个带有密钥的CancelRequest消息来完成的。在初始连接开始期间,服务端会发送该密钥。如果没有这个密钥,每个人都可以取消并终止我们的查询,这是一个不可接受的安全问题。

C库中的libpq提供了PQgetCancel()和PQcancel()函数来取消查询,其他数据库api也应该有类似的函数。在交互式psql会话中,我们可以简单地按Ctrl+C发送取消请求,这个操作我们应该会经常用到,GUI客户端通常有一个用于取消终止的按钮。

数据库服务如何处理取消请求

PostgreSQL进程间通信很大程度上依赖于信号。
postmaster进程接收到CancelRequest时,它向相应数据库会话的后端进程发送SIGINT信号。该信号是由函数pg_cancel_backend()发送。pg_terminate_backend()发送信号SIGTERM。文章最开始有说明。

每个PostgreSQL进程都有一个信号处理器,当接收到信号时,会对这些信号进行处理。这个信号处理器不会立即中断后端进程,但它会为该进程设置全局变量。SIGINT将设置QueryCancelPending, SIGTERM将设置ProcDiePending。这些变量作为标志,并在适当的时候,由后端进程负责对它们做出反应。这确保了进程不会在不适宜的时候被中断,例如,当该进程使共享内存处于不一致的状态的时候。

通过CHECK_FOR_INTERRUPTS()宏调用ProcessInterrupts()函数,这些调用分布在PostgreSQL代码中很多地方。然后,该函数将抛出是取消当前语句的错误,还是终止后端进程的错误,这取决于之前设置的变量标志(SIGINT将设置QueryCancelPending, SIGTERM将设置ProcDiePending)。

CHECK_FOR_INTERRUPTS宏定义如下:

/* Service interrupt, if one is pending and it's safe to service it now */
#define CHECK_FOR_INTERRUPTS() \
do { \
	if (INTERRUPTS_PENDING_CONDITION()) \
		ProcessInterrupts(); \
} while(0)

发出终止或者取消的命令后,有时候为什么没效果呢?

  1. 发出的命令被卡在不包含CHECK_FOR_INTERRUPTS()的循环中。
  2. 在SQL语句中调用的第三方C函数的执行被卡住了。在这种情况下,可以将错误报告给函数的作者。
  3. 在无法中断的系统调用中被卡住。这可能是操作系统或硬件层面的问题。注意,当进程处于内核空间中时,信号的传递会被延迟。
如果终止或取消的命令没效果,有些人可能会使用kill -9 pid,那么在postgresql中这会带来什么后果呢?

在PostgreSQL后台进程上使用普通的kill是完全没问题的。这将发送SIGTERM信号,它与调用pg_terminate_backend()是相同的。如果这都没有效果,那么很容易使用kill -9,它会发送SIGKILL。该信号无法被捕获并立即终止进程。问题是postmaster如果探测到一个子进程没有干净地关闭。那么,它将杀死所有其他PostgreSQL进程,并进行崩溃恢复,这将导致整个数据库中断,可能需要几秒到几分钟的时间。如果数据库很大并且很繁忙的话,可能需要更久。

注意,虽然在普通的后端进程使用kill -9会导致短时间的停机,但在postmaster进程上使用kill -9会造成更恶劣的影响,因此要避免在postmaster上使用kill -9。它打开了一个时间窗口,在此期间可以启动一个新的postmaster,而旧的postmaster的一些子进程还存活着,这很可能导致磁盘上的数据损坏。所以,永远,永远不要用kill -9杀死postmaster进程!

有时候,kill -9也不能杀死PostgreSQL后台进程。这意味着后端被卡在一个不可中断的系统调用中,例如在不再可用的网络存储上有I/O操作。如果这种情况一直持续,那么摆脱该进程的唯一方法就是重启操作系统

下面我们模拟一个无法终止查询的例子:

创建一个c函数:

--loop.c文件内容如下 

#include "postgres.h"
#include "fmgr.h"
 
#include <unistd.h>
 
PG_MODULE_MAGIC;
 
PG_FUNCTION_INFO_V1(loop);
 
Datum loop(PG_FUNCTION_ARGS)
{
    /* 一个无限循环 */
    while(1)
        sleep(2);
}

-- 创建共享库文件
 gcc -I /opt/pgsql13/include/server/ -fPIC -shared -o loop.so loop.c
 
-- 查看pg的libdir,拷贝共享库文件到该目录
pg_config --libdir
/opt/pgsql13/lib
mv loop.so  /opt/pgsql13/lib

进入数据库,使用共享库文件创建loop函数,然后调用

CREATE  OR REPLACE FUNCTION loop() RETURNS void as
'loop.so', 'loop'
   LANGUAGE c ;

#执行该函数
select loop()

#使用任意方式终止取消该查询
#在psql下ctrl+c无用,如下
postgres=# select loop();
^CCancel request sent

#另外开启一个session,查询pg_stat_activity,如下
postgres=# SELECT pid, query FROM pg_stat_activity WHERE query LIKE '%loop%';
  pid  |                               query                                
-------+--------------------------------------------------------------------
 31449 | select loop();
 31853 | SELECT pid, query FROM pg_stat_activity WHERE query LIKE '%loop%';

#使用终止函数,一样无效
postgres=# SELECT pg_terminate_backend(31449);
 pg_terminate_backend 
----------------------
 t
(1 row)

#该函数还在执行
postgres=# SELECT pid, query FROM pg_stat_activity WHERE query LIKE '%loop%';
  pid  |                               query                                
-------+--------------------------------------------------------------------
 31449 | select loop();
 31853 | SELECT pid, query FROM pg_stat_activity WHERE query LIKE '%loop%';


通过gdb调试一下

gdb /opt/pgsql13/bin/postgres 31449

#生成堆栈跟踪信息
(gdb) bt
#0  0x00007ff0535c58d0 in __nanosleep_nocancel () from /lib64/libc.so.6
#1  0x00007ff0535c5784 in sleep () from /lib64/libc.so.6
#2  0x00007ff03509f725 in loop () from /opt/pgsql13/lib/loop.so
#3  0x0000000000619db2 in ExecEvalFuncExprFusage (state=state@entry=0x15127f8, op=op@entry=0x1512a08, econtext=econtext@entry=0x1512520)
    at execExprInterp.c:2341
#4  0x000000000061c88e in ExecInterpExpr (state=0x15127f8, econtext=0x1512520, isnull=<optimized out>) at execExprInterp.c:715
#5  0x000000000064c5ff in ExecEvalExprSwitchContext (isNull=0x7ffc3a795f6f, econtext=0x1512520, state=0x15127f8)
    at ../../../src/include/executor/executor.h:322
#6  ExecProject (projInfo=0x15127f0) at ../../../src/include/executor/executor.h:356
#7  ExecResult (pstate=<optimized out>) at nodeResult.c:136
#8  0x0000000000620d12 in ExecProcNode (node=0x1512410) at ../../../src/include/executor/executor.h:248
#9  ExecutePlan (execute_once=<optimized out>, dest=0x1514838, direction=<optimized out>, numberTuples=0, sendTuples=true, operation=CMD_SELECT, 
    use_parallel_mode=<optimized out>, planstate=0x1512410, estate=0x15121e8) at execMain.c:1632
#10 standard_ExecutorRun (queryDesc=0x14f88e8, direction=<optimized out>, count=0, execute_once=<optimized out>) at execMain.c:350
#11 0x000000000077b25b in PortalRunSelect (portal=portal@entry=0x1498c08, forward=forward@entry=true, count=0, count@entry=9223372036854775807, 
    dest=dest@entry=0x1514838) at pquery.c:921
#12 0x000000000077c678 in PortalRun (portal=portal@entry=0x1498c08, count=count@entry=9223372036854775807, isTopLevel=isTopLevel@entry=true, 
    run_once=run_once@entry=true, dest=dest@entry=0x1514838, altdest=altdest@entry=0x1514838, qc=qc@entry=0x7ffc3a7961e0) at pquery.c:765
#13 0x000000000077835e in exec_simple_query (query_string=0x13fcf78 "select loop();") at postgres.c:1239
#14 0x00000000007796d2 in PostgresMain (argc=<optimized out>, argv=argv@entry=0x1427a78, dbname=0x14279b8 "postgres", username=<optimized out>)
    at postgres.c:4337
#15 0x000000000048650c in BackendRun (port=<optimized out>, port=<optimized out>) at postmaster.c:4550
#16 BackendStartup (port=0x141f650) at postmaster.c:4234
#17 ServerLoop () at postmaster.c:1739
#18 0x0000000000706b88 in PostmasterMain (argc=argc@entry=3, argv=argv@entry=0x13f7b90) at postmaster.c:1412
#19 0x000000000048718a in main (argc=3, argv=0x13f7b90) at main.c:210

#由以上堆栈我们看到loop () from /opt/pgsql13/lib/loop.so和sleep () from /lib64/libc.so.6,
#而且也设置了ProcDiePending,所以我们只需要调用ProcessInterrupts()即可退出正在执行的loop。
(gdb)  print ProcessInterrupts()
[Inferior 1 (process 31449) exited with code 01]
The program being debugged exited while in a function called from GDB.
Evaluation of the expression containing the function
(ProcessInterrupts) will be abandoned.

再次查看原来的session:

#已被终止,并重置了session
postgres=# select loop();
^CCancel request sent
  

FATAL:  terminating connection due to administrator command
server closed the connection unexpectedly
        This probably means the server terminated abnormally
        before or while processing the request.
The connection to the server was lost. Attempting reset: Succeeded.

#查看正在执行的语句已经消失不在
postgres=# SELECT pid, query FROM pg_stat_activity WHERE query LIKE '%loop%';
  pid  |                               query                                
-------+--------------------------------------------------------------------
 31853 | SELECT pid, query FROM pg_stat_activity WHERE query LIKE '%loop%';
修改代码,做一个可以中断取消的例子:
-- loop_01.c内容如下
#include "postgres.h"
#include "fmgr.h"
#include "miscadmin.h"
 
#include <unistd.h>
 
PG_MODULE_MAGIC;
 
PG_FUNCTION_INFO_V1(loop);
 
Datum loop(PG_FUNCTION_ARGS)
{
    /* 加入CHECK_FOR_INTERRUPTS,每两秒检查一次中断,以便可以手动终止 */
    while(1)
    {
        CHECK_FOR_INTERRUPTS();
        sleep(2);
    }
}
-- 方便起见,共享库文件还是用loop.so
gcc -I /opt/pgsql13/include/server/ -fPIC -shared -o loop.so loop_01.c

mv /home/postgres/loop.so /opt/pgsql13/lib/

--psql中可以取消终止
postgres=# select loop();
^CCancel request sent
ERROR:  canceling statement due to user request

--pg_terminate_backend也可以正常终止
postgres=#   SELECT pid, query FROM pg_stat_activity WHERE query LIKE '%loop%';
  pid  |                               query                                
-------+--------------------------------------------------------------------
   322 | select loop();
 31853 | SELECT pid, query FROM pg_stat_activity WHERE query LIKE '%loop%';
(2 rows)

postgres=#  SELECT pg_terminate_backend(322);
 pg_terminate_backend 
----------------------
 t
(1 row)

postgres=#   SELECT pid, query FROM pg_stat_activity WHERE query LIKE '%loop%';
  pid  |                               query                                
-------+--------------------------------------------------------------------
 31853 | SELECT pid, query FROM pg_stat_activity WHERE query LIKE '%loop%';
(1 row)

通过以上实例, 我们知道取消查询就是向后端发送一个SIGINT信号。如果SIGINT和SIGTERM都不能中断后端进程,我们可以用gdb连接到挂起的后端进程,并直接调用ProcessInterrupts()使其退出。

参考:
https://www.postgresql.org/docs/current/protocol-flow.html#id-1.10.5.7.9
https://postgreshelp.com/operating-system-kill-signals-on-postgresql/
https://www.cybertec-postgresql.com/en/cancel-hanging-postgresql-query/

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐