PHP 7.4中的預載入(Opcache Preloading)

2020-07-16 10:06:11
在PHP 7.4中,新增了對預載入的支援,這是一個可以顯著提高程式碼效能的特性。

簡而言之,這是它的工作方式:

● 為了預載入檔案,您需要編寫一個自定義PHP指令碼

● 該指令碼在伺服器啟動時執行一次

● 所有預載入的檔案在記憶體中都可用於所有請求

● 在重新啟動伺服器之前,對預載入檔案所做的更改不會產生任何影響

讓我們深入了解它。

#Opcache

雖然預載入是建立在opcache之上的,但它並不是完全一樣的。Opcache將獲取您的PHP原始檔,將其編譯為「 opcodes」,然後將這些編譯後的檔案儲存在磁碟上。

您可以將操作碼看作是程式碼的底層表示,在執行時很容易解釋。因此,opcache會跳過原始檔和PHP直譯器在執行時實際需要之間的轉換步驟。巨大的勝利!

但我們還有更多的收穫。Opcached檔案不知道其他檔案。如果類a是從類B擴充套件而來的,那麼仍然需要在執行時將它們連結在一起。此外,opcache執行檢查以檢視原始檔是否被修改,並將基於此使其快取失效。

因此,這就是預載入發揮作用的地方:它不僅將原始檔編譯為操作碼,而且還將相關的類、特徵和介面連結在一起。然後,它將這個「已編譯」的可執行程式碼blob(即:PHP直譯器可以使用的程式碼)儲存在記憶體中。

現在,當請求到達伺服器時,它可以使用已經載入到記憶體中的部分程式碼庫,而不會產生任何開銷。

那麼,我們所說的「程式碼庫的一部分」是什麼呢?

#實踐中的預載入

為了進行預載入,開發人員必須告知伺服器要載入哪些檔案。這是用一個簡單的PHP指令碼完成的,確實沒有什麼困難。

規則很簡單:

● 您提供一個預載入指令碼,並使用opcache.preload命令將其連結到您的php.ini檔案中。

● 您要預載入的每個PHP檔案都應該傳遞到opcache_compile_file(),或者在預載入指令碼中只需要一次。

假設您想要預載入一個框架,例如Laravel。您的指令碼必須遍歷vendor/laravel目錄中的所有PHP檔案,並將它們一個接一個地新增。

在php.ini中連結到此指令碼的方法如下:

opcache.preload=/path/to/project/preload.php

這是一個虛擬的實現:

$files = /* An array of files you want to preload */;
foreach ($files as $file) {
    opcache_compile_file($file);
}

#警告:無法預載入未連結的類

等等,有一個警告!為了預載入檔案,還必須預載入它們的依賴項(介面,特徵和父類別)。

如果類依賴項有任何問題,則會在伺服器啟動時通知您:

Can't preload unlinked class 
IlluminateDatabaseQueryJoinClause: 
Unknown parent 
IlluminateDatabaseQueryBuilder

看,opcache_compile_file()將解析一個檔案,但不執行它。這意味著如果一個類有未預載入的依賴項,它本身也不能預載入。

這不是一個致命的問題,您的伺服器可以正常工作。但你不會得到所有你想要的預載入檔案。

幸運的是,還有一種確保連結檔案也被載入的方法:您可以使用require_once代替opcache_compile_file,讓已註冊的autoloader(可能是composer的)負責其餘的工作。

$files = /* All files in eg. vendor/laravel */;
foreach ($files as $file) {
    require_once($file);
}

還有一些需要注意的地方。例如,如果您試圖預載入Laravel,那麼框架中的一些類依賴於其他尚不存在的類。例如,檔案系統快取類 lighting filesystem cache依賴於LeagueFlysystemCachedStorageAbstractCache,如果您從未使用過檔案系統快取,則可能無法將其安裝到您的專案中。

嘗試預載入所有內容時,您可能會遇到「class not found」錯誤。幸運的是,在預設的Laravel安裝中,只有少數這些類,可以輕易忽略。為了方便起見,我編寫了一個小小的preloader類,以使忽略檔案更容易,如下所示:

class Preloader
{
    private array $ignores = [];
    private static int $count = 0;
    private array $paths;
    private array $fileMap;
    public function __construct(string ...$paths)
    {
        $this->paths = $paths;
        // We'll use composer's classmap
        // to easily find which classes to autoload,
        // based on their filename
        $classMap = require __DIR__ . '/vendor/composer/autoload_classmap.php';
        $this->fileMap = array_flip($classMap);
    }
    
    public function paths(string ...$paths): Preloader
    {
        $this->paths = array_merge(
            $this->paths,
            $paths
        );
        return $this;
    }
    public function ignore(string ...$names): Preloader
    {
        $this->ignores = array_merge(
            $this->ignores,
            $names
        );
        return $this;
    }
    public function load(): void
    {
        // We'll loop over all registered paths
        // and load them one by one
        foreach ($this->paths as $path) {
            $this->loadPath(rtrim($path, '/'));
        }
        $count = self::$count;
        echo "[Preloader] Preloaded {$count} classes" . PHP_EOL;
    }
    private function loadPath(string $path): void
    {
        // If the current path is a directory,
        // we'll load all files in it 
        if (is_dir($path)) {
            $this->loadDir($path);
            return;
        }
        // Otherwise we'll just load this one file
        $this->loadFile($path);
    }
    private function loadDir(string $path): void
    {
        $handle = opendir($path);
        // We'll loop over all files and directories
        // in the current path,
        // and load them one by one
        while ($file = readdir($handle)) {
            if (in_array($file, ['.', '..'])) {
                continue;
            }
            $this->loadPath("{$path}/{$file}");
        }
        closedir($handle);
    }
    private function loadFile(string $path): void
    {
        // We resolve the classname from composer's autoload mapping
        $class = $this->fileMap[$path] ?? null;
        // And use it to make sure the class shouldn't be ignored
        if ($this->shouldIgnore($class)) {
            return;
        }
        // Finally we require the path,
        // causing all its dependencies to be loaded as well
        require_once($path);
        self::$count++;
        echo "[Preloader] Preloaded `{$class}`" . PHP_EOL;
    }
    private function shouldIgnore(?string $name): bool
    {
        if ($name === null) {
            return true;
        }
        foreach ($this->ignores as $ignore) {
            if (strpos($name, $ignore) === 0) {
                return true;
            }
        }
        return false;
    }
}

通過在相同的預載入指令碼中新增此類,我們現在可以像這樣載入整個Laravel框架:

// …
(new Preloader())
    ->paths(__DIR__ . '/vendor/laravel')
    ->ignore(
        IlluminateFilesystemCache::class,
        IlluminateLogLogManager::class,
        IlluminateHttpTestingFile::class,
        IlluminateHttpUploadedFile::class,
        IlluminateSupportCarbon::class,
    )
    ->load();

#有效嗎?

這當然是最重要的問題:所有檔案都正確載入了嗎?您可以簡單地通過重新啟動伺服器來測試它,然後將opcache_get_status()的輸出轉儲到PHP指令碼中。您將看到它有一個名為preload_statistics的鍵,它將列出所有預載入的函數、類和指令碼;以及預載入檔案消耗的記憶體。

# Composer支援

一個很有前途的特性可能是基於composer的自動預載入解決方案,它已經被大多數現代PHP專案所使用。人們正在努力在composer.json中新增預載入設定選項,它將為您生成預載入檔案!目前,此功能仍在開發中,但您可以在此處關注。

#伺服器要求

在使用預載入時,關於devops方面還有兩件更重要的事情需要提及。

您已經知道,需要在php.ini中指定一個條目才能進行預載入。這意味著如果您使用共用主機,您將無法自由地設定PHP。實際上,您需要一個專用的(虛擬)伺服器,以便能夠為單個專案優化預載入的檔案。記住這一點。

還要記住,每次需要重新載入記憶體檔案時,都需要重新啟動伺服器(如果使用php-fpm就足夠了)。這對大多數人來說似乎是顯而易見的,但仍然值得一提。

#效能

現在到最重要的問題:預載入真的能提高效能嗎?

答案是肯定的:Ben Morel分享了一些基準測試,可以在之前連結的相同的composer問題中找到。

有趣的是,您可以決定僅預載入「hot classes」,它們是程式碼庫中經常使用的類。Ben的基準測試顯示,只載入大約100個熱門類,實際上可以獲得比預載入所有類更好的效能收益。這是效能提升13%和17%的區別。

當然,應該預載入哪些類取決於您的特定專案。明智的做法是在開始時盡可能多地預載入。如果您確實需要少量的百分比增長,您將不得不在執行時監視您的程式碼。

當然,所有這些工作都可以自動化,將來可能會實現。

現在,最重要的是要記住composer將新增支援,這樣您就不必自己製作預載入檔案,並且只要您完全控制了此功能,就可以在伺服器上輕鬆設定此功能。

翻譯:https://stitcher.io/blog/preloading-in-php-74

以上就是PHP 7.4中的預載入(Opcache Preloading)的詳細內容,更多請關注TW511.COM其它相關文章!