0,0
рейтинг
15 декабря 2011 в 07:59

Разработка → Демоны на PHP из песочницы

PHP*
Памятка начинающему экзорцисту.

Прежде, чем начать: я знаю, что такое phpDaemon и System_Daemon. Я читал статьи по этой тематике, и на хабре тоже.

Итак, предположим, что вы уже определились, что вам нужен именно демон. Что он должен уметь?
  • Запускаться из консоли и отвязываться от неё
  • Всю информацию писать в логи, ничего не выводить в консоль
  • Уметь плодить дочерние процессы и контролировать их
  • Выполнять поставленную задачу
  • Корректно завершать работу

Отвязываемся от консоли


// Создаем дочерний процесс
// весь код после pcntl_fork() будет выполняться двумя процессами: родительским и дочерним
$child_pid = pcntl_fork();
if ($child_pid) {
    // Выходим из родительского, привязанного к консоли, процесса
    exit();
}
// Делаем основным процессом дочерний.
posix_setsid();

// Дальнейший код выполнится только дочерним процессом, который уже отвязан от консоли


Функция pcntl_fork() создает дочерний процесс и возвращает его идентификатор. Однако переменная $child_pid в дочерний процесс не попадает (точнее она будет равна 0), соответственно проверку пройдет только родительский процесс. Он завершится, а дочерний процесс продолжит выполнение кода.

В общем то демона мы уже создали, однако всю информацию (включая ошибки) он всё еще будет выводить в консоль. Да и завершится сразу после выполнения.

Переопределяем вывод


$baseDir = dirname(__FILE__);
ini_set('error_log',$baseDir.'/error.log');
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
$STDIN = fopen('/dev/null', 'r');
$STDOUT = fopen($baseDir.'/application.log', 'ab');
$STDERR = fopen($baseDir.'/daemon.log', 'ab');

Здесь мы закрываем стандартные потоки вывода и направляем их в файл. STDIN на всякий случай открываем на чтение из /dev/null, т.к. наш демон не будет читать из консоли — он от неё отвязан. Теперь весь вывод нашего демона будет логироваться в файлах.

Поехали!


include 'DaemonClass.php';
$daemon = new DaemonClass();
$daemon->run();

После того, как мы переопределили вывод, можно выполнять поставленную демону задачу. Создадим DaemonClass.php и начнем писать класс, который будет делать основную работу нашего демона.

DaemonClass.php


// Без этой директивы PHP не будет перехватывать сигналы
declare(ticks=1); 

class DaemonClass {
    // Максимальное количество дочерних процессов
    public $maxProcesses = 5;
    // Когда установится в TRUE, демон завершит работу
    protected $stop_server = FALSE;
    // Здесь будем хранить запущенные дочерние процессы
    protected $currentJobs = array();

    public function __construct() {
        echo "Сonstructed daemon controller".PHP_EOL;
        // Ждем сигналы SIGTERM и SIGCHLD
        pcntl_signal(SIGTERM, array($this, "childSignalHandler"));
        pcntl_signal(SIGCHLD, array($this, "childSignalHandler"));
    }

    public function run() {
        echo "Running daemon controller".PHP_EOL;

        // Пока $stop_server не установится в TRUE, гоняем бесконечный цикл
        while (!$this->stop_server) {
            // Если уже запущено максимальное количество дочерних процессов, ждем их завершения
            while(count($this->currentJobs) >= $this->maxProcesses) {
                 echo "Maximum children allowed, waiting...".PHP_EOL;
                 sleep(1);
            }

            $this->launchJob();
        } 
    } 
}

Мы ожидаем сигналы SIGTERM (завершения работы) и SIGCHLD (от дочерних процессов). Запускаем бесконечный цикл, чтобы демон не завершился. Проверяем, можно ли создать еще дочерний процесс и ждем, если нельзя.

    protected function launchJob() { 
        // Создаем дочерний процесс
        // весь код после pcntl_fork() будет выполняться
        // двумя процессами: родительским и дочерним
        $pid = pcntl_fork();
        if ($pid == -1) {
            // Не удалось создать дочерний процесс
            error_log('Could not launch new job, exiting');
            return FALSE;
        } 
        elseif ($pid) {
            // Этот код выполнится родительским процессом
            $this->currentJobs[$pid] = TRUE;
        } 
        else { 
            // А этот код выполнится дочерним процессом
            echo "Процесс с ID ".getmypid().PHP_EOL;
            exit(); 
        } 
        return TRUE; 
    } 

pcntl_fork() возвращает -1 в случае возникновения ошибки, $pid будет доступна в родительском процессе, в дочернем этой переменной не будет (точнее она будет равна 0).

    public function childSignalHandler($signo, $pid = null, $status = null) {
        switch($signo) {
            case SIGTERM:
                // При получении сигнала завершения работы устанавливаем флаг
                $this->stop_server = true;
                break;
            case SIGCHLD:
                // При получении сигнала от дочернего процесса
                if (!$pid) {
                    $pid = pcntl_waitpid(-1, $status, WNOHANG); 
                } 
                // Пока есть завершенные дочерние процессы
                while ($pid > 0) {
                    if ($pid && isset($this->currentJobs[$pid])) {
                        // Удаляем дочерние процессы из списка
                        unset($this->currentJobs[$pid]);
                    } 
                    $pid = pcntl_waitpid(-1, $status, WNOHANG);
                } 
                break;
            default:
                // все остальные сигналы
        }
    }

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

Осталось запретить запуск нескольких копий демона, об это отлично написано в этой статье.

Спасибо за внимание.

UPD: хабраюзер Dlussky в своем комментарии подсказал, что в PHP >= 5.3.0 вместо declare(ticks = 1) надо бы использовать pcntl_signal_dispatch()
Николай Васильчук @Anonym
карма
7,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • 0
    Можно было бы воспользоваться деструктором и обойтись без stopServer — на мой взгляд, это было бы красивее.
    • +3
      Объясните пожалуйста подробнее, что вы имели ввиду. При использовании флага демон дождется завершения дочерних процессов и корректно завершится сам, что будет при использовании деструктора?
      • +1
        Я имел в виду, что предпочел бы вместо
        $this->stop_server = true;
        

        использовать
        exit(1);
        

        который вызывает метод __destruct(), где можно завершить все, что нужно
        Ну и вместо
        while (!$this->stop_server)
        

        использовать
        while(TRUE)
        

        P.S.: про деструктор — это я на всякий случай написал, суть комментария была в том, что можно бы и без флага обойтись. В конкретно Вашем случае деструктор и не нужен — никакой «работы по очистке» не выполняется, но если бы она понадобилась, и стояла бы у Вас после while(! $this->stop_server) {...}, то, ее можно было бы вынести в деструктор.
        • +2
          А разве exit(1); дождется завершения очередного такта цикла?
          • +6
            Не дождется, я был неправ. Извиняюсь за свою невнимательность — пойду-ка посплю… :)
        • +1
          Последовательность выполнения деструкторов не определена. Надеяться на деструктор нельзя, сам огребал с этим много непонятных багов. Просто подобные вещи делаются методами, а не свойствами.
          • 0
            Подтверждаю, делал для одного проекта кастомный скелет приложения, там решил сделать деинициализацию по деструкторам. Огрёб по голове, почитал, подумал, реализовал в виде ручного вызова деструктора у ядра, которое все модули правильно по зависимостям убивало тоже вызовом деструкторов и не зацикливалось.
            В общем-то сделать это не проблема, но нужно предусматривать это сразу.
  • 0
    Спасибо Вам, добрый человек)

    Начинающие экзорцисты довольны)

    Все доступно и понятно новичку, который хотел попробовать написать демона, но не решался потому что не понимал как это сделать!
    • +1
      Я старался донести основную мысль как можно более просто и понятно, т.к. сам с трудом разобрался в этой теме. Найденные мной статьи не отражали в полной мере основ, поэтому пришлось собирать информацию по кусочкам.
      Возможно более опытные экзорцисты просто не находят здесь ничего сложного ))
  • +1
    с вводом/выводом прикольно. не знал. спасибо.

    остальное в прошлом. проблемы будут когда демон будет работать. а просто запустить несколько дочерних процессов не фокус. фокус как обработку по деткам раскидать и асинхронно ввод/вывод обработать на libevent. вот тут бубнов и танцов много.

    в свое время писали игрового демона с Вадимом Крючковым, который HTTP запросы обрабатывает и с БД работает и других демонов дергает. много интересного поимел. дитя было одно. асинхронное. демонов было много. был весело.
    • +1
      где то тут на хабре был исходник демки (отвечал в тему phpDaemon), которую представляли на phpconf. там как раз все это разжевано было.

      но автору большое спасибо. +1. понастальгироавл.
      • 0
        Не подскажите какую-нибудь хорошую статью про обмен данными между процессами? Всегда возникали проблемы именно с этим. С демонами и libevent обычно все просто.
        • 0
          Статью, к сожалению, не подскажу. Копать в сторону пайпов и сокетов.
          • 0
            В родительском процессе создавать слушающий сокет, а всех дочерних подключать к нему? Думал на эту тему. Но как-то смущает. Насчет пайпов покурю. Еще смотрел в сторону общей памяти, симофоров и тд. Вот и хотелось бы посмотреть на чужой опыт, может уже есть достаточно хорошее решение.
            • 0
              Напишите пожалуйста, если найдете хороший пример использования.
            • 0
              ИМХО можно stdin/stdout обойтись и не плодить тыщу лишних сокетов/пайпов.
            • 0
        • 0
          Я как-то начинал описывать работу с демонами в своём блоге, может эти заметки покажутся вам полезными
          • 0
            Спасибо. Все верно написано. Но я извиняюсь — уточню в чем у меня были еще проблемы. Было желание, чтобы процессы могли уведомлять друг друга о том, что новые данные появились. Тут как раз выигрывали сокеты, тк. все равно использовался libevent. С shared memory, я как понимаю такое можно организовать только запрашивая данные по таймеру.
            • 0
              Вы могли бы построить свою схему на очередях, где можно было бы присылать даже какие-то данные, или же можно было бы слать сигналы.
              • 0
                Судя по вашему примеру в статье проверка очереди заблокирует выполнение программы (у меня libevent же используется), или же придется опять использовать таймеры. Вот насчет сигналов — надо проверить. Поидее должно сработать.
    • 0
      Ну это ведь статья для «начинающих экзорцистов» )
    • +2
      Писал на сях, не сильно огребал, boost::asio хорошо помог в этом) А вот писать сложные демоны на php это все же некое извращение.
      • 0
        тоже стараюсь все демоны писать на сях, но вот выпал случай реализовать демона на пхп
  • +1
    всегда интересовал вопрос практического применения демонов на PHP? кто нить юзает их именно на PHP и с какой целью? :)
    • –1
      А какая разница на чем писать демона, на php или, к примеру на perl?
      • НЛО прилетело и опубликовало эту надпись здесь
        • +3
          это мы и были. ниче не пухло. работало на продакшене. демоны были прослойками объектов между фронтом и БД. т.е. вся логика, обсчет персов, бои работала в демонах. фронт тупо туда лазил.

          прикольно было организовать таймеры в демоне (на libevent). когда вызывался нужный метод через определенное время. например для сброса меченных данных в БД.

          так же демон общался по HTTP/REST. транспортом летел JSON.

          в общем было хорошо, интересно и ново.

          в эту нашу разработку как раз Тони и пофиксил баги в php_libevent.
          • 0
            мои демоны пухли, но как оказалось — это текла либа xCache (после выхода APC под 5.3 перешли сразу на АРС)
            мы их рестартовали, как процесс набирал память до 400М.
            Был написан специальный скрипт-монитор, который по крону следил:
            — не упал ли демон (и сразу поднимал его)
            — не превысила ли очередь критической метки
            — не превысил ли объем процесса выделяемую квоту
        • 0
          я киляю чилды после того, как они выполнят определенное количество задач (типа настройки MaxRequestsPerChild у апача).
    • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        Вы класс задачь перепутали, на PHP никто в здравом уме не будет это делать, в общем-то как и на perl или ruby.
        Python/Java/.NET/Mono
        • НЛО прилетело и опубликовало эту надпись здесь
          • +3
            Вы крайне плохо читали презентации и слушали доклады — PHP у них это шаблонизатор и сборщик данных из бекенд сервисов — собрал данные, связал, собрал из этого HTML и отдал — никаких демонов, вычислений и прочего он не производит. Демонов они на PHP они не пишут — сервисы у них на C/Haskell/Python/Java прочих, но никак не на PHP.
            PHP хоть и быстрый среди скриптовых языков, но он имеет внушительное потребление памяти и хранить в нём компактные структуры не получится — для этого нужны или строго/слабо типизированные языки, у которых данные мапятся на память без наклодных расходов на контейнеры и.т.д.
            Каждая переменная в PHP — это zval структура, которая занимает 24/48 байт на 32/64 bit архитектурах. Любая другая структура типа массива или объекта даёт ещё больший оверхед, что в итоге даже на миллионе-полтора элементов в массиве простейших значений сожрёт 100-130 мегов памяти. Объекты жрут ещё больше. Вот скажите, кто в здравом в Facebook/Yahoo/Badoo с их нагрузками и их колличеством данных будет делать хоть один демон на PHP?
            • НЛО прилетело и опубликовало эту надпись здесь
              • 0
                В вашем случае логику работы нужно перенести в сервис и вообще выкинуть её из PHP части. Кормите в сервис данные, читаете из него данные, а он реализует всю логику. А PHP занимается тем, что он делает лучше всего — рендерит страницы и прочую легковесную не бекенд работу. Если у вас что-то сложное, да ещё и с интенсивными вычислениями и большим кол-вом памяти — то PHP стадия это прототип для отладки алгоритмов, логики и архитектуры. А потом прототип переносят уже на что-то более подходящее и гораздо быстрее, нежели программировать с нуля.

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

                Что до модных штук — вы это зря — .NET/Java/Python далеко не модные — это конкретные инструменты, которые использует очень много людей для не разработок и посерьёзнее вашей. Модно — это Ruby.

                И да, я прекрасно знаю о том, что такое переделать проект. И что нужно сделать для этого, что бы верхи согласились на это. Это не решается за пару недель — такие мысли надо внедрять начальству зарание, нужно объяснять что «вот эту фитчу мы конечно можем и запихаем, но нам надо бы на будущее подумать об обновлении продукта, т.к. имеются следующие сложности в разработке и они накапливаются, могу предложить вот такие варианты для обдумывания». Однажды вам либо дадут добро, либо продукт просто сдохнет. Или загнётся от нагрузок, клиенты придут в ярость и вас просто уволят (вы сами написали, что у вас всё очень сильно нагруженно). Думать на будущее тоже надо.
                • +5
                  мы тоже на php прототип писали. думали на С потом демонов переписывать, но померили скорость отдачи и скорость обработки данных и она устроила более чем, а уж скорость разработки на php в разы превосходит C.

                  так вот и остались на php-демонах с ангельскими крылышками.

                  а так да… искренне верили, что это прототип, что это не на всегда.
                  =)
                  • 0
                    Ну так у вас и задачка другая совсем была — у вас игровая логика основная нагрузка, а не ворочание данных. Да и у вас ограничена нагрузка на один демон была, т.к. демонов у вас было много, к тому же они ещё и по задачам делились. Так что у вас изначально всё горизонтально масштабировалось и отдельные части игры обрабатывали отдельные по функционалу демоны :)

                    Согласись, это немного другое нежели конкретный сервис :)
            • +1
              Badoo пишет демоны на C/C++.
              «Я гарантирую это» (с), как один из людей, которые эти демоны пишет.
            • 0
              Вы совершенно, на 100% правы, но поставть ++ несколько раз не могу.
        • 0
          На ассемблере, наверно?
          • 0
            Иммелось ввиду что нужно писать на Python/Java/…
            Там символ точки после ruby :)
    • +1
      В процессе развития приложения от простого CRUD к чему-нить навороченному и, в частности, асинхронному (websockets например) понимаешь, что нужен демон. Или просто хочется нормального FastCGI, а не того, что предлагает PHP, хотя бы чтоб конфиги при каждом запросе из базы не читать. Опыта написания демонов нет, опыта серьёзной работы (кроме хелловорлдов, максимум «бложиков») с другими языками нет, особенно с компилируемыми. Как нет и опыта разворачивания чего-то кроме «LAMP» на продакшене. Надо выбирать или писать демона на PHP, изучая только написание демонов, или писать на более подходящем языке, изучая не только написание демонов, но и сам язык, и его инфраструктуру, да ещё думать о том, чтобы с существующей «LAMP» она не конфликтовала. Критический фактор, как всегда, время и деньги — если на несколько месяцев забить или сильно снизить темпы развития основной функциональности приложения, то лояльные пользователи станут нелояльными и перестанут приносить доход. Чтобы вы выбрали? «Аутсорсинг» не предлагать :)
      • 0
        Кстати, я всегда удивляюсь, когда вижу конфиги в .ini, в базе, где угодно, но не в php в виде массива. Сам facebook это советовал ещё в 2008 — используется L1 кэш в виде php globals arrays, который загружается через простой include, и который не надо парсить каждый раз благодоря APC (или другому кэшеру опкода).
        В самом деле, это же так просто — file_put_contents(«configinclude.php», "< ?".$GLOBALS['siteconfig']=var_export ($config,true)."?>",LOCK_EX)
        • 0
          Ну так это же кэш, а откуда-то надо взять значения, чтоб закэшировать. И желательно в более-менее понятном человеку виде.
          • 0
            Что значит «откуда»? Отсюда и взять. Что там непонятного человеку, или в чём сложность переделать админку конфига на сохранение в этот файл с помощью строчки выше?
            • 0
              Само слово «кэш» подразумевает, что хранится копия данных, обеспечивающая быстрый доступ, но оригинал хранится в другом месте. Ну и заставлять пользователей («веб-мастеров») писать конфиги в PHP тоже не хорошо, имхо.
              • 0
                Я не вижу никакой разницы между хранением конфига в .ini/базе и хранением сериализованного конфига в php-инклуде.
                Вообще никакой. Особенно если управление конфигом идёт через админку. Объясни, в чём видишь разницу?
                • 0
                  Доступ на запись к файловой системе как минимум. Его может просто не быть.
    • –1
      Демоны нужны для обсчета большого количества данных или сложных расчетов в бэдграунде
      или для реализации распределенных вычислений, например при организации сбора и обработки данных на шардинг-архитектуре.
  • 0
    АааААааа!!! Демоны!!! (с) Иван Васильевич меняет профессию
    • 0
      зомби на вас нет!
  • +4
    Знаете в чём минус практически всех статей по демонам на хабре, особенно тех что про PHP?
    Показали как продить процессы и на этом всё, а о дальнейших граблях ни слова, ни примеров, вообще ничего.

    Может написать свою статью не о теории как выше и скелете, а о практическом применении с граблями, танцами и бубнами? Правда у меня yiiframework, но опыт в прочем приминим к любому варианту.
    • +3
      Возможно напишу продолжение, когда закончу работу над своим демоном.
      • 0
        Продолжение то продолжение, будет весьма не скоро как показывает практика. А смысл в том, что точно таких же статей как эта тут на хабре я видел наверно уже с десяток — различается разве что стиль, оформление и грамматика. Должен признать — у вас оформленно лучше всех, и читается легко + бонус про STDIN, STDOUT и STDERR.
        • 0
          Видимо я плохо искал. На хабре нашел только одну более-менее годную статью, ссылку на неё я привел в конце топика.
          • 0
            Ну авторы могли убрать в черновики, прописать странные теги и.т.д.
            В любом случае людям статья полезна, критиковать вас в подаче материала невозможно — вам бы к нас в школу веб технологий преподавателем :)
            • 0
              Жаль, что вы из Латвии… )
              • 0
                Что-ж поделать то, вот так и живём. Правда преподавателей какраз латышей не хватает — мало их настолько, что даже выбрать не с кого не смотря на хорошую зарплату — всё рускоязычные веб девелоперы и дизайнеры у нас больше.
    • +2
      Пишите, даже не спрашивайте.
    • 0
      > Может написать свою статью не о теории как выше и скелете,
      > а о практическом применении с граблями, танцами и бубнами?

      Напишите, пожалуйста.
  • 0
    Отличная статья. Мне очень понравилось! Спасибо!
  • 0
    отличная статья, спасибо. Как раз вчера зашла на работе тема о демонах на РНР. Я читал о демонах на Си, но на РНР не знал точно, как сделать, а у вас раскрыто практически всё, что надо.
  • +6
    Добавьте кроссплатформенности, иногда php-демоны нужны и на винде, не виндовым же скедьюлером пользоваться для их запуска.
    $Flag = 'daemon.flag';
    $Self = daemon.php;
    if (!file_exists($Flag))
    	if (strtoupper(substr(PHP_OS,0,3)) === 'WIN') {
    		touch($Flag);
    		$fp = popen('start /B php -f '.$Self,'r');
    		if ($fp!==false) pclose($fp);
    		unlink($Flag);
    	}
    } else {
    	// Child process code
    }
    

    Но тут есть засада, этот метод отличается от форка, для того, чтобы по кругу не начать запускаться много раз, мы привлекаем еще файловый флаг 'daemon.flag'. Писал код прямо в хабраредакторе не проверяя, так что пользоваться осторожно.
    • +2
      Спасибо за толковый комментарий.
      К сожалению (или к счастью), у меня даже в мыслях не было запускать php под виндой, и уж тем более делать для винды демона на php. Ну и в топике я показал только основы, остальные нюансы каждый скорректирует под свои задачи.
      • +2
        Хм, а моё внимание вот не привлекло отсуствие PID файла у вас. А для нормальной работы он вообще нужен, да :)

        В общем решено — пишу свою статью, думаю на выходных будет :)
        • 0
          Объясните пожалуйста, зачем нужен PID файл, кроме как для получения PID демона чтобы его завершить и для проверки запущенности демона?
          • +2
            Как минимум что-бы сделать в системе обычный init скрипт и запускать/останавливать/перегружать по сигналам, а там можно и обновление конфигурации дописать, ротацию логов и прочее.
            Чёрт, да тот же автостарт при перезагрузке добавить, то же слежение за запущенными процессами через систему мониторинга.
            Ну и предотвращение дубликаций запуска тоже не последнее дело.

            Это только кажется мелочью, а когда демон переодически обновляется, то возможность его рестартнуть одной коммандой ./daemonname.php args вместо
            ps -AF
            kill -9 PID
            php /path/to/script
            • 0
              Ну в общем то, я правильно понял его предназначение. О том, что про PID файл я не забыл, я написал в комментарии ниже.
          • 0
            или чтоб всех сразу завалить:
            user=`whoami`
            daemon_name='daemonname.php'
            ps aux | awk '/^'`$user`'(.*)'$daemon_name'$/{print$2}' | xargs kill -9

            потому что без PID-файла легко может быть запущено несколько демонов, когда вам нужен только один.
        • 0
          Ctrl+Enter — зло.
          PID файл создается на этапе запрета запуска нескольких копий демона. Ссылка на статью указана в конце топика.
        • 0
          Да, ну и демону еще ж нужно общаться с другими демонами и получать запросы от обычных процессов, запущенных через вебсервер. Обязательно раскройте сигнальную систему: через базу, через файлы, через сокеты. Ну и нужно нацеливаться на множественность демонов, т.е. каждый под свою задачу, один базу чистит, другой мыло шлет, третий файлы лопатит. Внешнее API нужно (вызываемое из обычного процесса) для запуска, терминации, паузы, обмена информацией с каждым процессом демона, идентифицированного по имени. А есть вообще иногда необходимость запустить и несколько копий одинакового демона, например, если многоядерный сервер или нужно что-то параллельно делать, кравлить/парсить там… но с ограничениями, например не более 5 инстансов демона. Так что, задача обширная, удачи!
          • +1
            наши наружу HTTP торчали, а гоняли JSON. оказалось очень удобно.
            • 0
              Хе, я тоже так делаю, даже для локального с ними общения межпроцессового (в рамках локалхоста имею в виду).
          • 0
            Скажем так, для такой сложности разработки это вам с вопросами к 440hz, в моём случае я работаю с базой и очередью в ней, у меня параноидальная обработка ошибок (для PHP, но не для демонов как таковых) на исключениях и есть работа с внешними сайтами через proxy и без proxy. Основной критерий — стабильность любой ценой, ибо работаю с платёжными системами, соотвественно вылеты, некоректная работа и.т.д. как правило ведёт за собой потерю средств.

            В общем написать есть что, но задачка у меня попроще чем вы описали и до многопоточности я не добирался ещё — времени на разработку пока нет, да и нагрузка у меня минимальная.
            • 0
              Ну может YAAP опубликует наши с ним разработки, но их нужно рефакторить и описать хорошо, подход более универсальный, но сейчас уже хочется все причесать, а то разрослось и запуталось. Тем ни менее, все перечисленное имеется.
    • 0
      www.php.net/manual/en/pcntl.installation.php

      использование pcntl_fork сразу отсекает возможность использования на винде. Смысла нет.

      С другой стороны, я бы добавил us.php.net/clearstatcache

      Это не обязательно для всех, но если ваш демон проверяет даты файлов, права и т.д. и при этом они могут меняться, clearstatcache необходим, чтобы сбросить кэш.
      • +1
        Бзз… закройте глаза на первые две строки. Вы их не видите :)
        • 0
          а про clearstatcache согласен
  • 0
    На PHP у меня написан демон, который ходит по трекерам, и смотрит обновления в раздачах. Если есть обновление, то в rss-фид добавляется обновленная раздача.
    За время тестов обнаружил, что память действительно утекает, как писали выше. То есть за неделю работы демона он мог съедать порядка 40Мб памяти, а при старте демон забирал в районе 5Мб. Решается само собой очень просто — после n циклов демон себя убивает, а крон его подымает обратно. Наверное для серьезных серверных реализаций это очень некрасиво, но для таких частных штуковин вполне подходит.
    • 0
      Или просто нужно найти где память утекает. У меня демон без рестарта работает по 2-3 месяца и потребление памяти стабилизируется на ~35MB и не мегабайтом больше!
      • 0
        Все верно, но я же написал, что аптайм не критичен. Кстати, как например можно дебажить и искать что течет?
        • +4
          Элементарно — вставляете запись в лог потребление памяти и смотрите в каких местах она увеличивается, но не освобождается. И да, если у вас объект содержит в себе в properties ссылки на другие объекты — нужно обязательно их удалять в ручную хотя-бы через деструктор, а вообще за собой нужно подчищать с помощью unset'ов — это вас избавит от 99% проблем с памятью.

          Ну и PHP 5.3 имеет garbage collector, переодически запускайте его:
          <?php 
          // Somewhere before the endless loop
          
          $last_gc_cycle = time() - (24 * 3600);
          // Some more code
          
          while (true) {
          	// The main code here
          	
          	if (function_exists('gc_collect_cycles')) {
          		$time = time(); 
          		if ($time - $last_gc_cycle > 300) {
          			$last_gc_cycle = $time;
          			gc_collect_cycles();
          		}
          	}
          }
          
          • 0
            согласен. деструкторы + освобождение ссылкок + unset() спасут от утекания.

            мы тоже изрядно повозились, что бы не текло.
    • 0
      наши демоны отжирали по 1-2Гб, храня текущих персов + была подкачка когда не хватало места.
  • +9
    Демоны!

    image

    Интересная статья, даже не думал что в PHP это так просто делается.
  • +1
    Насчет трюка с STD*: насколько мне известно это недокументированная особенность, которую в любой момент могут прикрыть.
    Магия тут в том, что файловые дескрипторы выдаются по порядку. И закрыв STD*, а затем в том же порядке открыв файлы, мы получаем, что они имеют FD такие же как и STD*.
    PHP разработчики вполне могут в любой момент сделать ограничение с FD>2.
    Как альтернативу, пусть и похуже, зато документированную, можно предложить использование ob_* функций.
    • 0
      Мне приятнее считать, что «это не баг, а фича».
      Если поискать, то можно найти баги, связанные с закрытием STD*, которые уже пофиксили. А раз пофиксили, значит решили, что так делать можно.
    • 0
      Как это не документирована www.php.net/manual/en/features.commandline.io-streams.php?
      • 0
        Где вы видите там описание того, что можно подменять файловые дескрипторы STDIN, STDOUT и STDERROR?
        Про предопределенные константы я прекрасно знаю.
  • 0
    когда нужно по быстрому запустить скрипт как демон делаю так:
    nohup script.php > log
  • +2
    // Без этой директивы PHP не будет перехватывать сигналы
    declare(ticks=1);

    Не лучшее решение.

    For PHP >= 5.3.0, instead of declare(ticks = 1), you should now use pcntl_signal_dispatch().
    • 0
      Спасибо, не знал. Изучу этот вопрос.
  • +2
    В статье не хватает информации о том, зачем вообще нужен демон. Чем демон лучше обычных скриптов?

    Смотрим «что умеет делать демон» из статьи:

    1. «Запускаться из консоли и отвязываться от неё» — nohup script.php &
    2. «Всю информацию писать в логи, ничего не выводить в консоль» — nohup script.php 1>> out.log 2>>err.log &
    3. «Уметь плодить дочерние процессы и контролировать их» — а зачем оно надо? В крайнем случае можно сделать бесконечный цикл с вызовом php-скрипта
    4. «Выполнять поставленную задачу» — а чем обычный скрипт хуже?
    5. «Корректно завершать работу» — в скрипте прописывается что-то вроде такого:
    declare(ticks = 1);
    pcntl_signal(SIGTERM, "_shutdown");

    Вобщем все то же самое, только намного проще.

    Единственное что может быть — это realtime обработка большого количества поступающих сигналов. И в большинстве случаев это можно заменить redis BLPOP.
    Для background задач вроде обработки очередей — не вижу смысла в демонах.
    • 0
      Вот тут мой вопрос :)
    • 0
      Ну если вы сделаете, всё, о чем написали, у вас и получится демон =)
  • 0
    >«Уметь плодить дочерние процессы и контролировать их» — а зачем оно надо? В крайнем случае можно сделать бесконечный цикл с вызовом php-скрипта

    Пускай у нас бесконечный цикл с вызовом скрипта — одновременно он может отработать только один запрос. Попадается тяжелый (большая нагрузка на ЦПУ) или долгий (обращение к внешнему серверу, например) — все его ждут. Может быть очень долго ждут. Если на каждый запрос мы форкаем отдельный обработчик, то тяжелый запрос будет выполняться «параллельно» с остальными, пускай он сам дольше, и другие тормозить будут, но все они будут выполняться. Если же он долгий, то пока он будет ждать ответа от удаленного сервера, остальные будут обрабатываться практически вообще без замедления.
    • 0
      Но и скриптов можно запустить несколько.
      И будут они все брать задачи из очереди в Redis, например.
      И даже можно сделать отдельный скрипт, который будет динамически менять количество скриптов в зависимости от размера очереди, почти как php-fpm.

      Но тут я зайду уже с другой стороны.
      Допустим, мы пишем игру и у нас много realtime сигналов.
      Для их обработки люди создают php-демонов, делают им HTTP-интерфейсы на JSON.

      Вопрос, зачем вместо этого не использовать php-fpm + nginx? Сильный получается выигрыш в скорости? За счет чего?
      • 0
        Вот этот отдельный скрипт и будет демоном, плодящим дочерние процессы и контролирующий их :)

        Надеюсь, что будет сильный за счет того, что не нужно каждый раз инициализировать всё приложение, читать конфиги даже из мемкэша, не говоря о БД. Ведь по сути php-fpm только формально является FastCGI, а само приложение об этом не знает, идет полная инициализация при каждом запросе.

        • 0
          Да ну, все эти расходы на инициализацию, это мелочь.
          Независимость каждого запроса — это скорее плюс, а не минус.

          Вобщем сам задал вопрос, сам отвечаю.

          Суть демонов в том, что они являются серверами, которые держат сразу много коннекшенов. То есть один процесс обрабатывает много запросов «одновременно», и не нужно делать форк на каждый запрос.

          При запуске демон вешает себя на сокет (порт), и на события на этом порту вешаются на php-функции, созданные нами.
          События — это входящее соединение от нового клиента, получение данных от клиента, отключение клиента и ошибка.
          Разработчик демона пишет функции, обрабатывающие эти события, и специальная библиотека libevent вызывает эти функции в нужные моменты.

          В этом в кратце вся суть.
          • 0
            Как-то у вас всё в одну кучу. И демоны, и сервера, и сокеты с коннекшенами, и отсутствие форков, и либевент. Начнём с того, что не каждый демон это сервер, а форки для демонов допустимы. По идее, как раз в ответ на событие от libevent колбэк должен создавать или вызывать форк (коль скоро потоков в PHP нет) и забывать о нём, возвращая управление в libevent, как раз чтобы была «параллельная» работа, а не очередной запрос обрабатывался только когда, когда предыдущий будет обработан колбэком.
            • 0
              Честно говоря, не вижу смысла в демонах, которые не являются серверами.

              Насчет форков для длительных задач, я думаю, что долгие задачи на php-серверах лучше не выполнять. По причине того, что можно слишком нагрузить сервер при этом.
              Их лучше складывать в очередь и выполнять отдельно. Кстати, для этого специально есть такая штука как Gearman, организует очередь задач, можно выполнять на нескольких физических серверах.

              Задачи необязательно выполнять параллельно. Например, Redis все выполняет последовательно (ну кроме скидывания состояния на диск).
              • +1
                Демон, по сути, это лишь программа, которая не имеет UI. Она может быть как клиентом, так и сервером. Например, демон мониторинга серверов, который пингует сервера из списка и пишет в лог результаты. Зачем ему порты, сокеты и прочие серверные причиндалы, достаточно реагировать на несколько сигналов и то не обязательно. А вот многопоточность или многопроцессность не повредили бы такому демону, чтобы не застревала проверка всех серверов из-за того, что какой-то сервер лег и пинг ждет таймаута.
                • +1
                  Если в контексте php, то я не вижу смысла такого демона городить.
                  У меня примерно то, что вы описали, только вместо пингов http-запросы.
                  Через nohup в background'е запускаются шелл-скрипты («многопроцессность»), которые в бесконечном цикле вызывают php-скрипт. В этих php асинхронно выполняются http-запросы — почти «многопоточность».
                  Все хорошо работает без всяких демонов.
                  • 0
                    Тогда получается, что вы ту часть, которая может быть реализована на php, реализовали на баше с привлечением nohup. Имхо, оба подхода имеют право на жизнь.
          • 0
            Когда для каждого клиента нужно построить структуру в памяти (модель) на 10мб, то инициализация становится очень даже заметной. Сокеты позволяют держать эту модель в памяти не убивая и не создавая ее каждый раз, но есть другая особенность сокетов — проход по клиентским сокетам на сервере — это все же цикл, и значит, что пока одного клиента обрабатываем, то все ждут, я еще не встречал возможности писать на PHP серверы многопоточные на одном серверном сокете, а привлекать сюда C# очень не хочется, другаяплатформа все же, C++ немного лучше, но разработка усложняется и затягивается, Delphi — умер, Node.js — тоже проходит всех клиентов по очереди в цикле, даже не знаю что тут придумать можно кроме как открывать в PHP много демонов, распределять клиентов на них равномерно, чтобы проход в цикле был не долгим.
      • 0
        Это я Вам скажу, на php-fpm + nginx при большом количестве постоянно идущих запросов случается столько форков, что весь CPU идет на переключение контекстов, да и памяти уходит много. На сокетах в сотни раз меньше и нет переключения контекстов, можно поднять 10 сокетных серверов, например, для 16 ядерного сервера и это выдержит 10.000 событий в секунду, а php-fpm + nginx ляжет.
        • +1
          Ага, опередили меня.

          А кто-нибудь копал в сторону Facebook HipHop compiler? Он из PHP-кода делает веб-сервер, какое у него там устройство внутри?
          • 0
            Я лично не копал, но вообще стоит поинтересоваться, оно в открытом коде? Есть статьи? описание, примеры?
            • 0
              Да, он open source: github.com/facebook/hiphop-php
              Насчет статей, про их веб-сервер я пока не встречал.
        • 0
          А что вы имеете в виду под «на сокетах»?
          • 0
            Имеется в виду PHP + socket_select / socket_accept / socket_recv / socket_write
  • 0
    Для background задач вроде обработки очередей — не вижу смысла в демонах.


    Я нача лработать с очередями используя Zend_Queue. По крону достаю заданное кол-во сообщений, у них проставляется timeout (время в течении кот они не должны доставаться другой крон-задачей). Но если это время меньше, чем время обработки этих сообщений, получается, что другая крон задача также заберет эти сообщения (какую-то чась из них) и обработает их. Получается, что сообщения могут быть обработаны несколько раз.
    Если же использовать демон, то он может проверять через заданный интервал наличие новых сообщений в очереди и обрабатывать их последовательно. Такми образом можно избежать обработку идного и того же сообщения несколько раз наверняка.

    А как вы решаете задачу?
    • 0
      Через msg_*. Демону даже не нужно ждать интервала, клиент кладет сообщение в очередь (возможно с id внешнего ресурса типа имени временного файла или id записи в бд), и оно тут же поступает демону (если он не занят обработкой предыдущего). Если скрестить со «скелетом» из топика, то не сложно, наверное, сделать и параллельную обработку нескольких задач, если это имеет смысл.
      • 0
        То ли я не тула нажимаю, то ли Хабо глючит, вот мой ответ.
    • 0
      > Но если это время меньше, чем время обработки этих сообщений, получается, что другая крон задача также заберет эти сообщения (какую-то чась из них) и обработает их.

      Если таймаут слишком мал (меньше времени обработки этих сообщений), значит надо повысить таймаут.
      • 0
        В том то и дело, что нельзя наверняка знать время обработки сообщенийю
        Вот, например, я рассылаю емэйлы. STMP сервер может не отвечать по каким-лбо причинам в течении некоторого времени и получается, что мои эмейлы разошлются несколько раз :(
        Я хочу чтобы срипт заупскался каждую минуту и рассылал по 50 сообщений. Если коннект с SMTP сервером ок, то все работаеткак часы, если сервер тупит хотя бы минуту — у меня дубрируются емэйлы.

        Что делать, как знать?
        • 0
          Почта должна уходить через SMTP асинхронно.
          То есть SMTP-сервер должен почти моментально добавлять сообщение в Mail Queue, а дальше уже «тупить» с этой своей очередью сколько угодно.
          Короче говоря, надо по-нормальному настроить SMTP-сервер.
          • 0
            Ок, это просто пример был. Видимо неудачный.
            Я это все говорил к тому, что иной раз невозможно знать наверняка скорость обработки сообщений и из-за этого мне этот механизм кажется не самм лучшим в отличие от использования демона.
            • 0
              Ну допустим, демон взял сообщение и что-то очень долго с ним делает — то ли завис, то ли помер, то ли сервер, на котором этот демон работает, перезагрузился.

              И при этом непонятно, обработалось сообщение или нет.

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

                Какой вы видите выход?
  • 0
    Через msg_*.

    Не понял вас, если честно.

    Если скрестить со «скелетом» из топика, то не сложно, наверное, сделать и параллельную обработку нескольких задач, если это имеет смысл.

    Я думаю имеет смысл, только нужно котролировать кол-во дочерних процессов, чтобы он не было over 9000.

    А вообще, конечно, хотелось бы услышать ответ по поводу разбора сообщений с помощью крон-задач, если у кого есть опыт
  • +1
    Есть процесс (условно демон) с главным циклом типа:
    $queue = msg_get_queue(12345);
    $message = NULL;
    $msgtype = NULL;
    while (msg_recieve($queue, 0, $msgtype, 256, $message)) {
      dispatchMessage($msgtype, $message); // обрабатываем сообщение
    }
    

    и есть клиенты, которые при обработке пользовательских запросов вызывают что-то вроде
    $message = 'I have a task for daemon.';
    $queue = msg_get_queue(12345);
    msg_send($queue, $msgtype, $message);
    

    Демон постоянно (до ошибки) вызывает msg_recieve, она либо возвращает первое сообщение из очереди, либо ждет пока оно не появится и только тогда возвращает.

    По хрону когда-то делал так:
    клиенты складывают задачи в базу
    скрипт по хрону считывает пачку, ставит флаг «в работе», свой pid и время начала работы
    после успешной обработки удаляет задачи из базы
    если не фатальный сбой, то сбрасывает флаг в работе

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

    В итоге будем иметь:
    — каждая задача запускается минимум один раз
    — если задача крашит скрипт обработки или обрабатывается очень долго, то делается попытка запустить её второй раз (и только второй, чтоб некорректная задача не ставилась раз за разом)
    — задачи, которые за 2 попытки так и не смогли выполниться не удаляются, а висят в базе для ручного разбора причин.

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

    Если дочерний процесс завершается некоторое продолжительное время, и когда будет дана команда на остановку демона, будет создан новый дочерний процесс:
    while (!$this->_stopServer) {
                while (sizeof($this->_currentJobs) >= $this->maxProcesses) {
                    sleep(5);
                }
    
                 $this->_launchJob();
            }
    


    Чтобы дочки не создавались больше:
    while (!$this->_stopServer) {
                while (sizeof($this->_currentJobs) >= $this->maxProcesses) {
                    sleep(5);
                }
    
                if (!$this->_stopServer) {
                    $this->_launchJob();
                }
            }
    

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