суббота, 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:

  1. /*
  2. * Redis get|set implementation
  3. */
  4.  
  5. package net.multipi.connector.redis;
  6.  
  7. import java.io.IOException;
  8. import java.util.Map;
  9. import java.util.Set;
  10. import redis.clients.jedis.Jedis;
  11.  
  12. public class RedisConnector {
  13.     private Jedis cli;
  14.     
  15.     /**
  16.      * constructor: connect to Redis server and authorization
  17.      * @param host
  18.      * @param port
  19.      * @param password
  20.      */
  21.     public RedisConnector(String host, int port, String password) {
  22.         cli = new Jedis(host, port, 5000);
  23.         cli.auth(password);
  24.         try {
  25.             cli.connect();
  26.         } catch (Exception ex) {
  27.             ex.printStackTrace(System.err);
  28.         }
  29.     }
  30.  
  31.     /**
  32.      * calculate keys count (eg count of active sessions)
  33.      * @return
  34.      */
  35.     public long getKeysCount() {
  36.         return cli.dbSize();
  37.     }
  38.  
  39.     /**
  40.      * get all keys that begin with "begin" string
  41.      * @param begin
  42.      * @return
  43.      */
  44.     public Set<String> getAllKeys(String begin) {
  45.         return cli.keys(begin+"*");
  46.     }
  47.  
  48.     /**
  49.      * set session values as map
  50.      * @param sess
  51.      * @param m
  52.      * @return
  53.      */
  54.     public boolean setAll(String sess, Map<String, String> m) {
  55.         String r = cli.hmset(sess, m);
  56.         return (r.equals("OK"));
  57.     }
  58.  
  59.     /**
  60.      * set session values as map and set expire the session
  61.      * @param sess
  62.      * @param m
  63.      * @param expire
  64.      * @return
  65.      */
  66.     public boolean setAll(String sess, Map<String, String> m, int expire) {
  67.         boolean rez = false;
  68.         String r = cli.hmset(sess, m);
  69.         long er = cli.expire(sess, expire);
  70.         rez = (r.equals("OK") && er>0);
  71.         return rez;
  72.     }
  73.  
  74.     /**
  75.      * set one of session values
  76.      * @param sess
  77.      * @param key
  78.      * @param value
  79.      * @return
  80.      */
  81.     public boolean set(String sess, String key, String value) {
  82.         boolean rez = false;
  83.         Map m = getAll(sess);
  84.         if (m!=null) {
  85.             m.put(key, value);
  86.             rez = setAll(sess, m);
  87.         }
  88.         return rez;
  89.     }
  90.     
  91.     /**
  92.      * set or update session values and set expire
  93.      * @param sess
  94.      * @param key
  95.      * @param value
  96.      * @param expire
  97.      * @return
  98.      */
  99.     public boolean set(String sess, String key, String value, int expire) {
  100.         boolean rez = false;
  101.         Map m = getAll(sess);
  102.         if (m!=null) {
  103.             m.put(key, value==null ? "" : value);
  104.             rez = setAll(sess, m, expire);
  105.         }
  106.         return rez;
  107.     }
  108.     /**
  109.      * check if session exists
  110.      * @param sess
  111.      * @return
  112.      */
  113.     public boolean isExists(String sess) {
  114.         return cli.exists(sess);
  115.     }
  116.     /**
  117.      * get session values
  118.      * @param sess
  119.      * @return
  120.      */
  121.     public Map<String, String> getAll(String sess) {
  122.         return cli.hgetAll(sess);
  123.     }
  124.  
  125.     /**
  126.      * det session values and prolongs session
  127.      * @param sess
  128.      * @param expire
  129.      * @return
  130.      */
  131.     public Map<String, String> getAll(String sess, int expire) {
  132.         Map m = cli.hgetAll(sess);
  133.         cli.hmset(sess, m);
  134.         cli.expire(sess, expire);
  135.         return m;
  136.     }
  137.  
  138.     /**
  139.      * get one of session values
  140.      * @param sess
  141.      * @param key
  142.      * @return
  143.      */
  144.     public String get(String sess, String key) {
  145.         return cli.hget(sess, key);
  146.     }
  147.  
  148.     /**
  149.      * delete session with all session data
  150.      * @param sess
  151.      * @return
  152.      */
  153.     public boolean del(String sess) {
  154.         Long del = cli.del(sess);
  155.         return del>0;
  156.     }
  157.  
  158.     /**
  159.      * close connection
  160.      */
  161.     public void close() {
  162.       if (cli.isConnected()) {
  163.             try {
  164.                 cli.disconnect();
  165.             } catch (IOException ie) {}
  166.         }
  167.     }
  168. }
Как видно из кода все методы библиотеки есть простые java-обёртки над командами Redis. А все наши методы - вызовы методов библиотеки. В конструктор нужно передать ip-адрес машины на которой запущен Redis-сервер, порт и пароль доступа к серверу. Третьим параметром передаём время ожидания ответа сервера в миллисекундах. Вообще, если есть такая возможность Redis лучше всего поднимать на той же машине, где работает наше приложение. Скорость обработки запросов у Redis на базе из миллиона записей составляет 1-2 миллисекунды. В сравнении с этим потери 20-50 ms на соединение (в лучшем случае) выглядят неприятно. Требования к памяти у Redis весьма скромные.
Использование Redis в качестве сессионного хранилища приятно ещё и тем, что в нём реализован механизм "устаревания" ключей. Достаточно установить срок жизни ключу и сервер сам "приберёт" его через указанное время. В текущей версии была неприятная особенность: после обновления значения ключ становился "вечным". Поэтому мы "напоминаем" ключам время жизни после апдейта, там где это нужно.
Ещё одна особенность едва не испортила впечатление от Jedis: при использовании внутреннего механизма пула соединений в этой библиотеке спустя 5-10 минут работы под нагрузкой система "засыпала", без видимой причины и без каких-либо ошибок прекращая реагировать на запросы. От пула пришлось отказаться, благо высокая скорость обработки запросов делала контроль за числом открытых коннектов задачей чисто "академической" важности.
В целом: инструмент весьма порадовал. После изнурительной борьбы за производительность запросов на "основной" БД скорость ответов от Redis создаёт прямо-таки фантастическое впечатление. А возможность работать с сервером "чем угодно", просто посылая команды на порт так и провоцирует написать свою реализацию клиента.

пятница, 24 декабря 2010 г.

связка Apache+Tomcat

На днях пришлось решать задачу по настройке связки Apache 2+Tomcat 6.02 на Debian 5 сервере. Пришлось в очередной раз порыться в инете в поисках инструкций. Дабы не повторять этот процесс в следующий раз решил выложить тут алгоритм, который получил в итоге.

Итак, дано: VDS с Debian 5, Apache 2 уже установлен, на нём настроены несколько виртуальных хостов. Требуется: в поддиректории одного из сайтов исполнять jsp - страницы средствами Tomcat.

1. Устанавливаем JDK. 

apt-get install python-software-properties 
add-apt-repository ppa:sun-java-community-team/sun-java6 
apt-get update 
apt-get install sun-java6-jdk

Желательно после установки в /etc/profile добавить строки:

JAVA_HOME="/usr/lib/jvm/java-6-sun/"
export JAVA_HOME

2. Устанавливаем Tomcat:

Берём с официального сайта ссылку на последнюю версию и wget-ом вытягиваем её к себе на сервер. Распаковываем и переносим "на место постоянного проживания":

tar -xzvf apache-tomcat-6.0.29.tar.gz
mv apache-tomcat-6.0.29 /opt/tomcat

3. Делаем init-скрипт для запуска/останова/перезапуска Tomcat-а:

в /etc/init.d/tomcat
пишем:

#!/bin/sh
# Tomcat Init-Script
case $1 in
start)
sh /opt/tomcat/bin/startup.sh
;;
stop)
sh /opt/tomcat/bin/shutdown.sh
;;
restart)
sh /opt/tomcat/bin/shutdown.sh
sh /opt/tomcat/bin/startup.sh
;;
esac
exit 0

Ставим на него права 0755 и делаем:
update-rc.d tomcat defaults

4. Настраиваем логин в Tomcat Manager:

редактируем /opt/tomcat/conf/tomcat-users.xml

<tomcat-users>
<role rolename="manager"/>
<role rolename="admin"/>
<user username="USERNAME" password="PASSWORD" roles="admin,manager"/>
</tomcat-users>


5. Проверяем работу "свежеустановленного" сервера:

/etc/init.d/tomcat start
по адресу http://ваш домен:8080/manager/html будет работать админка Tomcat-а.

6. Настраиваем модуль для связи Tomcat и Apache

Устанавливаем модуль для Apache:
aptitude install libapache2-mod-jk

взаимодействие между серверами реализуется с помощью worker-а. Описываем его конфигурацию в файле /etc/apache2/workers.properties:

workers.tomcat_home=/opt/tomcat
workers.java_home=/usr/lib/jvm/java-6-sun
ps=/
worker.list=default
worker.default.port=8009
worker.default.host=localhost
worker.default.type=ajp13
worker.default.lbfactor=1

Тут, как видим, устанавливается имя нашего worker-а: default
Описываем конфигурацию модуля в файле /etc/apache2/conf.d/jk.conf

JkWorkersFile /etc/apache2/workers.properties
JkLogFile /var/log/apache2/mod_jk.log
JkLogLevel error
Перезагружаем сервера:
/etc/init.d/apache2 stop
/etc/init.d/tomcat restart
/etc/init.d/apache2 start

7. Создаём виртуальный хост на Tomcat и привязываем его к выбранному виртуальному хосту на Apache

добавляем в /opt/tomcat/conf/server.xml
<host name="www.testsrv.local" appBase="/var/www/vhost1"
unpackWARs="true" autoDeploy="true">
<context path="" docBase="htdocs" debug="0" reloadable="true"/>
<valve className="org.apache.catalina.valves.AccessLogValve"
directory="/var/www/vhost1/logs" prefix="tomcat_access_" suffix=".log"
pattern="common" resolveHosts="false"/>
</host>

Пути к директориям указываем соответственно те, где будут лежать наши .jsp

В секцию выбранного витуального хоста Apache в /etc/apache2/apache2.conf добавляем строку:

JkMount /j/*.jsp default

где /j/ - путь к директории, где будут лежать .jsp - страницы относительно корня сайта в Apache.

8. Проверяем:

Создаём файл test.jsp в выбранной директории:

<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
Today is: <%= new java.util.Date().toString() %>
</body>
</html>

и заходим по адресу http://ваш домен/j/test.jsp
наблюдаем работающую .jsp страницу, которую выполнил Tomcat по "поручению" Apache :)