主な変更点は、プロセスがWorkermanプロセスかどうかを判定するロジックを追加し、メインプロセスが生存しているかどうかの判定精度を向上させました。
問題の発見
年前、GitHubを閲覧していたところ、Workermanに2017年から開いているIssueを発見しました:already running。内容は以下の通りです:
Where is the problem?! I reboot the server and it is the first time I want to run workerman
php index.php start -d
The result is
Workerman[index.php] start in DAEMON mode
Workerman[index.php] already running
要約すると、サーバーを再起動した後で初めてWorkermanを起動すると既に実行中という警告が表示されますが、実際には実行されていません。
通常、Workerman終了時にPIDファイルをクリーンアップしますが、サーバーを再起動した場合はこのファイルがクリーンアップされず、Workermanが既に実行中と誤認してしまいます。
作者は応急処置として、手動でメインプロセスのPIDファイルを削除するように提案しました。一時的には解決しますが、都度手動で対応するのは面倒です。
この問題を解決するために、まず2つの点を明らかにする必要があります:
- なぜWorkermanはPIDファイルをクリーンアップしないのか?
- なぜサーバー再起動後にWorkermanを起動すると既に実行中と表示されるのか?
Workermanが既に実行中かどうかを判定するロジック
Workerman起動時にメインプロセスのPIDを記録するためのファイルを生成します。
// 起動ファイル
$backtrace = \debug_backtrace();
static::$_startFile = $backtrace[\count($backtrace) - 1]['file'];
// ファイル名を生成
$unique_prefix = \str_replace('/', '_', static::$_startFile);
// メインプロセスのPIDを記録するファイルパス
if (empty(static::$pidFile)) {
static::$pidFile = __DIR__ . "/../$unique_prefix.pid";
}
その後、Workermanが既に実行中かどうかを確認します。
// メインプロセスのPIDを取得。ファイルが存在しないか正常なファイルでない場合は0を返す
$master_pid = \is_file(static::$pidFile) ? \file_get_contents(static::$pidFile) : 0;
// PIDが存在する場合、信号`0`を送信。信号量`0`はpingのようにプロセスの生存確認に使用
// 現在のプロセスPIDがファイル记录的PIDと等しくないかを判定(等しくない場合はWorkermanが既に実行中で、さらにコマンドを実行しようとしている)
$master_is_alive = $master_pid && \posix_kill($master_pid, 0) && \posix_getpid() !== $master_pid;
if ($master_is_alive) {
// メインプロセスが生存しており、コマンドがstartの場合、Workerman実行中警告を表示して終了
if ($command === 'start') {
static::log("Workerman[$start_file] already running");
exit;
}
} elseif ($command !== 'start' && $command !== 'restart') {
// メインプロセスが生存しておらず、コマンドがstartでもrestartでもない場合、Workerman未実行警告を表示して終了
static::log("Workerman[$start_file] not run");
exit;
}
一連のチェックが通った後、メインプロセスのPIDを保存します。
protected static function saveMasterPid()
{
// Linux以外ではPIDを保存しない
if (static::$_OS !== \OS_TYPE_LINUX) {
return;
}
// メインプロセスのPIDを取得
static::$_masterPid = \posix_getpid();
// メインプロセスのPIDをファイルに書き込む
if (false === \file_put_contents(static::$pidFile, static::$_masterPid)) {
throw new Exception('can not save pid to ' . static::$pidFile);
}
}
SIGINT、SIGTERM、SIGHUPなどの信号を受け取った場合、プロセス状態をSTATUS_SHUTDOWNに設定し、子プロセスに終了を通知します。
メインプロセスの状態がSTATUS_SHUTDOWNで、すべての子プロセスが終了した場合、PIDファイルをクリアして終了します。
protected static function exitAndClearAll()
{
foreach (static::$_workers as $worker) {
$socket_name = $worker->getSocketName();
if ($worker->transport === 'unix' && $socket_name) {
list(, $address) = \explode(':', $socket_name, 2);
@\unlink($address);
}
}
// PIDファイルを削除
@\unlink(static::$pidFile);
static::log("Workerman[" . \basename(static::$_startFile) . "] has been stopped");
if (static::$onMasterStop) {
\call_user_func(static::$onMasterStop);
}
// プロセスを終了
exit(0);
}
問題の再現
ここで疑問を持つ方もいるでしょう。PIDファイルをクリーンアップする仕組みがあるなのに、なぜPIDを取得できるのか?
まず仮想環境でテストを行いました。サーバーが再起動的时候会发送SIGTERM信号をプロセスに通知し、Workermanは正常に終了してPIDファイルをクリーンアップできます。
しかし、クラウドサーバーでテストする際、「強制再起動」を選択すると、Workermanが信号を受け取れなくなり、exitAndClearAll()のコードを実行できなくなります。
サーバー提供者からの注意:強制再起動会导致云服务器中未保存的数据丢失,请谨慎操作。
なぜPIDファイルのプロセスに信号を送信するとtrueが返ってくるのか?
サーバーが再起動後、別のプロセスが起動し、そのPIDがWorkermanの古いPIDと同じだからです(このようにたまたま重複することはあり得ます)。
したがって、メインプロセスが生存しているかを確認する際に、そのプロセスがWorkermanのプロセスかどうかも判定する必要があります。
問題解決
Issueで@detainがshellスクリプトを使用した解決策を提案しました:
To check to see if its running and safely remove pid files can do something like:
if [ $(php start.php status 2>/dev/null|grep "PROCESS STATUS"|wc -l) -eq 0 ]; then
# clean up old run, remove pid file or run a stop command?
php start.php stop
php start.php start -d
fi
まずphp start.php statusコマンドでWorkermanの状態を取得し、PROCESS STATUSの出現回数をカウントします(各プロセスにはPROCESS STATUSがあります)。回数が0の場合、実行中のプロセスがないことになるため、stopコマンドを実行してからWorkermanを起動できます。
この方法に触発されて、Workermanがメインプロセスが生存しているかどうかを確認するロジックを改良し、複製と貼り付けを繰り返して最初のバージョンを実装しました:
// メインプロセスのPIDを取得
$master_pid = \is_file(static::$pidFile) ? \file_get_contents(static::$pidFile) : 0;
// メインプロセスが生存しているか?
if (static::verifyProcessAlive($master_pid)) {
if ($command === 'start') {
static::log("Workerman[$start_file] already running");
exit;
}
}
/**
* メインプロセスが生存しているか確認
*
* @param int $master_pid
* @return bool
*/
protected static function verifyProcessAlive(int $master_pid): bool
{
if (empty($master_pid)) {
return false;
}
$master_is_alive = $master_pid && \posix_kill($master_pid, 0) && \posix_getpid() !== $master_pid;
if (!$master_is_alive) {
return false;
}
// メインプロセスはすべての子プロセスにSIGUSR2信号を送信
\posix_kill($master_pid, SIGUSR2);
// 1秒待機
\sleep(1);
return stripos(static::formatStatusData(), 'PROCESS STATUS') !== false;
}
ロジックはshellスクリプトとほぼ同じなので、ここでは説明を省きます。この解決策には2つの小さな問題があります:
- コマンド実行時に1秒間の遅延が発生します。これは子プロセスがステータス情報を書き込むのを待つためです。
- 別のプロセスのPIDがWorkermanの古いPIDと同じ場合、そのプロセスがSIGUSR2信号を受け取ります。
起動時に1秒遅れるのは許容できると判断し、リクエスト処理時に遅くなければ問題ないと思い、PRを提出しました。使用目的と発生する問題点を記載しました:
Fixed: #125
There is a problem:
if another process starts and the pid is the same as the workerman's old pid, it will receive the SIGUSR2 signal.
しばらくすると作者から返信がありました:
Thank you for your pr.
There is a problem:
if another process starts and the pid is the same as the workerman's old pid, it will receive the SIGUSR2 signal.
If the PR is merged, some commands will be delayed by one second.
I think a better way is to read /proc/PID information to determine whether it is a PHP process or a workerman process.
まず1秒間の遅延問題について言及し、より良い解決策を提案しました:/proc/PID情報を読み込んで、PHPプロセスかWorkermanプロセスかを判定します。
資料を調査した結果、/proc/PID/cmdlineを読み取ることでプロセス起動時のコマンドを取得できることがわかりました。Workerman起動時にWorker::setProcessTitle()メソッドを呼び出してcmdlineの内容をオーバーライドするため、実際にはWorkermanのプロセス名を取得できます。
cmdlineがWorker::$processTitleを含むかどうかを判定するだけで、そのプロセスがWorkermanプロセスかどうかわかります。
プロセス名が截断される可能性があるため、包含而不是等于(等しいではなく含む)を使用します。
protected static function verifyProcessAlive(int $master_pid): bool
{
if (empty($master_pid)) {
return false;
}
// プロセスが生存しているかを確認
$master_is_alive = $master_pid && \posix_kill($master_pid, 0) && \posix_getpid() !== $master_pid;
if (!$master_is_alive) {
return false;
}
// ここに至った場合、プロセスは生存しているが、Workermanプロセスである保証はない
// プロセス情報を読み取ることで確定する必要がある。哪个步骤导致プロセス情報を取得できない場合でもtrueを返す
// 이유는 이전 检测结果에 따라 프로세스가 생존하기 때문이다
$cmdline = "/proc/{$master_pid}/cmdline";
// プロセス情報が読み取れないか、設定されたプロセス名が空
if (!is_readable($cmdline) || empty(static::$processTitle)) {
return true;
}
$content = file_get_contents($cmdline);
// プロセス情報を読み取れなかった
if (empty($content)) {
return true;
}
// プロセス名を含んでいるかどうかを判定
return stripos($content, static::$processTitle) !== false;
}
再度提出したところ、ほどなくしてマージされたという通知を受け取りました。
まとめ
上記の2つの質問への回答:
Q:なぜWorkermanはPIDファイルをクリーンアップしないのか?
A:Workermanが正常に終了していないため(強制シャットダウン、再起動、電源切断)
Q:なぜサーバー再起動後にWorkermanを起動すると既に実行中と表示されるのか?
A:サーバー再起動後、他のプロセスのPIDがWorkermanの古いPIDと偶然重複し、Workermanプロセスと誤認されるため。
関連リンク
already running Optimize the logic of checking whether the master is alive 优化 Workerman 检查主进程是否存活的逻辑