news 2026/10/8 7:45:58

Hyperf 信号处理(Signal Handler)实战指南:监听 Worker 与自定义进程信号,实现优雅停机与协程服务适配

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Hyperf 信号处理(Signal Handler)实战指南:监听 Worker 与自定义进程信号,实现优雅停机与协程服务适配
  • 后端
  • 微服务

【免费下载链接】hyperf

🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.

项目地址:https://gitcode.com/gh_mirrors/hy/hyperf
点击查看免费下载

导读

Hyperf 的hyperf/signal组件提供了一套基于协程的信号监听机制,它会在Worker进程和自定义(custom)进程启动后自动向信号管理器注册处理器,让开发者能够以统一的SignalHandlerInterface接口捕获SIGTERM、SIGINT等系统信号并执行自定义逻辑。本文以 docs/en/signal.md 为骨架,结合 signal 组件源码 与测试用例,完整讲解组件的安装配置、自定义处理器编写、优先级机制、优雅停机方案,以及协程风格服务下的监听器适配,帮助你在 Hyperf 项目中可靠地管理进程生命周期。


一、Signal Handler 是什么

在常驻内存的服务端程序中,进程生命周期管理依赖操作系统信号(Signal),例如:

  • SIGTERM(默认信号 15):请求进程终止,是kill命令、Docker Stop 等场景最常触发的信号;
  • SIGINT(信号 2):终端键盘Ctrl + C产生的中断信号。

Hyperf 的 Signal 组件把“某个进程类型监听到某个信号后做什么”抽象为一个个Handler(信号处理器)。它的核心能力包括:

  1. 自动注册:信号处理器会在Worker进程和自定义进程启动后,自动注册到SignalManager(见 SignalRegisterListener),无需手动初始化;
  2. 协程化等待:监听循环运行在协程中,通过EngineSignal::wait()异步等待信号,不阻塞进程主逻辑(见 SignalManager::listen());
  3. 优雅退出:提供开箱即用的WorkerStopHandler,让收到SIGTERM/SIGINT的 Worker 进程在等待业务处理完成后平滑退出。

二、安装与发布配置

1. 安装组件

composer require hyperf/signal

组件包hyperf/signal要求php >= 8.2,并依赖hyperf/contract、hyperf/coordinator、hyperf/coroutine、hyperf/engine、hyperf/stdlib、hyperf/support等 Hyperf 3.2 系列组件(见 src/signal/composer.json)。

2. 发布默认配置文件

php bin/hyperf.php vendor:publish hyperf/signal

发布命令会在项目中生成config/autoload/signal.php,其默认内容与仓库内置模板一致(见 src/signal/publish/signal.php):

<?php declare(strict_types=1); return [ 'handlers' => [ // Hyperf\Signal\Handler\WorkerStopHandler::class => PHP_INT_MIN ], 'timeout' => 5.0, ];

两个关键配置项的含义:

配置项默认值说明
handlers[]以「处理器类名 => 优先级」形式注册的处理器映射;若只写类名而不写优先级,源码中会被当作0处理(见 SignalManager::getQueue())
timeout5.0信号等待超时时间(秒),即每次EngineSignal::wait()的超时上限,同时是SignalManager读取配置signal.timeout时的兜底默认值

注意:即使不发布配置文件,SignalManager也会通过$this->config->get('signal.timeout', 5.0)使用内置的5.0秒默认超时(见 SignalManager.php),因此该配置文件是可选的。


三、编写自定义 Handler

1. 核心接口SignalHandlerInterface

所有信号处理器都必须实现 SignalHandlerInterface,该接口定义了两种进程类型常量与两个方法:

interface SignalHandlerInterface { public const WORKER = 1; // Worker 进程 public const PROCESS = 2; // 自定义进程 /** @return array [[ WORKER, SIGNAL ]] */ public function listen(): array; public function handle(int $signal): void; }
  • listen():声明要监听的「进程类型 + 信号」组合,返回形如[[WORKER, SIGTERM], [PROCESS, SIGUSR1]]的二维数组;
  • handle(int $signal):当对应信号被捕获后回调,参数为实际收到的信号值。

2. 注册方式一:#[Signal]注解

在处理器类上添加#[Signal]注解即可被自动扫描注册。注解本身只有一个可选参数priority(见 src/signal/src/Annotation/Signal.php):

#[Attribute(Attribute::TARGET_CLASS)] class Signal extends AbstractAnnotation { public function __construct(public ?int $priority = null) { } }

注册时,SignalManager::init()会通过AnnotationCollector::getClassesByAnnotation(Signal::class)收集所有带注解的类,并把注解上的priority ?? 0作为优先级插入队列(见 SignalManager::getQueue())。

3. 注册方式二:配置文件handlers

将处理器类名写入config/autoload/signal.php的handlers数组即可,同样可指定优先级:

return [ 'handlers' => [ App\Signal\TermSignalHandler::class => 100, ], 'timeout' => 5.0, ];

4. 完整示例:监听 Worker 进程的 SIGTERM

下面监听Worker进程的SIGTERM信号,并在收到信号时打印信号值(原文示例):

<?php declare(strict_types=1); namespace App\Signal; use Hyperf\Signal\Annotation\Signal; use Hyperf\Signal\SignalHandlerInterface; #[Signal] class TermSignalHandler implements SignalHandlerInterface { public function listen(): array { return [ [SignalHandlerInterface::WORKER, SIGTERM], ]; } public function handle(int $signal): void { var_dump($signal); } }

从源码角度看,该处理器被SignalManager::init()解析后,会落入handlers[WORKER][SIGTERM]分组(见 SignalManager::init()),随后listen(WORKER)会为SIGTERM单独创建一个协程持续等待信号。

5. 优先级如何生效

SignalManager使用SplPriorityQueue管理所有处理器,数值越大越先执行:

  • 配置中的处理器按handler => priority插入;
  • 注解处理器按priority ?? 0插入;
  • 未声明优先级的类默认按0处理。

测试用例 SignalManagerTest 验证了这一点:当SignalHandler2Stub::class => 1、SignalHandlerStub::class(优先级 0)同时注册时,getHandlers()[WORKER][SIGTERM][0]是优先级更高的SignalHandler2Stub实例。因此,把WorkerStopHandler配置为PHP_INT_MIN意味着它拥有最低优先级,保证其它业务处理器先完成清理,最后才执行进程停止逻辑。


四、优雅停机:WorkerStopHandler 详解

捕获SIGTERM后,如果没有任何“停止”处理器,Worker 进程会被信号直接打断,业务可能来不及收尾;而一旦被TermSignalHandler这类处理器捕获,进程又无法自行正常退出(这正是文档中“捕获后无法正常退出”的原因)。

Hyperf 提供了内置的 WorkerStopHandler 来解决该问题,其完整实现:

namespace Hyperf\Signal\Handler; use Hyperf\Contract\ConfigInterface; use Hyperf\Signal\SignalHandlerInterface; use Psr\Container\ContainerInterface; use Swoole\Server; class WorkerStopHandler implements SignalHandlerInterface { protected ConfigInterface $config; public function __construct(protected ContainerInterface $container) { $this->config = $container->get(ConfigInterface::class); } public function listen(): array { return [ [self::WORKER, SIGTERM], [self::WORKER, SIGINT], ]; } public function handle(int $signal): void { if ($signal !== SIGINT) { $time = $this->config->get('server.settings.max_wait_time', 3); sleep($time); } $this->container->get(Server::class)->stop(); } }

启用方式(在config/autoload/signal.php中注册,原文配置):

<?php declare(strict_types=1); return [ 'handlers' => [ Hyperf\Signal\Handler\WorkerStopHandler::class => PHP_INT_MIN ], 'timeout' => 5.0, ];

其工作流程可以概括为:

  1. 监听:同时监听 Worker 进程的SIGTERM与SIGINT;
  2. 等待收尾:收到非SIGINT信号(即SIGTERM)时,先sleep()等待server.settings.max_wait_time(默认3秒)——该配置即 Swoole Server 设置中的max_wait_time,用于给正在处理的请求/任务留出完成时间;收到SIGINT(Ctrl + C)时则跳过等待直接停止;
  3. 停止服务:通过容器取出Swoole\Server实例并调用stop(),触发当前进程平滑关闭。

因此,在生产环境(如 Docker/K8s 下发SIGTERM)中,注册WorkerStopHandler后即可实现“请求处理完毕再退出”的优雅停机;在本地调试时,也可以直接用Ctrl + C(SIGINT)退出。

注意:WorkerStopHandler面向异步风格服务(Swoole Server 模型)。它不适合协程风格(Coroutine Server)服务,协程场景需要自行实现或使用下文的自定义方案。


五、协程风格服务下的信号监听配置

Hyperf 支持「异步风格」与「协程风格」两种服务模型。由于协程风格服务在单 Worker 内通过协程调度,WorkerStopHandler所依赖的Swoole\Server::stop()语义并不适用,需要自定义处理器。仓库为此提供了 CoroutineServerStopHandler:

class CoroutineServerStopHandler implements SignalHandlerInterface { public function listen(): array { return [ [self::WORKER, SIGTERM], [self::WORKER, SIGINT], ]; } public function handle(int $signal): void { ProcessManager::setRunning(false); CoordinatorManager::until(Constants::WORKER_EXIT)->resume(); } }

它通过ProcessManager::setRunning(false)通知进程管理器停止运行,并通过CoordinatorManager唤醒等待WORKER_EXIT的协程,驱动协程风格服务正常收尾。

文档中的自定义实现示例

原文还给出了一种基于ServerManager的自定义协程风格停止处理器(适合需要显式关闭所有已监听服务的情形),可直接放在App\Kernel\Signal下:

<?php declare(strict_types=1); namespace App\Kernel\Signal; use Hyperf\Contract\ConfigInterface; use Hyperf\Process\ProcessManager; use Hyperf\Server\ServerManager; use Hyperf\Signal\SignalHandlerInterface; use Psr\Container\ContainerInterface; class CoroutineServerStopHandler implements SignalHandlerInterface { protected ContainerInterface $container; protected ConfigInterface $config; public function __construct(ContainerInterface $container) { $this->container = $container; $this->config = $container->get(ConfigInterface::class); } public function listen(): array { // There is only one Worker process in the coroutine style, so you only need to monitor the WORKER here. return [ [self::WORKER, SIGTERM], [self::WORKER, SIGINT], ]; } public function handle(int $signal): void { ProcessManager::setRunning(false); foreach (ServerManager::list() as [$type, $server]) { // Cyclically close open services $server->shutdown(); } } }

要点解读:

  • 协程风格服务只有一个 Worker 进程,因此listen()只需监听WORKER即可;
  • handle()首先ProcessManager::setRunning(false)让常驻协程/自定义进程感知停止意图;
  • 随后遍历ServerManager::list()中注册的每一个服务(返回[$type, $server]元组),逐一调用$server->shutdown()关闭服务端口与连接,实现平滑下线。

将该类注册进config/autoload/signal.php的handlers即可在协程风格服务中启用。


六、信号注册与注销的生命周期

信号监听并非全进程范围内“一刀切”,而是跟随进程事件精确注册与注销:

  • 注册时机:SignalRegisterListener监听三个事件(见 src/signal/src/Listener/SignalRegisterListener.php):

    • BeforeWorkerStart:Worker 进程启动前,注册WORKER组监听;
    • BeforeProcessHandle:自定义进程处理前,注册PROCESS组监听;
    • MainCoroutineServerStart:主协程服务启动时,注册WORKER组监听。

    每个监听循环都是一个独立协程(Coroutine::create),内部while (true)反复调用EngineSignal::wait($signal, timeout),收到信号后按序触发该信号下的全部处理器,直至进程退出标记被置为stopped(见 SignalManager::listen())。

  • 注销时机:SignalDeregisterListener监听OnWorkerExit、AfterProcessHandle、CoroutineServerStop、AllCoroutineServersClosed,通过SignalManager::setStopped(true)终止监听循环(见 src/signal/src/Listener/SignalDeregisterListener.php)。

这条“随事件注册、随事件注销”的链路,保证了监听循环不会在进程退出后残留协程,也不会在自定义进程中误监听本应属于 Worker 的信号。


七、验证与测试

组件自带测试 SignalManagerTest,可以直观验证处理器注册与优先级排序行为:

$manager = new SignalManager($container); $manager->init(); $this->assertArrayHasKey(SignalHandler::WORKER, $manager->getHandlers()); $this->assertArrayHasKey(SIGTERM, $manager->getHandlers()[SignalHandler::WORKER]); $this->assertInstanceOf(SignalHandler2Stub::class, $manager->getHandlers()[SignalHandler::WORKER][SIGTERM][0]); $this->assertInstanceOf(SignalHandlerStub::class, $manager->getHandlers()[SignalHandler::WORKER][SIGTERM][1]);

该用例使用 Mock 容器注册了两个监听WORKER + SIGTERM的处理器,其中SignalHandler2Stub优先级为1、SignalHandlerStub为0。断言结果[0]是SignalHandler2Stub、[1]是SignalHandlerStub,印证了SplPriorityQueue数值越大越先执行的排序规则——这也是配置WorkerStopHandler为PHP_INT_MIN能够“最后兜底”的原因所在。


八、小结

通过hyperf/signal组件,Hyperf 把进程信号管理收敛为清晰的 Handler 模型:

  1. 实现SignalHandlerInterface,用listen()声明「进程类型 + 信号」、用handle()定义响应逻辑;
  2. 通过#[Signal]注解或config/autoload/signal.php的handlers数组注册,并可借助优先级控制执行顺序;
  3. 异步风格服务启用内置WorkerStopHandler(优先级PHP_INT_MIN)实现优雅停机,并依赖server.settings.max_wait_time控制等待时长;
  4. 协程风格服务需使用CoroutineServerStopHandler或文档给出的ServerManager自定义方案;
  5. 整个监听生命周期由SignalRegisterListener/SignalDeregisterListener按进程事件自动管理,监听循环以协程方式异步运行于SignalManager中。

相关参考资源:组件源码 src/signal/src、默认配置模板 src/signal/publish/signal.php、测试用例 src/signal/tests/SignalManagerTest.php、原始文档 docs/en/signal.md。

  • 后端
  • 微服务

【免费下载链接】hyperf

🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.

项目地址:https://gitcode.com/gh_mirrors/hy/hyperf
点击查看免费下载

相关推荐

上一篇:Anchor框架程序结构深度解析
下一篇:VoxCPM开源生态盘点:从ComfyUI插件到ONNX部署的全方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 7:45:34

题解:洛谷 P1075 [NOIP 2012 普及组] 质因数分解

本文分享的必刷题目是从蓝桥云课、洛谷、AcWing等知名刷题平台精心挑选而来,并结合各平台提供的算法标签和难度等级进行了系统分类。题目涵盖了从基础到进阶的多种算法和数据结构,旨在为不同阶段的编程学习者提供一条清晰、平稳的学习提升路径。 欢迎大家订阅我的专栏:算法…

作者头像 李华
网站建设 2026/10/8 7:45:28

题解:洛谷 P1116 车厢重组

本文分享的必刷题目是从蓝桥云课、洛谷、AcWing等知名刷题平台精心挑选而来,并结合各平台提供的算法标签和难度等级进行了系统分类。题目涵盖了从基础到进阶的多种算法和数据结构,旨在为不同阶段的编程学习者提供一条清晰、平稳的学习提升路径。 欢迎大家订阅我的专栏:算法…

作者头像 李华
网站建设 2026/10/8 7:45:07

三国杀更新版本

#include<iostream> #include<cstdlib> #include<stdio.h> #include<time.h> using namespace std; int main(){srand(time(NULL));string b[8]{"杀","杀","杀","杀","杀","闪","闪…

作者头像 李华
网站建设 2026/10/8 7:43:20

IEC 104测试工具深度解析:协议栈调试与报文级故障定位

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 7:43:18

TPS259483与STM32F746ZG实现智能电源路径保护与热插拔控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 7:43:13

TPS259483 eFuse与STM32G431组合:工业电源路径保护方案全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华