Хочу рассказать об очередном результате моих изысканий в области оптимизации производительности Web-серверов.
На этот раз речь пойдет об оптимизации сложного логирования в однопоточном неблокирующем вэб-сервере.
Все не раз слышали о том, как lighttpd (далее «лайти») производителен и нетребователен к ресурсам. В последнем пререлизе версии 1.5 появилось много нововведений: в частности увеличена была производительность отдачи увесистых файлов на очень загруженных серверах (более 2-х тысяч потоков с суммарной скоростью в 50-200 МБ/с). Я, как управляющий весьма большим вэб-файлохранилищем, не мог пройти мимо таких возможностей. На тестовую машину с быстрым рейдом и двумя GigabitEthernet адаптерами, завязанными в bond интерфейс с распределением нагрузки, был поставлен дистрибутив Debian линукс. После пары часов проб и ошибок была собрана и настроена на оптимальную производительность связка лайти+php_fcgi. Файлы действительно отдавались очень быстро и 2000 потоков для лайти не оказались трудностями. Но тут я наткнулся на серьезную проблему.
Дело в том, что одно из требований к веб-серверу для работы на файлохранилище — чтоб он имел возможность отправлять строки лога через pipe в специализированную программу, которая бы их особым образом обрабатывала. В этой программе обрабатываться данные о соединении (что за пользователь качал, сколько действительно байт выкачанно за это соединение и т.д.) и отправляются для хранения в БД. Лайти умеет перенаправлять данные через pipe в программу для логирования, но проблема кроется в его однопоточной неблокирующей архитектуре. Т.к. лайти работает в один поток (процесс) и в неблокирующем режиме только отправляет данные в сокеты — то его основной поток уходит в ожидание снятия блока записи пока вспомогательная программа не обработает предыдущую запись лога. А программа эта может подвисать — поскольку соединение с БД — вещь не постоянная: то запрос подвиснет из-за табличной или строчной блокировки, то само соединение по неизвестным причинам потеряется и придется переподключаться. И получится, что при каждой такой задержке лайти перестанет отсылать данные в сокеты и будет выглядеть, как будто сервер работает «рывками» — то работает на полной скорости, то вдруг подвиснет.
Решить эту проблему сможет буферизация записей лога и распараллеливание задачи обработки этого лога. Реализовал я это так: всю подсистему логирования я разбил на две части — агрегатор и обработчик. Агрегатор будет отвечать за моментальное вычитывание из канала (pipe) записей лога (чтоб сервер не простаивал), запись этих данных в очередь (FIFO), порождение дочерних обработчиков (описаны далее) и собственно раздачу строк лога из очереди в эти обработчики. Обработчик же — это просто конечный процессор строк лога, который их вычитывает из канала stdin и пишет в базу, после этого сигнализирует агрегатору о том, что он освободился и готов к новой порции данных.
Начну с реализации самой простой части — обработчика. Все что он должен — это открыть stdin в блокирующем режиме и в цикле вычитывать из него строки, посылая символ '1' в конце каждой итерации. Завершать работу он должен по обнаружению eof в канале stdin (канал закрыт и данных больше нет). Вот код этой программы:
Агрегатор же реализован несколько сложнее. Сначала опишу несколько стандартных возможностей PHP, которые были использованы.
При чтении из потоков использовались неблокирующие вызовы. Для перевода поток в неблокирующий режим достаточно вызвать функцию stream_set_blocking с самим потоком в качестве первого элемента и с нулем в качестве второго. После этого вызвоа любое чтение или запись в поток будут завершаться моментально, не дожидаясь фактического чтения или записи. Результат выполнения этих операций будет зависить от фактического кол-ва байт, которые смогли быть записаны или прочтены.
Наличие же данных в потоках отслеживалось с помощью функции stream_select, полное описание которой можно найти в руководстве по PHP. Вкратце ее суть вот в чем — в нее передаются три массива, каждый из которых содержит дескрипторы потоков, первый для чтения, второй для записи, третий для особых случаев. Вызов этой функции завершается, когда в одном из переданных потоков произойдет что-либо интересное (в общем случае — исчезнет блокирование операции соответствующей параметру фунции, т.е. в одном из дескрипторов чтения можно будет прочитать данные без блокировки и т. д.). Так же эта функция может завершиться по таймауту, который передается в виде четвертого и пятого параметров, где четвертый — секунды, а пятый — микросекунды.
Теперь об алгоритме работы:
В итоге мы получили отличный инструмент для организации сколь угодно сложного логирования для однопоточных серверов, который дает нам возможность использовать все преимущества сервера, не задерживаясь на сохранении записей лога.
На этот раз речь пойдет об оптимизации сложного логирования в однопоточном неблокирующем вэб-сервере.
Введение
Все не раз слышали о том, как 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
- #!/usr/bin/php
- <?php
- $in = fopen('php://stdin', 'r'); //открыли стандартный ввод
- $db = NULL;
- while ($in_str = fgets($in)) { // вычитали строку со входа
- if (@mysql_ping($db) !== true) { // если не соединены с БД - соединяемся
- @mysql_close($db);
- $db = mysql_connect();
- }
- mysql_query('тут происходит передача $in_str в БД'); // обрабатываем строку в БД
- echo '1'; // говорим агрегатору, что готовы к новым данным
- }
- mysql_close($db);
- fclose($in);
- ?>
Агрегатор же реализован несколько сложнее. Сначала опишу несколько стандартных возможностей PHP, которые были использованы.
При чтении из потоков использовались неблокирующие вызовы. Для перевода поток в неблокирующий режим достаточно вызвать функцию stream_set_blocking с самим потоком в качестве первого элемента и с нулем в качестве второго. После этого вызвоа любое чтение или запись в поток будут завершаться моментально, не дожидаясь фактического чтения или записи. Результат выполнения этих операций будет зависить от фактического кол-ва байт, которые смогли быть записаны или прочтены.
Наличие же данных в потоках отслеживалось с помощью функции stream_select, полное описание которой можно найти в руководстве по PHP. Вкратце ее суть вот в чем — в нее передаются три массива, каждый из которых содержит дескрипторы потоков, первый для чтения, второй для записи, третий для особых случаев. Вызов этой функции завершается, когда в одном из переданных потоков произойдет что-либо интересное (в общем случае — исчезнет блокирование операции соответствующей параметру фунции, т.е. в одном из дескрипторов чтения можно будет прочитать данные без блокировки и т. д.). Так же эта функция может завершиться по таймауту, который передается в виде четвертого и пятого параметров, где четвертый — секунды, а пятый — микросекунды.
Теперь об алгоритме работы:
- Первым делом агрегатор создает дочерние процессы обработчиков и организует каналы для общения с ними.
- Затем обработчик в неблокирующем режиме читает данные из стандартного входа и записывает их в конец очереди.
- Проверяя наличие единицы на каналах чтения из обработчиков (опять же в неблокирующем режиме) взводятся нужные флаги в массиве готовности обработчиков.
- Если в очереди есть необработанные данные и есть свободный обработчик — отправляем данные в него и опускаем флаг его готовности в соответствующем массиве.
- Записываем все интересующие нас дескрипторы в массив и выполняем ожидание по ним с помощью stream_select с таймаутом в одну секунду.
- Проверяем очередь на наличие записей и стандартный ввод на конец данных. Если очередь не пуста или есть новые данные — Переходим к пункту 2.
aggregator.php:
Copy Source | Copy HTML
- #!/usr/bin/php
- <?php
- // кол-во обработчиков
- define('PROCESSORS_TO_SPAWN', 5);
- // полный путь к обработчику
- define('PROCESSOR_PATH', '/path/to/processor.php');
-
- $in=fopen("php://stdin",'r');
- // переводим стандартный ввод в неблокирующий режим
- stream_set_blocking($in, 0);
-
- // список флагов готовности обработчиков
- $processors_states = array_fill(0, PROCESSORS_TO_SPAWN, true);
- $processors = array();
- $proc_signal_pipes = array();
- $proc_data_pipes = array();
- $descriptorspec = array(
- 0 => array("pipe", "r"),
- 1 => array("pipe", "w"),
- 2 => array("file", "/dev/null", "a")
- );
- $buffer = array();
-
- // запускаем обработчики и переводим канал чтения в неблокирующий режим
- while (count($processors) < PROCESSORS_TO_SPAWN) {
- $processors[] = proc_open(PROCESSOR_PATH, $descriptorspec, $pipes, NULL, NULL);
- stream_set_blocking($pipes[1], 0);
- $proc_data_pipes[] = $pipes[0];
- $proc_signal_pipes[] = $pipes[1];
- }
-
- while(true) {
- $in_str = fgets($in);
- if($in_str !== false) {
- // тут можно проверять валидность строки лога
- if (true) {
- $buffer[] = $in_str;
- }
- }
- foreach ($processors_states as $proc_num => $processor_state) {
- // в неблокирующем режиме проверяем готовность обработчика
- if (fgets($proc_signal_pipes[$proc_num]) == '1') {
- $processors_states[$proc_num] = true;
- }
- }
- // пока есть свободные обработчики и очередь не пуста - скармливаем им данные
- while (count($buffer) > 0 and
- ($selected_proc = array_search(true, $processors_states)) !== false) {
- $item = array_shift($buffer);
- fwrite($proc_data_pipes[$selected_proc], $item);
- $processors_states[$selected_proc] = false;
- }
- // если стандартный ввод закрыт и очередь пуста - завершаем работу
- if (feof($in) and count($buffer) == 0) {
- break;
- }
- $check_list = $proc_signal_pipes;
- $check_list[] = $in;
- // ожидаем данных для чтения на стандартном вводе или из одного из обработчиков
- stream_select($check_list, $w = NULL, $e = NULL, 1);
- }
-
- // закрываем обработчики и прибираемся
- foreach($processors as $proc_num => $proc) {
- fclose($proc_data_pipes[$proc_num]);
- fclose($proc_signal_pipes[$proc_num]);
- proc_close($proc);
- }
- fclose($in);
-
- ?>
Послесловие
В итоге мы получили отличный инструмент для организации сколь угодно сложного логирования для однопоточных серверов, который дает нам возможность использовать все преимущества сервера, не задерживаясь на сохранении записей лога.