如何优化PHP和Laravel以提高Web应用的性能

这篇文章主要讲解了“如何优化PHP和Laravel以提高Web应用的性能”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“如何优化PHP和Laravel以提高Web应用的性能”吧!

四种类型的优化

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

  • 语言层面:这意味着你使用更快的语言版本,并避免语言中特定的功能/编码风格,使你的代码速度变慢。

  • 框架层面:这些是我们在本文中要涉及的内容。

  • 基础设施层面:调整你的 PHP 进程管理器、Web 服务器、数据库等。

  • 硬件层面:转向更好、更快、更强大的硬件主机提供商。

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

要注意 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, ...);

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

缓存配置!

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...
         */
        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 也会随着记录数量的增加而变慢

尽量使用缓存

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

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

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

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

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

优先选择内存缓存

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

理想情况下,你希望使用内存中(完全活在 RAM 中)的缓存,比如 Redis、Memcached、MongoDB 等,这样在较高的负载下,缓存就能起到至关重要的作用,而不是自己成为瓶颈。

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

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

缓存路由

就像应用程序的配置一样,路由不会随着时间的推移而改变,是缓存的理想选择。如果你像我一样无法忍受大文件,并且最终把你的 web.phpapi.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秒。一个非常糟糕的用户体验,我相信你会同意。

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

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

感谢各位的阅读,以上就是“如何优化PHP和Laravel以提高Web应用的性能”的内容了,经过本文的学习后,相信大家对如何优化PHP和Laravel以提高Web应用的性能这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是蜗牛博客,小编将为大家推送更多相关知识点的文章,欢迎关注!

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:niceseo99@gmail.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

评论

有免费节点资源,我们会通知你!加入纸飞机订阅群

×
天气预报查看日历分享网页手机扫码留言评论电报频道链接