内容简介:
- OpenSSL vs LibreSSL
- swoole 4.1.0 添加 coroutine runtime 支持原生 redis/pdo/mysqli
- php 实战 rabbitmq 任务队列: 多work + 协程
- QPS 限制: 令牌桶算法 + php 实战
OpenSSL vs LibreSSL
上一篇 php| 初探 rabbitmq 中, 使用的 composer package 版的 php rabbitmq client. rabbitmq 还支持扩展板的 php rabbitmq client: ext-amqp
. 抱着 扩展(ext, C语言编写) 比 包(package, php编写) 性能高 的朴实想法, 于是决定试试 ext-amqp 扩展. 然后就遇到了问题:
# 安装 amqp, 报错
pecl install amqp
checking for amqp using pkg-config... configure: error: librabbitmq not found
apk search rabbitmq # 查询相似依赖包
apk add rabbitmq-c-dev # 安装, 报错
ERROR: unsatisfiable constraints:
openssl-dev-1.0.2o-r1:
conflicts: libressl-dev-2.6.5-r0[pc:libcrypto=1.0.2o] libressl-dev-2.6.5-r0[pc:libssl=1.0.2o]
libressl-dev-2.6.5-r0[pc:openssl=1.0.2o]
satisfies: world[openssl-dev]
libressl-dev-2.6.5-r0:
conflicts: openssl-dev-1.0.2o-r1[pc:libcrypto=2.6.5] openssl-dev-1.0.2o-r1[pc:libssl=2.6.5]
openssl-dev-1.0.2o-r1[pc:openssl=2.6.5]
satisfies: rabbitmq-c-dev-0.8.0-r3[libressl-dev]
ok, 问题出来了: 系统中已经安装的 OpenSSL, 和 LibreSSL 不兼容
不兼容的原因, GitHub上有答案:
openssl-dev and libressl-dev provide many files that have the same path, therefore the two packages cannot be installed at the same time.
看来江湖要起一番争斗了: 扔掉 OpenSSL,拥抱 LibreSSL——远离心脏出血与溺亡
当然为此也争扎了一番, 说一下我的看法:
- 之前一直使用的 OpenSSL(
apk add openssl-dev
), LibreSSL 是第一次接触 - 发行版已经将原有的 OpenSSL 替换为 LibreSSL 了, Alpine Linux 自 3.5.0 起 -> 我喜欢使用 Alpine Linux
- 打算切一下试试, 探一探江湖的水有多深
swoole 4.1.0 添加 coroutine runtime 支持原生 redis/pdo/mysqli 协程化
这个新特性非常令人激动, 很早就想试了, 不bb, 放测试结果.
不太熟悉协程的小伙伴, 可以移步 swoole| swoole 协程初体验
- 4.1.0版本之前
要支持协程 redis, 需要 swoole 编译支持:
RUN curl -O https://gitee.com/swoole/swoole/repository/archive/v4.1.0.zip && unzip v4.1.0.zip && \
apk add linux-headers openssl-dev nghttp2-dev hiredis-dev && \
cd swoole && \
phpize && \
./configure --enable-openssl --enable-async-redis --enable-http2 && make && make install && \
docker-php-ext-enable swoole && \
rm -rf v4.1.0.zip swoole
需要安装 hiredis
, 并在编译参数中添加 --enable-async-redis
代码使用:
// 多协程版, 真正使用到协程带来的 IO 阻塞时的调度
for ($i = 0; $i < $cnt; $i++) {
go(function () {
$redis = new co\Redis();
$redis->connect('redis', 6379);
$redis->auth('123');
$redis->get('key');
});
}
- 4.1.0 版本
编译:
RUN pecl install swoole && docker-php-ext-enable swoole
PS: 如果需要修改 swoole 编译参数开启其他功能, 还是需要采用上面的方式进行调整
测试代码:
$cnt = 2000;
// 普通版
for ($i=0; $i<$cnt; $i++) {
test_redis();
}
// 协程版
Swoole\Runtime::enableCoroutine();
for ($i=0; $i<$cnt; $i++) {
go(function () {
test_redis();
});
}
function test_redis() {
$redis = new Redis();
$redis->connect("redis", 6379);
$redis->set('test', 'daydaygo');
$r = $redis->get('test');
// var_dump($r);
}
耗时对比:
time php origi_redis_co.php
# 普通版
real 0m 1.35s
user 0m 0.08s
sys 0m 0.41s
# 协程版
real 0m 0.74s
user 0m 0.10s
sys 0m 0.37s
只要开启 Swoole\Runtime::enableCoroutine()
, 就可以轻松切换到协程
需要注意的点:
- 目前只有原生 redis/pdo/mysqli 支持, 其他服务还是需要使用配套的协程 client, 比如 http, rabbitmq
Swoole\Runtime::enableCoroutine()
需要配合go()
一起使用, 否则会报错WARNING yield: Socket::yield() must be called in the coroutine
php 实战 rabbitmq 任务队列: 多work + 协程
任务队列是个老话题了, 使用 redis 自建或者手写一个都不叫事儿
- 基于 swoole 的分布式多进程任务系统: kcloze/swoole-jobs
- 基于 程序员的瑞士军刀 redis LIST 数据类型自建
自建的好处是灵活, 不好的地方就是要实现一些基础功能:
- 监控: 有哪些队列, 堆积了有多少, 任务处理的速度
- 重试
如果还在自建任务队列, 推荐试试 rabbitmq 先.
官方任务队列的 demo -> Tutorial two: Work Queues:
php new_task.php "A very hard task which takes two seconds.."
php worker.php
简单修改 worker.php
代码结构, 注意看注释:
<?php
require_once __DIR__ . '/vendor/autoload.php';
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
$callback = function (AMQPMessage $msg) {
// todo: 需要处理的任务
// 处理完后进行消息确认
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
};
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
$channel->queue_declare('task_queue', false, true, false, false);
$channel->basic_qos(null, 1, null);
$channel->basic_consume('task_queue', '', false, false, false, false, $callback);
// 取一条进行测试
// if (count($channel->callbacks)) {
// $channel->wait();
// }
// 循环直到处理完
while (count($channel->callbacks)) {
$channel->wait();
}
$channel->close();
$connection->close();
- 使用多进程加速
有了 Swoole\Process\Pool
, 要实现多进程实在太轻松:
use Swoole\Process\Pool;
$pool = new Pool($workNum); // 需要开启的进程数
$pool->on('workerStart', function ($pool, $workerId) {
// 上面的代码放进来即可
});
$pool->start();
- 使用协程加速
swoole 也提供了 rabbitmq 的协程版 client, 项目地址 swoole/php-amqplib
使用 composer 引入:
{
"name": "test",
"description": "test",
"minimum-stability": "dev", // 允许使用 dev 版的包
"require": {
"php-amqplib/php-amqplib": "dev-master" // 这里使用的 master 分支, 其他分支则改成 dev-branchname
},
"repositories": [
{
"type": "vcs", // 包的实际地址
"url": "https://github.com/swoole/php-amqplib"
}
]
}
对应的 worker 代码:
<?php
require "../vendor/autoload.php";
use PhpAmqpLib\Connection\AMQPSwooleConnection;
use PhpAmqpLib\Message\AMQPMessage;
use PhpAmqpLib\Channel\AMQPChannel;
$callback = function (AMQPMessage $msg) {
// todo: 消息处理
var_dump($msg->body);
// 处理完后确认消息
/** @var AMQPChannel $ch */
$ch = $msg->delivery_info['channel'];
$ch->basic_ack($msg->delivery_info['delivery_tag']);
};
// 多协程调度
for ($i=0; $i<$coNum; $i++) {
go(function () use ($callback) {
$connection = new AMQPSwooleConnection('rabbitmq', 5672, 'guest', 'guest');
$channel = $connection->channel();
$channel->queue_declare('task_queue', false, true, false, false);
$channel->basic_consume('task_queue', '', false, false, false, false, $callback);
// 循环直到处理完
while (count($channel->callbacks)) {
$channel->wait();
}
});
}
协程版 AMQP 通过重写 PhpAmqpLib\Connection\AMQPSwooleConnection
实现, 底层使用 Swoole\Coroutine\Client
QPS 限制: 令牌桶算法 + php 实战
遇到外部接口的 QPS 限制, 感谢 dreamer_link 提供的方案
/**
* 令牌桶实现限流
* @link https://www.jianshu.com/p/9f76dd2757c7
* @param string $key 设置key
* @param int $initNum 周期内访问次数
* @param int $expire 周期, 单位秒
* @return bool
*/
public static function qpsLimit($key, $initNum, $expire)
{
$time = time();
$redis = Yii::$app->redis->conn;
$redis->watch($key);
$limitVal = $redis->hGetAll($key);
if ($limitVal) {
$newNum = min($initNum, ($limitVal['num'] - 1) + (($initNum / $expire) * ($time - $limitVal['time'])));
if ($newNum > 0) {
$redisVal = ['num' => $newNum, 'time' => time()];
} else {
// 当前时刻令牌消耗完
return false;
}
} else {
$redisVal = ['num' => $initNum, 'time' => time()];
}
$redis->multi();
$redis->hMSet($key, $redisVal);
if (!$redis->exec()) {
// 访问频次过多
return false;
}
return true;
}