Как Composer на 70% ускорили

    image
    По всей видимости, на наших глазах родился еще один легендарный коммит (осторожно, в комментариях сплошные гифки):

    github.com/composer/composer/commit/ac676f47f7bbc619678a29deae097b6b0710b799

    При попытке разобраться с проблемой производительности Композера поступило предположение, что причина проблемы кроется в сборщике мусора:

    Это действительно может быть проблемой по части GC. Если создается много объектов, и все они не могут быть «удалены», то GC в PHP начинает сходить с ума — он постоянно пытается провести сборку мусора, но убирать-то нечего — поэтому он просто тратит лишнее время/такты процессора. На это указывает и то, что проблема выявляется только на больших проектах (= много объектов), но не так заметна на маленьких (= GC включается не так часто).

    В некоторых случаях, отключение GC сделает выполнение гораздо быстрее (правда, ценой потребления большего количества памяти). Если еще никто не попробовал, то стоит добавить gc_disable() к команде update/install.

    github.com/composer/composer/pull/3482#issuecomment-65131942

    Результаты, кстати, оказались более чем обнадеживающими:

    Before: Memory usage: 135.4MB (peak: 527.71MB), time: 119.82s
    After: Memory usage: 134.89MB (peak: 391.28MB), time: 11.26s
    
    Before: Memory usage: 163.66MB (peak: 403.82MB), time: 246.25s
    After: Memory usage: 163.34MB (peak: 350.36MB), time: 99.55s
    
    Share post

    Comments 66

      +22
      Это просто леген… Подожди-подожди!
        +21
        … дарно
          +2
          Рано. Выдержки нет.
        +22
        image
          +46
          Краткое описание того, как работает сборщик мусора в PHP?

          P.S. Заметьте — мусор он так и не убрал.
            +12
            Ну так это gc_disable()
              0
              Скорее уж в комментах к коммиту отличная картинка есть с мусоросборником

              До слез…
                +2
                Эта?
                image
                  0
                  Да, именно.
              +3
              Страница с комментами к коммиту — филиал упячки на гитхабе)
              +2
              Рождество в стиле PHP.
                0
                А где можно найти весь список «легендарных коммитов» — гугление не помогает
                  +11
                  Вот один из самых эпичных. До него установка драйвера выполняла rm -rf /usr
                    +1
                    Да, я вот на него и намекал.
                  +1
                  это победа.
                    0
                    Ура, товарисчи!
                      –1
                      Руки сами тянутся добавить gc_disable в контроллер ;)

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

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

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

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

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

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

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

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

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

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

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

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

                                                  3) Как так получается, что отключение сборки мусора ускоряет выполнение скрипта в десять раз? Что не так со сборщиком мусора? В нем нет поколений объектов, и ему приходится перебирать все объекты при каждой сборке?
                                                    +6
                                                    Ура! Наконец-то хоть кто-то начал задавать правильные вопросы. Я отвечу на второй, на мой взгляд он самый важный.
                                                    Композеру нужно столько памяти, потому что он туда(в память) закачивает скачанные пакеты (.tar.gz). Затем он там туда же(да да, в память) их распаковывает в поисках composer.json и на его основе скачивает зависимые пакеты(опять же в память).
                                                    Мы пытались сделать пакет размером в 700МБ. Мы падали по memory_limit в 2G.
                                                    Дело не а PHP и его GC. Дело в самом композере
                                                      0
                                                      То есть коммит «Сделать быстро» для композера все-таки добавит подводных камней в его работе?
                                                        0
                                                        Скорее всего, можно почувствовать что-то при обновлении очень больших пакетов, которые вместе не влезут в memory_limit.
                                                        0
                                                        Композеру нужно столько памяти, потому что он туда(в память) закачивает скачанные пакеты (.tar.gz). Затем он там туда же(да да, в память) их распаковывает в поисках composer.json и на его основе скачивает зависимые пакеты(опять же в память).


                                                        Composer так не делает. Сначала идёт разрешение зависимостей на основании данных от репозиториев (например Packagist). Только в случае успеха Composer грузит то, что необходимо.

                                                        Непосредственно по проблеме могу сказать что Jordi и сам понимает, что это временное решение и решать нужно не так.
                                                          0
                                                          Да, мы хотели сделать свои пакеты. Без метаинформации
                                                          Но если он не закачивает в память пакеты, для чего ему 350 мегабайт из теста?
                                                            0
                                                            Да, мы хотели сделать свои пакеты. Без метаинформации


                                                            Что вы имеете ввиду?

                                                            Но если он не закачивает в память пакеты, для чего ему 350 мегабайт из теста?


                                                            В issues проекта это уже обсуждали. Вот хороший ответ.

                                                            P.S. Не вводите людей в заблуждние догадками, а то FisHlaBsoMAN и z0rg поверили что Composer так криво написан :)
                                                        0
                                                        Странная реализация… Зачем всё скачивать и распаковывать в память, если есть /tmp? Я не сталкивался с Composer, но для примера apt-get не хавает память гигабайтами, а ворочает пакетами, размеры которых скорее всего больше чем у Composer. А если у пакета тысяча зависимостей — тушите свет? А если у меня memory_limit скажем 128Мб или меньше? Что то тут не так.
                                                          –1
                                                          Я не знаю, может для совместимости в windows, в котором нет /tmp, tar, gz а есть php extension
                                                            +1
                                                            В windows есть свой temp. Как там в bat, переменная %temp%? Я не считаю это проблемой, которая может как либо помешать в принципе. А на счет архивов я без понятия, но если оно сейчас распаковывается, значит всё есть и всё работает и остаётся сбросить на диск. Надо просто посмотреть код распаковки, хотя у меня сейчас совсем нет желания это делать.
                                                          0
                                                          Пакеты целиком закачиваются что ли? Не через буфер, записывая на диск периодически?
                                                          0
                                                          3) Как так получается, что отключение сборки мусора ускоряет выполнение скрипта в десять раз? Что не так со сборщиком мусора? В нем нет поколений объектов, и ему приходится перебирать все объекты при каждой сборке?
                                                          Резкое падение эффективности сборки мусора в условиях нехватки памяти — это общая беда всех сборщиков мусора. Даже при наличии поколений объектов, десятая сборка мусора подряд в любом случае затащит все объекты в перманентное поколение.

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

                                                            Пример на Питоне
                                                            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, что умудряется выдать результат хуже, чем синтетический тест.
                                                              –1
                                                              Вы создаете всего лишь 60000 объектов, этого мало. Проблемы с GC начинаются именно при исчерпании памяти.
                                                                0
                                                                Ка вы это насчитали? На каждом уровне создается 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 МБ памяти. Мало?
                                                                  0
                                                                  Извиняюсь, не в том порядке в степень возвел случайно.
                                                            0
                                                            Так ответы на эти вопросы есть в комментарии того чувака, который и предложил отключать 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.
                                                            +1
                                                            Вот отличная статья по теме: blog.ircmaxell.com/2014/12/what-about-garbage.html

                                                            Only users with full accounts can post comments. Log in, please.