Pull to refresh

Comments 67

Краткое описание того, как работает сборщик мусора в PHP?

P.S. Заметьте — мусор он так и не убрал.
Скорее уж в комментах к коммиту отличная картинка есть с мусоросборником

До слез…
Страница с комментами к коммиту — филиал упячки на гитхабе)
А где можно найти весь список «легендарных коммитов» — гугление не помогает
Вот один из самых эпичных. До него установка драйвера выполняла rm -rf /usr
Руки сами тянутся добавить gc_disable в контроллер ;)

Надо будет попробовать эту новую «супер-фичу»

Эх, если бы php был «standalone» для web-приложений из коробки…

Походу сейчас гитхаб висит от наплыва читателей…
Вы правы — это. Хотя это совершенно несправедливо. За что???
github — террористический ресурс, рассылающий детскую порнографию. Вы что, не знали?
Я бы рекомендовал перед этим сделать ini_set('memory_limit',0)
Не травите душу про github
Проверил. Ощутимого прироста нет. Хинт весьма специфичный.
Читаю комментарии на гитхабе через аномайзер… Ощущаю какие то странные эмоции… Спасибо Роскомнадзор! Как же хорошо что я перешёл на битбакет.
Это не решение. Добровольный проброс https-каналов через третьих лиц лишь усугубляет ситуацию — ненамеренно можно открыть доступ к приватным данным или аккаунтам.

Извиняюсь за оффтопик конечно.
Согласен что это не решение. Я не использую свои учетные данные через аномайзеры никогда и нигде, а комментарии посмотреть, которые в открытом доступе можно хоть через что.
Я вынужденно перешёл с тарифа за $5 в DigitalOcean на тариф за $10 — именно из-за того, что composer на 512 мегабайтах не фурычил.
Судя по бенчмарку в конце поста, помимо ускорения работы, отключение GC ещё и уменьшает потребление памяти!
Может, попробовать откатиться назад на $5?..
На крайний случай всегда есть очень хардкорный вариант, «по старинке» пушить папочку вендоров прямо из локалки по sftp\ftp ;)
Ага, у меня весь проект занимает 12 МБ, public занимает ещё 20 МБ. А vendor — 160 МБ!
Кстати, пошёл мерить папки, обнаружил, что var/cache/dev/profiler весит 480 МБ! Пойду прибью его…
Ну так вендоры раз в 100 лет обновляются, да и накрайняк в гит можно заткнуть, там только диффы будут прилетать.
А вы установите вендоры с --prefer-dist, или даже так 160 мб?

В любом случае я к примеру собираю проект у себя или на CI сервере так что composer на сервере уже не нужен.
Хм, у меня были проблемы с GitLab на тарифе за $10 (git'у иногда не хватало оперативки при загрузку больших diff'ов), но проблему я решил не переходом на более дорогой тариф, а просто включением свопа.
Зачем платить больше?

cat updatedeps.sh
#!/bin/bash
# Enable swap, run composer update, disable swap. That's it.

if [ ! -f /swapfile ]; then
    sudo dd if=/dev/zero of=/swapfile bs=1024 count=1024m
    sudo mkswap /swapfile
fi

sudo swapon /swapfile
composer update
sudo swapoff /swapfile
А зачем дизейблить своп? У DO SSD, это быстро + лучше уж своп, чем не работает совсем.
 Видимо, в большинстве случаев из за использования свопа система начинает работать настолько медленно, что лучше уж дать прибить composer, чем, считай, повесить весь сервер.
И судя по всему это связано с ограничением IOPS в вм.
Есть такой параметр swappiness, который можно несколько понизить. Тогда совсем фейла когда память скушается не будет, но и память будет предпочитать свопу.
1. Не на SSD.
2. Если свопа нет и память кончилась, сервер гарантировано повиснет. Опробовано, по крайней мере, на Ubuntu Server.
Вам даже на «composer install» с имеющимся composer.lock памяти не хватает? Насколько я знаю, это очень щадящий режим, который не производит разрешения зависимостей, на что и уходит так много памяти.
Честно говоря, я не пробовал install делать. Я делаю update — пока сайт в разработке, пусть всегда будут свежие версии пакетов.
Сейчас вы подсказали, нужно будет попробовать делать update у себя на dev-машине, composer.lock добавить в git, и делать install на test-сервере.
Я уже решил проблему включением swap, как подсказал Xobb, только я делал своп не его скриптом (не заработал), а по оф.инструкции DO.
Спасибо!

Интересно, что я в нескольких разных источниках видел совет убирать composer.lock из git.
Но по вашей ссылке явно написано:
Commit your application's composer.lock (along with composer.json) into version control.
Я правильно понимаю что вы еще не сталкивались с «веселыми приключениями с разными версиями пакетов на разных машинах»?
Нет, так я не веселился :) Поделитесь весельем?

Когда у меня композер не заработал, я пробовал вручную накатить обновления в папку vendor, но заставить autoloader понять их не удалось.
Сильно сократить объём используемой памяти композером при обновлении можно если прописать более поздние версии пакетов в composer. Т.е. если у вас, например, в зависимостях стоит symfony: dev-master, то это несколько десятков версий для symfony и по нескольку десятков зависимостей для каждого компонента symfony — а это по отдельному Package на версию.

Если же прописать версию как ~2.6 (где 2.6 — самая свежая версия), то при вычислении зависимостей не будут анализироваться версии меньше 2.6 (если только у других пакетов нет таких зависимостей или если с последними версиями не получится удовлетворить зависимости и придётся перебирать более старые версии). Ну и minimum-stability: stable тоже помогает.

Можно почитать stof об этом.
Рискую быть оплеванным ликующей толпой, но не могу не сказать. Да, выполнение скрипта ускорилось в разы, но что действительно это значит?

1) Отключать GC — плохо. «Но расход памяти не увеличивается, значит циклических ссылок нет, так какая разница?» На самом деле они есть до этого места. И даже если сейчас после выключения GC их нет, не факт, что они не появятся в будущем. Теперь каждый комит надо проверять на наличие циклических ссылок.

2) Composer — пакетный менеджер. Что он делает, что ему в памяти постоянно нужно держать сотни мегабайт и создавать огромное количество объектов? Отключение сборщика не устранило проблему, а лишь отложила её решение.

3) Как так получается, что отключение сборки мусора ускоряет выполнение скрипта в десять раз? Что не так со сборщиком мусора? В нем нет поколений объектов, и ему приходится перебирать все объекты при каждой сборке?
Ура! Наконец-то хоть кто-то начал задавать правильные вопросы. Я отвечу на второй, на мой взгляд он самый важный.
Композеру нужно столько памяти, потому что он туда(в память) закачивает скачанные пакеты (.tar.gz). Затем он там туда же(да да, в память) их распаковывает в поисках composer.json и на его основе скачивает зависимые пакеты(опять же в память).
Мы пытались сделать пакет размером в 700МБ. Мы падали по memory_limit в 2G.
Дело не а PHP и его GC. Дело в самом композере
То есть коммит «Сделать быстро» для композера все-таки добавит подводных камней в его работе?
Скорее всего, можно почувствовать что-то при обновлении очень больших пакетов, которые вместе не влезут в memory_limit.
UFO just landed and posted this here
Да, мы хотели сделать свои пакеты. Без метаинформации
Но если он не закачивает в память пакеты, для чего ему 350 мегабайт из теста?
UFO just landed and posted this here
Странная реализация… Зачем всё скачивать и распаковывать в память, если есть /tmp? Я не сталкивался с Composer, но для примера apt-get не хавает память гигабайтами, а ворочает пакетами, размеры которых скорее всего больше чем у Composer. А если у пакета тысяча зависимостей — тушите свет? А если у меня memory_limit скажем 128Мб или меньше? Что то тут не так.
Я не знаю, может для совместимости в windows, в котором нет /tmp, tar, gz а есть php extension
В windows есть свой temp. Как там в bat, переменная %temp%? Я не считаю это проблемой, которая может как либо помешать в принципе. А на счет архивов я без понятия, но если оно сейчас распаковывается, значит всё есть и всё работает и остаётся сбросить на диск. Надо просто посмотреть код распаковки, хотя у меня сейчас совсем нет желания это делать.
Пакеты целиком закачиваются что ли? Не через буфер, записывая на диск периодически?
3) Как так получается, что отключение сборки мусора ускоряет выполнение скрипта в десять раз? Что не так со сборщиком мусора? В нем нет поколений объектов, и ему приходится перебирать все объекты при каждой сборке?
Резкое падение эффективности сборки мусора в условиях нехватки памяти — это общая беда всех сборщиков мусора. Даже при наличии поколений объектов, десятая сборка мусора подряд в любом случае затащит все объекты в перманентное поколение.

Отсюда и ответ на пункт 1. Да, отключать GC — плохо. Но это единственный способ утилизировать 100% памяти без тормозов.
Даже при наличии поколений объектов, десятая сборка мусора подряд в любом случае затащит все объекты в перманентное поколение.
Да, затащит. И они будут проверяться значительно реже, чем в других поколениях. В том и смысл.

Пример на Питоне
import time
import gc

gc_counters = [0, 0, 0]
def gc_cb(phase, info):
    gc_counters[info['generation']] += 1

# gc.disable()
gc.callbacks.append(gc_cb)

class Node(object):
    def __init__(self, children):
        self.children = children

    @classmethod
    def tree(cls, depth, numchildren):
        if depth == 0:
            return []
        return [
            cls(cls.tree(depth-1, numchildren))
            for _ in range(numchildren)
        ]

start = time.time()
ref = Node.tree(9, 5)
print('>>>', gc_counters, time.time() - start)

Результаты при включенном сборщике мусора:
>>> [19180, 1744, 34] 10.722499132156372
При выключенном:
>>> [0, 0, 0] 4.036884069442749

Запусков нулевого поколения — 19k, первого — 1,7k, второго — всего 34. Т.е разница в производительности всего в 2,5 раза, хотя это синтетический тест и только и делает, что создает объекты.

Такой же пример на PHP
<?php

class Node {
  public function __construct($children) {
    $this->children = $children;
  }
  
  public static function tree($depth, $numchildren) {
    if ($depth == 0) {
      return [];
    }
    $children = [];
    for ($i=0; $i < $numchildren; $i++) { 
      $children[$i] = new self(self::tree($depth - 1, $numchildren));
    }
    return $children;
  }
}

ini_set('memory_limit', '-1');
// gc_disable();

$start = microtime(true);
$ref = Node::tree(9, 5);
print '>>> '.(microtime(true) - $start)."\n";

В PHP нельзя посмотреть запуски сборщика без перекомпиляции, но результаты очень похожи:
>>> 7.6519370079041
>>> 3.9215548038483

Поэтому скорее всего там сборщик по поколениям, возможно с другими лимитами. И поэтому еще более непонятно, что такого делает Composer, что умудряется выдать результат хуже, чем синтетический тест.
Вы создаете всего лишь 60000 объектов, этого мало. Проблемы с GC начинаются именно при исчерпании памяти.
Ка вы это насчитали? На каждом уровне создается 5 в степени глубины нод. Т.е. всего:

5**9 + 5**8 + 5**7 + 5**6 + 5**5 + 5**4 + 5**3 + 5**2 + 5**1 = 2 441 405 нод

Плюс у каждой ноды есть массив детей, даже если он пустой. Т.е. всего 4 882 811 объектов.

В PHP это занимает 1,37 ГБ памяти, а Питоне 545 МБ памяти. Мало?
Извиняюсь, не в том порядке в степень возвел случайно.
Так ответы на эти вопросы есть в комментарии того чувака, который и предложил отключать gc (https://github.com/composer/composer/pull/3482#issuecomment-65199153):

@naderman, generally if you create many many objects, you pretty much want to always disable GC. This is because PHP has a hard-coded limit (compile-time) of root objects that it can track in its GC implementation (I believe it's set to 10000 by default).

If you get close to this limit, GC kicks in. If it cannot clean-up, it will still keep trying in frequent intervals. If you go above the limit, any new root objects are not tracked anymore, and cannot be cleaned-up whether GC is enabled, or not.

This might also be why the memory consumption for bigger projects does not vary. If GC is enabled, it's just not working anymore even if there is potentially something to clean-up. For smaller projects, you might see a memory difference.

+ вот еще

@naderman the GC in PHP is only used to be able to handle circular object graphs. For non-circular graphs, the refcount is enough to destroy values without needing the GC. So as long as you avoid circular object graphs in objects used a lot in Composer, you could indeed disable it.
Sign up to leave a comment.

Articles