Для начала вопрос: есть подозрение, что операция fork() в Перле — довольно тормознутая вещь. Так ли это, и если так, то есть ли более-менее эффективные приёмы многопоточной обработки данных, не использующие fork()?
А теперь более подробно. Имеется простенький веб-сервер на Перле под Linux. В качестве иллюстративного примера я сделал урезанный вариант, который только и умеет, что принять GET-запрос, породить дочерний процесс, который займётся обработкой запроса, и вернуться к ожиданию нового подключения. Обработка же в дочернем процессе заключается в отправке клиенту содержимого HTML-файлика или XML-данных. Вот почти полный код примера:
Скрытый текст
my $server = new IO::Socket::INET(Proto => 'tcp',
LocalPort => 1234,
Listen => SOMAXCONN,
Reuse => 1);
while (my $client = $server->accept()) {
# Читаем первую строку HTTP-запросаmy $req_line = <$client>;
$req_line =~ s/[\r\n]//g;
# Читаем остальное (обработка POST-запросов выкинута из данного примера для простоты)my $line;
while (<$client>) {
s/[\r\n]//g;
last if (m/^$/);
}
if (fork() != 0) {
# Родительский процесс закрывает сокет и ждёт следующего коннектаclose($client);
}
else {
# Дочерний процесс занимается обработкойif ($req_line =~ m/^GET \/test HTTP\/\d\.\d$/) {
# Для урла вида http://example.com:1234/test отдаём заранее приготовленный файликmy $data;
open(FILE, '/home/user/public_html/test.html');
read(FILE, $data, -s FILE);
close(FILE);
print $client 'HTTP/1.0 200 OK' . Socket::CRLF . 'Content-Type: text/html' . Socket::CRLF . Socket::CRLF;
print $client $data;
}
else {
# Для всех остальных запросов вываливаем простенький XMLprint $client 'HTTP/1.0 200 OK' . Socket::CRLF . 'Content-type: text/xml' . Socket::CRLF . Socket::CRLF;
print $client '<?xml version="1.0" encoding="utf-8"?><root><success/></root>';
}
# Завершаем дочерний процессclose($client);
close($server);
exit;
}
}
В файле test.html, который отправляется клиенту, находится JS-код, отсылающий серверу сотню AJAX-запросов и засекающий время, потраченное на их обработку. Вот упрощённый код скрипта:
Скрытый текст
var cnt = 100;
var rest = cnt;
var t1;
function start_test() {
t1 = (new Date()).getTime();
for (var i = 0; i < cnt; ++i)
ajax_call_get("/ajax?param=" + i);
// Функция ajax_call_get — из самописного AJAX-фреймворка: посылает серверу запрос,
// и при отсутствии ошибок вызывает пользовательскую функцию-обработчик с именем ajaxProcessResult.
}
function ajaxProcessResult(responseXML) {
--rest;
if (rest == 0) {
var t2 = (new Date()).getTime();
alert("Прошло " + (t2 - t1) + " мс");
}
}
setTimeout("start_test()", 3000);
Так вот, запускаю я этот сервер, открываю в браузере урлу /test, через некоторое время получаю сообщение с измеренной длительностью, каковая в моём случае составляет от 11,5 до 12 секунд. Если же я отказываюсь от многопоточности (т.е. основной поток выполнения делает всё то, что делал дочерний процесс), то время выполнения тех же самых ста запросов резко падает до 0,7–0,8 секунды, т.е. в 15 раз, больше, чем на порядок! В реальном коде, конечно, обработка более мудрёная, чем в вышеприведённом примере, но цифры остаются почти такими же, и скорость реакции веб-приложения даже визуально отличается очень и очень сильно.
Очевидно, что оставлять однопоточный веб-сервер ни в коем случае нельзя: отдельные запросы могут выполняться по нескольку минут, а то и часов (специфика веб-приложения), и иметь сдохший интерфейс в течение всего этого времени совершенно недопустимо. Но и настолько терять в производительности тоже не хочется, так что имею желание разобраться в причинах и найти какой-нибудь компромисс.
У меня есть подозрение, что в падении производительности виноват сам вызов fork(), а точнее — дублирование контекста. Напрямую проверить это я не могу, но когда я удалил из сервера часть стартового кода, инициализирующего разного рода структуры (благо в моём мини-примере они никак не используются), используемая процессом память снизилась с 14,5 метров до 8-ми, а измеренное время той же сотни запросов упало почти вдвое: до шести секунд. (Сразу скажу: перенос этой инициализации внутрь форка не подходит, т.к. эти данные нужны при обработке практически каждого запроса, а повторная их реинициализация на каждом запросе только замедляет выполнение, это я замерил). Моя последняя выдумка: выносить в форк исключительно обработку динамических запросов и отдачу больших файлов, а всякую статическую мелочь отдавать однопоточно. Это, действительно, дало неплохой прирост в скорости отклика интерфейса, поскольку на каждую страницу обычно приходится только один динамический запрос, все остальные — подгрузка картинок, скриптов и прочей шелухи.
Тем не менее, хочется идеала. Собственно, вопрос: можно ли как-то решить эту проблему, и если можно, то как?
Здравствуйте, CaptainFlint, Вы писали:
CF>Для начала вопрос: есть подозрение, что операция fork() в Перле — довольно тормознутая вещь.
Не должно быть. По крайней мере в тех пределах, которые у вас получаются.
Хотя я не в курсе специфики именно Perl — может, он что-то серьёзно в этом случае перемучивает в своих данных...
CF> Так ли это, и если так, то есть ли более-менее эффективные приёмы многопоточной обработки данных, не использующие fork()?
Если вспоминать HTTP сервера, то у апача prefork MPM создаёт fork'ом пачку процессов (их число может динамически меняться в зависимости от нагрузки), каждый из которых отрабатывает запросы последовательно.
CF>Так вот, запускаю я этот сервер, открываю в браузере урлу /test, через некоторое время получаю сообщение с измеренной длительностью, каковая в моём случае составляет от 11,5 до 12 секунд. Если же я отказываюсь от многопоточности (т.е. основной поток выполнения делает всё то, что делал дочерний процесс), то время выполнения тех же самых ста запросов резко падает до 0,7–0,8 секунды, т.е. в 15 раз, больше, чем на порядок! В реальном коде, конечно, обработка более мудрёная, чем в вышеприведённом примере, но цифры остаются почти такими же, и скорость реакции веб-приложения даже визуально отличается очень и очень сильно.
Это сильно недостаточный анализ. Вы таким образом не можете установить, проблема в нагрузке или в каких-то задержках.
Попробуйте запустить, например, 5 таких тестов впараллель и сравните времена в двух вариантах исполнения. Если проблема именно в нагрузке, у вас каждый из тестов потратит десятки секунд (например, 60), если в задержках — каждый потратит ненамного больше тех же 12 секунд.
При этом тесте померяйте загрузку системы — затраты на user, system...
CF>Очевидно, что оставлять однопоточный веб-сервер ни в коем случае нельзя: отдельные запросы могут выполняться по нескольку минут, а то и часов (специфика веб-приложения), и иметь сдохший интерфейс в течение всего этого времени совершенно недопустимо. Но и настолько терять в производительности тоже не хочется, так что имею желание разобраться в причинах и найти какой-нибудь компромисс.
CF>У меня есть подозрение, что в падении производительности виноват сам вызов fork(), а точнее — дублирование контекста. Напрямую проверить это я не могу, но когда я удалил из сервера часть стартового кода, инициализирующего разного рода структуры (благо в моём мини-примере они никак не используются), используемая процессом память снизилась с 14,5 метров до 8-ми, а измеренное время той же сотни запросов упало почти вдвое: до шести секунд.
Ну а измерить напрямую? Снимите время прямо перед fork() и после него, запишите разницу...
CF>Тем не менее, хочется идеала. :) Собственно, вопрос: можно ли как-то решить эту проблему, и если можно, то как?
Куда копать — вам уже было несколько подсказок — действуйте по ним, потом расскажете, что накопалось.
Здравствуйте, netch80, Вы писали:
CF>> Так ли это, и если так, то есть ли более-менее эффективные приёмы многопоточной обработки данных, не использующие fork()?
N>Есть. Можно треды плодить, можно автоматы переключать... N>старое доброе, не теряющее актуальности.
N>Если вспоминать HTTP сервера, то у апача prefork MPM создаёт fork'ом пачку процессов (их число может динамически меняться в зависимости от нагрузки), каждый из которых отрабатывает запросы последовательно.
Думал о префорке, но остановила сложность реализации. У меня всё-таки не проект уровня Апача.
Насчёт тредов — где-то я слышал, что они внутрях перла теми же форками реализованы. Это не так? Попробую поэкспериментировать в этом направлении…
CF>>Так вот, запускаю я этот сервер, открываю в браузере урлу /test, через некоторое время получаю сообщение с измеренной длительностью, каковая в моём случае составляет от 11,5 до 12 секунд. Если же я отказываюсь от многопоточности (т.е. основной поток выполнения делает всё то, что делал дочерний процесс), то время выполнения тех же самых ста запросов резко падает до 0,7–0,8 секунды, т.е. в 15 раз, больше, чем на порядок! В реальном коде, конечно, обработка более мудрёная, чем в вышеприведённом примере, но цифры остаются почти такими же, и скорость реакции веб-приложения даже визуально отличается очень и очень сильно.
N>Это сильно недостаточный анализ. Вы таким образом не можете установить, проблема в нагрузке или в каких-то задержках.
N>Попробуйте запустить, например, 5 таких тестов впараллель и сравните времена в двух вариантах исполнения. Если проблема именно в нагрузке, у вас каждый из тестов потратит десятки секунд (например, 60), если в задержках — каждый потратит ненамного больше тех же 12 секунд. N>При этом тесте померяйте загрузку системы — затраты на user, system...
Запустил пять штук — время возросло в пять раз (для многопоточного — до 57 секунд, для однопоточного — ≈3,3 секунды). Нагрузка многопоточника такая же, как для одиночного запуска (см. соседний ответ
): проц загружен по максимуму, треть в пользовательских процессах, две трети — в ядре.
CF>>У меня есть подозрение, что в падении производительности виноват сам вызов fork(), а точнее — дублирование контекста. Напрямую проверить это я не могу, но когда я удалил из сервера часть стартового кода, инициализирующего разного рода структуры (благо в моём мини-примере они никак не используются), используемая процессом память снизилась с 14,5 метров до 8-ми, а измеренное время той же сотни запросов упало почти вдвое: до шести секунд.
N>Ну а измерить напрямую? Снимите время прямо перед fork() и после него, запишите разницу...
Замерил, но получился очень большой разброс. В родительском процессе вызов форка занимает от половины миллисекунды до 88 мс (в среднем 19 мс), в дочернем — от 3 до 115 мс (в среднем 21,5).
Re[2]: [perl] Тормознутые fork'и? >>66% в ядре — многовато, но как определить, чем оно там занимается? >>Не начинай дело, если не умеешь хотеть настолько, чтобы мочь.
Да это вообще то очень много. Чтобь понять что там происходит в ядре есть утилиты типа strace, ktrace, dtrace(все завист от вашей OS). Сидя в sys процесс например можут осуществлять подкачку(судя по размеру swap у вас не очень много оперативы)
Как вариант проверки скорости fork щапустить такой скрипт(stress.pl):
если у тебя много перловых данных, то сюрприз может быть в том что даже если ты используешь их только на чтение, то на уровне ниже перла они будут использоваться и на запись -- потому что перл делает подсчет ссылок. В результате будет write и copy on write, которых по хорошему не должно быть.
Здравствуйте, meandr, Вы писали:
>>>66% в ядре — многовато, но как определить, чем оно там занимается?
M>Да это вообще то очень много. Чтобь понять что там происходит в ядре есть утилиты типа strace, ktrace, dtrace(все завист от вашей OS). Сидя в sys процесс например можут осуществлять подкачку(судя по размеру swap у вас не очень много оперативы)
Так вроде ж, наоборот, своп не используется практически: эти 44 метра там как были с самого начала, так и остались, да ещё оперативы сотня свободна. Дисковой активности тоже не наблюдается.
M>Как вариант проверки скорости fork щапустить такой скрипт(stress.pl): M>и замерить сколько будет исполняться:
M>time perl stress.pl
Прогнал, получилось очень шустро:
real 0m0.735s
user 0m0.177s
sys 0m0.382s
Но вот что любопытно: стоило мне добавить весь инициализационный код из сервера в этот скриптик, как секунд через 10 после запуска начался дикий своп со всеми его сопутствующими прелестями в виде капитального зависона всей системы до окончания работы скрипта. К счастью, длилось это меньше минуты, результаты же получились следующими:
real 0m42.047s
user 0m0.212s
sys 0m0.217s
Повторюсь, при работе веб-сервера подобных каверз не наблюдаю. При запуске моей сотни запросов процессор, конечно, загружен полностью, но система, хоть и подтормаживает, остаётся вполне жизнеспособной и на мои действия реагирует адекватно, никакого своппинга не наблюдается.
CF>Но вот что любопытно: стоило мне добавить весь инициализационный код из сервера в этот скриптик, как секунд через 10 после запуска начался дикий своп со всеми его сопутствующими прелестями в виде капитального зависона всей системы до окончания работы скрипта.
Здравствуйте, dilmah, Вы писали:
D>если у тебя много перловых данных, то сюрприз может быть в том что даже если ты используешь их только на чтение, то на уровне ниже перла они будут использоваться и на запись -- потому что перл делает подсчет ссылок. В результате будет write и copy on write, которых по хорошему не должно быть.
Ну, во-первых, не совсем ясно, при чём тут подсчёт ссылок. Это же новый, независимый процесс, со своим набором данных и своим отдельным счётчиком. Но даже если предположить, что он там что-то мудрит: объём занимаемой памяти у родительского процесса — 15 мегабайт, виртуальной — 43. Даже если она вся скопируется, ну сколько времени это займёт? Я ради интереса замерил чистое время копирования 64-мегабайтного блока в памяти (программкой на C), получилось 12 миллисекунд. Так что само по себе копирование ботлнеком не является. Либо Перл вместо простого копирования блока памяти перетряхивает всю структуру (непонятно зачем), либо проблема кроется в чём-то другом.
Здравствуйте, dilmah, Вы писали:
CF>>Но вот что любопытно: стоило мне добавить весь инициализационный код из сервера в этот скриптик, как секунд через 10 после запуска начался дикий своп со всеми его сопутствующими прелестями в виде капитального зависона всей системы до окончания работы скрипта.
D>http://www.rsdn.ru/forum/dynamic/4032845.aspx
На это я уже ответил, но даже если это так, пока непонятно, чем скрипт с самостоятельными форками отличается от сервера, делающего те же форки, но по запросам извне. Пока единственное предположение, что у сервера количество входящих подключений (и, следовательно, дочерних процессов) чем-то ограничено и память не успевает забиться, тогда как стресс-тест этого ограничения не содержит и быстро забивает систему своими копиями. Попробую поддать памяти виртуалке и перепроверить стресс-тест.
CF>Ну, во-первых, не совсем ясно, при чём тут подсчёт ссылок. Это же новый, независимый процесс, со своим набором данных и своим отдельным счётчиком.
ты точно понимаешь что такое copy on write?
CF> Но даже если предположить, что он там что-то мудрит: объём занимаемой памяти у родительского процесса — 15 мегабайт, виртуальной — 43. Даже если она вся скопируется, ну сколько времени это займёт? Я ради интереса замерил чистое время копирования 64-мегабайтного блока в памяти (программкой на C), получилось 12 миллисекунд. Так что само по себе копирование ботлнеком не является. Либо Перл вместо простого копирования блока памяти перетряхивает всю структуру (непонятно зачем), либо проблема кроется в чём-то другом.
я не понимаю. Ты сделал 100 форков. объем данных ты сам пишешь 15-40 мбайт. Умножь на 100. У тебя хватает памяти? Если нет, то почему ты удивляешься своппингу.
CF>Даже если она вся скопируется, ну сколько времени это займёт? Я ради интереса замерил чистое время копирования 64-мегабайтного блока в памяти (программкой на C), получилось 12 миллисекунд. Так что само по себе копирование ботлнеком не является.
при copy on write, копирование выполняется постранично (4 кб) по мере того как наступают page faults. Разумеется, это медленнее обычного копирования.
Здравствуйте, CaptainFlint, Вы писали:
CF>Для начала вопрос: есть подозрение, что операция fork() в Перле — довольно тормознутая вещь. Так ли это, и если так, то есть ли более-менее эффективные приёмы многопоточной обработки данных, не использующие fork()?
fork никак не медленнее просто создания нового процесса. Хотя и быстрее ему быть не с чего. Его имеет смысл использовать если процессу нужна долгая инициализация. В данном примере можно данные из файла прочитать до форка.
Здравствуйте, dilmah, Вы писали:
CF>>Ну, во-первых, не совсем ясно, при чём тут подсчёт ссылок. Это же новый, независимый процесс, со своим набором данных и своим отдельным счётчиком.
D>ты точно понимаешь что такое copy on write?
Я считал, что да, но, разумеется, от ошибок никто не застрахован. Вот как я это себе представлял, прошу поправить, если в чём ошибся.
Есть процесс со своим набором страниц памяти и внутренней виртуальной адресацией. Делаем форк: порождается новый процесс с абсолютно идентичным набором страниц и данных в них. Чтобы не тратить лишнее время, эти страницы физически не копируются, а остаются эдакими "хард-линками": два процесса ссылаются на одну и ту же страницу. И только когда один из процессов начинает туда писать, ему выделяется копия страницы, куда он может писать, не затрагивая бывшую копию страницы второго процесса.
Почему я считаю, что подсчёт ссылок здесь ни при чём: если мы сделали копию всей памяти (неважно, реальную или "ссылочную"), то у нас все переменные, все хэши, все массивы — всё раздвоилось. Как в старом процессе была переменная A со счётчиком ссылок, пусть, 5, так и в новом процессе будет такая же переменная A с точно таким же (уже своим) счётчиком ссылок 5. Переменные нового процесса не ссылаются на переменные старого, и наоборот, поэтому значения счётчиков в результате самого форка поменяться не могут (не считая переменных, затронутых самим форком, таких как возвращаемое им значение, но таких переменных очень мало по сравнению с общим объёмом памяти). Поменяться счётчики могут лишь позже, когда начнутся присвоения переменных в процессе дальнейшего выполнения программы, но это уже другой вопрос.
CF>> Но даже если предположить, что он там что-то мудрит: объём занимаемой памяти у родительского процесса — 15 мегабайт, виртуальной — 43. Даже если она вся скопируется, ну сколько времени это займёт? Я ради интереса замерил чистое время копирования 64-мегабайтного блока в памяти (программкой на C), получилось 12 миллисекунд. Так что само по себе копирование ботлнеком не является. Либо Перл вместо простого копирования блока памяти перетряхивает всю структуру (непонятно зачем), либо проблема кроется в чём-то другом.
D>я не понимаю. Ты сделал 100 форков. объем данных ты сам пишешь 15-40 мбайт. Умножь на 100. У тебя хватает памяти? Если нет, то почему ты удивляешься своппингу.
Я, скорее, удивлялся не своппингу, а тому, что его в веб-сервере нет, хотя код по сути тот же самый.
В общем, перепроверил с памятью, увеличенной до двух гигов, — своппинга не было. Похоже, причина, действительно, в разном количестве одновременно работающих процессов. Когда запускаю веб-сервер со стресс-тестом, там одновременно крутится не больше двух десятков дочерних процессов, а когда консольный тест — то вплоть до 60–70, вот выскакивал за пределы 768 метров.
CF>>Даже если она вся скопируется, ну сколько времени это займёт? Я ради интереса замерил чистое время копирования 64-мегабайтного блока в памяти (программкой на C), получилось 12 миллисекунд. Так что само по себе копирование ботлнеком не является.
D>при copy on write, копирование выполняется постранично (4 кб) по мере того как наступают page faults. Разумеется, это медленнее обычного копирования.
А насколько медленнее? Есть какие-нибудь данные на этот счёт?
Здравствуйте, любой, Вы писали:
CF>>Для начала вопрос: есть подозрение, что операция fork() в Перле — довольно тормознутая вещь. Так ли это, и если так, то есть ли более-менее эффективные приёмы многопоточной обработки данных, не использующие fork()?
Л>fork никак не медленнее просто создания нового процесса. Хотя и быстрее ему быть не с чего. Его имеет смысл использовать если процессу нужна долгая инициализация. В данном примере можно данные из файла прочитать до форка.
Не совсем понял: из какого файла?
У меня чтение идёт из сокета, и его я уже перенёс в до-форковую часть, это ни малейшим образом ситуацию не улучшило (ну или улучшило в пределах погрешности).
CF>Почему я считаю, что подсчёт ссылок здесь ни при чём: если мы сделали копию всей памяти (неважно, реальную или "ссылочную"), то у нас все переменные, все хэши, все массивы — всё раздвоилось. Как в старом процессе была переменная A со счётчиком ссылок, пусть, 5, так и в новом процессе будет такая же переменная A с точно таким же (уже своим) счётчиком ссылок 5. Переменные нового процесса не ссылаются на переменные старого, и наоборот, поэтому значения счётчиков в результате самого форка поменяться не могут (не считая переменных, затронутых самим форком, таких как возвращаемое им значение, но таких переменных очень мало по сравнению с общим объёмом памяти). Поменяться счётчики могут лишь позже, когда начнутся присвоения переменных в процессе дальнейшего выполнения программы, но это уже другой вопрос.
да, но перл производит запись даже тогда, когда кажется что он только читает. Когда перловые данные просто используются на чтение, то в них меняются счетчики ссылок => происходит запись => происходит page fault и копирование данных, которого в идеале можно было бы избежать.
Здравствуйте, CaptainFlint, Вы писали:
CF>Не совсем понял: из какого файла?
my $data;
open(FILE, '/home/user/public_html/test.html');
read(FILE, $data, -s FILE);
close(FILE);
CF>У меня чтение идёт из сокета, и его я уже перенёс в до-форковую часть,
Интересно, как же оно тогда работает.
CF> это ни малейшим образом ситуацию не улучшило (ну или улучшило в пределах погрешности).
Ну конкретно я не могу сказать в чем проблема.