优化 PHP 和 Laravel 以提升 Web 应用的性能

Laravel

转载自 Laravel 论坛: https://learnku.com/laravel/t...

Laravel 有不少东西。可是快不是其中之一。让咱们学习一些优化技巧,以加快运行速度!php

自从 Laravel 诞生以来,没有一个 PHP 开发人员不受她的影响。他们是喜欢 Laravel 提供的快速开发的初级或中级开发人员,或者是因为市场压力而被迫学习 Laravel 的高级开发人员。css

无论怎样,不能否认的是,Laravel 已经振兴了 PHP 生态系统(我肯定,若是没有 Laravel,早就离开了 PHP 世界了)。前端

Laravel

对 Laravel 的评价节选laravel

可是,因为 Laravel 不遗余力让您的事情变得简单,这意味着它在底层作了大量工做,以确保您做为开发人员能有一个温馨的编程体验。 Laravel 全部看似「神奇」的功能都有一层又一层的代码,每当运行一个功能时都须要启动这些代码层。甚至是一个简单的异常都会深究到底层 (从错误那里开始,一直到内核):git

Laravel

对于一个视图中彷佛是编译错误的状况,有 18 个函数调用要跟踪。我我的遇到过 40 个的,若是您使用其余库和插件,则可能会更多。github

重点是,默认状况下,这样层层嵌套的代码,使得 Laravel 速度很慢。web

Laravel 有多慢?redis

说实话,这个问题根本没法回答,缘由有几个。数据库

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

第二,一个 Web 应用取决于不少东西(数据库、文件系统、网络、缓存等),因此谈论速度是很愚蠢的。一个很是快的 Web 应用,若是有一个很是慢的数据库,那么它就是一个很是慢的 Web 应用。

但这种不肯定性正是基准测试受欢迎的缘由。尽管它们毫无心义(参见 这里这里),但它们提供了一些 参考框架,帮助咱们避免生气。所以,最好有所保留,让咱们对 PHP 框架之间的速度有一个错误的、粗略的认识。

根据这个至关值得尊敬的 GitHub 源码,如下是 PHP 框架的对比状况。

Laravel

你可能根本不会注意到 Laravel 在这里 (即便你真的很努力地眯着眼睛), 除非你把你的目光投到最尾部。是的,亲爱的朋友们,Laravel 排在最后! 如今,理所固然的,这些「框架」中的大多数都不是很实用,甚至没有什么用处,但它确实告诉咱们,与其余更流行的框架相比,Laravel 是多么的慢。

一般状况下,这种「慢」在应用中不会出现, 由于咱们平常的 Web 应用不多达到很高的数据量。可是一旦达到了(好比高达 200-500 以上的并发量),服务器就会开始阻塞而死。这时候即便扔再多的硬件也解决不了问题,基础架构费用迅速攀升,你对云计算的崇高理想轰然倒塌。

Laravel

不过,嘿嘿,振做起来吧! 这篇文章并非讲什么不能作, 而是讲什么能够作。

好消息是, 你能够作不少事情来让你的 Laravel 应用更快。几倍的速度。 是的,不是开玩笑。你可让一样的代码库变得快速,每月节省几百美圆的基础设施/托管费用。 怎么作?让咱们开始吧。

四种类型的优化

在我看来,优化能够在四个不一样的层面上进行(当涉及到PHP应用时,就是):

  1. 语言层面:这意味着你使用更快的语言版本,并避免语言中特定的功能/编码风格,使你的代码速度变慢。
  2. 框架层面:这些是咱们在本文中要涉及的内容。
  3. 基础设施层面:调整你的 PHP 进程管理器、Web 服务器、数据库等。
  4. 硬件层面:转向更好、更快、更强大的硬件主机提供商。

全部这些类型的优化都有其存在的意义(例如,php-fpm 的优化是很是关键和强大的)。但本文的重点是纯粹的第 2 类优化:那些与框架相关的优化。

顺便说一下,这些编号背后没有任何理由,也不是一个公认的标准。我只是编了这些。请千万不要引用个人话说:「咱们的服务器须要 type-3 优化」,不然你的团队负责人会杀了你,找到我,而后把我也杀了。

如今,咱们终于到了应许之地。

要注意 n+1 数据库查询

n+1 查询问题是使用 ORM 时常见的问题。Laravel 有其强大的 ORM,叫 Eloquent,它是如此的漂亮,如此的方便,以致于咱们经常忘记了看是怎么回事。

考虑一个很是常见的场景:显示指定客户列表下的全部订单。这在电子商务系统和任何须要显示与某些实体相关的全部实体的列表中很是常见,

咱们能够想象有这样一个控制器:

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]);
    }
}

太好了!更重要的是,优雅,美丽。🤩🤩

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

ps:我以为原做者理解有误,预查询使用的where in,产生的语句应该是这样:

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

而后在循环插入到对应的对象中。

固然,一次查询比多查询一千次要好。想象一下,若是有一万个客户要处理,会发生什么状况!或者说,若是咱们还想显示每一个订单中包含的项目,那简直就是天方夜谭!记住,这个技术的名字叫预加载,它几乎在任什么时候候都能派上用场。

缓存配置!

Laravel 的灵活性的缘由之一是它有大量的配置文件, 这些文件是框架的一部分。想要改变图片的存储方式/位置?

好吧,只要修改 config/filesystems.php 文件就能够了(至少写到这里)。想要使用多个队列驱动?能够在 config/queue.php 中随意描述。我刚刚统计了一下,发现针对框架的不一样方面有 13 个配置文件,保证你不管想改什么都不会失望。

Laravel

鉴于 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' 数组键的一部分。让咱们来看看个人状况:

/*
    |--------------------------------------------------------------------------
    | 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...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
        Illuminate\Bus\BusServiceProvider::class,
        Illuminate\Cache\CacheServiceProvider::class,
        Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
        Illuminate\Cookie\CookieServiceProvider::class,
        Illuminate\Database\DatabaseServiceProvider::class,
        Illuminate\Encryption\EncryptionServiceProvider::class,
        Illuminate\Filesystem\FilesystemServiceProvider::class,
        Illuminate\Foundation\Providers\FoundationServiceProvider::class,
        Illuminate\Hashing\HashServiceProvider::class,
        Illuminate\Mail\MailServiceProvider::class,
        Illuminate\Notifications\NotificationServiceProvider::class,
        Illuminate\Pagination\PaginationServiceProvider::class,
        Illuminate\Pipeline\PipelineServiceProvider::class,
        Illuminate\Queue\QueueServiceProvider::class,
        Illuminate\Redis\RedisServiceProvider::class,
        Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
        Illuminate\Session\SessionServiceProvider::class,
        Illuminate\Translation\TranslationServiceProvider::class,
        Illuminate\Validation\ValidationServiceProvider::class,
        Illuminate\View\ViewServiceProvider::class,

        /*
         * Package Service Providers...
         */

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::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 让 DB 交互的不少方面变得愉悦,但它是以速度为代价的。做为一个映射器,ORM 不只要从数据库中获取记录,还要实例化模型对象,并用列数据对其进行填充。

因此,若是你作一个简单的 $users = User::all(),好比有10000个用户,框架会从数据库中获取 10000 行记录,并在内部作 10000 个 new User(),并用相关数据填充他们的属性。这是大量的工做在幕后进行,若是数据库是你的应用成为瓶颈的地方,绕过 ORM 有时是个好主意。

这对于复杂的 SQL 查询来讲尤为如此,在这种状况下,你必须跳不少的圈子,写一个又一个的闭包,但最终仍是能获得一个高效的查询。在这种状况下,最好作一个 DB::raw(),而后手工写查询。

根据 这个 的性能研究, 即便是简单的插入, Eloquent 也会随着记录数量的增长而变慢:

Laravel

尽可能使用缓存

Web 应用优化中最保守的秘密之一就是缓存。

对于新手来讲,缓存的意思是预先计算和存储昂贵的结果(昂贵的 CPU 和内存使用量),并在重复相同的查询时简单地返回。

例如,在一个电商商店里,可能会遇到,在 200 万种产品中,大多数时候人们都会对那些新鲜出炉的、在必定价格范围内的、针对特定年龄段的产品感兴趣。在数据库中查询这些信息是很浪费的——由于查询的内容不会常常变化,因此最好把这些结果存储在咱们能够快速访问的地方。

Laravel 内置支持多种类型的缓存。除了使用缓存驱动和从底层构建缓存系统外,你可能还想使用一些Laravel 包,方便模型缓存查询缓存等。

可是请注意, 在必定的简化用例以外, 预制的缓存包可能会带来更多的问题, 而不是解决这些问题.

优先选择内存缓存

当你在 Laravel 中缓存一些东西时, 你有几个选项能够选择将须要缓存的计算结果存储在哪里。这些选项也被称为 缓存驱动。因此, 虽然使用文件系统来存储缓存结果是可能的,也是彻底合理的,但这并非缓存的真正目的。

理想状况下,你但愿使用内存中(彻底活在 RAM 中)的缓存,好比 Redis、Memcached、MongoDB 等,这样在较高的负载下,缓存就能起到相当重要的做用,而不是本身成为瓶颈。

如今,你可能会认为拥有 SSD 磁盘和使用 RAM 棒几乎是同样的,但还差得远。即便是非正式的 基准测试也显示,在速度方面,RAM优于SSD的10-20倍。

在缓存方面,我最喜欢的系统是 Redis。它的速度 快得离谱(每秒 10 万次读取操做是很常见的),对于很是大的缓存系统,能够很容易地演变成一个 集群

缓存路由

就像应用程序的配置同样,路由不会随着时间的推移而改变,是缓存的理想选择。若是你像我同样没法忍受大文件,而且最终把你的 web.php api.php 分割成几个文件的话,这一点尤为适用。 一个简单的Laravel命令就能够把全部可用的路由打包并保存起来, 方便之后的访问:

php artisan route:cache

而当你最终要增长或改变路由时,只需这样作便可。

php artisan route:clear

图像优化和 CDN

图片是大多数网络应用的核心和灵魂。巧合的是,它们也是最大的带宽消耗者,也是致使应用程序/网站速度慢的最大缘由之一。若是你只是简单地将上传的图片天真地存储在服务器上,而后以 HTTP 响应的方式发送回来,你就会让一个巨大的优化机会溜走。

个人第一个建议是不要在本地存储图片——有数据丢失的问题要处理,并且取决于你的客户在哪一个地理区域,数据传输可能会很是缓慢。

相反,选择像 Cloudinary 这样的解决方案,它能够自动动态调整和优化图像的大小。

若是这不可能,使用相似 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

与队列交朋友

队列 是指当有不少事情时,你如何处理这些事情,并且每件事情都须要几毫秒才能完成。一个很好的例子是发送电子邮件——在网络应用中,一个普遍的用例是当用户执行一些操做时,发出几封通知邮件。

例如,在一个新推出的产品中,你可能但愿每当有人下单超过必定值时,公司领导层(大约6-7个电子邮件地址)就会收到通知。假设你的邮件网关能在500ms内响应你的SMTP请求,那么在订单确认启动以前,用户须要等待3-4秒。一个很是糟糕的用户体验,我相信你会赞成。

补救的办法是在任务进来的时候就把它们存储起来,告诉用户一切都很顺利,而后再处理它们(几秒钟)。若是出现错误,在宣布失败以前,排队的任务能够重试几回。

Laravel

虽然队列系统使设置复杂化了一些 (并增长了一些监控开销), 但它在现代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');

mix.js('resources/js/app.js', 'public/js')
    .sass('resources/sass/app.scss', 'public/css');

当您准备部署生产环境并运行 npm run production 时,它将自动处理导入,最小化,优化以及整个工做流程。 Mix 不只关心传统的 JS和 CSS 文件,并且还关心您在应用程序工做流程中可能使用的 Vue 和 React 组件。

更多信息参考 这里!

结论

性能优化与其说是科学,不如说是艺术 —— 知道如何作以及作多少比作什么更重要。也就是说,在 Laravel 应用中能够优化的内容和数量是无限的。

但不管您作什么,我都但愿留给您一些临别的建议 —— 优化应该在有充分的理由时进行,而不是由于它听起来不错,也不是由于您对 超过 100,000 个用户的应用程序的性能抱有偏执,而实际上只有 10 个用户。

若是你不肯定是否须要优化你的应用,那你就不要去捅这个马蜂窝。一个能正常运转的应用,虽然有时感受很无趣,但却作了它必须作的事情,这比一个优化成突变体混合型超级机器却时不时会失败的应用要可取十倍。

讨论请前往专业的 Laravel 论坛: https://learnku.com/laravel/t...
相关文章
相关标签/搜索