主要新增了判断进程是否为 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 退出时会清理掉这个文件,但是该用户重启服务器后文件并没有被清理,导致 Workerman 误认为已经在运行中。

作者给出了一个补救方法:手动删除记录主进程 PID 的文件。虽然临时解决了问题,但是每次出现都要去手动处理一下,感觉不太友好。

要想解决这个问题,首先得弄清楚两个问题:

  • 为什么 Workerman 没有清理 PID 文件?
  • 为什么重启服务器后启动 Workerman 提示已经在运行中?

Workerman 判断是否已运行的逻辑

Workerman 在启动的时候会生成一个文件,用于记录主进程的 PID。

// Start file.
$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);
}
}

当收到 SIGINTSIGTERMSIGHUP 等信号时,将进程状态设置为 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 说明没有运行中的进程,就可以执行停止命令,再启动 Workerman。

受到这个方法启发,然后基于它改造了 Workerman 检查主进程是否存的逻辑,一顿复制粘贴之后就有了第一版的代码:

// Get master process PID.
$master_pid = \is_file(static::$pidFile) ? \file_get_contents(static::$pidFile) : 0;
// Master is still alive?
if (static::checkMasterIsAlive($master_pid)) {
if ($command === 'start') {
static::log("Workerman[$start_file] already running");
exit;
}
} /**
* Check master process is alive
*
* @param $master_pid
* @return bool
*/
protected static function checkMasterIsAlive($master_pid)
{
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;
} // Master process will send SIGUSR2 signal to all child processes.
\posix_kill($master_pid, SIGUSR2);
// Sleep 1 second.
\sleep(1); return stripos(static::formatStatusData(), 'PROCESS STATUS') !== false;
}

逻辑跟 shell 脚本差不多,就不再解释了。这个解决方法也有两个小问题:

  • 执行命令时会延迟一秒钟,因为执行一些命令的时候需要 sleep 一秒钟等待子进程写入状态信息。
  • 如果另一个进程的 PID 与 Workerman 的旧 PID 相同,它将接收 SIGUSR2 信号。

感觉在启动的时候慢一秒应该还能接受,只要处理请求的时候不慢就行了,于是就提交了 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.

没过多久作者便在 PR 下面回复了我:

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.

先说了延迟一秒钟的问题,接着又给出了更好的解决方法:读取 /proc/PID 信息来确定它是其它进程还是 Workerman 进程。

搜索资料之后发现可以读取 /proc/PID/cmdline 得到启动进程时的命令。Workerman 在启动时会调用 Worker::setProcessTitle() 方法覆盖 cmdline 的内容,所以实际上得到的是 Workerman 的进程名称。

只需要判断 cmdline 是否包含 Worker::$processTitle 就可以知道该进程是否为 Workerman 进程。

因为进程名称可能会被截取掉,所以这里用的是包含而不是等于。

protected static function checkMasterIsAlive($master_pid)
{
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;
}

再次提交,没过多久就收到了代码被合并的邮件。

总结

回答一下上面提出的两个问题:

Q:为什么 Workerman 没有清理 PID 文件?

A:因为 Workerman 没有正常退出(强制关机、重启、断电)

Q:为什么重启服务器后启动 Workerman 提示已经在运行中?

A:因为服务器重启后,其他进程的 PID 与 Workerman 的旧 PID 相同,误认为是 Workerman 进程。

相关链接

优化 Workerman 检查主进程是否存活的逻辑的更多相关文章

  1. 360等杀掉了app的主进程后 ,如何自动开启 如何防止被kill

    如何阻止360等进程查杀工具停止App后台进程安全软件优化内存时需要关闭没用的进程既然你同意使用360,,也允许了360的最高权限..那么他就有足够的权限来杀掉app后台进程. 一 如何保证app进程 ...

  2. 【学习笔记】启动Nginx、查看nginx进程、查看nginx服务主进程的方式、Nginx服务可接受的信号、nginx帮助命令、Nginx平滑重启、Nginx服务器的升级

     1.启动nginx的方式: cd /usr/local/nginx ls ./nginx -c nginx.conf 2.查看nginx的进程方式: [root@localhost nginx] ...

  3. Android SharePreference 在主进程和次进程间共享数据不同步出错

      SharedPreference作为android五大存储(网络,数据库,文件,SharedPreference,contentProvider)之中最方便使用的一个,从类名上来看就不是一个存储大 ...

  4. WPF工作笔记:本地化支持、主进程通知、两种最常用异步编程方式

    1.本地化支持 (1)重写控件默认的依赖属性LanguageProperty FrameworkElement.LanguageProperty.OverrideMetadata( typeof(Fr ...

  5. kill -9杀掉nginx主进程、reload失败解决办法

    前言: 无意间使用 kill -9 命令杀掉了nginx的主进程,当我再次使用 ./nginx -s reload 重新刷新nginx的时候,一直出现了下面的错误信息: nginx: [alert] ...

  6. 【LINUX】主进程、父进程、子进程、守护进程的概念

    一.摘要 详解父进程.子进程.守护进程的区别,例子稍候补充 二.定义区别 主进程 程序执行的入口,可以理解为常用的main 函数 父进程 对于子进程而言, 子进程的创造者,可有多个子进程. 任何进程都 ...

  7. 向 Nginx 主进程发送 USR1 信号

    [1]Nginx重新打开日志文件 向 Nginx 主进程发送 USR1 信号.USR1 信号是重新打开日志文件: 方式一: kill -USR1 $(cat /usr/local/lib/ubcsrv ...

  8. electron 主进程,和渲染进程的通信

    ipcMain https://electronjs.org/docs/api/ipc-main 当在主进程中使用时,它处理从渲染器进程(网页)发送出来的异步和同步信息, 当然也有可能从主进程向渲染进 ...

  9. Python开发【笔记】:关于子线程(子进程)与主线程(主进程)的关联

    前言: 主要分析下面的问题: 主线程启线程  主线程执行完毕,会关闭子线程吗? 子线程启线程  主线程执行完毕,会结束吗? 主进程启动进程,主进程执行完毕,会怎样? 子进程启动进程,进程执行完毕,又会 ...

随机推荐

  1. swagger 注解使用

    @Api() 用于类:表示标识这个类是swagger的资源 tags–表示说明 value–也是说明,可以使用tags替代 但是tags如果有多个值,会生成多个list @ApiOperation() ...

  2. DHCP的原理与配置

    DHCP 动态主机配置协议(Dynamic Host Configuration Protocol) 可以减少管理员的工作量 避免用户手工配置网络参数时造成的地址冲突 DHCP报文类型: 报文类型   ...

  3. 五、SELinux安全防护

    rwx 针对用户和组   SELinux  针对程序 targeted:定义网络程序规则   minimum:限制少量软件   mls:限制全部,没定义的全拒绝 [root@proxy ~]# vim ...

  4. 12:media配置以及后端指定资源暴露

    django需要用到的静态文件默认都是放在static目录下 而针对后期用户上传的静态文件也应该统一存储 # media配置:规定用户上传的静态文件存储位置 MEDIA_ROOT = os.path. ...

  5. Winform中用户自定义控件中外部设置子控件属性的方法

    假设我们新建立一个用户自定义控件,由一个lable1和pictureBox1组成 此时我们在外部调用该控件,然后想动态改变lable1的值,我们该怎么办? 假设实例化的用户控件名为UserContro ...

  6. JavaScript的核心语法

    1.JavaScript同其他程序设计语言一样,有着独特的语法结构,主要包含:变量.数据类型.运算符号.控制语句和注释等. 2.变量是存储数据的基本单位,JavaScript通常利用变量来参与j各种运 ...

  7. Golang编写Windows动态链接库(DLL)及C调用范例

    一.准备. 1.GoLang在1.10版本之后开始支持编译windows动态链接库,可以打开命令行工具使用go version 查看自己的go版本. 2.你的电脑上需要gcc,如果没有的话[点击这里] ...

  8. 1、mysql基础入门(1)

    1.mysql基础入门: 1.1.数据库介绍:

  9. 11、文件比较与同步工具(FreeFileSync)

    11.1.基本介绍: 1.FreeFileSync是一个用于文件同步的免费开源程序.FreeFileSync通过比较其内容,日期或文件大小上的一个或多个文件夹,然 后根据用户定义的设置同步内容.除了支 ...

  10. 使用Retrofit上传图片

    Retrofit使用协程发送请求参考文章 :https://www.cnblogs.com/sw-code/p/14451921.html 导入依赖 app的build文件中加入: implement ...