Предыстория


image Есть сайт на Laravel с посещаемостью real-time в 700-1000 человек. Ранее сайт использовал чат стороннего разработчика. Он использовал WebSockets.

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

Выбор технологии

Многие скажут, что сейчас уже никто не использует старые браузеры типа Опера12 или ИЕ8, однако таких людей пока еще довольно таки много, поэтому решено было выбрать именно Long Polling.

Изначально сайт был размещен на Shared hosting, из-за чего был огромный ряд ограничений. Тем не менее, хостинг справлялся с нагрузкой. Это радовало. И единственной возможной реализацией чата был long polling на PHP.

Реализация


От слов к делу

image


Принцип понятен и прост. Аяксом посылается запрос на сервер, который считывает сообщения. Длительность запроса, а точнее ответ от сервера, зависит от того, добавил ли кто-то сообщение. То есть, после запроса запускается цикл, который проверяет, нет ли новых сообщений, и если есть новое сообщение — цикл прерывается, возвращается ответ с новым сообщением. После получения ответа запрос повторяется. И так далее.

Реализовать такую штуку на PHP проще не куда. Я потратил на реализацию в сумме около 3х часов своей жизни.

Проблемы

Тем не менее, оставил его в стадии MVP (Minimum viable product). И, как оказалось, не зря. При теситровании функционала на продакшн сайт просто падал. Оперативка заполнялась в течение 1-2 минут и гасила сервер.

Подумав немного, я решил, что дело в сервере. И спустя некоторое время сайт был перенесен на VPS. Скажу сразу, что конфигурация слабенькая, и оперативки всего 1Гб. Тем не менее, на VPS чат держался 3-4минуты. Уже лучше.

В итоге, после длительных раскопок интернета я понял, что писать чат на PHP — бессмысленно, если нагрузка на него будет больше, чем 10 человек.

Осталось 2 варианта:

  1. Ставить nodeJS и писать чат на веб-сокетах
  2. Писать сервис на чем-то другом

И как вы думаете, что я выбрал? Конечно же второй вариант, иначе бы не писал эту статью.

imageПоскольку C++\C реализаций серверных приложений не так уж много, тем более кросс-платформенных, и тем более, что я не силен в Сях, я выбрал Lazarus. Хорошо, что у него есть такой компонент как FPHTTPServer и пример реализации приложения.
Оговорюсь еще о том, что у меня нет под рукой Linux. Все приложения я делаю на винде. Я просто был уверен что перекомпилировать приложение на другой системе — это раз плюнуть. Ведь так же гласит девиз! «Write once — compile everywhere!». Впрочем, мне это удавалось, когда на моем ноуте было 2 системы (win7 + linux mint).

Скомпилировав приложение на win, я проверил функцонал локально. Все работало как часы. Тем не менее, это был простой exe-шник и хотелось что-то больше — написать нормальный сервис типа apache. Lazarus — это вам не Delphi, хоть и очень похож. Во-первых он бесплатный, что автоматически вызывает подозрение относительно качества продукта. А во-вторых,… впрочем как и в-третьих и еще н-ное количество — все неудобства связаны с этим. Большинство вопросов на форуме лазаруса так и остаются без ответа. Это очень печально. Так же как и отладка приложения с выскакивающей ошибкой типа «uncatchable error». Но, к счастью по окончанию недельного срока — я завершил аналог того функционала который был написан на PHP за 3 часа.

Довольный как слон, я решил скомпилить приложение на линуксе. Залогинившись под root на сервер и выполнив команду apt-get-install lazarus — я быстро поставил лазаря, скопировал исходники и, с помощью отрытых ранее в интернете команд lazbuild -r project.lpi, скомпилировал приложение. Это был единственный, поистине, самый удачный и быстрый этап.

Проверив работу чата уже на реальной нагрузке, я понял, что моя идея была оправдана. Чат работал, нагрузка на сервер увеличилась не намного. Это радовало. Теперь я мог вернуться на винду и продолжить работу над улучшенной версией — сервис (daemon). Как и следовало ожидать — на винде все получилось, хоть и с небольшой задержкой по времени.

image


На линуксе проект не скомпилился. При компиляции выдавалась ошибка о том, что не найден юнит Interfaces. Это было очень странно. На форуме лазаря внятного ответа так и не появилось. Многие лишь писали о том, что при переустановке все заработало. Однако переустановка не помогала. В конце-концов я понял, что моя версия лазаря отличается от той, которая стоит на линуксе. Обновить репозиторий не получилось. Точнее репозиторий обновился, а вот лазарь так и остался прежней версии.

Тогда я попытался скачать установочный пакет с помощью wget. В результате вылезла ошибка 177 (точный номер уже не помню). После очередного выплеска эмоций я понял, что проблема в каком-то символе в ссылке. Я перелил установочный пакет на MS OneDrive и создал короткую ссылку, после чего успешно скачал файл.

Далее я удалил старый лазарус и поставил новую версию. И как вы думаете, что сообщил компилятор? Конечно же! Непонятные ошибки!

Опираясь на прошлый горький опыт с поиском ответов в интернете я начал изучать конфиги лазаря. Проблема оказалась проста — при установке лазарус почему-то не создал новую папку с конфигами. Папочка осталась от предыдущей версии, хотя я удалял старый лазарус с помощью apt-get remove --purge. Ну что ж, я забекапил старые конфиги, удалил папку и снова попытался перекомпилить Lazarus IDE. Магическим образом, после этого папка с конфигами создалась и я смог наконец скомпилировать даймон для линукс.

Победа! Или нет?

imageСервисное приложение успешно скомпилировалось, но как же его теперь запустить?

Как и следовало ожидать — простой скрипт для создания записи в service start… не сработал. Более того, я опять же нашел посты на форуме с моей проблемой без ответа.

Поковыряв еще часик интернет, я нашел как можно запустить сервис с помощью start-stop-daemon. В итоге я понял, что код скрипта запуска, который выложили в примерах лазаруса — работает, но только если запускать его из терминала. В противном случае он выдавал что-то типа «не найден какой-то файл».

Финиш

Проблему с запуском\остановкой даймона через service я так и не решил. Тем не менее, остался доволен проделанной работой. Тем более, что чат смог выдержать 1000 соединений в течение 4х часов. К сожалению, конфигурация сервера не позволила ему работать дольше. Однако эту проблему я решил с помощью кнопочки «показать чат». В результате нагрузку на чат давали только те пользователи, которым он реально нужен.

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


  1. FZambia
    22.01.2016 10:51
    +3

    Привет, 1000 человек онлайн это совсем не много — например у нас Centrifugo отъедает только 1% одного ядра CPU и 200мб памяти с 1к пользователями онлайн. Так что та же Центрифуга или dklab_realplexor, который часто упоминается в постах о PHP (в том числе и том, на который вы давали ссылку в тексте статьи), могут стать для вас лучшим выбором в конечном итоге.


    1. AlexSlipknot
      22.01.2016 11:02

      Да, согласен. Но статьей я хотел больше доказать, что любой человек, который знает какой-нибудь язык программирования (не PHP) сможет написать свое highload-приложение. За Центрифугу спасибо :) Если будет возможность — попробую. Тем не менее, выше я упомянул, что вебсокеты как вариант отпали.


      1. FZambia
        22.01.2016 11:13
        +1

        Не за что:) Я из статьи не очень хорошо понял, почему вебсокеты отпали как вариант. Если только из-за слабой браузерной поддержки, то оба решения, озвученных выше, используют fallback HTTP-транспорты, в том числе и long-polling (xhr-polling). Более того у нас 95% пользователей уже могут подключиться по вебсокетам (возможно в вашем случае эта цифра меньше, зависит от аудитории сайта). Если вы видите еще какую-то проблему с вебсокетами и просто не желаете их использовать — то их просто можно отключить и использовать только HTTP — xhr-streaming, eventsource, xhr-polling и более изощренные для совсем старых браузеров (iframe-htmlfile, jsonp-polling).


        1. AlexSlipknot
          22.01.2016 11:24

          почему вебсокеты отпали как вариант

          1 — да, поддержка браузеров
          2 — сервер не выделен полностью для чата. Стоит nginx+apache+php и было рискованно ставить вебсокеты и догружать его
          Но в случае с отдельным серваком — да, я бы скорее всего выбрал вебсокеты.


      1. neolink
        22.01.2016 11:37
        +1

        если использовать libevent Long-polling на php и c по реализации и производительности на 1k подключений (это не highload) практически не будет отличаться


        1. AlexSlipknot
          22.01.2016 11:53
          -1

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


          1. Igogo2012
            22.01.2016 12:16

            Может быть скрипт был написан не достаточно качественно?


            1. AlexSlipknot
              22.01.2016 12:30

              не буду спорить относительно качества :) С PHP достаточно долго и реализовывал достаточно сложные вещи, да и перечитал много литературы чтобы правильно выбрать логику.
              А вот к стати, цитата партнера, может просто нагрузка была больше 1000:

              привет чат не упал я выключил его временно, у тебя на сайте онлайн вырос, было 300000 соеденений одновременных, так что ширины канала просто не хватило, не знаю даже что тут можно сделать, убери пока код с сайта


              1. VolCh
                22.01.2016 18:15

                Судя по всему, или клиенты не ждали сообщения от сервера и открывали всё новые и новые коннекты, или сервер не закрывал соединения после отправки сообщений, а клиенты открывали всё новые и новые.


          1. alekciy
            22.01.2016 13:36
            +1

            Вам предстоит еще долго и упорно учиться. Задаться темой чата, реализовать его на php, выбрать при этом long polling и не знать при libevent это в наши дни, хм… немного безответственно.


  1. xytop
    22.01.2016 10:56
    +2

    Вы на каждое соединение цикл запускали? :)
    Нужно делать общий цикл, который мониторит новые сообщения и возвращает их в соединение из пула. 1000 подключений — это совсем небольшая нагрузка.


    1. AlexSlipknot
      22.01.2016 11:05

      Да, так и планировал сделать изначально. Но, к сожалению, не было достаточно времени для переработки компонента FPHTTPServer. Весь смысл сводился к ручному управлению потоками, но до перепила исходников я так и не добрался.


      1. xytop
        22.01.2016 11:07

        Учите Go, там это нетрудно реализовать :)


        1. AlexSlipknot
          22.01.2016 11:18

          Синтаксис не впечатлил :) Да и не сильно копался в нем, может в этом и есть моя ошибка :). А чат хотел сделать кроссплатформенным, потому остановился на лазаре.


          1. neolink
            22.01.2016 11:45

            забавно слышать аргумент про кроссплатформенность не в пользу Go, когда в нем используя базовую поставку можно кроскомпилять под *nix, *BSD, Solaris, Windows операционки на arm(64), ppc(64), 386, amd64 платформы


            1. AlexSlipknot
              22.01.2016 11:50

              Да и не сильно копался в нем, может в этом и есть моя ошибка :)

              Еще раз дико извиняюсь :)


  1. Assada
    22.01.2016 11:09

    Как я помню — socket.io умеет сам делать выбор между сокетами и long pollin и кучей других способов в зависимости от «способностей» вашего браузера. Работает очень круто. Зря Вы отказались от node.js, тем более, как сказали выше 1000 — не такая уж и большая нагрузка.


    1. AlexSlipknot
      22.01.2016 11:15

      сам делать выбор между сокетами

      Оу, про это не в курсе. Сори, видимо недогуглил.
      тем более, как сказали выше 1000 — не такая уж и большая нагрузка

      Это то что я смотрел в реалтайме, по факту точное количество сказать не могу. Но вот смущает то, что партнер отказался от предоставления своего чата — сказал слишком много соединений из-за чего его сервер падал. На чем он построил свой чат я не знаю, но использовал он nodeJS.


      1. alekciy
        22.01.2016 13:41

        Проблема с нодой в том, что внутри есть несколько моментов при которых происходит глобальное блокирование. Как следствие — все подключенные клиенты накрылись медным тазом. Более стабильный вариант erlang (VM под винду есть сразу в виде готового бинарника), но он по синтаксису и пониманию специфичный. Как пример: Comet–приложение для Mochiweb c нагрузкой в 1 000 000 пользователей.


        1. AlexSlipknot
          22.01.2016 14:11

          О, да, это интересно.


  1. p4s8x
    22.01.2016 12:18
    +1

    C10k problem
    Мы у себя используем nginx-pushstream-module. Libevent вам тут подсказывали намекая на Ratchet или ReactPHP тоже позволят вам держать поболее соединений


    1. AlexSlipknot
      22.01.2016 12:33

      Спасибо, для nginx модуль действительно стоящий. А по поводу PHP — сама его идеология не подходит. Я когда-то пытался писать драйвера на Delphi и получалось, но он для этого не предназначен


      1. p4s8x
        22.01.2016 14:02

        Какая идеология PHP вам мешает?


        1. AlexSlipknot
          22.01.2016 14:09

          1. neolink
            22.01.2016 14:51

            и смысл раскапывать этих мамонтов в которых 2009 год совсем недавно?
            в php есть сборка мусора с поддержкой циклических ссылок как раз с 5.3, а в 7 версии можно фатальные ошибки обрабатывать в приложении, тоесть при желании оно ещё и фиг упадет, даже если вы какой-то левый код подключаете по мере работы, так, что в чем же он принципиально отличается от тойже ноды, питона и т.п. именно как язык + стандартная реализация?


            1. AlexSlipknot
              22.01.2016 17:38

              смысл в рациональном использовании ресурсов, и выборе техноголий. Идея возникла после того как мне напомнили про книгу «совершенный код» и факт про nginx, который изначально писался вообще для своих целей, а в результате оказался очень востребованным.


  1. AlexTest
    22.01.2016 12:28
    +1

    Сколько постов в секунду происходит в чате на 1000 человек, описанном в статье?


    1. AlexSlipknot
      22.01.2016 12:31

      в секунду — 2-3 поста


      1. AlexTest
        22.01.2016 12:46

        Ну тогда думаю, что вы рано отказались от простого решения на PHP. Если нет особых ограничений по трафику, то Varnish на фронт — раздавать страницу чата аяксом (Varnish выдержит тысячи подключений в секунду даже на слабом VPS), а PHP только обновляет эту страницу в кеше Varnish и БД при новых постах. Все очень просто и без комет технологий.


        1. AlexSlipknot
          22.01.2016 12:55

          PHP только обновляет эту страницу в кеше Varnish

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


          1. AlexTest
            22.01.2016 13:16

            Нет, не будет, т.к. PHP скрипт вызывается только для постов клиента, т.е. 2-3 раза в секунду (как вы ранее описали) он обновляет кеш варниша и сразу же завершает свою работу, освобождая ресурсы. Для обновления чата клиент обычным аяксом запросом получает контент чата из варниша например раз в 5 сек. Итого имеем примерно 200 http запросов в секунду к варнишу, и 2-3 http запроса в секунду к PHP скрипту на вебсервере, что, повторюсь, даже для самого убогого VPS — вообще не проблема.


            1. AlexSlipknot
              22.01.2016 13:24

              хм, тогда идея довольно таки интересная :)


            1. aleks_raiden
              22.01.2016 22:19

              Это можно и на голом веб-сервере сделать — nginx мог бы проверять статичный файл json, а РНР бы только обновляло его


  1. dirtyHabrBobr
    23.01.2016 02:25

    писать чат на PHP — бессмысленно, если нагрузка на него будет больше, чем 10 человек

    про chatvdvoem на хабре


    1. alekciy
      27.01.2016 12:03

      Сейчас там больше 1000 человек. И как, реально работает ровно на том стеке, что был изначально? Ресурсов сколько требует под себя?


      1. AlexSlipknot
        28.01.2016 15:05

        i.imgur.com/3bgALml.jpg — это сайт в пассивном режиме. Процесс со всеми потоками съедает 1% оперативы и 5% процессора.
        В час пик может доходить до 20% оперативы и 15% процессора