優化 PHP 和 Laravel 以提高 Web 應用的性能

Laravel

轉載自 Laravel 論壇:https://learnku.com/laravel/t/47213

Laravel 有很多東西。但是快不是其中之一。讓我們學習一些優化技巧,以加快運行速度!

自從 Laravel 誕生以來,沒有一個 PHP 開發人員不受她的影響。他們是喜歡 Laravel 提供的快速開發的初級或中級開發人員,或者是由于市場壓力而被迫學習 Laravel 的高級開發人員。

不管怎樣,不可否認的是,Laravel 已經振興了 PHP 生態系統(我確定,如果沒有 Laravel,早就離開了 PHP 世界了)。

Laravel

對 Laravel 的評價節選

但是,由于 Laravel 竭盡全力讓您的事情變得簡單,這意味著它在底層做了大量工作,以確保您作為開發人員能有一個舒適的編程體驗。 Laravel 所有看似「神奇」的功能都有一層又一層的代碼,每當運行一個功能時都需要啟動這些代碼層。甚至是一個簡單的異常都會深究到底層 (從錯誤那里開始,一直到內核):

Laravel

對于一個視圖中似乎是編譯錯誤的情況,有 18 個函數調用要跟蹤。我個人遇到過 40 個的,如果您使用其他庫和插件,則可能會更多。

重點是,默認情況下,這樣層層嵌套的代碼,使得 Laravel 速度很慢。

Laravel 有多慢?

說實話,這個問題根本無法回答,原因有幾個。

首先,目前還沒有公認的、客觀的、合理的標準來衡量網絡應用的速度。與什么相比更快或更慢?在什么條件下?

第二,一個 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.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秒。一個非常糟糕的用戶體驗,我相信你會同意。

補救的辦法是在任務進來的時候就把它們存儲起來,告訴用戶一切都很順利,然后再處理它們(幾秒鐘)。如果出現錯誤,在宣布失敗之前,排隊的任務可以重試幾次。

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/47213

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。