優化 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...
相關文章
相關標籤/搜索