惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Security Archives - TechRepublic
Security Archives - TechRepublic
P
Privacy & Cybersecurity Law Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
W
WeLiveSecurity
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
L
LINUX DO - 热门话题
C
Cybersecurity and Infrastructure Security Agency CISA
S
Security Affairs
Latest news
Latest news
Security Latest
Security Latest
N
News and Events Feed by Topic
Spread Privacy
Spread Privacy
P
Proofpoint News Feed
T
The Blog of Author Tim Ferriss
Y
Y Combinator Blog
Google DeepMind News
Google DeepMind News
www.infosecurity-magazine.com
www.infosecurity-magazine.com
T
The Exploit Database - CXSecurity.com
The Last Watchdog
The Last Watchdog
C
Cyber Attacks, Cyber Crime and Cyber Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
V
Vulnerabilities – Threatpost
Hacker News - Newest:
Hacker News - Newest: "LLM"
Microsoft Azure Blog
Microsoft Azure Blog
V
Visual Studio Blog
The Cloudflare Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
G
GRAHAM CLULEY
博客园_首页
S
Secure Thoughts
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
AWS News Blog
AWS News Blog
腾讯CDC
D
Darknet – Hacking Tools, Hacker News & Cyber Security
The Register - Security
The Register - Security
N
News and Events Feed by Topic
A
Arctic Wolf
MongoDB | Blog
MongoDB | Blog
爱范儿
爱范儿
Project Zero
Project Zero
A
About on SuperTechFans
罗磊的独立博客
云风的 BLOG
云风的 BLOG
Know Your Adversary
Know Your Adversary
S
Security @ Cisco Blogs
Google Online Security Blog
Google Online Security Blog
K
Kaspersky official blog
L
LINUX DO - 最新话题
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
F
Fortinet All Blogs

文章列表 - 她和她的猫

使用小鹤双拼一个月 折腾 Hugo PaperMod 主题 去长鹿旅游休博园 1Password 自动化登录堡垒机 深入 Hyperf:Inject 注解是如何工作的? 译|理解容器镜像层 使用 Rust 实现高性能预写日志(Write Ahead Log) 译|使用 Linux 命名空间隔离系统 深入 Hyperf:HTTP 服务是如何处理请求的? Linux 文件权限学习笔记 译|了解 Bitcask:基于日志结构的 KV 存储引擎 还原 SM2 压缩公钥的几种方法 深入 Hyperf:HTTP 服务启动时发生了什么? 一次 Hyperf 注解失效问题分析 将博客迁移到又拍云 译|PHP 7 新的 Hashtable 实现 排查 ES 查询问题:深入了解 json_encode() 函数 浅析 Redlock 分布式锁实现原理 Yar 源码阅读笔记:RPC 服务端 Yar 源码阅读笔记:客户端的并行调用 Yar 源码阅读笔记:客户端的同步调用 Yar 源码阅读笔记:数据传输模块 Yar 源码阅读笔记:消息编码模块 Yar 源码阅读笔记:RPC 通信协议 Yar 源码阅读笔记:开篇 PHP 8 新特性介绍 基于 OpenSSL 实现国密 SM4 加解密 PHP 源码阅读笔记:编译与调试 PHP 什么是惊群问题 大端模式和小端模式 基于 GitHub Actions 定时推送网址到百度站长平台 聊聊五种 I/O 模型 Workerman 源码分析:文件上传 浅入浅出 HTTP 协议 PHP 网络编程:构建 MySQL 蜜罐获取攻击者微信 ID 使用 Workerman 接入 Bilibili 直播弹幕协议 使用 GitHub Actions 自动部署 Hexo 什么是二进制安全 【转载】PHP 程序员进阶之路 基于 Redis 实现分布式锁 《PHP 实现 Base64 编码/解码》笔记 PHP 多进程下载必应壁纸 基于 Redis 实现延迟队列 【转载】PHP 实现 Base64 编码/解码 使用 MySQL FIELD() 函数自定义排序规则 Laravel 的 Facades 实现原理 C语言单链表实现约瑟夫环 数据结构学习笔记:顺序栈和链栈(C语言) 使用 QueryList + Redis 下载壁纸 在 Laravel 登录/注册中使用 mews/captcha 扩展包 使用 mews/captcha 扩展包为 Laravel 应用添加图片验证码 给 YOURLS 短网址系统编写插件《批量生成短网址》 给 YOURLS 短网址系统编写插件《Hello World!》 PHP 7.1 使用 json_encode 函数造成浮点类型数据出现精度问题 Laravel 授权策略(Policy)的基本使用 Ubuntu 系统下 WiFi 频繁掉线解决方法 Laravel 应用部署到 Nginx 服务器上的第一个坑 Ubuntu 16.04 LST 安装 Redis 和 Composer PHP 采集逐浪小说章节列表 Nginx 环境 SSL 强制 HTTP 301 跳转到 HTTPS 【工作&生活】第一周 ServiceStack.Redis 与多线程 Count the number of Duplicates C# Base64 加密及解密 ASP.NET MVC 使用动态类型传递数据 Django 使用 order_by() 对数据进行排序操作 ASP.NET + jQuery + AJAX 实现用户登录 ASP.NET 从客户端中检测到有潜在危险的 Request.Form 值 ASP.NET 使用 FormData 实现 AJAX 上传图片 Python 使用 BeautifulSoup 抓取网页 贪心算法之字符串的完美度 一言不和就造轮子之 CookieHelper ASP.NET 上传图片 DBHelper 类 C# 捕获屏幕源码 用 JavaScript 做一些有趣的事情 第一篇文章 好久不见。 如何防止非法调用 PHP 文件 清明小记。 PHP 生成随机字符串 Z-Blog PHP 程序做 301 跳转 PHP 处理数组常用的几个函数 Sublime Text 3 快捷键总结 PHP 使用 file_get_contents() 函数实现采集网页 使用 C# 编写学生信息管理系统 终于到了。 zblog报错: magic_quotes_gpc 和 safe_mode 世事无常。 如何使用 PHP 上传文件 安卓手机编写和运行 PHP 程序的软件 使用 AJAX 刷新、验证 PHP 图片验证码 PHP 生成图片验证码 PHP 之 session 的使用方法 PHP 之 cookie 的使用方法 PHP 实现分页原理详解 Thinkphp 框架中系统常量在 Js 文件中不解析的解决方法
优化 Workerman 检查主进程是否存活的逻辑
她和她的猫 · 2021-03-17 · via 文章列表 - 她和她的猫

主要新增了判断进程是否为 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 在启动的时候会生成一个文件,用于记录主进程的 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";
}

// 设置主进程名称(记住这个进程名称,后面会用到)
static::setProcessTitle(static::$processTitle . ': master process  start_file=' . static::$_startFile);

然后检查 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 的进程名称。

/**
 * Set process name.
 *
 * @param string $title
 * @return void
 */
protected static function setProcessTitle($title)
{
    // 设置一个空的错误处理函数,避免提示 PHP 错误
    \set_error_handler(function(){});
    // >=php 5.5
    if (\function_exists('cli_set_process_title')) {
        \cli_set_process_title($title);
    } // Need proctitle when php<=5.5 .
    elseif (\extension_loaded('proctitle') && \function_exists('setproctitle')) {
        \setproctitle($title);
    }
    // 还原错误处理函数
    \restore_error_handler();
}

默认情况下,进程名称将被设置为 WorkerMan: master process start_file=启动文件的完整路径。

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

cmdline可以保存多少个字符跟启动命令有关,比如启动命令是 php index.php start -d,那么进程名称就有可能被截取为 WorkerMan: worker proce,所以这里用的是包含而不是等于。

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 进程。

相关链接