Перевод транскрипции подкаста подготовлен в преддверии старта курса «Backend-разработчик на PHP»
Описание
В этом эпизоде «PHP Internals News» я беседую с Никитой Поповым (Twitter, GitHub, Сайт) о проблемах предзагрузки PHP 7.4 и его RFC WeakMaps.
Стенограмма
Дерик Ретанс 0:16
Привет, я Дерик. И это PHP internals news — еженедельный подкаст, посвященный демистификации развития языка PHP. Это 38-й эпизод. Я собираюсь обсудить с Никитой Поповым несколько вещей, которые произошли за время праздников. Никита, как прошли твои праздники?
Никита Попов 0:34
Мои праздники прошли замечательно.
Дерик Ретанс 0:36
Я подумал начать беседу не так, как в прошлом году. В любом случае, сегодня утром я хочу поговорить с тобой о том, что произошло с PHP 7.4 в эти праздничные дни. А именно — о проблемах с предзагрузкой в PHP 7.4 на Windows. Я понятия не имею, в чем собственно проблема. Не мог ли бы ты мне это объяснить?
Никита Попов 0:56
На самом деле в ранних версиях PHP 7.4 было довольно много проблем с предзагрузкой. Эта фича определенно была не достаточно протестирована. Большинство проблем было исправлено в 7.4.2. Но если вы используете preload-user (собственно то, что вы должны использовать, если вы работаете в руте), то вы, вероятно, все равно столкнетесь с крашами, и это будет исправлено только в следующем релизе.
Дерик Ретанс 1:20
В 7.4.3.
Никита Попов 1:22
Да. Но вернемся к Windows. Windows имеет совершенно другую архитектуру процессов, нежели Linux. В частности, на Linux или BSD у нас есть fork, который в целом просто берет процесс и копирует все его состояние в памяти, чтобы создать новый процесс. Это намного дешевле, чем кажется, потому что это повторное использование памяти до тех пор, пока она на фактически не изменится.
Дерик Ретанс 1:48
Это копирование при записи.
Никита Попов 1:49
Именно, копирование при записи. Эта же функциональность не существует в Windows, или, по крайней мере, она не доступна для широкого применения. Таким образом, в Windows вы можете создавать новые процессы только с нуля без повторного использования памяти из предыдущих. А для OPcache это проблема, потому что OPcache хотел бы ссылаться на внутренние классы, как определяет PHP. Но поскольку мы храним объекты в общей памяти, которая совместно используется несколькими процессами, у нас возникает проблема, заключающаяся в том, что эти внутренние классы могут находиться по разным адресам в разных процессах. В Linux мы всегда будем иметь один и тот же адрес, потому что мы используем fork, и адрес сохраняется. В Windows у каждого процесса может быть свой адрес. И особенно потому, что Windows, насколько я помню, начиная с Windows Vista, использует рандомизацию адресного пространства. Скорее даже почти всегда это будет другой адрес.
Дерик Ретанс 2:51
Из соображений безопасности?
Никита Попов 2:52
Именно. Из соображений безопасности.
Дерик Ретанс 2:54
Если вместо форка вы будете запускать новый процесс, будет ли это проблемой для Linux?
Никита Попов 2:59
Да, это будет проблемой. Разница в том, что в Unix мы так не делаем. OPcache в Windows имеет совершенно другую архитектуру. В Linux мы не разрешаем подключаться к существующему OPcache из отдельного процесса. Таким образом, единственный способ разделить OPcache — это использовать fork. В Windows из-за ограничения в виде отсутствия fork, мы разрешаем такие подключения, и именно здесь нам приходится иметь дело с целым рядом проблем. Так что на самом деле это общая проблема, а не только предзагрузки, разница только в том, что обычно мы можем просто сказать: что ж, ладно, мы не разрешаем никаких ссылок на внутренние классы из общей памяти в Windows. Это небольшой недостаток со стороны оптимизации, но это не супер важно. Но что до предварительной загрузки, мы должны связать весь граф классов во время ее выполнения. И если у вас есть классы, которые расширяются из внутреннего класса, например, из Exception. Или, в некоторых случаях, вы можете просто использовать внутренний класс в качестве подсказки типа, тогда вы не сможете хранить ссылки такого рода в общей памяти в Windows. И так как для предварительной загрузки почти неизбежно, что вы попадаете в подобную ситуацию, получается, что вы просто не можете использовать предзагрузку в Windows.
Дерик Ретанс 4:18
Следовательно, напрашивается решение отключить ее, вместо того, чтобы пытаться заставить ее работать и практически всегда терпеть неудачу.
Никита Попов 4:24
Ага, я имею в виду, раньше она с горем пополам работала, просто вы получили кучу предупреждений, что такие классы не были предварительно загружены. И люди могут попасть в ситуацию, когда они попробуют ее на простых примерах и по результатам приходят к выводу, что предварительная загрузка отлично работает. Но как только они переходят к своему действительно сложному приложению, которое использует внутренние классы в различных местах, оказывается, что на самом деле, нет, на практике это не работает. И поэтому единственный способ избежать проблем — отключит ее.
Дерик Ретанс 4:51
Кажется, пока это самое разумное решение, но как ты думаешь, в какой-то момент это можно будет исправить другим умным способом?
Никита Попов 4:58
Основной способ, с помощью которого с этим можно бороться, — это избегать многопроцессных вложений в Windows. Альтернативой наличию нескольких процессов является наличие нескольких потоков, которые совместно используют адресное пространство. По сути, то же самое, что fork, только с потоками. Но это, конечно, зависит от того, какой веб-сервер и SAPI вы используете. И я думаю, что в настоящее время в Windows многопоточные веб-серверы несколько популярнее, чем в Linux, но это все еще не главный тренд в разработке.
Дерик Ретанс 5:34
Я думаю, что раньше модели потоковых процессов в Windows были гораздо более распространены, когда PHP только вышел для Windows, потому что это был модуль ISAPI, который всегда был потоковым. Насколько я помню, это основная причина, из-за которой у нас был ZTS, в первую очередь. Да, в какой-то момент они начали переходить на модели PHP FPM, потому что они не использовали многопоточность, и были таким образом гораздо безопаснее в использовании.
Никита Попов 5:57
Верно. Я имею в виду, что у потоков есть проблемы, в частности, потому что такие вещи, как локали, относятся к процессу, а не к потоку. Таким образом, процессы, как правило, безопаснее использовать.
Дерик Ретанс 6:08
Произошло ли что-нибудь еще интересное, что пошло не так с предзагрузкой, или вы не хотите об этом рассказывать?
Никита Попов 6:12
Остальное проистекает в основном из того, что у нас есть два разных способа реализовать предварительную загрузку. Один использует файл компиляции OPcache, а другой используют require или include, и разница между ними заключается в том, что файл компиляции OPcache объединяет файл, но не выполняется. В этом случае способ, которым мы выполняем предварительную загрузку, заключается в том, что мы сначала собираем все классы, а затем постепенно их связываем, фактически регистрируем их, всегда следя за тем, чтобы все зависимости уже были связаны. И это тот способ, который, как мне кажется, в целом хорошо работал с релиза PHP 7.4. И другой, require подход, в котором, собственно, require непосредственно выполняет код и регистрирует классы. И в этом случае, если оказывается, что какая-то зависимость не может быть предварительно загружена по какой-либо причине, мы просто должны прервать предзагрузку, потому что мы не сможем восстановиться после этого. Это прерывание отсутствовало. И как оказалось, в конце концов, люди на практике используют предзагрузку, используя require подход, а не подход с использованием файла компиляции OPcache.
Дерик Ретанс 7:26
Хотя это тот пример, который можно наблюдать в большинстве примеров, которые я видел, и в документации.
Никита Попов 7:30
Да, у него есть некоторые преимущества по сравнению с require.
Дерик Ретанс 7:34
Что еще произошло за праздники, так это то, что вы работали над несколькими RFC, о которых можно говорить слишком долго, чтобы это поместилось в этом эпизоде. Но одним из более ранних был WeakMap или WeakMaps RFC, который был построен на основе слабых ссылок, которые мы уже имеем в PHP 7.4. Что не так со слабыми ссылками, и зачем нам понадобились слабые ассоциативные массивы?
Никита Попов 7:58
Со слабыми ссылками все в порядке. Просто напомню, что представляют из себя слабые ссылки — они позволяют ссылаться на объект, не исключая его из цикла сборки мусора. Так что, если объект будет уничтожен, то у вас останется висячая ссылка. Если вы попытаетесь получить к нему доступ, вы получите некоторую информацию об объекте. На сегодняшний день, вероятно, наиболее распространенным вариантом использования любой слабой структуры данных является ассоциативный массив (map), где у вас есть объекты и вы хотите связать с ними какие-то данные. Типичными вариантами использования являются кэши и другие мемоизирующие структуры данных. И причина, по которой важно, чтобы эта структура была слабой, заключается в том, что вы бы не хотели… хммм, скажем, если вы хотите кэшировать некоторые данные связанные с объектом, но никто другой в итоге так и не использует этот объект, вы бы не хотели продолжать хранить эти данные в кэше, потому что никто никогда не будет использовать их снова. Они будут только бессмысленно занимать память. И здесь выходит на поле WeakMap. Здесь вы используете объекты в качестве ключей, а в качестве значения — какие-то данные. И если объект больше не используется за пределами этого ассоциативного массива, он также удаляется из него.
Дерик Ретанс 9:16
Итак, вы упомянули объекты в качестве ключей. Это что-то новенькое? Потому что я не думаю, что в настоящее время PHP поддерживает это.
Никита Попов 9:22
Да, вы не можете использовать объекты в качестве ключей в обычных массивах. Это не сработает. Но, например, интерфейсу ArrayAccess и интерфейсу Traversable все равно, какие у вас типы. Так что вы можете использовать что угодно в качестве ключей.
Дерик Ретанс 9:37
Я освежил это в памяти, да. Но weak map — это то, что затем реализует ArrayAccess.
Никита Попов 9:44
Правильно.
Дерик Ретанс 9:45
Как выглядит интерфейс Weak Map? Как бы вы с ним взаимодействовали?
Никита Попов 9:49
Ну, на самом деле, он просто реализует все магические интерфейсы из PHP. Таким образом, ArrayAccess — вы можете получить доступ к weak map по ключу, где ключ это объект. Traversable — то есть вы можете перебирать weak map и получать ключи и значения, и, конечно, Countable, так что вы можете подсчитать, сколько там элементов. Вот и все.
Дерик Ретанс 10:12
Все эти методы, их там предостаточно, должно быть девять или десять, или около того, верно?
Никита Попов 10:17
Пять.
Дерик Ретанс 10:18
Нет, там еще шестерка итераторов.
Никита Попов 10:20
Правильно, да, есть маленькая деталь, когда при реализации внутренних классов Traversable вам на самом деле не нужно реализовывать методы итератора. Вот почему их там несколько меньше.
Дерик Ретанс 10:33
Кто получит выгоду от этой новой фичи?
Никита Попов 10:35
Один из пользователей weak map, такие вещи как ORM. Где, ну, записи базы данных представлены как объекты, и есть хранилище данных, связанных с этими объектами. И я думаю, что это хорошо известная проблема, что если вы используете ORM, вы можете иногда сталкиваться с Memory Usage проблемами. И отсутствие слабых структур является одной из причин, почему это может произойти. Так как они просто продолжают держаться за информацию, хотя приложение на самом деле больше не использует ее.
Дерик Ретанс 11:12
Запрашивал ли какой-то конкретный ORM эту функцию?
Никита Попов 11:15
Я так не думаю.
Дерик Ретанс 11:16
Поскольку weak maps — это что-то вроде внутреннего класса в PHP, как эти вещи реализованы? Есть ли что-то интересное? Потому что я помню, как говорил с Джо о слабых ссылках в прошлом году — есть некоторая функциональность, когда она автоматически что-то делает с деструктором или, скорее, с объектами. Это то, что также происходит с weak maps?
Никита Попов 11:37
Итак, механизм работы слабых ссылок и ассоциативных массивов практически одинаков. Таким образом, на каждом объекте есть флаг, который можно установить, чтобы указать, что на него ссылается слабая ссылка или слабый ассоциативный массив. Если объект уничтожен и имеет этот прекрасный флаг, то мы выполняем колбек, который собирается удалить объект из Weak Reference или Weak Map, или сразу из нескольких ассоциативных массивов.
Дерик Ретанс 12:05
Это потому, что существует какой-то реестр, который связывает объект?
Никита Попов 12:08
Потому что мы храним объекты как часть слабые ссылок, и слабых ассоциативных массивов, мы можем эффективно удалить его.
Дерик Ретанс 12:16
Когда я читал RFC, я увидел упоминание чего-то вроде SPL ID объекта, что является способом, как в основном идентифицировать конкретный объект. Это связано со слабыми ссылками или слабыми ассоциативными массивами? Или это что-то, что больше не используется, или люди больше не должны использовать это в значительной степени, потому что, я предполагаю, что ранее это был способ идентифицировать объект и затем связывать с ним дополнительные данные. Как ты упоминал, что ORM должны были делать для кэширования.
Никита Попов 12:44
Верно. Это как-то связано, и в то же время нет. Одно не является заменой другого, это просто разные варианты использования. Раньше у нас очень долгое время был SPL хеш объекта. И я думаю, где-то в PHP 7.0, или, может быть, позже был введен SPL ID объекта, который является целым числом и потому более эффективен. Но, в конце концов, эти функции возвращают уникальный идентификатор объекта. Но этот идентификатор уникален только до тех пор, пока объект жив. Эти ID объектов используются повторно при уничтожении объектов.
Дерик Ретанс 13:30
И это делает их непригодными для связи данных кэша с конкретным объектом?
Никита Попов 13:35
Это делает их пригодными для связывания данных кэша. Но вы также должны хранить объект, чтобы быть уверенным, что он не будет уничтожен. Так вы обходите ограничение, что вы не можете использовать объекты в качестве ключей массива. Вот для чего вам нужен ID. Но вы все равно должны хранить сильную ссылку на объект, чтобы убедиться, что он не мусор. Ведь этот ID начинает ссылаться на какие-то другие объекты.
Дерик Ретанс 14:04
Когда вы говорите «Сильная ссылка», это то, чем традиционно являются ссылки в PHP?
Никита Попов 14:08
Это обычная ссылка.
Дерик Ретанс 14:10
Ну, потому что прошло довольно много времени с тех пор, как RFC было представлено до того, как оно было принято, насколько я понял?
Никита Попов 14:16
Оно принято: 25, ноль
Дерик Ретанс 14:18
25, ноль. Это случается не очень часто.
Никита Попов 14:22
Большинство RFC, возможно, не являются анонимными, но, как правило, они либо приняты на 95%, либо строго отвергнуты. Бывает не много промежуточных решений.
Дерик Ретанс 14:34
Это очень хорошо. В любом случае, мы увидим это в PHP 8, как я полагаю, в конце года.
Никита Попов 14:39
Все верно. Да.
Дерик Ретанс 14:41
Ну, спасибо, что нашли время поговорить со мной о слабых ссылках и предварительной загрузке, особенно в Windows. Спасибо, что нашли время.
Никита Попов 14:50
Спасибо за то, что пригласили меня Дерик.
Дерик Ретанс 14:52
Спасибо за прослушивание этой серии PHP internals news, еженедельного подкаста, посвященного демистификации развития языка PHP. Я веду учетную запись Patreon для поклонников этого подкаста, а также для инструмента отладки Xdebug. Вы можете подписаться на Patreon по адресу. Если у вас есть комментарии или предложения, не стесняйтесь присылать их по электронной почте derick@phpinternals.news. Спасибо, что слушали, и увидимся на следующей неделе.
Заметки к подкасту
RFC: WeakMaps
Подробнее о курсе «Backend-разработчик на PHP»