fairycat

Created:
Updated:

用 systemd 管理 laravel 队列和定时任务

之前在使用 laravel 的队列时,使用管理员启动的服务来管理进程。这本来是没有问题,但是我在服务器上架设网站的时候,是不使用管理员帐号进行网站的管理的。若非特殊情况都是使用普通帐号对网站进行管理和更新。如果以管理员运行服务进行 laravel 的任务管理,问题是网站进行更新之后需要重启任务,这普通用户就没办法重启管理员的服务。

于是就用普通用户自己启动一个 supervisrod 之类的服务。有一段时间给一个网站单独启用了一个 supervisrod 服务,后来想想干脆使用 systemd 进行管理。

这次的最主要的目标当然是允许普通用户自己重启自己挂起的服务,所以 systemd 应该使用用户模式:systemctl 命令要加上 --user。用户的单元文件保存在 ~/.config/systemd/user/ 目录下。

另外一个重点,默认情况下 systemd 的用户实例会跟随用户会话启动和停止。而我们需要让普通用户实例在系统启动的时候就自动启动。这时候需要让管理员启用驻留我们用来作为网站管理员的用户。

systemd用户实例在用户首次登陆时启动,并在最后一个会话退出时终止。 但有时候,对于一些不依赖于会话的用户进程,在系统启动时加载用户实例,在会话全部结束时,也不停止用户实例是比较有用的。Lingering 就是用来实现这个的。 使用以下命令来启用驻留指定用户:

# loginctl enable-linger username

https://wiki.archlinux.org/index.php/Systemd/User_(简体中文)#随系统自动启动systemd用户实例


任务队列

通常我们习惯给 queue 队列启动几个进程,而我没有找到方法如何使用一个 service 挂起几个进程,除非另外编辑脚本让主进程开启子进程。但这里并不打算再添加另外的脚本了。所以用模板的形式,再用一个 target 带起几个模板实例。

既然使用了模板,则必然会出现 slice 单元,从同一个模版实例化出来的所有服务单元, 默认全部属于与模版同名的同一个 slice 单元。而这里依然使用 target 单元来管理,当时我使用 slice 单元时,用起来不太合适,具体什么原因我也忘了。

目标 target 单元主要被默认目标依赖就行了,因为是普通用户实例,所以是 default.target

laravel.queue.target

[Unit]
Description=laravel queue target
Documentation=https://blog.fairycat.cn

[Install]
WantedBy=default.target

接下来是服务单元。运行 php 脚本,打开的进程直接作为服务的主进程,所以类型为 simple 即可。同时设定自动重启。

laravel.queue.process@.service

[Unit]
Description=laravel queue worker process
Documentation=https://blog.fairycat.cn
PartOf=laravel.queue.target

[Service]
Type=simple
ExecStart=/usr/bin/php /var/www/laravel/artisan queue:work --sleep=3 --tries=3
Restart=always

[Install]
WantedBy=laravel.queue.target

注意文件名包含 @ 符号,这里是作为模板使用的,后边可以添加参数而且文件中可以使用这个参数,这里并不需要使用这个参数,只是为了可以启动几个单元而已。

WantedBy 指向我们刚刚建立的 target 单元,当 target 单元启动的时候,会启动 service 单元。

PartOf 的目的是,当我们停止 target 单元的时候,service 单元也应该停止,所以 service 单元指定为 target 单元的一部分。

ExecStart 就是 laravel 的 queue 的命令了,间隔为 3, 尝试次数为 3。如今 laravel 已经升级到了 6.x 版本了,默认尝试次数不再是不限制,但尽量写全了。另外, ExecStart 命令的第一个参数也就是运行的文件路径需要绝对路径,这里是 php 的绝对路径。而 artisan 的路径,可以根据工作目录来指定,这里就直接使用绝对路径了。

尝试启动 service 服务,查看服务单元的状态。可以在后边加入不同的数字来启动多个 service。

systemctl --user start laravel.queue.process@0
systemctl --user status laravel.queue.process@0

服务单元正常之后,开始连接具体的我们需要的服务单元了,刚刚建立的是服务单元的模板,从这个模板可以启动多个服务单元。因为我们的服务单元是被 target 单元依赖的,进行 enable 命令,会自动生成软链,每一个软链就是一个具体的服务单元,而且软链保持在被依赖的单元的 .wants 目录里边也就是 laravel.queue.target.wants 目录

systemctl --user enable laravel.queue.process@{0..3}

运行命令之后,会自动在 laravel.queue.target.wants 目录生成 4 个软连,参数从 0 到 3,如此之后,只要 target 单元启动,则会启动 4 个 service 单元。花括号是简便的写法,也可以一个一个的运行命令,或者闲得慌的也可以手动建立软链

systemctl --user enable laravel.queue.process@0
systemctl --user enable laravel.queue.process@1
systemctl --user enable laravel.queue.process@2
systemctl --user enable laravel.queue.process@3

需要删除相应的软链,把 enable 改成 disable 就行了。

尝试启动 target 单元

systemctl --user start laravel.queue.target

再分别查看 4 个服务的状态,没有意外的话四个 service 单元都在正常运行。

尝试关闭 target 单元

systemctl --user stop laravel.queue.target

再分别查看 4 个服务的状态,都被关闭了。

最后则是允许开机启动,我们的 target 单元写好了 WantedBy=default.target,只要运行 enable 就行了,当不再需要开机启动,用 disable

systemctl --user enable laravel.queue.target
systemctl --user disable laravel.queue.target

到目前为止运行是没有问题,不过还有一个小问题是,systemd 的单元都有日志的,而我们使用四个单元来分别管理这四个进程,它们的日志是分开的~

journalctl --user -u laravel.queue.process@0 -u laravel.queue.process@1 -u laravel.queue.process@2 -u laravel.queue.process@3

任务定时器

laravel 的自带任务调度可以使用 artisan schedule:run 来运行,但这里还是记录一下 systemd 的定时器。

假设前提已经建立好了 Artisan 命令 php artisan statistics:send,现在需要每天上午 8 点发送统计信息。

先建立好 service 单元,这个单元只要运行完成就退出,所以使用类型为 oneshot。如果程序运行要花不少时间,可以设置为 simple,但是不要设置自动重启,或者失败才重启。

laravel.statistics.send.service

[Unit]
Description=laravel send statistics
Documentation=https://blog.fairycat.cn

[Service]
Type=oneshot
ExecStart=/usr/bin/php /var/www/laravel/artisan statistics:send

然后是 timer 单元。注意两个文件除了后缀不同,名称相同。为了可以开机启动,和前面的 target 单元一样这个 timer 单元同样被依赖于 default.target

laravel.statistics.send.timer

[Unit]
Description=Send laravel statistics daily

[Timer]
OnCalendar=*-*-* 8:00:00
Persistent=true

[Install]
WantedBy=default.target

最后命令 enable 就行了

systemctl --user enable laravel.statistics.send.timer

horizon

使用模板来启动多个进程还是不太方便,这里使用 Laravel 框架的官方包 horizon 进行任务队列管理。horizon 的用法可在官方文档查询。

而守护进程就简单了,只需要拉起一个进程就行了,horizon 进程会根据配置信息,控制打开和关闭子进程。就像前边提到的,可以用脚本主进程开几个子进程,但是写脚本麻烦。这就是已有现成可用的功能直接使用就行了。

运行 horizon 只需要打开一个主进程,只需要写一个 service 文件就行

laravel.horizon.target

[Unit]
Description=laravel horizon worker process

[Service]
Type=simple
WorkingDirectory=/www/
ExecStart=/usr/bin/php artisan horizon
Restart=always
RestartSec=3
#User=
#Group=

[Install]
#WantedBy=default.target
WantedBy=multi-user.target

老样子,如果是系统服务,由 root 打开,可以通过设置 UserGroup 指定运行用户,安装被依赖于 multi-user.target。如果是用户服务, UserGroup 不能设置,而安装被依赖于 default.target

评论

Name

Email

Website

Subject