如何为高性能优化PHP Laravel Web应用程序?

Laravel是很多东西。但它并不快速。让我们学习一些行业的技巧,让它变得更快!

如今,没有PHP开发人员能够不受到它的影响。他们要么是初级或中级开发人员,喜欢Laravel提供的快速开发,要么是高级开发人员,因为市场压力而被迫学习Laravel。

无论如何,不可否认的是,Laravel已经使PHP生态系统焕发了活力(如果没有Laravel,我肯定早就离开PHP世界了)。

Laravel对自己的某种合理自夸的片段

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

对于一个看起来是一个视图中的编译错误,有18个函数调用来进行跟踪。我个人遇到过40个,如果您使用其他库和插件,可能还会有更多。

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

Laravel有多慢?

老实说,由于几个原因,这个问题根本无法回答。

首先,对于衡量Web应用程序速度的标准,没有被广泛接受的、客观的、明智的标准。比什么更快还是更慢?在什么条件下?

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

但正是因为这种不确定性,基准测试才受欢迎。尽管它们毫无意义(参见this),但它们提供了一些参考框架,并帮助我们不至于发疯。因此,请准备好几大撮盐,让我们对PHP frameworks之间的速度有一个错误、粗略的概念。

根据这个相当可靠的GitHubsource,这是PHP框架与其他框架相比的排名:

你可能甚至都不会注意到Laravel在这里(即使你眯起眼睛看),除非你将你的案例一直投向尾部。是的,亲爱的朋友们,Laravel排在最后!现在,当然,这些“框架”中的大多数不是非常实用甚至有用,但它确实告诉我们当与其他更受欢迎的框架相比时,Laravel有多慢。

通常,这种“慢速”在应用程序中并不常见,因为我们的日常Web应用程序很少达到高数量。但一旦达到(比如,超过200-500并发),服务器就开始窒息和死亡。这是即使投入更多的硬件也无法解决问题的时候,基础设施账单飞涨得如此之快,以至于您对云计算的理想信念被摧毁。

但是,喂,振作起来!本文不是关于不能做的,而是关于可以做的。🙂

好消息是,您可以采取很多措施使您的Laravel应用程序变得更快。几倍更快。是的,不是开玩笑。您可以使同一代码库变得疯狂,并每月节省几百美元的基础设施/托管费用。如何?接下来就来看看吧。

四种优化类型

在我看来,优化可以在四个不同的级别上进行(至少在涉及PHP applications的情况下是这样):

  • 语言级别:这意味着您使用的是语言的更快版本,并避免使用使您的代码变慢的特定功能/编码风格。
  • 框架级别:这些是本文将涵盖的内容。
  • 基础设施级别:调整您的PHP进程管理器、Web服务器、数据库等。
  • 硬件级别:升级到更好、更快、更强大的硬件hosting provider

所有这些类型的优化都有其用途(例如,PHP-fpm optimization非常关键且强大)。但本文的重点将仅限于类型2的优化:与框架相关的优化。

顺便说一下,这些编号没有理由,也不是一种公认的标准。我只是凭空编的。请不要引用我并说,“我们的服务器需要类型3的优化”,否则你的团队领导会杀了你,然后找到我,再杀了我。😀

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

要注意n+1数据库查询

当使用ORM时,n+1查询问题是常见的问题。Laravel有一个强大的ORM,名为Eloquent,它非常漂亮、方便,我们经常忘记查看其中发生的情况。

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

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

class OrdersController extends Controller 
{
    // ... 

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

很棒!更重要的是,优雅、漂亮。🤩🤩

不幸的是,在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, . . .);

一次查询当然比一千次额外的查询好。想象一下如果有一万个客户要处理会发生什么!或者如果我们还想显示每个订单中的商品!记住,这种技术叫做急加载,几乎总是一个好主意。

缓存配置!

Laravel灵活性的一个原因是它的框架中有大量的配置文件。想要改变图片存储的方式/位置吗?

那就改变config/filesystems.php文件(至少在写作时是这样)。想要使用多个队列驱动程序?随意在config/queue.php中描述它们。我刚数了一下,发现框架有13个不同方面的配置文件,确保无论你想要改变什么,都不会令你失望。

考虑到PHP的特性,每当有一个新的Web请求到来,Laravel会启动,加载一切,并解析所有这些配置文件,以找出如何在这次执行中做出不同的事情。但是如果在过去的几天里什么都没有改变,那是很愚蠢的!在每个请求中重新构建配置是一种浪费,可以(实际上,必须)避免,而解决方法是Laravel提供的一个简单命令:

php artisan config:cache

这个命令将所有可用的配置文件合并成一个单一的文件,并在某个位置进行缓存以便快速检索。下次有Web请求时,Laravel将简单地读取这个单一文件并开始工作。

话虽如此,配置缓存是一项非常细微的操作,可能会爆炸。最大的陷阱是,一旦你发出这个命令,除了配置文件之外的任何地方的env()函数调用都将返回null

如果你仔细想一下,这是有道理的。如果你使用配置缓存,你告诉框架,“你知道吗,我认为我已经很好地设置了一切,我100%确定我不希望它们改变。”换句话说,你期望环境保持静态,这就是.env文件的用途。

话虽如此,以下是一些关于配置缓存的坚定、神圣、不可破坏的规则:

  1. 只在生产系统上进行。
  2. 只在你确实、确实确定要冻结配置时进行。
  3. 如果出现问题,请使用php artisan cache:clear撤销设置。
  4. 祈祷对业务造成的损害不要太大!

减少自动加载的服务

为了提供帮助,当Laravel启动时,它加载了大量的服务。这些服务在config/app.php文件的'providers'数组键中可用。让我们看看我这里有什么:

/*
|————————————————————————–
| 自动加载的服务提供者
|————————————————————————–
|
| 在请求到达您的应用程序时,这里列出的服务提供者将自动加载。
| 可以随意添加自己的服务到此数组,以扩展应用程序的功能。
|
*/

‘providers' => [

/*
* Laravel 框架的服务提供者…
*/
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,

/*
* 包的服务提供者…
*/

/*
* 应用程序的服务提供者…
*/
AppProvidersAppServiceProvider::class,
AppProvidersAuthServiceProvider::class,
// AppProvidersBroadcastServiceProvider::class,
AppProvidersEventServiceProvider::class,
AppProvidersRouteServiceProvider::class,

],

再次数了一下,有27个服务!也许你需要它们中的所有服务,但是这是不太可能的。

例如,我目前正在构建一个REST API,这意味着我不需要Session Service Provider、View Service Provider等等。而且由于我正在按照自己的方式进行一些操作,而不是按照框架的默认设置,我还可以禁用Auth Service Provider、Pagination Service Provider、Translation Service Provider等等。总的来说,对于我的用例来说,其中近一半都是不必要的。

仔细审视一下你的应用程序。它是否需要所有这些服务提供者?但是,天啊,请不要盲目地注释掉这些服务并推向生产环境!在触发之前,请运行所有测试,在开发和测试环境中手动检查,并要非常非常谨慎。🙂

明智地使用中间件堆栈

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

然而,随着应用程序的增长,如果每个请求中都存在所有(或大部分)全局中间件,即使没有业务上的原因,这个全局中间件集合也可能对应用程序造成沉默的负担。

换句话说,在添加/应用新的中间件时要小心。全局添加某些东西可能更方便,但长期来看性能惩罚非常高。我知道如果每次有新的更改都要选择性地应用中间件,你可能会感到痛苦,但这是我愿意接受和推荐的痛苦!

避免使用ORM(有时)

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

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

对于复杂的SQL查询,这一点尤其正确,你需要进行大量的跳跃和写闭包,最终得到一个高效的查询。在这种情况下,最好使用DB::raw()手动编写查询。

根据性能研究,即使对于简单的插入操作,随着记录数量的增加,Eloquent的速度也慢得多:

尽量使用缓存

网站应用程序优化中最好的保密技巧之一就是缓存。

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

例如,在电子商务商店中,可能会发现在200万个产品中,大多数时候人们对那些新上市的产品感兴趣,价格在一定范围内,并且适用于特定年龄段的人群。查询数据库以获取这些信息是浪费的 – 因为查询不经常更改,所以最好将这些结果存储在我们可以快速访问的地方。

Laravel内置支持多种类型的缓存。除了使用缓存驱动程序和从头开始构建缓存系统之外,你可能还想使用一些Laravel的包来促进缓存、标记缓存等。

但要注意,除了某些简化的用例之外,预建的缓存包可能会引起更多问题。

优先使用内存缓存

当在Laravel中缓存某些内容时,你有几个选项可以存储计算出的结果。这些选项也被称为缓存驱动程序。所以,虽然可以使用文件系统来存储缓存结果,但这并不是缓存的真正意义。

理想情况下,你应该使用基于内存的缓存(完全存储在RAM中),如Redis、Memcached、MongoDB等,这样在负载较高时,缓存才能发挥重要作用,而不会成为瓶颈。

现在,你可能认为拥有SSD硬盘与使用RAM条几乎相同,但实际上并不接近。即使非正式的性能研究也显示,当涉及速度时,RAM的性能比SSD快10-20倍。

在缓存方面,我的最爱是Redis。它的性能非常好(每秒100,000次读取操作很常见),对于非常大的缓存系统,它还可以轻松地发展成分布式缓存。

缓存路由

就像应用程序配置一样,路由在时间上不会发生太多变化,是缓存的理想候选对象。这尤其适用于像我这样无法忍受大文件而分散web.phpapi.php到多个文件中的情况。一个Laravel命令可以打包所有可用的路由,并将它们方便地保留以供将来访问:

php artisan route:cache

当你添加或更改路由时,只需执行以下操作:

php artisan route:clear

图像优化和CDN

图像是大多数Web应用程序的核心。巧合的是,它们也是带宽消耗最大的元素,也是应用程序/网站运行缓慢的最大原因之一。如果你只是简单地将上传的图像天真地存储在服务器上并在HTTP响应中发送它们,那么你将错失一个巨大的优化机会。

我的第一个建议是不要将图像存储在本地 – 这会带来数据丢失的问题,并且根据您的客户所在的地理区域,数据传输可能非常慢。

相反,选择一个像Cloudinary这样的解决方案,它可以自动调整大小和优化图像。

如果这不可能,可以使用类似Cloudflare的东西来缓存和提供存储在服务器上的图像。

如果甚至这也不可能,可以稍微调整一下您的Web服务器软件,压缩资源并指示访问者的浏览器缓存内容,这将产生很大的差异。这是一个Nginx配置片段的例子:

server {

   # 文件已删除
    
    # gzip压缩设置
    gzip on;
    gzip_comp_level 5;
    gzip_min_length 256;
    gzip_proxied any;
    gzip_vary on;

   # 浏览器缓存控制
   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

与队列结交

Queues是在有很多任务时,每个任务需要几毫秒才能完成时,处理任务的方法。一个很好的例子是发送电子邮件 – 在Web应用程序中,当用户执行某些操作时,通常会发送几封通知邮件。

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

解决方法是接收到任务后将其存储起来,告诉用户一切都进行得很顺利,并稍后(几秒钟)处理它们。如果出现错误,可以在宣布失败之前多次重试队列中的任务。

图片来源:Microsoft.com

虽然队列系统会使设置变得稍微复杂(并增加一些监控开销),但它在现代Web应用程序中是不可或缺的。

资产优化(Laravel Mix)

对于您的Laravel应用程序中的任何前端资产,请确保有一个流水线来编译和压缩所有资产文件。那些熟悉类似Webpack、Gulp、Parcel等捆绑程序的人不需要费心,但如果您尚未这样做,请使用Laravel Mix

混合(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时,它会自动处理导入、缩小、优化和整个过程。Mix不仅处理传统的JS和CSS文件,还处理您应用工作流中可能有的Vue和React组件。

更多信息here

结论

性能优化更多是一门艺术而非科学 – 知道如何以及何时进行优化比做什么重要。也就是说,在Laravel应用程序中,您可以进行无尽的优化。

但无论您做什么,我想给您一些建议 – 优化应该有一个充分的理由,而不是因为听起来不错,或者因为您对拥有超过10万用户的应用程序的性能感到担忧,而实际上只有10个用户。

如果您不确定是否需要优化您的应用程序,那就不需要踢这个谚语中的蜂窝。一个工作正常但感觉无聊却正好做到了它应该做到的应用程序,比一个被优化成突变体混合超级机器的应用程序更可取,尽管后者偶尔失败。

而且,想要成为Laravel大师的新手,请查看这个online course

愿您的应用程序运行得更快!:)

类似文章