Отже, припустимо, що у нас є завдання порахувати швидко якусь задачу по великому обсягу даних. Що взагалі означає "великий обсяг" даних? Це такий обсяг, завантажувати який на клієнта не має сенсу з огляду на те, що клієнт не зможе вмістити на своєму боці всі необхідні дані. Дилема в тому, як отримати результат, без завантаження всього обсягу даних на клієнта. Можливим шляхом вирішення буде зробити підмножини величезної кількості даних і збирати на клієнті проміжні результати в циклі. Таке рішення добре всім, крім того, що послідовне виконання буде набагато довше, ніж виконання по всій множині за один раз (час буде витрачатися на запит / відповідь, підготовку підмножини даних і пересилання підмножин даних на клієнта для підрахунку). Також, за час виконання цього послідовного дії дані можуть застаріти. Тобто інтуїтивно ми розуміємо, що дані повинні оброблятися там, де лежать (без пересилання по мережі), і до того ж одночасно по всій множині.
Ось тут на підмогу приходять такі рішення, як Oracle Coherence, Hadoop, Gemfire і т.д.
Давайте пройдемося по азам Oracle Coherence.
Читаємо документацію і бачимо наступне: "Oracle Coherence is an in memory data grid solution that enables ...".
"In memory" - це означає, що дані тримаються в пам'яті комп'ютера (можна і на диску, але про це потім).
"Data grid solution" - це означає, що дані розподілені по кластеру і не сконцентровані в одному місці.
Але про все по порядку. Давайте спочатку зрозуміємо, які "будівельні блоки" є для реалізації поставлених завдань.
Нод це просто java процес (запустив клас com.tangosol.net.DefaultCacheServer) з coherence.jar в classpath і файлами. Можна запустити кілька нодов на одній / різних машинах, під одним або різними користувачами без обмежень. Тобто важливо розуміти, що це просто java процес і його можна / потрібно дебажіть так само, як і будь-якого серверного додаток, яке ви пишете.
Кластер це набір кількох нодов. Ноди в конфігурації за замовчуванням будуть знаходити один одного автоматично по multicast. При необхідності можна настроїти WKA (well known addresses), якщо системні адміністратори будуть незадоволені, що ви "забили всю мережу своїм мультикаст". У кластері завжди є майстер нод (senior member), який дивиться за тим, що відбувається з кластером (скільки нодов є, які з них зберігають дані, куди копіювати дані, якщо один з нодов "впав" і т.д.). Майстер нод - це перший нод, який стартував. Якщо майстер нод "впав" з якоїсь причини, то наступний майстер нод призначається автоматично. Слід зауважити, що під час обробки даних майстер нод не використовується. Обчислення виконуються на нодах, де лежать необхідні дані. Як правило, Ноди поділяють за призначенням: проксі, обчислювальні і ноди для зберігання даних. Якщо взагалі все Ноди "впали", то даних у вас немає. Тобто треба заздалегідь продумати, як дані / зміни будуть зберігатися і як завантажуватися після завантаження системи.
В процесі розробки рекомендується конфігурувати середу розробки, схожу на продакшн. Це дозволить знаходити багато помилок сериализации і комунікації між нодамі до того, як ви випустили версію в продакшн.
конфігурація нодов
За замовчуванням, конфігураційні файли не потрібні, в цьому випадку будуть використовуватися файли з coherence.jar. Файли, що надаються за замовчуванням, не підходять для продакшн системи, їх потрібно міняти під конкретну задачу. Деякі навіть рекомендують видаляти ці файли з coherence.jar файлу.
Існує 3 основних конфігураційних файлу, з якими вам доведеться працювати:
tangosol-coherence.xml - цей файл відповідає за конфігурацію кластера в цілому. Наприклад, ім'я кластера конфигурируется в цьому файлі.
coherence-cache-config.xml - цей файл відповідає за конфігурацію різних кешів, які кластер буде обслуговувати.
coherence-pof-config.xml - цей файл призначений для того, щоб визначити, які дані будуть оброблятися кластером. Також, в цьому файлі визначається, яким чином дані будуть серіалізіроваться для передачі і зберігання в кластері.
Існують так звані overrirde файли (наприклад, tangosol-coherence-override.xml). Установки в цьому файлі переписують установки базових файлів. Тобто, якщо у вас є tangosol-coherence.xml і tangosol-coherence-override.xml в classpath, то все установки завантажаться з першого файлу і перепишуть поверх установками з другого.
Ви можете мати кілька однакових файлів в classpath, але тільки перший знайдений файл буде використовуватися. Також можна встановити необхідні конфігураційні файли за допомогою системних (-D) установок.
Коли кластер стартує, він пише, які файли використовувалися для конфігурації системи. В логах з'явиться щось схоже на наступне:
Loaded operational configuration from resource ...
Loaded operational overrides from resource ...
Loaded operational overrides from resource ...
Проксі (extend) Ноди
Ноди для зберігання даних (storage nodes)
Це Ноди, у яких виставлена змінна оточення -Dtangosol.coherence.distributed.localstorage = true. За замовчуванням нод зберігає дані в java heap, але можна так само «скидати» їх на диск і довантажувати по потребі. На цих нодах можна робити обчислення, але треба розуміти, що потрібно робити якомога менше сміття в процесі обчислень для того, щоб нод не "впав" через нестачу пам'яті (OutOfMemory). У разі, якщо нод «впаде» по якійсь причині, дані з нього будуть скопійовані на інші Ноди, таким чином загальна ємність кластера зменшиться. Це може породити ефект каскаду, якщо вільного місця в кластері виявиться недостатньо, і тоді може «впасти» весь кластер, нод за нодом. Як правило, важливі дані мають другу копію (що прописується в настройках конфігурації), таким чином втрата окремого нода не критична.
Дані, які є проміжним результатом і легко обчислюються за основними даними, не потребують в другій копії. Зберігання даних може бути налаштоване таким чином, щоб мати копії на іншому ноді, на іншій фізичній машині або взагалі на інший стійці в іншому місті. Це все конфігураційні параметри і програмувати тут нічого не треба. Параметри зберігання даних досить гнучкі і дозволяють конфігурувати систему під конкретну задачу.
Обчислювальні Ноди (application tier / storage disabled nodes)
Це Ноди, у яких виставлена змінна оточення -Dtangosol.coherence.distributed.localstorage = false. Дані Ноди використовується для того, щоб рівномірно розподілити обчислення на кластер. На цих нодах можна також кешувати проміжні обчислення. Вся бізнес логіка, яку ви хочете реалізувати в межах цієї програми, повинна виконуватися в кластері на цих нодах.
Давайте розглянемо, яким чином реалізується процес пробрасиваніе виклику через ієрархію нодов. У вас є ноди для зберігання даних, обчислювальні Ноди і проксі Ноди. На проксі нодах дані не зберігаються і кеші НЕ конфигурируются. На обчислювальних нодах ви конфігуріруете кеші, але без можливості збереження даних в кешах. На нодах для зберігання даних у вас знаходяться дані. З боку клієнта ви не повинні використовувати кеші, на яких зберігаються дані. Тобто ви не виконуєте обчислення на самих даних безпосередньо з клієнта, а завжди використовуєте обчислювальні Ноди для виконання операцій над даними. Таким чином, ви ізолюєте дані від клієнтських додатків, що дає вам можливість в майбутньому змінювати архітектуру зберігання даних без зміни клієнта. Все Ноди в кластері «знають», де і який кеш знаходиться. Виходить, що якщо ви посилаєте завдання на виконання на кеш, конфігурований для обчислень, він буде виконаний в групі обчислювальних нодов, використовуючи дані з нодов, на яких зберігаються дані. Можливо, це звучить не зовсім зрозуміло, але це окрема тема для статті.
Локалізація даних (data affinity)
У деяких випадках корисно мати дані, згруповані разом з будь-якого принципу. Наприклад, ви можете згрупувати дані таким чином, що Ноди, що знаходяться на одній фізичній машині, матимуть залежні дані. В цьому випадку у вас не буде затримки мережі і обчислення будуть відбуватися швидше.
Механізми відправки завдання на виконання наступні: EntryAggregator, EntryProcessor, InvocationService, MapListener, EventInterceptor
Агрегатор (EntryAggregator) - це завдання, яке буде виконано над копіями даних. Тобто поміняти дані в кеші з агрегатора у вас не вийде. Робота відбувається з read-only даними. Типовими завданнями є сума, мінімум, максимум.
Процесор (EntryProcessor) - це завдання, яка передбачає зміни даних всередині кеша. Тобто, якщо ви хочете поміняти дані всередині кешу, там, де фізично знаходяться дані, вам потрібно використовувати для цього процесор. Приємною особливістю процесора є блокування на дані в процесі обробки. Тобто, якщо у вас є кілька операцій, які повинні бути викликані послідовно, то для цього потрібно використовувати процесор, так як тільки один процесор буде працювати над цим фрагментом даних в конкретний момент часу.
InvocationService - це завдання рівня нода. В даному випадку, ви працюєте грубо кажучи з Java процесами, а не з даними. Завдання даного типу повинні реалізовувати Invocable, що в свою чергу є Runnable.
MapListener - це завдання буде виконано асинхронно, як реакція на події на рівні кеша.
EventInterceptor - це завдання схожа на попередню в тому сенсі, що вона буде виконана як реакція на подію, але різниця полягає в тому, що listener буде виконаний на всіх нодах, на яких налаштований кеш, а interceptor - тільки на нодах, які мають дані, для яких виконано подія. У interceptor'а також є можливість бути викликаним до або після події.
Детальне пояснення, як працюють різні типи завдань, виходить за рамки цієї статті.
POF (portable object format) сериализация
Всі дані в кластері зберігаються в байтовому масиві. Поля серіалізовані об'єкта зберігаються послідовно (у кожного поля свій індекс) і саме так, як ви напишіть в методах readExternal / writeExternal класу, який реалізує інтерфейс PortableObject або serialize / deserialize класу, що реалізовує інтерфейс PofSerializer. Усередині байтового масиву поля зберігаються послідовно. Сканування масиву також відбувається послідовно. З цього слід не очевидний висновок: найбільш використовувані поля повинні знаходитися ближче до початку байтового масиву. Дані об'єкта, що записуються в масив, можуть бути вкладені і мати свою сериализацию. Тобто, при реалізації інтерфейсів PortableObject і PofSerializer, ви переводите ієрархічну структуру java об'єкта в плоску структуру байтового масиву.
Coherence надає сериализацию для стандартних об'єктів з jdk (java.lang). Всі об'єкти, які повинні бути збережені в кластері, повинні бути описані у файлі coherence-pof-config.xml. Кожен тип даних має свій номер. Номери ваших типів даних повинні починатися c 1000. Таким чином, у вас виходить структура, добре переноситься з однієї платформи на іншу, і з однієї мови програмування на іншу. Кожен клас, який буде серіалізовані в кластері, повинен мати коректно реалізовані hashCode і equals методи.
Витяг даних з кластера (ValueExtractor)
З попереднього пункту ми знаємо, що дані зберігаються в байтовому масиві. Для того, щоб витягти дані, потрібно написати клас, який реалізує інтерфейс ValueExtractor. Coherence буде використовувати цей клас для того, щоб дістати необхідну частину серіалізовані об'єкта і представити його вигляді класу, з яким ви можете працювати. Тобто у вас є можливість «витягнути» з даних не весь об'єкт цілком, а тільки те, що вам потрібно в даний момент для обчислень. Таким чином, у вас зменшується кількість даних, що пересилаються по мережі.
Партишн (partition)
Coherence надає можливість зберігати дані у вигляді «ключ-значення», але ключ і значення - це логічні поняття системи. На фізичному рівні дані групуються в Партишн. Тобто, кілька ключів і значень можуть належати одній Партишн. Партишн є одиницею зберігання даних. Коли Ноди падають і дані перегрупуються між нодамі, Партишн копіюється цілком. Клас, який призначає Партишн для конкретного об'єкта, реалізує інтерфейс KeyPartitioningStrategy. За замовчуванням, Партишн призначається відповідно до хеш-коду Binary об'єкта ключа (com.tangosol.util.Binary об'єкт «обертає» байт масив). Ви самі можете вплинути на те, як призначається Партишн, надавши свою реалізацію інтерфейсу KeyPartitioningStrategy.
Як і в базі даних, індекс в Coherence використовується для прискорення пошуку даних. Для того, щоб створити індекс, використовується метод QueryMap.addIndex (ValueExtractor extractor, boolean ordered, java.util.Comparator comparator).
ValueExtractor використовується для того, щоб вибрати з масиву байт необхідні дані для індексу. Коли ви викликаєте addIndex метод, це зовсім не означає, що кластер почне індексувати дані прямо зараз. Цей виклик є рекомендацією щодо створення індексу, коли ресурси будуть дозволяти це зробити. Після його створення, зміни даних будуть відображатися коректно в індексі. Даний метод можна викликати кілька разів, і якщо індекс вже існує, то він не буде заново створено. Індекс - це структура рівня нода. Тобто, коли дані копіюються з одного нода на інший, індекси не буде скопійований, замість цього він буде змінений відповідно до даних, які знаходяться на цьому ноді. Дані в індексі зберігаються в десеріалізованном вигляді, тому якщо у вас є необхідність дістати дані швидко і без десеріалізациі, створіть індекс. Природно, за зручність і швидкість треба «платити», і ви платите вільним місцем в кластері і обчислювальними ресурсами. Усередині індекс складається з двох під-індексів (прямий і зворотний). Прямий індекс зберігає дані У вигляді ключ-> значення (яке надав екстрактор), зворотний індекс зберігає дані у вигляді значення-> безліч ключів.
Кеші: replicated, distributed, near
Replicated - це кеш, в якому всі дані зберігаються в десеріалізованном вигляді на кожному з нодов. Даний тип кешу, який надає найшвидші операції читання, але повільні операції записи. Справа в тому, що в разі записи, дані повинні бути скопійовані на всі ноди, де налаштований даний кеш. Даний тип кешу, як правило, застосовується для рідко змінюваних даних малого об'єму.
Distributed - це основний тип кешу, який використовується для зберігання даних. Він дозволяє подолати обмеження за розміром оперативної пам'яті, виділеної на окремий нод, як би «розмазуючи» дані по всьому кластеру. Цей тип кешу також надає горизонтальну масштабованість за рахунок включення нових нодов в кластер, а також відмовостійкість за рахунок зберігання копії даних на інших нодах.
Near - це гібридний тип кешу, який конфігурується на викликає стороні (що викликає сторона може бути як клієнт так і інший нод всередині кеша). Як правило, цей кеш «стоїть» перед distributed кешем, і зберігає найбільш часто використовувані дані. Дані зберігаються в десеріалізованном вигляді. У випадку з near кешем, існує ймовірність того, що дані застаріють, тому вам потрібно конфігурувати, яким чином дані будуть оновлюватися.
Тепер, коли у нас є базові цеглинки понять, можна відповісти на питання, навіщо ж взагалі потрібен Coherence.
Відповідь проста: у вас є стійке до збоїв, горизонтально масштабується сховище даних, яке надає швидкий доступ до даних для паралельних обчислень. За рахунок наявності декількох нодов, у вас немає обмеження за розміром даних, які ви можете зберігати в кластері (звичайно, ви обмежені розміром доступної пам'яті на фізичних машинах, виділених для даної задачі). У вас немає обмеження на розмір окремої пари ключ-значення. Ви також можете отримувати від збережених даних лише те, що вам потрібно для обчислень, таким чином по мережі буде копіюватися мінімум інформації. Взагалі, вся ідеологія Coherence побудована на тому, щоб пересилати по мережі тільки те, що необхідно. Також, ви можете налаштовувати сервіси та рівні обчислень досить гнучко для вашого завдання. В результаті, складні завдання будуть вирішуватися швидко.
З точки зору менеджменту, ви купуєте рішення, яке дозволить задовольнити безлічі вимог. Одного разу завантаживши дані в систему, ви зможете отримувати їх різним чином і використовувати в інших системах, які використовують Coherence як сховище даних. Таким чином, поставивши в основу Coherence, ви можете побудувати екосистему по вилученню та обробці даних.
Якщо у вас виник інтерес з даної теми, я можу продовжити серію статей по Coherence. Пишіть, що саме вам цікаво, і я постараюся відповісти.
Поки що в плані:
- конфігурація базової структури кластера
- робота з EntryAggregator
- робота з EntryProcessor
- асинхронні виклики в Coherence
- дивимося всередину Binary об'єкта
- робота з індексами
Взагалі, слід враховувати, що з Coherence дуже просто починати, але дуже важко зробити добре і швидко, тому мета циклу статей заповнити прогалину між початковим рівнем знайомства з системою і просунутим рівнем розробника.
Нещодавно мій колега по роботі написав книгу для просунутих розробників, яку рекомендую до прочитання. У цій книзі ви не знайдете базових знань, але знайдете приклади розв'язання (з поясненнями) досить складних задач.