Ниже я предложу два решения: первое я сочинил сам, второе "подсмотрел" в исходниках jedis - популярного "драйвера" для подключения java-приложений к noSQL хранилищу Redis.
Решения конкретных задач программирования. Java, Android, JavaScript, Flex и прочее... Настройка софта под Linux, методики разработки и просто размышления.
Показаны сообщения с ярлыком Redis. Показать все сообщения
Показаны сообщения с ярлыком Redis. Показать все сообщения
четверг, 16 января 2014 г.
Делаем алгоритм шардинга. Два решения: простое и красивое
Надеюсь все знают что такое шардинг. Ну а если вы всего лишь "слышали об этом где-то", то вы по-своему счастливый человек. Шардинг данных это решение "последнего выхода", когда никакие другие оптимизации системы хранения данных вроде индексации, денормализации и кластеризации уже не помогают. При шардинге вы берёте свою таблицу (или коллекцию для noSQL) размером в десять миллионов записей и разрезаете её, к примеру, на 10 таблиц по миллиону. Эти таблицы могут лежать в разных базах (нодах) на разных серверах. Так мы получаем то, за что разработчики высоконагруженных проектов так любят шардинг: бесконечное горизонтальное масштабирование. Чтобы определить для каждой записи ноду в которую её нужно положить или где её следует потом искать мы должны реализовать алгоритм определения ноды исходя из ключа и общего числа нод. Для записей определённой структуры с заранее известным "ключевым" полем это сделать несложно. Если вы режете таблицу пользователей с инкрементным id в качестве ключа, то можно просто определить диапазоны id для каждой ноды. То же самое при разбиении набора записей с датой в качестве ключа. Но возьмём тяжёлый случай: ключ имеет произвольную структуру. Это не последовательное число и не дата. Просто строка. Как гарантированно отнести такой ключ к определённой ноде?
понедельник, 14 ноября 2011 г.
Nginx+Redis: делаем асихронное web-приложение для больших нагрузок
Как работает обычное web-приложение?
Примерно так:
Проходит время, нагрузка растёт и узким местом становится база данных. Разработчики, стараясь снять с неё нагрузку, переходят к асинхронной схеме. Тут база данных не используется в каждом запросе. В основном используется только быстрое noSQL-хранилище или специализированный сервер очередей, для передачи заданий пулу обработчиков. Основную работу эти обработчики выполнят уже позже, когда довольный клиент получит быстрый ответ и пойдёт заниматься своими делами:
Но нагрузка может расти и дальше. Оставим в стороне "горизонтальное" масштабирование при котором мы строим кластера и плодим инстансы приложений - речь сейчас не об этом. Что в последней схеме становится узким местом? База данных уже не в счёт: спрятанная за слоем очередей и обработчиков, кэшей и буферов, она может чувствовать себя спокойно. Обработчики великолепно масштабируются, ведь они "висят" на безразмерной шине очереди. Nginx - один из самых высопроизводительных серверов, за него не беспокоимся. noSQL-хранилища тоже как правило замечтально держат нагрузку и масштабируются.
Что остаётся? К сожалению "крайним" остаётся наше web-приложение. Это оно разбирает запрос, авторизует его, создаёт обьекты, манипулирует с ними, сериализует в базу или в очередь. А потом ещё вычитать данные, сериализовать для ответа... Приложение может содержать неэффективный код, плохо масштабироваться... Кстати, а зачем нам оно вообще нужно? Давайте уберём его из схемы:
Как же так? Очень просто. Задача получить данные из запроса и уложить в очередь вообще-то тривиальная. И обратная задача тоже. Зачем программировать тривиальные вещи? Всё уж сделано за нас :)
Nginx при помощи HttpRedis2Module может уложить в noSQL-хранилище Redis любые параметры из запроса в виде такого набора ключ-значение, который нам нужен. И вычитать нужный нам набор ключей для возврата клиенту. Вы не используете параметры запросов? У вас обмен с клиентской частью в формате JSON? Нет проблем! Используя set-misc-nginx-module мы можем прямо в конфиге nginx-а описать правила получения данных из запроса: UrlDecode, JSONDecode, Base64Decode и т.п.
Теперь посмотрим, как настроить такое "сверхтонкое" web-приложение:
Примерно так:
Проходит время, нагрузка растёт и узким местом становится база данных. Разработчики, стараясь снять с неё нагрузку, переходят к асинхронной схеме. Тут база данных не используется в каждом запросе. В основном используется только быстрое noSQL-хранилище или специализированный сервер очередей, для передачи заданий пулу обработчиков. Основную работу эти обработчики выполнят уже позже, когда довольный клиент получит быстрый ответ и пойдёт заниматься своими делами:
Но нагрузка может расти и дальше. Оставим в стороне "горизонтальное" масштабирование при котором мы строим кластера и плодим инстансы приложений - речь сейчас не об этом. Что в последней схеме становится узким местом? База данных уже не в счёт: спрятанная за слоем очередей и обработчиков, кэшей и буферов, она может чувствовать себя спокойно. Обработчики великолепно масштабируются, ведь они "висят" на безразмерной шине очереди. Nginx - один из самых высопроизводительных серверов, за него не беспокоимся. noSQL-хранилища тоже как правило замечтально держат нагрузку и масштабируются.
Что остаётся? К сожалению "крайним" остаётся наше web-приложение. Это оно разбирает запрос, авторизует его, создаёт обьекты, манипулирует с ними, сериализует в базу или в очередь. А потом ещё вычитать данные, сериализовать для ответа... Приложение может содержать неэффективный код, плохо масштабироваться... Кстати, а зачем нам оно вообще нужно? Давайте уберём его из схемы:
Как же так? Очень просто. Задача получить данные из запроса и уложить в очередь вообще-то тривиальная. И обратная задача тоже. Зачем программировать тривиальные вещи? Всё уж сделано за нас :)
Nginx при помощи HttpRedis2Module может уложить в noSQL-хранилище Redis любые параметры из запроса в виде такого набора ключ-значение, который нам нужен. И вычитать нужный нам набор ключей для возврата клиенту. Вы не используете параметры запросов? У вас обмен с клиентской частью в формате JSON? Нет проблем! Используя set-misc-nginx-module мы можем прямо в конфиге nginx-а описать правила получения данных из запроса: UrlDecode, JSONDecode, Base64Decode и т.п.
Теперь посмотрим, как настроить такое "сверхтонкое" web-приложение:
воскресенье, 22 мая 2011 г.
Redis: пишем Java-клиент
В одном из прежних постов я описывал свои впечатления от знакомства с Redis. Тогда я использовал библиотеку Jedis. А недавно мне попалось описание протокола Redis-сервера и я решил поупражняться в его освоении. Чтобы было нагляднее я "завернул" результат в простое swing-приложение, которое можно скачать на моём сайте. Итак...
Коротко о протоколе
Для соединения с сервером используем tcp-протокол, порт по умолчанию - 6379. Команды и наборы данных в ответах завершаются последовательностью "\r\n" (CRLF). Первый символ ответа указывает на тип возвращаемого значения. Тут ":" - число, "-" - сообщение об ошибке, "+" - однострочный ответ. Интереснее ответы, начинающиеся с "$". Это, так называемые "составные" ответы. Они содержат две строки, первая из которых - число байт, которые нужно прочитать во второй. Такие ответы будем получать наиболее часто, их возвращает команда GET для обычных ключей. Если же ключ необычный, например хэш, то ответ будет "многосоставной". В нём первый символ - "*", затем число результатов в ответе, затем результаты, каждый по две строке в виде составных ответов, начинающихся с "$".
Архитектура
В последней (на сегодня) версии Redis я насчитал более 120 команд. Из них в обычной практике понадобится не более десятка, но хорошая библиотека должна легко расширяться.
Схема работы будет такой: каждая команда будет представлена одним классом, в нём - специфическая логика по формированию запроса и интерпретации ответа. Сам метод, который пишет/читает данные будет реализован в их общем предке. Все классы будут реализовывать один и тот же абстрактный метод. Приложение о классах команд ничего знать не должно, для него у нас будет Factory, которое будет принимать запрос, подгружать в рантайме нужный класс, создавать его инстанс, инициализировать его (например отдавать ему уже открытый сокет) и дёргать его стандартный метод. В коде это выглядит так:
И, наконец, пример класса команды:
Результат:
За пару часов работы получился прототип, который ищет ключи по маске, позволяет просматривать их содержимое и удалять их.
По мере добавления функционала буду выкладывать новые версии у себя на сайте. Если кому пригодится - всегда на здоровье :).
Коротко о протоколе
Для соединения с сервером используем tcp-протокол, порт по умолчанию - 6379. Команды и наборы данных в ответах завершаются последовательностью "\r\n" (CRLF). Первый символ ответа указывает на тип возвращаемого значения. Тут ":" - число, "-" - сообщение об ошибке, "+" - однострочный ответ. Интереснее ответы, начинающиеся с "$". Это, так называемые "составные" ответы. Они содержат две строки, первая из которых - число байт, которые нужно прочитать во второй. Такие ответы будем получать наиболее часто, их возвращает команда GET для обычных ключей. Если же ключ необычный, например хэш, то ответ будет "многосоставной". В нём первый символ - "*", затем число результатов в ответе, затем результаты, каждый по две строке в виде составных ответов, начинающихся с "$".
Архитектура
В последней (на сегодня) версии Redis я насчитал более 120 команд. Из них в обычной практике понадобится не более десятка, но хорошая библиотека должна легко расширяться.
Схема работы будет такой: каждая команда будет представлена одним классом, в нём - специфическая логика по формированию запроса и интерпретации ответа. Сам метод, который пишет/читает данные будет реализован в их общем предке. Все классы будут реализовывать один и тот же абстрактный метод. Приложение о классах команд ничего знать не должно, для него у нас будет Factory, которое будет принимать запрос, подгружать в рантайме нужный класс, создавать его инстанс, инициализировать его (например отдавать ему уже открытый сокет) и дёргать его стандартный метод. В коде это выглядит так:
Так, во имя стандартизации мы параметры в запрос передаём как массив строк, а ответ получаем в виде такого себе объекта-оборотня Response:
- public class RedisFactory {
- private Socket s;
- public static final String COMMAND_AUTH = "redisui.connector.command.Auth";
- public static final String COMMAND_KEYS = "redisui.connector.command.Keys";
- public static final String COMMAND_GET = "redisui.connector.command.Get";
- public static final String COMMAND_TYPE = "redisui.connector.command.Type";
- public static final String COMMAND_HGETALL = "redisui.connector.command.Hgetall";
- public static final String COMMAND_DEL = "redisui.connector.command.Del";
- public void connect(String host, String port, String pass) throws Exception {
- String[] params = {host, port, pass};
- exec(COMMAND_AUTH, params);
- }
- public Response exec(String cmd, String[] params) throws Exception {
- Class<? extends CommandBased> command = Class.forName(cmd).asSubclass(CommandBased.class);
- CommandBased ci = command.newInstance();
- ci.setS(this.s);
- Response r = ci.exec(params);
- this.s = ci.getS();
- return r;
- }
- }
На стороне приложения мы либо знаем, какого типа будет ответ и в таком случае вызываем нужный getter, либо обрабатываем все варианты, получая тип конкретного ответа из getType().
- public class Response {
- private String type;
- private byte[] out;
- public static final String TYPE_LINE = "+";
- public static final String TYPE_ERROR = "-";
- public static final String TYPE_INT = ":";
- public static final String TYPE_STRING = "$";
- public static final String TYPE_ARRAY = "*";
- public Response(String type, byte[] out) {
- this.type = type;
- this.out = out;
- }
- public String getLineOut() {
- ...
- }
- public int getIntOut() {
- ...
- }
- public String getStringOut() {
- ...
- }
- public String[] getArrayOut() {
- ...
- }
- public String getType() {
- return type;
- }
- }
И, наконец, пример класса команды:
Как видим, добавлять новые команды в такую библиотеку - одно удовольствие.
- public class Get extends CommandBased {
- @Override
- public Response exec(String[] params) throws Exception {
- String command = "get "+params[0]+"\r\n";
- Response res = interrupt(command);
- if (res.getType().equals(Response.TYPE_ERROR)) {
- throw new Exception(res.getLineOut());
- }
- return res;
- }
- }
Результат:
За пару часов работы получился прототип, который ищет ключи по маске, позволяет просматривать их содержимое и удалять их.
По мере добавления функционала буду выкладывать новые версии у себя на сайте. Если кому пригодится - всегда на здоровье :).
суббота, 25 декабря 2010 г.
Использование Redis в Java WEB applcation
В жизни каждого достаточно большого web-проекта наступает момент, когда требования к производительности заставляют поставить между кодом и данными в базе что-то достаточно быстрое и нетребовательное... Например Memcached или Redis. Первый инструмент достаточно известен, и обладает серьёзным недостатком: всё что не в памяти - удаляется. Второй - практически полноценная noSQL БД. И благодаря этой своей природе может быть крайне полезна, в первую очередь как дополнение к традиционной SQL СУБД. Действительно, так ли редко мы складываем в таблицы что-то совсем не "табличное"? Например, недавно мы вынесли в базу хранилище сессий пользователей (очень удобно для горизонтального масштабирования web-фронтэндов). Возникли проблемы с нагрузкой, потребовалась "тонкая" настройка и т.п. А ведь всё дело в том, что хранение таких данных в реляционной базе вообще-то "притянуто за уши". Что мы пишем в сесию? Правильно, пары ключ-значение. При чём тут SQL?. Короче, Redis с этой задачей справился идеально, что позволило нашему SQL-серверу снова "расправить крылья".
Вспомню, как я с этим разбирался...
После недолгого изучения разных источников нашёл вот эту wiki. Это перевод официальной документации. Пользовался почти исключительно этим сайтом. Но прежде чем начать изучение, установим себе эту систему.
На официальном сайте скачиваем версию (на данный момент последняя стабильная - 2.0). Это архив с исходниками, собираем сами: после распаковки в каталоге делаем make. После сборки запускаем сервер с помощью:
$ ./redis-server
По умолчанию он поднимается на порту 6379 и дальше с ним можно взаимодействовать хотя бы даже и обычным telnet-ом. Но мы не будем так суровы :)
Для начала сделаем минимальные правки в конфиге: redis.conf в каталоге приложения - поставим daemonize yes, это позволит нам запускать сервер не отдавая ему консоль. Более тонкую настройку оставим на потом.
Запускать сервер теперь будем с явным указанием конфига:
$ ./redis-server redis.conf
Для освоения минимального набора команд воспользуемся консольным клиентом, который есть в сборке:
$ ./redis-cli <команда> <параметры ...>
Например, чтобы сохранить пару ключ-значение в Redis:
$ ./redis-cli set mykey myval
$ OK
... а чтобы получить значение по ключу:
$ ./redis-cli get mykey
$ "myval"
Redis поддерживает несколько типов данных для значений, которые мы "вкладываем" в ключи. Это строки, списки, множества и хэши. Детальнее посмотрите в руководстве, на пока понадобятся лишь имена команд, для каждого типа данных "геттеры" и "сеттеры" свои. Например для хэшей это:
hget, hset, hmset, hgetall... пока достаточно... (установить значение поля хэша, получить его, установить все поля-значения и получить все). Детально разбираться не будем, всё равно работать нам не в консоли. Достаточно просто понимать, какие есть инструменты.
Теперь переходим к Java.
Для работы с Redis есть несколько библиотек. Самая "живая" из них - Jedis, поддерживает версию 2.0, что нам и требуется.
Скачиваем её отсюда. Подключаем jedis-1.5.2.jar к проекту. Пишем класс для взаимодействия с redis:
Использование Redis в качестве сессионного хранилища приятно ещё и тем, что в нём реализован механизм "устаревания" ключей. Достаточно установить срок жизни ключу и сервер сам "приберёт" его через указанное время. В текущей версии была неприятная особенность: после обновления значения ключ становился "вечным". Поэтому мы "напоминаем" ключам время жизни после апдейта, там где это нужно.
Ещё одна особенность едва не испортила впечатление от Jedis: при использовании внутреннего механизма пула соединений в этой библиотеке спустя 5-10 минут работы под нагрузкой система "засыпала", без видимой причины и без каких-либо ошибок прекращая реагировать на запросы. От пула пришлось отказаться, благо высокая скорость обработки запросов делала контроль за числом открытых коннектов задачей чисто "академической" важности.
В целом: инструмент весьма порадовал. После изнурительной борьбы за производительность запросов на "основной" БД скорость ответов от Redis создаёт прямо-таки фантастическое впечатление. А возможность работать с сервером "чем угодно", просто посылая команды на порт так и провоцирует написать свою реализацию клиента.
Вспомню, как я с этим разбирался...
После недолгого изучения разных источников нашёл вот эту wiki. Это перевод официальной документации. Пользовался почти исключительно этим сайтом. Но прежде чем начать изучение, установим себе эту систему.
На официальном сайте скачиваем версию (на данный момент последняя стабильная - 2.0). Это архив с исходниками, собираем сами: после распаковки в каталоге делаем make. После сборки запускаем сервер с помощью:
$ ./redis-server
По умолчанию он поднимается на порту 6379 и дальше с ним можно взаимодействовать хотя бы даже и обычным telnet-ом. Но мы не будем так суровы :)
Для начала сделаем минимальные правки в конфиге: redis.conf в каталоге приложения - поставим daemonize yes, это позволит нам запускать сервер не отдавая ему консоль. Более тонкую настройку оставим на потом.
Запускать сервер теперь будем с явным указанием конфига:
$ ./redis-server redis.conf
Для освоения минимального набора команд воспользуемся консольным клиентом, который есть в сборке:
$ ./redis-cli <команда> <параметры ...>
Например, чтобы сохранить пару ключ-значение в Redis:
$ ./redis-cli set mykey myval
$ OK
... а чтобы получить значение по ключу:
$ ./redis-cli get mykey
$ "myval"
Redis поддерживает несколько типов данных для значений, которые мы "вкладываем" в ключи. Это строки, списки, множества и хэши. Детальнее посмотрите в руководстве, на пока понадобятся лишь имена команд, для каждого типа данных "геттеры" и "сеттеры" свои. Например для хэшей это:
hget, hset, hmset, hgetall... пока достаточно... (установить значение поля хэша, получить его, установить все поля-значения и получить все). Детально разбираться не будем, всё равно работать нам не в консоли. Достаточно просто понимать, какие есть инструменты.
Теперь переходим к Java.
Для работы с Redis есть несколько библиотек. Самая "живая" из них - Jedis, поддерживает версию 2.0, что нам и требуется.
Скачиваем её отсюда. Подключаем jedis-1.5.2.jar к проекту. Пишем класс для взаимодействия с redis:
Как видно из кода все методы библиотеки есть простые java-обёртки над командами Redis. А все наши методы - вызовы методов библиотеки. В конструктор нужно передать ip-адрес машины на которой запущен Redis-сервер, порт и пароль доступа к серверу. Третьим параметром передаём время ожидания ответа сервера в миллисекундах. Вообще, если есть такая возможность Redis лучше всего поднимать на той же машине, где работает наше приложение. Скорость обработки запросов у Redis на базе из миллиона записей составляет 1-2 миллисекунды. В сравнении с этим потери 20-50 ms на соединение (в лучшем случае) выглядят неприятно. Требования к памяти у Redis весьма скромные.
- /*
- * Redis get|set implementation
- */
- package net.multipi.connector.redis;
- import java.io.IOException;
- import java.util.Map;
- import java.util.Set;
- import redis.clients.jedis.Jedis;
- public class RedisConnector {
- private Jedis cli;
- /**
- * constructor: connect to Redis server and authorization
- * @param host
- * @param port
- * @param password
- */
- public RedisConnector(String host, int port, String password) {
- cli = new Jedis(host, port, 5000);
- cli.auth(password);
- try {
- cli.connect();
- } catch (Exception ex) {
- ex.printStackTrace(System.err);
- }
- }
- /**
- * calculate keys count (eg count of active sessions)
- * @return
- */
- public long getKeysCount() {
- return cli.dbSize();
- }
- /**
- * get all keys that begin with "begin" string
- * @param begin
- * @return
- */
- public Set<String> getAllKeys(String begin) {
- return cli.keys(begin+"*");
- }
- /**
- * set session values as map
- * @param sess
- * @param m
- * @return
- */
- public boolean setAll(String sess, Map<String, String> m) {
- String r = cli.hmset(sess, m);
- return (r.equals("OK"));
- }
- /**
- * set session values as map and set expire the session
- * @param sess
- * @param m
- * @param expire
- * @return
- */
- public boolean setAll(String sess, Map<String, String> m, int expire) {
- boolean rez = false;
- String r = cli.hmset(sess, m);
- long er = cli.expire(sess, expire);
- rez = (r.equals("OK") && er>0);
- return rez;
- }
- /**
- * set one of session values
- * @param sess
- * @param key
- * @param value
- * @return
- */
- public boolean set(String sess, String key, String value) {
- boolean rez = false;
- Map m = getAll(sess);
- if (m!=null) {
- m.put(key, value);
- rez = setAll(sess, m);
- }
- return rez;
- }
- /**
- * set or update session values and set expire
- * @param sess
- * @param key
- * @param value
- * @param expire
- * @return
- */
- public boolean set(String sess, String key, String value, int expire) {
- boolean rez = false;
- Map m = getAll(sess);
- if (m!=null) {
- m.put(key, value==null ? "" : value);
- rez = setAll(sess, m, expire);
- }
- return rez;
- }
- /**
- * check if session exists
- * @param sess
- * @return
- */
- public boolean isExists(String sess) {
- return cli.exists(sess);
- }
- /**
- * get session values
- * @param sess
- * @return
- */
- public Map<String, String> getAll(String sess) {
- return cli.hgetAll(sess);
- }
- /**
- * det session values and prolongs session
- * @param sess
- * @param expire
- * @return
- */
- public Map<String, String> getAll(String sess, int expire) {
- Map m = cli.hgetAll(sess);
- cli.hmset(sess, m);
- cli.expire(sess, expire);
- return m;
- }
- /**
- * get one of session values
- * @param sess
- * @param key
- * @return
- */
- public String get(String sess, String key) {
- return cli.hget(sess, key);
- }
- /**
- * delete session with all session data
- * @param sess
- * @return
- */
- public boolean del(String sess) {
- Long del = cli.del(sess);
- return del>0;
- }
- /**
- * close connection
- */
- public void close() {
- if (cli.isConnected()) {
- try {
- cli.disconnect();
- } catch (IOException ie) {}
- }
- }
- }
Использование Redis в качестве сессионного хранилища приятно ещё и тем, что в нём реализован механизм "устаревания" ключей. Достаточно установить срок жизни ключу и сервер сам "приберёт" его через указанное время. В текущей версии была неприятная особенность: после обновления значения ключ становился "вечным". Поэтому мы "напоминаем" ключам время жизни после апдейта, там где это нужно.
Ещё одна особенность едва не испортила впечатление от Jedis: при использовании внутреннего механизма пула соединений в этой библиотеке спустя 5-10 минут работы под нагрузкой система "засыпала", без видимой причины и без каких-либо ошибок прекращая реагировать на запросы. От пула пришлось отказаться, благо высокая скорость обработки запросов делала контроль за числом открытых коннектов задачей чисто "академической" важности.
В целом: инструмент весьма порадовал. После изнурительной борьбы за производительность запросов на "основной" БД скорость ответов от Redis создаёт прямо-таки фантастическое впечатление. А возможность работать с сервером "чем угодно", просто посылая команды на порт так и провоцирует написать свою реализацию клиента.
Подписаться на:
Сообщения (Atom)