PHP + Java, или In-memory кластер теперь и для PHP разработчиков

в 7:59, , рубрики: caching, cluster, highload, in-memory-data-grid, open source, php, высокая производительность, кеширование, кластер, кэширование

Intro

image
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 объединяет достоинства обоих подходов и при этом имеет ряд преимуществ перед упомянутыми выше решениями:

  1. хорошая горизонтальная масштабируемость
  2. высокая скорость доступа
  3. настоящая кластеризация (класть данные можно на любой узел, запрашивать данные можно также на любом узле кластера), автоматическая балансировка данных между узлами
  4. кластер знает о всех полях объекта, следовательно можно искать объекты не только по ключам, но и значениям полей
  5. есть возможность создавать индексы по полям либо по их комбинации
  6. при использовании механизмов read-through и write-behind (или write-through) данные будут синхронизироваться с БД, что позволит другим приложениям (либо другим модулям приложения) продолжать пользоваться традиционной БД (MySQL или Mongo — неважно)
  7. При использовании схемы работы из предыдущего пункта исчезает проблема актуализации данных в кэше, т.к. они всегда там будут такие же, как и в БД

Рассмотрим поближе эти 2 интересных механизма: read-through и write-behind (write-through)

read-through

Read-through — это механизм, который позволяет подтягивать данные из БД во время запроса.
Например вы хотите получить из кэша объект по ключу 'key', и при этом оказывается, что объекта с таким ключом в кластере нет, тогда автоматически этот объект будет прочитан из БД (или любого другого persistence storage), затем положен в кэш, после чего будет возвращен как ответ на запрос.
В случае отсутствия такого объекта в БД пользователю будет возвращен null.
Естественно, что необходимый sql-запрос, а также маппинг результатов запроса на объект лежит на плечах пользователя

write-behind (write-through)

Для оптимизации скорости записи вы можете писать не в БД, а напрямую в кэш. Звучит на первый взгляд странно, но на практике это хорошо разгружает БД и повышает скорость работы приложения.
Выглядит это примерно так:

  1. Пользователь делает вызов cache.put(key, value), объект 'value' сохраняется в кеше по ключу 'key'
  2. В кластере срабатывает обработчик этого события, происходит составление sql-запроса для записи данных в БД и его выполнение
  3. Управление возвращается пользователю

Такая схема взаимодействия называется write-through. Она позволяет синхронизировать обновления с БД одновременно с обновлениями в кластере. Как можно заметить, такой подход не ускоряет процесс записи данных, но обеспечивает согласованность данных между кэшом и БД. Также при таком виде записи данные попадают в кэш, а значит доступ к ним на чтение всё равно будет выше, чем запрос к БД.

Если же одновременнаяя запись в БД не является критичным условием, тогда можно использовать более популярный механизм write-behind, он позволяет организовать отложенную запись в БД (любой другой сторадж). Примерно так:

  1. Пользователь делает вызов cache.put(key, value), объект 'value' сохраняется в кэше по ключу 'key'
  2. Управление возвращается пользователю
  3. Через некоторое время (конфигурируется пользователем) срабатывает обработчик события записи в кэш
  4. Обработчик собирает всю пачку объектов, которые были изменены со времени предыдущего срабатывания обработчика
  5. Пачка отправляется в БД на запись

При использовании write-behind операция записи существенно ускоряется, потому что пользователь не ждет, пока апдейт дойдет до БД, а просто кладет данные в кэш, а все апдейты одного и того же объекта будут слиты в один результирующий апдейт, при этом запись в БД происходит пачками, что тоже положительно сказывается на загрузке сервера БД,
Таким образом можно сконфигурировать свой IMDG так, чтоб каждые 3 секунды (либо 2 мин, либо 50 мс) все обновления данных асинхронно отправлялись в базу.

Что из этого есть в Sproot Grid?

В первой версии я решил не реализовывать сразу всё, о чем рассказал выше, т.к. это отняло бы много времени, а мне хотелось бы побыстрее получить фидбэк от пользователей.
Итак, что доступно в Sproot Grid 1.0.0:

  1. Горизонтальная масштабируемость и честная кластеризация с балансировкой количества данных между узлами кластера
  2. Возможность хранения как встроенных PHP типов, так и доменных объектов
  3. Возможность построения индекса по полю и поиска по этому индексу

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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js