Как стать автором
Обновить

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

Жуть… Асинхронный сервер с тяжелыми вычислениями

В случае, если при прохождении цикла оказалось, что ни одна функция не установлена с помощью process.nextTick(), нет ни одного таймера и очереди запросов в libev и libeio пусты, то node завершает работу.
Node наверное работу не завершает, просто Loop завершается…

А есть ли в Node способ считывать файл не целиком а по кусочкам? По крайней мере ваши функции хеширования можно применять не ко всему файлу а к его кусочкам с таким же результатом, считывая кусочки файла и хешируя их, так сказать, «в конвейерном режиме».

Таким образом, время обработки будет равняться min(Tread, Tcalc), а не (Tread + Tcalc)
Что то не вызывает доверия это утверждение. Что такое время обработки? Через сколько клиент получит ответ? Да через столько же т.к. пока вы файл не загрузите, калькуляции вы над ним не проведете.
Node наверное работу не завершает, просто Loop завершается


Да, Node в таком случае завершит работу только если не осталось обработчиков (и, соответственно, не могут обрабатываться никакие события).
Имеется в виду не классическое поднятие сервера, просто последовательность javascript команд?
Ну да. Пока есть хотя бы один блок команд который может выполниться (обработчик события либо исходный скрипт), нода не завершится.
О чём я, впрочем, и написал :)
Node наверное работу не завершает, просто Loop завершается…
Наверное, это вопрос терминологии, считать ли момент ожидания событий честью цикла. По идее, нужно, тогда наши с вами утверждения идентичны, разве нет? :)

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

Что то не вызывает доверия это утверждение. Что такое время обработки? Через сколько клиент получит ответ? Да через столько же т.к. пока вы файл не загрузите, калькуляции вы над ним не проведете.
Согласен с вами, некорректно оценил время. Тем не менее, если представить себе одновременный приход двух запросов, то возможен случай, когда будет инициализировано чтение двух файлов, далее один из них прочитается, обработается и результат вернётся клиенту, а потом будет обработан второй файл. В этом случае второй клиент получит результат раньше, чем при синхронной реализации сервера.
Ну да, по этой части придирки в основном к терминологии.
Я добавил ссылку на англоязычную презентацию, там довольно подробно описан event loop. Думаю кто захочет разобраться — разберётся.
А есть ли в Node способ считывать файл не целиком а по кусочкам? По крайней мере ваши функции хеширования можно применять не ко всему файлу а к его кусочкам с таким же результатом, считывая кусочки файла и хешируя их, так сказать, «в конвейерном режиме».

Да, есть:

fs.createReadStream('sample.txt', {start: 90, end: 99});
Но что делать, если время обработки файла сильно больше времени чтения файла и кроме того сильно флуктуирует?
В этом случае надо НЕиспользовать асинхронный сервер)))
Расскажите самоучке, почему? На мой взгляд это как раз хороший use-case, так как это позволяет быстрее обслуживать клиентов, для запросов которых не нужны долгие вычисления.
Для запросов для которых НЕ нужны долгие вычисления конечно хороший use-case. Но мой комментарий как раз по поводу «что делать, если время обработки файла сильно больше времени чтения»

Дело в том, что асинхронный сервер в общем то нужен для долгоживущих соединений (IM сервер, веб-сокет, Long-pool) или если действительно мало работы нужно проделать над соединением.

А если над каждым запросом нужно пыхтеть считая хеши файлов или генерируя тяжелые веб-странички, то в какой то момент сервер перестанет успевать обрабатывать запросы, очередь запросов и время ожидания вырастет, ну и потом вообще Reject начнется. Упремся в CPU короче.

А для масштабирования нужно запускать все тех-же воркеров…
Согласен с вами.
О, здорово!
Побольше бы на хабре новых статей, освещающих пути входа в царство node.js
Почему то мне кажется, что на реальных ситуациях будет уместным загружать файл, который может быть загружен заранее в отдельном потоке, проверять таймаутом изменения в файле (проверять дату изменения или ещё чем-либо), при наличии таковых заново загружать файл.
При появлении клиента отдавать текущий контент.
Тогда количество клиентов не будет зависеть от размера обрабатываемого файла.
Даже не потоке, а тамже, возможно даже тупо в лоб загружать в переменную
var content
setTimeout(function(){
if(файл изменён) загрузитьфайл(файл, function() {content = вычислитьхеш()})
})
server.(){
write (content)
}).listen(8080)
Реальные ситуации в любом случае отличаются от приводимого в статье примера. Своей задачей я в основном ставил сравнения различных способов обработки, так как часто возникают вопросы о том, зачем нужен process.nextTick().

Что касается кеширования, то тут нужно глубже копаться в libeio, возможно там используются пользовательские функции чтения, которые и так кешируют данные.
Данные (считанный файл) кешируется ОС, для этого каких-то особых действий предпринимать не надо. Тут, если я правильно понял, предложили кешировать не сам файл, а уже посчитанный хеш. Но, имхо, его нужно в файл или в мемкеш сохранять а не внутри сервера… А то понадобится сервер перегрузить и заново придется все считать и хешировать.
«Как видно, использование асинхронного чтение действитьно улучшает производительность сервера при больших размерах файла»

поднимите мне веки, ничего не вижу
Имеются в виду графики в конце статьи.
уважаю потраченное автором время, но о том и речь — на графике все в пределах погрешностей.
не говоря уже о том, что почему-то их два :)
Я из графиков вижу что на маленьких файлах быстрее синхронный код, а на больших readFile-and-sync-chain.js
который не должен быть быстрей readFile.js

В чем подвох?
Основная проблема теста в том, что запрашивался один и тот же файл. Кроме того я не очень уверен в «чистоте» окружения при проведении тестов.
В реальной ситуации стоит брать отдельные диапазоны размеров, генерировать несколько файлов каждого размера и посылать запросы на обработку в случайном порядке. Тогда и статистически всё будет честнее, и к реальности ближе. В любом случае, правильная архитектура, имхо, стоит 10% потерь.
А если попробовать брать файл из /dev/random или иного некеширующего источника?
Можно попробовать c /dev/urandom, с /dev/random неохота долго возиться :)
НЛО прилетело и опубликовало эту надпись здесь
Тоже не понял смысла вписывать в коллбеки nextTick.
Иногда может быть полезным.
Например, клиент запросил скрипт, сервер создал запись в логе.
обычный подход:
сгенерировать_контент()
записать_файл()
отослать_и_закрыть_соединение()

С nexttick
сгенерировать_контент()
nextTick(function(){записать_файл()});
отослать_и_закрыть_соединение()

Тогда время реагирования скрипта будет меньше.
ну а разве вот это не то же самое:

сгенерировать();
записать_файл_async();
отослать_и_закрыть();

?
Ведь асинхронные функции вроде для того и нужны, чтобы поток не ждал их выполнения.
В случае, если в цепочке обслуживания запроса есть много «дырок», это позволяет в них начать обрабатывать другой запрос. А если он требует асинхронного ввода/вывода, то мы получим преимущество за счёт того, что запрос на асинхронное чтение запустится раньше.
То есть, вы предлагаете просто искуственно натыкать побольше таких дырок?
Главное не переборщить.
Ну да, просто пример не идеальный.
Допустим, перед записью лога в файл, его надо ещё долго обрабатывать, тогда можно запихнуть обработки и запись в nextTick, а соединение с клиентом завершить уже сейчас.
сгенерировать_контент()
отослать_и_закрыть_соединение()
записать_файл()

Так вроде тоже можно. в Node передача ответа и закрытие соединения клиенту идет не через return а через res.end(data);
отослать_и_закрыть_соединение() псевдокод, конечно же я имел в виду res.end(контент_сгенерированный_ранее);
не, псевдокод я понял. Имею в виду что можно закрыть соединение с клиентом, отключить его и уже после этого безо всяких nextTick доделать остальную работу, которая не повлияет на отдаваемый клиенту результат (запись в лог и т.д.).
Постарался описать, почему от разрыва в основном потоке выполнения может получиться преимущество и увеличение RPS. Возможно, иллюстрации, которые я сейчас делаю, помогут в этом убедиться или наоборот придумать убедительные аргументы против моей точки зрения.
НЛО прилетело и опубликовало эту надпись здесь
За счёт того, что при наличии такого промежутка в вычислениях в него может попасть начало асинхронного чтения, а значит чтение произойдёт раньше и возрастёт отзывчивость. Но, конечно, это работает только для серверов с активным вводом/выводом.
НЛО прилетело и опубликовало эту надпись здесь
> Но, конечно, это работает только для серверов с активным вводом/выводом.

> Время максимальной блокировки определяется самым тяжелым вычислением.

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

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

Тут есть одна «проблема»: libeio справится с асинхронным чтением, но для этого нужно, чтобы из основного потока поступил запрос на начало чтения. А если будет длительная (но не настолько длительная, чтобы использовать другую архитектуру) обработка файла для первого запроса, то когда бы не пришёл второй запрос, ему придётся ждать пока обработка завершится и только после этого в основном потоке начнётся его обработка и отправка запроса для libeio для асинхронного чтения. А если обработка будет разбита на отдельные кусочки с process.nextTick() между ними, то в эту обработку может вклиниться первичное получение HTTP-запроса и запрос на асинхронное чтение. За счёт этого суммарное время обслуживания первого запроса увеличится на небольшое время принятия второго HTTP-запроса, тогда как суммарное время обслуживания второго запроса уменьшится за счёт того, что асинхронное чтение начнётся раньше.

Вы однозначно правы в том, что здесь не стоит делать категоричных заявлений о преимуществах того или иного кода. Уж слишком много факторов, влияющих на результат: размеры и диапазон размеров файлов, время обработки и его возможная нелинейная зависимость от размера файла, частота запросов и размера пула потоков в libeio. Я постараюсь в ближайшее время нарисовать «планы обработки» запросов для примеров из этого топика и различных вариантов «взаимного расположения» поступающих запросов, судя по моим наброскам они намного нагляднее и понятнее. Возможно мы сойдёмся на промежуточной точке зрения :)
НЛО прилетело и опубликовало эту надпись здесь
Графики неубедительные. Нужно было делать одну диаграмму — количество запросов в секунду от метода обработки. Но нагружать при тесте не одним файлом, а файлами с разным размером от 1/8 Кб до 1 Мб с каким то шагом, в случайном порядке с равномерным распределением частоты запросов от размера файла. Впрочем, эмпирически видится, что тип распределения тоже будет влиять на график. Поэтому для чистоты надо было бы еще и с нормальным распределением попробовать.
Согласен, нужно провести такой тест с множеством файлов. Для равномерного распределения применения не могу придумать, а случай с нормальным распределением вокруг какого-то значение подойдёт для моделирования сервиса потоковой обработки изображений.

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

Мне, к сожалению, не хватает практического опыта для того, чтобы придумать качественный пример для тестирования :(
Пример аналогичного неблокирующего веб-серева с использованием fibers (библиотека node-sync):

var Sync = require('sync');

http.createServer(function (req, res) {
    
    // Very simple and dangerous check
    var filename = req.url.replace(/\?.*/, '').replace(/(\.\.|\/)/, '');
    
    // Start new fiber
    Sync(function(){
        
        // Read file from disk
        var filecontent = fs.readFile.sync(fs, filename, 'utf8');
        var str = func1_cb.sync(null, filecontent);
        var hash = func2_cb.sync(null, str);
        
        res.writeHead(200, {'Content-Type': 'text/plain'});
        res.end(hash);
        
    }, function(err){

        // Error handling
        if (err) {
            res.writeHead(404, {'Content-Type': 'text/plain'});
            res.end(err);
        }
    })
    
}).listen(8124, "127.0.0.1");


Было бы интересно сравнить производительность данного подхода.
Попробую. Как бы не забыть об этом :)
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории