Ehcache в качестве RESTful сервера кеширования и PHP

7 августа 2009

logoПриветствую наших читателей. Сегодня мы снова продолжим исследования различных новых и не очень технологий, необычного их применения или просто оригинальных вещей. Те, кто уже давно читают наш блог, возможно вспомнят о короткой статье, в которой я рассказывал о интересном проекте распределённого кеша EHcache для платформы Java. Сегодня настало время продолжить эту тему, однако в другом ракурсе.

Сперва еще раз упомянем о EHcache. Это высокопроизводительная и масштабируемая система кеширования для Java, которая является зрелым и серьезным проектом. Доступны варианты кеширования как в оперативной памяти, так и дискового кеша, а также комбинированные стратегии (также есть и вариант обеспечения сохранности данных при перезагрузках виртуальной машины или сервера). Масштабируемость реализована с помощью асинхронных репликаций и кластеризации кешей при помощи JGroup, JMS, RMI, также можно строить распределенные системы на базе сторонних продуктов (Terracota). Именно распределённость мне нравиться больше всего - индивидуальные настройки для каждого кеша (синхронная/асинхронная репликация) в паре с возможность запускать несколько различных экземпляров внутри одной JVM (или разных). Хотя следует заметить, что EHCache хранит данные в памяти JVM-процесса, соответственно, на его объем наложены некоторые ограничения (на 32-битных системах), однако дискового кеша никто не отменял, да и серьезные сервера все уже 64-битные. Известны инсталляции с 20 и боле Гб данных. Кроме этого, прекрасно поддерживается утилизация многопоточных возможностей, конкурентного доступа и многоядерности (хотя здесь можно поспорить немного, в JBoss Cache с этим вроде как ещё лучше, так как поддерживаются транзакции и кое-какие другие "вкусности", однако он сложнее в освоении, а его API достаточно сложен для понимания, у меня за два подхода никак не вышло с ним разобраться, в то время как EHcache запустился сразу).

Кеш очень быстрый и по доступных в сети тестах обгоняет другие системы кеширования (однако в кластерах из 4-х машин JBoss Cache показывает немного лучший результат, но это система немного другого уровня), в том числе и популярнейший Memcached. Знаю, сравнение немного неверное, так как EHcache является in-process кешером, в то время как memcached отдельный демон и работает как внешний сетевой сервис (поэтому в нем нет таких ограничений на размер кеша). В то же время, если бы сравнивать, что ehcache все же более предпочтителен в силу гораздо большей гибкости и масштабируемости, комбинации памяти/диска и тонких настроек кешей. Вот здесь меня и посетила мысль... а можно ли использовать EHcache вместо memcached-а (или вместе с), при этом оставаясь на привычной мне платформе РНР? Да, можно!

Все дело в том, что комьюнити разработчиков, кроме самого кеша, реализовало еще и кеширующий REST-сервер, доступ к которому можно получить через REST-интерфейс или SOAP. Сделано такое решение на базе embedded-версии сервера Sun GlassFish v3 Prelude и является самодостаточным, включая в себя все необходимые компоненты и зависимости. Работа с сервером происходит по протоколу HTTP, используя методы GET/POST/PUT/DELETE/OPTIONS/HEAD, либо через SOAP (также поверх HTTP) через XML. Поддерживаются все возможности HTTP/1.1, в том числе, keep-alive, а также Last-Modified, ETag, то есть, сервер отдает все корректные заголовки, поэтому часто можно использовать встроенное кеширование на промежуточных узлах при передаче или в самом клиенте. Интересным моментом является возможность работы с несколькими форматами данных, а если быть точным, возможность получить ответ в формате XML или JSON, для чего достаточно задать корректный заголовок MIME-type в запросе.

И так, у нас есть возможность буквально в один клик запустить доступный по простому и понятному протоколу сервер кеширования, а также обращаться к нему с любого языка или платформы, которая поддерживает HTTP-запросы. Давайте попробуем!

Загрузить последнюю версию сервера можно на SourceForge, однако я рекомендую параллельно загрузить и дистрибутив последней версии кеша, а затем обновить файлы сервера, так как в нем используется предыдущая версия кеша. Нас интересует ehcache-standalone-server, который на текущий момент имеет версию 0.7.

В дистрибутиве уже есть скрипты для запуска, читайте README, или для запуска перейдите в каталог lib и выполните запуск вручную:

  1. java -jar ./ehcache-standalone-server-0.7.jar 8080 ../wa

После указания основного файла сервера стоит номер порта, по которому он будет доступен, а также путь к каталогу с веб-приложением (war-файлом). В консоли после запуска вы увидите ход подключения, а также информацию о запущенных сервисах и портах - например так я узнаю, на каком порту доступен JMX-сервис для управления. Так как используется встраиваемая версия сервера GlassFish (это и веб-сервер и сервер приложений), то его настроек и возможностей достаточно мало, но вы всегда можете развернуть полноценный сервер, не обязательно даже GlassFish, а потом использовать только сам EHcache-сервер, который доступен и отдельно, без веб-сервера.

По-умолчанию, кеши доступны по адресу /ehcache/rest - зайдя браузером или выполнив GET-запрос, мы получим XML-документ с описанием всех текущих настроек кеша. Изначально в конфигурационном файле есть описания нескольких кешей, для примера, в том числе парочка распределенных. Для начала работы лучше всего удалить все базовые настройки и создать свои кеши. Простой кеш, без репликаций, мы сейчас сделаем.

Все настройки кеша сконцентрированы в одном xml-файле - /war/WEB-INF/classes/ehcache.xml, который мы и будем редактировать. Внутри есть достаточно много комментариев и описаний всех опций, поэтому я только опишу кратко как сделать базовый кеш, чтобы продолжить эксперименты.

Что означают эти опции:

  • name - имя кеша, которое будет использоваться для доступа (будет в URL, поэтому на содержание накладываются те же ограничения, лучше всего - краткое и четко обозначающее тип хранимых данных). В рамках одного сервера может быть множество кешей с разными именами.
  • maxElementsInMemory - максимальное количество элементов, которые размещаются в памяти
  • maxElementsOnDisk - максимальное количество элементов на диске (0 - без ограничений)
  • eternal - указывает, что можно игнорировать установки жизни кеша, тогда элементы будут всегда в кеше, пока вы их не удалите вручную
  • overflowToDisk - указывает, могут ли элементы быть вытеснены на диск, если достигнуто максимальное количество объектов в памяти
  • timeToIdleSeconds - время от последнего доступа к объекту до момента признания его невалидным.(если он не помечен как eternal). Опциональный параметр
  • timeToLiveSeconds - время жизни элемента (с версии 1.6, если не ошибаюсь, эта опция может быть задана для каждого отдельного объекта кеша
  • diskPersistent - указывает на сохранение состояния кеша на диске между рестартами
  • diskExpiryThreadIntervalSeconds - периодичность запуска процесса проверок объектов на диске на истечение TTL (времени жизни).
  • diskSpoolBufferSizeMB - объем пула, который выделен кешу для буферизации записи на диск. Когда пул заполнен, асинхронно вызывается команда записи на диск состояния кеша
  • memoryStoreEvictionPolicy - обозначает стратегию определения, какие объекты кеша должны быть вытеснены на диск. Может быть LRU (Least Recently Used), по дате последнего использования, FIFO (First In First Out, первый добавлен, первый вытеснен) и LFU (Less Frequently Used, по частоте использования)

Мы пока не обсуждаем варианты репликации - это уже углубленная специфика, о которой пусть лучше расскажут более компетентные специалисты.

  1.  
  2. cache name="testRestCache"
  3. maxElementsInMemory="10000"
  4. eternal="true"
  5. timeToIdleSeconds="0"
  6. timeToLiveSeconds="0"
  7. overflowToDisk="true"
  8. diskSpoolBufferSizeMB="4"
  9. maxElementsOnDisk="1000000000"
  10. diskPersistent="true"
  11. diskExpiryThreadIntervalSeconds="3600"
  12. memoryStoreEvictionPolicy="LFU"
  13.  

И так, наш кеш сконфигурирован так, чтобы держать в памяти 10 тыс. элементов, на диске 1 млрд, не использовать настройки времени жизни, обеспечить постоянство данных между перезагрузками. Объем пула для дисковой записи я выбрал достаточно небольшим, а время проверок на срок жизни дисковых элементов - очень большим (но думаю надо больше, в идеале - посмотрим, можно ли отключить вообще). Для чего именно такая конфигурация? Мне интересно попробовать на базе этого кеша сделать простую key-value базу данных (сегодня это очень популярная тема), при этом обеспечив себе возможность как прямого обращения к кешу из внешних сервисов, так и изнутри РНР веб-приложения. Один нюанс - даже если вам не нужна проверка на время жизни и необходим постоянный кеш, не устанавливайте параметры timeToIdle/timeToLive в 0, иначе сервер может не запускаться (вернее - сервис кеша, сам сервер стартует по выдает исправно 404 ошибку).

Для проверки сохраните отредактированный файл ehcache.xml и перезапустите сервер. Теперь откроем в браузере URL: http://localhost:8080/ehcache/rest/testRestCache - вы должны получить XML-документ со всеми настройками кеша, а также текущую статистику использования кеша (объем, количество данных, процент попадания и промахов) - в дальнейшем это можно разбирать программно, чтобы выводить в нужном виде (например, в админке).

В дальнейшем я буду рассматривать только REST-часть, для работы через SOAP вам надо поменять в URL rest на soap, получить описание сервисов в WSDL-формате и т.п. Для производительности я у себя просто отключил все неиспользуемое, в том числе ненужные мне кеши и доступ по SOAP-протоколу. Настройки сервлетов доступны в файле web.xml в директории /war/WEB-INF.

Работа с кешем заключается в отправке запросов по HTTP-протоколу и разбору ответа. В случае ошибки, ответ будет в формате text/plain, а в теле запроса будет текст ошибки, HTTP-код будет 404 - например, вы обратились к несуществующему кешу или элементу, тогда ответом будет строка "Element not found: 333" (если вы запросили элемент с ключем 333). Но это справедливо для тех URL, которые обслуживаются сервлетом EHcache, если же ошибка будет в другой части, вы получите стандартную 404 страницу ошибки от GlassFish, которая менее приспособлена к автоматическому разбору.

Работать можно как с сервером вообще (с менеджером кешей), так и индивидуально с каждым кешем и элементом, для этого просто дополните строку URL и используйте нужный метод с параметрами.

Для всего кеш (CacheManager-а):

  • GET - возвращает в XML-формате список доступных кешей на сервере и их параметры. Это обычный запрос через браузер, как мы делали выше, например: http://localhost:8080/ehcache/rest/

cache_manager_options

Дальше по иерархии, если указать в URL конкретное имя кеша, над ним можно производить следующие операции:

  • OPTIONS - аналогично вышеописанному, возвращает WADL-описание доступных операций
  • HEAD - возвращает те же мета-данные с описанием параметров кеша, но в виде HTTP-заголовков, а не в теле ответа (как в GET)
  • GET - XML документ с параметрами кеша и его статистикой.
  • PUT - позволяет создать новый кеш (имя которого передано в строке URL) на основе настроек дефолтного кеша (задается в конфигурационном файле).
  • DELETE - удаляет указанный в URL кеш. Именно удаляет, а не очищает (для этого есть другая команда, как ни странно, среди операций над элементами кеша), похоже, до следующей перезагрузки сервера (но я не проверял еще этот момент).

На уровне элементов кеша поддерживаются следующие операции:

  • OPTIONS - аналогично вышеописанному, возвращает WADL-описание доступных операций
  • HEAD - возвращает содержимое элемента в виде строки в HTTP-заголовке (здесь есть неоднозначность в справке, так как для остальных случаев HEAD дублирует GET, для элементов же кеша указано что он именно метаданные возвращает, а не значение).
  • GET - возвращает непосредственно содержимое элемента кеша в теле ответа.
  • PUT - ложит данные в кеш. Сами данные передаются в теле запроса, имя объекта - в URL, а дополнительный параметр, время жизни, можно передать в HTTP-заголовке с именем "ehcacheTimeToLiveSeconds", учитывайте, что если параметра нет, будет использован параметр из описания кеша, а интервал допустимых значений - 0 (вечно) ... 2147483647 (69 лет примерно).
  • DELETE - удаляет указанный элемент. Если надо удалить все элементы кеша, используйте маску *, жаль, что другие методы такого не поддерживают (то есть, multi-get-а на уровне REST нет, хотя сам по себе кеш вполне его поддерживает в JavaAPI).

Когда вы сохраняете элемент в кеш, можно указать его MIME-тип (из списка поддерживаемых), тогда при извлечении мы сразу получим нужные данные. Поддерживаются:

  • text/plain - обычный текст или произвольные данные
  • text/xml - XML-документ, согласно RFC 3023
  • application/json - самое интересное, JSON-формат (согласно RFC 4627)
  • application/x-java-serialized-object - сериализированный Java-объект

Собственно, вот и все описание самого сервера, теперь практическая часть - как работать с сервером из веб-приложения на PHP. Первоначальной идеей было написание специального Cache Backend для Zend Framework, по аналогии с классом для Memcached-а, но я сначала решил просто экспериментировать, как это все может работать. Возможно, такой класс я все же напишу, если это кому-либо, кроме меня, будет интересно и полезно.

Мы будем использовать Zend Framework для экспериментов, в частности, его классы для работы с HTTP-запросами (Zend_Http_Client) и класс для работы с JSON (Zend_Json).

Для начала необходимо установить подключение к серверу. Zend_Http предоставляет для этого несколько возможностей, разные адаптеры, однако по тестах самым быстрым оказался Socket-адаптер, Curl я бы использовал в последнюю очередь, в случае, если сервер кеша удаленный и к нему другим способом не добраться (например, надо использовать SSL, но для кеша это странное требование, однако в ряде случаев такое необходимо, поправка - сокет также может использовать ssl).

Опишем опции подключения, исходя из максимальной производительности, учитывая, что мы не один запрос в рамках страницы будет делать:

  1. $_config = Array(
  2. 'timeout' => 5,
  3. 'maxredirects' => 1,
  4. 'httpversion' => 1.1,
  5. 'adapter' => 'Zend_Http_Client_Adapter_Sockets',
  6. 'options' => array(
  7. 'persistent' => true,
  8. ),
  9. 'keepalive' => true
  10. );

Напомним, что наш основной URL следующий:

  1. $_url = 'http://localhost:8080/ehcache/rest/testRestCache';

Для первого примера мы попробуем положить в кеш содержимое большого массива, в качестве которого выступит $_SERVER, при этом мы зададим JSON как тип данных (предварительно мы конвертируем массив в JSON перед отправкой).

  1. //создаем объект подключения
  2. $ehcache_connect = new Zend_Http_Client('http://localhost', $_config);
  3.  
  4. //имя нашего объекта в кеше, его уникальный id
  5. $_chache_item_name = 'testitem1';
  6.  
  7. // задаем полный путь к элементу
  8. // http://localhost:8080/ehcache/rest/testRestCache/testitem1
  9. $ehcache_connect->setUri($_url . '/' . $_chache_item_name);
  10.  
  11. //укажем, что мы используем JSON
  12. $ehcache_connect->setHeaders('Content-type', 'application/json');
  13.  
  14. //установим метод
  15. $ehcache_connect->setMethod(Zend_Http_Client::PUT);
  16.  
  17. //добавляем данные с кодированием в JSON
  18. $ehcache_connect->setRawData(Zend_Json::encode($_SERVER));
  19.  
  20. //Все! Выполняем запрос
  21. $responce = $ehcache_connect->request();
  22.  
  23. // мы получили ответ в виде объекта класса Zend_Http_Response
  24. if ($responce->isSuccessful())
  25. {
  26. //все ок, запрос успешен, сервер вернул правильный HTTP-ответ с кодом 200
  27. echo 'Request OK!';
  28. }
  29. else
  30. {
  31. echo $responce->getMessage();
  32. }

Теперь получим назад свой массив, для этого можно даже не менять URL, а только сменить тип запроса, остальное такое же, как в предыдущем коде:

  1. // URL нашего объекта
  2. $ehcache_connect->setUri($_url . '/' . $_chache_item_name);
  3.  
  4. // Метод
  5. $ehcache_connect->setMethod(Zend_Http_Client::GET);
  6.  
  7. //выполняем запрос
  8. $_result = $ehcache_connect->request();
  9.  
  10. //если все ОК
  11. if ($_result->isSuccessful())
  12. {
  13. // получаем тело запроса и декодируем его из JSON обратно в Array
  14. $_json_res = Zend_Json::decode($_result->getBody(), Zend_Json::TYPE_ARRAY);
  15.  
  16. // Выведем на экран
  17. Zend_Debug::dump($_json_res);
  18. }
  19. else
  20. {
  21. echo $responce->getMessage();
  22. }

Остальные команды можно задать таким же способом. Первое, что немного ограничивает, что в Zend_Http нет поддержки HEAD-запросов, однако они обычно дублируют другие, так что большой надобности в них нет. Второе неудобство - метаданные о кеше или конкретных элементах отдаются в формате XML, хотя работать с элементами можно и в JSON. Статистика отдается вместе со всеми данными, хотя ее хорошо бы вынести в отдельную страницу. Третье неудобство - нет развитых возможностей по извлечению и добавлению данных. Нельзя сразу положить или запросить несколько элементов (хотя в самом Java API есть). А вот удалить все сразу вполне можно. Ну и безопасность никак не обеспечена, поэтому не храните конфиденциальные данные с доступном по HTTP наружу.

В заключение расскажу о главной мысли этого исследования. Так как данные мы можем получить напрямую в JSON, а веб-сервер поддерживает все возможности HTTP, клиентское приложение, например, на AJAX, вполне может напрямую взаимодействовать с кешем, запрашивая данные и получая их в JSON, а серверная сторона будет асинхронно ложить новые данные, когда они есть. Клиент сперва сам может проверить, есть ли в кеше данные, а если нет - напрямую обратится к серверной части.

Также достаточно просто реализовать шардинг кеша и балансировку нагрузки. Кстати, тогда лучше разворачивать сервер на базе полной версии GlassFish, так как в встраиваемой нет некоторых полезных возможностей, вроде админки, gzip-сжатия траффика и балансировщика нагрузки. Можно также использовать фронт-эндом nginx, который будет балансировать нагрузку между серверами, а они в фоне между собой реплицируются средствами Java. Протокол HTTP простой и достаточно гибкий, поэтому мы можем реализовать любую стратегию поведения кеширующего сервера, комбинируя возможности HTTP и платформы Java.

P.S. Пара слов о производительности. Конечно, мои тесты далеки от реальных и никак не могут быть достоверными и вообще что-либо значить. Усредненная цифра, полученные на моей машине (ноутбук для разработки, 1.5 Гб RAM/Celeron M 1.7 Ггц, WinXP SP3) в процессе подготовки материала - 0.020 - 0.025 сек. на операции чтения/записи (если использовать cURL, то примерно в два раза дольше). Интересно конечно протестировать вариант с репликацией и балансировкой нагрузки, но это уже совсем другой уровень, но я бы с радостью принял участие и посмотрел на результаты.

P.P.S. Отвечая на вопрос - а зачем это все? В некоторых случаях может заменить другие системы кеширования, тот же memcached, так как дает более гибкие настройки кешей, постоянство данных, различные системы репликации, отлично масштабируется и распределяется, данные могут быть получены клиентской системой напрямую (AJAX). В то же время, если у вас часть бекенда работает на Java, или даже весь, ей будет гораздо проще складывать туда данные. EHcache также может работать как отлично масштабируемая и надежная key-value база данных, обеспечивая именно репликацию и кластеризацию серьезного уровня, в отличие от множества новых решений, ehcache имеет уже продолжительную историю развития и оптимизации.

Мне кажется, если взять только сам сервлет, обеспечивающий REST-интерфейс, и поставить его на какой-либо быстрый и максимально легкий веб-сервер, например, Tjws, добавив легкий балансировщик, выделив отдельную JVM под каждый кеш (развернув двух-узловой кластер на каждом физическом сервере по сути) - мы получим намного более быструю и легкую систему с отличной масштабируемостью. А если добавить свой сервлет, буквально несколько строк, можем организовать поддержку и других протоколов/форматов - очень интересен был бы такой кешер с возможностью получать данные через Thrift/Google ProtoBuff, учитывая, что клиенты под эти протоколы есть на клиентских машинах (на JS и ActionScript). Поле для исследований широкое и интересное, верно?

  1. 7 августа 2009 в 13:01 | #1

    «ложит данные в кеш»… Кладёт? Записывает?

  2. 7 августа 2009 в 13:02 | #2

    @Sam
    да, вроде все слова-синонимы «put»-а

  3. 7 августа 2009 в 14:35 | #3

    EhCache успішно використовується на LinkedIn.

    як альтернатива сервера можна використати Jetty

    • 7 августа 2009 в 14:39 | #4

      Привет! твой комментарий как раз ожидал. Да, Jetty также можно использовать, но мне кажется, что все сервера такие будут излишними здесь — надо минимально допустимый сервер с поддержкой сервлетов и все — Tjws здесь, по моему, идеально подходит. + Nginx или Balancer перед ним, тогда можно делать два узла с балансировкой на одном сервере и разделить такие двухузловые штуки между несколькими физическими серверами. При этом почему два на узле — один полностью в памяти, второй комбинированый, память/диск, для обеспечения персистентности. И репликация между ними. Так в лучшем случае выиграем в скорости, в худшем — немного проиграем в скорости, но получим персистентность данных.

    • 7 августа 2009 в 14:39 | #5

      кстати, на хабре писал уже, чистый сервер занимает 62.5 Мб памяти (при отключенных всех ненужных кешах и SOAP-е)

  4. 11 августа 2009 в 16:43 | #6

    Да для сервера вес не большой получается.

Комментирование отключено.
Developers.org.ua