Friday, April 17, 2009

Пример архитектуры кэширования(Caching Architecture)

Общую структуру кэширования и получения контента можно представить в таком виде:

  • Красным цветом отображены наиболее критичные части без которых система не может функционировать в нормальном состоянии.
  • Синим цветом соответственно представлены все части отвечающие за стабильное(нормальное) состояние сервиса.
  • Зеленым цветом - наиболее производительный вариант работы.
Каждый овал может представлять как отдельный сервер, так и несколько овалов может быть размещено на одном сервере, кроме вариантов дублирования веток обработки.

Контент любого сервиса можно разделить на 3 составляющие: статический контент, медиа контент и динамический контент.

Статический контент


Статический контент - изменяется редко, и создается разработчиками ресурса и изменяется только при смене версии ресурса. И как следствие его желательно отдавать легковесным сервером с максимальной производительностью и поддержкой системных вызовов(writev, sendfile). Для запросов на эти файлы вебсервер должен выдавать последнюю дату изменения, при последующих запросах сервер выдает Not Changed.

Медиафайлы


Медиафайлы - политика кэширования подобна, с некоторыми уточнениями: файл может изменяться только по запросу пользователя, на данный момент используется только процедуры добавления и удаления файлов. И политика заключается в том чтобы снизить количество запросов единичного пользователя для получения файлов.

Система содержит поддержку зеркалирования и пользователь может получать файл с нескольких источников, в наиболее универсальной системе файл должен скачивается с ближайшего свободного сервера. Добавления файлов может быть осуществлено на любое из зеркал, с последующей синхронизацией или запросом к другим зеркалам, если на текущем зеркале такого файла нет.

Методы кэширования: при приходе запроса на получение файла без заголовков(HTTP_IF_MODIFIED_SINCE) - выдать файл с указанием времени создания(Last-Modified) для локального файла или указания даты формирования ответа для файла полученного с другого зеркала, и указания для дополнительных заголовков (Cache-Control: max-age=28800). При приходе заголовка (HTTP_IF_MODIFIED_SINCE), если файл с того времени не обновился выдается что контент не изменился(HTTP/1.x 304 Not Modified). Это позволяет дать возможность закешировать файл на месяц и более - причем первый месяц браузер не будет делать никаких запросов к серверу. Желательно иметь одно запасное синхронизируемое зеркало, чтобы при проблемах с основным на него можно было перевести запросы.

Динамические страницы


Запрос поступает на frontend сервер, для всех запросов которые могут попасть под кэшируемость, который запрашивает этот файл с Memcached. Если файл там присутствует, то он возвращается с указанием, что его можно в течении 15 секунд не запрашивать - это блокирует повторных запрос от браузера на это контент на 15 секунд (Cache-Control: max-age=15). Если контент в кэше отсутствует, то выполняется запрос к backend, который повторно запрашивает контент от кеша, так как исходный url может не совпадать с результирующим url идущим на обработку. При отсутствии данных в кеше или не кэшируемости запроса запрос перенаправляется в SlaveDb. Возвращенный от туда запрос при возможности кэширования сохраняется в кэше с указанием времени кэширования определяемого для этого url, для примера: 30 секунд главная страница, 90 - страницы второго уровня. В ответе для кэшируемых станиц указывается в значении max-age тоже время, что указывалось для времени хранения. Как следствие мы получаем, что все станицы кэшироватся в браузере пользователя первым подавшим запрос на полное время, все последующие до окончания времени кэширования на 15 секунд.

Вторая ветвь(зеленая) нужна для снижения нагрузки на slave, так как на всех slave должен быть одинаковый контент, то при разнесении запросов на несколько сервером мы должны получить ~2 увеличение скорости обработки. Memcached - один, так как при использовании 2 экземпляров для каждого slave приведет к дублированию контента на каждом.

Пример кода


function exitIfNotModifiedSince($last_modified) {
if(@array_key_exists("HTTP_IF_MODIFIED_SINCE",$_SERVER)) {
$if_modified_since=@strtotime(@preg_replace('/;.*$/','',$_SERVER["HTTP_IF_MODIFIED_SINCE"]));
if($if_modified_since >= $last_modified) {
header("HTTP/1.x 304 Not Modified");
header("Last-Modified: ".date("D, j M Y G:i:s T", $last_modified));
exit();
}
}
}


function dump_foto_content($photo) {
exitIfNotModifiedSince(strtotime("now")- 100000);
// send the right headers
header("Content-Type: ".$photo["mime"]);
header("Content-Length: ".strlen($photo["photo_data"]));
header("Last-Modified: ".date("D, j M Y G:i:s T"));
header("Cache-Control: max-age=28800");
echo $photo["photo_data"];
exit;
}


P.S.: Для разрешения кэширования на промежуточных серверах нужно добавить параметр:
header("Cache-control: public");

Thursday, April 9, 2009

Be Linux

Наиболее понравившийся мне видеоролик к конкурсу We're Linux



И ролик BBC использовавшийся при создании этого ролика:

Sunday, April 5, 2009

Оптимизация времени кэширования

  1. Оптимизация времени кэширования под количество запросов, обычно пользователей загрузивших страницу можно разделить на 2 группы: зашедших на страницу с другой станицы или непосредственно на страницу и другую группу обновляющих станицу в ожидании изменений. Первой группе не столь важно сколько кэшируеться страница им более важна скорость получения страницы и можно использовать максимальное допустимое время кэширования. Для второй же группы нужно кэшировать минимальное время - их можно определить по referer и тому что у них будет стоять заголовок "if not modifined since" - используя эти особенности можно при приходе первого типа запроса использовать удвоенное время кэширования, а для вторых одинарное - в результате вторая группа получает всегда обновленной контент, а первая скорость(и при совпадении запросов обоих групп еще и более быстрое обновлении, но они этого не замечают так видят страницу первый раз).
  2. При появлении запроса с заголовками не изменилось ли что то ("if not modifined since"), если еще существует кэш этого запроса можно сравнивать указанный в запросе хеш с возвращенным после обработки и стараться возвратить не изменилось так как это позволит быстрее освободить ресурсы.
  3. При варианте, когда контент уже существует на диске и его расположение высчитывается скриптом, более разумно использовать не хеш, а дату изменения и стараться, если это возможно не выдавать, а перенаправлять пользователя на это контент, в результате мы получаем минимальное потребление памяти и скорость отработки близкую к отображению статики. Перенаправлять желательно даже, если результат находиться на другом ресурсе, меньше посредников быстрее ресурс освободиться.
  4. Очень желательно кэшировать результаты запросов не на самодельных скриптах кэширования, а использовать более распостраннённые системы кэширования написаные на компилируемых языках - так больше вероятность, что выдача будет максимально быстро отдаваться и не нужно тратить время на создание велосипеда и его тестирование. Как следствие лучше перенаправить на кэш или файловую систему, чем генерировать контент скриптом - так больше вероятность, что будет использована максимально эффективная стратегия отдачи контента.

Friday, April 3, 2009

Недостаток в коде модуля балансировки

Обнаружилась маленький недостаток в модуле балансировки - не контролируется количество поставленных в ожидание освобождения ресурсов (очередь на основе семафора) - при большом превышении количества запросов по отношению к ограничению - запрос может быть выполнен даже после того как клиент отсоединиться от сервера - время ожидание ничем не ограничено. То есть существует вероятность: что модуль при очень большом количестве запросов будет давать запросы ответы на которые пользователь уже не ждет. Этот недостаток не должен сильно повлиять не производительность системы - так как на DOS атаку он не был рассчитан, он предназначен только для выравнивания и снижения единовремменой пиковой нагрузки.

Friday, March 27, 2009

Nothing is always absolutely so

Сменил заголовок своего блога на утверждение высказанное писателем-фантастом Theodore_Sturgeon, думаю оно максимально выразит идею, что все записаное в блоге является только моим субъективным мнением и на истину в полной мере не претендует.

Простейшая реализация OpenID

Документация изначально написана для внутреннего пользования при создании модуля работы с OpenId. Модуль поддерживает только часть функционала openid (только регистрацию по первой версии OpenId). Результат можно посмотреть в здесь. Поддерживает только идентификаторы вида http://имя пользователя с именем провайдера - на этот идентификатор все openid провайдеры должны возвращать свое описание в заголовках страницы. Решил выложить, так как за пол года не появилось более полной реализации по сравнению с той, что есть у меня.

Общие шаги(OpenID *)

Полученному идентификатору запрашиваем страницу в ней ищем значения openid2.provider(OpenId 2.0), openid.delegate (OpenId 1.0 в общем случае должен совпадать с введенным именем пользователя), openid2.local_id(OpenId 2.0 в общем случае должен совпадать с введенным именем пользователя), openid.server (OpenId 1.0) и X-XRDS-Location (OpenId 2.0).

Например:
  • <link rel="openid2.provider" href="http://openid.yandex.ru/server/" />
  • <link rel="openid2.local_id" href="http://openid.yandex.ru/имя пользователя/" />
  • <link rel="openid.server" href="http://openid.yandex.ru/server/" />
  • <meta http-equiv="X-XRDS-Location" content="http://openid.yandex.ru/имя пользователя/yadis/" />
Для OpenId 1.0 Перенаправление пользователя для проверки его валидности

Сделать запрос к серверу (openid.server) на проверку пользователя (openid2.local_id) режим checkid_setup - так как это простейший режим и требующий минимальной реализации с нашей стороны:
  1. [openid.server]?
  2. openid.mode=checkid_setup&
  3. openid.identity=[openid2.local_id]&
  4. openid.return_to=[Путь на который нужно вернуться]&
  5. openid.trust_root=[Проверяем доверие серверу(доменное имя)]&
  6. openid.claimed_id=[openid2.local_id]
Для OpenId 2.0 Получение дополнительной информации

По полученному X-XRDS-Location получаем Yadis документ:
<?xml version="1.0" encoding="UTF-8" ?>
<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)">
<XRD>
<Service priority="20">
<Type>http://specs.openid.net/auth/2.0/signon</Type>
<Type>http://openid.net/signon/1.0</Type>
<URI>http://openid.yandex.ru/server/</URI>
<LocalID>http://openid.yandex.ru/имя пользователя/</LocalID>
</Service>
</XRD>
</xrds:XRDS>

По нему нужно сформировать адрес перенаправления на сайт провайдера для проверки, но в текущей реализации это не сделано и перенаправление всегда генерируется на основе первой версии OpenId. Вторая версия более сложная в реализации, но позволяет получить дополнительные поля при регистрации (описание пользователя).

Возвращаемый сайтом ответ через обратное переполнение
В результате проверки на сайт мы должны вернуться в любом случае с такими параметрами:
  1. Удачный:
  • [Вернутся на такой url]?
  • openid.assoc_handle=[Тип подписи]&
  • openid.identity=[openid2.local_id]&
  • openid.mode=id_res& (нам доверяют)
  • openid.op_endpoint=[openid.server]&
  • openid.response_nonce=[временной штамп]&
  • openid.return_to=[Вернутся на такой url]&
  • openid.sig=&(подпись)
  • openid.signed=(от каких полей подпись)
  1. Не удачный
  • [Вернутся на такой url]?
  • openid.mode=cancel (вам не доверяют)
Проверка подписи
Для яндекса проверка не сработала, когда я тестировал модуль, возможно я что-то делаю не так. Стандартный код проверки OpenId тоже не делал эту проверку.

После получения мы должны проверить, а те ли нам ответили и запросить с сервера дополнительные сведения:
  • [openid.server]?
  • openid.mode=check_authentication&
  • openid.assoc_handle=[Значение openid.assoc_handle из предыдущего запроса]&
  • openid.sig=[Значение openid.sig из предыдущего запроса]&
  • openid.signed=[Значение openid.signed из предыдущего запроса]&
  • openid.identity=[Значение openid.identity из предыдущего запроса]&
  • openid.return_to=[Значение openid.return_to из предыдущего запроса]

Saturday, March 21, 2009

Продолжая тему кэширования: идеи относительно ответов

Еще пара идей:
  • Если пришел запрос на проверку и у нас есть закэшированный вариант(и время кэширования его истекло) ответа можно сравнить результат ответа на запрос с кэшем, если контент не изменился - дату кэширования оставить тем же(дата создания) - но увеличить время критического и валидного кэширования - ответ не изменилось гораздо предпочтителен по сравнению с полным результатом.
  • Станицу ответа - желательно разбивать на несколько частей - чтобы они грузились через отдельные запросы, тогда можно для разных частей указывать разные политики кэширования и кэшировать более маленькие кусочки.
  • Уменьшить количество запросов дающих одинаковый результат с разными url, например a=b/c=d === c=d/a=b. Если избежать их по каким то причинам нельзя - нужно всегда на уровне сервера перенаправлять на наиболее популярный (по каким то причинам) вариант запроса.