Я убью тебя, Google Reader! Высоконагруженный сервис своими руками. Статья для журнала Хакер.
Статья написана специально для журнала Хакер и опубликована на сайте журнала (в несколько сокращенной и отредактированной версии). Ниже я публикую свой оригинальный вариант, без правок или ограничения на объем материала.
Шеф, что делать будем?
Получение новостей через фиды или RSS-ленты сегодня самый быстрый и простой способ следить за жизнью любимых сайтов или блогов. Твиттер, конечно, крут, но рсс-кам ещё рано умирать. Сегодня у тебя большой выбор - RSS можно читать кучей способов, начинаю от почтового клиента (да, в Mozilla Thunderburd 3 встроен клиент) и браузера, заканчивая специальными программами и онлайн сервисами. Самым крутым из таких будет, конечно, Google Reader. Пользоваться им вроде бы легко - вводишь адрес сайта и отныне просто заходи и читай новости, никакой кнопки Refresh жать не надо, гугл сам позаботится о том, чтобы найти новые новости и показать их тебе. Но при этом у него есть много врождённых проблем, например, не очень интуитивный интерфейс, а также часто сильные задержки в получении новых сообщений. Особенно это касается лент из Твиттера. Ага, популярнейший сервис также вовсю использует RSS, транслируя туда твои сообщения. Но ведь ты уже в курсе, как быстро там все изменяется - написал о новом хаке винды, а через минуту уже весь интернет с криками "ура!" бросается ломать империю Билли. Но читатели через Google живут в параллельном мире и просто тормозят - там лента из твиттера обновляется часто с задержками в минуты и часы, непорядок!
Давай напишем для себя любимого (ну и друзей, куда ж без них) простую онлайновую читалку RSS-новостей. Фичами будет возможность автоматически находить фиды с любого адреса (если они там есть, конечно) и полное игнорирование нюансов форматов ( мы сможем работать с любым фидом). Кроме этого, научимся высшему пилотажу РНР-программирования - созданию распределённых систем. Когда у тебя две-три ленты, с ними справиться даже твой нетбук, но когда друзья поймут, что гугл теперь отстой, и начнут добавлять свои ленты, станет плохо. А мы сделаем так, что читалка будет работать с любым количеством лент, просто успевай доставлять сервера! Помнишь, мы не раз рассказывали тебе про облачный компьютинг, Amazon EC2 и прочие заморские технологии? Вот с их помощью это сделать очень легко - нажав кнопочку и у тебя уже два сервера вместо одного. Используя РНР и немного магии, ты сможешь заставить работать на наше благо столько серверов, сколько достанешь.
Ну что же, начнём? Строить все это дело мы будем на базе стандартного LAMP-набора (если у тебя его ещё нет, быстро читай нашу статью), а в качестве основной библиотеки будем использовать Zend Framework и jQuery. Также нам понадобиться база данных - MySQL, для эффективной работы на нескольких серверах придётся поставить Gearman. Но сейчас не парься, об этом я детальнее расскажу ниже. Предупрежу сразу, что буду рассказывать только о самых важных моментах - хочу научить тебя строить супер-веб-приложения, а не дать скопипастить готовый код. Но ты можешь скачать исходники с диска журнала и посмотреть его самостоятельно.
В разведку за лентами
Первым делом тебе надо иметь возможность добавлять фиды, которые хочешь читать. Скажу честно, это реально сложно, так как сайтов тысячи, и каждый автор, каждая CMS по-своему что-то делает. Мы же хотим, чтобы пользоваться читалкой мог любой, а значит, требовать прямого URL или, упаси бог, задания формата (RSS или Atom) никак нельзя. Да и сам URL может быть задан кучей способов. Мы попробуем написать универсальную получалку из любого введённого адреса точного и прямого адреса новостной ленты.
Что же можно вводить? Самый простой способ – просто указать адрес сайта, ленту которого мы будем читать. Адрес может быть как полный, с указанием http:// в начале, так и просто домен. Поэтому первым делом надо проверить, указан ли протокол, так как наш фреймворк не умеет работать с такими адресами. Здесь уже придётся написать немного кода. Используем компонент Zend_Uri, который проверит введённый адрес. Если все нормально, мы получим адрес, который следует проверить, если не получится – вполне возможно, что кто-то пытается надурить систему, подсунув ей просто строку?
Сам код функции _validURI ты найдёшь в дистрибутиве (файл validURI.php), я лишь кратко опишу, как он работает. Первым делом, мы предполагаем, что введённая строка все же нормальный адрес (ага, доверяем пользователю), поэтому используем метод Zend_Uri::factory для создания объекта адреса. Если что-то пойдёт не так, будет исключение, иначе мы проверим встроенным методом valid, действительно ли адрес корректный. Если все правильно, возвращаем канонизированный URL, полученный методом getUri(). Если все плохо и есть ошибки, сначала проверим, а может мы просто забыли http:// в начале строки. Потому что адрес ‘xakep.ru’ хоть и внешне выглядит правильным, но не пройдёт проверки, необходимо строго ‘http://xakep.ru’. Добавив протокол, мы снова запустим ту самую проверку, и если ошибка есть и на этот раз – все, сообщаем пользователю, что нифига это не URL.
Хорошо, мы имеем корректный адрес сайта. Это или прямая ссылка на ленту, или ссылка на веб-страницу (или корень сайта), где нам надо самостоятельно найти ленту. Для этого у нас есть уже заботливо созданные инструменты. Используй недавно появившейся в Zend-е компонент Zend_Feed_Reader, который просто отлично справляется с любыми типами лент, с любым форматированием и даже экзотическими лентами с встроенными XSLT-стилями (этим балуются некоторые западные сайты вроде CNBC). В нем есть встроенный метод поиска линков на страницах, поэтому если RSS-лента на этом сайте есть, он её найдёт! Для обработки доступны все форматы - RSS до версии 2.0, Atom, RDF, так что можно не париться по поводу различий.
Здесь будет ещё одна проблемка. Если у тебя прямой линк на ленту, например, адрес ‘http://xakep.ru/articles/rss/default.asp’, то использование автоматического поиска приведёт к ошибке. Но наперёд ты никак не можешь знать, что это за адрес, поэтому, как и с проверкой URL-а, мы будем постепенно проверять все предположения. Сначала мы пытаемся использовать адрес как просто страницу, на которой ищем фиды:
<?php try { $feed_url = Zend_Feed_Reader::findFeedLinks($our_site_url); if ($feed_url instanceOf Zend_Feed_Reader_FeedSet) { //если мы получили список лент - экземпляр класса Zend_Feed_Reader_FeedSet $direct_feed_url = $feed_url[0]['href']; } else { //строим предположение, что у нас прямой адрес $_tmp_feed = Zend_Feed_Reader::import($our_site_url); if ($_tmp_feed instanceOf Zend_Feed_Reader_FeedAbstract) { // да, мы сразу получили прямой адрес $direct_feed_url = $our_site_url; } } catch (Zend_Exception $e) { //точно нет ничего echo "<h2>По указанному адресу нет новостных лент</h2>"; exit(1); } ?>
Полный код ты найдёшь в файле getFeedURL.php. Перед самым интересным - получением новостей, стоит ещё минутку поговорить о возможностях Zend_Feed_Reader. В нем есть встроенное кеширование - это очень пригодится, если ты не хочешь постоянно перекачивать сотни Кб данных просто так, ведь если много лент, хостеру это точно не понравиться. Поэтому мы будем использовать файловый кеш. К тому же, если будет много пользователей, многие будут читать одни и те же ленты, зачем же загружать их несколько раз? Кроме этого, не следует пренебрегать встроенными в HTTP возможностями управлять кешированием. Если сервер сообщает нашему приложению, что лента не изменилась (при помощи служебных HTTP-заголовков), то можно смело доставать кешированную версию.
Программисты Zend-а и здесь сделали за тебя всю работы, поэтому чтобы все это включить и дать передохнуть серверу, достаточно поместить несколько строчек до первого использования компонента Zend_Feed_Reader:
<?php $cache = Zend_Cache::factory('Core', 'File', array( 'lifetime' => 24 * 3600, // 24 часа нам хватит? 'automatic_serialization' => true, 'caching' => true, 'cache_id_prefix' => 'xakep_' ), array( 'read_control_type' => 'adler32', 'cache_dir' => '/tmp/xakep/cache' //куда же всё кешировать? )); //Разрешаем использовать кеш Zend_Feed_Reader::setCache($cache); //Разрешаем учитывать HTTP-заголовки для проверки кеша Zend_Feed_Reader::useHttpConditionalGet(true); ?>
Вот так мы существенно снизим нагрузку на сервер и даже самая простая конфигурация сможет обрабатывать десятки и сотни лент. И если ты хочешь построить совсем уж крутую систему, посмотри в документации по Zend_Cache - там не только множество настроек, но и кешировать можно в память, например, Memcached. И ещё, ты обратил внимание, почему я проверяю тип результата по абстрактному классу Zend_Feed_Reader_FeedAbstract? Очень просто - библиотека умеет работать с разными типами лент, и для каждого из них есть свой класс, умеющий обрабатывать свой формат. Но нас же не интересуют такие детали, все равно работа с лентой унифицирована. А так как все эти классы наследуются от одного базового, то чтобы за одну проверку выяснить, что это точно лента, неважно какого формата, я и сравниваю с общим классом-родителем. Вот такой вот трюк.
Ага, правильно, ты забыл с таким трудом добытые ссылки на фиды куда-то сохранить. Для этого создаём две таблицы - feeds для хранения ссылок, user_subscriptions для хранения подписок. Зачем две таблицы? Очень просто - так ты сможешь хранить только одну ссылку, если на одну ленту подписаны двое пользователей, то они оба будут использовать один и тот же адрес.
CREATE TABLE `feeds` ( `fid` INT UNSIGNED NOT NULL AUTO_INCREMENT , `feed_url` VARCHAR( 1024 ) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL , `added_at` INT UNSIGNED NOT NULL , `errors` INT UNSIGNED DEFAULT '0' NOT NULL , PRIMARY KEY ( `fid` ) ) TYPE = InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci COMMENT = 'таблица лент';
Заметь, есть специальное поле - счётчик ошибок. В реальности сервера, которые отдают ленты, часто бывает, лежат, или проблемы хостера, поэтому не всегда мы сможем получить ленту. Чтобы найти тех, кто вообще не отвечает длительное время (и не тратить на их проверку ресурсы), мы будем хранить количество ошибок. Думаешь, это лишнее? Поверь, время от времени лежат все ленты, даже от сервиса Google News.
CREATE TABLE `users_subscription` ( `uid` INT UNSIGNED NOT NULL , `fid` INT UNSIGNED NOT NULL , `subscript_at` INT UNSIGNED NOT NULL , INDEX ( `uid` ) ) TYPE = InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci COMMENT = 'подписки пользователей';
Здесь все просто, сам разберёшься, а вот дальше - самое интересное!
Приказано получить новости!
Теперь у тебя есть все, что необходимо для начала реального программирования. На самом деле здесь нет ничего сложного, и я постараюсь тебя в этом дальше убедить. В общих чертах, все работает так. Периодически (например, по cron-у) запускается скрипт, который берет все ленты из базы данных, и последовательно перебирает их, проверяя новые сообщения. Если сообщения есть, он добавляет их в базу данных. При первом запуске все сообщения в лентах будут новыми, поэтому нагрузка на сервер будет значительной (если у тебя много лент), потом добавляться будут только новые сообщения - обычно это несколько сообщений в час (хотя есть ленты, которые обновляются каждую минуту и там несколько десятков сообщений каждый раз).
Кстати, а как понять, что сообщение из ленты новое? Ни один из атрибутов использовать для полной уверенности не получится. Поэтому реализуем собственные уникальные идентификаторы, и будем пропускать каждую новость через сравнение со списком уже загруженных. Самый простой путь - просто использовать нашу MySQL базу данных, однако сразу скажу тебе, что это путь для лохов. Подумай сам, если у тебя будет хоть с десяток-другой лент, то очень скоро, в течении пары недель, таблица с сообщениями разрастается до огромного размера. Я вот делал как раз такую штуку у себя, так за неделю по 50 - 100 тыс. сообщений. Если мы будем постоянно проверять все новости, при каждом запуске нашего скрипта база будет ложиться. Помнишь, мы недавно рассказывали о приёмниках SQL-а в веб-приложениях - key/value хранилищах. Если не помнишь, [[почитай статью на сайте журнала]]. Предлагаю дополнить нашу систему кешем, который будет использовать самую быструю NoSQL-систему - Redis. В него мы поместим все идентификаторы сообщений, которые уже загружены в базу данных, и только если встретим новость, которой там нет, запишем её как новую. Круто было бы добавить возможность архивирования - ведь кеш быстро заполнится устаревшими данными, которые будут только мешать, но это можно сделать и потом.
Для работы с Redis-ом есть отличный класс - Rediska, кстати, он добавляет в Zend Framework множество классов-адаптеров, например, можно переключить на него стандартный кеш. Подключить редиску также просто:
<?php $redis_conf = Array( 'namespace' => 'xakep_', 'servers' => array( array('host' => 'localhost', 'port' => 6379, 'weight' => 1) ), 'keyDistributor' => 'crc32' ); try { $redis = new Rediska($redis_conf); } catch (Rediska_Exception $e) { echo "[ERROR] Error creating Redis instance: " . $e->getMessage() . "\n"; } ?>
В случае успеха у нас будет объект для доступа к Redis или сообщение об ошибке. Подробнее о Redis-е мы уже писали в статье [], поэтому расскажу только про особенности нашего проекта.
Для кеша мы будем использовать структуру данных SET, так как она позволяет всего одной командой узнать, содержится ли в ней указанное значение. Соответственно, у нас будет столько сетов, сколько уникальных лент мы читаем. В каждом сете будет набор MD5 хешей сообщений, которые уже загружены. Если мы встретим новую новость, её хеш будет сразу добавлен. Вот таким простым способом, используя комбинацию различных типов баз данных, ты получишь большую скорость, большую гибкость и расширяемость, вместе с тем, даже обычной MySQL базы и средненького сервера хватит для обработки сотен лент.
Идём дальше. Сначала мы напишем простую функцию, которая будет принимать один или несколько адресов лент и последовательно проверять их на наличие новых сообщений. Позже, на базе этого кода мы за пять минут построим гибкую распределённую систему!
Функция processFeed (смотри файл processFeed.php) принимает массив, в котором должен быть как минимум один элемент - прямая ссылка на ленту. Для пущей надёжности мы ещё раз проверим корректность этого адреса - ну так, на всякий пожарный. Дальше пытаемся получить все новости, используя Zend_Feed_Reader и его возможности кеширования. Класс автоматически загрузит ленту, если необходимо, либо вытащит её из кеша. Тебе остаётся только проверить, что получилось.
<?php $feed = Zend_Feed_Reader::import($feed_url); if ($feed instanceOf Zend_Feed_Reader_FeedAbstract) { /* код обработки ленты здесь */ } ?>
Видишь, все просто. Теперь у нас есть лента, разберёмся с сообщениями. Ты же пока не знаешь, какие сообщения новые, какие старые, поэтому в любом случае надо обработать все полученные данные. Сделать это просто, ведь класс, который мы получили, не только содержит все данные из ленты, но и, что важнее, допускает работу с собой как с обычным массивом! Поэтому просто запусти цикл, и он переберёт все новости из ленты.
foreach ($feed as $fitem) { /* $fitem это и массив и объект */ }
Для начала вычислим уникальный ключ новости - используя md5 хеш от заголовка новости, даты публикации и линка. Далее проверим, что существует кеш для этой ленты в Redis-е. Если его нет, значит лента добавлена только что, и мы ещё её не обрабатывали.
Получить поля сообщения также очень просто. Для простоты и удобства мы пока опустим работу с самим телом новости - это будет хорошим домашним заданием, а будем выводить только заголовок, ссылку и дату:
<?php $_item['title'] = htmlspecialchars(trim(str_replace("\n", '', $feed->getTitle())), ENT_QUOTES); $_item['time'] = $feed->getDateCreated()->getTimestamp(); $_item['link'] = trim($feed->getLink()); //Теперь создадим ключ новости: $_item['hash'] = md5($_item['title'] . '|' . $_item['link'] . '|' . $_item['time']); ?>
<?php //Все готово, чтобы проверить новость на «старость»: $_fhash = md5($feed_url); //хеш кеша для ленты if ((!$redis->exists($_fhash)) || (($redis->exists($_fhash)) && (!$redis->existsInSet($_fhash, $_item['hash']))) { //ух ты, новость! Добавить! $redis->addToSet(md5($feed_url), $_item['hash']); } else continue; ?>
Мы храним идентификатор ленты, заголовок (она намерено сделан достаточно большим), ссылку и две даты - время самой новости и время, когда она добавлена в базу данных, а также посчитанный md5-хеш.
CREATE TABLE `feeds_items` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT , `nid` INT UNSIGNED NOT NULL , `news_title` VARCHAR( 4096 ) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL , `news_link` VARCHAR( 1024 ) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL , `news_at` INT UNSIGNED NOT NULL , `added_at` INT UNSIGNED NOT NULL , `hash` VARCHAR( 32 ) NOT NULL, PRIMARY KEY ( `id` ) ) TYPE = InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci COMMENT = 'таблица новостей';
Записывать сообщения конечно необходимо в транзакции, но учитывая, что вставок в цикле может быть много, а ты ж не хочешь завалить сервер, можно ограничиться транзакцией на всю ленту. Соответственно, мы вставим все сообщения, а потом подтвердим транзакцию. При помощи Zend-а это делается одной командой: $db->beginTransaction() и $db->commit(), ну а в крайнем случае, если что-то не так - $db->rollBack().
Вот и все! Мы получили ссылку на ленту, получили ленту (задействовав при этом возможности кеширования и корректного понимания HTTP-заголовков), разобрали ее на отдельные сообщения, не парясь с конкретным форматом, а потом проверили по кешу все сообщения и записали новые в базу данных. При этом минимально трогая самое узкое место любого веб-приложения - собственно, саму СУБД!
Сервера в упряжке или выход Gearman-а!
Если запустить написанный нами скрипт, то он будет работать как и обычная “ПеХаПешка” - сначала первая лента, потом вторая и так далее. И неважно, на скольких серверах ты его запустишь. К тому же, ошибка при обработке какой-то ленты вырубит сразу весь скрипт, а значит, остальные новости будут пропущены. Непорядок, согласись. Настоящие пацаны пишут масштабируемые и параллельные приложения на Java, С или хоть бы Python. И будут косо смотреть в твою сторону, если ты скажешь, что напишешь такое же на РНР. Не смотри на них, давай писать на РНР!
А поможет тебе в этом почётном деле крутая штука под названием Gearman. Это такой специальный сервер, который берет на себя всю работу по управлению заданиями, которые ты ему поручишь. Он сам выстроит их в очередь, посмотрит, сколько серверов и процессов у него есть, потом разошлёт всем работу, проконтролирует её выполнение и соберёт результаты. Если ты добавишь себе ещё один сервер, стоит только запустить на нем обработчики, как Gearman сам поймёт, что теперь задания можно распределить на новый сервер, а значит количество работы, которая выполняется параллельно, будет больше. И так с каждым новым сервером! Единственное, чего пока Gearman не может, так это выполнять работы по расписанию, то есть, заменить cron им нельзя. Сам сервер написан на С и очень быстрый, API есть для многих языков, также можно встроить в MySQL как UDF (пользовательскую функцию). Для твоего РНР есть два варианта - самый лучший, это использовать С-модуль (но придётся компилировать и ставить расширение) или использовать PEAR пакет Net_Gearman, написанный на чистом РНР, но более медленный и плохо документированный. Так что мы выберем модуль на С, тем более поставить его очень легко - он есть в PECL (http://pecl.php.net/package/gearman).
Чтобы лучше понимать принцип работы, давай рассмотрим основные понятия архитектуры Gearman-а. Их немного:
- Задание (работа, job) - это строка, которая передаётся обработчику и содержит данные для работы. Здесь ты должен подумать, как передавать сложные структуры данных. Если ничего кроме РНР тебя не интересует, не парься и бери обычную сериализацию. Если захочешь попробовать разные языки, надо, чтобы формат понимался всеми. Например, хорошо подходит JSON.
- Обработчик - скрипт на РНР (на самом деле, на любом языке, на котором есть Job API), который выполняет работу. Одновременно можно запустить несколько обработчиков, хоть на одном сервере, хоть на тысяче. Каждый из них это отдельный РНР-процесс (его можно запустить в консоли или через nohup).
- Задача (Task) - это несколько заданий, которые необходимо выполнить параллельно. Ты ставишь серверу задачу, чтобы он выполнил одну или несколько работ, а дальше дело техники.
- Клиент - скрипт, который создаёт задания и работы и посылает их на сервер.
- Сервер - собственно, центральный элемент системы. Принимает задачи от клиентов и смотрит, каким обработчикам и в какой очерёдности распределить конкретные работы. Он же следит за доступностью обработчиков, собирает ответы и отсылает их назад клиенту. Если ты послал задание обработчику, но посреди этого возникла ошибка, сервер это воспримет нормально и попробует переназначить это задание другому доступному обработчику. На случай, если надо перегружать сервер, Gearman может задействовать MySQL или memcachedb для хранения там заданий, тогда ему нестрашны никакие падения. Конечно, самих серверов также может быть много, чтобы вырубание одного никак не влияло на выполнения работ.
Понравилось? А то, эта штука потрясающе мощная и в то же время простая. Я пробовал посмотреть на аналогичные штуки на Java - реально чуть мозги не расплавились от таких замутных структур и настроек, чтобы просто запустить все!
Сначала поставим Gearman: apt-get install gearman-job-server, а после запустим как демон: gearmand -d. По умолчанию, она слушает порт 4730, на который и будем отправлять задания, конечно, не вручную.
Теперь давай подумаем, как превратить наш код, который написали ранее, в распределённый. У нас уже есть функция, которая принимает ссылку на ленту и полностью её обрабатывает. Мы напишем обработчик для Gearman-а, который будет принимать задание - JSON-массив с линками на ленты. В случае ошибок при обработке мы все равно будем отправлять сообщение серверу, что все ОК, потому что если ошибка связана с самой лентой (например, удалённый сервер лежит), то нам же не надо сразу же ещё раз пробовать выполнить задание, лучше дождаться следующего цикла обработки. Иначе стоит появиться одной сбойной ленте, как вся система зависнет, постоянно пытаясь выполнить задание.
Далее у нас будет специальный скрипт, который запускается по Cron-у каждые 5 минут (но если хочешь, то чаще, от этого зависит частота опроса лент), будет выбирать все ссылки на ленты, которые надо обновить, формировать из них задания, а потом скомандует Gearman-у выполнить все задания параллельно. Если у тебя будет 10 лент, то смотря, сколько обработчиков ты запустишь, сервер сможет проверять одновременно столько же лент. Если обработчиков будет меньше, то каждый из них будет последовательно обрабатывать все свои задания, пока не завершит всё.
Хочу, чтобы ты обратил внимание - обработчики остаются запущенными все время, они, по сути, демоны, поэтому если будешь небрежно писать код, накапливаются утечки памяти. Поэтому хорошо бы время от времени перезапускать обработчики, например, раз в неделю. Но это никак не повлияет на работу всей системы, даже если ты просто вырубишь все скрипты - задания останутся и будут обработаны как только ты запустишь хоть один скрипт. А если вдруг увидишь, что сервер не справляется с работой, добавь ещё один, запусти там новые обработчики - и они сразу включатся в работу! Если пойти ещё дальше, то можешь поставить счётчик, и на каждые, примером, пять новых лент, добавленных пользователями, запускать на сервере новый обработчик. А если кто-то отпишется от лент, то можно и убрать лишний процесс. Так ты получишь динамическое масштабирование в зависимости от нагрузки.
Давай кратко распишу, как устроены все скрипты, более подробный код с комментариями посмотришь на диске.
Обработчик ленты (файл feedWorker.php) после запуска создаёт объект GearmanWorker, который скрывает все подробности взаимодействия с сервером. Далее необходимо зарегистрироваться на сервере - методом addServer(). Сервером может быть много, если необходимо построить отказоустойчивую систему. И наконец, зарегистрируем функцию как обработчик, используя произвольное имя - методом addFunction. Теперь, если на сервере будут задания, ассоциированные с этим именем, обработчик вызовет нашу функцию и передаст ей строку с заданием. В одном скрипте можно описать несколько функций и зарегистрировать их под разными именами, но учти, что одновременно сможет работать только одна.
<?php $worker= new GearmanWorker(); $worker->addServer(); $worker->addFunction("feedProcesor", "myFeedProcessor"); function myFeedProcessor($job) { $feeds = Zend_Json::decode( $job->workload() ); } while ($worker->work()); ?>
Когда обработчик вызван, ему передаётся специальный объект задания, из которого сперва надо получить нужные нам для работы данные. Так как они закодированы в строку JSON-ом, используем специальный компонент Zend_Json, который превратит строку в обычный массив. Класс удобен тем, что использует, если доступно, нативные РНР-функции, а если их нет, то пытается сам разобрать, к тому же, без проблем работает с кириллицей.
Посмотри, всего шесть строк, и мы превратили нашу функцию обработки лент в распределённую. Теперь ты можешь просто заинклудить файл processFeed.php и передать функции processFeed() полученный массив ссылок $feeds.
Что-бы сообщить серверу Gearmand, что все отлично и мы выполнили задание, достаточно вызвать $job->sendComplete('OK'). Хотя вполне можно просто вернуть из функции true и все, система достаточно гибкая и простая, чтобы делать за тебя всю работу.
Ещё необходимо сформировать задачу, чтобы сервер знал, что необходимо выполнить. Этот код также очень простой:
<?php $gmclient= new GearmanClient(); $gmclient->addServer(); $feed_links = $db->fetchAll('SELECT fid, feed_url FROM feeds WHERE errors < 3'); foreach ($feed_links as $fl) { echo "Add to processing queue: " . $fl['feed_url'] . "\n"; $gmclient->addTaskBackground('feedProcesor', Zend_Json::encode(array($fl))); } $gmclient->runTasks(); ?>
Объясняю, как это работает. Так как задания раздет клиент, мы первым делом используя Gearman API, создаём объект клиента и регистрируем его на сервере. Здесь также можно добавить несколько серверов и рассылать задания сразу всем. А потом просто выбираем из базы данных ссылки на все ленты, которые имеют менее 3-х ошибок. Если ошибок больше, вероятно что-то с лентой не так, поэтому даже не будем пытаться проверять. Потом в цикле перебираем все ленты и формируем из них задания, закодированные в JSON-формат.
После того, как весь набор задач сформирован, запускаем его на выполнение командой runTasks(). Все задачи будут выполнены по возможности параллельно и в фоновом режиме. Это означает, что мы не сможем получить ответ от обработчиков. Если же это необходимо, следует задавать задачу командой do, тогда она выполнится в синхронном режиме, а результат будет передан тебе. Но нам значения не нужны, так же как и вообще следить за выполнением работы - задали задачи и все, выходим из скрипта, остальную работу оставим серверу. Осталось запустить этот скрипт из Cron-а, но с этой задачей думаю, справишься сам. Частота обновления лент зависит от многих факторов, наверное, лучше всего начать с 10 минут, если лент не очень много. Обрати внимание, желательно, чтобы суммарное время обработки всех лент было меньше, чем интервал обновления, иначе сильно возрастёт нагрузка на сервер. В таком случае просто добавь обработчиков или новый сервер, если перестанет справляться внешний канал, ведь тебе же сначала надо запросить у сервера ленты, а это порядочный траффик.
Свежие комментарии