如何优化 PHP Laravel Web 应用程序以获得高性能?

Laravel 有很多东西。 但快速不是其中之一。 让我们学习一些交易技巧,让它走得更快!

没有 PHP 开发人员不受影响 拉维 这些日子。 他们要么是喜欢 Laravel 提供的快速开发的初级或中级开发人员,要么是由于市场压力被迫学习 Laravel 的高级开发人员。

无论哪种方式,不可否认的是 Laravel 重振了 PHP 生态系统(如果没有 Laravel,我肯定早就离开 PHP 世界了)。

来自 Laravel 的一段(有点道理的)自我表扬

然而,由于 Laravel 竭尽全力让事情变得简单,这意味着在它的背后正在做大量的工作以确保您作为开发人员过上舒适的生活。 Laravel 的所有“神奇”功能似乎都可以正常工作,但每次运行功能时都需要编写一层又一层的代码。 即使是一个简单的 Exception 跟踪兔子洞有多深(注意错误从哪里开始,一直到主内核):

对于其中一个视图中似乎是编译错误的内容,有 18 个函数调用需要跟踪。 我个人遇到过 40 个,如果您使用其他库和插件,很容易就会有更多。

重点是,默认情况下,这种层层叠叠的代码会使 Laravel 变慢。

Laravel 有多慢?

老实说,由于多种原因,回答这个问题显然是不可能的。

首先,没有公认的、客观的、合理的标准来衡量网络应用程序的速度。 与什么相比更快或更慢? 在什么条件下?

其次,Web 应用程序依赖于很多东西(数据库、文件系统、网络、缓存等),因此谈论速度是很愚蠢的。 具有非常慢的数据库的非常快的 Web 应用程序是非常慢的 Web 应用程序。 🙂

但这种不确定性正是基准受欢迎的原因。 尽管它们毫无意义(见 这个这个),它们提供了一些参考框架,帮助我们避免发疯。 因此,准备好几撮盐后,让我们对 PHP 框架中的速度有一个错误的、粗略的认识。

通过这个相当受人尊敬的 GitHub 资源下面是 PHP 框架在比较时的排列方式:

你可能甚至没有注意到这里的 Laravel(即使你眯着眼睛看),除非你把你的案例直接扔到尾巴的末端。 是的,亲爱的朋友们,Laravel 排在最后! 现在,当然,这些“框架”中的大多数都不是很实用,甚至不是很有用,但它确实告诉我们,与其他更流行的框架相比,Laravel 是多么的迟钝。

通常,这种“缓慢”不会出现在应用程序中,因为我们日常的 Web 应用程序很少会达到很高的数量。 但是一旦他们这样做了(比如说,超过 200-500 个并发),服务器就会开始窒息和死亡。 现在是时候,即使投入更多硬件解决问题也无济于事,基础设施费用攀升如此之快,以至于您对云计算的崇高理想破灭了。

但是,嘿,振作起来! 这篇文章不是关于什么不能做,而是关于什么可以做。 🙂

好消息是,你可以做很多事情来让你的 Laravel 应用运行得更快。 快了好几倍。 是的,不是开玩笑。 您可以使相同的代码库运行起来,并每月节省数百美元的基础设施/托管费用。 如何? 让我们开始吧。

四种类型的优化

在我看来,可以在四个不同的层面上进行优化(对于 PHP 应用程序,即):

  • 语言级别:这意味着您使用该语言的更快版本,并避免使用使您的代码变慢的语言的特定功能/编码风格。
  • 框架级别:这些是我们将在本文中介绍的内容。
  • 基础架构级别:调整您的 PHP 进程管理器、Web 服务器、数据库等。
  • 硬件级:转向更好、更快、更强大的硬件托管提供商。

所有这些类型的优化都有它们的位置(例如,PHP-fpm 优化非常关键和强大)。 但本文的重点将是纯粹类型 2 的优化:与框架相关的优化。

顺便说一句,编号背后没有任何理由,也不是公认的标准。 我只是编造了这些。 请永远不要引用我的话说,“我们需要在我们的服务器上进行 3 类优化”,否则你的团队领导会杀了你,找到我,然后再杀了我。 😀

现在,我们终于到达了应许之地。

注意 n+1 数据库查询

n+1 查询问题是使用 ORM 时的常见问题。 Laravel 有其强大的 ORM,称为 Eloquent,它非常漂亮,非常方便,以至于我们常常忘记查看发生了什么。

考虑一个非常常见的场景:显示给定客户列表下的所有订单的列表。 这在电子商务系统和任何我们需要显示与某些实体相关的所有实体的一般报告界面中很常见。

在 Laravel 中,我们可以想象一个控制器函数来完成这样的工作:

class OrdersController extends Controller 
{
    // ... 

    public function getAllByCustomers(Request $request, array $ids) {
        $customers = Customer::findMany($ids);        
        $orders = collect(); // new collection
        
        foreach ($customers as $customer) {
            $orders = $orders->merge($customer->orders);
        }
        
        return view('admin.reports.orders', ['orders' => $orders]);
    }
}

甜的! 更重要的是,优雅、美丽。 🤗🤗

  通过强大的优化让您的 Mac 再次焕然一新 – CleanMyMac

不幸的是,这是在 Laravel 中编写代码的灾难性方式。

这就是为什么。

当我们要求 ORM 寻找给定的客户时,会生成一个像这样的 SQL 查询:

SELECT * FROM customers WHERE id IN (22, 45, 34, . . .);

这完全符合预期。 结果,所有返回的行都存储在控制器函数内的集合 $customers 中。

现在我们逐一遍历每个客户并获取他们的订单。 这将执行以下查询。 . .

SELECT * FROM orders WHERE customer_id = 22;

. . . 有多少客户就有多少次。

换句话说,如果我们需要获取 1000 个客户的订单数据,则执行的数据库查询总数将为 1(用于获取所有客户的数据)+ 1000(用于获取每个客户的订单数据)= 1001。这是名称 n+1 的来源。

我们能做得更好吗? 当然! 通过使用所谓的预先加载,我们可以强制 ORM 执行 JOIN 并在单个查询中返回所有需要的数据! 像这样:

$orders = Customer::findMany($ids)->with('orders')->get();

生成的数据结构当然是嵌套的,但是可以轻松提取订单数据。 在这种情况下,生成的单个查询是这样的:

SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id WHERE customers.id IN (22, 45, . . .);

当然,单个查询优于一千个额外查询。 想象一下,如果有 10,000 个客户需要处理,会发生什么情况! 或者如果我们还想展示每个订单中包含的项目,上帝保佑! 请记住,该技术的名称是预加载,它几乎总是一个好主意。

缓存配置!

Laravel 的灵活性的原因之一是作为框架一部分的大量配置文件。 想要更改图像的存储方式/位置?

好吧,只需更改 config/filesystems.php 文件(至少在撰写本文时)。 想要使用多个队列驱动程序? 请随意在 config/queue.php 中描述它们。 刚数了一下,框架的不同方面一共有13个配置文件,保证你改什么都不会失望。

考虑到 PHP 的性质,每次有新的 Web 请求进入时,Laravel 都会唤醒,启动所有内容,并解析所有这些配置文件,以弄清楚这次如何做不同的事情。 除了最近几天没有任何变化是愚蠢的! 在每个请求上重建配置是一种可以(实际上,必须)避免的浪费,解决方法是 Laravel 提供的一个简单命令:

php artisan config:cache

这样做是将所有可用的配置文件组合成一个文件,缓存在某个地方以便快速检索。 下次有 Web 请求时,Laravel 将简单地读取这个文件并开始。

也就是说,配置缓存是一项极其微妙的操作,可能会在您面前爆炸。 最大的陷阱是,一旦你发出了这个命令,除了配置文件之外的任何地方的 env() 函数调用都将返回 null!

当您考虑它时,它确实有意义。 如果你使用配置缓存,你就是在告诉框架,“你知道吗,我认为我已经很好地设置了一切,我 100% 确定我不希望它们发生变化。” 换句话说,您希望环境保持静态,这就是 .env 文件的用途。

话虽如此,这里有一些铁定的、神圣的、牢不可破的配置缓存规则:

  • 仅在生产系统上执行此操作。
  • 仅当您真的非常确定要冻结配置时才这样做。
  • 如果出现问题,请使用 php artisan cache:clear 撤消设置
  • 祈祷对企业造成的损失不大!
  • 减少自动加载的服务

    为了提供帮助,Laravel 在唤醒时会加载大量服务。 这些在 config/app.php 文件中作为 ‘providers’ 数组键的一部分可用。 让我们看看我的情况:

    /*
        |--------------------------------------------------------------------------
        | Autoloaded Service Providers
        |--------------------------------------------------------------------------
        |
        | The service providers listed here will be automatically loaded on the
        | request to your application. Feel free to add your own services to
        | this array to grant expanded functionality to your applications.
        |
        */
    
        'providers' => [
    
            /*
             * Laravel Framework Service Providers...
             */
            IlluminateAuthAuthServiceProvider::class,
            IlluminateBroadcastingBroadcastServiceProvider::class,
            IlluminateBusBusServiceProvider::class,
            IlluminateCacheCacheServiceProvider::class,
            IlluminateFoundationProvidersConsoleSupportServiceProvider::class,
            IlluminateCookieCookieServiceProvider::class,
            IlluminateDatabaseDatabaseServiceProvider::class,
            IlluminateEncryptionEncryptionServiceProvider::class,
            IlluminateFilesystemFilesystemServiceProvider::class,
            IlluminateFoundationProvidersFoundationServiceProvider::class,
            IlluminateHashingHashServiceProvider::class,
            IlluminateMailMailServiceProvider::class,
            IlluminateNotificationsNotificationServiceProvider::class,
            IlluminatePaginationPaginationServiceProvider::class,
            IlluminatePipelinePipelineServiceProvider::class,
            IlluminateQueueQueueServiceProvider::class,
            IlluminateRedisRedisServiceProvider::class,
            IlluminateAuthPasswordsPasswordResetServiceProvider::class,
            IlluminateSessionSessionServiceProvider::class,
            IlluminateTranslationTranslationServiceProvider::class,
            IlluminateValidationValidationServiceProvider::class,
            IlluminateViewViewServiceProvider::class,
    
            /*
             * Package Service Providers...
             */
    
            /*
             * Application Service Providers...
             */
            AppProvidersAppServiceProvider::class,
            AppProvidersAuthServiceProvider::class,
            // AppProvidersBroadcastServiceProvider::class,
            AppProvidersEventServiceProvider::class,
            AppProvidersRouteServiceProvider::class,
    
        ],

    我又数了一下,列出了27个服务! 现在,您可能需要所有这些,但这不太可能。

      使用 Monday.com 有效管理您的业务任务

    例如,我目前正在构建一个 REST API,这意味着我不需要会话服务提供者、视图服务提供者等。而且因为我按照自己的方式做一些事情,而不是遵循框架默认设置,我还可以禁用 Auth Service Provider、Pagination Service Provider、Translation Service Provider 等。 总而言之,其中几乎一半对于我的用例来说是不必要的。

    仔细审视您的申请。 它需要所有这些服务提供商吗? 但是看在上帝的份上,请不要盲目地注释掉这些服务并推向生产! 运行所有测试,在开发和登台机器上手动检查,并在你扣动扳机之前非常非常偏执。 🙂

    明智地使用中间件堆栈

    当您需要对传入的 Web 请求进行一些自定义处理时,创建一个新的中间件就是答案。 现在,打开 app/Http/Kernel.php 并将中间件粘贴到 web 或 api 堆栈中是很诱人的; 这样,它就可以在整个应用程序中使用,如果它没有做一些侵入性的事情(例如日志记录或通知)。

    然而,随着应用程序的增长,如果所有(或大部分)这些都出现在每个请求中,即使没有商业原因,这些全局中间件的集合也会成为应用程序的无声负担。

    换句话说,请注意添加/应用新中间件的位置。 全局添加一些东西会更方便,但从长远来看性能损失非常高。 我知道如果每次有新变化时您都要有选择地应用中间件,您将不得不承受这种痛苦,但我愿意接受并推荐这种痛苦!

    避免使用 ORM(有时)

    虽然 Eloquent 使数据库交互的许多方面变得愉快,但它是以速度为代价的。 作为一个映射器,ORM 不仅需要从数据库中获取记录,还需要实例化模型对象并用列数据混合(填充)它们。

    因此,如果您执行一个简单的 $users = User::all() 并且有 10,000 个用户,则该框架将从数据库中获取 10,000 行并在内部执行 10,000 个 new User() 并使用相关数据填充它们的属性. 这是在幕后完成的大量工作,如果数据库成为应用程序的瓶颈,绕过 ORM 有时是个好主意。

    对于复杂的 SQL 查询尤其如此,在这种情况下,您必须跳很多圈并在闭包上编写闭包,但仍然会得到一个高效的查询。 在这种情况下,首选执行 DB::raw() 并手动编写查询。

    通过去 这个 性能研究,即使是简单的插入,随着记录数量的增加,Eloquent 也会变慢:

    尽可能使用缓存

    Web 应用程序优化的最佳秘密之一是缓存。

    对于初学者来说,缓存意味着预先计算和存储昂贵的结果(在 CPU 和内存使用方面昂贵),并在重复相同查询时简单地返回它们。

    例如,在电商中,可能会遇到200万种产品,大多数时候人们对刚进货、价格在一定范围内、适合特定年龄段的产品感兴趣。 查询数据库以获取此信息是一种浪费——因为查询不经常更改,所以最好将这些结果存储在我们可以快速访问的地方。

    Laravel 内置了多种类型的支持 缓存. 除了使用缓存驱动程序和从头开始构建缓存系统之外,您可能还想使用一些 Laravel 包来促进 模型缓存, 查询缓存, ETC。

    但请注意,除了某些简化的用例之外,预构建的缓存包可能会导致比它们解决的问题更多的问题。

    首选内存缓存

    当你在 Laravel 中缓存某些东西时,你有多种选择来存储需要缓存的计算结果。 这些选项也称为 缓存驱动程序. 因此,虽然使用文件系统来存储缓存结果是可能且完全合理的,但这并不是缓存的真正含义。

    理想情况下,您希望使用内存中(完全存在于 RAM 中)缓存,如 Redis、Memcached、MongoDB 等,以便在更高负载下,缓存发挥重要作用,而不是成为瓶颈本身。

    现在,您可能认为拥有 SSD 磁盘与使用 RAM 棒几乎相同,但两者相差甚远。 甚至是非正式的 基准 显示 RAM 在速度方面优于 SSD 10-20 倍。

      修复 Apple TV Remote 无法正常工作

    在缓存方面,我最喜欢的系统是 Redis。 它是 快得离谱 (每秒 100,000 次读取操作很常见),对于非常大的缓存系统,可以演化为 容易地。

    缓存路由

    就像应用程序配置一样,路由不会随时间变化太多,是缓存的理想选择。 如果您不能像我一样忍受大文件并最终将 web.php 和 api.php 拆分为多个文件,则尤其如此。 一个 Laravel 命令打包所有可用的路由,并将它们保存在方便的地方以供将来访问:

    php artisan route:cache

    当您最终添加或更改路线时,只需执行以下操作:

    php artisan route:clear

    图片优化和CDN

    图像是大多数 Web 应用程序的核心和灵魂。 巧合的是,他们也是最大的带宽消费者,也是应用程序/网站运行缓慢的最大原因之一。 如果您简单地将上传的图像天真地存储在服务器上并在 HTTP 响应中将它们发回,那么您就错过了一个巨大的优化机会。

    我的第一个建议是不要在本地存储图像——存在数据丢失的问题需要处理,而且根据您的客户所在的地理区域,数据传输可能会非常缓慢。

    相反,寻求像这样的解决方案 多云 即时自动调整图像大小和优化图像。

    如果那不可能,请使用 Cloudflare 之类的东西来缓存和提供图像,同时它们存储在您的服务器上。

    即使这不可能,稍微调整您的网络服务器软件以压缩资产并引导访问者的浏览器缓存内容,也会产生很大的不同。 Nginx 配置的片段如下所示:

    server {
    
       # file truncated
        
        # gzip compression settings
        gzip on;
        gzip_comp_level 5;
        gzip_min_length 256;
        gzip_proxied any;
        gzip_vary on;
    
       # browser cache control
       location ~* .(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ {
             expires 1d;
             access_log off;
             add_header Pragma public;
             add_header Cache-Control "public, max-age=86400";
        }
    }

    我知道图像优化与 Laravel 无关,但它是一个如此简单而强大的技巧(而且经常被忽视),我无法帮助自己。

    自动加载器优化

    自动加载是 PHP 中一个简洁的、不那么古老的特性,可以说它使该语言免于厄运。 也就是说,通过解密给定的命名空间字符串来查找和加载相关类的过程需要时间,并且可以在需要高性能的生产部署中避免。 再一次,Laravel 对此有一个单命令解决方案:

    composer install --optimize-autoloader --no-dev

    与队列交朋友

    队列 是当有很多事情时你如何处理事情,并且每个事情都需要几毫秒才能完成。 一个很好的例子是发送电子邮件——Web 应用程序中的一个广泛用例是在用户执行某些操作时发送一些通知电子邮件。

    例如,在新推出的产品中,您可能希望在有人下的订单超过特定值时通知公司领导(大约 6-7 个电子邮件地址)。 假设您的电子邮件网关可以在 500 毫秒内响应您的 SMTP 请求,我们说的是在订单确认开始之前等待用户 3-4 秒。一个非常糟糕的用户体验,我相信你会同意。

    补救措施是在作业进入时存储作业,告诉用户一切顺利,然后(几秒钟)处理它们。 如果出现错误,排队的作业可以在宣布失败之前重试几次。

    致谢:Microsoft.com

    虽然排队系统使设置变得有点复杂(并增加了一些监控开销),但它在现代 Web 应用程序中是必不可少的。

    资产优化(Laravel Mix)

    对于 Laravel 应用程序中的任何前端资产,请确保有一个管道可以编译和缩小所有资产文件。 那些熟悉 Webpack、Gulp、Parcel 等打包系统的人不需要费心,但如果你还没有这样做, 混合 是一个可靠的建议。

    Mix 是一个轻量级(老实说,令人愉快!)的 Webpack 包装器,它负责处理所有用于生产的 CSS、SASS、JS 等文件。 一个典型的 .mix.js 文件可以像这样小,但仍然可以创造奇迹:

    const mix = require('laravel-mix');
    
    mix.js('resources/js/app.js', 'public/js')
        .sass('resources/sass/app.scss', 'public/css');

    当您准备好生产并运行 npm run production 时,这会自动处理导入、缩小、优化和整个 shebang。 Mix 不仅处理传统的 JS 和 CSS 文件,还处理应用程序工作流中可能包含的 Vue 和 React 组件。

    更多信息 这里

    结论

    性能优化与其说是科学,不如说是一门艺术——知道如何做以及做多少比做什么更重要。 也就是说,您可以在 Laravel 应用程序中优化多少以及可以优化什么。

    但无论你做什么,我都想给你一些临别建议——优化应该在有充分理由的情况下进行,而不是因为它听起来不错,或者因为你对 100,000 多个用户的应用程序性能感到偏执,而实际上只有10个。

    如果您不确定是否需要优化您的应用程序,那么您不需要去踢马蜂窝。 一个感觉无聊但确实可以正常工作的应用程序比一个已经优化为突变混合超级机器但时不时地表现平平的应用程序更令人满意十倍。

    而且,要让新手成为 Laravel 大师,请查看这个 在线课程.

    愿您的应用运行得更快、更快! 🙂