Skip to content

Node.js: псевдо-потоки в асинхронном коде

15/04/2010

Итак, за время прошедшее с предыдущей статьи наш веб-паучок подрос и теперь может складывать данные в базу CouchDB для дальнейшей обработки другими инструментами. Кроме того, паук обзавёлся файлом конфигурации (это будет полезно когда он будет состоять из нескольких частей, и все части будут использовать одни настройки).

Теперь перед нами встаёт вопрос об эффективности обхода сайта. Сейчас паук делает это в один поток, запрашивая страницы строго по одной, в псевдо-рекурсивной манере. Но большинство сайтов могут пережить обращение и в два, и в три потока (тем более что статику мы не запрашиваем). Надо добавить в паука возможность запрашивать данные в несколько потоков (при асинхронных запросах всё равно большую часть времени паук простаивает в ожидании страницы от сервера). Но для начала посмотрим как вообще выполняется код в Node.

По сути, каждый callback, повешенный на ввод/вывод или nextTick, действует как отдельный блок кода. Файл, передаваемый Node.js изначально, тоже можно рассматривать как такой блок. Как только блоков для выполнения не остаётся (нет событий, на которые повешены обработчики, и нет ждущих обработчиков в пайпе) процесс node.js завершается. Вот так сейчас выглядит выполнение нашего паука:

Линии между блоками — ожидание асинхронных функций. Блок функций crawl_page + getPage перед завершением вызывает себя же с аргументом следующей страницы. Всё это выполняется без стека, в итеративной манере. Ничего не мешает нам пустить параллельно ещё один такой же «поток»:

…или даже два…

(Да, я не умею пользоваться Фотошопом :)) Потоки друг о друге вообще ничего не обязаны знать. Т.к. V8 выполняет код строго в один поток, можно обращаться к общим переменным (например, списку посещённых страниц) не задумываясь о блокировке ресурсов.

Таким образом, дополнительный поток можно создать просто вызвав начальный crawl_page() ещё раз. Сколько вызовов, столько и потоков мы получим. Но тут нас ждёт другая проблема: вначале у нас известна только одна страница сайта: главная. Создавать дополнительный поток можно только когда будут известны хотя бы ещё две. А значит, создавать его придётся в конце функции crawl_page, когда получены новые данные о структуре сайта:

var crawl_page = function (URL) {
    sys.puts('Visiting ' + URL);
    getPage(URL, function(code, text, headers) {

        /* Page processing  */

        crawl_page(get_next_page());

        // Create new stream if available and have unvisited pages
        if (num_of_streams < settings.max_streams && known_pages.length > visited_pages.length) {
            num_of_streams++;
            crawl_page(get_next_page());
            sys.puts('Starting another stream: ' + num_of_streams + ' of ' + settings.max_streams);
        }
    });
}
    

Если мы хотим, чтобы обработчик знал в каком именно потоке он выполняется, можно передавать id потока последним параметром:

var crawl_page = function (URL, stream_id) {
    sys.puts('Stream ' + stream_id + ' visiting ' + URL);
    getPage(URL, function(code, text, headers) {

        /* Page processing  */

        crawl_page(get_next_page(), stream_id);

        // Create new stream if available and have unvisited pages
        if (num_of_streams < settings.max_streams && known_pages.length > visited_pages.length) {
            num_of_streams++;
            crawl_page(get_next_page(), num_of_streams);
            sys.puts('Starting another stream: ' + num_of_streams + ' of ' + settings.max_streams);
        }
    });
}

// Запуск
crawl_page('/', 1);
num_of_streams = 1;
    

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

Выглядит всё неплохо, но если мы запустим код хотя бы в три потока, мы увидим что работает он совсем не быстро, да ещё и «пробуксовывает» на медленных страницах. Секрет в том, что все потоки сейчас используют одно соединение (http.Client) с сервером. Всё, что нам надо сделать — создавать соединение при создании потока и передавать его так же как передаётся stream_id:

var crawl_page = function (URL, connection, stream_id) {
    sys.puts('Stream ' + stream_id + ' visiting ' + URL);
    getPage(URL, connection, function(code, text, headers) {

        /* Page processing  */

        crawl_page(get_next_page(), connection, stream_id);

        // Create new stream if available and have unvisited pages
        if (num_of_streams < settings.max_streams && known_pages.length > visited_pages.length) {
            num_of_streams++;
            crawl_page(get_next_page(), http.createClient(80, settings.targethost), num_of_streams);
            sys.puts('Starting another stream: ' + num_of_streams + ' of ' + settings.max_streams);
        }
    });
}

// Запуск
crawl_page('/', http.createClient(80, settings.targethost), 1);
num_of_streams = 1;
    

Вот так, теперь всё работает намного лучше.

Создание поискового сервера

Ссылки по теме

Работа с CouchDB

Реклама

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: