一、前言
最近使用自己搭建的php框架写一些东西,需要用到异步脚本任务的执行,但是是因为自己搭建的框架没有现成的机制,所以想自己搭建一个类似linux系统的crontab服务的功能。
因为如果直接使用linux crontab的服务配置起来很麻烦,如果不了解的人接手,也不知道你配置了crontab,后续拆分生产和测试环境也会很复杂,不能一套代码包含所有。
二、配置文件
先在相关配置目录下放一个配置文件,例如:config/crontab.ini,里面配置如下结构,其中需要注意的是request_uri参数,这个参数是各自框架中使用命令行形式执行任务的命令,可以根据自己的框架进行修改。
例:我目前的框架执行命令形式是:
php index.php request_uri="/cli_Test/test"
;【使用说明】
;1、检测机制:每分钟自动检测一次该配置
;
;2、参数说明
;request_uri:为执行任务的命令行
;time:控制启动时间,分别为分、时、日、月、周
;proc_total:运行脚本的进程数
;ip_limit:服务器ip限制(多个ip英文,分隔)
[test]
request_uri = "/cli_Test/test"
time = "0 3 * * *"
proc_total = "1"
;ip_limit = 10.235.62.241
注:
time:配置方式是一比一复制的linux crontab配置
proc_total:支持多个进程执行任务的方式,比如队列消费要启用多个进程消费就很方便,并且会自动检测执行任务的进程是否存在,存在不会重复启动。
ip_limit:有时候集群服务器太多,但是你只想单台机器执行,可以使用该配置,限制执行的服务器ip是什么,也可以配置多个。
三、监控相关类
1、配置读取类,可以解析上述配置文件结构
<?php
/**
* 配置信息读取类
*
* @package Comm
*/
class Config {
private static $_config = array();
/**
* 读取配置信息
*
* @param string $path 节点路径,第一个是文件名,使用点号分隔。如:"app","app.product.test"
* @return array|string
*/
public static function get($path) {
if(!isset(self::$_config[$path])) {
$arr = explode('.', $path);
try {
$conf = parse_ini_file(APP_PATH . 'config/'.$arr[0].'.ini', true);
} catch (Exception $e) {
}
if (!empty($conf)) {
if (isset($arr[1]) && !isset($conf[$arr[1]])) {
throw new Exception("读取的配置信息不存在,path: " . $path);
}
if (isset($arr[1])) $conf = $conf[$arr[1]];
if (isset($arr[2]) && !isset($conf[$arr[2]])) {
throw new Exception("读取的配置信息不存在,path: " . $path);
}
if (isset($arr[2])) $conf = $conf[$arr[2]];
}
if (!isset($conf) || is_null($conf)) {
throw new Exception("读取的配置信息不存在,path: " . $path);
}
self::$_config[$path] = $conf;
}
return self::$_config[$path];
}
}
2、任务监控类
注:其中需要注意的是shell方法的内容,如果自己的框架不适用这种执行命令方式,可以更改为自己框架的命令。
<?php
namespace app\controllers\Cli;
/**
* Crontab监控
*
* 注:切勿轻易修改
*/
class MonitorController
{
/**
* 检查计划任务
*/
public function index()
{
$appEnv = \Context::param("APP_ENV");//获取运行环境
$appEnvParam = !empty($appEnv) ? "&APP_ENV=".$appEnv : "";
echo "\033[35mCheck Crontab:\r\n\033[0m";
$config = $this->getConfig();
foreach ($config as $key => $value) {
if (!$this->checkTime(time(), $value['time'])) {
echo "{$key}:[IGNORE]\r\n";
continue;
}
$ip_limit = isset($value['ip_limit']) ? explode(',',$value['ip_limit']) : false;
for ($i = 1; $i <= $value['proc_total']; ++$i) {
$request_uri = "{$value['request_uri']}?proc_total={$value['proc_total']}&proc_num={$i}{$appEnvParam}";
//检查进程是否存在
$shell = $this->shell($request_uri);
$num = $this->shell_proc_num($shell);
echo "{$key}_{$i}:";
if ($num >= 1) { //进程已存在
echo "\033[33m[RUNING]\033[0m";
} else { //进程不存在,操作
if($ip_limit){
if(in_array(\Util::getServerIp(),$ip_limit)){
echo "\033[32m[OK]\033[0m";
$this->shell_cmd($request_uri);
}else{
echo "\033[32m[IP LIMIT]\033[0m";
}
}else{
echo "\033[32m[OK]\033[0m";
$this->shell_cmd($request_uri);
}
}
echo "\r\n";
}
}
}
/**
* 获取crontab配置
*
* @return array
*/
public function getConfig()
{
return \Config::get('crontab');
}
/**
* 检查是否该执行crontab了
*
* @param int $curr_datetime 当前时间
* @param string $timeStr 时间配置
* @return boolean
*/
protected function checkTime($curr_datetime, $timeStr)
{
$time = explode(' ', $timeStr);
if (count($time) != 5) {
return false;
}
$month = date("n", $curr_datetime); // 没有前导0
$day = date("j", $curr_datetime); // 没有前导0
$hour = date("G", $curr_datetime);
$minute = (int)date("i", $curr_datetime);
$week = date("w", $curr_datetime); // w 0~6, 0:sunday 6:saturday
if ($this->isAllow($week, $time[4], 7, 0) &&
$this->isAllow($month, $time[3], 12) &&
$this->isAllow($day, $time[2], 31, 1) &&
$this->isAllow($hour, $time[1], 24) &&
$this->isAllow($minute, $time[0], 60)
) {
return true;
}
return false;
}
/**
* 检查是否允许执行
*
* @param mixed $needle 数值
* @param mixed $str 要检查的数据
* @param int $TotalCounts 单位内最大数
* @param int $start 单位开始值(默认为0)
* @return type
*/
protected function isAllow($needle, $str, $TotalCounts, $start = 0)
{
if (strpos($str, ',') !== false) {
$weekArray = explode(',', $str);
if (in_array($needle, $weekArray))
return true;
return false;
}
$array = explode('/', $str);
$end = $start + $TotalCounts - 1;
if (isset($array[1])) {
if ($array[1] > $TotalCounts)
return false;
$tmps = explode('-', $array[0]);
if (isset($tmps[1])) {
if ($tmps[0] < 0 || $end < $tmps[1])
return false;
$start = $tmps[0];
$end = $tmps[1];
} else {
if ($tmps[0] != '*')
return false;
}
if (0 == (($needle - $start) % $array[1]))
return true;
return false;
}
$tmps = explode('-', $array[0]);
if (isset($tmps[1])) {
if ($tmps[0] < 0 || $end < $tmps[1])
return false;
if ($needle >= $tmps[0] && $needle <= $tmps[1])
return true;
return false;
} else {
if ($tmps[0] == '*' || $tmps[0] == $needle)
return true;
return false;
}
}
/**
* 执行Shell命令
*
* @param string $request_uri
*/
public function shell_cmd($request_uri)
{
if (IS_WIN) {
$cmd = $this->shell($request_uri);
pclose(popen("start /B " . $cmd, "r"));
}else{
$cmd = $this->shell($request_uri) . " > /dev/null &";
$pp = @popen($cmd, 'r');
@pclose($pp);
}
}
/**
* 获取Shell执行命令
*
* @param string $request_uri
* @return string
*/
public function shell($request_uri)
{
return PHP_BIN . ' ' . rtrim(APP_PATH, '/') . "/index.php request_uri=\"{$request_uri}\"";
}
/**
* 检查指定shell命令进程数
*
* @param string $shell shell命令
* @return int
*/
public function shell_proc_num($shell)
{
if (IS_WIN) {// Windows 环境下的逻辑
if (!extension_loaded('com_dotnet')) {
die("COM extension is not installed or loaded.");
}
$num = 0;
$shell = str_replace([' ', '\\'], ['', '/'], $shell);
$computer = ".";
$obj = new \COM('winmgmts:{impersonationLevel=impersonate}!\\\\' . $computer . '\\root\\cimv2');
$processes = $obj->ExecQuery("SELECT * FROM Win32_Process");
foreach ($processes as $process) {
$line = str_replace([' ', '\\'], ['', '/'], $process->CommandLine);
if (strpos($line, $shell) !== false) {
$num++;
}
}
return $num;
} else {
$shell = str_replace(array('-', '"'), array('\-', ''), $shell);
// $shell = preg_quote($shell);
$shell = str_replace("\?", '?', $shell);
$cmd = "ps -ef | grep -v 'grep' |grep \"{$shell}\"| wc -l";
$pp = @popen($cmd, 'r');
$num = trim(@fread($pp, 512)) + 0;
@pclose($pp);
return $num;
}
}
}
四、执行初始化脚本
1、init.sh,这个脚本主要是把监控任务类的执行方式写入到了linux中的crontab服务,后续就不用管了。
2、后续想要增加执行的任务,可以直接在crontab.ini中增加即可。
#开启crond
/sbin/service crond start
#追加写入crontab监控
phpBin=$1
projectPath=$2
appEnv=$3
if [[ "$phpBin" == "" || "$projectPath" == "" ]]; then
echo "请先输入【php bin路径】和【项目根目录路径】"
exit
fi
if [[ "$appEnv" != "pro" && "$appEnv" != "dev" ]]; then
echo "请输入环境变量参数:pro生产环境 或 dev测试环境"
exit
fi
if [[ ! -e "$phpBin" ]]; then
echo "【php bin路径】不正确,可尝试使用which php来获取bin路径"
exit
fi
if [[ ! -e "$projectPath/index.php" ]]; then
echo "【项目根目录路径】不正确"
exit
fi
crontabl=`crontab -l | grep "$phpBin" | grep "$projectPath" | grep "request_uri" | grep "cli_monitor"`
if [ -z "$crontabl" ]; then
echo "* * * * * $phpBin $projectPath/index.php request_uri='/cli_monitor?APP_ENV=$appEnv'" >> /var/spool/cron/root
echo -e "cli_monitor write \033[32m[SUCCESS]\033[0m"
else
echo -e "cli_monitor already exist \033[32m[SUCCESS]\033[0m"
fi