优化 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开发【笔记】:关于子线程(子进程)与主线程(主进程)的关联
前言: 主要分析下面的问题: 主线程启线程 主线程执行完毕,会关闭子线程吗? 子线程启线程 主线程执行完毕,会结束吗? 主进程启动进程,主进程执行完毕,会怎样? 子进程启动进程,进程执行完毕,又会 ...
随机推荐
- 狂神说Mybatis笔记
环境说明: jdk 8 + MySQL 5.7.19 maven-3.6.1 IDEA 学习前需要掌握: JDBC MySQL Java 基础 Maven Junit 第一节:入门 什么是MyBati ...
- 【NX二次开发】移动WCS坐标系
说明:移动WCS坐标系 用法: #include <uf.h> #include <uf_csys.h> extern DllExport void ufusr(char *p ...
- 【NX二次开发】常用的标准对话框
1.uc1601 单按钮模态对话框 1 //来自"王牌飞行员_里海"的测试源码(qq群753801561) 2 extern DllExport void ufusr(char * ...
- 学习响应式编程 Reactor (3) - reactor 基础
Reactor Reactor 项目的主要 artifact 是 reactor-core,这是一个基于 Java 8 的实现了响应式流规范的响应式库. Reactor 提供了实现 Publisher ...
- Springboot自定义starter打印sql及其执行时间
前面写到了通过实现mybatis提供的org.apache.ibatis.plugin.Interceptor接口实现了打印SQL执行时间,并格式化SQL及其参数,如果我们使用的是ssm还得再配置文件 ...
- 「模拟8.23」one递推,约瑟夫
前置芝士约瑟夫问题 这样大概就是板子问题了 考场的树状数组+二分的60分暴力??? 1 #include<bits/stdc++.h> 2 #define int long long 3 ...
- C#WebApi的创建与发布
VS中新建项目-Web-ASP.NET Web应用程序 然后确定,选择空模版就可以了,勾上Webapi(也可以选择webapi模板,这样生成的文件比较多) 添加好之后Controllers和Model ...
- hive学习笔记之三:内部表和外部表
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- Kubernetes的资源管理
本节讲解为一个pod配置资源的预期使用量和最大使用量.通过设置这两组参数,可以确保pod公平地使用Kubernetes集群资源,同时也影响着整个集群pod的调度方式. 1.为pod中的容器申请资源 创 ...
- git schnnel failed to receive handshake, SSLTLS connection failed
git schnnel failed to receive handshake, SSLTLS connection failed 报错,查看原因为git安装时ssl选择的不是openssl.重新安装 ...