簡而言之,這是它的工作方式:
● 為了預載入檔案,您需要編寫一個自定義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其它相關文章!