Хитрости с логированием в однопоточных неблокирующих серверах.

    Хочу рассказать об очередном результате моих изысканий в области оптимизации производительности Web-серверов.
    На этот раз речь пойдет об оптимизации сложного логирования в однопоточном неблокирующем вэб-сервере.

    Введение


    Все не раз слышали о том, как lighttpd (далее «лайти») производителен и нетребователен к ресурсам. В последнем пререлизе версии 1.5 появилось много нововведений: в частности увеличена была производительность отдачи увесистых файлов на очень загруженных серверах (более 2-х тысяч потоков с суммарной скоростью в 50-200 МБ/с). Я, как управляющий весьма большим вэб-файлохранилищем, не мог пройти мимо таких возможностей. На тестовую машину с быстрым рейдом и двумя GigabitEthernet адаптерами, завязанными в bond интерфейс с распределением нагрузки, был поставлен дистрибутив Debian линукс. После пары часов проб и ошибок была собрана и настроена на оптимальную производительность связка лайти+php_fcgi. Файлы действительно отдавались очень быстро и 2000 потоков для лайти не оказались трудностями. Но тут я наткнулся на серьезную проблему.
    Дело в том, что одно из требований к веб-серверу для работы на файлохранилище — чтоб он имел возможность отправлять строки лога через pipe в специализированную программу, которая бы их особым образом обрабатывала. В этой программе обрабатываться данные о соединении (что за пользователь качал, сколько действительно байт выкачанно за это соединение и т.д.) и отправляются для хранения в БД. Лайти умеет перенаправлять данные через pipe в программу для логирования, но проблема кроется в его однопоточной неблокирующей архитектуре. Т.к. лайти работает в один поток (процесс) и в неблокирующем режиме только отправляет данные в сокеты — то его основной поток уходит в ожидание снятия блока записи пока вспомогательная программа не обработает предыдущую запись лога. А программа эта может подвисать — поскольку соединение с БД — вещь не постоянная: то запрос подвиснет из-за табличной или строчной блокировки, то само соединение по неизвестным причинам потеряется и придется переподключаться. И получится, что при каждой такой задержке лайти перестанет отсылать данные в сокеты и будет выглядеть, как будто сервер работает «рывками» — то работает на полной скорости, то вдруг подвиснет.

    Задумка


    Решить эту проблему сможет буферизация записей лога и распараллеливание задачи обработки этого лога. Реализовал я это так: всю подсистему логирования я разбил на две части — агрегатор и обработчик. Агрегатор будет отвечать за моментальное вычитывание из канала (pipe) записей лога (чтоб сервер не простаивал), запись этих данных в очередь (FIFO), порождение дочерних обработчиков (описаны далее) и собственно раздачу строк лога из очереди в эти обработчики. Обработчик же — это просто конечный процессор строк лога, который их вычитывает из канала stdin и пишет в базу, после этого сигнализирует агрегатору о том, что он освободился и готов к новой порции данных.

    Реализация


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

    processor.php:
    Copy Source | Copy HTML
    1. #!/usr/bin/php
    2. <?php
    3. $in = fopen('php://stdin', 'r'); //открыли стандартный ввод
    4. $db = NULL;
    5. while ($in_str = fgets($in)) { // вычитали строку со входа
    6.   if (@mysql_ping($db) !== true) { // если не соединены с БД - соединяемся
    7.     @mysql_close($db);
    8.     $db = mysql_connect();
    9.   }
    10.   mysql_query('тут происходит передача $in_str в БД'); // обрабатываем строку в БД
    11.   echo '1'; // говорим агрегатору, что готовы к новым данным
    12. }
    13. mysql_close($db);
    14. fclose($in);
    15. ?>

    Агрегатор же реализован несколько сложнее. Сначала опишу несколько стандартных возможностей PHP, которые были использованы.
    При чтении из потоков использовались неблокирующие вызовы. Для перевода поток в неблокирующий режим достаточно вызвать функцию stream_set_blocking с самим потоком в качестве первого элемента и с нулем в качестве второго. После этого вызвоа любое чтение или запись в поток будут завершаться моментально, не дожидаясь фактического чтения или записи. Результат выполнения этих операций будет зависить от фактического кол-ва байт, которые смогли быть записаны или прочтены.
    Наличие же данных в потоках отслеживалось с помощью функции stream_select, полное описание которой можно найти в руководстве по PHP. Вкратце ее суть вот в чем — в нее передаются три массива, каждый из которых содержит дескрипторы потоков, первый для чтения, второй для записи, третий для особых случаев. Вызов этой функции завершается, когда в одном из переданных потоков произойдет что-либо интересное (в общем случае — исчезнет блокирование операции соответствующей параметру фунции, т.е. в одном из дескрипторов чтения можно будет прочитать данные без блокировки и т. д.). Так же эта функция может завершиться по таймауту, который передается в виде четвертого и пятого параметров, где четвертый — секунды, а пятый — микросекунды.
    Теперь об алгоритме работы:
    1. Первым делом агрегатор создает дочерние процессы обработчиков и организует каналы для общения с ними.
    2. Затем обработчик в неблокирующем режиме читает данные из стандартного входа и записывает их в конец очереди.
    3. Проверяя наличие единицы на каналах чтения из обработчиков (опять же в неблокирующем режиме) взводятся нужные флаги в массиве готовности обработчиков.
    4. Если в очереди есть необработанные данные и есть свободный обработчик — отправляем данные в него и опускаем флаг его готовности в соответствующем массиве.
    5. Записываем все интересующие нас дескрипторы в массив и выполняем ожидание по ним с помощью stream_select с таймаутом в одну секунду.
    6. Проверяем очередь на наличие записей и стандартный ввод на конец данных. Если очередь не пуста или есть новые данные — Переходим к пункту 2.
    Вот исходный текст агрегатора:

    aggregator.php:
    Copy Source | Copy HTML
    1. #!/usr/bin/php
    2. <?php
    3. // кол-во обработчиков
    4. define('PROCESSORS_TO_SPAWN', 5);
    5. // полный путь к обработчику
    6. define('PROCESSOR_PATH', '/path/to/processor.php');
    7.  
    8. $in=fopen("php://stdin",'r');
    9. // переводим стандартный ввод в неблокирующий режим
    10. stream_set_blocking($in, 0);
    11.  
    12. // список флагов готовности обработчиков
    13. $processors_states = array_fill(0, PROCESSORS_TO_SPAWN, true);
    14. $processors = array();
    15. $proc_signal_pipes = array();
    16. $proc_data_pipes = array();
    17. $descriptorspec = array(
    18.   0 => array("pipe", "r"),
    19.   1 => array("pipe", "w"),
    20.   2 => array("file", "/dev/null", "a")
    21. );
    22. $buffer = array();
    23.  
    24. // запускаем обработчики и переводим канал чтения в неблокирующий режим
    25. while (count($processors) < PROCESSORS_TO_SPAWN) {
    26.   $processors[] = proc_open(PROCESSOR_PATH, $descriptorspec, $pipes, NULL, NULL);
    27.   stream_set_blocking($pipes[1], 0);
    28.   $proc_data_pipes[] = $pipes[0];
    29.   $proc_signal_pipes[] = $pipes[1];
    30. }
    31.  
    32. while(true) {
    33.   $in_str = fgets($in);
    34.   if($in_str !== false) {
    35.     // тут можно проверять валидность строки лога
    36.     if (true) {
    37.     $buffer[] = $in_str;
    38.     }
    39.   }
    40.   foreach ($processors_states as $proc_num => $processor_state) {
    41.     // в неблокирующем режиме проверяем готовность обработчика
    42.     if (fgets($proc_signal_pipes[$proc_num]) == '1') {
    43.       $processors_states[$proc_num] = true;
    44.     }
    45.   }
    46.   // пока есть свободные обработчики и очередь не пуста - скармливаем им данные
    47.   while (count($buffer) > 0 and
    48.     ($selected_proc = array_search(true, $processors_states)) !== false) {
    49.     $item = array_shift($buffer);
    50.     fwrite($proc_data_pipes[$selected_proc], $item);
    51.     $processors_states[$selected_proc] = false;
    52.   }
    53.   // если стандартный ввод закрыт и очередь пуста - завершаем работу
    54.   if (feof($in) and count($buffer) == 0) {
    55.     break;
    56.   }
    57.   $check_list = $proc_signal_pipes;
    58.   $check_list[] = $in;
    59.   // ожидаем данных для чтения на стандартном вводе или из одного из обработчиков
    60.   stream_select($check_list, $w = NULL, $e = NULL, 1);
    61. }
    62.  
    63. // закрываем обработчики и прибираемся
    64. foreach($processors as $proc_num => $proc) {
    65.   fclose($proc_data_pipes[$proc_num]);
    66.   fclose($proc_signal_pipes[$proc_num]);
    67.   proc_close($proc);
    68. }
    69. fclose($in);
    70.  
    71. ?>


    Послесловие


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

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

    Комментарии 105

      +2
      В закладки. И по плюсу в топик и карму.

      BTW, Раскрасил исходники, осталось скопипастить в топик: s-c.me/ji s-c.me/jh
        0
        Публикуюсь тут впервые. Буду знать как правильно выкладывать сырцы. Спасибо ;)
          0
          stream_set_blocking($in, );

          $processors_states = array_fill(, PROCESSORS_TO_SPAWN, true);

          Куда-то потерялись парочка аргументов. При раскраске или так и было?
            0
            $proc_data_pipes[] = $pipes[];

            Эта строка вообще приведет к ошибке. Ясное дело, что причина все та же — habrahabr.ru/blogs/server_side_optimization/54140/#comment_1444927
              0
              Хабр съедает раскрашенный ноль. Поправил.
            +4
            Поддержите отечественного производителя, ставьте nginx! ;)
              +7
              Топик не о холиварах.
              +1
              Почему нельзя было писать данные в файл, периодически считывая с него данные другим процессом?
                –2
                Во-первых, это лишняя операция и диск куда более медленное устройство чем память. Во-вторых, при описанной мной в топике нагрузке, файл будет очень быстро расти и его надо будет регулярно чистить. Все это лишние операции которые сильно усложнят схему работы.
                  0
                  Вы не подумайте, я не то, чтобы ради критики — мне статья очень понравилась, необычный подход к решению задачи.

                  Просто со временем начинаешь тяготеть к более простым вещам. В общем-то, небезосновательно.
                    0
                    ну файл можно располагать в памяти.
                    • НЛО прилетело и опубликовало эту надпись здесь
                        0
                        4) асинхронная работа с дисками, насколько я знаю, до сих пор не очень хорошо реализована — простого неблокирующегося write(2), насколько я знаю, нет. Возможно, я сильно ошибаюсь, но хотелось бы тогда услышать кейворды.
                        Про aio_{read,write,...} слышал, но не в самом лестном свете.
                          0
                          Можно её вручную организовать. Через fork'и. Не так уж и накладно в итоге получается, потому что пишушие/читающие процессы будут сидеть в ядре, и не будут расходовать много ресурсов на переключение контекстов.

                          А вот POSIX aio действительно штука шибко неудачная из-за необходимости насиловать программу обработкой сигналов. Но вот в Linux есть eventfd, например, что упрощает работу с aio. Вобщем, жить можно.
                            0
                            Процессы в ядре? С этого момента по подробнее :)
                              0
                              НУ… Это же, вроде, общеизвестно. В современных моноядерных ОС, когда вы делаете системный вызов (write, например), ваш процесс начинает работать в kernel-mode, исполняя код ядра.
                                0
                                Ааа… Вы про это… Я просто не так понял :) Извините.
                              0
                              Ну в линуксах времен 2.4.х aio тредами (на уровне glibc) собственно и реализовывался, треды даже полегче форков будут в контексте переключений контекста, про другие юниксы вообще ничего не знаю.

                              Про сидящий в ядре форкнувшийся процесс, на который не нужно переключение контекста — вообще понял плохо. Переключение mm-контекста всё равно будет происходить в добавок к переключению контекста cpu (по сравнению с тредами).
                                0
                                Вот как раз переключения mm и не будет — зачем, если процесс в ядре? Доступ к user-space данным организовывается изнутри ядра по 'физическим' адресам.
                                  0
                                  Если он всю жизнь в ядре сидит — конечно, но он из ядра довольно часто вылезать будет, нет?
                                    0
                                    Не будет. Он делает только write/read и exit. А логика обрабатывается уже в основном процессе, который управляет всеми этими асинхронностями.
                                      0
                                      Тогда я не очень хорошо понял вашу идею. Вы предлагаете вынести всю io-очередь в отдельный процесс (вместо того, чтоб полагаться на ядро) и взаимодействовать с ним через сокеты и общий mmap? Это же, как я понимаю, тот же aio в стиле linux-2.4.x, только в профиль.
                            +1
                            Пройдусь по пунктам :)
                            1) Вы забыли про буферизацию записи, а значит ваше умозаключение о равенстве частоты записей в секунду на диск будут ограничивать частоту записей в файл лога. Это ваше первое упущение. Но тем не менее, запись лога в файл не подходит для реализованной мной системы.
                            2) Учет данных в БД был мною описан как довольно простой. Могу сказать больше — осуществляется вставка записей в таблицу с одним первичным ключом. Что касается куска кода — то приведен он мной тут как раз по теме статьи, т.к. писал я о способе избавления от минусов однопоточного сервера. И код мой, как показала практика, справляется с задачей на ура. Напомню — топик мой о примере решения проблемы, а не обсуждение построенной мной системы.
                            3) Решение с СУБД отлажено и работает уже не первый год. И еще раз предлагаю почитать название топика — речь идет о решении проблем сложного логирования однопоточного сервера, а не обсуждение работы СУБД.
                            4) Напомню еще раз — писать в моей задаче нужно не в файл, а в БД. Т.к. необходим живой анализ и учет трафика. Если писать в файл (неважно какой, физический файл, mmap или еще что), а потом вычитывать оттуда данные и писать в БД — то это ничем не отличается от приведенного мною решения с записью данных в буфер памяти PHP. Разве что только PHP скрипт — это drop-in решение и не требует абсолютно никаких дополнительных действий при развертывании.
                            5) Вопрос отслеживания обработчиков и поднятия их после падения мною решен, просто я не стал это приводить в этом топике и оставил этот вопрос на решение тому, кто решит воспользоваться. Напомню — код никогда в статьях не приводится как конечный результат, а как подсказка и руководство к дальнейшим изысканиям. Готовые решения на SourceForge

                            И вот что добавлю. Не все записи что отдает сервер должны попадать в лог, и агрегатор с этим справляется — он отбрасывает ненужные записи до того как они попадают в обработчик и дополнительного ожидания обработки со стороны сервера н таких записях не происходит. Ну и про производительность решения в СУБД — если писатьв один поток данные в БД — то конечно буффер будет расти и система не будет успевать отправлять данные на хранение. Но если писать в несколько потоков — то мы получим многократное увеличение производительности, т.к. будут получены все плюсы concurent inserts в MySQL, а так же мы победим overhead от обработки запроса и подготовки его выполнения в СУБД.

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

                            А насчет ваших прогнозов о моем росте как специалиста — спасибо. Правда ваше высокомерие опять заставило вас ошибиться. Я уже являюсь разработчиком со стажем и руковожу весьма большим региональным проектом. Будьте сдержанней в высказываниях и заочной оценке собеседника.
                            • НЛО прилетело и опубликовало эту надпись здесь
                                –2
                                Нет, это как раз высокомерие.
                                Целью статьи я не ставил сравнительный анализ. Те кто столкнулся с похожей проблемой прекрасно поймут мое решение и не будет разводить демагогию о том какой у кого уровень. Вы же затеяли спор ради спора.
                                • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                не буду с вами спорить, просто приведу пример.
                                Oracle (знаете такую СУБД?) _все_ операции логирует. И пишет это все, знаете куда? угу, в файл :)
                                и чистит эти логи, и использует их для восстановления после сбоев.
                                так что вы еще раз подтвердили мое мнение по поводу программирования на php.
                                  0
                                  К чему все это было сказано? Суть комментария в констатации фактов?
                                    0
                                    суть комментария показать то, как вашу задачу решают в промышленном программировании
                                      0
                                      Нет, ваш комментарий имел суть указание того, что вы знаете что такое Oracle, и то что вы сомневаетесь в том, что я знаю о такой СУБД. Далее вы сказали что Oracle пишет лог транзакций. Более ничего вы не сказали и мысль свою никак не обозначили. Будьте конкретней.
                                        0
                                        эм…
                                        конкретней: вы реализовали глупость.
                                        это делается более привычными средствами.
                                        для меня Oracle — это система, на реализации которой можно учиться, потому я привел пример реализации такой же функционаьлности, как доказательство своей правоты.
                                          0
                                          Вы опять ничего не сказали. Если вам непоянта суть моего текст — поясню в двух словах — избавиться от нелостатков однопоточности лайти при логировании в пайп. То что вы написали — не несет никакого смысла. Разверните мысль.
                                            0
                                            Разворачиваю. Вот как нужно было делать:

                                            1) Реализуем логирование в файлы
                                            2) Реальзуем мониторинг файлов отдельным потоком и делаем с этими данными что хотим. кидайте их в базу, отправляйте по почте, все что душе угодно.

                                            результат: меньше зависимостей -> повышение надежности, упрощение

                                            что вас не устраивает в таком решении?
                                              0
                                              Как раз наоборот — больше зависимостей и больше ненужных операций. Зачем писать в файл когда можно писать в память?
                                                +1
                                                сорри, я заканчиваю на сегодня эту дискуссию.
                                                спасибо. вы подняли мне настроение. я понимаю, что работа у меня будет всегда.
                                                  0
                                                  Самоутвердились? Ладно бы написали что-либо осознанное, но кроме самозабвенного написания бессмысленных постов вы ничего не сделали.
                                                  0
                                                  Если так страшны дисковые операции, что мешает смонтировать tmpfs и писать в лог как в файл?
                                                  Неужели, за 30 секунд у Вас скопится столько логов, что это забьет всю память? А пропарсить файл легче, чем городить систему из пайпов и двух пхпшных программ.
                                                    0
                                                    А кто сказал легче. Вот ради того чтоб прекратить все эти «а если» — напишите аналогичную систему с использованием промежуточных файлов и приведите ее код. А еще расскажите сколько времни уходит на ее развертывание и настройку.
                                                    Для сравнения — мое решение достаточно скопировать на целевой сервер и прописать как цель ведения логов в конфиге вэб-сервера.

                                                    PS и почему «городить систему из пайпов и двух пхпшных программ»?? Пайпы создаются автоматически при запуске любого процесса. Да и сами программы сложно назвать программами. Это два коротеньких и предельно простых скрипта.
                                                      +2
                                                      1. Реализация на файлах не потребует агрегатора вообще, достаточно раз в Н времени запускать скрипт парсинга и делать truncate/ftruncate/переименовывать файл. По поводу lighthttpd не могу, честно говоря, ничего сказать, так как в основном используют nginx, который умеет по сигналу переоткрывать логи. Думаю, при грамотном использовании truncate, с лайтом проблем не возникнет, если он открывает логи с O_APPEND. А если нет — можно будет переписать и сделать так, чтобы открывал :)
                                                      2. Странно, ниже Вы писали, что Ваше решение как раз не готовое и его не надо копипастить в продакшн.
                                                      3.1. Про пайпы при создании каждого процесса, пожалуйста, поподробнее.
                                                      3.2. С простотой первого еще можно согласиться, но никак не с простотой второго. В то время, как вся система работы с файлами будет на 2 строчки длиннее первого скрипта.
                                                        –1
                                                        1) все слова это. Покажите решение. На написание моего у меня ушло 40 минут. Если ваша задумка проще — приведите все что необходимо для релизации.
                                                        2) Код приведенный выше — это код который нужно еще доработать в некоторых местах. Я же говорю про решение в целом, а не про пример кода. Решение простое и его достаточно просто поместить на сервер и казать как цель логирования.
                                                        3.1) Посмотрите в код мой. Пайпы создаются простым указанием на то что их нужно создать. Это никак не соответствует тому что вы написали «городить систему из пайпов». Что в этом сложного?
                                                        3.2) Код второго файла прост до неприличия. Если этот код вам кажется сложным значит в ыне умеете читать чужой код.

                                                        И еще раз повторюсь — хватит пустых слов, покажите пример организации с файлами. С вычиткой файла, с защитой от непредвиденного завершения. По вашим словам написание этого займет у вас не более 20-ти минут.
                                                          +1
                                                          2. Решение — абстрактное понятие. Его нельзя скопировать и использовать.
                                                          3.1. Пайпы мало создать, их еще использовать надо.
                                                          3.2. Все относительно (с) не я.

                                                          1,4: Вот Вам решение.
                                                          Copy Source | Copy HTML
                                                          1. #!/usr/bin/php
                                                          2. <?php
                                                          3. $log = '/dev/shm/mylog';
                                                          4. $db = mysql_connect();
                                                          5. $in = fopen($log, 'r');
                                                          6. while ($in_str = fgets($in))
                                                          7. {
                                                          8.     mysql_query('тут происходит передача $in_str в БД'); // обрабатываем строку в БД
                                                          9. }
                                                          10. // ifdef SUPPORT_SIG
                                                          11. fclose($in);
                                                          12. unlink($log);
                                                          13. posix_kill($master_pid, SIGUSR1);
                                                          14. // else
                                                          15. ftruncate($in, 0);
                                                          16. fclose($in);
                                                          17. // endif
                                                          18. }
                                                            –1
                                                            1) упала софтина — откуда узнаем где остановились в прошлый раз при обработке?
                                                            2) мы пстоянно читаем, а чистим файл только когда все прочитали. Мы так можем никогда до чистки не дойти

                                                            Исправьте эти два допущения и получите код такой же длинны как и в моем варианте.
                                                              +1
                                                              1. Не понял. Что упало? Этот скрипт надо запускать по крону раз в Н времени. Падать при обработке она не должна. Если упал сам сервер — вариант с пайпом этого недостатка отнюдь не лишен.
                                                              2. Если у Вас логи генерируются быстрее, чем их можно парсить, надо задумываться над масштабированием на кластер/кластер больше/другую архитектуру.
                                                                0
                                                                Так это по крону? Тогда это не подходит к моему условию. Мне необходимо вносить записи лога с минимальными задержками.
                                                                  +2
                                                                  30секундная задержка — это много?
                                                                  И, все-таки, любопытно, назовите задачу, требующую такого странного логгирования.
                                                                    0
                                                                    Я же писал — файлхостинг. Причем с очень жесткими требованиями по учету трафика (требования эти не я выдвигал, но следовать им должен).
                                                                      –1
                                                                      Если задача такая — то я вообще не понимаю, где тут проблема.
                                                                      Считайте не после скачки, а до: пускай пользователю дается не прямая ссылка на файл, а ссылка на php-скрипт, пишущий заветную строку в базу и отдающий X-Accel-Redirect.
                                                                        +1
                                                                        Опять же внимательное чтение топика избавит от необходимости задавать этот вопрос. Я написал что одно из значений, которое необходимо для учета — фактическое кол-во байт переданное за соединение. Эти данные доступны только после завершения соединения.
                                                                          0
                                                                          Ну так и парсите еррор-лог на предмет разорванных до передачи всего файла соединений.
                                                                            0
                                                                            А вот это уже мягко говоря бред. Во-первых в errorlog не попадают данные о предварительно разорванных соединениях, т.к. это вовсе не ошибки а нормальное поведение http соединения (клиент может выкачать часть файла). Во-вторых, даже если бы данные о разрыве соединения писались бы в какой то лог — то это довольно сильное усложнение парсинга, т.к. данные из этого лога необходимо было бы сопоставлять с данными от скрипта.
                                                                            Вы зачем-то прицепились к идее записи лога в файл и парсинга его, хотя это вовсе и не просто, да и не имеет никаких преимуществ по сравнению с моей реализацией.
                                                                            Опять же спор ради спора.
                                +1
                                На деле всё сложнее. Диск, конечно, устройство медленное, только в современных ОС файл — это не просто данные на диске. Эти данные ещё кэшируются в RAM (при чём кэшируются весьма агрессивно), что уже делает файлы не такими-уж-и-медленными, а работа с закэшированными данными осуществляется с хитрыми оптимизациями, вроде zero-copy. Поэтому не так уж и страшно активно использовать файлы. Чистить же их не нужно будет. Существует известная техника logrotate.
                                  –1
                                  При том размере логов которые накапливаются за полчаса работы на моем ресурсе — говорить о кешировании уже не приходится.
                                  Про logrotate уже написано в комментариях (их нужно читать, чтоб не задавать одинаковые вопросы), так что основная проблема с файлами как раз в их чистке.
                                    +1
                                    Дело не в общем размере лога, а в объёме одной записи. Вы что, по сотне мегабайт за один write скидываете? А когда вы скидываете помаленьку, то происходит как раз асинхронная работа с диском. Ядро системы записывает старые данные на диск, а новые небольшие порции складывает в кэш. И это всё происходит параллельно. При этом кэш — это весьма нетривиальная штука, она собирает статистику, подстраивается под профиль работы ваших программ, чтобы действовать эффективнее.

                                    Вот если бы вы записывали по гигабайту за раз, тогда да. Кэш не помог бы. Но это же не ваш случай. А с logrotate я так и не понял, в чём вы видите проблему.
                                      –1
                                      Повторю в сто десятый раз — а кто чистить будет все это? Вопрос не в том сколько за раз пишется, а в том что за час накопится много записей. Зачем писать в файл и дополнительно организовывать себе геморрой с чисткой из него отработанных данных, если можно писать в память и раскидывать данные оттуда.
                                        +1
                                        А какие проблемы с чисткой? Вы организуете лог в виде набора файлов с номерами log_{1,2,3,...}, когда ваш анализатор логов очередной том парсит и загружает в базу данных, вы удалаете log с этим номером. В качестве номера можно использовать дату. А в логи писать только те данные, которые вы хотите загрузить в БД. Примерно такая схема.

                                        IMHO, ваше решение точно так же работает, только оно вместо файлов складывает всё в память. Но в этом случае вы в своём приложении делаете повторно ту же самую работу, что может сделать за вас и OS. Да ещё при этом теряете часть гибкости, которая была бы, если бы вы работали с файлами. Например, можно было бы кроме аплоадера в базу данных прикрутить к анализу файлов процесс, который выявлял бы какие-нибудь атаки на сайт и высылал бы письмо админу.
                                          –2
                                          А кто будет расскладывать лог в файлы 1, 2, 3? logrotate — не подходит, т.к. требует перезапуска сервера. Если писать в эти файлы через программу-посредника, то получается вариант как у меня 1к1, разве что только запись будет в файл а не в память.
                                            0
                                            Да. Посредник нужен будет. Но он будет простым — пишет в логи и всё. Без этого мощного цикла раздачи работ процессам. Раздачу можно заменить на более простую работу с файлами. А с абстрактной точки зрения, согласен, один в один.
                                          0
                                          Отвечаю в стоодиннадцатый — вы рассматривали вариант с ftruncate() лога?
                                            –1
                                            А как очистка файла лога скажется на записи данных в этот файл сервером? Подумайте.
                                              +2
                                              А как? Нормально скажется — файл очистится и сервер будет писать с начала файла.
                                              В худшем случае можно потерять несколько записей которые будут записаны между моментом вычитки лога аггрегатором и truncate — но и этот race можно обойти, насколько я знаю.

                                              Hint: inode не меняется а логи открываются обычно с O_APPEND.
                                                0
                                                Лайти очень сильно глючит с записью логов если их вручную обрезать. Пробовал этот вариант — абсолютно не годится.
                                                  0
                                                  Гм… судя по коду, глючить особо не должен, хотя могу ошибаться.

                                                  А как именно глючит и как обрезали? Можете развернуть мысль?
                                                    0
                                                    После truncate теряется около 10 записей, что в моем случае абсолютно неприемлемо.
                                                      0
                                                      А mandatory lock ставить пробовали перед последним read? Он, правда, не POSIX и в нём свой race есть, но в данном случае он, как я понимаю, не мешает.
                                                        0
                                                        Может и поможет. Но повторюсь, как я уже неоднократно говорил — я написал drop-in решение не требующее перекомпиляции ни лайти, ни ядра (это я к предложению одного товарищя увеличить буфер под пайп). Просто положил, настроил в конфиге — и заработало.
                                                          +1
                                                          Чёрт! Я только сейчас понял шутку про drop-in.

                                                          drop-in
                                                          вставка паразитного сигнала в полезный
                                                          (Лингво11, Telecoms)
                                0
                                Имхо, конечно.

                                В случае каких-то проблем с Вашим кодом не любой программист сразу поймёт что и как он делает. В случае с файлом — 99% сразу поймут что к чему.

                                Т.е. тут вопрос эффективности другой части системы — человека. Усложняя код Вы усложняете систему для человека.

                                Память конечно быстрее, никто не спорит — можно было запустить демона, который бы писал данные буферизованно, экономя и время и кол-во обращений к диску.

                                Чистить файл не обязательно, есть стандартные инструменты типа logrotate. Перевернул лог, удалил старый, никаких проблем.
                                  +1
                                  Согласен, демон на С, который вычитывает данные и пишет их в какой-нибудь memory-mapped фаил, было бы куда эффективнее.
                                    –1
                                    Вопрос в легкости развертывания кода. PHP-скрипт — это drop-in решение. Да и в скорости работы не уступает нативному решению (я проверял, т.к. изначальный вариант был написан на C)
                                      0
                                      Беда PHP в том, что в нём никто не проверяет ошибки. Вот, например, у вас код возврата select не анализируется :-)
                                      То ли дело питон — просто вылетит исключение, всё в лучших традициях fail early, fail often.
                                        –2
                                        А зачем анализировать возврат stream_select? Он тут служит не для выбора обновившегося потока, а просто как способ задержки.
                                        Внимательно изучите код.
                                          +1
                                          Хабрапарсер съел шутливый тэг <holywar> в моём сообщении, ну да ладно.

                                          Дело в том, что вы какие-то массивы с файловыми дескрипторами вроде передаёте в select… А вот представьте, что в один прекрасный момент какой-то из файловых дескрипторов станет невалидным — тогда select() вернет ошибку без задержки и может быть весь код зациклится в busy-loop (реальный случай из практики).

                                          Ну и, на мой взгляд, в программировании вопроса «а зачем анализировать код ошибки» вообще не должно возникать.
                                            –1
                                            Этот код — заготовка для дальнейшего развития. У меня на рабочих серверах реализовано все что надо — и отслеживание умерших обработчиков, и их перезапуск…
                                            Код из топика может взять любой желающий и развивать далее.
                                              +1
                                              Вот-вот, поэтому я и недолюбливаю примеры как без обработки ошибок так и без упоминания о том, что её сделать не плохо было бы — мифический «желающий» пример скопипастит, а потом в один прекрасный момент наступает горе.

                                              Если на боевых серверах всё обрабатывается — замечательно.
                                                0
                                                А копипастеру поделом. Кто же чужой код в продакшн кидает без анализа и осмысления???
                                                  –1
                                                  А копипастер и не рассчитывал, что его небольшой опенсурцный проект будет использоваться кем-то кроме его самого =)

                                                  itblogs.ru/blogs/the7ofdiamonds/archive/2007/04/24/15634.aspx
                                                    0
                                                    Хых, весело написано. :)
                                    –1
                                    logrotate подразумевает временную остановку сервера. В случае с нагруженным ресурсом — это не вариант.
                                      0
                                      Не совсем так.

                                      Насчёт лайти говорить не буду, т.к. не знаю, а nginx по USR1 просто переоткрывает логи, без остановки.

                                      Управление nginx
                                        0
                                        А вот лайти надо перезапускать…
                                    0
                                    Жаль, что нгинкс не рекомендуется запускать с записью лога в пайп… хотя, может это и есть обход той самой проблемы?
                                      0
                                      И правильно не рекомендуют — что делать, если пайп умирает? В противном случае вебсервер должен брать на себя роль «daemontools» и сам запускать процесс, читающий пайп.
                                        0
                                        Кстати, согласно багтраку лайти так не поступает, при том баге уже почти два года.
                                        Всё это какбэ говорит нам «писать логи в пайп — не лучшая идея».
                                    • НЛО прилетело и опубликовало эту надпись здесь
                                        0
                                        К слову, функция mysql_ping сама восстанавливает подключение к mysql серверу, если оно было утeряно к моменту проверки. И только в случае невозможности его установить возвращается false;
                                          +2
                                          Since MySQL 5.0.13, automatic reconnection feature is disabled.
                                          +1
                                          я решау проблему переправки логов и обработки так:

                                          на сервере где нужно читать лог:
                                          tail -f logFile | nc ListenLoggerIp LoggerPort

                                          ну а на ListenLoggerIp: LoggerPort сидит монстрик на java и всё это обрабатывает…
                                            +4
                                            мысли:

                                            С агрегатором всё понятно, его основная цель — увеличить объём буфера, как бы продлить буфер pipe.

                                            Просто так, к сожалению, нельзя увеличить объём буфера пайпа (по-умолчанию он вроде 4096), хотя можно пропатчить ядро :) В этом случае агегатор будет не нужен.

                                            а вот с обработчиком… Не думаю, что что 5 обработчиков справятся со скармливанием потока БД лучше, чем один.

                                            Попробуйте такой вариант — один обработчик, работающий по след. схеме:
                                            Обработчик накапливает некоторый объём данных для слива в БД, к примеру до 100 штук но в течении не более 1-2 секунд.

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

                                            Суть вот в чём — база всё равно одна, нет смысла тискать её 5-ю обработчиками — быстрее не получится, скорее наоборот.
                                              0
                                              Идея для накопления данных в обработчике и составления большого запроса на вставку — интересна.
                                              Вот только насчет записи в БД несколькими потоками вы ошиблись. Опыт показал что это позволяет вносить записи гораздо быстрее.
                                                +1
                                                > Опыт показал что это позволяет вносить записи гораздо быстрее.
                                                Если каждый агрегатор будет добавлять по-одной строчке, то да, может быть.
                                                Если пачками — то, уверен, лучше будет работать один процесс.
                                                  0
                                                  Это точно. Попробую поработать в этом направлении.
                                                    +1
                                                    У PostgreSQL операция bulk insert (COPY) работает сильно быстрее последновательных INSERT. У MySQL тоже какая-то такая операция должна быть
                                              0
                                              Ээээ а нельзя ли в системе увеличить объем буфера дял пайпа (или как там это называется), чтобы его хватало для отправляемых данных? Или данных в лог пишется так много?

                                              p.s. Прочео в комментах, что там есть захаркоденное дурное ограничение, имхо — лучше патчить ядро, наверняка достаточно пару цифр поменять.
                                                0
                                                При этом увеличится размер всех буферов всех пайпов в системе. Не очень бы этого хотелось.
                                                  +1
                                                  Не, насколько я знаю не всё так просто, там размер буфера завязан на PAGE_SIZE — это раз, можно увеличить размеры ВСЕХ очередей, а не избранной, что может привести к интенсивному использованию памяти и доп. тормозам (пайпы ну очень часто используются в линукс-подобных системах)- это два.
                                                  0
                                                  Ужос!
                                                  В nginx-ru не раз обсуждалась тема и первым ответом авторам подобных решений было «пишите в файл». Почему? Ну, предположим, аггрегатор умер по какой-то причине. Хоп, и лайти прилетает SIGPIPE при записи в лог — с ненулевой вероятностью лайти от такой невежливости возьмет и умрёт.

                                                  Вот вы говорите, что надо перезапускать лайти для переоткрытия файлов — это, конечно, никуда не годится (неужели он действительно дропает соединения при SUGHUP? На форуме пишут обратное), но если допустимо потерять одну-две записи в лог, можно использовать ftruncate после того, как лог распарсен, если, конечно лайти открывает логи в режиме APPEND.
                                                    0
                                                    Лайти перезапускает программу для записи в пайп при ее падении.
                                                    «Вежливая» перезагрузка сервера подразумевает прекращение приема новых соединений до момента пока сервер не отработает все оставшиеся соединения и не перезапустится.
                                                      0
                                                      Лайти перезапускает пайп при падении? А почему же соответствующий баг не закрыт?
                                                        0
                                                        Представления не имею почему багрепорт не закрыт. Может забыли про него :) Но факт остается фактом — перезапуск происходит. В trunc версии точно.
                                                          0
                                                          Ну и славно тогда. В любом случае, архитектуру лайти я не знаю настолько хорошо, чтоб знать достоверно о том, насколько прямо и/или криво там весь этот перезапуск и обработка реализована и что происходит, если данные уходят в буфер пайпа, а клиент их не вычитывает и умирает.
                                                    0
                                                    можно развязать сервер и БД через шину обмена сообщениями вроде memcacheq или spread
                                                      0
                                                      Можно, но это лишний инструмент. И выполняет те-же функции что и мой код.
                                                      0
                                                      а тут RRDtool вам никак не поможет?
                                                        +1
                                                        У RRDtool немного другие задачи и область применения.
                                                        0
                                                        Сегодня в рассылке nginx'a была затронута схожая тема, описано неплохое решение: www.lexa.ru/nginx-ru/msg23177.html

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

                                                        Самое читаемое