allsilaevex / swoole-connection-pool
Swoole 连接池
v0.4.0
2024-04-15 16:34 UTC
Requires
- php: ^8.2
- ext-swoole: ^5.1
- psr/log: 3.0.0
Requires (Dev)
- php-standard-library/psalm-plugin: 2.3.*
- phpbench/phpbench: 1.2.*
- phpstan/phpstan: 1.10.*
- phpstan/phpstan-phpunit: 1.3.*
- phpstan/phpstan-strict-rules: 1.5.*
- phpunit/phpunit: 10.3.*
- psalm/plugin-phpunit: 0.18.*
- slevomat/coding-standard: 8.13.*
- swoole/ide-helper: 5.0.3
- symplify/coding-standard: 12.0.*
- symplify/easy-coding-standard: 12.0.*
- vimeo/psalm: 5.18.*
This package is auto-updated.
Last update: 2024-09-17 18:45:24 UTC
README
一个基于 Swoole 的坚实、灵活且高性能的连接池。
⚙️ 安装
composer require allsilaevex/swoole-connection-pool
要求
- PHP 8.2.0 或更高版本
- Swoole 5.1.0 或更高版本
警告
未对 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); }); });