環境
PostgreSQL 10.5
イントロ
非常に重いSQLが実行されてしまい、DBのパフォーマンスが著しく低下する時がありました。対応するバックエンドプロセスのクエリをキャンセルしようと思い、pg_cancel_backend(pid int)
を使ったのですが、クエリはキャンセルされず、pg_terminate_backend(pid int)
を使っても、プロセスは殺せず、そのクエリの実行をお願いしたプロセスを起動しなおしても、状況は変わらず、最終的にDBを再起動しました。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)
の第一引数となるsigaction
構造体の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);
StartupBlockSig
はpgsignal.cを見れば分かりますが、BlockSig
から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
がtrueなら、ProcessInterrupts()
を実行します。もしsigintやsigtermのシグナルハンドラーが実行されるとInterruptPending
はtrueになるので、ProcessInterrupts()
が実行されます。sigtermのシグナルハンドラーが実行されると、ProcDiePending
はtrueとなっていますが、ProcessInterrupts()
内では、ProcDiePending
はtrueとなっていると、ほぼ必ずereport()
がエラーレベルがFATAL
で呼ばれます。ereport()
がエラーレベルがFATAL
で呼ばれるとexit(2)
するので、プロセスが殺されることになります。
まとめ
pg_terminate_backend()でプロセスがすぐに殺されない理由は以下の二点です。
-
sigprocmask(2)
でシグナルがブロックされている時がある - シグナルハンドラーが実際に実行されてもフラグを立てるだけなので、何らかの理由で処理が止まっている場合、実際の期待する処理が行われない。