记一次 swoole task 实践
需求背景
- 平时基本上都在写http+json这种api,经常遇到想把一些不重要的操作改成异步的时候
- 之前的方案是利用redis, lpush到队列, 然后另起个php脚本,brpop出来操作。
- 为了防止brpop连接闲置太久抛异常,引入的supervisor进行管理,出现异常脚本退出了supervisor会自动重启
- 最近做一个新的独立项目,刚好看到这篇文章 在php-fpm/apache中使用task功能,打算实践下
开发调试跑起来
- 首先是把代码抄过来,然后调试+加东西,完成版的代码贴出来
//$server = new Server("127.0.0.1", 9501, SWOOLE_BASE);
$server = new Server("127.0.0.1", 9501, SWOOLE_PROCESS);
$server->set([
'task_worker_num' => 2,
'worker_num' => 1,
]);
$server->setHandler('LPUSH', function($fd, $data) use ($server) {
$taskID = $server->task($data);
if ($taskID === false) {
$server->send($fd, Server::format(Server::ERROR));
} else {
$server->send($fd, Server::format(Server::INT, $taskID));
}
});
$server->on('Start', function($serv) {
cli_set_process_title("php_swoole_task: master");
\Yii::info("redis server master start... pid={$serv->master_pid}", 'business');
});
//不回调这里,不知道为啥,进程是有的
$server->on('ManagerStart', function($serv) {
cli_set_process_title("php_swoole_task: manager");
\Yii::info("redis server manager start... pid={$serv->manager_pid}", 'business');
});
$server->on('WorkerStart', function($serv, $worker_id) {
$type = $serv->taskworker ? 'task' : 'worker';
cli_set_process_title("php_swoole_task: {$type}");
\Yii::info("redis server {$type} start ....worker_id [{$worker_id}]", 'business');
});
$server->on('WorkerError', function($serv, $worker_id, $worker_pid, $exit_code, $signal) {
$type = $serv->taskworker ? 'task' : 'worker';
$msg = "{$type} error, worker_id=[{$worker_id}], pid={$worker_pid}, exit_code=$exit_code, signal=$signal";
\Yii::info($msg, 'business');
});
//task 进程处理完任务回调到这里
$server->on('Finish', function($serv, $taskID, $data) {
\Yii::info('redis_server task finish,id=' . $taskID . ',res=' . $data, 'business');
$stats = $serv->stats();
if ($stats['tasking_num'] > 10) { //tasking_num 当前正在排队的任务数
echo "剩余任务信息:" . json_encode($serv->stats()) . "\n";
\Yii::info('redis_server status tasking_num waring ' . json_encode($serv->stats()), 'business');
}
});
// kill -9 master 进程不会触发这个回调,而且工作进程啥的都还活着
$server->on('Shutdown', function($serv) {
\Yii::info('redis_server shutdown....', 'business');
});
$server->on('Task', function ($serv, $taskID, $workerID, $data) {
\Yii::info('redis_server receive task ' . $taskID, 'business');
list($queue, $info) = $data;
$info = json_decode($info, true);
$res = true;
switch($queue) {
case 'present_multi_gift_order':
$res = LogOrderManager::addPresentSendMultiOrder($info['orderInfo'], $info['receivers']);
break;
case 'present_gift_order':
$res = LogOrderManager::addPresentSendOrder($info['orderInfo']);
break;
default:
echo "不认识的queue\n";
break;
}
return $res ? 'OK' : 'FAIL:' . json_encode($data);
});
$server->start();
复制代码
- 服务启动后的进程树如下,这块不熟先看下文档 Swoole server->start
➜ ~ pstree -p 19455
-+= 00001 root /sbin/launchd
\-+= 00758 momo /Applications/iTerm.app/Contents/MacOS/iTerm2
\-+= 13102 momo /Applications/iTerm.app/Contents/MacOS/iTerm2 --server login -fp momo
\-+= 13103 root login -fp momo
\-+= 13104 momo -zsh
\-+= 19454 root sudo php yii redis-server/start
\-+- 19455 root php_swoole_task: master
|--- 19460 root php_swoole_task: task
|--- 19461 root php_swoole_task: task
\--- 19462 root php_swoole_task: worker
复制代码
如果你发现没有文档里manager进程,这个运行模式有关,后面再说
- 既然希望异步,那在php-fpm里lpush(提交异步任务)肯定要快啊
测试了下, swoole 的 worker进程在收到请求后执行 $server->task()将任务转给task进程这个操作是非阻塞的, 后面压测会发现
压测下 task 进程的处理能力和反应
- 压测准备
onTask
里加个sleep(1)s 来控制每个任务的处理时间
- 用
redis-benchmark -h 127.0.0.1 -p 9501 -c 1 -n 20 -t lpush
进行测试 - 测试1,启动2个task进程,lpush 1000个任务,花了8分多钟,跟推测吻合(每个任务1s,2个人干), 压测时通过Swoole server->stats() 观察到tasking_num(排队任务数)
- 测试2,启动20个task,lpush 1000个任务,执行时间50s
- 测试3,还是20个进程,lpush 110万,很快就会出现
[2019-06-03 10:54:20 *10542.0] WARNING swReactor_write (ERROR 1008): socket#18 output buffer overflow
, 同时 worker 进程cpu很快飚到100%
此时lpush是失败的,worker进程也没死,收到的任务也仍然在按个处理
- 测试4, 去掉sleep,onTask不处理任何逻辑空跑,lpush个20万,QPS大概12000
结论
- 设置多少个task 进程要根据每个task处理的耗时+QPS来定。 每个任务10ms,那1s能处理100个任务,你qps是1000的话,就得启动10个task进程
- 如果投递容量超过处理能力,task会塞满缓存区,导致worker进程发生阻塞。worker进程将无法接收新的请求;但是已经转给task进程的任务会继续执行
部署运维---平滑重启
这种常驻内存的服务不想nginx+php-fpm,需要我们自己写脚本来搞定这个事情
- 需求我总结了下
- 代码部署完能自动生效,立刻还是延迟点无所谓
- 同时不影响正在处理的任务
- 调用方没有感知
- 先看下官方文档 wiki.swoole.com/wiki/page/2…,试了下主要几个信号的结果
- kill -9 master_pid 只是干掉master,其他还活着
- kill -15 master_pid 干掉所有
- kill -USR1 平滑重启所有worker进程
- kill -USR2 平滑重启所有task进程
- 按照 使用systemd管理swoole服务 来配置, reload 使用 USR2信号
[Unit]
Description=Swoole Task Server
After=network.target
After=syslog.target
[Service]
Type=simple
LimitNOFILE=65535
ExecStart=/usr/bin/php /home/deploy/api-mj/yii redis-server/start
ExecReload=/bin/kill -USR2 $MAINPID
Restart=always
[Install]
WantedBy=multi-user.target
复制代码
- 测试结果
- task进程不重启,
新部署的代码是不会生效的
systemctl restart swoole_task.service
所有进程都重启,积压的task会丢弃systemctl reload swoole_task.service
也就是kill -USR2 {master_pid}
, 会启动新task进程,旧task进程会继续处理积压的任务,处理完后退出
坑 SWOOLE_BASE 模式
-
$server = new Server("127.0.0.1", 9501, SWOOLE_BASE);
这句.SWOOLE_BASE
是Server的两种运行模式 之一,这种模式下 kill -USR1 或者 kill -USR2 都只能重启worker进程,不会重启task进程,也就做不到平滑重启(因为无法让新代码生效) -
SWOOLE_BASE 模式下运行的结果跟文档说的也有点不一样
- 文档说BASE模式没有master进程,我发现是有的
- 文档说manager进程可选,我测试的结果是不管怎么着都没有manager进程
- 测试代码
//$server = new Server("127.0.0.1", 9501, SWOOLE_BASE);
$server = new Server("127.0.0.1", 9501, SWOOLE_PROCESS);
$server->set([
'task_worker_num' => 2,
'worker_num' => 1,
]);
复制代码
SWOOLE_PROCESS
模式下,master(1)+manager(1)+worker(1)+task(2)
共5个
➜ ~ pstree -p 19319
-+= 00001 root /sbin/launchd
\-+= 00758 momo /Applications/iTerm.app/Contents/MacOS/iTerm2
\-+= 13102 momo /Applications/iTerm.app/Contents/MacOS/iTerm2 --server login -fp momo
\-+= 13103 root login -fp momo
\-+= 13104 momo -zsh
\-+= 19318 root sudo php yii redis-server/start
\-+- 19319 root php_swoole_task: master
\-+- 19320 root php_swoole_task: manager
|--- 19321 root php_swoole_task: task
|--- 19322 root php_swoole_task: task
\--- 19323 root php_swoole_task: worker
复制代码
SWOOLE_BASE
模式下master(1)++worker(1)+task(2)
共4个
➜ ~ pstree -p 19455
-+= 00001 root /sbin/launchd
\-+= 00758 momo /Applications/iTerm.app/Contents/MacOS/iTerm2
\-+= 13102 momo /Applications/iTerm.app/Contents/MacOS/iTerm2 --server login -fp momo
\-+= 13103 root login -fp momo
\-+= 13104 momo -zsh
\-+= 19454 root sudo php yii redis-server/start
\-+- 19455 root php_swoole_task: master
|--- 19460 root php_swoole_task: task
|--- 19461 root php_swoole_task: task
\--- 19462 root php_swoole_task: worker
复制代码
Todo 部署运维--监控
- 服务挂掉了要报警
机器监控进程是否活者
+定时ping下看是否活着
- task finish回调里,看下剩余的tasking_num, 有积压发报警出来
Todo 部署运维--服务高可用
这个redis server 肯定不能部署个单点,部署多个的话,这又不是http,可以靠dns来负载均衡。。
- 有个可用的ip:port 清单,放哪?
- 单个节点服务启动和挂掉时,怎么方便的从
可用清单
注册和移除该服务?- 调用方什么方式拿到可用清单?
- 服务重启了,phpredis留的坏链接怎么处理? redis->ping 一下再用
- 要解决高可用的问题,按上面的思路我觉得太大了。。。看来得换成http server,这样用nginx做个代理就行了