Memcached Multi-Get, зачем?

Memcached сегодня является самым популярным решением кеширования данных в мире (в Web приложениях). Масштабирование и оптимизация - в двух этих задачах зачастую фигурирует memcached. В этой статье мы не будем в очередной раз хвалить этот продукт, а рассмотрим его дополнительные возможности (точнее всего одну).
Мы рассмотрим очень полезную функциональную особенность про которую многие забывают (а некоторые даже и не знают). Это операция множественного чтения или multi-get. В чем ее суть и действительно ли ее использование оправдано?
Зачем нужен multi-get
Операция multi-get позволяет за один запрос к серверу memcached получить сразу несколько объектов. Работает она очень просто, принимая в аргумент список ключей, по которым необходимо вернуть объекты.
Зачем это нужно? В статье “Правила кеширования” были рассмотрены удобные подходы при кешировании списков данных. При этом кешируются только первичные ключи в списках, а данные кешируются отдельными объектами. Это обеспечивает отличную управляемость данными. В этом случае Вам придется доставать достаточно большое число объектов каждый раз, когда Вы отображаете список данных. Тут и нужна операция multi-get.
Например, пусть у Вас есть список новостей. Тогда сам список будет хранится в ключе под именем, например “news”. Каждая новость будет храниться в ключе под именем “news_item_
# Функция обработки списка ключей для добавления префиксов - ее лучше в прослойке реализовывать
function make_list_keys(&$value, $key, $prefix) { $value = $prefix . $value; };
...
$m = new Memcache;
...
# Получаем список первичных ключей
$list = $m->get('news')
# генерируем массив ключей
array_walk($list, 'make_list_keys', 'news_item_');
$news_list = $m->get($list);
...
Если бы мы обращались к серверу кеширования за каждой новостью, чтений было бы намного больше (а именно столько, сколько новостей в списке).
В принципе, с помощью метода множественного чтения мы решаем две проблемы:
- Снижаем оверхед операций чтения, т.к. их становится намного меньше, тем самым улучшая производительность системы
- Понижаем внутренний сетевой трафик (ощутимо, когда у Вас большие кластеры memcached и множество бекенд серверов)
Действительно ли результаты будут ощутимыми? Давайте тестировать:
Один Multi-get vs много get
Напишем простой скрипт сравнения временных затрат на получение данных в обоих случаях:
$tests = 15000;
$m = new Memcache;
$m->connect('localhost', '11211');
for ( $i = 0; $i < $tests; $i ++ )
{
$m->set('test' . $i, md5($i));
}
$t = microtime(true);
for ( $i = 0; $i < $tests; $i ++ )
{
$list_get[] = $m->get('test' . $i);
$keys[] = 'test' . $i;
}
echo "Let's see our results:\n";
echo 'Fetched ' . $tests . ' objects with standard get in ' . (microtime(true) - $t) . 's';
echo "\n";
$t = microtime(true);
# Метод Memcache::get в PHP работает в режиме multi-get,
# когда получает массив в первый аргумент
$list_mget = $m->get($keys);
echo 'Fetched ' . $tests . ' objects with multiple get in ' . (microtime(true) - $t) . 's';
echo "\n";
Результаты весьма интересные:
Fetched 15000 objects with standard get in 0.47441411018372s Fetched 15000 objects with multiple get in 0.023789882659912s
В случае multi-get мы потратили на порядок меньше времени. И стоит учесть, что тестирование выполнялось на локальном компьютере, а значит сетевой трафик отсутствовал. Если учесть это, да еще и то, что сетевой трафик генерирует не только memcached
(скорее не столько memcached, сколько всё остальное), разница будет еще больше.
Подводя итог, можно сказать, что multi-get очень полезная операция и использовать ее стоит. Тем более, что это потребует минимального изменения Вашего кода.
А Вы использовали этот метод?


хм, вроде как все правильно написал, но есть небольшое нюансы, которые могут немного изменить результаты теста:
1) дело в префиксах! если ты добавляешь их для каждого элемента внутри функции, то должен отдать вы выход функции ключи без префиксов. можно решить эту проблему на уровне приложения и писать обращение в массивам тоже через функцию “make_list_keys”:
$keys = make_list_keys(array(1,2,3,4), ‘news’);
$result = $memcache->get($keys);
$news1 = $result[$keys[0]];
$news2 = $result[$keys[1]];
правда это тоже не совсем так:
2) мемкеш multiget отпдает только те элементы которые есть, остальные просто вытерает из списка. другими словами не жди null или false в возвращаемом массиве
$keys = make_list_keys(array(1,2,3,4), ‘news’);
$result = $memcache->get($keys);
var_export($result);
array(
‘news_1′ => ‘qwerty’,
‘news_4′ => ‘qwerty’
)
Так вот если перебирать два раза массив: первый раз для добавления ключей, а второй чтобы их убрать - будет использовать больше ресурсов и следовательно больше времени.
p.s. прости но пример 1 и пример 2 у тебя в тесте неправильные. результаты get’a мемкеша отличаются
поправка,
$list_get != $list_mget
@tarasov
Большой спасибо за комментарии!
Вы не совсем правильно понимаете саму суть multiget и того, что написано (скорее всего, я не очень удачно описал все):
1. Я думаю понятно, что ресурсы, которые траться на перебор массива, просто не сравнимы с теми, которые тратяться на запрос к удаленному серверу. Что лучше: перебрать 100 числовых элементов или сделать 100 сетевых запросов?
2. Проблема с отдачей неполного массива после multiget решается в два счета, т.к. Вы знаете список всех ключей, а следовательно можете обработать отсутсвующие элементы. Тогда использование multiget всеравно оправдано, т.к. сэкономленные ресурсы уменьшаться всего на эту отсутствующую разницу.
3. Про отдачу ключей с префиксами и без вообще ничего не понял
4. Поделитесь своими результатами, было бы здорово?
не пришло на почту уведомление о новом комментарии, только сейчас увидел(
multi-get VS single-get: конечно же multi коллосально выигрывает!
по поводу префиксов:
например у вас на сайте есть статьи, пользователи и галлереи и вы кешируете строчки БД, то в результате должны использовать префиксы в кеше, как например,
users: user_1, user_2
articles: article_1, article_2, article_3
galleries: gallery_1, gallery_2
следовательно в теле програмы вы будете использовать вызов по самим id, что-то вроде
getUsers(array(1,2,3,4))
getArticles(array(1,2,3,4))
сама функция внутри getUsers конвертирует в правильные ключи. я беру Ваш пример:
# генерируем массив ключей
array_walk($list, ‘make_list_keys’, ‘news_item_’);
$news_list = $m->get($list);
так вот на выход вы получите массив вида
array(
‘user_1′ => array(name => Petya, group => 1)
‘user_4′ => array(name => Vasya, group => 1)
)
Но для логичной и правильно работы функция должна отдавать в таком же виде в каком и запросили
array(
1 => array(name => Petya, group => 1)
4 => array(name => Vasya, group => 1)
)
так вот на перебор больших массивов кеша будет уходить довольно много времени.
тут я хочу извиниться, но тестовые скрипты уже давно затерялись
если вы используете один мемкеш для нескольких схожих проектов, то вам нужно добавлять в ключи еще и id/name сайта, для примера:
site1_user_1, site1_user_2
site2_user_1
конечно же, это лучше делать сразу в самой функции getUsers(), иначе прийдется делать еще один обход массива.
так вот, если у вас в результатах массива сравнительно большой текст, как например тексты статей, или просто множество заголовков + доп инфа о самых статьях - то обхождения массива довольно затратное дело, поскольку создается 2 одинаковых массива в памяти.
Что по поводу отдачи неполного массива, то это необходимо учитывать сразу. Несколько сложнее, если вы переписываете готовый проект, например:
вполне вероятно, что для каждого элемента вам нужно вывести на страницу описание и если такого не имеется нарисовать какую-то заглушку:
$users_id = array(1,2,3,4,5);
$users = getUsers($users_id);
переборы массивов несколько отличаются при существующих ключах и нет:
есть пустые ключи:
foreach($users as $user)
{
if (empty($user)) echo ‘anonymous’;
}
нет пустых ключей, но нужно вывести инфу
foreach($users_id as $id)
{
if (!isset($users[$id])) echo ‘anonymous’;
}
возможно все первоклассные программисты и мой комментарий излишен, но в самой статье в тесте не учтено повторное обхождение массива, которое может повлиять на результат.
P.S. Простите за много букв.
похоже на то что капча експайрится по времени. поставьте немного больше времени, пожалуйста. а то неудобно оставлять комменты
@tarasov
Спасибо!
Комментарий очень исчерпывающий.
Дествительно в случае использования multi-get Вам нужно несколько усложнить логику с переборами списков ключей. Могу только добавить, что следует максимально использовать стандартные функции PHP по работе с массивами, т.к. они гораздо производительнее, чем обычный перебор.
Действительно описанные Вами моменты не представлены в статье, но думаю сам Ваш комментарий уже очень хорошо дополняет ее