• 详解PHP解决swoole守护进程Redis假死 ,mysql断线重连问题


    PHP如何解决swoole守护进程Redis假死 ,mysql断线重连问题?

    最近公司有个项目,要举办一个线上活动,我这边负责提供接口记录用户访问记录,与操作记录,由于活动参与人数可能比较多,为了不影响正常业务运行,我们决定不在接口中直接写入数据库,而采用异步写入,也就是调用接口,数据先写入reids 队列,然后在编写一个消费进程读取队列消息写入数据库。大概就是这么个流程

    为了消费进程能一直读取队列数据写入数据库,我们需要 编写一个守护进程让它一直为我们工作,php编写守护进程,有几个选择可以用,第一个php提供的pcntl*扩展,第二就是workerman,第三就是 Swoole

    这里我选择swoole 来编写守护进程

    1. 下载swoole 扩展源码 由于生产环境php版本是7.1 所有 swoole 扩展版本也不能选择最新版本,我这里选用 swoole-4.4.4,下载到php 扩展目录

      cd /usr/local/php/include/php/ext
      wget -c http://pecl.php.net/get/swoole-4.4.4.tgz
      
      • 1
      • 2
    2. 接下来我们解压 swoole 源码,进入源码目录

      tar xzvf swoole-4.4.4.tgz
      cd swoole-4.4.4
      
      • 1
      • 2
    3. 第三步执行phpize 生成configure配置文件

      phpize
      
      • 1
    4. 第四步 指定php配置文件进行预编译,如果php配置文件目录不同自行调整

      ./configure --with-php-config=/usr/local/php/bin/php-config
      
      • 1
    5. 第五步 编译与安装,编译安装完会在 /usr/local/php/lib/php/extensions/no-debug-non-zts-20160303 下找到一个 swoole.so 动态库文件

      #编译
      make
      # 安装
      make install
      
      • 1
      • 2
      • 3
      • 4

      在这里插入图片描述

    6. 修改 php.ini文件,加入 swoole.so 动态库文件,文件路径不同自行修改

    [Swoole]
    extension = /usr/local/php/lib/php/extensions/no-debug-non-zts-20160303/swoole.so
    
    • 1
    • 2

    在这里插入图片描述

    修改完成后就可以使用 php -m 命令查看是否安装上了 swoole 扩展
    在这里插入图片描述
    接下来进入正题,编写守护进程, swoole 编写守护进程极其简单调用 daemon() 方法就好

    
    
    use Swoole\Process;
    
    // 定义回调处理函数
    function callback_function (Swoole\Process $worker) {
    	//执行一个外部程序,此函数是 exec 系统调用的封装。
        $worker->exec('/usr/bin/php', array(__DIR__.'/think','activityupd'));
    };
    
    
    
    // 创建一个子进程,第一个参数是子进程创建成功执行的回调函数,第二个参数的意思是 重定向子进程的标准输入和输出。【启用此选项后,在子进程内输出内容将不是打印屏幕,而是写入到主进程管道。读取键盘输入将变为从管道中读取数据,调试代码时可以设置为 tree
    $p = new Swoole\Process('callback_function',false);
    
    // 启动子进程
    $p->start();
    //使当前进程蜕变为一个守护进程。
    Swoole\Process::daemon(false,false);
    while(1){
    sleep(1);
    }
    // 回收结束运行的子进程。
    Swoole\Process::wait();
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    项目使用的是tp框架,所有使用命令行模式 来编写主流程逻辑

    
    
    namespace app\admin\command;
    
    use think\console\Command;
    use think\console\Input;
    use think\console\Output;
    use think\Db;
    use think\Log;
    
    class ActivityUpd extends Command
    {
        protected function configure()
        {
            $this->setName('activityupd')->setDescription('Command Test');
        }
    
        protected function execute(Input $input, Output $output)
        {
            $redis = new \Redis();
            $redis->connect('127.0.0.1', 6379);
            
            $db = Db::connect('xxx');
            
            Log::init([
                    'type'  =>  'File',
                    'path'  =>  APP_PATH.'logs/'
            ]);
            
            try {
                while (true) {
                    // 写入数据库
                    // ==============
                    // do somthing
                    // ==============
                    // 业务队列
                    $data = unserialize($redis->rPop('300activity:recordLists'));
                    if ($data) {
                        if ($data['h5'] == 1) {
                            unset($data['h5']);
                            $db->name('carve_upf')->insert($data);
                        } else if ($data['h5'] == 2) {
                            unset($data['h5']);
                            $db->name('letter')->insert($data);
                        }
                        Log::info("消费数据:".json_encode($data));
                    } else {
                        sleep(1); // 睡眠 1 秒。
                    }
                }
            } catch (\Exception $e) {
                Log::error("Error:{$e->getMessage()}");
                exit("Error:{$e->getMessage()}");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    这段代码我们很容易看懂。

    它就是通过 循环从 Redis 队列中取出数据并处理。如果没有取到数据就休眠一秒。之所以休眠是为了保证 CPU 能得到充分的利用。

    当我们的业务出现任何错误,我们通过try catch进行异常捕获然后将错误信息记录日志并退当前脚本。
    这样就可以正常消费写入数据库啦

    但是,好景不长。过了一段时间,我发现 Redis 队列的数据出现了未消费的情况。我查看了日志。发现日志里有错误

    [ error ] [8]think\db\Connection::free(): send of 9 bytes failed with errno=32 Broken pipe[/usr/share/nginx/html/gift/byd/thinkphp/library/think/db/Connection.php:310]
    [ error ] 异常Error:Error while sending STMT_CLOSE packet. PID=16040
    
    • 1
    • 2

    解决这个错误前先讲两个概念

    交互式连接&非交互式连接

    什么是交互式连接?

    通俗的说,就是你在你的本机上打开mysql的客户端,就是那个黑窗口,在黑窗口下进行各种sql操作,当然走的肯定是tcp协议。

    什么是非交互式操作?

    就是你在你的项目中进行程序调用。比如一边是nginx web服务器,一边是数据库服务器,两者怎么通信?在php web里,我们通常会选择pdo或者是mysqli来连接。那么这时候就是非交互式操作。

    我们的问题明显出在非交互式操作上

    mysql 配置中有两个 参数 对非交互式操作有直接影响

    interactive_time:是指如果空余 N秒(N就是这个属性的值),那么就会自动关闭mysql的连接。关闭什么样的mysql连接?刚刚,我们讲了什么是mysql的交互式操作和非交互式操作中, mysql是有两种操作方式,那就有两种连接的,一种是交互式,一种是非交互式。而这个属性控制的是交互式。就是你打开一个mysql客 户端黑窗口,进入操作之后,又隔了N秒你不操作了,之后你想继续操作,对不起,mysql会在之前关闭了你的那个连接,mysql会帮你自动重新连接。

    wait_time:是指如果空余 N秒(N就是这个属性的值),那么会自动 kill 掉mysql的一部分连接线程。这里的连接就是指的是非交互式连接。

    编写fpm服务的时候,我们基本上不关心这两个属性,都是用的是mysql服务推荐的默认值,就是8小时,反正一个请求结束就会释放所有连接。

    但是,我这次编写的cli守护进程需要一直运行,这就出现了 “mysql的8小时自动关闭”问题。

    所有这个错误的原因是因为mysql连接长时间没活动被mysql服务端关闭,当进程关闭(停止服务、reload服务、重启服务)时php会向mysql服务端发送一个关闭包告知mysql服务端自己将要关闭,但是因为mysql连接已经断开,所以导致 Warning: Error while sending STMT_CLOSE packet. PID=16040。如果本来进程就是要关闭的,mysql链接断开也无所谓了,但是我并不是要断开,只是因为消费队列里长时间没有数据,自然也就不会执行sql语句,导致服务端主动关闭了客户端连接

    我们复现一下这个问题
    首先设置 wait_time 为120秒过期 :

    set global wait_timeout=120;
    
    • 1

    在这里插入图片描述
    然后启动服务,先写入一条数据到redis队列,等120秒 在写入一条数据就出现下面错误或者出现 Error:PDOStatement::execute(): MySQL server has gone away 错误

    在这里插入图片描述

    有什么办法解决这个问题呢,可以把mysql服务端 wait_timeout改长一些?治标不治本
    或者每次处理数据都建立新连接,处理完数据就关闭mysql连接。这一看就不符合社会主义价值观

    好在 V5.0.6+版本开始,thinkphp支持Mysql的断线重连机制,默认关闭,需要的话,是数据库配置文件中添加

    // 开启断线重连
    ‘break_reconnect’ => true,

    不过尴尬的是生产环境的tp 版本 5.0.5 没有这个功能,那只能自己撸

    
    namespace app\admin\command;
    
    use think\Db;
    /**
     * 数据库主动重连
     */
    class ReloadDb {
    
    	private static $time = null;
        private static $db;
        /**
         * 检测或执行主动重连
         * @author andy3513
         * @param int     $timeout 超时时间
         * @param array $config   连接参数
         */
        public static function init($timeout = 7200){
    		$time = time();
    		if(null === self::$time){
    		self::$time = $time;
    		}
    		$exprie = $time - self::$time;
    		if($exprie >= $timeout || empty(self::$db)){
    		self::$db = Db::connect('db_config_gift', true);
    		self::$time = $time;
    		}
    		return self::$db;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    增加 mysql 主动重连代码

    
    
    namespace app\admin\command;
    
    use think\console\Command;
    use think\console\Input;
    use think\console\Output;
    use think\Db;
    use think\Log;
    
    class ActivityUpd extends Command
    {
        protected function configure()
        {
            $this->setName('activityupd')->setDescription('Command Test');
        }
    
        protected function execute(Input $input, Output $output)
        {
            $redis = new \Redis();
            $redis->connect('127.0.0.1', 6379);
            Log::init([
                    'type'  =>  'File',
                    'path'  =>  APP_PATH.'logs/'
            ]);
            
            try {
                while (true) {
                    // 写入数据库
                    // ==============
                    // do somthing
                    // ==============
                    // 业务队列
                    $data = unserialize($redis->rPop('300activity:recordLists'));
                    if ($data) {
                    	$db = ReloadDb::init();
                        if ($data['h5'] == 1) {
                            unset($data['h5']);
                            $db->name('carve_upf')->insert($data);
                        } else if ($data['h5'] == 2) {
                            unset($data['h5']);
                            $db->name('letter')->insert($data);
                        }
                        Log::info("消费数据:".json_encode($data));
                    } else {
                        sleep(1); // 睡眠 1 秒。
                    }
                }
            } catch (\Exception $e) {
                Log::error("Error:{$e->getMessage()}");
                exit("Error:{$e->getMessage()}");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    通过代码对比,我们在第一版代码的基础上加了如下代码:

       	$db = ReloadDb::init();
    
    • 1

    修改后的代码 mysql的8小时自动关闭 的问题再也没出现了。

    可是天不遂人愿过了1天,我发现 Redis 队列的数据又出现了未消费的情况,查看日志,的确没有产生新的消费日志。也没发现有任何的错误发生。

    • 常驻后台进程处理存活状态。并没有变成孤儿进程。
    • 常驻后台进程内存也没有出现泄漏。
    • 系统 CPU/内存 资源都处理正在状态。
    • 系统打开的句柄资源也是低消状态。

    redis 服务端我测试也正常运行写入,也就排除了 Redis 故障的问题。

    我当时也怀疑过是不是像MySQL一样常时间连接不进行任何操作,服务器端会主动断开连接。但是,MySQL 服务器端主动段掉连接会提示:MySQL server has gone away的错误。但是,我们的 Redis 服务器端没有给我们报任何错误信息呀。

    后面我尝试升级 redis 版本 结果 Redis 还是假死了。或者说我们的 Redis 处于伪活状态。

    你认为 Redis 活着,其实它早已经死了。你认为 Redis 死了,但是它却没有死亡的特征。

    最后去redis 官网查看 Redis 的 API。发现它提供了一个ping()的方法来检测连接是否存活。
    我把这个命令加入逻辑中,队列没有数据操作我就ping 一下连接,预防连接出问题

    
    
    namespace app\admin\command;
    
    use think\console\Command;
    use think\console\Input;
    use think\console\Output;
    use think\Db;
    use think\Log;
    
    class ActivityUpd extends Command
    {
        protected function configure()
        {
            $this->setName('activityupd')->setDescription('Command Test');
        }
    
        protected function execute(Input $input, Output $output)
        {
            $redis = new \Redis();
            $redis->connect('127.0.0.1', 6379);
            
            Log::init([
                    'type'  =>  'File',
                    'path'  =>  APP_PATH.'logs/'
            ]);
            
            try {
                while (true) {
                    // 写入数据库
                    // ==============
                    // do somthing
                    // ==============
                    // 业务队列
                    $data = unserialize($redis->rPop('300activity:recordLists'));
                    if ($data) {
                       	$db = ReloadDb::init();
                        if ($data['h5'] == 1) {
                            unset($data['h5']);
                            $db->name('carve_upf')->insert($data);
                        } else if ($data['h5'] == 2) {
                            unset($data['h5']);
                            $db->name('letter')->insert($data);
                        }
                        Log::info("消费数据:".json_encode($data));
                    } else {
                    //此方法TRUE在成功时返回
                        $pong = $redis->ping();
                        if (!$pong) {
                            throw new \Exception('Redis ping failure!', 500);
                        }
                        sleep(1); // 睡眠 1 秒。
                    }
                }
            } catch (\Exception $e) {
                Log::error("Error:{$e->getMessage()}");
                exit("Error:{$e->getMessage()}");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    通过代码对比,我们在第一版代码的基础上加了如下代码:

    //此方法TRUE在成功时返回
    $pong = $redis->ping();
    if (!$pong) {
        throw new \Exception('Redis ping failure!', 500);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    注意:在 PhpRedis 5.0.0 之前,此命令只返回字符串+PONG。

     $pong = $redis->ping();
     if ($pong != '+PONG') {
         throw new \Exception('Redis ping failure!', 500);
     }
    
    • 1
    • 2
    • 3
    • 4

    当我们每次 ping 的时候,Redis 服务器就会认为我们的 Redis 客户端连接处于存活状态。就不会断掉我们的连接了。

    修改后代码假死的问题再也没出现了。

    为了提高稳定性我,后面又给 redis 加上断线重连逻辑,预防网络问题导致连接断开出现的问题

    
    
    namespace app\admin\command;
    
    class Redis
    {
    
        private static $_instance; //存储对象
    
        private function __construct()
        {
            self::$_instance= new \Redis();
            self::$_instance->connect('127.0.0.1', 6379);
    
        }
    
        public static function getInstance()
        {
            if (!self::$_instance) {
                new self();
            } else {
                try {
                	// 定义用户错误级别
                    @trigger_error('flag', E_USER_NOTICE);
                    self::$_instance->ping();
                    $error = error_get_last();
                    if ($error['message'] != 'flag')
                        throw new \Exception('Redis server went away');
                } catch (\Exception $e) {
    				// 断线重连
                    new self();
                }
            }
            return self::$_instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    引入redis 断线重连逻辑

    
    
    namespace app\admin\command;
    
    use think\console\Command;
    use think\console\Input;
    use think\console\Output;
    use think\Db;
    use think\Log;
    
    class ActivityUpd extends Command
    {
    	private  $handler;
        protected function configure()
        {
            $this->setName('activityupd')->setDescription('Command Test');
        }
    
        protected function execute(Input $input, Output $output)
        {   
            Log::init([
                    'type'  =>  'File',
                    'path'  =>  APP_PATH.'logs/'
            ]);
            
            try {
                while (true) {
                    // 写入数据库
                    // ==============
                    // do somthing
                    // ==============
                    // 业务队列
                    $this->handler = Redis::getInstance();
                    $data = unserialize($this->handler->rPop('300activity:recordLists'));
                    if ($data) {
                       	$db = ReloadDb::init();
                        if ($data['h5'] == 1) {
                            unset($data['h5']);
                            $db->name('carve_upf')->insert($data);
                        } else if ($data['h5'] == 2) {
                            unset($data['h5']);
                            $db->name('letter')->insert($data);
                        }
                        Log::info("消费数据:".json_encode($data));
                    } else {
                        sleep(1); // 睡眠 1 秒。
                    }
                }
            } catch (\Exception $e) {
                Log::error("Error:{$e->getMessage()}");
                exit("Error:{$e->getMessage()}");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    我们测试一下加了连接重试与不加的效果

    不加redis 连接重试

    先启动服务,在通过命令 : CLIENT list 查看所有客户端连接
    在这里插入图片描述

    id=107 就是我们编写的客户端连接,id=108 就是我们当前操作的窗口,如何分辨呢?,看 cmd 代表客户端正在执行的命令就知道了

    服务端主动kill 掉客户端连接,执行 redis kill 命令 :CLIENT KILL 127.0.0.1:52115

    查看我们的服务发现已经异常退出
    在这里插入图片描述

    加上redis 连接重试,关闭客户端连接,客户端会再次主动连接上

    在这里插入图片描述
    虽然redis 报错误但是进程依然运行
    在这里插入图片描述

  • 相关阅读:
    任务调度线程池-应用定时任务
    K8s Pod 创建埋点处理(Mutating Admission Webhook)
    中级经济师各专业通过率是多少
    磨损对输送带安全的影响
    Codesys结构变量编程应用(STRUCT类型)
    二极管为何会单向导通
    ETCD数据库源码分析——gRPC 拦截器
    render() 函数即渲染函数 转换器 converter JS函数用于获取url参数:
    Linux操作系统之线程
    Web5到底是什么?Web4去哪了?
  • 原文地址:https://blog.csdn.net/csdn_leidada/article/details/127717354