尽管crontab+shell已经很强大很好用了,但在部署时,还是需要专门去写crontab配置,有那么一丢丢不方便,这里将备份配置放到项目里来,可以实现统一管理。
脚本直接采用swoole定时器实现,并通过swoole的蜕变守护进程达到常驻内存运行目的。之所以不在swoole的server里,通过workstart调用定时器,是因为server还需要监听端口,不够简单直接,而且在服务退出时,work进程是没法响应信号退出的,只能被master强制回收。
下面开始具体实现
一、创建thinkphp自定义命令的类文件
php think make:command TaskMysqlBackup task:mysqlbackup
CLI模式下,执行以上命令后会自动创建app/command/TaskMysqlBackup.php文件,也可以不用命令手动创建。
二、编辑TaskMysqlBackup.php类文件,实现自定义命令:
<?php
declare (strict_types = 1);
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\Output;
class TaskMysqlBackup extends Command
{
protected $pid_file;
protected $log_file;
protected $after_timer;
protected $tick_timer;
protected $period;
protected $worktime;
protected $bin_dir;
protected $bak_dir;
protected $keep;
protected function configure()
{
// 指令配置
$this->setName('task:mysqlbackup')
->addArgument('action', Argument::OPTIONAL, "start|stop|restart|status|backup", 'start')
->setDescription('数据库定时备份');
$this->log_file = root_path() . 'extend' . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR . 'mysqlbackup.log';
// pid文件不要放到runtime目录,否则容易被清除掉,造成进程无法正常关闭
$this->pid_file = root_path() . 'extend' . DIRECTORY_SEPARATOR . 'marks' . DIRECTORY_SEPARATOR . 'mysqlbackup.pid';
// 运行周期(秒)
$this->period = 86400;
// 运行时间点,进程生命周期内只有一个值,不一定就是上一次/下一次的运行时间点,运行周期在时间轴上分割出一系列的点,这些点平移后,其中的某一个点与运行时间点重合
$this->worktime = strtotime(date('Y-m-d 03:00:00'));
// mysqldump命令所在目录
$this->bin_dir = '/usr/bin/';
// 备份文件存放目录
$this->bak_dir = root_path() . 'extend' . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR;
// 备份文件保留时间(秒)
$this->keep = 7 * 86400;
}
protected function execute(Input $input, Output $output)
{
if (!extension_loaded('swoole')) {
$output->error('Can\'t detect Swoole extension installed.');
return;
}
$action = $input->getArgument('action');
if (in_array($action, ['start', 'stop', 'restart', 'status', 'backup'])) {
$this->$action();
} else {
$output->error("Invalid argument action:{$action}, Expected start|stop|restart|status|backup.");
return;
}
}
protected function checkFile($file)
{
$dir = dirname($file);
if (!is_dir($dir)) {
@mkdir($dir, 0700, true);
}
@touch($file);
if (!is_writable($file)) {
return false;
}
return true;
}
protected function backup()
{
$database = config('database.connections.mysql.database');
$username = config('database.connections.mysql.username');
$password = config('database.connections.mysql.password');
$host = config('database.connections.mysql.hostname');
$port = config('database.connections.mysql.hostport');
$charset = config('database.connections.mysql.charset');
$time = date('Ymd_His');
$filename = "{$this->bak_dir}{$database}_{$time}.gz";
if (!$this->checkFile($filename)) {
echo "[" . date('Y-m-d H:i:s') . "]\r\nCan\'t create file under: {$this->bak_dir}.\r\n\r\n";
return;
}
@unlink($filename);
$this->clear($database);
$command = "{$this->bin_dir}mysqldump -h{$host} -P{$port} --default-character-set={$charset} -u{$username} -p{$password} {$database}";
$command .= "|gzip>{$filename}";
$result = shell_exec($command);
if (!empty($result)) {
echo "[" . date('Y-m-d H:i:s') . "]\r\nBackup failure.\r\n\r\n";
return;
}
}
protected function clear($filePrefix)
{
foreach (glob($this->bak_dir . '*.gz') as $file) {
if (preg_match('/^' . $filePrefix . '_(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})\.gz$/', basename($file), $matches)) {
$time = mktime((int) $matches[4], (int) $matches[5], (int) $matches[6], (int) $matches[2], (int) $matches[3], (int) $matches[1]);
if ($time !== false && $time <= time() - $this->keep) {
@unlink($file);
}
}
}
}
protected function start()
{
if ($this->isRunning()) {
$this->output->error('Process is already running.');
return;
}
$this->output->writeln('Starting process...');
if (!$this->checkFile($this->pid_file)) {
$this->output->error("Can\'t write file: {$this->pid_file}.");
return;
}
if (!$this->checkFile($this->log_file)) {
$this->output->error("Can\'t write file: {$this->log_file}.");
return;
}
$timespan = ($this->worktime - time()) % $this->period;
if ($timespan <= 0) {
$timespan += $this->period;
}
$this->after_timer = \swoole_timer::after($timespan * 1000, function () {
$this->tick_timer = \swoole_timer::tick($this->period * 1000, function () {
ob_start();
$this->backup();
file_put_contents($this->log_file, ob_get_clean(), FILE_APPEND);
});
ob_start();
$this->backup();
file_put_contents($this->log_file, ob_get_clean(), FILE_APPEND);
});
\swoole_process::daemon(true, false);
file_put_contents($this->pid_file, getmypid());
\swoole_process::signal(SIGTERM, function () {
if ($this->after_timer) {
\swoole_timer::clear($this->after_timer);
}
if ($this->tick_timer) {
\swoole_timer::clear($this->tick_timer);
}
@unlink($this->pid_file);
});
$nextTime = date('Y-m-d H:i:s', time() + $timespan);
$this->output->writeln("Starting success, Task will run in the {$nextTime} for the first time.");
\swoole_event::wait();
}
protected function stop()
{
if (!$this->isRunning()) {
$this->output->error('No process running.');
return;
}
$this->output->writeln('Stopping process...');
$pid = (int) file_get_contents($this->pid_file);
\swoole_process::kill($pid, SIGTERM);
$end = time() + 15;
while (time() < $end && \swoole_process::kill($pid, 0)) {
usleep(100000);
}
if ($this->isRunning()) {
$this->output->error('Unable to stop the process.');
return;
}
$this->output->writeln('Stopping success.');
}
protected function restart()
{
if ($this->isRunning()) {
$this->stop();
}
$this->start();
}
protected function status()
{
$this->output->writeln($this->isRunning() ? 'Process is running.' : 'Process stopped.');
}
protected function isRunning()
{
if (!is_readable($this->pid_file)) {
return false;
}
$pid = (int) file_get_contents($this->pid_file);
return $pid > 0 && \swoole_process::kill($pid, 0);
}
}
三、配置命令使生效
在config/console.php修改配置如下:
<?php
return [
'commands' => [
'task:mysqlbackup' => 'app\command\TaskMysqlBackup',
],
];
四、手动运行命令测试备份逻辑
php think task:mysqlbackup backup
五、开启自动备份
php think task:mysqlbackup start
此时,进程已常驻后台,会定时进行备份
虽然前面有提到蜕变守护进程,但并不是多了一个守护进程,只是swoole变为后台进程的说法,要想真正守护此进程,得另外实现一个进程来监控此进程,当然更简单的方法是采用第三方守护工具,比如Python的supervisor。
命令帮助:
php think task:mysqlbackup -h
Usage:
task:mysqlbackup [<action>]
Arguments:
action start|stop|restart|status|backup [default: "start"]
Options:
-h, --help Display this help message
-V, --version Display this console version
-q, --quiet Do not output any message
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
|