新聞中心
作為PHP開發(fā)人員,我們并不經(jīng)常需要擔(dān)心內(nèi)存管理。PHP 引擎在我們背后做了很好的清理工作,短期執(zhí)行上下文的 Web 服務(wù)器模型意味著即使是最潦草的代碼也不會(huì)造成持久的影響。

很少情況下我們可能需要走出這個(gè)舒適的地方 —— 比如當(dāng)我們試圖在一個(gè)大型項(xiàng)目上運(yùn)行 Composer 來創(chuàng)建我們可以創(chuàng)建的最小的 VPS 時(shí),或者當(dāng)我們需要在一個(gè)同樣小的服務(wù)器上讀取大文件時(shí)。
后面的問題就是我們將在本教程中深入探討的。
在 GitHub 上可以找到本教程的源碼。
衡量成功的標(biāo)準(zhǔn)
確保我們對代碼進(jìn)行任何改進(jìn)的唯一方法是測試一個(gè)不好的情況,然后將我們修復(fù)之后的測量與另一個(gè)進(jìn)行比較。換句話說,除非我們知道“解決方案”對我們有多大的幫助(如果有的話),否則我們不知道它是否真的是一個(gè)解決方案。
這里有兩個(gè)我們可以關(guān)系的衡量標(biāo)準(zhǔn)。首先是CPU使用率。我們要處理的進(jìn)程有多快或多慢?第二是內(nèi)存使用情況。腳本執(zhí)行時(shí)需要多少內(nèi)存?這兩個(gè)通常是成反比的 - 這意味著我們可以以CPU使用率為代價(jià)來降低內(nèi)存使用,反之亦然。
在一個(gè)異步執(zhí)行模型(如多進(jìn)程或多線程的PHP應(yīng)用程序)中,CPU和內(nèi)存的使用率是很重要的考量因素。在傳統(tǒng)的PHP架構(gòu)中,當(dāng)任何一個(gè)值達(dá)到服務(wù)器的極限時(shí),這些通常都會(huì)成為問題。
測量PHP內(nèi)的CPU使用率是不切實(shí)際的。如果這是你要關(guān)注的領(lǐng)域,請考慮在Ubuntu或MacOS上使用類似top的工具。對于Windows,請考慮使用Linux子系統(tǒng),以便在Ubuntu中使用top。
為了本教程的目的,我們將測量內(nèi)存使用情況。我們將看看在“傳統(tǒng)”的腳本中使用了多少內(nèi)存。我們將執(zhí)行一些優(yōu)化策略并對其進(jìn)行度量。最后,我希望你能夠做出一個(gè)有經(jīng)驗(yàn)的選擇。
我們查看內(nèi)存使用多少的方法是:
- // formatBytes is taken from the php.net documentation
- memory_get_peak_usage();
- function formatBytes($bytes, $precision = 2) {
- $units = array("b", "kb", "mb", "gb", "tb");
- $bytes = max($bytes, 0);
- $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
- $pow = min($pow, count($units) - 1);
- $bytes /= (1 << (10 * $pow));
- return round($bytes, $precision) . " " . $units[$pow];
- }
我們將在腳本的最后使用這些函數(shù),以便我們能夠看到哪個(gè)腳本一次使用最大的內(nèi)存。
我們的選擇是什么?
這里有很多方法可以有效地讀取文件。但是也有兩種我們可能使用它們的情況。我們想要同時(shí)讀取和處理所有數(shù)據(jù),輸出處理過的數(shù)據(jù)或根據(jù)我們所讀取的內(nèi)容執(zhí)行其他操作。我們也可能想要轉(zhuǎn)換一個(gè)數(shù)據(jù)流,而不需要真正訪問的數(shù)據(jù)。
讓我們設(shè)想一下,對于第一種情況,我們希望讀取一個(gè)文件,并且每10,000行創(chuàng)建一個(gè)獨(dú)立排隊(duì)的處理作業(yè)。我們需要在內(nèi)存中保留至少10000行,并將它們傳遞給排隊(duì)的工作管理器(無論采取何種形式)。
對于第二種情況,我們假設(shè)我們想要壓縮一個(gè)特別大的API響應(yīng)的內(nèi)容。我們不在乎它的內(nèi)容是什么,但我們需要確保它是以壓縮形式備份的。
在這兩種情況下,如果我們需要讀取大文件,首先,我們需要知道數(shù)據(jù)是什么。第二,我們并不在乎數(shù)據(jù)是什么。讓我們來探索這些選擇吧...
逐行讀取文件
有許多操作文件的函數(shù),我們把部分結(jié)合到一個(gè)簡單的文件閱讀器中(封裝為一個(gè)方法):
- // from memory.php
- function formatBytes($bytes, $precision = 2) {
- $units = array("b", "kb", "mb", "gb", "tb");
- $bytes = max($bytes, 0);
- $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
- $pow = min($pow, count($units) - 1);
- $bytes /= (1 << (10 * $pow));
- return round($bytes, $precision) . " " . $units[$pow];
- }
- print formatBytes(memory_get_peak_usage());
- // from reading-files-line-by-line-1.php
- function readTheFile($path) {
- $lines = [];
- $handle = fopen($path, "r");
- while(!feof($handle)) {
- $lines[] = trim(fgets($handle));
- }
- fclose($handle);
- return $lines;
- }
- readTheFile("shakespeare.txt");
- require "memory.php";
我們讀取一個(gè)文本文件為莎士比亞全集。文件大小為5.5MB,內(nèi)存占用峰值為12.8MB。現(xiàn)在讓我們用一個(gè)生成器來讀取每一行:
- // from reading-files-line-by-line-2.php
- function readTheFile($path) {
- $handle = fopen($path, "r");
- while(!feof($handle)) {
- yield trim(fgets($handle));
- }
- fclose($handle);
- }
- readTheFile("shakespeare.txt");
- require "memory.php";
文本文件大小不變,但內(nèi)存使用峰值只是393KB。即使我們能把讀取到的數(shù)據(jù)做一些事情也并不意味著什么。也許我們可以在看到兩條空白時(shí)把文檔分割成塊,像這樣:
- // from reading-files-line-by-line-3.php
- $iterator = readTheFile("shakespeare.txt");
- $buffer = "";
- foreach ($iterator as $iteration) {
- preg_match("/\n{3}/", $buffer, $matches);
- if (count($matches)) {
- print ".";
- $buffer = "";
- } else {
- $buffer .= $iteration . PHP_EOL;
- }
- }
- require "memory.php";
猜到我們使用了多少內(nèi)存嗎?我們把文檔分割為1216塊,仍然只使用了459KB的內(nèi)存,這是否讓你驚訝?考慮到生成器的性質(zhì),我們使用的最多內(nèi)存是使用在 迭代中 我們需要存儲(chǔ)的最大文本塊。在本例中,最大的塊為101985字符。
我已經(jīng)撰寫了 使用生成器提示性能 和 Nikita Popov的迭代器庫 ,如果你感興趣就去看看吧!
生成器還有其它用途,但是最明顯的好處就是高性能讀取大文件。如果我們需要處理這些數(shù)據(jù),生成器可能是最好的方法。
管道間的文件
在我們不需要處理數(shù)據(jù)的情況下,我們可以把文件數(shù)據(jù)傳遞到另一個(gè)文件。通常被稱為管道(大概是因?yàn)槲覀兛床坏匠藘啥说墓茏永锩?,?dāng)然,它也是不透明的),我們可以通過使用流方法實(shí)現(xiàn)。讓我們先寫一個(gè)腳本從一個(gè)文件傳到另一個(gè)文件。這樣我們可以測量內(nèi)存的占用情況:
- // from piping-files-1.php
- file_put_contents(
- "piping-files-1.txt", file_get_contents("shakespeare.txt")
- );
- require "memory.php";
不出所料,這個(gè)腳本使用更多的內(nèi)存來進(jìn)行文本文件復(fù)制。這是因?yàn)樗x取(和保留)文件內(nèi)容在內(nèi)存中,直到它被寫到新文件中。對于小文件這種方法也許沒問題。當(dāng)為更大的文件時(shí),就 捉襟見肘了…
讓我們嘗試用流(管道)來傳送一個(gè)文件到另一個(gè):
- // from piping-files-2.php
- $handle1 = fopen("shakespeare.txt", "r");
- $handle2 = fopen("piping-files-2.txt", "w");
- stream_copy_to_stream($handle1, $handle2);
- fclose($handle1);
- fclose($handle2);
- require "memory.php";
這段代碼稍微有點(diǎn)陌生。我們打開了兩文件的句柄,第一個(gè)是只讀模式,第二個(gè)是只寫模式,然后我們從第一個(gè)復(fù)制到第二個(gè)中。最后我們關(guān)閉了它,也許使你驚訝,內(nèi)存只占用了393KB
這似乎很熟悉。像代碼生成器在存儲(chǔ)它讀到的每一行代碼?那是因?yàn)榈诙€(gè)參數(shù) fgets 規(guī)定了每行讀多少個(gè)字節(jié)(默認(rèn)值是-1或者直到下一行為止)。
第三個(gè)參數(shù) stream_copy_to_stream 和第二個(gè)參數(shù)是同一類參數(shù)(默認(rèn)值相同), stream_copy_to_stream 一次從一個(gè)數(shù)據(jù)流里讀一行,同時(shí)寫到另一個(gè)數(shù)據(jù)流里。 它跳過生成器只有一個(gè)值的部分(因?yàn)槲覀儾恍枰@個(gè)值)。
這篇文章對于我們來說可能是沒用的,所以讓我們想一些我們可能會(huì)用到的例子。假設(shè)我們想從我們的CDN中輸出一張圖片,作為一種重定向的路由應(yīng)用程序。我們可以參照下邊的代碼來實(shí)現(xiàn)它:
- // from piping-files-3.php
- file_put_contents(
- "piping-files-3.jpeg", file_get_contents(
- "https://github.com/assertchris/uploads/raw/master/rick.jpg"
- )
- );
- // ...or write this straight to stdout, if we don't need the memory info
- require "memory.php";
設(shè)想一下,一個(gè)路由應(yīng)用程序讓我們看到這段代碼。但是,我們想從CDN獲取一個(gè)文件,而不是從本地的文件系統(tǒng)獲取。我們可以用一些其他的東西來更好的替換 file_get_contents (就像 Guzzle ),即使在引擎內(nèi)部它們幾乎是一樣的。
圖片的內(nèi)存大概有581K?,F(xiàn)在,讓我們來試試這個(gè)
- // from piping-files-4.php
- $handle1 = fopen(
- "https://github.com/assertchris/uploads/raw/master/rick.jpg", "r"
- );
- $handle2 = fopen(
- "piping-files-4.jpeg", "w"
- );
- // ...or write this straight to stdout, if we don't need the memory info
- stream_copy_to_stream($handle1, $handle2);
- fclose($handle1);
- fclose($handle2);
- require "memory.php";
內(nèi)存使用明顯變少(大概 400K ),但是結(jié)果是一樣的。如果我們不關(guān)注內(nèi)存信息,我們依舊可以用標(biāo)準(zhǔn)模式輸出。實(shí)際上,PHP提供了一個(gè)簡單的方式來完成:
- $handle1 = fopen(
- "https://github.com/assertchris/uploads/raw/master/rick.jpg", "r"
- );
- $handle2 = fopen(
- "php://stdout", "w"
- );
- stream_copy_to_stream($handle1, $handle2);
- fclose($handle1);
- fclose($handle2);
- // require "memory.php";
還有其它一些流,我們可以通過管道來寫入和讀取(或只讀取/只寫入):
- php://stdin (只讀)
- php://stderr (只寫, 如php://stdout)
- php://input (只讀) 這使我們能夠訪問原始請求體
- php://output (只寫) 讓我們寫入輸出緩沖區(qū)
- php://memory 和 php://temp (讀-寫) 是我們可以臨時(shí)存儲(chǔ)數(shù)據(jù)的地方。 不同之處在于一旦它變得足夠大 php://temp 會(huì)將數(shù)據(jù)存儲(chǔ)在文件系統(tǒng)中,而 php://memory 將一直持存儲(chǔ)在內(nèi)存中直到資源耗盡。
還有一個(gè)我們可以在stream上使用的技巧,稱為 過濾器 。它們是一種中間的步驟,提供對stream數(shù)據(jù)的一些控制,但不把他們暴露給我們。想象一下,我們 會(huì)使用Zip擴(kuò)展名 來壓縮我們的shakespeare.txt文件。
- // from filters-1.php
- $zip = new ZipArchive();
- $filename = "filters-1.zip";
- $zip->open($filename, ZipArchive::CREATE);
- $zip->addFromString("shakespeare.txt", file_get_contents("shakespeare.txt"));
- $zip->close();
- require "memory.php";
這是一小段整潔的代碼,但它測量內(nèi)存占用在10.75MB左右。使用過濾器的話,我們可以減少內(nèi)存:
- // from filters-2.php
- $handle1 = fopen(
- "php://filter/zlib.deflate/resource=shakespeare.txt", "r"
- );
- $handle2 = fopen(
- "filters-2.deflated", "w"
- );
- stream_copy_to_stream($handle1, $handle2);
- fclose($handle1);
- fclose($handle2);
- require "memory.php";
此處,我們可以看到名為php://filter/zlib.deflate的過濾器,它讀取并壓縮資源的內(nèi)容。我們可以在之后將壓縮數(shù)據(jù)導(dǎo)出到另一個(gè)文件中。這僅使用了 896KB .
我知道這是不一樣的格式,或者制作zip存檔是有好處的。你不得不懷疑:如果你可以選擇不同的格式并節(jié)省約12倍的內(nèi)存,為什么不選呢?
為了解壓此數(shù)據(jù),我們可以通過執(zhí)行另一個(gè)zlib filter將壓縮后的數(shù)據(jù)還原:
- // from filters-2.php
- file_get_contents(
- "php://filter/zlib.inflate/resource=filters-2.deflated"
- );
Streams have been extensively covered in Stream在“ 理解PHP中的流 ”和“ U高效使用PHP中的流 ”中已經(jīng)被全面介紹了。如果你喜歡一個(gè)完全不同的視角,可以閱讀一下。
本文名稱:如何在不會(huì)導(dǎo)致服務(wù)器宕機(jī)的情況下,用PHP讀取大文件
標(biāo)題URL:http://www.dlmjj.cn/article/dphspgh.html


咨詢
建站咨詢
