Intro
PHP + Java. Картинка взята отсюда
В этом комментарии к статье под названием «Пишите код каждый день» я сказал, что скоро покажу свой проект, на который я выделял ежедневно 1 час (кроме выходных). Так как в последнее время моя работа связана с написанием распределенных Java приложений, которые используют in-memory data grid (IMDG) в качестве хранилища данных, то мой проект связан именно с этим.
Подробнее про IMDG можно почитать в моих предыдущих статьях (1, 2). Но если кратко, то это кластерное распределенное хранилище объектов по ключам, которое держит все данные в памяти, за счет чего достигается высокая скорость доступа к данным. Позволяет не только хранить, но и обрабатывать данные, не извлекая их из кластера.
И если интерфейс для обработки данных у каждого конкретного IMDG свой, то интерфейс доступа к данным обычно идентичен хеш-таблице.
О чем эта статья
Большинство IMDG написано на Java и поддерживают API для Java, C++, C#, при этом API для веб языков программирования (Python, Ruby, PHP) не поддерживается, а протокол для написания клиентов сильно ограничен. Именно этот факт я и считаю основным тормозом для проникновения IMDG в массы — отсутствие поддержки самых массовых языков.
Так как производители IMDG пока не предоставляют поддержку веб языков, то веб программисты не имеют возможностей по такому же легкому масштабированию приложений, какие есть у серверных Java разработчиков. Поэтому я решил сделать нечто подобное самостоятельно и выложить в open source, взяв в качестве движка open source IMDG JBoss Infinispan (компания JBoss, принадлежащая Red Hat, довольно хорошо известна в кругу java разработчиков). Мой проект называется Sproot Grid, пока доступен только для PHP, но если у сообщества будет интерес, то сделаю и интеграцию с Ruby и Python.
В этой статье я еще раз расскажу про in-memory data grid и про то, как конфигурировать, запускать и использовать Sproot Grid.
Зачем нужен IMDG?
Самым узким местом многих высоконагруженных проектов является хранилище данных, в частности реляционная БД. Для борьбы с недостатками традиционных БД в основном используется 2 подхода:
1) Кэширование
плюсы:
- высокая скорость доступа к данным
минусы:
- очень редко встречаются настоящие кластерные решения, в основном пользователю самому приходится заниматься распределением данных по серверам, а при доступе к данным определять тот сервер, на котором лежат эти данные. Равномерности заполненности всех узлов кластера в такой системе достичь сложно
- требует компромисса между актуальностью данных и скоростью доступа, т.к. данные в кэше могут устареть, а удалять старые данные из кэша с последующим кэшированием новых — это дополнительные задержки и нагрузка на систему
- Обычно данные кэшируются не в виде доменных объектов, которые используются в приложении, а в виде BLOB либо строк, т.е. при использовании данных, полученных из кэша, необходимо сначала сконструировать нужные объекты
2) NoSQL решения
плюсы:
- хорошая горизонтальная масштабируемость
минусы:
- не такая высокая скорость получения результатов в случае использования диска
- практически невозможно обеспечить работу внутрикорпоративного софта, который ориентирован на работу с конкретной реляционной БД
IMDG объединяет достоинства обоих подходов и при этом имеет ряд преимуществ перед упомянутыми выше решениями:
- хорошая горизонтальная масштабируемость
- высокая скорость доступа
- настоящая кластеризация (класть данные можно на любой узел, запрашивать данные можно также на любом узле кластера), автоматическая балансировка данных между узлами
- кластер знает о всех полях объекта, следовательно можно искать объекты не только по ключам, но и значениям полей
- есть возможность создавать индексы по полям либо по их комбинации
- при использовании механизмов read-through и write-behind (или write-through) данные будут синхронизироваться с БД, что позволит другим приложениям (либо другим модулям приложения) продолжать пользоваться традиционной БД (MySQL или Mongo — неважно)
- При использовании схемы работы из предыдущего пункта исчезает проблема актуализации данных в кэше, т.к. они всегда там будут такие же, как и в БД
Рассмотрим поближе эти 2 интересных механизма: read-through и write-behind (write-through)
read-through
Read-through — это механизм, который позволяет подтягивать данные из БД во время запроса.
Например вы хотите получить из кэша объект по ключу 'key', и при этом оказывается, что объекта с таким ключом в кластере нет, тогда автоматически этот объект будет прочитан из БД (или любого другого persistence storage), затем положен в кэш, после чего будет возвращен как ответ на запрос.
В случае отсутствия такого объекта в БД пользователю будет возвращен null.
Естественно, что необходимый sql-запрос, а также маппинг результатов запроса на объект лежит на плечах пользователя
write-behind (write-through)
Для оптимизации скорости записи вы можете писать не в БД, а напрямую в кэш. Звучит на первый взгляд странно, но на практике это хорошо разгружает БД и повышает скорость работы приложения.
Выглядит это примерно так:
- Пользователь делает вызов cache.put(key, value), объект 'value' сохраняется в кеше по ключу 'key'
- В кластере срабатывает обработчик этого события, происходит составление sql-запроса для записи данных в БД и его выполнение
- Управление возвращается пользователю
Такая схема взаимодействия называется write-through. Она позволяет синхронизировать обновления с БД одновременно с обновлениями в кластере. Как можно заметить, такой подход не ускоряет процесс записи данных, но обеспечивает согласованность данных между кэшом и БД. Также при таком виде записи данные попадают в кэш, а значит доступ к ним на чтение всё равно будет выше, чем запрос к БД.
Если же одновременнаяя запись в БД не является критичным условием, тогда можно использовать более популярный механизм write-behind, он позволяет организовать отложенную запись в БД (любой другой сторадж). Примерно так:
- Пользователь делает вызов cache.put(key, value), объект 'value' сохраняется в кэше по ключу 'key'
- Управление возвращается пользователю
- Через некоторое время (конфигурируется пользователем) срабатывает обработчик события записи в кэш
- Обработчик собирает всю пачку объектов, которые были изменены со времени предыдущего срабатывания обработчика
- Пачка отправляется в БД на запись
При использовании write-behind операция записи существенно ускоряется, потому что пользователь не ждет, пока апдейт дойдет до БД, а просто кладет данные в кэш, а все апдейты одного и того же объекта будут слиты в один результирующий апдейт, при этом запись в БД происходит пачками, что тоже положительно сказывается на загрузке сервера БД,
Таким образом можно сконфигурировать свой IMDG так, чтоб каждые 3 секунды (либо 2 мин, либо 50 мс) все обновления данных асинхронно отправлялись в базу.
Что из этого есть в Sproot Grid?
В первой версии я решил не реализовывать сразу всё, о чем рассказал выше, т.к. это отняло бы много времени, а мне хотелось бы побыстрее получить фидбэк от пользователей.
Итак, что доступно в Sproot Grid 1.0.0:
- Горизонтальная масштабируемость и честная кластеризация с балансировкой количества данных между узлами кластера
- Возможность хранения как встроенных PHP типов, так и доменных объектов
- Возможность построения индекса по полю и поиска по этому индексу
Getting Started
Сначала вам надо скачать дистрибутив отсюда и распаковать его.
Установка необходимого ПО
Так как JBoss Infinispan — это Java приложение, то необходимо было выбрать способ взаимодействия между Java и PHP. В качестве такого связующего звена был выбран Apache Thrift (протокол был разработан для сериализации и транспорта между узлами в Cassandra), поэтому для того, чтоб Sproot Grid мог работать на вашей системе необходимо установить следующее:
- Java
- Thrift — установка в production не требуется, установка нужна только на девелоперской машине (подробности в пункте Генерация кода). При деплое в production вам потребуется только скопировать .php файлы библиотеки Thrift и java библиотеку в формате .jar
- PHP (если еще не установлен)
Инструкции по установке расположены на wiki проекта
Конфигурация
Файл конфигурации должен находиться в $deploymentFolder/sproot-grid/config/definition.xml, где deploymentFolder — это путь к директории, в которой вы распаковали дистрибутив
<?xml version="1.0" encoding="UTF-8"?>
<sproot-config>
<dataTypes>
<dataType type="somepackageUser" cache-name="user-cache">
<field name="id" type="integer" />
<field name="name" type="string" indexed="true" />
<field name="cars" type="array" key-type="string" value-type="somepackageCar"/>
</dataType>
<dataType type="somepackageCar" cache-name="car-cache">
<field name="model" type="string" />
<field name="isNew" type="boolean" />
</dataType>
<dataType type="string" cache-name="string-cache"/>
<dataType type="array" value-type="somepackageCar" cache-name="list-car-cache"/>
</dataTypes>
<cluster name="Sproot">
<multicast host="224.3.7.0" port="12345"/>
<caches>
<cache name="user-cache" backup-count="1">
<eviction max-objects="1000" lifespan="2000" max-idle-time="5000" wakeup-interval="10000" />
</cache>
<cache name="car-cache" backup-count="1" />
<cache name="string-cache" backup-count="1" />
<cache name="list-car-cache" backup-count="1" />
</caches>
<nodes>
<node id="1" role="service" thrift-port="34567" minThreads="5" maxThreads="100" />
<node id="2" role="storage-only" />
</nodes>
</cluster>
</sproot-config>
Подробнее о конфигурации можно почитать на wiki проекта
Как можно заметить из конфигурации, для каждого типа объектов мы можем прописать имя кеша (а можем и не прописывать, если не хотим хранить такие объекты в отдельном кеше). Cache — это хеш-таблица, распределенная по кластеру, в кластере может быть сколько угодно кэшей. В одном кэше могут храниться только объекты одного и того же типа.
Все кеши должны быть описаны в секции <caches/>.
В конфигурации есть отдельная секция для описания структуры кластера и список кешей, которые будут в нем храниться.
<datatypes/> — описание типов, которые будут храниться в вашем кластере. Можно использовать как встроенные PHP типы, так и кастомные. Как можно заметить, для каждого типа объектов мы можем прописать имя кеша (а можем и не прописывать, если не хотим хранить такие объекты в отдельном кеше)
<cluster/> — описание структуры кластера и список кешей, которые будут в нем храниться.
<caches/> описывает кеши. Имя кеша должно быть уникальным, параметр backup-count определяет, сколько узлов кластера вы можете потерять без потери данных. Чем большее значение имеет backup-count, тем надежнее ваш кластер, но тем больше памяти он потребляет. Также можно сконфигурировать eviction (автоматическое удаление объектов из кеша), подробнее об этом на wiki страничке
<multicast/> определяет мультикастовый адрес, который будет использоваться для сборки кластера. Как известно, для мультикаста доступны только сети класса D (224.0.0.0 — 239.255.255.255)
<nodes/> описывает количество и типы узлов кластера. Сейчас есть только 2 типа узлов: storage-only — занимается только хранением данных и выполнением внутренних запросов service — не только хранит данные, но и обрабатывает внешние запросы, поэтому для узлов данного типа необходимо указать порт, на котором будут приниматься запросы от PHP клиентов.
Генерация кода для интеграции с вашим приложением
Для эффективной работы кластеру необходимо сгенерировать код, специфичный для вашего приложения (вашей доменной модели) и скомпилировать его Java часть, так как это работает быстрее, чем доступ к объектам через reflection. Чтобы сгенерировать и скомпилировать весь необходимый код, надо:
1) cd $deploymentFolder/sproot-grid/scripts
2) build.sh(or build.cmd)
, где $deploymentFolder — это тот каталог, в который вы распаковали дистрибутив
Генерацию кода необходимо производить только в случае изменения описания доменной модели, т.е. если ваша модель стабильна, то эту операцию вам придется произвести лишь один раз, после этого сгенеренные php исходники можно хранить в репозитории кода, а java часть будет скомпилирована в библиотеку. Т.е. не надо ничего генерить по 10 раз перед тем, как задеплоить ваше приложение, это делается только 1 раз на этапе разработки.
После окончания выполнения генерации кода, скопируйте папку с .php файлами из $deploymentFolder/sproot-grid/php/org в корень вашего приложения
Запуск
1) cd $deploymentFolder/sproot-grid/scripts
2) run.sh(run.cmd) nodeId memorySize
, где nodeId — значение атрибута id секции в конфигурационном файле,
memorySize — количество памяти (в Мб или Гб), которые вы хотите выделить узлу
Например:
run.sh 1 256m
или
run.cmd 2 2g
Использование внутри приложения
На шаге генерации кода вы получили всё необходимое для интеграции с вашим приложением. Остальось только скопировать этот код в свое приложение, для этого скопируйте всё из папки $deploymentFolder/sproot-grid/php в корень своего приложения
Всё! Теперь можете использовать кластер из своего приложения.
<?php
require_once 'org/sproot_grid/SprootClient.php';
require_once 'some/package/User.php';
use orgsproot_gridSprootClient;
use somepackageUser;
$client = new SprootClient('localhost', 12345); // в качестве параметров в конструктор передаются хост и порт узла кластера типа 'service'
echo $client -> cacheSize('user-cache');
$user = new User();
$user->setName('SomeUser');
$user->setId(1234);
$client->put('user-cache', '1234', $user);
echo $client -> cacheSize('user-cache');
?>
Описание API можете найти здесь, но если вкратце, то API сейчас такой:
- get($cacheName, $key)
- getAll($cacheName, array $keys)
- cacheSize($cacheName)
- cacheKeySet($cacheName)
- containsKey($cacheName, $key)
- search($cacheName, $fieldName, $searchWord)
- remove($cacheName, $key)
- removeAll($cacheName, array $keys)
- put($cacheName, $key, $domainObject)
- putAll($cacheName, array $domainObjects)
- clearCache($cacheName)
Заключение
Sproot Grid опубликован под лицензией MIT.
Исходники
Вики
Дистрибутив
Автор: gricom