PHP + Java, або In-memory кластер тепер і для 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.  
  7. кластер знає про всіх полях об'єкта, отже можна шукати об'єкти не тільки по ключах, а й значенням полів
  8.  
  9. є можливість створювати індекси по полях або за їх комбінації
  10.  
  11. при використанні механізмів read-through і write-behind (або write-through) дані будуть синхронізуватися з БД, що дозволить іншим додаткам (або інших модулів додатка) продовжувати користуватися традиційної БД (MySQL або Mongo — неважливо)
  12.  
  13. При використанні схеми роботи з попереднього пункту зникає проблема актуалізації даних в кеші, т.к. вони завжди там будуть такі ж, як і в БД
  14.  
Розглянемо ближче ці 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.  
  3. У кластері спрацьовує обробник цієї події, відбувається складання sql-запроса для запису даних у БД і його виконання
  4.  
  5. Управління повертається користувачеві
  6.  
Така схема взаємодії називається write-through . Вона дозволяє синхронізувати оновлення з БД одночасно з оновленнями в кластері. Як можна помітити, такий підхід не прискорює процес запису даних, але забезпечує узгодженість даних між кешом і БД. Також при такому вигляді запису дані потрапляють в кеш, а значить доступ до них на читання все одно буде вище, ніж запит до БД.
 
Якщо ж одновременнаяя запис в БД не є критичним умовою, тоді можна використовувати більш популярний механізм write-behind , він дозволяє організувати відкладений запис в БД (будь-який інший сторадж). Приблизно так:
 
     
  1. Користувач робить виклик cache.put (key, value) , об'єкт 'value ' зберігається в кеші по ключу 'key '
  2.  
  3. Управління повертається користувачеві
  4.  
  5. Через деякий час (конфігурується користувачем) спрацьовує обробник події записи в кеш
  6.  
  7. Оброблювач збирає всю пачку об'єктів, які були змінені з часу попереднього спрацьовування обробника
  8.  
  9. Пачка відправляється в БД на запис
  10.  
При використанні write-behind операція запису істотно прискорюється, тому що користувач не чекає, поки апдейт дійде до БД, а просто кладе дані в кеш, а все апдейти одного і того ж об'єкта будуть злиті в один результуючий апдейт, при цьому запис у БД відбувається пачками, що теж позитивно позначається на завантаженні сервера БД,
Таким чином можна конфігурувати свій IMDG так, щоб кожні 3 секунди (або 2 хв, або 50 мс) всі оновлення даних асинхронно відправлялися в базу.
 
 

Що з цього є в Sproot Grid?

У першій версії я вирішив не реалізовувати відразу все, про що розповів вище, тому що це забрало б багато часу, а мені хотілося б швидше отримати фідбек від користувачів.
Отже, що доступно в Sproot Grid 1.0.0:
 
     
  1. Горизонтальна масштабованість і чесна кластеризація з балансуванням кількості даних між вузлами кластера
  2.  
  3. Можливість зберігання як вбудованих PHP типів, так і доменних об'єктів
  4.  
  5. Можливість побудови індексу по полю і пошуку за цим індексом
  6.  
 
 

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="some\package\User" 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="some\package\Car"/>
        </dataType>
        <dataType type="some\package\Car" 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="some\package\Car" 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 org\sproot_grid\SprootClient;
    use some\package\User;

    $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.
 Ісходникі
 Вікі
 Дистрибутив
  

    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.