swoole 4.x 连接池协程版本细节坑

Posted PHP技术大全

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了swoole 4.x 连接池协程版本细节坑相关的知识,希望对你有一定的参考价值。

使用swoole 4.x协程版本后,系统资源利用率提高很多,与此同时发现很多开发习惯已经不同

设计如下:

<?php

namespace WhetStone\Stone\Driver;

/**
* 触发式连接池
* 空余连接数不够才会增长连接数
* 连接到达上限会阻塞等待指定秒数后抛异常
*/
abstract class Pool
{

private $_pool = NULL;

protected $_config = NULL;

//整体最多连接数
private $_maxObjCount = 50;

//正在被调用个数
private $_invokeObjCount = 0;

/////////////////////////////////////////////////////
//获取对象,对象必须自带重连

/**
* Fend_Pool constructor.
* @param int $maxObjCount 最大连接数
* @param float $waitTimeout 连接池满等待超时时间
* @param array $config 数据连接配置
*/
public function __construct($maxObjCount = 50, $waitTimeout = 3.0, $config = array())
{
$this->_maxObjCount = $maxObjCount;
$this->_waitDelay = $waitTimeout;
$this->_config = $config;
$this->_pool = new \Swoole\Coroutine\Channel($maxObjCount);

//一次性创建好所有连接
//失败一个会异常抛出
//桶哥建议,好处是请求响应时间稳定
for ($count = 0; $count < $maxObjCount; $count++) {
$obj = $this->getDriverObj();
$this->_pool->push($obj);
}
}

//对象报错时会调用这个函数整理错误

/**
* 对象调用操作
* @param string $name 动作名称
* @param array $arguments 参数
* @return mixed
* @throws
*/
public function __call($name, $arguments)
{
$obj = null;

try {
$obj = $this->fetchObj();

$result = call_user_func_array(array(
$obj,
$name
), $arguments);

$this->recycleObj($obj);

return $result;
} catch (\Exception $e) {
$this->onError($obj, $e);
throw $e;
}
}

/////////////////////////////////////////////////////

private function fetchObj()
{
//pool is empty and have idle space
if ($this->_pool->isEmpty() && ($this->_invokeObjCount + $this->_pool->length() < $this->_maxObjCount)) {

$obj = $this->getDriverObj(); //这里有坑!!!!!后续讲解
$this->_invokeObjCount++;

return $obj;
}

//channel have obj
//fetch obj by 3 second wait
$obj = $this->_pool->pop(3.0);

if ($obj !== FALSE) {
//increase count
$this->_invokeObjCount++;
return $obj;
}

//fetch fail max arrive
throw new \Exception("fetch pool obj fail... please increase the connection pool size.", -123);
}

/**
* 获取数据对象,return对象即可
* @return mixed
*/
abstract public function getDriverObj();

private function recycleObj($obj)
{
if (empty($obj)) {
return;
}

//decrease count
$this->_invokeObjCount--;

return $this->_pool->push($obj);
}

abstract public function onError($obj, $e);


}

从上图可以看到我做了一个父类,给各种驱动继承后回调方式传回不同对象,以此实现一个池管理,当有命令调用的时候直接通过__call方法将请求传递到obj内。

子类如果使用可以用如下方式使用

<?php

namespace WhetStone\Stone\Driver\Redis;

/**
* 基础新版Redis驱动做的
* 新版本明确服务器不可回应时抛出异常
*
* Class Redis
* @package WhetStone\Stone\Driver\Redis
*/
class Redis
{
private $config = null;

private $dbName = "";

private $redis = null;

public function __construct($dbname, $config)
{
$this->dbName = $dbname;
$this->config = $config;

//do connect
$this->reconnect();
}

//if connection is broken reconnect
public function checkConnection()
{
try {
if ($this->redis->ping() != "+PONG") {
$this->reconnect();
}
} catch (\Exception $e) {
$this->reconnect();
}
}

public function reconnect()
{
$this->redis = new \Redis();

//connect the server
$ret = $this->redis->connect($this->config["host"], $this->config["port"], $this->config["timeout"] ?? 0);
if (!$ret) {
throw new \Exception("connect Redis Server fail:" . $this->redis->getLastError(), -24);
}

$this->redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_RETRY);
$this->redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);

//prefix
if (isset($this->config["prefix"]) && !empty($this->config["prefix"])) {
$this->redis->setOption(\Redis::OPT_PREFIX, $this->config["prefix"]);
}

//auth
if (isset($this->config["auth"]) && !empty($this->config["auth"])) {
if ($this->redis->auth($this->config["auth"]) == FALSE) {
throw new \Exception("Redis auth fail.dbname:" . $this->dbName, -23);
}
}

//db
if (isset($this->config["db"]) && !empty($this->config["db"])) {
$this->redis->select($this->config["db"]);
}


}

public function __call($name, $arguments)
{
//check is work well
$this->checkConnection();

//do the cmd,如果刚检测完还报错,那。。。
return call_user_func_array(array($this->redis, $name), $arguments);
}
}

实际运行当中却碰到了一个奇怪的问题,我在pool中规定连接池对象只能有50个,但是使用ab压测-c 1000的时候发现实际会超过我规定的数量,并且请求出现卡死现象。

经过深入及swoole作者之一twose帮助,通过gdb发现代码卡在channel->push上

invoke count:236 pool len:0
invoke count:237 pool len:0
invoke count:238 pool len:0
invoke count:239 pool len:0
invoke count:240 pool len:0
invoke count:241 pool len:0
invoke count:242 pool len:0
invoke count:243 pool len:0
invoke count:244 pool len:0
invoke count:245 pool len:0
invoke count:246 pool len:0
invoke count:247 pool len:0
invoke count:248 pool len:0

反向推理后发现关键问题在这里:

private function fetchObj()
{
//pool is empty and have idle space
if ($this->_pool->isEmpty() && ($this->_invokeObjCount + $this->_pool->length() < $this->_maxObjCount)) {
try {
$this->_invokeObjCount++; //此处是关键,一定要放在getDriverObj前面
$obj = $this->getDriverObj();
} catch (\Exception $e) {
$this->_invokeObjCount--;
throw $e;
}

return $obj;
}

//channel have obj
//fetch obj by 3 second wait
$obj = $this->_pool->pop(3.0);

if ($obj !== FALSE) {
//increase count
$this->_invokeObjCount++;
return $obj;
}

//fetch fail max arrive
throw new \Exception("fetch pool obj fail... please increase the connection pool size.", -123);
}

故障原因为:并发请求1000时,1000个请求会同时调用池获取对象,这时new redis及connect会自动进入协程,由于计数没有先添加,其他并发的请求拿到的已经分配的_invokeObjCount个数没有超过指定数量,所以导致了1000个redis对象被new及connect,最终导致channel超过了之前规定的50…

虽然swoole4.x的协程底层只有一个线程,但是一定要识别出哪里会自动协程……

这个连接池有很大缺点,刚才讨论后发现还需要改进,如pipline,事务会有问题,稍晚会继续介绍新版本

干货分享


以上是关于swoole 4.x 连接池协程版本细节坑的主要内容,如果未能解决你的问题,请参考以下文章

Python进程间通信进程池协程

python基础之进程间通信进程池协程

Python开发基础--- 进程间通信进程池协程

Python入门学习-DAY37-进程池与线程池协程gevent模块

用Swoole4 打造高并发的PHP协程Mysql连接池

swoole2-mysqlpool:基于 Swoole 2 协程特性实现的 MySQL 连接池