allsilaevex/swoole-connection-pool

v0.4.0 2024-04-15 16:34 UTC

README

一个基于 Swoole 的坚实、灵活且高性能的连接池。

PHP Programming Language Download Package CI Status Codecov Code Coverage Psalm Type Coverage PHPStan level Psalm level License

⚙️ 安装

composer require allsilaevex/swoole-connection-pool

要求

警告

未对 swoole.enable_preemptive_scheduler = 1 的池进行测试。请自行承担风险!

⚡️ 快速入门

以下示例演示了创建一个简单的连接到 MySQL 数据库的连接池。

<?php

declare(strict_types=1);

require_once __DIR__ . '/vendor/autoload.php';

\Swoole\Coroutine\run(static function () {
    $connectionPoolFactory = \Allsilaevex\ConnectionPool\ConnectionPoolFactory::create(
        size: 2,
        factory: new \Allsilaevex\ConnectionPool\ConnectionFactories\PDOConnectionFactory(
            dsn: 'mysql:host=0.0.0.0;port=3306;dbname=default',
            username: 'root',
            password: 'root',
        ),
    );

    $pool = $connectionPoolFactory->instantiate();

    \Swoole\Coroutine\parallel(n: 4, fn: static function () use ($pool) {
        /** @var \PDO $connection */
        $connection = $pool->borrow();

        $result = $connection->query('select 42')->fetchColumn();

        var_dump($result);
    });
});

✨ 特点

  • 即使在异常情况下也能保持高性能(见 基准测试
  • 处理连接失败和自我恢复
  • 不会增加垃圾回收器的负担
  • 由静态分析器(PHPStan,Psalm)覆盖并支持泛型
  • 开箱即用的连接池提供
    • 基于负载调整连接数量
    • 长时间连接的重连
    • 泄漏连接检测
    • 支持连接的生存周期钩子
  • 可以轻松存储到 Prometheus 并用于分析的指标

❓ 为什么我应该使用连接池?

最明显的原因:连接池通过不为每个请求建立新的连接来节省时间。

另一个不那么明显的原因在于 Swoole 和协程的工作方式。按设计,不能同时在两个不同的协程中使用相同的连接,这意味着你必须为每个协程创建一个单独的连接。与按顺序使用单个连接相比,这增加了额外的开销。它也可能导致连接数量无控制地增长。

以及最不明显的问题:由于执行期间多次上下文切换导致的减慢。上下文切换发生在 IO 操作期间,包括建立连接。因此,以下代码的执行流程可能不明显

<?php

declare(strict_types=1);

\Swoole\Coroutine\run(static function () {
    \Swoole\Coroutine\go(static function () {
        echo '1' . PHP_EOL;

        $pdo = new \PDO(
            dsn: 'mysql:host=0.0.0.0;port=3306;dbname=default',
            username: 'root',
            password: 'root',
        );

        echo '2' . PHP_EOL;

        $pdo->query('select 1')->fetchAll();

        echo '4' . PHP_EOL;
    });

    \Swoole\Coroutine\go(static function () {
        echo '3' . PHP_EOL;
    });
});

// output:
// 1
// 3
// 2
// 4

如果第二个协程中出现 CPU 密集型负载会发生什么?由于协程是在同一进程内执行的,它们不能并行执行 CPU 密集型代码。因此,如果一个协程正在执行 PHP 代码,其他协程将等待。因此,查询的执行将被推迟,直到第二个协程完成其工作并将控制权返回给第一个协程。

🚀 基准测试

无连接池

Running 10s test @ http://0.0.0.0:11111/test
  8 threads and 64 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   238.48ms  301.25ms   3.06s    85.59%
    Req/Sec    55.56     54.91   353.00     92.78%
  Latency Distribution
     50%  103.29ms
     75%  328.69ms
     90%  691.90ms
     99%    1.12s
  4280 requests in 10.02s, 667.52KB read
  Non-2xx or 3xx responses: 1736
Requests/sec:    426.94
Transfer/sec:     66.59KB

有连接池

Running 10s test @ http://0.0.0.0:11111/test
  8 threads and 64 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    11.33ms    1.72ms  38.38ms   82.34%
    Req/Sec   709.47     34.52   777.00     73.12%
  Latency Distribution
     50%   11.08ms
     75%   11.86ms
     90%   12.97ms
     99%   18.03ms
  56525 requests in 10.02s, 8.19MB read
Requests/sec:   5642.51
Transfer/sec:    837.56KB

有关测试的更多信息,请参阅 文档

🔧 配置

为了简化配置和创建池,有 ConnectionPoolFactory。工厂具有默认设置,但强烈建议根据您的具体需求自定义参数。

以下示例演示了配置选项

<?php

declare(strict_types=1);

require_once __DIR__ . '/vendor/autoload.php';

\Swoole\Coroutine\run(static function () {
    $connectionPoolFactory = \Allsilaevex\ConnectionPool\ConnectionPoolFactory::create(
        // Maximum number of connections in the pool
        size: 4,

        // A trivial PDO connection factory
        // For other connections, you need to define factory that implements \Allsilaevex\Pool\PoolItemFactoryInterface
        factory: new \Allsilaevex\ConnectionPool\ConnectionFactories\PDOConnectionFactory(
            dsn: 'mysql:host=0.0.0.0;port=3306;dbname=default',
            username: 'root',
            password: 'root',
        ),
    );

    // The minimum number of connections that the pool will maintain
    // Setting it to 0 means the pool will create connections only when needed
    // Setting it to MAX means the pool will always keep exactly MAX connections
    $connectionPoolFactory->setMinimumIdle(2);

    // The time during which connections can remain idle in the pool
    // After the timeout expires, connections will be destroyed until the pool size reaches the minimumIdle value
    $connectionPoolFactory->setIdleTimeoutSec(15.0);

    // Maximum connection lifetime
    // When setting this, it's recommended to consider database limits and infrastructure constraints.
    $connectionPoolFactory->setMaxLifetimeSec(60.0);

    // Maximum waiting time for reserving a connection for re-creation (see maxLifetimeSec)
    // This can be useful when all connections in the pool are constantly occupied for a long time
    // Setting it to .0 means there will be no waiting during reservation
    $connectionPoolFactory->setMaxItemReservingForUpdateWaitingTimeSec(.5);

    // The maximum waiting time for a connection from the pool during a borrow attempt
    // After this time expires, an \Allsilaevex\Pool\Exceptions\BorrowTimeoutException will be thrown
    $connectionPoolFactory->setBorrowingTimeoutSec(.1);

    // The maximum waiting time for returning a connection to the pool
    // After this time expires, the connection will be destroyed
    $connectionPoolFactory->setReturningTimeoutSec(.01);

    // If true, then connection will automatically return to the pool after the coroutine in which it was borrowed finishes execution
    // Auto return can only work with coroutine binding!
    $connectionPoolFactory->setAutoReturn(true);

    // If true, then when borrowing a connection from the pool for one coroutine, the same connection will always be returned
    $connectionPoolFactory->setBindToCoroutine(true);

    // A logger is used to signal abnormal situations
    // Any logger that implements \Psr\Log\LoggerInterface is allowed
    $connectionPoolFactory->setLogger(logger: new \Psr\Log\NullLogger());

    // Maximum time that a connection can be out of the pool without leak warnings
    $connectionPoolFactory->setLeakDetectionThresholdSec(1.0);

    // Allows adding a KeepaliveChecker that must implement the \Allsilaevex\ConnectionPool\KeepaliveCheckerInterface
    // This checker will be called at a specified interval and can trigger connection re-creation (if it returns false)
    $connectionPoolFactory->addKeepaliveChecker(
        new class () implements \Allsilaevex\ConnectionPool\KeepaliveCheckerInterface {
            public function check(mixed $connection): bool
            {
                try {
                    $connection->getAttribute(\PDO::ATTR_SERVER_INFO);
                } catch (\Throwable) {
                    return false;
                }

                return true;
            }

            public function getIntervalSec(): float
            {
                return 60.0;
            }
        },
    );

    // Allows adding a ConnectionChecker that must be callable
    // This checker will be called before connection borrowing and can trigger connection re-creation (if it returns false)
    $connectionPoolFactory->addConnectionChecker(
        static function (\PDO $connection): bool {
            try {
                return !$connection->inTransaction();
            } catch (\Throwable) {
                return false;
            }
        },
    );

    // You can specify a pool name for identifying logs, metrics, etc.
    // Or leave the field empty and the name will be generated based on the factory class
    $pool = $connectionPoolFactory->instantiate(name: 'my-pool');

    \Swoole\Coroutine\parallel(n: 8, fn: static function () use ($pool) {
        /** @var \PDO $connection */
        $connection = $pool->borrow();

        $result = $connection->query('select 42')->fetchColumn();

        var_dump($result);
    });
});

许可协议

MIT