优化 Workerman 检查主进程是否存活的逻辑
主要新增了判断进程是否为 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);
}
}
当收到 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 说明没有运行中的进程,就可以执行停止命令,再启动 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 进程。
相关链接
- already running
- Optimize the logic of checking whether the master is alive
- 优化 Workerman 检查主进程是否存活的逻辑
优化 Workerman 检查主进程是否存活的逻辑的更多相关文章
- 360等杀掉了app的主进程后 ,如何自动开启 如何防止被kill
如何阻止360等进程查杀工具停止App后台进程安全软件优化内存时需要关闭没用的进程既然你同意使用360,,也允许了360的最高权限..那么他就有足够的权限来杀掉app后台进程. 一 如何保证app进程 ...
- 【学习笔记】启动Nginx、查看nginx进程、查看nginx服务主进程的方式、Nginx服务可接受的信号、nginx帮助命令、Nginx平滑重启、Nginx服务器的升级
1.启动nginx的方式: cd /usr/local/nginx ls ./nginx -c nginx.conf 2.查看nginx的进程方式: [root@localhost nginx] ...
- Android SharePreference 在主进程和次进程间共享数据不同步出错
SharedPreference作为android五大存储(网络,数据库,文件,SharedPreference,contentProvider)之中最方便使用的一个,从类名上来看就不是一个存储大 ...
- WPF工作笔记:本地化支持、主进程通知、两种最常用异步编程方式
1.本地化支持 (1)重写控件默认的依赖属性LanguageProperty FrameworkElement.LanguageProperty.OverrideMetadata( typeof(Fr ...
- kill -9杀掉nginx主进程、reload失败解决办法
前言: 无意间使用 kill -9 命令杀掉了nginx的主进程,当我再次使用 ./nginx -s reload 重新刷新nginx的时候,一直出现了下面的错误信息: nginx: [alert] ...
- 【LINUX】主进程、父进程、子进程、守护进程的概念
一.摘要 详解父进程.子进程.守护进程的区别,例子稍候补充 二.定义区别 主进程 程序执行的入口,可以理解为常用的main 函数 父进程 对于子进程而言, 子进程的创造者,可有多个子进程. 任何进程都 ...
- 向 Nginx 主进程发送 USR1 信号
[1]Nginx重新打开日志文件 向 Nginx 主进程发送 USR1 信号.USR1 信号是重新打开日志文件: 方式一: kill -USR1 $(cat /usr/local/lib/ubcsrv ...
- electron 主进程,和渲染进程的通信
ipcMain https://electronjs.org/docs/api/ipc-main 当在主进程中使用时,它处理从渲染器进程(网页)发送出来的异步和同步信息, 当然也有可能从主进程向渲染进 ...
- Python开发【笔记】:关于子线程(子进程)与主线程(主进程)的关联
前言: 主要分析下面的问题: 主线程启线程 主线程执行完毕,会关闭子线程吗? 子线程启线程 主线程执行完毕,会结束吗? 主进程启动进程,主进程执行完毕,会怎样? 子进程启动进程,进程执行完毕,又会 ...
随机推荐
- java并发编程工具类JUC第七篇:BlockingDeque双端阻塞队列
在之前的文章中已经为大家介绍了java并发编程的工具:BlockingQueue接口.ArrayBlockingQueue.DelayQueue.LinkedBlockingQueue.Priorit ...
- 28.qt quick-ListView高仿微信好友列表和聊天列表
1.视图模型介绍 在Qml中.常见的View视图有: ListView: 列表视图,视图中数据来自ListModel.XmlListModel或c++中继承自QAbstractItemModel或Q ...
- 『动善时』JMeter基础 — 43、JMeter对数据库的查询操作
目录 1.使用"用户自定义变量"实现参数化 2. 在SQL Query中使用占位符传递参数 (1)传递的参数值是常量 (2)传递的参数值是变量 3.Variables names参 ...
- nacos 实战(史上最全)
文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...
- 【dp】10-8题解 vacation
vacations 原题codeforeces round 363 (Div2) c 题目描述 暑假到了, Pb 正在计划他的假期. Pb 准备假期去体育馆锻炼或看电影.但体育馆和电影院都有可能当天不 ...
- 关于kubernetes的十七个实验(一)
实验综述 Kubernetes用来管理云平台上的容器化应用,这里从 https://www.katacoda.com/courses/kubernetes 学习Kubernetes的使用,对Kuber ...
- windows+R键的应用
windows+R:然后输入以下几个命令 1.cmd :用于Windows命令行操作,比如:ping某个网络,看看是不是通的,或者directory等等Windows命令行操作 2.远程桌面连接:ms ...
- 安装redHat6.5详细图文教程
进入VM虚拟机,双击进入已经创建好的红帽6虚拟机 双击进入CD/DVD,准备添加红帽6.5的iso镜像文件 [红帽6.5的iso镜像文件需要先下载,redhat_6.5下载地址:https:/ ...
- C#winform的Richtextbox控件实现自动滚动到最后一行功能
这里有两种情况 如果是采用的是richtextbox的AppendText的方法添加的内容,则只需 设置HideSelection为false 如果采用的是其他添加内容的方法,则需要添加TextCha ...
- 百炼 POJ2393:Yogurt factory【把存储费用用递推的方式表达】
2393:Yogurt factory 总时间限制: 1000ms 内存限制: 65536kB 描述 The cows have purchased a yogurt factory that m ...