积木成楼
首页 / php

php-fpm模式下的优化思路与实践

2022-01-21 · php · 约 13 分钟

php-fpm模式下的优化思路与实践

一般来说,计算机体系中所谓的优化,无非三类,串行改并行,同步转异步,以及加缓存,减少执行。

串行改并行

程序执行总时间= (不可并行子模块…) 相加

php-fpm 模式下 每个请求都是单进程单线程去处理,而 pcntlphp 进程处理扩展)在 fpm 模式下无法使用。所以就有了各种勉强的处理方案。

基于redis || 各种mq 队列的并行操作

具体来说 就是利用 中间件 来对外发布任务,由消费者(比如用 golang 监听 redis ,开协程去处理多个 task)来进行并行的消费 , 然后把结果传回来。

那么你肯定也会有疑问,我直接http调用go的服务然后响应不行吗?当然可以,但是 curlphp-fpm 中是阻塞操作,用队列的方式更加灵活且不会堵塞。

$randKey = Str::random();
$redis->lpush($key,$task1.'-'.$task2.':'.$rankKey);
// 堵塞等待消费者将 task 完成 然后把结果 塞入  $key.$rankKey 
$data = $redis->brPop($key.$randKey, 10);
......//其他逻辑

基于 popen || proc_open || exec 的并行操作

    for ($i=0; $i < 10; $i++) {
        $pipe[$i] = popen('php ./exec.php ', 'w');
    }
    for ($i=0; $i < 10; $i++) {
        pclose($pipe[$i]);
    }

这种用 shell 的方式虽然看起来确实是简单了,但是存在两个问题,一个是函数的平台限制,第二是安全性较低,但不得不说 shell 有些特殊场景下确实很有用。

同步转异步

在这类操作里面 除了串行改并行的的两种方式可以实现(需要额外的进程去处理),将执行操作异步,日常用的最多的是 fastcgi_finish_request() ,简单实在看下在 laravel 中的使用

# public/index.php

$response = $kernel->handle(
    $request = Request::capture()
)->send();

$kernel->terminate($request, $response);

# vendor/symfony/http-foundation/Response.php:391
public function send()
{
    $this->sendHeaders();
    $this->sendContent();

    if (\function_exists('fastcgi_finish_request')) {
    	fastcgi_finish_request();
    } elseif (\function_exists('litespeed_finish_request')) {
    	litespeed_finish_request();
    } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
    	static::closeOutputBuffers(0, true);
    }

    return $this;
}

其实 tp 里面也一样使用了这个函数来进行收尾,优点很明显,代码还是在同一个进程内,无需额外的配置,开箱即用。缺点的话,就是在响应之后的逻辑比较重的情况下,fpm 的进程数会随着请求量的增加而增加。也会受到脚本最大执行时间的限制,只适合一些比较轻量级的异步操作,重要的业务逻辑,建议还是以 MQ 为主。

fpm 中的性能优化实践

缓存的重要性就不说了,在 php-fpm 的生命周期中,最重要的缓存就是 OPcache ,特别是 laravel 这种文件特别多,逻辑很重的框架 开不开 OPcache ,是两种体验,相关的配置 可以看我之前写过的 博文

  1. 不开启 OPcache 的情况下相关统计
Label# 样本平均值最小值最大值标准偏差异常 %吞吐量接收 KB/sec发送 KB/sec平均字节数
HTTP请求35053773961151.250.00%12.52556.42.52523
总体35053773961151.250.00%12.52556.42.52523
  1. 开启 OPcache 不开 JIT 的情况
Label# 样本平均值最小值最大值标准偏差异常 %吞吐量接收 KB/sec发送 KB/sec平均字节数
HTTP请求35055917326.020.00%92.8628347.4318.68523
总体35055917326.020.00%92.8628347.4318.68523
  1. 开启 OPcache 开启 JIT 的情况
Label# 样本平均值最小值最大值标准偏差异常 %吞吐量接收 KB/sec发送 KB/sec平均字节数
HTTP请求35053918022.50.00%100.5169451.3420.22523
总体35053918022.50.00%100.5169451.3420.22523
  1. 在开启 OPcache 开启 JIT 的情况下,根据 对 sql 执行顺序的分析,我发现更新的操作是最耗时且无用的,如果将这一步,移入到fastcgi_finish_request() 之后去执行,开启异步操作,效果如何呢?
Label# 样本平均值最小值最大值标准偏差异常 %吞吐量接收 KB/sec发送 KB/sec平均字节数
HTTP请求3501271016533.530.00%47.7685324.49.61523
总体3501271016533.530.00%47.7685324.49.61523

很明显,异步操作并没有减轻系统的压力(毕竟在同一个进程内)反而,使得系统的吞吐量大减,原因为 响应提前给了 JMeter 但是 实际上 fpm 进程还在被占用,从而使得 大量的请求被堵塞。数据才是最真实的。所以对于高并发与负载的系统,异步操作应该尽可能的移出主逻辑程序。

相关对比图如下所示

laravel 写接口,开不开 OPcache 吞吐性能差距在10倍左右,而开 JIT 比不开 提升大概有 5%-10% ,当然这是测试环境,正式服的话,一个是 fpm 的进程数要根据监控还有服务器配置进行动态的调整,OPcahe 的配置项也要根据监控数据进行动态的修改,laravel 本身的路由与配置缓存,根据实际情况再考虑加不加,单机 fpm + 4H16G linux 基本上能满足市面上绝大多数中小型企业的日常业务需求。

如果再进一步优化,就考虑 长连接与连接池,常驻内存的框架,其它包括GC,内存,JIT,调度,这些就要看语言特性了,没法强求。在一般业务场景中,opcache+jit 已经足够使用了。目前的困境在于两点,一是 fpm 的简易性[语言下限跟上限都比较低]与 专攻web的局限性,二是缺少大公司背书与就业市场&舆论环境的双重恶化。

梳理完 fpm 的基础优化后,发现 408的4门基础课还是永远的神,应用层的东西,抽象的再厉害,一旦需要性能,底层知识你永远也避不开。

← 返回文章列表