Swoft 源码剖析 - 链接池

做者:bromine
连接:https://www.jianshu.com/p/1a7...
來源:简书
著做权归做者全部,本文已得到做者受权转载,并对原文进行了从新的排版。
Swoft Github: https://github.com/swoft-clou...php

为何须要引入链接池?

对于基于php-fpm的传统php-web应用,包括且不限于Mysql,Redis,RabbitMq,每次请求到来都须要为其新建一套独享的的链接,这直接带来了一些典型问题:git

  1. 链接开销:链接随着http请求到来而新建,随着请求返回而销毁,大量链接新建销毁是对系统资源的浪费。
  2. 链接数量太高:每个请求都须要一套本身的链接,系统链接数和并发数会成一个近线性的关系。若是系统并发量达到了1w,那么就须要创建1w个对应的链接,这对于Mysql之类的后端服务而言,是一个大的负荷。
  3. 空闲链接:假设咱们有一个接口使用了一个Mysql链接。该接口在一开始进行一次sql查询后,后面的操做都是sql无关的,那么该请求占据的空闲链接彻底就是一种资源的浪费。

对于异步系统而言,这个问题变得更加的严峻。一个请求处理进程要对同一个服务进行并发的操做,意味着这个请求要持有1个以上同类的链接,这对于系统压力而言,无疑是雪上加霜了,因此链接池对于基于Swoole的Web框架而言已是一个必需实现的机制了。github

Swoft链接池的生命周期与进程模型

链接池做为一个SCOPESINGLETON的典型Bean,
其实例最先会在Swoft\Bean\BeanFactory::reload()阶段被初始化。web

Worker/Task进程

对于RPC或者HTTP请求而言,关系最密切的进程确定是Worker和Task进程了。
对于这二者而言 SwoftBeanBeanFactory::reload()会在swoole的onWorkerStart事件的回调阶段阶段被调用。sql

//Swoft\Bootstrap\Server\ServerTrait(HttpServer和RpcServer都使用了该性状)
/**
 * OnWorkerStart event callback
 *
 * @param Server $server server
 * @param int $workerId workerId
 * @throws \InvalidArgumentException
 */
public function onWorkerStart(Server $server, int $workerId)
{
    // Init Worker and TaskWorker
    $setting = $server->setting;
    $isWorker = false;

    if ($workerId >= $setting['worker_num']) {
        // TaskWorker
        ApplicationContext::setContext(ApplicationContext::TASK);
        ProcessHelper::setProcessTitle($this->serverSetting['pname'] . ' task process');
    } else {
        // Worker
        $isWorker = true;
        ApplicationContext::setContext(ApplicationContext::WORKER);
        ProcessHelper::setProcessTitle($this->serverSetting['pname'] . ' worker process');
    }

    $this->fireServerEvent(SwooleEvent::ON_WORKER_START, [$server, $workerId, $isWorker]);
    //beforeWorkerStart()内部会调用BeanFactory::reload();
    $this->beforeWorkerStart($server, $workerId, $isWorker);
}

这意味着此时的链接池对象的生命周期是 进程全局期而不是程序全局期
将进程池设计为进程全局期,而不是共享程度最高的程序全局期缘由,我的认为主要有3个数据库

  1. 多个进程同时对一个链接进行读写会致使数据传输错乱,须要保证链接不会被同时访问。
  2. Worker进程对程序全局期的对象进行写操做时会致使写时复制,产生一个进程全局期的副本,程序全局期较难维持。
  3. 使用进程全局期的话能够利用现有的Bean机制管理对象,减小的特殊编码。

Process中的链接池

//Swoft\Process\ProcessBuilder.php
/**
     * After process
     *
     * @param string $processName
     * @param bool   $boot 该参数即Process 注解的boot属性
     */
    private static function beforeProcess(string $processName, $boot)
    {
        if ($boot) {
            BeanFactory::reload();
            $initApplicationContext = new InitApplicationContext();
            $initApplicationContext->init();
        }

        App::trigger(ProcessEvent::BEFORE_PROCESS, null, $processName);
    }
}

Swoft中的Process有两种:一种是定义Process 注解的boot属性为true的 前置进程,这种进程随系统启动而启动的 ;另外一种是定义Process 注解的boot属性为false的 用户自定义进程 ,该类进程须要用户在须要的时候手动调用ProcessBuilder::create()启动 。bootstrap

可是不管是何者,最终都会在Process中调用beforeProcess()进行子进程的初始化。对于 boot为true的 前置进程 ,因为其启动时父进程还未初始化bean容器,因此会单独进行bean容器初始化,而对于boot为false的其余 用户自定义进程,其会直接继承父进程的Ioc容器。segmentfault

Swoft基本上遵照着一个进程拥有一个单独链接池的规则,这样全部进程中的链接都是独立的,保证了链接
不会被同时读写。惟独在Process中有一个特例。若是对先使用依赖链接池的服务,如对Mysql进行CRUD,再调用ProcessBuilder::create()启动 用户自定义进程,因为用户自定义进程 会直接继承父进程的Bean容器而不重置,这时子进程会得到父进程中的链接池和链接。后端

Command

/**
 * The adapter of command
 * @Bean()
 */
class HandlerAdapter
{
    /**
     * before command
     *
     * @param string $class
     * @param string $command
     * @param bool   $server
     */
    private function beforeCommand(string $class, string $command, bool $server)
    {
        if ($server) {
            return;
        }
        $this->bootstrap();
        BeanFactory::reload();

        // 初始化
        $spanId = 0;
        $logId = uniqid();

        $uri = $class . '->' . $command;
        $contextData = [
            'logid'       => $logId,
            'spanid'      => $spanId,
            'uri'         => $uri,
            'requestTime' => microtime(true),
        ];

        RequestContext::setContextData($contextData);
    }
}

命令行脚本拥有本身单独的Bean容器,其状况和Process类似且更简单,严格遵循一个进程一个链接池,这里再也不累述。swoole

Swoft链接池
假设Worker数目为j,Task数目为k,Process数为l,Command数为m,每一个进程池内配置最大链接数为n,部署机器数为x,不难看出每一个swoft项目占用的链接数为(j+k+l+m)*n*x

天峰本人曾经提过另外一种基于Swoole的链接池模型。
Rango-<基于swoole扩展实现真正的PHP数据库链接池>

Rango曾提出的链接池方案
这种方案中,项目占用的链接数仅仅为k*x
除了Task进程各个进程并不直接持有链接池,而是经过向Task进程提交指令(task(),sendMessage())让其代为进行链接池相关服务的操做,至少须要额外的一次进程间通讯(默认为Unix Socket)。
该方案虽然可以更好的复用链接和节省链接数,但机制实现并不方便。从另外一个角度去看,Swoft的链接池方案是为了解决使用Swoole时,单进程并发执行的链接数要求问题;Range提出的链接池方案是为了解决超大流量系统下对Mysql等服务的压力控制问题。二者适合不一样的场景,其目的和意义在必定程度下是重合的,但并非彻底同样的。

Swoft链接池的实现

池的容器

链接池根据当前是否协程环境选择一种合适的队列结构做为链接的容器。

  1. \SplQueue:SplQueue是PHP标准库的数据结构,底层是一个双向链表,在队列操做这种特化场景下,性能远高于底层使用链表+哈希表实现的array()数据结构。
  2. \Swoole\Coroutine\Channel是Swoole提供的协程相关的数据结构,不只提供了常规的队列操做。在协程环境下,当其队列长度从0至1之间切换时,会自动让出协程控制权并唤醒对应的生产者或消费者。

链接的获取

\\Swoft\Pool\ConnectionPool.php
abstract class ConnectionPool implements PoolInterface {
    /**
     * Get connection
     *
     * @throws ConnectionException;
     * @return ConnectionInterface
     */
    public function getConnection():ConnectionInterface
    {
        //根据执行环境选择容器
        if (App::isCoContext()) {
            $connection = $this->getConnectionByChannel();
        } else {
            $connection = $this->getConnectionByQueue();
        }

        //链接使用前的检查和从新链接
        if ($connection->check() == false) {
            $connection->reconnect();
        }
        //加入到全局上下文中,事务处理和资源相关的监听事件会用到
        $this->addContextConnection($connection);
        return $connection;
    }
}
\\Swoft\Pool\ConnectionPool.php
/**
 * Get connection by queue
 *
 * @return ConnectionInterface
 * @throws ConnectionException
 */
private function getConnectionByQueue(): ConnectionInterface
{
    if($this->queue == null){
        $this->queue = new \SplQueue();
    }
    
    if (!$this->queue->isEmpty()) {
        //队列存在可用链接直接获取
        return $this->getEffectiveConnection($this->queue->count(), false);
    }
    //超出队列最大长度
    if ($this->currentCount >= $this->poolConfig->getMaxActive()) {
        throw new ConnectionException('Connection pool queue is full');
    }
    //向队列补充链接
    $connect = $this->createConnection();
    $this->currentCount++;

    return $connect;
}
\\Swoft\Pool\ConnectionPool.php
/**
 * Get effective connection
 *
 * @param int  $queueNum
 * @param bool $isChannel
 *
 * @return ConnectionInterface
 */
private function getEffectiveConnection(int $queueNum, bool $isChannel = true): ConnectionInterface
{
    $minActive = $this->poolConfig->getMinActive();
    //链接池中链接少于数量下限时直接获取
    if ($queueNum <= $minActive) {
        return $this->getOriginalConnection($isChannel);
    }

    $time        = time();
    $moreActive  = $queueNum - $minActive;
    $maxWaitTime = $this->poolConfig->getMaxWaitTime();
    //检查多余的链接,如等待时间过长,表示当前所持链接数暂时大于需求值,且易失效,直接释放
    for ($i = 0; $i < $moreActive; $i++) {
        /* @var ConnectionInterface $connection */
        $connection = $this->getOriginalConnection($isChannel);;
        $lastTime = $connection->getLastTime();
        if ($time - $lastTime < $maxWaitTime) {
            return $connection;
        }
        $this->currentCount--;
    }

    return $this->getOriginalConnection($isChannel);
}

加点注释就很是清晰了,此处再也不赘述。

链接的释放

链接的释放有两种不一样的容易引发歧义的用法,为此咱们作如下定义:
一种是链接已经再也不使用了,能够关闭了,这种咱们称为 链接的销毁
一种是链接暂时再也不使用,其占用状态解除,能够从使用者手中交回到空闲队列中,这种咱们称为 链接的归队

连接的销毁

通常经过unset变量,或者经过其余手段清除链接变量的全部引用,等待Zend引擎实现连接资源清理。
这一点在上文的getEffectiveConnection()中出现过。执行到$this->currentCount--;的时候 ,链接已经出队了,而$connection变量会在下个循环时做为循环变量被替换或者方法返回时做为局部变量被清除,链接资源的引用清0.引用降到0的资源会在下次gc执行时被回收,因此你没看到主动的链接释放代码也很正常。
若是你的代码在其余地方引用了这链接而没管理好,可能会致使资源泄露。

连接的归队

/**
 * Class AbstractConnect
 */
abstract class AbstractConnection implements ConnectionInterface
{
    //Swoft\Pool\AbstractConnection.php
    /**
     * @param bool $release
     */
    public function release($release = false)
    {
        if ($this->isAutoRelease() || $release) {
            $this->pool->release($this);
        }
    }
}
//Swoft\Pool\ConnectionPool.php
/**
 * Class ConnectPool
 */
abstract class ConnectionPool implements PoolInterface
{
    /**
     * Release connection
     *
     * @param ConnectionInterface $connection
     */
    public function release(ConnectionInterface $connection)
    {
        $connectionId = $connection->getConnectionId();
        $connection->updateLastTime();
        $connection->setRecv(true);
        $connection->setAutoRelease(true);

        if (App::isCoContext()) {
            $this->releaseToChannel($connection);
        } else {
            $this->releaseToQueue($connection);
        }

        $this->removeContextConnection($connectionId);
    }
}

当用户使用完某个链接后,好比执行了完了一条sql后,应当调用链接的release()方法。
链接自己是持有链接池的反向链接,在用户调用ConnectionInterface->release()方法时,并不会立刻销毁自身,而是清理自身的标记,调用PoolInterface->release()从新加入到链接池中。

//Swoft\Event\Listeners\ResourceReleaseListener.php
/**
 * Resource release listener
 *
 * @Listener(AppEvent::RESOURCE_RELEASE)
 */
class ResourceReleaseListener implements EventHandlerInterface
{
    /**
     * @param \Swoft\Event\EventInterface $event
     * @throws \InvalidArgumentException
     */
    public function handle(EventInterface $event)
    {
        // Release system resources
        App::trigger(AppEvent::RESOURCE_RELEASE_BEFORE);

        $connectionKey = PoolHelper::getContextCntKey();
        $connections   = RequestContext::getContextDataByKey($connectionKey, []);
        if (empty($connections)) {
            return;
        }

        /* @var \Swoft\Pool\ConnectionInterface $connection */
        foreach ($connections as $connectionId => $connection) {
            if (!$connection->isRecv()) {
                Log::error(sprintf('%s connection is not received ,forget to getResult()', get_class($connection)));
                $connection->receive();
            }

            Log::error(sprintf('%s connection is not released ,forget to getResult()', get_class($connection)));
            $connection->release(true);
        }
    }
}

考虑到用户可能会在使用完后没有释放链接形成链接泄露,Swoft会在Rpc/Http请求或者Task结束后触发一个Swoft.resourceRelease事件(注:Swoft是笔者添加的前缀,方便读者区分Swoole相关事件和Swoft相关事件),将链接强制收包并归队。

Swoft源码剖析系列目录: https://segmentfault.com/a/11...
相关文章
相关标签/搜索