Джино: хостинг и веб-сервисы

Система Orphus
Russian version
Добавить на Del.icio.us
English version
Добавить на Digg.com

 dkLab | Конструктор | CacheLRUd: добавляет функцию LRU-очистки данных (как в memcached) в MongoDB 

Карта сайта :: Форум «Лаборатории» :: Проект «Денвер»
Проект «Orphus» :: Куроводство: наблы :: Конструктор


2014-04-10

Принять участие в разработке библиотеки/утилиты можно на GitHub.

На тему "использование MongoDB вместо memcached" гуглится немало историй успеха. Похоже, есть широкий класс задач, для которых эта идея работает неплохо: прежде всего это проекты, где интенсивно используется тэгирование кэша. Но если вы попробуете, то заметите, что в MongoDB не хватает функции удаления из кэша записей, которые читаются реже всего (LRU - Least Recently Used). Как поддерживать размер кэша в разумных рамках? LRU - это, кстати, "конек" memcached; вы можете писать в memcached, не задумываясь о том, что ваш кэш переполнится; но как же быть с MongoDB?

Раздумывая над этим, я написал на Python небольшую утилиту CacheLRUd. Это демон для поддержки LRU-удаления записей в различных СУБД (в первую очередь, конечно, в MongoDB). Ферма таких демонов (по одному на каждой MongoDB-реплике) следит за размером коллекции, периодически удаляя записи, к которым доступ на чтение производится реже всего. Отслеживание фактов чтения той или иной записи кэша происходит децентрализовано (без единой точки отказа) по протоколу, основанному на UDP (почему так? потому что "наивный" вариант - писать из приложения в мастер-базу MongoDB при каждой операции чтения - плохая идея, особенно если мастер-база окажется в другом датацентре). Читайте подробности чуть ниже.

Но зачем?

Зачем может потребоваться заменять memcached на MongoDB? Попробуем разобраться. Понятие "кэш" имеет два различных типа использования.

  1. Кэш применяют, чтобы снизить нагрузку на перестающую справляться базу данных (или другие подсистемы). Например, пусть у нас есть 100 запросов в секунду на чтение некоторого ресурса. Включив кэширование и выставив маленькое время устаревания кэша (например, 1 секунду), мы тем самым снижаем нагрузку на базу в 100 раз: ведь теперь до СУБД доходит только один запрос из ста. И нам почти не нужно опасаться, что пользователь увидит устаревшие данные: ведь время устаревания очень мало.
  2. Есть и другой тип кэша: это кэш более-менее статических кусков страницы (или даже всей страницы целиком), и применяют его, чтобы снизить время формирования страницы (в том числе редко посещаемой). Он отличается от первого тем, что время жизни кэшированных записей велико (часы или даже дни), а значит, во весь рост встает вопрос: как же гарантировать, что кэш содержит актуальные данные, как его чистить? Для этого применяют тэги: каждый кусочек данных в кэше, имеющий отношение к некоторому крупному ресурсу X, помечают теми или иными тэгами. При изменении ресурса X дают команду "очистить тэг X".

Для первого варианта использования кэша ничего лучше, чем memcached, похоже, не изобретено. А вот для второго memcached буксует, и тут на помощь может прийти идея "MongoDB вместо memcached". Возможно, это как раз ваш случай, если ваш кэш:

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

MongoDB и ее репликация с автоматическим failover мастера (превращением реплики в мастера при "смерти" последнего) позволяют гарантировать надежность очистки того или иного тэга. В memcached же с этим проблема: серверы memcached независимы друг от друга, и для удаления тэга вам нужно "пойти" на каждый из них с командой очистки. Но что, если в этот момент какой-то из серверов memcached окажется недоступным? Он "потеряет" команду очистки и начнет отдавать старые данные; MongoDB данную проблему решает. Ну и, наконец, MongoDB очень быстра в операциях чтения, ведь она использует событийно-ориентированный механизм работы с соединениями и memory mapped files, т.е. чтение производится напрямую из оперативной памяти при достаточном ее количестве, а не с диска. (Многие пишут, что MongoDB настолько же быстра, как memcached, но я не думаю, что это так: просто разница между ними с огромным запасом тонет на фоне сетевых задержек.)

Вот как выглядит результат работы CacheLRUd на одном не слишком нагруженном проекте. Видно, что размер коллекции с кэшем действительно поддерживается постоянным на заданном в конфиге уровне 1G.

Установка CacheLRUd

Листинг 1
# Install the service on EACH MongoDB NODE:
cd /opt
git clone git@github.com:DmitryKoterov/cachelrud.git
ln -s /opt/cachelrud/bin/cachelrud.init /etc/init.d/cachelrud

# Configure:
cp /opt/cachelrud/cachelrud.conf /etc/cachelrud.conf  # and then edit

# For RHEL (RedHat, CentOS):
chkconfig --add cachelrud
chkconfig cachelrud on

# ...or for Debian/Ubuntu:
update-rc.d cachelrud defaults

Как работает демон

Чудес не бывает, и ваше приложение должно сообщать демону CacheLRUd (ферме демонов), какие записи в кэше оно читает. Приложение, очевидно, не может это делать в синхронном режиме (например, обновляя в мастер-базе MongoDB поле last_read_at в кэш-документе), потому что а) мастер-база может оказаться в другом датацентре относительно текущей веб-морды приложения, б) MongoDB использует протокол TCP, грозящий timeout-ами и "подвисанием" клиента при нестабильности связи, в) негоже выполнять запись при каждом чтении, не работает это в распределенных системах.

Для решения задачи применяется протокол UDP: приложение посылает UDP-пакеты со списком недавно прочитанных ключей тому или иному демону CacheLRUd. Какому именно - вы можете решить самостоятельно в зависимости от нагрузки:

  • Если нагрузка сравнительно невысока, посылайте UDP-пакеты тому демону CacheLRUd, который "сидит" на текущей мастер-ноде MongoDB (остальные просто будут простаивать и ждать своей очереди). Определить, кто в текущий момент мастер, на стороне приложения очень легко: например, в PHP для этого применяют MongoClient::getConnections.
  • Если же один демон не справляется, то вы можете отправлять UDP-сообщения, например, демонам CacheLRUd в текущем датацентре.

Подробности описаны в документации.

Что еще есть полезного

CacheLRUdWrapper: это простенький класс для общения с CacheLRUd из кода приложения на PHP, оборачивающий стандартный Zend_Cache_Backend (правда, этот класс для Zend Framework 1; если перепишете его для ZF2 или вообще для других языков, буду рад pull-request'ам).

Zend_Cache_Backend_Mongo: это реализация Zend_Cache_Bachend для MongoDB из соседнего GitHub-репозитория. Оберните объект данного класса в CacheLRUdWrapper, и получите интерфейс для работы с LRU-кэшем в MongoDB в стиле ZF1:

Листинг 2
$collection = $mongoClient->yourDatabase->cacheCollection;
$collection->w = 0;
$collection->setReadPreference(MongoClient::RP_NEAREST); // allows reading from the master as well
$primaryHost = null;
foreach ($mongoClient->getConnections() as $info) {
    if (in_array($info['connection']['connection_type_desc'], array("STANDALONE", "PRIMARY"))) {
        $primaryHost = $info['server']['host'];
    }
}
$backend = new Zend_Cache_Backend_Mongo(array('collection' => $collection));
if ($primaryHost) {
    // We have a primary (no failover in progress etc.) - use it.
    $backend = new Zend_Cache_Backend_CacheLRUdWrapper(
        $backend,
        $collection->getName(),
        $primaryHost,
        null,
        array($yourLoggerClass, 'yourLoggerFunctionName')
    );
}
// You may use $backend below this line.

Документация из README.txt

CacheLRUd: implements cache LRU cleanup on various databases (e.g. MongoDB)
Version: 0.57
Author: Dmitry Koterov, dkLab (C)
GitHub: http://github.com/DmitryKoterov/
License: GPL2

MOTIVATION

Sometimes (not always, but in many projects) it's quite handy to use some database (e.g. MongoDB) instead of plain-old memcached. This is probably your case if your cache is: a) relatively small (hundreds of GBs in one replica set, no more); b) contains many long-living keys (live for days); c) strongly tagged, and tags cleanup robustness is really important; d) needs to be replicated through multiple machines or datacenters; e) possibly is sharded (where MongoDB is very good at). So, probably in these cases it would be good to use MongoDB (or another database) as a caching engine. MongoDB is very fast (almost as fast as memcached on reads), it supports replication with auto-failover, could be sharded etc. But it does not implement an LRU cleaning algorithm, and you cannot, of course, update a "last hit" field in your collection on each cache read hit synchronously (especially if MongoDB master is in another datacenter). So there is no easy way to keep the database size constant. CacheLRUd tries to solve this problem: it looks after your database size and removes keys which were not READ for too long. But how CacheLRUd knows which keys were READ recently? Your caching layer should notify the daemon on which keys were read recently by sending UDP packets to it. UDP is asynchronous and does not block your application, so you may even send an UDP packet per each cache hit pack, even to another datacenter (if you do not have millions of page hits per second, of course). To eliminate single point of failure, install CacheLRUd service on each MongoDB node and send UDP notifications to alive nodes only.

HOW TO SEND UDP PACKETS TO THE DAEMON

See binding/ directory for client-side libraries. In general, to notify CacheLRUd that a cache key "key" has been read recently in a collection configured as "[collection_name]" in /etc/cachelrud.conf, just send an UDP packet to the daemon's port (defaults to 43521): collection_name:key If you register many hits, you may group them and send in a single UDP message separated by newline characters (to save bandwidth): collection_name:key1 collection_name:key2 ...

INSTALLATION ON LINUX

## Install the service on EACH MongoDB NODE: cd /opt git clone git@github.com:DmitryKoterov/cachelrud.git ln -s /opt/cachelrud/bin/cachelrud.init /etc/init.d/cachelrud ## Configure: cp /opt/cachelrud/cachelrud.conf /etc/cachelrud.conf # and then edit ## For RHEL (RedHat, CentOS): chkconfig --add cachelrud chkconfig cachelrud on ## ...or for Debian/Ubuntu: update-rc.d cachelrud defaults

SUPPORT FOR YOUR FAVORITE DATABASE/LANGUAGE

CacheLRUd is written in Python. To make it support a new database, you may create a file lib/cachelrud/storage/your_database_name.py (use mongodb.py for inspiration). You may also add support for more frameworks/languages, please put client-side libraries to binding/ directory.

WORKING WITH A REPLICA SET

Suppose we have a replica set with 3 machines: A, B and C. Assume A is the master (primary) node currently. Run CacheLRUd daemon on all this nodes for fail-tolerance. Then you have 2 different ways to configure /etc/cachelrud.conf: 1. If you want only one CacheLRUd daemon to be a reaper (a process who deletes outdated LRU keys), set up in cachelrud.conf at A: dsn = "mongodb://user:password@localhost/" and send UDP messages to the master A node only (it's typically easy to detect automatically who is primary by running something like client.getConnections() at the client side and check connection_type field). If remastering happens and A becomes a secondary (and e.g. B is a new master), CacheLRUd on B will activate reaping, and your application will also need to send UDP messages to the new master B. 2. If one reaper is not enough (you have too high keys creation rate, so you want to reap in parallel), specify replicaSet in your cachelrud.conf: dsn = "mongodb://user:password@localhost/?replicaSet=YOUR_RS" After that you may send UDP messages to ANY of CacheLRUd daemons: they will accept them and, at the same time, perform reaping in parallel.






Дмитрий Котеров, Лаборатория dk. ©1999-2016
GZip
Добавить на Del.icio.us   Добавить на Digg.com   Добавить на reddit.com