ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

laravel-websockets 优雅重启指南:掌握 websockets:restart 命令与软关停机制

laravel-websockets 优雅重启指南:掌握 websockets:restart 命令与软关停机制
  • 后端
  • 即时通讯

【免费下载链接】laravel-websockets

Websockets for Laravel. Done right.

项目地址:https://gitcode.com/gh_mirrors/la/laravel-websockets
点击查看免费下载

本文聚焦 laravel-websockets 包中的websockets:restart命令,讲解如何像queue:restart一样,在 Supervisor 托管环境下优雅地重启 WebSocket 服务器。读完本文,你将掌握该命令的用法、10 秒内完成重启的底层原理、优雅关停(软关停)的完整流程,以及搭配 Supervisor 实现零中断运维的实战配置。

适用场景:为什么 WebSocket 服务器需要"优雅重启"

WebSocket 服务器是一个长期驻留的守护进程。官方建议在生产环境使用 Supervisor 这类进程管理器来保证它始终存活(详见 部署文档)。当你修改了配置、更新了代码、或注册了新的自定义路由(参见 自定义 WebSocket 处理器)之后,往往需要重启服务器才能让改动生效。

此时如果直接粗暴地 kill 进程,正在连接中的客户端会立即断线,且可能丢失状态。laravel-websockets 为此提供了与 Laravel 队列系统queue:restart类似的解决方案:向正在运行的服务器广播一个重启信号,让它优雅地关闭现有连接,再交由 Supervisor 重新拉起进程。

快速上手:一条命令触发重启

根据 官方文档 的定义,使用方式极其简单,与php artisan queue:restart的体验完全一致:

php artisan websockets:restart

执行后终端会输出如下提示:

Broadcasted the restart signal to the WebSocket server!

该命令本身没有参数选项,其全部职责就是"广播重启信号"。自命令下达起,最多 10 秒内,正在运行的 WebSocket 服务器会完成软关停;随后由 Supervisor(配置了autorestart=true)自动把服务重新拉起。

底层原理一:如何"广播"重启信号

websockets:restart的实现非常精简,完整源码位于 src/Console/Commands/RestartServer.php,核心逻辑只有两步:

public function handle() { Cache::forever( 'beyondcode:websockets:restart', $this->currentTime() ); $this->info( 'Broadcasted the restart signal to the WebSocket server!' ); }
  1. 通过 Laravel 的Cache门面,以Cache::forever将当前时间戳写入固定键名beyondcode:websockets:restart(forever表示该值永不过期,直到下一次命令再次覆盖它);
  2. 在控制台输出成功提示。

也就是说,命令本身并不直接操作服务器进程,它只是向共享的缓存写入一个"信号值"(时间戳)。服务器进程通过周期性地读取这个缓存键,感知到时间戳变化后自行触发关停流程。

一个关键前提:Cache 驱动必须是共享且持久的

从上述机制可以推断出一个重要约束:命令进程与服务器进程必须能够读到同一个缓存值。

  • 生产环境应使用file、redis、memcached或database等持久化且跨进程共享的 Cache 驱动;
  • 绝不能使用array驱动,否则命令写入的信号只存在于当前 CLI 进程的内存中,服务器进程根本读不到,重启信号会"静默丢失"。

这一点虽然未在文档中显式强调,但从 RestartServer.php 与 StartServer.php 均通过Illuminate\Support\Facades\Cache读写同一键名的代码结构可以直接确认。

底层原理二:服务器端如何感知信号并触发软关停

服务器端由websockets:serve命令(StartServer.php)驱动。该命令在启动时会调用configureRestartTimer()注册一个每 10 秒触发一次的周期性定时器:

public function configureRestartTimer() { $this->lastRestart = $this->getLastRestart(); $this->loop->addPeriodicTimer(10, function () { if ($this->getLastRestart() !== $this->lastRestart) { $this->triggerSoftShutdown(); } }); } protected function getLastRestart() { return Cache::get( 'beyondcode:websockets:restart', 0 ); }

工作流程如下:

  1. 服务器启动时,将缓存中的重启时间戳读入内存变量$this->lastRestart作为基线;
  2. 事件循环每 10 秒执行一次回调,调用getLastRestart()重新读取缓存中的时间戳;
  3. 若两次读取的值不一致,说明有人执行过websockets:restart,随即调用triggerSoftShutdown()进入优雅关停。

这正是文档中"最多 10 秒内服务器会被重启"的由来——10 秒是轮询周期上限,而不是延迟固定为 10 秒。实际等待时间取决于轮询定时器恰好落在哪个相位上,最短可能仅需数毫秒。

优雅关停(Soft Shutdown)完整流程

triggerSoftShutdown()是整套机制的落点,其实现(见 StartServer.php)展示了一个教科书式的优雅关停流程:

protected function triggerSoftShutdown() { $channelManager = $this->laravel->make(ChannelManager::class); // 1. 关闭新连接入口 $channelManager->declineNewConnections(); // 2. 收集本地全部连接,逐个触发 onClose 并关闭 $channelManager->getLocalConnections() ->then(function ($connections) { return all(collect($connections)->map(function ($connection) { return app('websockets.handler') ->onClose($connection) ->then(function () use ($connection) { $connection->close(); }); })->toArray()); }) ->then(function () { // 3. 全部连接处理完毕后,停止事件循环 $this->loop->stop(); }); }

三个阶段环环相扣:

  1. 拒绝新连接:调用ChannelManager::declineNewConnections(),将实例内部标记acceptsNewConnections置为false(对应 LocalChannelManager.php),此后新的 WebSocket 握手请求将不再被接受;
  2. 逐个关闭现有连接:通过getLocalConnections()获取当前实例上的所有连接,逐个交给websockets.handler的onClose()完成反订阅清理(连接会自动从所有频道中注销),随后调用$connection->close()真正断开。这些操作以 React Promise 链式串联,全部完成前不会进入下一步;
  3. 停止事件循环:所有连接优雅关闭后,$this->loop->stop()让进程自然退出。

进程退出后,Supervisor 检测到子进程结束(配置了autorestart=true),会自动重新启动websockets:serve,新进程从缓存读取到的是更新后的重启时间戳基线,从而继续正常运行。

与 SIGTERM/SIGINT 软关停的异同

值得一提的是,同样的triggerSoftShutdown()也被用于处理系统信号。在 StartServer.php 中,当 PCNTL 扩展可用时,服务器会注册 SIGTERM 与 SIGINT 的监听器,收到信号后打印Closing existing connections...并执行同样的软关停流程:

$this->loop->addSignal(SIGTERM, function () { $this->line('Closing existing connections...'); $this->triggerSoftShutdown(); }); $this->loop->addSignal(SIGINT, function () { $this->line('Closing existing connections...'); $this->triggerSoftShutdown(); });

两者的区别在于触发源:

  • websockets:restart:由外部命令驱动,通过缓存信号触发,适合在部署脚本、CI 流程或运维操作中调用;
  • SIGTERM/SIGINT:由操作系统驱动,例如手动Ctrl+C或执行kill时触发。

殊途同归:两者最终都走declineNewConnections()→ 逐个onClose()→loop->stop()这条优雅路径,确保连接不丢失、频道状态被正确清理。相关行为在 StartServerTest.php 中有对应测试(test_pcntl_sigint_signal与test_pcntl_sigterm_signal均会先建立活跃连接再发送信号)。

结合 Supervisor 的完整运维方案

既然"优雅重启"依赖 Supervisor 自动拉起进程,这里给出完整的配套配置。首先安装 Supervisor(Debian/Ubuntu:apt install supervisor,Red Hat/CentOS:yum install supervisor并systemctl enable supervisord),然后在/etc/supervisor/conf.d(Debian/Ubuntu)或/etc/supervisord.d(Red Hat/CentOS)下创建websockets.conf:

[program:websockets] command=/usr/bin/php /home/laravel-echo/laravel-websockets/artisan websockets:serve numprocs=1 autostart=true autorestart=true user=laravel-echo

加载配置并启动:

supervisorctl update supervisorctl start websockets supervisorctl status

这样一套组合的日常运维节奏是:

  1. 发布新代码、修改配置或注册新路由后,执行php artisan websockets:restart;
  2. 服务器在 10 秒内优雅关闭所有连接并退出;
  3. Supervisor 检测到进程退出,立即自动重启websockets:serve,服务无缝衔接。

完整的 Supervisor 部署说明(包括minfds打开文件数上限调整)可参考 部署文档;服务启动参数(如--host、--port)详见 启动文档。

多实例 / 多服务器场景下的行为

从机制上看,该重启信号是广播式的:所有运行中的 WebSocket 服务器实例都会周期性地读取同一个缓存键,因此一次websockets:restart会让集群中的每个实例都在各自的事件循环内感知到信号变化,并各自执行软关停。在多节点横向扩展部署时(参见 横向扩展文档),这意味着你只需要在任意一台机器上执行一次命令,即可对整个集群发出重启请求,无需逐个节点操作。

测试与验证

仓库为这套机制提供了自动化测试保障:

  • tests/Commands/RestartServerTest.php 中的test_it_can_broadcast_restart_signal验证了执行websockets:restart后,缓存键beyondcode:websockets:restart的值会更新为当前时间(assertGreaterThanOrEqual断言新值不小于执行前的时间);
  • tests/Commands/StartServerTest.php 则验证了服务器在 SIGINT / SIGTERM 信号下能够正常完成软关停,间接佐证了triggerSoftShutdown()流程的可用性。

你也可以在自己的环境中手动验证:先以php artisan websockets:serve启动服务器并保持终端在前台运行,另开一个终端执行php artisan websockets:restart,观察服务器终端——它会在 10 秒内安静地完成关停流程并退出(若由 Supervisor 托管,则会立刻看到进程被重新拉起)。

常见问题与注意事项

  • 重启命令没有生效:优先检查 Laravel 的 Cache 驱动配置。array驱动下信号无法跨进程传递;同时确认服务器进程与命令运行在同一个应用实例/同一个.env配置下,二者读取的缓存后端必须一致。
  • 10 秒是从哪里来的:来自 StartServer.php 中addPeriodicTimer(10, ...)的轮询周期,这是"最迟 10 秒"而非固定 10 秒。
  • 为什么是优雅关闭而非强杀:软关停会先拒绝新连接、再逐个清理频道订阅关系、最后才停止事件循环,避免客户端数据与频道成员状态不一致;强杀进程则可能留下脏状态。
  • 务必配合进程管理器使用:websockets:restart只是"发出信号",它不会自己拉起新进程。没有 Supervisor 守护时,进程退出后将不再提供服务,此时需要手动重新执行php artisan websockets:serve。
  • 后端
  • 即时通讯

【免费下载链接】laravel-websockets

Websockets for Laravel. Done right.

项目地址:https://gitcode.com/gh_mirrors/la/laravel-websockets
点击查看免费下载

相关推荐

上一篇:3大核心技术深度解析MFCUK:从DarkSide攻击到MiFare Classic安全研究实战
下一篇:Olares files 路径语法与命名空间规则全解:从 drive/Home 到云盘账号的地址体系与命令约束

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进