乐趣区

关于php:Chapter-14PHPFPM模式下我为框架增加了伪异步defer功能

欢送来到「我是真的狗杂谈世界」,关注不迷路

略读

  • CGI+ 同步阻塞计划异步工作计划有点重,但又不想放弃傻瓜计划的长处;
  • 思考后将问题转换成:

    • 提前返回响应后持续同步执行非重要工作;
    • 程序编写逻辑,提早执行局部非重要工作;
  • 解决这两个问题:

    • 应用 fastcgi_finish_request;
    • 借鉴 golang defer;
  • 实现、成果和注意事项。

背景

团队技术背景

目前咱们团队小组的技术状况如下:

  • 以 PHP 作为开发语言开发 Web 类接口服务;
  • 采纳传统的 Nginx+FPM 模式运行服务;
  • 我创立并保护了新的开发框架;
  • 框架基于 Slim v3.7,是小组之前依赖的,因思考过渡老本临时没扭转。

场景问题

近期在一些我的项目中发现常常遇到业务接口有以下特点:

  • 有主:一个接口中局部逻辑(比方下方栗子中的 1 /3/5,后续简称主逻辑)是须要保障解决胜利并将后果反馈给调用方的;
  • 有支:另一部分逻辑(比方下方栗子中的 2 /4,后续简称支逻辑)则可容忍(临时)失败,甚至调用方并不关怀后果或说感知不显著;
  • 简略:大部分支逻辑比较简单,简略判断加上打文件日志、写条 MySQL 日志记录、发个 HTTP 申请等类;
  • 混合:主支逻辑在代码编写程序上往往是穿插混同而非若明若暗的。

对于代码编写程序当然也能够特意把两局部离开,但这样并不合乎惯例开发同学实现的思路脉络,也不利于代码浏览了解和变量管制


举一个接口栗子(瞎编的):

  1. 【主】一个重要的扣除逻辑,胜利能力持续;
  2. 【支】扣除失败则发送一条 info 级别的邮件音讯,胜利与否都行;
  3. 【主】一个重要的发货逻辑,胜利能力持续;
  4. 【支】发货失败则发送一条 error 级别的邮件音讯,发送失败则记录本地文件日志;
  5. 【主】组装后果并返回。

同步模型

先闭上眼睛一把梭程序编写代码实现,同步执行流程如下(事实上一开始我真是这么一把梭实现的):

异步模型

  • 遗憾的是有一天 QA 同学说你这个接口响应耗时太高了,压测时的体现更显著;
  • 更遗憾的是线上竟然还产生了 2 / 4 步骤 DNS 解析超时问题(前面发现是整个 libcurl 问题,不仅 DNS 解析,当然这是另一个话题了)

这种场景下传统常见的计划兴许咱们不会生疏——过程级别的异步工作计划!

  • Laravel/Lumen 也是采纳这个计划的
  • 咱们组内另一位同学也为咱们的开发框架反对了这种异步工作的计划,其实是能够间接采纳的,只是它并不是本文的配角~

执行流程如下:

多线程、协程 + 异步 IO 调度模型

当然还有很多其余计划,不过也都比较复杂,且不利于放弃 CGI+ 同步阻塞这套模型给团队同学的门槛益处和对服务的稳固平安,所以也不是本文的配角~

特地是团队往年的主基调是品质、与效率,更加不想这个时候搞事件啦~

思路

上述异步模型其实是比拟成熟的计划抉择,只是它也存在着一些问题 / 弊病:

  • 队列服务依赖:须要额定依赖一个队列服务,这也肯定水平上依赖了其可用性、同时自身也是一条网络 IO 开销;
  • 生产过程治理:须要独立的生产过程生产队列中的异步工作,独立生产过程也须要额定思考其运行状态保护和管制;
  • 解决链路增长:这个就不多说了,尽管这对于异步工作而言倒不算什么。

剖析转换

那有没有(应用老本和运行效率上)轻量级又保留现有劣势的计划呢?


思考剖析:

  • 既然依然是采纳同步阻塞计划,也就是说不去做串行改并行的优化,仍旧是原来的执行工夫开销;
  • 同时想要轻量级,那须要将队列服务、生产过程都干掉,只能仍旧由以后解决该申请的 FPM 解决过程来执行全副逻辑;
  • 那能不能搞一个伪 ” 异步 ” 呢?让调用方在感知上提前结束,但该 FPM 解决过程仍旧会实现剩下逻辑。

问题转换:

  1. PHP 在 FPM 运行模式下能不能让申请响应提前返回给调用方?
  2. 能不能程序编写代码逻辑,但执行时将指定局部的代码逻辑块提早到某个指定逻辑之后?(这是因为我须要思考封装成不便大家应用的框架能力)

问题 1 解法:fastcgi_finish_request

很容易想到 fastcgi_finish_request
,FPM 正好又是 FastCGI 模式,能够应用

当然应用它是要留神一些点的,具体我放在本文最初了~


另外印象里 Laravel/Lumen 有个终结者中间件,如同也是实现了相似性能(先返回响应给调用方,再继续执行终结者中间件逻辑),于是打算去翻翻源码回顾验证一下其实现原理:

  • 对这个有印象是因当年有共事应用 Laravel 时遇到通过框架提供的 session 批改和保留办法然而并没有失效的问题
  • 帮助排查时大略看到过框架对于 session 的操作都是在过程内存级别的,只在终结者中间件中才会将 session 残缺笼罩到存储驱动中
  • 问题出在共事一通逻辑解决后没有采纳框架提供的 response 办法返回响应,间接 echo 而后 exit,以至于没法走到框架后续返回响应和执行终结者中间件。。。
  • 所以历历在目~~

通过 Laravel 源码验证也是通过 fastcgi_finish_request 来实现此性能的:

  • 浏览了解为:kernel->handle 后失去 response,response->send 中执行了 header 和 body 的设置输入后,执行了 fastcgi_finish_request
  • 当然它对其余运行模式也做了兼容,然而我临时用不到
$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Kernel::class);

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

$kernel->terminate($request, $response);
    /**
     * Sends HTTP headers.
     *
     * @return $this
     */
    public function sendHeaders(): static
    {
        // headers have already been sent by the developer
        if (headers_sent()) {return $this;}

        // headers
        foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {$replace = 0 === strcasecmp($name, 'Content-Type');
            foreach ($values as $value) {header($name.':'.$value, $replace, $this->statusCode);
            }
        }

        // cookies
        foreach ($this->headers->getCookies() as $cookie) {header('Set-Cookie:'.$cookie, false, $this->statusCode);
        }

        // status
        header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);

        return $this;
    }

    /**
     * Sends content for the current web response.
     *
     * @return $this
     */
    public function sendContent(): static
    {
        echo $this->content;

        return $this;
    }

    /**
     * Sends HTTP headers and content.
     *
     * @return $this
     */
    public function send(): static
    {$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;
    }

问题 2 解法:参考 golang defer

前文说到我要思考框架封装不便组内同学应用的,所以不能裸写而要思考大家应用时的便捷性

我想如果能够同步编写代码逻辑块,但在执行程序上将标记的代码逻辑块提早到申请响应返回后继续执行就很现实了~


等等,这不是跟 golang 中的 defer 很像吗?而且都是提早执行的意思!当然它们实质是有区别的:

  • golang 的 defer 在语言级,作用是将逻辑块执行提早到以后函数栈帧要销毁前执行,参考「Chapter 15.Go defer 介绍与实现浅析」;
  • 而我想做的是在框架层的(尽管我也感觉如果 php 有语言级的 defer 也挺好),用来将逻辑块的执行提早到申请响应完结后的意思。

实现

golang 的 defer 是通过链表实现的,而 php 吗——array 走天下哈哈!


先定义一个 Defer 类用来注册、判断和执行要被 ’defer’ 的逻辑块(callable):

class Defer
{protected array $deferList = [];

    public function defer(callable $function): void
    {$this->deferList[] = $function;
    }

    public function isEmpty(): bool
    {return empty($this->deferList);
    }

    public function run(): void
    {foreach ($this->deferList as $function) {$function();
        }
    }
}

  • 实例化 Defer 并挂载到 Container 上;
  • 执行 slim App run,其中能够任由业务逻辑中进行 ’defer’ 注册;
  • 如果注册了 ’defer’,则提前返回申请响应后执行各 ’defer’:
$container['defer'] = new Defer();

$app->run();

// 响应后工作
$container = $app->getContainer();
if ($container->has('defer')) {$defer = $container['defer'];
    if (! $defer->isEmpty()) {fastcgi_finish_request();
        $defer->run();}
}

业务逻辑中的 ’defer’ 注册:


    // TODO 1

    $container['defer']->defer(function () {// TODO 2});
    
    // TODO 3
    
    $container['defer']->defer(function () {// TODO 4});
    
    // TODO 5

成果

对于嵌套 defer

实践上也能够做成像 go 的 defer 任意嵌套,但既然是提早到申请返回响应后嵌套就没意义,反而会减少编码和浏览的复杂性,所以罗唆不反对。

对于闭包变量

应用时需注意变量生命周期,毕竟不像 go 的 defer 是语言层面执行到 defer 时先处理函数签名再持续的,具体闭包与变量生命周期相干参考:

  • 「Chapter 16.PHP 变量生命周期」
  • 「Chapter 17.PHP 闭包」

defer 运行模型

通过上述一系列的姿态,咱们依照程序编写的代码逻辑执行程序就产生了变动:

实践成果

从下面的解决时序不难看出,伪 ’ 异步 ’ 模型能带来:

  • 晋升接口响应速度(因为提前返回响应了);
  • 在 CPU 等硬件资源和 FPM 等软件资源未达瓶颈时晋升 QPS 体现(QPS= 并发度 * 均匀响应耗时,并发度不变,响应耗时升高,收到响应后持续申请,因为资源够用,QPS 升高);
  • 在 CPU 等硬件资源和 FPM 等软件资源达到瓶颈时无奈晋升 QPS 体现(申请提前返回后,再次发动申请,原先的逻辑还没解决完,又达到瓶颈了,故此无奈晋升 QPS)。

应用留神

另外伪 ’ 异步 ’ 仍旧在 FPM 过程中,可不能像后面的传统异步一样解决长工作:

  • 仍旧受到 PHP 最大 CPU 执行工夫(php.ini 的 max_execution_time)的限度;
  • 但不再受到 FPM 申请终结超时(fpm.conf 的 request_terminate_timeout)的限度;
  • 想想长工作占据光了 FPM 的话,新的申请怎么办呢?
退出移动版