php 并发和请求次数限制的实现

在 PHP 中

  • 如何限制并发
    • 比如 100 个请求同时过来了,只处理前 10 个请求,让另外 90 个等待着去处理,第 102 个请求等待或丢弃掉。
  • 如何限制单位时间内请求次数
    • 比如一秒内 100 个请求过来了,只处理 10 个请求,其他 90 个请求发出 429 状态码或者等待 1 秒后在继续处理 10 个。当第 102 个请求过来时等待或丢弃掉。

限制并发#

传统的 PHP-FPM 似乎不支持并发限制,因为 PHP-FPM 是同步阻塞的,一个请求进来,一个请求处理完,再处理下一个请求。

reactphp/http 中 自带的有一个中间件 LimitConcurrentRequestsMiddleware 可以限制并发请求。

$http = new React\Http\HttpServer(
    new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10), // 10 concurrent buffering handlers
    $handler
);

它能做到的是 100 个请求过来了,处理 10 个请求,后面的请求会一直等待,第 101 个请求也在等待,不会丢掉。

如何将第 101 个请求,丢掉呢。

在原有的基础上,记录最大总请求数,超过了就丢掉,不失为一个办法。

需要注意的是,对于返回的流响应,怎么定义一次请求的完成,是一种业务上的选择。上方的方式仅仅是 header 准备好,就算一次完成,对于 stream body 是不是发送完成,就没有关注了。

在当前返回 text/event-stream (chatgpt 的事件流) 是一种比较常见的用法后,定义 stream 全部发送完成作为一次请求的 finished, 在业务上就比较有了强的需求。

reactphp/http 支持 stream 响应,且能知道什么时候 stream 发送完成。

在 laravel 中怎么使用呢?需要将 laravel 和 reactphp 结合起来。

laravel-zero 是一个精简版的命令行 laravel,将其与 reactphp 结合起来,就能使用 laravel 的便利性和 reactphp 的异步特性。

安装体验

composer create-project reactphp-x/reactphp-x reactphp-x dev-master -vvv

cd reactphp-x && copy .env.example .env && composer require reactphp-x/limiter-concurrent -vvv

在 route/api.php 中添加


Route::get('/concurrent', new class {

    protected $concurrent;
    public function __construct() {
        // 100个请求同时过来了,只处理前10个请求,让另外90个等待着去处理,第102个请求等待或丢弃掉(一个请求完成:header 和body 发送完毕)。
        $this->concurrent = new \ReactphpX\Concurrent\Concurrent(10, 100, true);
    }
    public function __invoke(ServerRequestInterface $request, $next) {
       return $this->concurrent->concurrent(fn () => $next($request))->then(null, function ($error) {
        // 第101 和 102 请求会返回 503 状态码
        if ($error instanceof \OverflowException) {
            \Log::info('Server busy');
            return new Response(503, [], 'Server busy');
        }
        throw $error;
    });
    }
}, function (ServerRequestInterface $request) {
    $stream = new \React\Stream\ThroughStream();
    \React\EventLoop\Loop::get()->addTimer(1, function () use ($stream) {
        $stream->end("Hello wörld!\n");
    });
    return new Response(200, ['Content-Type' => 'text/plain; charset=utf-8'], $stream);
});

在终端运行

php artisan reactphp:http start

访问 127.0.0.1:8082/concurrent

测试

ab -n 102 -c 102 http://127.0.0.1:8082/concurrent

看看日志里是不是有 2 个 Server busy

限制请求次数#

在 laravel 中

Route::middleware('throttle:10,1')->group(function () {
    Route::get('/user-data', function (Request $request) {
        // 处理用户数据的逻辑
    }); 
});

这样只是返回 429 状态码,可以 block 1 秒后再去处理,那样系统就阻塞在那里了,整体性能会下降。

请求次数的限制,可以使用令牌桶算法,每秒生成 10 个令牌,请求过来,拿一个令牌,没有令牌就返回 429 状态码或继续等待直到获取到令牌。

reactphp-x/limiter 实现了令牌桶算法。接上方的例子,只需要在 route/api.php 中添加

Route::get('/limiter', new class {
    protected $limiterConcurrent;

    public function __construct() {
        // 一秒内100个请求过来了,只处理10个请求,其他90个请求发出429状态码或者等待1秒后在继续处理10个。当第102个请求过来时等待或丢弃掉 (一个请求完成:header 和body 发送完毕)。
        $this->limiterConcurrent = new \ReactphpX\LimiterConcurrent\LimiterConcurrent(10, 1000, false, 100, true);
        $this->limiterConcurrent->release(10);
    }
    public function __invoke(ServerRequestInterface $request, $next) {

        // 立即 429
        // if (!$this->limiterConcurrent->tryAcquire(1)) {
        //     return new Response(429, [], 'Too many requests');
        // }
        // return $next($request);

        // 一秒内100个请求过来了,只处理10个请求,其他90个请求等待1秒后在继续处理10个。当第102个请求过来时等待或丢弃掉 (一个请求完成:header 和body 发送完毕)
        return $this->limiterConcurrent->concurrent(fn () => $next($request))->then(null, function ($error) {
            // 第101 和 102 请求会返回 503 状态码
            if ($error instanceof \OverflowException) {
                \Log::info('limiter Server busy');
                return new Response(503, [], 'Server busy');
            }
            throw $error;
        });
    }
}, function (ServerRequestInterface $request) {
    $stream = new \React\Stream\ThroughStream();
    \React\EventLoop\Loop::get()->addTimer(1, function () use ($stream) {
        $stream->end("Hello wörld!\n");
    });
    return new Response(200, ['Content-Type' => 'text/plain; charset=utf-8'], $stream);
});

测试

ab -n 102 -c 102 http://127.0.0.1:8082/limiter

看看日志里是不是有 2 个 limiter Server busy

这些有什么用?#

你的业务不需要异步操作,这些其实用处不大,但是当你的业务需要异步操作时,这些就能发挥作用了。

对于当下的 gpt 而言,chatgpt 会话模型,对于一个用户的请求,需要等待上一个请求完成后才能继续下一个请求,这时候就需要限制并发请求,保证每个用户能有合理的调用频率。

或者你有一个 gpt 和画图的 token 池,需要对 token 池进行限制,平台的 token 并发和请求次数都是有限制的,这时候做层限制,保证 api 的稳定性。

本作品采用《CC 协议》,转载必须注明作者和本文链接
Make everything simple instead of making difficulties as simple as possible
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 3

常见的一般都是 redis 来控制

6个月前 评论
jcc123 (楼主) 6个月前

可以用 nginx 控制

6个月前 评论