背景介绍
年前对下单中的异步出单队列进行了重构,用主进程fork子进程处理消息队列。因为可以通过控制子进程的数量,提高该消息队列的处理效率。由于一开始,都只是先上消息量不大的队列,所以都只fork了一个子进程。在生产上偶发子进程丢失mysql连接,一开始认为是mysql超时了,修改了代码,改为每次拿到新消息都示例化一个新的model去拿数据,并且在代码上加入了异常告警,后面仍出现丢失MySQL连接。后面发现是在父进程一开始就示例化了mysql连接,并且在php的tp3.2框架中,对model做了静态单例。
第一次遇到mysql超时
身为一个弱到爆的开发,我一开始认为是mysql超时了,为了快速把生产问题处理掉,就在每次拿到消息时,实例化model。并以为这样就处理掉了。
第二次遇到
过了几天,子进程丢失mysql连接的事故依然发生。这次我在代码上加入了异常告警,并且开始想着如果把这个问题处理掉。
查看mysql超时设置,发现mysql的超时时间为86400,但是生产的进程每天凌晨都会进行一次重启,显然,并不是mysql连接超时导致的。
为了找出问题,将生产的代码,写了简化版本在测试环境跑了起来。
以下为示例代码:
<?php
namespace UnionGate\Daemon;
use Home\Common\Logger;
Logger::init();
use \Org\Util\MsgQ;
class TestMysqlConnect{
private $conn;
private $model;
public function __construct()
{
$this->model = new AModel();
}
//跑这个方法运行
public function run()
{
for ($i = 0; $i < 3; $i++) {
$nPID = \pcntl_fork();//创建子进程
if ($nPID == 0) {
$this->work();
exit(0);
}
}
$n = 0;
while ($n < $this->procNum) {
$nStatus = -1;
$nPID = \pcntl_wait($nStatus);
if ($nPID > 0) {
++$n;
}
}
exit;
}
//起进程
protected function work()
{
$MsgData = "";
while (true) {
usleep(10000);
\Org\Util\MsgQ::init();
$ret = MsgQ::BlockSubsribe('testMysqlConnect', $MsgData);
if ($ret === false) {
continue;
}
$this->doSomething();
}
}
protected function doSomething()
{
}
}
use Think\Model;
class AModel extends Model{
protected $tablePrefix = 't_';
protected $tableName = 'underwrite_tmp';
protected $dbName = "underwrite";
//数据库配置
protected $connection = "UNDERWRITE_DB_CONFIG";
}
在测试环境跑了大概有12小时候之后,果然问题复现了。
这里通过netstat -anp | grep pid 命令查看进程监听的端口,发现问题发生的时候,mysql的连接已经不见了。
mysql连接关闭的原因
- mysql 的server端主动关闭,(超过了超时时间)
- mysql的server端主动关闭,mysql发现tcp的包时序出现了异常(即多个进程用了同一个连接)
- mysql的client端关闭,资源被回收了(进程退出,变量被unset)
对应的
- 因为mysql的系统设置 超时时间都是86400,且进程在每天都会被重启一次,所以不是因为超时关闭的。
- 因为主进程后面只做对子进程的监听,并没有尝试与mysql进行连接,所以应该不是主进程和子进程复用同一个连接导致的
- 阅读了tp3.2的db实现的底层源码,发现tp在db连接上,做了静态单例,查了php的fork,发现php直接调用了系统的fork函数。
/* {{{ proto int pcntl_fork(void)
Forks the currently running process following the same behavior as the UNIX fork() system call*/
PHP_FUNCTION(pcntl_fork)
{
pid_t id;
id = fork();
if (id == -1) {
PCNTL_G(last_error) = errno;
php_error_docref(NULL, E_WARNING, "Error %d", errno);
}
RETURN_LONG((zend_long) id);
}
出现连接超时的原因
因为有构造函数的存在,导致一开始,父进程就有了mysql连接的静态单例,直接用了linux的fork,可以直接认为这时候子进程和主进程的变量都是一致。因而,子进程用了父进程的MySQL连接,而且,因为做了静态单例,所以不管怎么实例化,都是得到同一个mysql连接。
解决方法
主进程一进来,只做fork子进程的工作,别的连接都不处理。让子进程自己去实例化各个资源即可