PostgreSQL的信号处理部分

环境

PostgreSQL 10.5:PostgreSQL 10.5 版本

中文翻译:开场白

有时会出现执行非常重的SQL导致数据库性能显著下降的情况。我尝试取消相应的后端进程查询,使用了pg_cancel_backend(pid int),但查询并未被取消。即使使用pg_terminate_backend(pid int)也无法终止进程,并重新启动请求执行该查询的进程,情况依然没有改变,最终我选择重新启动了数据库。我知道pg_cancel_backend()发送了sigint信号,pg_terminate_backend()发送了sigterm信号,但为什么查询无法被正确取消,进程无法被终止呢?于是我查看了实现。

信号相关的代码

首先,PostgreSQL有一个名为postmaster的守护进程,用于接收来自客户端的连接,并在需要时fork出后端进程(可以说所有构成PostgreSQL的进程都是从postmaster中fork出来的)。postmaster在初始化时还会进行信号屏蔽和信号处理程序的注册。后端进程从postmaster中fork出来,因此会注册针对sigint和sigterm的后端进程信号处理程序。在源代码中,可以在main()的最后调用PostmasterMain(argc, argv)函数,这个函数会执行非常多的操作(如处理选项、创建套接字等)。其中一个操作是处理信号。以下是注册信号的部分(实际上还涉及更多的信号,但在这里我们主要看sigterm和sigint)。

pqinitmask();
PG_SETMASK(&BlockSig);
                                                 * have children do same */
pqsignal_no_restart(SIGINT, pmdie); /* send SIGTERM and shut down */
pqsignal_no_restart(SIGQUIT, pmdie);    /* send SIGQUIT and die */
pqsignal_no_restart(SIGTERM, pmdie);    /* wait for children and shut down */

从上往下看。在pqinitmask()函数中,初始化了全局变量BlockSig(以及StartupBlockSig)。然后,PG_SETMASK(mask)是sigprocmask(SIG_SETMASK, mask, NULL)的宏。也就是说,进行信号阻塞。pqsignal_no_restart()将sigaction(2)结构体的第一个参数sa_flags设为0,并调用sigaction(2)函数将传入的函数作为信号处理函数。而pqsignal()将sa_flags设为SA_RESTART,并调用sigaction(2)函数将传入的函数作为信号处理函数。

PostmasterMain(argc, argv)进入一个循环,最终等待来自客户端的连接。这个循环就是ServerLoop()函数。在ServerLoop()函数中,我们使用select(2)函数来监视来自客户端的连接。然后,在以下部分,我们生成一个新的进程,并在该进程中处理查询。这就是后端进程。

Port       *port;

port = ConnCreate(ListenSocket[i]);
if (port)
{
    BackendStartup(port);

    /*
     * We no longer need the open socket or port structure
     * in this process
     */
    StreamClose(port->sock);
    ConnFree(port);
}

确认了可读写的套接字之后,我们进行路径选择。在这里,调用BackendStartup()函数来生成后端进程。实际上我们可以看到正在进行的fork(2)操作。

pid = fork_process();
if (pid == 0)               /* child */
{
    free(bn);

    /* Detangle from postmaster */
    InitPostmasterChild();

    /* Close the postmaster's sockets */
    ClosePostmasterPorts(false);

    /* Perform additional initialization and collect startup packet */
    BackendInitialize(port);

    /* And run the backend */
    BackendRun(port);
}

pid为0是子进程,因此这是后端进程的执行路径。在其中,有关信号处理的操作在BackendInitialize()函数中处理。

pqsignal(SIGTERM, startup_die);
pqsignal(SIGQUIT, startup_die);
InitializeTimeouts();       /* establishes SIGALRM handler */
PG_SETMASK(&StartupBlockSig);

从pgsignal.c中可以看出,StartupBlockSig是除了sigterm、sigquit和sigalarm之外的一个项。是的,这样一来,后端进程在启动时将startup_die注册为信号处理程序。它会执行exit(2)。

接下来我们来看一下BackendRun()。在这里调用了PostgresMain(),而在该函数中有一个注册查询处理信号处理器的部分。以下是相关片段。

pqsignal(SIGINT, StatementCancelHandler);   /* cancel current query */
pqsignal(SIGTERM, die); /* cancel current query and exit */

每个信号处理程序都被实现为以下方式。

void
StatementCancelHandler(SIGNAL_ARGS)
{
    int         save_errno = errno;

    /*
     * Don't joggle the elbow of proc_exit
     */
    if (!proc_exit_inprogress)
    {
        InterruptPending = true;
        QueryCancelPending = true;
    }

    /* If we're still here, waken anything waiting on the process latch */
    SetLatch(MyLatch);

    errno = save_errno;
}
void
die(SIGNAL_ARGS)
{
    int         save_errno = errno;

    /* Don't joggle the elbow of proc_exit */
    if (!proc_exit_inprogress)
    {
        InterruptPending = true;
        ProcDiePending = true;
    }

    /* If we're still here, waken anything waiting on the process latch */
    SetLatch(MyLatch);

    /*
     * If we're in single user mode, we want to quit immediately - we can't
     * rely on latches as they wouldn't work when stdin/stdout is a file.
     * Rather ugly, but it's unlikely to be worthwhile to invest much more
     * effort just for the benefit of single user mode.
     */
    if (DoingCommandRead && whereToSendOutput != DestRemote)
        ProcessInterrupts();

    errno = save_errno;
}

我正在做各种事情,但重要的是设置标志(InterruptPending,QueryCancelPending,ProcDiePending)。相反,没有取消查询或杀死进程的处理。我的意思是,这是针对PostgreSQL信号的处理。当接收到信号时,在信号处理程序中只设置标志。然后,定期检查该标志(当然,在不会产生问题的情况下取消查询或杀死进程),并执行与信号相对应的处理。

在PostgresMain()函数中,还执行查询操作,但是到处都会调用CHECK_FOR_INTERRUPTS()宏。该宏的定义如下。

#define CHECK_FOR_INTERRUPTS() \
do { \
    if (InterruptPending) \
        ProcessInterrupts(); \
} while(0)
#else

如果InterruptPending为真,则执行ProcessInterrupts()。如果执行sigint或者sigterm的信号处理程序,InterruptPending将设置为真,然后执行ProcessInterrupts()。当执行sigterm的信号处理程序时,ProcDiePending将设置为真,在ProcessInterrupts()内,如果ProcDiePending为真,则几乎总是会调用ereport()函数,并将错误级别设置为FATAL。当ereport()函数以FATAL错误级别被调用时,会执行exit(2),因此进程将被终止。

概括

以下是导致pg_terminate_backend()无法立即终止进程的两个原因。

sigprocmask(2)でシグナルがブロックされている時がある
シグナルハンドラーが実際に実行されてもフラグを立てるだけなので、何らかの理由で処理が止まっている場合、実際の期待する処理が行われない。

bannerAds