掌握提高 Web 應用的效能的方法 之 優化 PHP 和 Laravel

it阿布發表於2020-08-14

在這裡插入圖片描述

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

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

不管怎樣,不可否認的是,Laravel 已經振興了 PHP 生態系統。

在這裡插入圖片描述

對 Laravel 的評價節選

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

在這裡插入圖片描述

對於一個檢視中似乎是編譯錯誤的情況,有 18 個函式呼叫要跟蹤。我個人遇到過 40 個的,如果您使用其他庫和外掛,則可能會更多。

重點是,預設情況下,這樣層層巢狀的程式碼,使得 Laravel 速度很慢。

Laravel 有多慢?

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

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

其次,一個 Web 應用取決於很多東西(資料庫、檔案系統、網路、快取等),所以談論速度是很愚蠢的。一個非常快的 Web 應用,如果有一個非常慢的資料庫,那麼它就是一個非常慢的 Web 應用。

但這種不確定性正是基準測試受歡迎的原因。儘管它們毫無意義,但它們提供了一些 參考框架,幫助我們避免生氣。因此,最好有所保留,讓我們對 PHP 框架之間的速度有一個錯誤的、粗略的認識。

根據這個相當值得尊敬的 GitHub 原始碼,以下是 PHP 框架的對比情況。

在這裡插入圖片描述

你可能根本不會注意到 Laravel 在這裡 (即使你真的很努力地眯著眼睛), 除非你把你的目光投到最尾部。是的,親愛的朋友們,Laravel 排在最後! 現在,理所當然的,這些「框架」中的大多數都不是很實用,甚至沒有什麼用處,但它確實告訴我們,與其他更流行的框架相比,Laravel 是多麼的慢。

通常情況下,這種「慢」在應用中不會出現, 因為我們日常的 Web 應用很少達到很高的資料量。但是一旦達到了(比如高達 200-500 以上的併發量),伺服器就會開始阻塞而死。這時候即使扔再多的硬體也解決不了問題,基礎架構費用迅速攀升,你對雲端計算的崇高理想轟然倒塌。

在這裡插入圖片描述

不過,嘿嘿,振作起來吧! 這篇文章並不是講什麼不能做, 而是講什麼可以做。

好訊息是, 你可以做很多事情來讓你的 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, ...);

當然,一次查詢比多查詢一千次要好。想象一下,如果有一萬個客戶要處理,會發生什麼情況!或者說,如果我們還想顯示每個訂單中包含的專案,那簡直就是天方夜譚!記住,這個技術的名字叫預載入,它幾乎在任何時候都能派上用場。

快取配置!

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' 陣列鍵的一部分。讓我們來看看我的情況:

/*
    |--------------------------------------------------------------------------
    | 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 並將中介軟體粘在 web 或 api 堆疊中是很有誘惑力的;這樣一來,它就會在整個應用程式中變得可用,而且如果它沒有做一些侵入性的事情(例如,像日誌或通知)。

然而,隨著應用程式的增長,如果所有(或大多數)這些全域性中介軟體都存在於每個請求中,那麼這個全域性中介軟體的集合可能會成為應用程式的一個無聲負擔,即使沒有業務原因。

換句話說,要小心你在哪裡新增 / 應用新的中介軟體。在全域性範圍內新增一些東西可能會更方便,但從長遠來看,效能懲罰是非常高的。我知道如果每次有新的變化都要有選擇地應用中介軟體,你要承受的痛苦,但這是我心甘情願承受的痛苦,也是我所推薦的!

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

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

在這裡插入圖片描述

雖然佇列系統使設定複雜化了一些 (並增加了一些監控開銷), 但它在現代 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 個使用者。

如果你不確定是否需要優化你的應用,那你就不要去捅這個馬蜂窩。一個能正常運轉的應用,雖然有時感覺很無趣,但卻做了它必須做的事情,這比一個優化成突變體混合型超級機器卻時不時會失敗的應用要可取十倍。

以上內容希望幫助到大家,很多PHPer在進階的時候總會遇到一些問題和瓶頸,業務程式碼寫多了沒有方向感,不知道該從那裡入手去提升,對此我整理了一些資料,包括但不限於:分散式架構、高可擴充套件、高效能、高併發、伺服器效能調優、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql優化、shell指令碼、Docker、微服務、Nginx等多個知識點高階進階乾貨需要的可以免費分享給大家,需要的可以點選連結瞭解進階PHP月薪30k>>>架構師成長路線【視訊、面試文件免費獲取】

相關文章