21 ноября 2008 в 13:40

Функция echo в PHP может выполняться более 1 секунды

PHP*

Или об особенностях управления отдаваемым контентом в PHP.


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

Начну с того, что если бы мне такое сказали вчера, то я покрутил бы сам у этого человека пальцем у виска, однако серия проведенных тестов неумолимо свидетельствует об этом.



Генерация страницы: 1 секунда на мощном сервере и 200 мс. на слабом.


Всё началось с того, что я внедрил самописный профайлер в CakePHP фреймворк и встроил туда функцию подсчёта интервалов выполнения после основных логических частей кода. На локальном сервере всё работало хорошо, профайлёр показывал 200-300 мс., но на продакшене (сервере гораздо более мощном, на который мы ещё не нагнали посетителей) время выполнения было иногда 1-3 секунды!

Применив любимый способ отладки производительности:
  1. $microtime = microtime(true);
  2. // Некий участок
  3. echo (microtime(true) - $microtime);

выявил, что самая медленная конструкция это строка:
  1. print $out;

которая за раз пуляет всю страницу.

Дальнейший поиск по интернету показал, что я не одинок и проблема давно известна, и описана 6 лет назад в багах PHP. Согласно данному багу, проблема возникает при отправке за один раз слишком большого текстового блока, около 11-32 Киб.
На проблему влияет некий Nagle algorithm, который задерживает отправку пакета пользователю. Отключается который только при создании сокет-сервера, то есть в исходном коде Apache. Поэтому следующих два дня я потратил на конкретное изучение проблемы с целью понять причину и найти возможные варианты исправления.

Скрипт для обнаружения проблем.


Итак, согласно приведённому выше багу, я написал следующий тестовый скрипт:
  1. $index = !empty($_GET['index']) ? $_GET['index'] : 1;
  2. $example_output = str_repeat(str_repeat("*", 1024), $index);
  3.  
  4. $microtime = microtime(true)*1000;
  5. echo $example_output;
  6. $interval = microtime(true)*1000 - $microtime;
  7.  
  8. echo '<br>Display Length: ', $index, ' KiB.<br>';
  9.  
  10. if($interval < 100 && $_GET['index'] < 100)
  11.   echo '<meta http-equiv="refresh" content="1; url=?index='.($index + 1).'" />';
  12.  
  13. echo 'Reach end file: ', round($interval, 2), ' ms.'."<br>\n";

При запуске скрипта, он запрашивает одну и туже страницу в браузере, выводя в него каждый раз всё больший и больший блок бесполезных текстовых данных до тех пор, пока время выполнения функции echo меньше 100 мс.

Получаем интересный результат, скрипт выводит для блока в 11 Киб:
*****
Display Length: 11 KiB.
Reach end file: 0.07 ms.


а для блока в 12 Киб:
*****
Display Length: 12 KiB.
Reach end file: 348.92 ms.


При этом данная проблема не воспроизводится стабильно. Запускаем тот же скрипт с американской машинки — проблема начинает воспроизводиться с 13 Киб. Запускаем с канадской (там же где стоит сервер) — нет проблем при любом значении.

Дальнейшие эксперименты показали, что на значение 348.92 ms также влияет текущая загруженность интернета, ибо с американской машинки значениях хоть и большие, но в разы меньшие, чем с белорусской.

Отдача контента пользователю шаг за шагом.


Таким образом постепенно у меня сформировалась картина того, как происходит отдача контента в PHP.
Итак, когда нет никаких задержек:
PHP good output buffering
Обозначения схемы:
  • Зелёный — обработка
  • Жёлтый — ожидание
  • Красный — обмен данными
  • Синяя полоса — PHP shutdown интервал

Шаги:
1. Посетитель посылает запрос.
2. Запрос обрабатывается Apache`ем.
3. Начинается обработка запроса в PHP.
4. Выполняется echo, а затем весь оставшийся код в PHP файлах.
5. Параллельно Apache передаёт данные пользователю.
6. Начинается PHP shutdown интервал. Для начала вызываются функции, зарегистрированные через register_shutdown_function.
7. Затем вызываются все деструкторы. Происходит освобождение памяти от всех объектов.
8. PHP закрывает сессию пользователя (имеется ввиду автоматический вызов session_write_close).
9. Apache закрывает сокет.
10. Посетитель получает уведомление об окончании соединения.

В проблемной ситуации отдача контента происходит следующим образом:
PHP output buffering problems
У нас появляются следующие изменения в текущих:
4a. Выполняется echo. И ждёт сигнала с Apache.
4b. Apache передаёт данные пользователю, и пока не отправит всё, не происходит выхода из операции echo.
8a. PHP процесс отправляет все оставшиеся выходные потоки, если есть, а затем ждёт команды завершения от апача.
8b. Apache посылает все оставшуюся информацию.
8c. PHP закрывает сессию пользователя.

Таким образом на шаге 4 я и столкнулся с ситуацией, когда простейший вывод зависит от скорости передачи контента посетителю. При этом если мы обратим внимание на примерный график загрузки памяти, то PHP простаивает в момент когда он ещё не начинал глобальное освобождение ресурсов.

Об управлении выходным потоком.


Дальнейшее исследование особенностей выходного потока приводит нас к статье о настройках выходного потока PHP, а также к куче сообщений «умных дядек» на форумах, что ставьте output_buffering=200K и решайте таким образом все проблемы.

Но рассмотрим более детально, на что мы можем повлиять. Существуют следующие переменные конфигурации PHP:
  • output_buffering — буфер выходного потока.
  • output_handler — перенаправление выходного потока в функцию.
  • implicit_flush — принудительная посылка контента после каждой операции вывода.

Принудительная посылка нам ничего не даёт, поскольку у нас зависание в самой операции вывода. Да и перенаправление в функцию нам не нужно. А вот установка переменной output_buffering частично решает нашу проблему, поскольку мы переносим тормоза на PHP shutdown интервал, гарантируя при этом полное выполнение логики до.
Если ставить эту переменную в определенное значение:
php_value output_buffering 131072

то нужно подобрать такое значение, которое больше чем размер какой-либо страницы, что не удобно. Поэтому лучше позволить ей динамически подбирать размер:
php_flag output_buffering On


После применения этого «лекарства» имеем следующий график работы:
PHP output buffering problem solution
То есть, мы добились только следующего:
  • Зависание PHP происходит после отработки основной логики.
  • PHP в среднем расходует меньше памяти, поскольку в момент единственного простоя почти вся она освобождена.


Мнимая таблетка.


Неудовлетворённый результатом, я решил поискать ещё решения по данной проблеме и нашёл предложение бить вывод на блоки, каждый из которых выводить через echo. Обрадовавшись, я попробовал, и о чудо, для 11 Киб у меня исчезла полностью задержка на PHP стороне. Но к несчастью, при суммарной отдаче контента размером более 18 Киб она снова появилась и дальше опять уже не важно бъётся он на блоки или нет.

Итоги.


Начиная с 1984 люди мучаются с алгоритмом Nagle, данная проблема не обошла и PHP и пока не видно способа её решения. Можно только немного минимизировать потери, в случае, если она у вас воспроизводится.

Послесловие или коллективный разум решает.


Спасибо все хабраюзерам за реакцию на данную статью, это помогло понять в этой проблеме ещё один момент и осознать некоторые описанные заблуждения.
Для начала всем кто пытался воспроизвести и не смог. Я уже научился воспроизводить локально без проблем. Для этого мой тестовый пример записываем в файл, а затем используя wget, качаем медленно кусок размером в 128Киб и наслаждаемся.
wget --limit-rate=1K www.test.lo/nagle_test.php?index=128
Display Length: 128 KiB.
Reach end file: 57234.61 ms.


А теперь работа над ошибками, которая возникла у меня после чтения коментариев и написания этого последнего теста:
  • Алгоритм Nagle`а тут действительно не причём, поскольку он используется при малых объёмах данных, кроме того для модуля prefork всегда стоит значение TCP_NODELAY, которое и означает, что он всегда выключен. Возможно, если бы он был включен, то у нас могли возникать проблемы, например:
    echo '*';
    flush();

    не возвращало бы в броузер ничего
    там
  • Cуть задержек состоит в том, что Apache не возвращает управление при операциях вывода PHP, поскольку имеет небольшой промежуточный буфер для пересылки данных и, кстати, вовсе не обязан делать его большим, поэтому он и забирает данные по чуть-чуть не позволяя продолжать работу.
  • Решать данную проблему надо, поскольку мы минимизируем среднее время жизни PHP, что позволяет нам обрабатывать одновременно большее число пользователей. Естественно без фанатизма, на проектах с не очень большой загрузкой это решать не надо.
  • Решать данную проблему можно настраивая софтверный лоад балансер или реверс-прокси с помощью Apach, nginx, lighttpd. Нужно только не забыть почитать документацию о размерах буферов. Решать данную проблему правильно так, поскольку мы минимизируем нагрузку на сервера, на которых производится вычисление.
  • Использовать output_buffering = On не всегда правильно, поскольку мы можем отдавать файл, и нам в этом случае не важны задержки в вычислениях, поскольку основная наша задача — отдать файл. К тому же в этом случае файл загружается в память, что тоже плохо.
  • Есть вещи «оттягиващие» наступление проблемы. Это во первых: — бить отдаваемый контент по 8Киб (не знаю почему, но помогает). А во-вторых: использовать сжатие gzip. Хотя предпочитаю управлять способом отдачи на стороне веб-сервера, но тем не менее.
  • Из самых простых решений — установить sendbuffersize в настройках Apache, но применить эту настройку можно, к сожалению, только полностью перезапустив веб-сервер и влияет она на все хосты.
Андрей Нехайчик @gnomeby
карма
43,2
рейтинг 0,0
Самое читаемое Разработка

Комментарии (161)

  • +1
    Так и на чем остановились? Бить на блоки?

    PS: у вас meMMory, вместо meMory
    • +3
      Спасибо, подправил.

      Нет, пока на: php_flag output_buffering = On
      Чтобы основная логика обрабатывась без тормозов
      • +2
        спасибо, интересно было прочесть.
        Но правильный синтаксис для директивы все-таки по-моему
        php_flag output_buffering On, а не
        php_flag output_buffering = On
  • +1
    Нипанимаю насчёт мнимой таблетки. Кусок-то меньше 18 кибов? В чём возникла проблема?
    • +2
      Ах, да.
      В том что при применении «мнимой таблетки» проблема решается только при суммарной отдаче не более 18 Киб. в моём случае.
      • 0
        суммарного echo?
        то есть 4 echo, каждый отдаёт по 5 кибов — это уже затык?
        • 0
          Да, на 5-м.
    • 0
      исправил, спасибо.
  • +7
    Прочитав:
    функции echo и print на самом деле могут работать очень долго и их производительность зависит от качества интернета конечного пользователя

    Сначала даже не поверил своим глазам. Потом решил почитать чуть-чуть, и зачитался. Спасибо.
    • +4
      Ни что иное, как проблема медленного клиента.
      Попробуйте файлик размером в один мегабайт отдать через readfile(), это может занять минут пять-десять через GPRS.
  • +6
    А не пробовали воспользоваться парой ob_start(); ob_flush();?
    • 0
      Тоже интересует, почему не пробовали.
    • 0
      В CakePHP всё кроме последнего вывода примерно так и оборачивается. Просто данной конструкцией мы переносим проблему в конец.
  • +24
    А теперь попробуй поставить nginx перед apache и повторить тест
    • +3
      следующим шагом будет убрать apache оставить nginx :)
    • –2
      При фиксированном размере буфера — ничего не изменится.
      • +5
        Изменится еще как, apache все максимально быстро сбросит на nginx и он уже будет отдавать контент медленному клиенту одним легким тредом, а тяжелый процесс апача будет уже готов обслуживать нового клиента.
        • –4
          «apache все максимально быстро сбросит на nginx»
          Ха. И еще раз ха.

          Угадайте, где теперь буфер? Правильно, на nginx. Если такой же буфер выделить для самого PHP, то будет то же самое. Единственное, что PHP сможет полностью закрыться, а не оставаться в фазе закрытия, что позволит сэкономить еще немного памяти.

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

          PS: Хотя конечно nginx для непосредственной отдачи намного лучше апача в отношении использования ресурсов.
          • +7
            Сейчас подумал, что в принципе в nginx буфер по реализации все же намного лучше. Так что даже при равных размерах буферов он может сильно выигрывать.
          • +2
            > Хотя конечно nginx для непосредственной отдачи намного лучше апача в отношении использования ресурсов

            Так вот именно в этом-то и дело. Чтобы не быть голословным, приведу пример. Есть маленький VPS с 128 Мб RAM, в памяти висят сейчас 4 процесса апача (апач конечно же в prefork mode, т.к. mod_php). 3 процесса жрут каждый по 12% RAM, четвертый 5% (мастер видимо), согласно top. Nginx жрет всего 0,6% RAM. Если будет много запросов к апачу, с которыми не справятся уже поднятые 3 процесса (будут ждать медленных клиентов в этот момент), то апач начнет поднимать новые процессы, каждый из которых тоже быстро наберет жирок и тоже будет занимать около 12% памяти. В моем случае это все грозит залезанием в своп и просто жуткими тормозами.

            Буфер конечно никуда не денется, он будет в nginx, но буфер это ерунда по сравнению с новым процессом апача со всем его модулями.
            • 0
              А еще на OpenVZ VPS, нет свопа и дело принимает совсем печальный оборот
            • 0
              mod_php нормально работает и в worker-е (другой вопрос, падение дочернего со связкой тредов...).

              Но лично я даже с worker-а ушел, т.к. при nginx+php-fpm мы совсем избавляемся от apache.
    • –12
      А после этого попробуй выкинуть PHP и поставить Python.
      • 0
        Это слишком радикально :)
        • –3
          Это слишком умно, чтобы современное поколение Хабра на это пошло, судя по оценкам.
          • +1
            Да нет, просто статья то о PHP… Я вот свой плюсик поставил…
          • 0
            это даже очень слишком мега умно… интересно, кто первый здесь напишет rubyonrails, java или с++…

            а что, mod_python как-то по-другому работает? автор какбе «в курсе», или автор просто хотел сказать что он реально «серьёзный разработчик»?
            • +1
              Обычно веб-приложения на Python развертывают через FastCGI или WSGI. В связке с lighttpd или nginx. Apache отсутствует как факт.
              • +1
                ничто не мешает точно так же запускать и php… от этого мой вопрос к автору не поменялся
              • +2
                Аналогично, например, у меня на сервере в связке lighttpd + php-fcgi Апач отсутствует как факт.

                При чём тут языки?
            • 0
              mod_python никто не использует для нормального деплоя. только mod_wsgi. Учите матчасть.
              • 0
                вы мне на вопрос ответьте, а не давайте свои мега-советы про матчасть.

                здесь какбе речь идет про mod_php, появляетесь вы и говорите очень умную везчъ «выкинуть PHP и поставить Python», после чего доносите до нас знания про mod_wsgi.

                даже не знаю… «скажи наркотикам нет...»
    • 0
      Мы нгинксом только и спасаемся от таких проблем :)
  • 0
    Встречался с подобным. Делал так:
    • –9
      print $out;
      flush();
      • +3
        ну и как это поможет если тормозить будет перед flush?
        • +2
          может быть так?
          ob_start();
          print $out;
          ob_flush();

          тогда print всё отдаст быстро, а тормозить будет уже flush :)
          • 0
            Вроде того, я думаю.
          • 0
            Ну да. Просто у меня весь вывод из скрипта накапливается в $out, а не выводится сразу. Поэтому и использую flush()
            • 0
              даже и не знаю, как у меня выводится, рендер всё за меня делает, а в небольших «одностраничных» рабочих скриптах это и не важно, там вывод всё равно в логи идёт
  • +1
    Спасибо. Интересная статья. Нужно будет протестировать свой код подобным образом.
  • +14
    Вот потому-то и нужен проксирующий сервер
    • +2
      за что минусят этого комментатора? Он совершенно правильно говорит. Поэтому люди и спаривают apache с реверс-проксей на nginx
  • 0
    Возникала у меня такая проблема, echo больше секунды выполнялся. Решил её gzip-сжатием отдаваемого контента.
    • 0
      Это все круто, если бы все браузеры нормально это понимали.
      • +1
        хм, всегда удивляло такое примечание. а какие браузеры не понимают заgzipованую страницу?
        • –1
          Не поверишь, IE6 )))
          • 0
            Нормально он понимает. Бывают некоторые проблемы, но уж на слишком экзотических задачах. На обычном сайте все работает нормально.
            • 0
              support.microsoft.com/default.aspx?scid=kb;en-us;823386&Product=ie600 я бы не называл это «некоторыми проблемами в экзотических задачах»…

              When Microsoft Internet Explorer receives compressed HTTP data, you may experience one of the following problems:

              — HTML pages may only partially appear, or the pages may not appear at all.
              — Your HTTP connection may stop responding.

              Саuse: This behavior occurs because Internet Explorer does not read all the data in the HTTP response.
              • 0
                Возможно, я ошибаюсь и очень сильно. Если у вас есть рабочий IE6 под рукой, не смогли бы вы один сайтик проверить? У меня deflate включен для всех браузеров, без указания т. н. «старых».
                • +1
                  Давайте ссылку — на работе стоит Multiple IE, в пн проверю… Дома виста, поэтому тулзу не поставить толком :(
                  • +1
                    Для висты существует подобное:
                    www.my-debugbar.com/wiki/IETester/HomePage
                    • 0
                      Спасибо. Я как раз ей и пользуюсь. К сожалению, там нет возможности смотреть http-заголовки. Чисто визуально в этой программе IE6 на моем сайте прекрасно со сжатием работает.
                    • 0
                      спасибо!
                  • 0
                    Оправил вам в личку. :)
                • 0
                  Советую поставить Multiple IE, очень удобно.
        • 0
          у меня safari не понимает gzip контент.
          • 0
            быть того не может… Сжатие DEFLATE он точно поддерживает, значит и GZIP тоже должен. Посмотрите Ваши настройки.
          • 0
            Неужели у вас настолько старая версия сафари? :)
          • 0
            Подумал еще разок — может тот скрипт, что отдает GZIP просто не ставит соответствующие заголовки Content-Encoding: gzip? Это тупо конечно, но другой причины я реально не вижу, по крайней мере с серверной стороны.
            • 0
              Так вы посмотрите, что на клиент приходит.
              • 0
                Дык эт вы не мне пишите ))) У меня то все ок, только что проверял Safari 3
                • 0
                  Тьфу :)
      • +2
        Браузеры, обычно, об этом сообщают через Accept-type
  • 0
    Не знал. Большое спасибо! Нада будет заняться тестированием.
  • –34
    это же надо было так долго и путанно объяснять, что вода мокрая и 2х2=4…
    статья из серии — есть клюнуть себя в жопу — будет больно
  • 0
    молодец автор, расписал красиво.
  • +2
    некий Nagle aLgorithm
  • 0
    что если выводить контент таким образом: ?>контент контент контент<?

    • +1
      я не понимаю зачем минуснули. дело в том что как я понимаю проблема не именно в функции echo или print. Данная ситуация должна происходить при любой отправке контента клиенту
  • 0
    ob_start
  • +21
    Не понимаю нападок на «Nagle's algorithm». Очень уж отдаленно он связан с проблемой. Его суть сводится лишь к тому, что не посылаются пакеты слишком малого размера, а чуть чуть придерживаются, пока не наберется большой пакет. И всё.

    У вас же проблема прямо-таки противоположная — вы хотите сразу послать много больших пакетов, да вот только клиент их не может принять. А пока он пакеты не принял, вам их нужно где-то хранить. Где? Правильно в буфере вывода PHP. Да вот только буфер тоже не резиновый (по умолчанию). Как следствие — приходится полностью останавливать PHP, чтобы он нам не переполнял этот буфер. Последствия вы уже описали.

    Проблема вызвана не использованием накопления пакетов («Nagle's algorithm»), а тем что мы не можем мгновенно перебросить клиенту всё, что нагенерировал наш PHP код. И эта проблема как была так и останется. Всё что можно с ней разумного сделать — выставить размер буфера в соответствие с размером страниц сайта.

    PS: Выставлять неограниченный буфер — на самом деле довольно плохая идея. Буфер должен быть большим, но не резиновым.
    • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        Кстати, в комментариях к статье, на которую ссылается автор, тоже высказывают непонимание относительно того, при чем тут этот алгоритм. Кстати, там есть отсылка к его спецификации.
    • 0
      Суть, как я понял, в том, что РНР притормаживает при отдаче print'ов и пока не отдаст их юзеру дальше не выполняется, так?

      А если, допустим, вначале скрипта поставить размер буфера на 30-40К, включить ob_start и держать его до самого конца ничего не отдавая, и отдать уже в самом конце одним куском в самом-самом конце работы — решит ли это проблему?
      • –1
        Я вот даже как-то не понял — а в чем проблема-то? Юзер все равно не увидит конечный результат раньше чем страничка прокачается через его соединение. Соответственно, мнимая задержка на echo никакого влияния на конечный результат не окажет.
        • 0
          Логично предположить, что когда буфер забит, а запись в него все идет и идет, то РНР начинает его отправлять, и ждет пока юзер его не заберет, соответственно ничего нового туда не будет записано. Но это мои догадки…
          • 0
            Нет, там дополнительная задержка совсем незначительна. Проявиться это может только тогда, когда у нас не только отдается много, но это «много» еще и очень медленно генерируется. А это уже скорее вопрос оптимизации скриптов.
        • +2
          Проблема в том, что пока не закончил работать PHP — под него зарезервировано значительное количество памяти. А на серверах именно память и загрузка процессоров — основной ресурс.
          • 0
            Загрузка проца будет равна нулю, процесс apache будет висеть на ожидании возможности записи в декриптор сокета.
            Память — да, но скорее всего это будут копейки. Ну провисят они лишние 300ms в памяти, ну и что?
            • 0
              А если клиент будет тянуть страницу по 10 минут? :)
            • 0
              А если 10 таких запросов? 3 секунды провисания… Многовато…
            • 0
              В большинстве случаев, да. Но когда 9 серверов обслуживают 500000 посетителей в день, а в моменты рождественнских акций нагрузка зашкаливает и сервера еле справляются возможно это сможет помочь пользователям получать инфу чуть быстрее. А это приятно.
      • +1
        Да.

        Если у вас, страница влазит в буфер — то разницы не будет: отдавать сразу или одним куском в конце. Точнее будет разница на время генерации страницы. Если страница в буфер не влезла, то в PHP останавливается, пока не будет отдано всё, что до этого нагенерилось. В любом из двух случаев накопление контента и отдача его одним куском ситуацию не улучшит. Но и не ухудшит сильно.

        Основной метод решения проблемы — минимизация передаваемого объема. Так что Tidy и Gzip — наше всё :)
        • 0
          GZIP об IE6 споткнётся, а так у меня на сайтах так и делается — сначала все в буфер, потом если есть возможность — в GZIP или DEFLATE, в результате имеем

          Сжатие DEFLATE: 8.84 КБ → 2.78 КБ (68,6%)
          Полное время выполнения: 0,051 сек.
          • 0
            Как насчет того, чтобы проверять заголовки, которые пришли клиента? Если клиент говорит, что скушает gzip — кормить gzp'ом, отказался от gzip'а, но сказал, что ест deflate, то deflate'ом :)
            • 0
              Я как раз так и делаю :)

              if(isset($_SERVER['HTTP_ACCEPT_ENCODING'])) $acceptEnc = $_SERVER['HTTP_ACCEPT_ENCODING']; else $acceptEnc = $_SERVER['HTTP_TE'];
              $_SERVER[cmsGZIP] = array(
              «enabled» => (stristr($acceptEnc, 'gzip') || stristr($acceptEnc, 'deflate'))? true: false,
              «algorythm» => (stristr($acceptEnc, 'deflate'))? «deflate»: «gzip»,
              );
              • 0
                лучше через Apache — там еще Cache-Control: private нужно выставлять
          • +1
            IE6 поддерживает gzip
            • 0
              Я вообще то про то же
        • 0
          Долбаный CTRL+ENTER :)

          А насчет TIDY я еще не думал честно говоря… Кстати есть мнение, что нужно не только JS, но и CSS и HTML отдавать в «minified» виде. Не удобно при разработке, зато после публикования можно смело врубать.
          • 0
            Почему не удобно в разработке? При разработке храним всё форматированным, со всеми отступами для удобства. А при выставлении на продакшн настраиваем сжатие всего и вся. Не вижу проблемы.
            • 0
              Я имел ввиду, что на этапе разработки не надо включать minify ибо разрабатывать будет неудобно. «зато после публикования можно смело врубать»
              • 0
                Ну, это и так понятно :)
          • 0
            Ну css и js кешируются, вообще говоря.
            Другое дело, если вы в странице отдаете стили и код JS. Так что много все-равно не выиграете.
      • +1
        Проблема в том, что пока PHP не отдаст апачу весь контент, который он должен отдать, скрипт не завершается. А апач в некоторых случаях не торопится этот контент принять, как считает автор. Я честно говоря это себе по другому немного представлял, но автор такие графики красивые нарисовал, что наверное стоит ему поверить.

        ob_start тут никак не поможет, т.к. автор и так отдает контент в скрипте одним большим куском в конце. Поможет тут, как уже несколько раз сказали выше, установка reverse proxy (nginx или lighthttpd).
        • 0
          Я имел ввиду, что надо отдавать в самом конце, после всей логики. Чтобы после тяжелого принта не было выполнения. Т.к. пока принт не выполнится, до того, что за ним дело не дойдет. Нам то важно, чтобы скрипт быстро отработал и отдал данные — тогда перед принтом можно и соединения позакрывать и память почистить.
          • 0
            Если в этом смысле, то в принципе да, проблему неэффективного использования ресурсов это решает. Правда в таком случае придется потратить веремя на всю эту чистку памяти и проверку, да и все же всё вы не вычистите. Проще передать это всё из PHP во внешний по отношению к нему буфер, а его (PHP) благополучно освободить.
            • 0
              Да, по-хорошему так и надо. Но если брать в расчет шаред-хостинг, то становится тоскливо :(
              • 0
                А в чем проблема найти шаред за ngix + Apache + что вам там из БД надо?
                • 0
                  Ну я как-то больше привык сам настраивать :) потому что хрен его знает что там админы намутили… Кстати есть какие-то хорошие хостеры на примете? (лучше в личку наверно)
    • 0
      Да и вообще не очень понятно причём здесь PHP, apache, nginx (и, как справедливо замечено, Nagle's algorithm).

      Неумеренность в буферах может привести к меньшей устойчивости к DDoS'y (условно). Впрочем, это требует исследования, на конкретных «попугаях» — куда будет быстрей расходоваться память — на создание новой копии интерпретатора или на буферизацию. Точнее отношение этих объёмов. Может быть создание копии интерпретатора потребляет памяти значительно больше, чем выделяется под буферы. Поэтому эффект от увеличения буфера может быть, в целом, положительным.
      • 0
        Чтобы сайт был стоек к DDoS вероятно лучше пользоваться FastCGI, но только не с PHP, а допустим с Perl. Потому что перл песочницу создает однократно и надо только будет следить, чтоб не было утечек памяти, а РНР будет все равно запускаться каждый раз с нуля.

        В большинстве сайтов, которые мне довелось видеть, всяческие autoexec и фреймворки грузятся зачастую дольше, чем сам скрипт работает ;). Поэтому если каждый раз экономить по 50-100 мс на подготовке, то за 100 запросов мы получим нехилую экономию. Даже при учете, что допустим сам скрипт работает еще столько же.
    • 0
      Вопрос не столько по вопросу, столько по практике, просто хочется понять решение этого вопроса.
      Если например у меня вывод происходит в самый последний момент исполнения, соотв будет только больше нужного использоваться память, но не процессор, т.е. будет ожидание пока буффер освободиться чтобы все остальное завершить?

      Т.е. всю работу скрипта вызываются классы, и т.д. и т.п., происходит рендеринг страницы, а далее последней команды идет принт того что отрендерено — это лучше или хуже?
      • 0
        Да.

        Почти всегда это хуже, если используется только ради накопления (а не для какого-либо специального сжатия). Но вот насколько — зависит от нескольких факторов. Основной — соотношение времени работы скрипта и времени отдачи результата. В подавляющем большинстве случае (когда время работы скрипта — десятые секунды и меньше) — разницей можно пренебречь.
    • 0
      Про Nagle algorithm вообще спор бесполезный, в Apache он всегда выключен за ненадобностью
      • 0
        Да при современных каналах даже 1 байт ценной инфы может обрамляться 40 байтами заголовков и не страшно… лишь бы дошел.
        • 0
          По большому счету да. Терминальные приложения сейчас составляют малый процент траффика, а программам передающим данные большими блоками nagle-алгоритм только мешает. Хотя раз держат его включенным по умолчанию в ОС значит чем-то это оправдано (надеюсь, не только историческими соображениями ;-)
    • 0
      Именно к тому, что вы написали я постепенно и пришёл. Всё верно, добавил в статью апдейт.
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Именно. Он касается обработки малых пакетов, а не больших.
    • 0
      в apache nagle алгоритм выключен
    • 0
      Ничего оно не ухудшит. Почитайте википедию — там доступно разъяснено, что нужен он когда надо переслать 1 байт, а заголовков при этом на 40. Это довольно расточительно, поэтому пакет собирается по кускам и отправляется за один заход.

      А у нас наоборот пакет здоровый.
      • НЛО прилетело и опубликовало эту надпись здесь
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      никто не мешает вызывать echo($str) или print($str) — не придирайтесь ;)
      • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      По данным php.net
      Справочник функций — Text Processing — Функции работы со строками — echo
      Но пишут «На самом деле echo() — это не функция, а конструкция языка, поэтому заключать аргументы в скобки не обязательно, даже при использовании нескольких аргументов». Я в шоке :)
      • 0
        Просто по правилам — это конструкция языка, поскольку имеет не все свойства функции. Но для упрощения никто не мешает называть её как функцию. Если быть слишком концептуальным и вводить новое понятие в мануал, то это только запутает начинающих программистов.
  • 0
    протстил на нескольких серверах, проблемы такой не разглядел. Даже если сильно увеличть длинну вывода. Браузер виснет, а время отдачи не больше 10мс. Странно.
    • 0
      Какой размер буфера указан для PHP?
  • 0
    Надо тюнить tcp_wmem, php и apache тут не при чем.
    • 0
      Почитал описание tcp_wmem. Весьма похоже на правду, что это действительно может быть основным решением.
      • 0
        Да вариантов как-бы и нет ;-)
        php делает echo, через всякие ob_извращения дело доходит до sapi->ub_write(), который в случае с mod_php равен php_apache_sapi_ub_write. Там вызывается ap_rwrite() в блокирующийся сокет. Если памяти в буфере недостаточно — ядро блокирует вызов пока вся запись не пройдет.
        Можно подкрутить SendBufferSize, по идее результат должен быть такой же.
  • 0
    php 5.2.6
    output_buffering 4096

    Display Length: 28 KiB.
    Reach end file: 0.12 ms.

    не вижу проблем — что я делаю не так? как ощутить эту проблему?
    • 0
      Читайте апдейт к статье, там указано как воспроизводить даже в локали
      • 0
        я пробовал в локали и на 4-х серверах — все работает нормально — воспроизвести не удалось
        • 0
          Хм. А у вас output_buffering выключен? Ибо данный тестовый пример не имеет смысла если он включен. И попробуйте ещё ограничить скорост скачки.
  • 0
    5.2.6 Буфер выключен.

    Display Length: 1304 KiB.
    Reach end file: 1.03 ms.
    • 0
      Читайте апдейт к статье, там указано как воспроизводить даже в локали.
      • 0
        Дык вы на цифры посмотрите… я метр прокачивал уже, а глюка нет…
        • 0
          ну так, если включен output_buffering или используете реверс прокси, то глюка может и не быть. И в принципе это хорошо.
          • 0
            «Буфер выключен.» прокси нет.
  • 0
    осветите вопрос с gzip: сейчас непонятно, помогает или нет он
    • 0
      Да, помогает. Меньший нужен буфер.
      • 0
        супер, буду пропагандировать gzip дальше :)
        • 0
          Флаг в руки. Я обеими руками за. А майкрософту надо оторвать кой-чего за то, что IE6 GZIP'у не обучен. Имейте ввиду и проверяйте заголовки, я выше писал как.
      • 0
        Кроме того, канал держится меньше времени.
  • –1
    Автор такой трактат накатал

    ob_start как же?
  • +5
    Nagle-алгоритм был придуман для программ вроде telnet, в которых нажатие клавиши пользователем приводит к вызову системного вызова write(), приводящего к отсылке пакета с данными. Чтобы в сеть не летело много мелких пакетов, nagle-алгоритм немного притормаживает передачу дабы собрать идущие подряд вызовы write() в один пакет.

    Приложения которые умеют нормально буферизовать вывод (а не дергают write на каждый выводимый байт) выключают nagle алгоритм путем выставления опции сокета TCP_NODELAY. При этом каждый write приводит немедленной отсылке пакета без задержек. В apache такая опция на сокете стоит практически от рождения.

    Однако единственный побочный эффект, который может вызвать включенный Nagle — увеличение времени отклика сервера. К вашей проблеме это имеет весьма отдаленное отношение.
  • +5
    Проблема которую вы на самом деле пытаетесь решить вот в чем: пока пользователь скачивает страничку с сервера, он занимает его ресурсы. Если вы его «кормите» выводом скрипта, то без буферизации скорость работы скрипта будет ограничена тем, с какой скоростью пользователь готов «есть» генерируемые им данные.

    Буферизация позволяет освободить дорогие ресурсы сервера (соединения к БД, треды выполняющие PHP-запросы и т.п.) за счет более дешевых ресурсов вроде оперативной памяти. Благодаря буферизации скорость исполнения скрипта не ограничивается скоростью клиента. При этом буферизацию лучше выполнять не средствами PHP, а в веб-сервере nginx/lighttpd. За счет своей архитектуры они способны «кормить» буферизованными данными тысячи медленных пользователей практически не занимая системных ресурсов в момент ожидания готовности пользователя (главное чтобы памяти хватило). Apache подходит хуже т.к. на одного пользователя занимает тред/процесс вместе с довольно дорогим контекстом.

    Так что либо надо жертвовать памятью, либо отказываться от скриптов которые генерируют слишком большие страницы.
  • +2
    Вот блин, все б так статьи писали и оформляли. Я не программирую на php — и то зачитался.
    Спасибо
  • 0
    хорошая статья, молодец автор, подробно и доступно, с примерами.
  • 0
    Отличная статья. Я даже и не подозревал о таких проблемах :) Судя по комментариям проблема неплохо решается реверс-прокси в виде nginx?
    • 0
      Да, можно и так.
  • 0
    Думал щась начнется баттл принт ето функция а ехо конструкция аннет
    Спасибо автор очень занимательно и полезно
  • +1
    dklab.ru/chicken/nablas/50.html Обьяснение проблемы медленных клиентов и её решение

    З.Ы. на теги кармы не хватает =(
  • +1
    Для профилирования очень рекомендую xdebug :) Это намного удобнее, чем ручные измерения. Получаются прекрасные и информативные отчёты, какие методы и функции сколько чего жрут:

    www.xdebug.org/docs/profiler

    А с идеологической точки зрения, всё же, рекомендую во-первых, буферирование выхода, во-вторых, переход к идеологии «в проекте должно быть только одно echo — вывод результата».

    Ну и сам сайт хорошо бы проектировать так, чтобы динамика отдавалась небольшими кусками. А всё толстое — статикой или иными решениями, не PHP. Например, тем же mod_cml на lighttpd.
    • 0
      Да, я знаю xdebug, спасибо. Просто формат статей вообще препологает простые и доступные примеры. У меня есть тестовые скрипты, который меряет более правильный чем приведённый, просто не хотелось простыню постить.
      Идеологии поддерживаю на 100%.
      • 0
        >Просто формат статей вообще препологает простые и доступные примеры.

        Так можно было просто написать: «профилирование с помощью xdebug показало, что echo выполняет N секунд» :)
        • 0
          А как же самому попробывать? :-) в этом и суть, чтобы любой смог затестить за 30 секунд без необходимых затрат.
  • +1
    Вообще-то умные дядьки давно придумали, что сначала надо отработать ВСЮ логику, а потом нарисовать HTML. MVC там, все дела…
    • 0
      Вы невнимательны, я писал, что использую CakePHP, где всё так и происходит.
      • –1
        С CakePHP лично не знаком, так что подробностей не знаю. А если так, то в чём проблема? После финального echo кода уже не должно быть, движок должен зашатдаунен и наступить полное счастье. Ну а если есть косяки со стороны веб-сервера, то PHP тут уже ни при чём.

        Я не против статьи как таковой, статья полезная ибо обращает внимание на старый подводный камень о котором многие на самом деле не знают.
  • 0
    а почему бы не выводить всё одним echo в самом конце скрипта?
    • 0
      Там же написано почему)
  • +1
    Статья просто суппер! Все так подробно, с графиками и диаграмами, большое спасибо автору за старание!
  • 0
    помоему проблема решается установкой кеширующего сервера.
  • +1
    я бы автору медаль за подробный разбор дал. спасибо.
    p.s. давно перешёл на nginx + apache, ессно с gzip кто поддерживает.
  • 0
    Я может где-то что-то упустил, но какая версия PHP использовалась? Какой Apache и тестировалось ли это дело из коммандной строки?
    • 0
      Проблема при взаимодействии с веб-сервером. Версии окружения уже не важны. Важно только их настройка, но об этом подробнее в статье и в коментариях.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.