Проектируя архитектуру сервиса вы выбираете инструменты, наиболее подходящие для решаемых вами задач. Но чтобы использовать их по максимуму, необходимо найти самый надёжный и удобный драйвер. Конечно, если вы программируете на Python или, к примеру, PHP, найти нужный драйвер не проблема, ведь за много лет разработчики понаписали всякого, что проверено годами и стабильно работает. Но если вы программируете для node.js — это становится проблемой, драйверы скрипят, утекают и отказываются стабильно работать.
В данной статье мы расскажем о проблемах, с которыми столкнулись при выборе драйверов, и как их решили.
LiveTex — сервис, которым ежедневно пользуются более 10 миллионов посетителей. Как оперативную память нашего приложения мы используем базу данных Redis, в которой за день создаётся более двух миллионов ключей при обработке 34000 команд в секунду. Также используем Beanstalkd как очередь задач в которой каждую секунду обрабатывается более 10000 тасков. Мы хотим, чтобы наш сервис работал быстро и стабильно, от драйверов требуем того же.
Изначально мы планировали использовать для наших задач уже написанные клиенты, но пытаясь «прикрутить» их к нашему сервису, столкнулись со многими проблемами:
- Проблема чанков.
Не учитывается фрагментация данных tcp пакета. Этим страдают beanstalk_client, fivebeans, а также node-amqp, клиент для RabbitMQ, который мы изначально планировали использовать вместо Beanstalkd, и node-thrift для hbase. - Смещение ответов на запрос.
При определенных нагрузках происходило смещение ответов. На запрос 2 нам приходил ответ на запрос 1. То есть система возвращала неправильные ответы. - Не предназначены для высоких нагрузок.
При нагрузках в более миллиона запросов для Redis и более 100000 задач для Beanstalkd с payload около 10Кб многие клиенты просто не возвращали результат и вешали всю систему. - Отсутствие fallback
Для нашего сервиса было необходимо, чтобы все задачи и запросы поставленные в очередь на выполнение не терялись в процессе передачи, даже если некоторое время не было соединения с сетью.
Не найдя готового надёжного клиента для Redis и Beanstalkd, мы решили написать свой.
Любой клиент, обобщённо, это инструмент реализации запросов к сервису и получения от него результатов. Он должен устанавливать соединение и обеспечивать передачу данных между вашим приложением и используемым сервисом. С помощью диаграммы классов UML это можно представить следующим образом:
Модель очень проста и понятна. Именно по такой схеме и работает наш модуль Node-Polina. Пока что, в рамках данной модели мы реализовали клиент для Redis и Beanstalkd.
Node-Polina. Redis-Client.
О возможностях драйвера.
На данный момент в драйвере реализованы такие команды к базе данных Redis, как: set, get, mget, incrby, incr, decr, setex, expire, keys, del, sismember, sadd, srem, smembers
. Благодаря тому, что мы не привязывались к конкретной реализации команд, а разделили их по типам на возвращающие числа, строки или массивы, то добавить новую команду в драйвер очень просто. При необходимости вы можете написать нам на gitHub в issues с пометкой enhancement, и мы добавим нужный функционал. Кроме того, в Node-Polina есть поддержка использования connection pooling и shard.
Использование
Простой клиент
// для начала необходимо подключить модуль:
var polina = require('livetex-polina');
// создать клиент с помощью конструктора Client, передав в него порт и // опционально хост:
var client = new polina.redis.Client(6397, '127.0.0.1');
// теперь можно выполнить любую команду:
client.set('key', 'value', function() {
console.log('Complete.');
}, function(error, opt_code) {
console.error(error);
});
Connection pooling
// создать клиент с помощью конструктора Bundle, передав в него максимальное
// количество одновременных подключений, порт и опционально хост:
var client = new polina.redis.Bundle(100, 6397, '127.0.0.1');
Shard
// создать Bucket, его указав размер:
var bucket = new polina.redis.Bucket(9);
// зарегистрировать клиенты, вызвав команду registerClient, передав в // неё значения интервала ключей, клиент и идентификатор:
bucket.registerClient(0, 3, new polina.redis.Bundle(3, 6397), 'alpha');
bucket.registerClient(3, 6, new polina.redis.Bundle(3, 6398), 'beta');
bucket.registerClient(6, 9, new polina.redis.Bundle(3, 6399), 'gamma');
Node-Polina VS Redis VS nodejs-redis
Сравнение по времени
Из графика видно, что на высоких нагрузках Node-Polina гораздо лучше справляется со своей работой. Данные графики актуальны для node версии 0.10.15. Мы также тестировали драйверы на версии 0.8.24, в которой даже на больших нагрузках Node-Polina продолжает отдавать результаты на запросы, когда node_redis с этим уже не справляется.
При обработке данных в 10Кбайт на малых нагрузках отличие Node-Polina от других драйверов почти незаметно. При запросах около 300 000 — 450 000, обработка запросов с помощью Node-Polina требует больше времени, так как внутри драйвера реализуется механизм failover. При нагрузках более 230 000 запросов Node-Polina опять же быстрее.
Сравнение по памяти
По расходуемой памяти Node-Polina практически сопоставима с node_redis, но на больших нагрузках опять же требует больше для реализации failover.
При обработке данных в 10Кбайт Node-Polina, очевидно, лучше Hiredis. Также, видно, что Полина более стабильна, чем node_redis, её график возрастает равномерно, без значительных флуктуаций.
Написав свой драйвер мы решили проблемы с фрагментацией tcp-пакетов, смещением ответов на запросы и проблему падения с ошибкой при кратковременном разрыве соединения. Мы написали Node-Polina так, чтобы она выдерживала высокие нагрузки, была проста в использовании и имела возможность быстрого и простого добавления функционала. Кроме того, наш драйвер обеспечивает создание пула соединений и шардинг, что часто необходимо при работе с базой данных Redis.
Исходники можно найти в нашем git-репозитории: https://github.com/LiveTex/Node-Polina
Установить через npm можно с помощью команды: npm install livetex-polina
Автор: LiveTex