首页 > 解决方案 > 与 CLI 请求相比,导致 HTTP 请求速度变慢的原因是什么?

问题描述

我正在 WAMP 环境中使用 PHP 7.1 开发 Laravel 5.5 应用程序。我正在使用 Money 的值对象,并希望对其进行基准测试以确保使用它的基本算术运算不会太昂贵。因此,我编写了以下测试,比较了使用 PHP 浮点数(控件)和使用 Money 对象(测试)添加货币。它平均了多次测试的时间。

<?php

namespace App\Delegators;
use App\Admin\Attributes\Money;
use App\Admin\General\Currency;
use App\Admin\Marketplaces\NetworkStore;
use App\Admin\Repo;

/**
 * Benchmark helper class.
 */
class Benchmark
{
    public function money()
    {
        /** @var NetworkStore $store */
        $store = Repo::GetSelectedStore();

        /**
         * Declare at what numbers the test starts and ends.
         *
         * This numbers represent the bounds for the number of times money will be added together.
         */
        $testFrom = 90;
        $testTo = 100;

        // Declare the number of times each test will be run.
        $numberOfTests = 2;

        dump('Money Benchmark: Control');

        // Foreach test.
        for ($t = $testFrom; $t < $testTo; $t++)
        {
            // Declare the average time taken for this test.
            $averageTimeTaken = 0;

            // Average the times over multiple such tests.
            for ($c = 0; $c < $numberOfTests; $c++)
            {
                $from = microtime(true);

                $money1 = 100;

                for ($i = 0; $i < $t; $i++)
                {
                    $money2 = (float) random_int(1, 10);

                    $money1 += $money2;
                }

                $averageTimeTaken += microtime(true) - $from;
            }

            // Divide the average by the total number of tests.
            $averageTimeTaken /= $numberOfTests;

            // Declare the themed time in ms.
            $themedTime = round($averageTimeTaken * 1000) .'ms';

            dump("Test $t: $themedTime");
        }

        dump('Money Benchmark: Value Object');

        /** @var Currency $currency */
        $currency = $store->getCurrency();

        // Foreach test.
        for ($t = $testFrom; $t < $testTo; $t++)
        {
            // Declare the average time taken for this test.
            $averageTimeTaken = 0;

            // Average the times over multiple such tests.
            for ($c = 0; $c < $numberOfTests; $c++)
            {
                $from = microtime(true);

                $money1 = new Money(100, $currency);

                for ($i = 0; $i < $t; $i++)
                {
                    $money2 = new Money(random_int(1, 10), $currency);

                    $money1->add($money2);
                }

                $averageTimeTaken += microtime(true) - $from;
            }

            // Divide the average by the total number of tests.
            $averageTimeTaken /= $numberOfTests;

            // Declare the themed time in ms.
            $themedTime = round($averageTimeTaken * 1000) .'ms';

            dump("Test $t: $themedTime");
        }

        dd("Money Test Complete");
    }
}

我在两个地方测试它:在控制器索引方法的顶部和工匠 CLI 命令内部,分别如下所示。

控制器:

<?php

namespace App\Http\Controllers\Admin;

...

class HomeController extends Controller
{
    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Http\Response
     */
    public function index(Request $request)
    {
        $benchmark = new Benchmark;
        $benchmark->money();

        return view('admin.home');
    }
}

命令行:

<?php

namespace App\Console\Commands;

...

class Test extends Command
{
    ...

    /**
     * Execute the console command.
     *
     * @return mixed
     * @throws
     */
    public function handle()
    {
        $benchmark = new Benchmark;
        $benchmark->money();
    }
}

但是 CLI 环境中的基准测试结果比我通过 HTTP 请求获得的结果快 10 倍以上,如下所示。我希望基于缓存和其他配置的两个环境之间存在差异,但我不明白其中任何一个会如何影响我编写的测试的性能。

HTTP 请求的结果:

"Money Benchmark: Control"
"Test 90: 0ms"
"Test 91: 0ms"
"Test 92: 0ms"
"Test 93: 0ms"
"Test 94: 0ms"
"Test 95: 0ms"
"Test 96: 0ms"
"Test 97: 0ms"
"Test 98: 0ms"
"Test 99: 0ms"
"Money Benchmark: Value Object"
"Test 90: 27ms"
"Test 91: 23ms"
"Test 92: 23ms"
"Test 93: 24ms"
"Test 94: 24ms"
"Test 95: 24ms"
"Test 96: 24ms"
"Test 97: 25ms"
"Test 98: 24ms"
"Test 99: 25ms"
"Money Test Complete"

CLI 请求的结果:

"Money Benchmark: Control"
"Test 90: 0ms"
"Test 91: 0ms"
"Test 92: 0ms"
"Test 93: 0ms"
"Test 94: 0ms"
"Test 95: 0ms"
"Test 96: 0ms"
"Test 97: 0ms"
"Test 98: 0ms"
"Test 99: 0ms"
"Money Benchmark: Value Object"
"Test 90: 2ms"
"Test 91: 1ms"
"Test 92: 1ms"
"Test 93: 1ms"
"Test 94: 1ms"
"Test 95: 1ms"
"Test 96: 1ms"
"Test 97: 1ms"
"Test 98: 1ms"
"Test 99: 1ms"
"Money Test Complete"

例如,“Test 90: 1ms”中的数字 90 表示 $money2 已创建并添加到 $money1 90 次。

我唯一的猜测是这是一个内存问题,通过 HTTP 请求加载的应用程序占用更多内存,所以我尝试在应用程序顶部使用 gc_disable(),确认垃圾收集已禁用,但这没有任何作用。我还尝试将 php.ini 中的内存限制加倍,但这也没有效果。

在这一点上,我几乎不知道是什么导致了这里的性能如此巨大的差异。有任何想法吗?

更新

此后,我进行了进一步的测试,将问题缩小到一般性能差距。它的重现性更高,而且是一个简单的加法测试:

<?php

...

/**
 * Benchmark helper class.
 */
class Benchmark
{
    public function addition()
    {
        /**
         * Declare the number of times to add a float.
         */
        $numberOfAdditions = 10000;

        // Declare the number of times each test will be run.
        $numberOfTests = 4;

        dump('Addition Benchmark');

        // Declare the number to add to.
        $number = 0;

        // Declare the average time taken for this test.
        $averageTimeTaken = 0;

        // Average the times over multiple such tests.
        for ($c = 0; $c < $numberOfTests; $c++)
        {
            $from = microtime(true);

            for ($i = 0; $i < $numberOfAdditions; $i++)
            {
                $number += rand(1, 5);
            }

            $averageTimeTaken += microtime(true) - $from;
        }

        // Divide the average by the total number of tests.
        $averageTimeTaken /= $numberOfTests;

        // Declare the themed time in ms.
        $themedTime = round($averageTimeTaken * 1000) .'ms';

        dd("Addition Test Complete: $themedTime");
    }
}

这里又是通过 artisan 命令在控制器与 CLI 中运行的测试。

控制器:

"Addition Benchmark"
"Addition Test Complete: 20ms"

命令行:

"Addition Benchmark"
"Addition Test Complete: 2ms"

更新 2

以下是有关我在 WAMP 上运行的开发环境的其他详细信息:

PHP Version: 7.1.22
System: Windows NT LAPTOP 10.0 build 17134 (Windows 10) AMD64
Build Date: Sep 13 2018 00:39:35
Compiler: MSVC14 (Visual C++ 2015)
Architecture: x64
Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.1.22, Copyright (c) 1999-2018, by Zend Technologies
    with Xdebug v2.6.1, Copyright (c) 2002-2018, by Derick Rethans

Apache/2.4.35 (Win64) OpenSSL/1.1.1a PHP/7.1.22

标签: phplaravelcommand-line-interfacebenchmarkinglaravel-5.5

解决方案


更新

正如您所指出的,与内核无关的性能问题。我进一步深入研究了这个问题,发现在我的系统上行为是相反的。

浏览器响应

 "Addition Test Complete: 1ms"

CLI 响应

 "Addition Test Complete: 13ms"

所以我开始想,也许 php 和 web 服务器版本以及操作系统可能会影响运行的结果。

此外,在查看 cli 中 php 的帮助时,我注意到一个标志:

-n      No configuration (ini) files will be used

因此,我尝试使用该标志 ( php -n bench-test.php) 运行 cli 命令,它确实花费了与 Web 浏览器相同的时间:1ms

我仍然没有 100% 的答案,但我认为这是 php.ini 中的一些参数,它正在做某种缓存,在你的情况下,它在你的 cli 中默认触发,但在从 webserver 执行 php 时不会触发。

您能否提供有关您的 php 版本、操作系统和 Web 服务器的更多信息?以及如果您进行了任何类型的特殊配置或安装了任何扩展

作为参考,我的机器正在运行具有最新更新和以下版本的 Windows 10:

PHP

PHP 7.3.2 (cli) (built: Feb  5 2019 23:16:38) ( ZTS MSVC15 (Visual C++ 2017) x86 )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.2, Copyright (c) 1998-2018 Zend Technologies
    with Xdebug v2.7.0RC2, Copyright (c) 2002-2019, by Derick Rethans

阿帕奇

Server version: Apache/2.4.35 (Win64)
Apache Lounge VC15 Server built:   Sep 19 2018 16:08:47

原始答案

与通过 artisan (cli) 执行命令时相比,接收HTTP 请求时引导的内容是显着的区别之一。

这是因为,出于某些显而易见的原因,我们不需要在 CLI 执行中处理所有路由、请求、中间件和 http 相关的东西。

这就是为什么两次执行具有相似但不同的内核引导过程的原因。


CLI 引导程序(控制台内核)

此过程在您执行任何工匠命令时开始:

php artisan [rest of command]

简而言之,artisan ( source )是一个 php 文件,其行为类似于public/index.php(稍后我们将分析的 laravel 应用程序的 HTTP 入口点)。

artisan 脚本在加载boostrap/app.php 文件以获取应用程序实例($app)后,解析绑定到Illuminate\Contracts\Console\KernelIoC 容器中接口的类。

此类刚刚使用以下代码绑定到App\Console\Kernel.php ( source )中的boostrap/app.php ( source )

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

然后 artisan 继续执行他刚刚得到的实例(记住这是一个类的实例)handle上的方法,它扩展了 Laravel 的核心控制台内核类:这个方法在第 126 行定义的源代码) 。$kernelApp\Console\Kernel.phpIlluminate\Foundation\Console\Kernel

内核执行以下操作:

  1. 通过执行$bootstrappers属性中声明的几个类来引导内核(第 64 行)
  2. Illuminate\Console\Application 获取( source )的单例实例(如果尚未创建,则创建一个新实例)(第 340 行)

注意:这个类可以被“简化”为 symfony 控制台应用程序类的包装器(这超出了本说明的范围)。

  1. 将命令注册到应用程序实例中(第 341 行)
  2. 将输入传递给应用程序的run方法(第 131 行)

注意: run 方法是在 symfony 控制台应用程序类中定义的,但它是由Illuminate\Console\Application该类继承的

  1. 返回第 4 点的结果(第 131 行)

Web 浏览器引导程序(HTTP 内核)

过程非常相似,但这次的入口点不再是工匠,而是public/index.php文件。

如果将此文件与 artisan 进行比较,您只会注意到一些差异,关键是从容器解析的内核是绑定到类的Illuminate\Contracts\Http\Kernel类。

同样,这个类刚刚被绑定到App\Http\Kernel ( source )中,boostrap/app.php代码如下:

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

所以这次Kernel类文件是:Illuminate\Foundation\Http\Kernel source

您可以开始注意到两个内核使用的组件以及两者的大小的一些差异,因为 http 包含更多与路由器组件相关的代码。

实际上,http 内核在创建类时会执行以下操作:

  1. 在路由器中复制中间件优先级
  2. 注册中间件组和别名

然后索引文件捕获并解析传入的HTTP请求并执行该handle方法,因此http内核的操作将继续进行:

  1. 准备请求
  2. 引导内核(与 CLI 内核的第 1 点逻辑相同)
  3. 通过所有已注册的中间件发送请求(全局中间件,而不是附加到路由的中间件,稍后将在路由器内部完成)。
  4. 将请求发送到必须找到匹配路由的路由器,检查其中间件,运行相关的函数/控制器方法,从中获取响应并将其返回给调用者以将其显示为输出。

结论

特别是 http 内核的最后一点(除了一句话总结了路由器的所有工作),与注册几个类(用于命令)并在控制台内核中进行输入匹配相比,这是一项相当繁重的工作。

随着更多组件/功能在它们之间交互(您可以考虑请求、响应、中间件、策略、api 资源、验证、身份验证、授权等),前者有更多的内容。

我给了你一个更技术性的答案,因为我想让你知道这个框架的底层到底发生了什么。我希望这是您正在寻找的答案。

如果我的回答中有任何不清楚的地方,我愿意更详细地讨论我的回答。


推荐阅读