Как произвести чтение больших файлов в PHP (и не угробить сервер)

Как произвести чтение больших файлов в PHP

От автора: PHP разработчики редко заботятся об управлении памятью. PHP движок превосходно выполняет свою работу и подчищает за нами. Серверная модель кратковыполняемого контекста значит, что даже самый плохой код имеет долгосрочный эффект.

Нам мало когда необходимо выходить за эти комфортные рамки. Например, когда мы пытаемся запустить Composer в большом проекта на минимальном VPS, или когда необходимо произвести в PHP чтение большого файла на все таком же маленьком сервере.

В этом уроке мы обсудим последнюю проблему. Код к уроку можно найти на GitHub.

Измеряем успех

Единственный способ понять, что мы что-то улучшили в коде, это измерить плохой участок, после чего сравнить эти измерения после фикса. Другими словами, если мы не знаем, насколько «решение» помогло нам (если вообще помогло), мы не можем утверждать, что это вообще решение.

Нас заботят два фактора. Первый – потребление CPU. С какой скоростью работает процесс, над которым мы будем работать? Второй фактор – потребление памяти. Сколько памяти выделяется на выполнение скрипта? Зачастую эти два фактора обратно пропорциональны – т.е. мы можем разгрузить память за счет CPU и наоборот.

В асинхронной модели выполнения (многопроцессовые или многопоточные приложения PHP) потребление CPU и памяти важные факторы. В стандартной архитектуре PHP они становятся проблемой, когда один из факторов достигает ограничений сервера.

Внутри PHP измерять потребление CPU непрактично. Если вы хотите сосредоточиться на этой области, попробуйте использовать что-то типа top, Ubuntu или macOS. В Windows попробуйте использовать Linux Subsystem, чтобы использовать top в Ubuntu.

В рамках урока мы будем измерять потребление памяти. Мы посмотрим, сколько памяти используется в «обычных» скриптах. Проведем пару стратегий оптимизации и измерим их. В конце я хочу, чтобы вы могли делать образованный выбор.

Методы, которые мы будем использовать для измерения потребления памяти:

Эти функции мы будем вызывать в конце скриптов и смотреть, какой скрипт использует больше всего памяти.

Какие у нас варианты?

Эффективно читать файлы можно множеством разных способов. Также есть 2 вероятных сценария их использования. Нам может понадобиться считывать и обрабатывать все данные одновременно, выводить обработанные данные или выполнять другие действия на основе считанного. Также нам может понадобиться трансформировать поток данных без необходимости получать к нему доступ.

Представим, что для первого сценария мы хотим иметь возможность читать файл и создавать отдельные очереди фоновых задач каждые 10 000 строк. Нам понадобится хранить в памяти минимум 10 000 строк и передавать их в менеджер очереди фоновой задачи (какую бы форму она не принимала).

Для второго сценария представим, что мы хотим сжать контент определенного большого ответа от API. Нам неважно что в ответе, но нам необходимо, чтобы он был в сжатой форме.

В обоих сценариях нам необходимо читать большие файлы. В первом нам нужно знать, что в данных. Во втором нам неважно, что в данных. Разберем эти варианты…

Чтение файлов построчно

Для работы с файлами существует множество функций. Давайте соединим парочку функций в нативный файл ридер:

Мы читаем текстовый файл с полным собранием сочинений Шекспира. Текстовый файл весит 5.5Мб, пиковое потребление памяти составляет 12.8Мб. Теперь давайте считаем все строки через генератор:

Размер текстового файла тот же, но пиковое потребление памяти уже 393Кб. Но это ничего не значит, пока мы на начнем выполнять операции со считанными данными. Можно разбить документ на куски по двум пустым строкам. Вот так:

Как думаете, сколько теперь памяти используется? Удивитесь ли вы, когда узнаете, что несмотря на разбиение документа на 1 216 кусков, у нас все равно используется 459Кб памяти? Природа генераторов такова, что больше всего памяти используется, когда хранится самый большой кусок текста в итерации. У нас самый большой кусок составляет 101 985 символов.

Я уже писал об ускорении производительности с помощью генераторов и библиотеке итераторов от Nikita Popov. Можете почитать, если интересно!

У генераторов есть другое применение, но наш пример отлично подходит для быстрого чтения больших файлов. Для работы с данными генераторы, возможно, подходят лучше всего.

Сообщение между файлами

В ситуации, когда нам не нужно работать с данными, мы можем передавать данные файла в другой файл. Процесс называется piping (по-видимому, потому что мы не знаем, что внутри «трубы», но знаем что на ее концах… пока труба непрозрачна, конечно!). Для этого нам понадобятся поточные методы. Давайте сначала напишем скрипт передачи данных из одного файла в другой, чтобы измерить потребление памяти:

Неудивительно, что этот скрипт использует чуть больше памяти для запуска, чем текстовый файл, который он копирует. Это происходит потому, что метод должен считать (и хранить) контент файла в память, пока данные не запишутся в новый файл. С маленькими файлами все будет хорошо. С большими файлами не очень…

Давайте попробуем передать данные из одного файла в другой:

Код немного странный. Мы открываем обработчики в обоих файлах, первый в режиме чтения, второй в режиме записи. Далее данные копируются из первого файла во второй. В конце оба файла закрываются. Возможно, вы удивитесь, используется 393Кб памяти.

Знакомо. Не столько ли генератор использовал для хранения при считывании построчно? Объем памяти такой, потому что второй аргумент в fgets определяет количество байт считывания для каждой строки (по умолчанию -1 или пока не дойдет до новой строки).

Третий аргумент stream_copy_to_stream точно такой же параметр (с теми же значениями по умолчанию). stream_copy_to_stream читает из одного потока построчно и пишет в другой поток. Части, где генератор дает значение, пропускаются, так как нам не нужно работать со значением.

Передавать текст нам неудобно, поэтому давайте подумаем о другим примерах. Например, мы хотим вывести изображение с CDN, как своего рода перенаправленный роут приложения. Это можно проиллюстрировать похожим кодом:

Представьте, что роут приложения привел нас к этому коду. Но вместо получения файла с локального хранилища мы хотим получить его с CDN. Мы можем заменить file_get_contents на что-то более элегантное (Guzzle), но внутри все то же самое.

Памяти используется (для изображения) около 581Кб. Так как же передавать изображение через поток?

Потребление памяти немного меньше 400Кб, но результат тот же. Если нам не нужна информация о памяти, можно было бы выводить по-старому. Для этого в PHP есть простой способ:

Другие потоки

Мы можем соединять другие поток и/или писать в них и/или читать из них:

php://stdin (read-only)

php://stderr (write-only, как php://stdout)

php://input (read-only) дает доступ к тексту запроса

php://output (write-only) позволяет писать в буфер вывода

php://memory и php://temp (read-write) места временного хранения данных. Разница в том, что php://temp будет хранить данные в файловой системе, пока они не перепрыгнут определенную отметку, а php://memory хранит данные, пока не кончится память.

Фильтры

С потоками можно использовать другой трюк – фильтры. Это что-то промежуточное. Фильтры дает небольшой контроль над данным поток без необходимости их открытия. Представьте, что нам необходимо сжать shakespeare.txt. Мы можем использовать Zip расширение:

Аккуратный код, но он работает со скоростью 10.75Мб. С фильтрами можно лучше:

В коде используется фильтр php://filter/zlib.deflate, который читает и сжимает контент ресурса. Далее сжатые данные можно передать в другой файл. И операция занимает всего 896Кб.

Знаю, формат другой, и с созданием zip архивов есть проблемы. Однако нужно подумать: если вы можете выбрать другой формат и использовать в 12 раз меньше памяти, стоит ли оно того?

Для распаковки данных можно запустить файл через другой zlib фильтр:

Потоки подробно разобраны в «понятие потоков в PHP» и «Эффективное использование потоков в PHP». Хотите узнать что-то новое, почитайте.

Настройка потоков

У fopen и file_get_contents есть свой набор опций по умолчанию, но их можно настраивать. Для установки параметров необходимо создать новый контекст потока:

В этом примере мы пытаемся отправить POST запрос в API. API защищен, но нам все равно нужно использовать контекстное свойство http (как для http и https). Мы задали пару заголовков и открыли обработчик файла для API. Обработчик можно открыть только для чтения, так как запись ложится на контекст.

Настроить можно массу параметров, лучше посмотрите документацию.

Создание кастомных протоколов и фильтров

Прежде чем мы завершим, давайте поговорим о создании кастомных протоколов. Если посмотреть в документации, то можно найти пример класса:

Мы не будем ничего реализовывать, так как считаю, что интерфейс достоит отдельного урока. Много чего нужно сделать. Но после этого можно с легкостью зарегистрировать наш поток:

Точно так же можно создать кастомные потоковые фильтры. В документации есть пример класса фильтра:

Зарегистрировать можно легко:

highlight-names должен совпадать со свойством filtername нового класса фильтра. Также в строке php://filter/highligh-names/resource=story.txt можно использовать кастомные фильтры. Намного легче определить фильтры, чем протоколы. Одна из причин – протоколы обрабатывают операции над директориями, а фильтры обрабатывают только куски данных.

Если вы уловили суть, рекомендую вам поэкспериментировать с кастомными протоколами и фильтрами. Если вы можете применить фильтры к операциям stream_copy_to_stream, то ваше приложение почти не будет использовать память, даже при работе с неприлично большими файлами. Представьте, что вы пишите фильтр resize-image или encrypt-for-application.

Заключение

С этой проблемой мы сталкиваемся не так часто, однако ее легко получить при работе с большими файлами. В асинхронных приложениях очень легко положить весь сервер, если не думать о потреблении памяти.

Надеюсь, этот урок показал вам пару новых идей (или освежил их в вашей памяти), чтобы вы могли подумать, как эффективно читать и писать большие файлы. Когда мы познакомимся с потоками и генераторами и перестанем использовать функции типа file_get_contents, из приложения исчезнет целый ряд ошибок. Хорошая цель!

Автор: Christopher Pitt

Источник: //www.tutorialspoint.com/

Редакция: Команда webformyself.

Метки:

Похожие статьи:

Комментарии Вконтакте: