Хочу поделиться проделанной работой по исследованию производительности различных библиотек для работы с memcached в Node.JS. Для исследования были отобраны 4 кандидата.
Краткие описания взяты прямо из источников и приведены в оригинале. Вот получившийся список с версиями и ссылками.
- mc v1.0.6 — The Memcache Client for Node.js (mc)
- node-memcache v0.3.0 — A pure-JavaScript memcached library for node. (node-memcache)
- node-memcached v0.2.6 — Fully featured Memcached client for Node.js (node-memcached)
- memjs v0.8.0 — MemJS is a pure Node.js client library for using memcache. (memjs)
Есть и другие библиотеки, но были выбраны именно эти четыре. Причина простая: информации и упоминаний в сети о них было больше чем по остальным.
Выбирались они около года назад, а до статьи руки дошли только сейчас. Буду рад, если укажете еще на парочку-другую достойных соперников. Обязательно выложу тесты и по ним.
Из предыстории
Изучать Node.JS, экспериментировать с ним и реально на нем писать я начал относительно недавно, примерно около года назад. Задача была поставлена достаточно интересная и фактически никаких требований по платформе не было. Можно сказать, я самолично тогда выбрал Node.JS, как альтернативу «вдоль и поперек» изученному PHP и об этом нисколько не жалею. Другими словами, только рад такому выбору. Сейчас уже запущен второй проект и Node.JS себе оправдывает. Скажу честно, я получил массу удовольствия от более глубокого изучения языка JavaScript. Мне сразу же стало понятно, что это совсем не тот язык, который я уже достаточно давно, казалось бы, знаю. А как я был поражен, попробовав писать модульные тесты с использованием nodeunit, как все изящно и лаконично в отличие от того же PHP.
Немного отклонился от темы. Это, конечно, неоспоримые достоинства, которые тут на хабре и на других ресурсах уже ни один раз описаны. Но это не PHP, на котором у нас написана масса внутренних библиотек, и существует масса чужих на все случаи жизни. Ведь опыта написания проектов различной сложности на PHP уже больше 10 лет. Мне опять пришлось все делать с нуля. Как когда-то, при переходе сначала с C++ на перл, а потом с перла на PHP. Тут я также брал новенький, незнакомый инструмент и пытался как-то его настроить под себя.
Нужно было научиться работать со многими системами, будь то работа с mongoDb или с RabbitMQ, или даже с тем же Mysql. Практически ничего стандартного, как в PHP тут не было. Нужно было выбирать из массы библиотек самую подходящую и перекрывать своими объектами нужную мне функциональность. Вот так, по шагам, я исследовал различные инструменты и библиотеки и в определенный момент добрался и до мемкеша.
Итак, приступим
Для чистоты эксперимента я обволоку единым интерфейсом все эти библиотеки, чтобы в тестовом скрипте просто указать нужный вариант, а сами действия будут одинаковы для всех испытуемых.
Интерфейс будет состоять из 4-х методов:
Init(cb) — инициализация объекта
Set(key, val, cb) — установка значения
Get(key, cb) — получене значения
End() — закрываем все активные соединения
Пример реализации интерфейса для библиотеки mc будет выглядеть где-то так:
impl.mc = function(){};
impl.mc.prototype = {
Init : function(cb) {
var self = this;
var Mc = require('mc');
self.mmc = new Mc.Client('<ip>:11211', Mc.Adapter.binary);
self.mmc.connect(function() {
cb(self);
});
},
Set : function(key, val, cb) { this.mmc.set(key, val, {flags: 0, exptime : 100}, cb); },
Get : function(key, cb) { this.mmc.get(key, cb); },
End : function() { this.mmc.disconnect(); }
};
Пишем такие-же реализации для остальных 3-х библиотек и помещаем их в модуль impl.js. Сразу оговорюсь, чтобы не нагромождать и так не маленький код я не добавлял в примеры обработку ошибок. Это всего навсего тесты и, естественно, в реальных обертках библиотек это все учитывается.
Как будет происходить тестирование.
Первый шаг — записываем 1000 случайных целых значений с названиями ключей вида:
__key_[libName]_[0...999],
где libName это название одной из исследуемых библиотек (mc, memcached…)
Измеряем время и считаем кол-во записей в секунду. Имя библиотеки добавляется в ключ, чтобы они не пересекались в рамках теста между разными реализациями.
Второй шаг – после того как записали, начинаем читать. Читаем 100k раз по случайному ключу из диапазона, опять же засекаем время и считаем кол-во чтений в секунду.
А вот и сам код, который последовательно запустит все варианты. Старался минимизировать код по максимуму, ну тут уж как получилось.
var cacheImpl = require('./impl');
// функция создания ключа
// с учетом реализации className для того, чтобы избежать одинаковых ключей для различных библиотек
function _key(className, i) { return '__key_' + className + '_' + i; }
// выполнить тесты для реализации className (memjs, memcached, memcache, mc)
function testPerform(className, cb) {
var impl = new cacheImpl[className]();
impl.Init(function(pc) {
var keysCount = 1000, // кол-во ключей
getNum = 100000, // кол-во чтений
all = 0,
d1, d2;
// запуск установки значений кеша
var startSet = function (cb) {
all = keysCount;
for (var i = 0;i < keysCount;i++) {
(function(num){
setImmediate(function(){
// ~~(Math.random() * 100000) - это я так привожу к целочисленному
pc.Set(_key(className, num), ~~(Math.random() * 100000), function(err) {
if (--all === 0) return cb();
});
});
})(i);
};
};
// запуск чтений из кеша по случайному ключу из диапазона
var startGet = function (cb) {
all = getNum;
for (var j = 0;j < getNum;j++) {
setImmediate(function(){
pc.Get(_key(className, ~~(Math.random() * 1000)), function(err, res) {
if (--all === 0) {
cb();
pc.End();
}
});
});
};
}
// запуск полследовательно всего вместе
var start = function (cb) {
d1 = Date.now();
startSet(function() {
d2 = Date.now();
console.log(className + ' Set qps: ' + Math.round(keysCount / (d2 - d1) * 1000));
d1 = Date.now();
startGet(function() {
d2 = Date.now();
console.log(className + ' Get qps: ' + Math.round(getNum / (d2 - d1) * 1000));
cb();
});
});
}
start(cb);
});
}
// вот так вот некрасиво запускаем последовательно все реализации
testPerform('memcache', function(){
testPerform('memcached', function(){
testPerform('memjs', function(){
testPerform('mc', function(){});
});
});
});
Меня тут прежде всего интересовали не абсолютные значения количества записей и чтений в секунду (хотя и это тоже), а именно какая из библиотек будет быстрее вообще.
Тесты проводились на рабочем кластере, запись/чтение производились на memcached на соседнем сервере. Сервера нагружены реальным трафиком, поэтому на абсолютные значения можно не особо обращать внимание. Важны именно относительные величины.
Итого, после запуска скрипта, получаем такой результат:
memcache Set qps: 8929 memcache Get qps: 19444 memcached Set qps: 6098 memcached Get qps: 8924 memjs Set qps: 8850 memjs Get qps: 12857 mc Set qps: 14286 mc Get qps: 23207
Сразу же видим явного лидера, причем и по записи и по чтению, распишу в процентном соотношении от лидера.
Рейтинг по запси:
1. mc 100.00% 2. memcache 62.50% 3. memjs 61.95% 4. memcached 42.69%
Рейтинг по чтению:
1. mc 100.00% 2. memcache 83.79% 3. memjs 55.40% 4. memcached 38.45%
Вот такие интересные результаты. Безусловно, библиотеки отличаются функциональностью, удобством работы с ними, возможностью работы с кластером мемкешей, пулом соединений и т. д. Я намеренно упускаю эти моменты и беру самую распространенную ситуацию, когда имеется один сервер мемкеша. Если с одним будет работать быстрее, то и с несколькими серверами относительный результат не должен сильно отличаться.
Тесты достаточно синтетические, так как записываются только целые числа, чтение производится только по существующим ключам (не учтены случаи, когда ключа нет) и т. д. Но, тем не менее, все библиотеки находятся в равных условиях. Все же, надеюсь, что результаты будут полезными.
Буду рад, если кому-то помог определиться с выбором библиотеки, или хотя бы заставил задуматься о том, что не все библиотеки одинаково хороши в плане производительности.
Хотелось бы выслушать конструктивную критику проделанной мною работы: последовательность, стиль изложения и все-все, что может помочь сделать статью лучше. Будем учиться вместе.
Вот такой мой «дебют» на хабре. Насколько он удачен, решать вам.