ECP і API управління процесами

Технологія розподілу навантаження між декількома серверами порівняно невеликої потужності є стандартною можливістю СКБД Caché вже досить давно. В її основі лежить протокол розподіленого кешу ECP (Enterprise Cache Protocol); тут мається на увазі саме «cache» («кеш»), а не «Caché» («кашЭ»). ECP відкриває багаті можливості для горизонтального масштабування прикладної системи, забезпечуючи високу продуктивність при не менш високої стійкості до відмов, залишаючи при цьому бюджет проекту в досить скромних рамках. До переваг мережі ECP справедливо буде віднести і можливість приховування особливостей її архітектури в надрах конфігурації СУБД, так що прикладні програми, спочатку розроблені для традиційної (вертикальної) архітектури, як правило, легко переносяться в горизонтальну ECP-середовище. Ця легкість настільки заворожує, що хочеться, щоб так було завжди. Наприклад, всі звикли до можливості управляти не тільки поточних, але і «чужими» процесами Caché: системна змінна $Job і пов'язані з нею функції і класи в умілих руках дозволяють «творити чудеса». Стоп, але тепер процеси можуть виявитися на різних серверах Caché… Нижче — про те, як вдалося досягти майже такої ж прозорості в управлінні процесами у середовищі ECP, як і без неї.

Термінологія ECP
Перш ніж заглиблюватися в тему, пригадаємо основні поняття, пов'язані з ПЕК.
ECP, або протокол розподіленого кешу, лежить в основі взаємодії серверів даних і серверів додатків. Він працює поверх TCP/IP, що забезпечує надійне транспортування пакетів. Протокол ECP є власністю фірми InterSystems.
Сервери даних (СБД), іноді називають серверами ECP, – це звичайні установки Caché, на яких знаходяться локальні бази даних програми. Дієслово «знаходяться» не варто розуміти буквально: бази даних можуть фізично знаходитися, наприклад, на мережевих системах зберігання даних, доступних через iSCSI або FC; важливо, що СУБД вважає їх локальними.
Сервери додатків (СП), іноді звані клієнтами ECP, – це звичайні установки Caché, на яких працюють процеси, які обслуговують користувачів прикладної системи. Іншими словами, на серверах додатків виконується прикладної код (звідси їх назва). Оскільки це звичайні установки Caché, на них є стандартний набір системних локальних баз даних: CACHESYS, CACHELIB, CACHETEMP і т. д. Це важливо, але не це головне. Набагато важливіше, що прикладні БД, локальні по відношенню до серверів даних, монтуються як віддалені бази даних на серверах програми. У загальному вигляді схема взаємодії показана на малюнку.


Основні компоненти ECP

Що таке кеш даних, сподіваюся, пояснювати не треба. Розподілений кеш, що фігурує в назві протоколу ПЕК, в загальному-то не більше, ніж метафора: кеш, звичайно ж, завжди локальний. Але якщо на сервері додатків відбувається читання вузла віддаленого глобал, відповідний блок копіюється з кешу сервера даних в кеш сервера додатків, так що повторні звернення до сусідніх вузлів того ж глобал будуть відбуватися локально, без повторного звернення до мережі і сервера даних. Чим довше працює система, тим актуальніше стає наповнення локального кешу на СП (як кажуть, тим краще він «розігрівається»), і тим рідше відбуваються операції мережевого доступу. В ідеалі, повторних читань з сервера даних вимагають лише змінені дані; виникає ілюзія існування розподіленого (між серверами) кешу.
Запис відбувається дещо складніше: запит на запис йде на сервер даних. Сервер даних шле у відповідь команду «викреслити блок з кешу», причому тільки тим серверів додатків, які раніше прочитали в свій кеш попередній стан цього блоку. Важливо, що сам змінений блок примусово не шле, так як, можливо, він більше нікому не потрібен. Якщо ж у ньому знову виникає потреба, блок заново запитується з сервера даних і знову потрапляє в локальний кеш сервера додатків, як це було описано раніше.

ECP очима архітектора
Рекламна пауза: трохи про плюси горизонтального масштабування засобами ECP. Уявіть, що потрібно спланувати систему на 10000 користувачів, і відомо, що на кожні 50 користувачів потрібні обчислювальні ресурси в кількості 1 ядра CPU і 10 GB RAM. Порівняємо альтернативи. В таблиці СБД&СП – традиційний сервер Caché (сервер даних і додатків «в одному флаконі»). З міркувань стійкості до відмов їх необхідно запланувати два.

Вертикальне vs. горизонтальне масштабування

Вертикальне масштабування Горизонтальне масштабування
Установки сервера Кількість серверів Установки сервера Кількість серверів
200 ядер CPU, 2 TB RAM 2 СБД&СП 16 ядер CPU, 160 GB RAM 13 СП, 2 СБД
У схемі з горизонтальним масштабом, з тих же міркувань плануємо два сервера даних, але думаю нікого не треба переконувати в ціновому переваги цієї схеми над попередньою… В якості бонусу отримуємо чудову особливість сумісного використання ECP і «дзеркала» (Mirroring): при перемиканні серверів даних – вузлів дзеркальної пари – користувачі серверів додатків відчувають лише коротку паузу в роботі (що вимірюється секундами), після чого їх сеанси тривають. У вертикальною схемою клієнтські процеси підключаються безпосередньо до сервера даних, тому при перемиканні серверів розриви користувальницьких сеансів неминучі.


Спільне використання ECP і «дзеркала»

Згадаймо, що на кожному сервері додатків є і локальні БД, тому зберігання проміжних даних природним чином перекладається на ці БД. Така робота може бути достатньо інтенсивною, а значить, залучення в неї великого числа навіть недорогих дисків може розвантажити центральну систему зберігання даних (СЗД), зменшивши час обслуговування (await) дискових операцій, без залучення ще однієї дорогої СГД для проміжних даних.

ECP очима програміста
«Нормальні» програми працюють в областях, отже, вам не доведеться міняти конструкції виду ^|"^^c:\intersystems\cache\mgr\qmsperf "|QNaz на ^|"^PERF^c:\intersystems\cache\mgr\qmsperf"|QNaz. Всі ці зокрема сховані в конфігурації Caché у визначення віддаленого сервера даних і віддаленої бази даних. Навіть якщо іноді доводиться звертатися до глобаль областей, відмінних від поточної області процесу, синтаксис подібних звернень (^|«qmsperf»|QNaz) залишається незмінним.
Семантика роботи з глобальними даними також практично не змінюється, просто працюючи з локальними БД, ми зазвичай про неї не замислюємося. Перерахую основні положення:
• Всі операції поділяються на синхронні (всі функції читання: $Get, $Order і т. д., а також Lock і $Increment) і асинхронні (запис даних: Set, Kill тощо). Подальше виконання програми не вимагає очікування завершення асинхронної операції. Для синхронних операцій це не так. У разі ECP вони можуть вимагати додаткового звернення до сервера даних, якщо блок даних відсутня в локальному кеші.
• Синхронні операції не чекають завершення асинхронних операцій, ініційованих тим самим сервером додатків.
• Команда Lock чекає завершення запису даних, розпочатої попереднім господарем блокування.
• Закінчення таймауту Lock не гарантує, що блокуванням володіє хтось інший.

А ось кілька незвичних моментів:
• Рядки довші половини розміру блоку не кешуються на серверах додатків. Насправді, цей поріг трохи нижче, для 8 KB-блоків – 3900 байт. Таке рішення було прийнято розробниками, щоб не забивати кеш BLOBами і CLOBами: відомо, що такі дані зазвичай пишуться один раз і згодом вкрай рідко читаються. На жаль, це рішення негативно позначилося на обробці bitmap-індексів, які теж, як правило, є довгими рядками. Якщо ви їх використовуєте, доведеться або зменшувати розмір chunk-а, або збільшувати розмір блоку; оптимальний вибір можна зробити лише на основі результатів тестування.

• Присвоювання
 set i=$Increment(^a)
може виявитися дорожче, ніж його функціонально близький аналог:
 lock +^a set (i,^a)=^a+1 lock -^a
Справа в тому, що функція $Increment завжди виконується на сервері даних, отже, очікування завершення подорожі пакетів «туди-назад» неминуче, а блокування (lock) викликає подібний ефект, лише коли її запитують процеси з різних серверів додатків.

• Необхідно обробляти помилки <NETWORK>. Такі помилки виникають, коли сервер додатків не може відновити втрачене ECP-з'єднання протягом Time to wait for recovery (за замовчуванням – 1200 секунд). Правильний спосіб обробки: відкотити розпочату транзакцію і спробувати заново.

ECP і управління процесами
Освіживши в пам'яті основні концепції ECP, перейдемо до основної теми статті. Управління процесами будемо трактувати в широкому сенсі. Реальні потреби прикладних програмістів, з якими доводиться стикатися, і системні засоби, наявні для їх задоволення «прямо з коробки», див. нижче.

Основні прикладні потреби в управлінні процесами

Функція Без ECP З ПЕК
Запуск фонових процесів. job
$job, $zchild, $zparent
Команда Job працює в мережі, але без передачі параметрів.
Номери процесів унікальні лише в межах кожного сервера.
Відстеження «жвавості» процесів. $data(^$job(pid)) Доступу до таблиці процесів іншого сервера немає.
Отримання списку процесів. $order(^$job(pid))
$zjob(pid)
См. вище.
Доступ до властивостей інших процесів. Class %SYS.ProcessQuery См. вище.
Завершення іншого процесу. Class SYS.Process Завершити процес на іншому сервері неможливо.
Відповіддю на ці «виклики» стала розробка API керування процесами, реалізованого у вигляді класу Util.Proc.
Щоб вам цікавіше було читати далі, наведу пару нескладних прикладів коду з використанням API.

Приклади використання API Util.Proc
• Вивести список процесів із зазначенням області та імені користувача медичної інформаційної системи (МІС), позначаючи «*» власний (поточний) процес:
 set cnt=0
 for {
     set proc=##class(Util.Proc).NextProc(proc,.sc) quit:proc="".||'sc   // наступного процес
     write proc_$select(##class(Util.Proc).ProcIsMy(proc):".*".,1:"".)  // помічаємо себе «*»
     write " область: "
     write ##class(Util.Proc).GetProcProp(proc,"NameSpace".) // властивість процесу: поточна область
     write " користувач: "
     write ##class(Util.Proc).GetProcVar(proc,$name(qARM("User".))),! // змінна процесу: ім'я користувача МІС
     set cnt=cnt+1
 }
 write ": "_cnt_" процесів.".

• Видалити процес, відмінний від поточного, якщо він виконується під тим же ім'ям користувача МІС (для виключення повторного входу користувачів):
 if '##class(Util.Proc).ProcIsMy(proc),
     ##class(Util.Proc).GetProcVar(proc,$name(qARM("User".)))=$name(qARM("User".)) {
   set res=##class(Util.Proc).KillProc(proc)
 }

Адресація процесів в мережі
При розробці API необхідно було вибрати спосіб адресації процесів в мережі ECP, причому хотілося отримати:
• унікальність адреси в локальній мережі,
• можливість безпосереднього використання адреси з мінімальними перетвореннями,
• легко читається формат.

Для адресації сервера в мережі можна використовувати його ім'я (hostname) або IP-адресу. Вибір імені у якості ідентифікатора хоч і привабливий, але накладає додаткові вимоги до бездоганності роботи служби імен. Оскільки подібні вимоги зазвичай не пред'являються при настроюванні конфігурації Caché, не хотілося вводити нових обмежень. До того ж в різних ОС hostname може мати різний формат, що ускладнить подальший розбір дескриптор процесу. Виходячи з цих міркувань, я вважав за краще використовувати IPv4-адресу.
Для ідентифікації установки Caché на сервері можна використовувати її ім'я («CACHE», «CACHEQMS», etc.) або номер порту суперсервера (1972, 56773, etc.). Але підключитися до встановлення Caché її імені не можна, тому вибираємо порт.
У підсумку, як дескриптора (унікального ідентифікатора) процесу було вирішено використовувати рядок у форматі децимального номери: xx.yy.zz.uu.Port.PID, де
xx.yy.zz.uu – IPv4 адреса сервера Caché,
Port – tcp-порт суперсервера Caché,
PID – номер процесу на сервері Caché.

Приклади коректних дескрипторів процесів:
192.168.11.19.56773.1760 – процес з PID=1760 на установці Caché з IP=192.168.11.19 і Port=56773.
192.168.11.77.1972.62801 – процес з PID=62801 на установці Caché з IP=192.168.11.77 і Port=1972.

Методи класу Util.Proc
У результаті був розроблений клас Util.Proc, відкриті методи якого перераховані нижче. Всі методи є методами класу (ClassMethod).

Зведення методів API управління процесами

Метод Функція
IsECP() As %Bolean В мережі ECP виконується код чи ні.
NextProc(proc, ByRef sc As %Status) As %String Наступний процес після процесу з дескриптором proc.
DataProc(proc, ByRef sc As %Status) As %Integer
If ##class(Util.Proc).DataProc(proc), процес з дескриптором proc існує.
GetProcProp(proc, Prop, ByRef sc As %Status) As %String
Отримати властивість з ім'ям Prop процесу з дескриптором proc. Можливий опитування наступних властивостей (див. клас %SYS.ProcessQuery):
Pid, ClientNodeName, UserName, ClientIPAddress, NameSpace, MemoryUsed, State, ClientExecutableName
GetProcVar(proc, var, ByRef sc As %Status) As %String
Отримати значення змінної var процесу з дескриптором proc.
KillProc(proc, ByRef sc As %Status) As %String
Завершити процес з дескриптором proc.
RunJob(EntryRef, Argv...) As %List
Запустити процес на сервері даних з точки входу EntryRef, передавши йому необхідну кількість фактичних параметрів (Argv). Повертає $lb(%Status, pid), де pid – номер процесу на сервері даних.
CheckJob(pid) As %List
Перевірити, «живий» процес з номером pid на сервері даних.
СКК(ClassMethodName, Argv...) As %String
Виконати на сервері даних довільний метод класу ClassMethodName (або $$-функцію, передавши необхідну кількість фактичних параметрів (Argv), і отримавши результат виконання.
Зіставляючи зведення методів з таблицею Основні прикладні потреби в управлінні процесами, бачимо, що вдалося їх задовольнити тепер вже і в мережному середовищі. Метод CCM() був доданий пізніше: в процесі перенесення нашого додатка (регіональної медичної інформаційної системи qMS) у середу ECP з'ясувалося, що деякі функціональні блоки зручніше і правильніше виконувати безпосередньо на сервері даних. Причини можуть бути різні:
• Бажання уникнути разової перекачування великого обсягу даних на сервер додатків, характерної, наприклад, при генерації звіту.
• Необхідність централізовано обслуговувати якийсь загальний ресурс, наприклад, черги обміну повідомленнями з іншою системою (в нашому випадку – з HealthShare).

Зауважу, що більшість методів API призначені для роботи в середовищі ECP. Без ECP вони все ще працездатні, але беруть повертають лише малоосмысленные дескрипторів процесів виду 127.0.0.1.Port.pid. Виняток становлять методи, орієнтовані на роботу з сервером даних: RunJob(), CheckJob(), СКК(), так як вони повертають беруть не дескриптор процесу (proc), а його номер (pid) на сервері даних. Тому ці методи зроблені універсальними з погляду прикладного програміста: їх інтерфейс однаковий як у середовищі ПЕК, так і без неї, хоча працюють вони, звичайно, по-різному.

Трохи про реалізації
Необхідно було вибрати метод взаємодії між процесами, що працюють на різних серверах. Розглядалися наступні альтернативи:
• Клас %SYSTEM.Event.
   o Офіційно не працює в мережі, тому підтримка мережевої роботи може бути припинена InterSystems в будь-який момент.

• Власний TCP-сервер.
   o В принципі, хороша ідея.
   o Необхідно задіяти додатковий TCP-порт (крім порту супер-сервера), що неминуче тягне за собою додаткові зусилля по встановленню та налаштуванню крім стандартних налаштувань Caché. А хотілося обійтися мінімумом настройок.

• Web-сервіси.
• Клас %Net.RemoteConnection. Для тих, хто забув: цей клас забезпечує віддалене виконання коду на інших серверах, використовуючи той же протокол, що і клієнти сервісу %Service_Bindings. Якщо у систему цей сервіс вже задіяний для підключення клієнтів, то ніякі додаткові налаштування не потрібні, а це якраз наш випадок. Накладні витрати на обмін даними незначні, як правило, він менше, ніж у випадку web-сервісів.

Виходячи з цих міркувань, я вибрав %Net.RemoteConnection. З його недоліків найбільш серйозний, на мій погляд, полягає в тому, що він не дозволяє повертати рядки довші 32KB, але це сильно не завадило.
Інша не менш цікава проблема, з якою довелося зіткнутися: як визначити, в мережі працює код чи ні? Відповідь на це питання необхідне як для внутрішніх потреб API (щоб правильно формувати дескрипторів процесів), так і для написання методу IsECP(), дуже затребуваного прикладними програмістами. Причина такої популярності досить очевидна: охочих переписувати ділянки свого коду, пов'язаного із взаємодією між процесами, на якомусь універсальному API, виявилося не дуже багато (хоча таке API і було реалізовано). Набагато простіше і природніше виявилося додати гілку коду для ПЕК. Але як визначити, в якому середовищі працює код? Розглядалися варіанти:
1. Основна база даних області є віддаленою.
  • Плюси: це дуже просто, всього-навсього:
        if $piece(##class(%SYS.Namespace).GetGlobalDest(),"^".)'="" //  в середовищі ПЕК
  • Мінуси: це справедливо тільки на сервері додатків і виключає мережеву роботу на сервері даних.
2. 1 або (основна БД області змонтована ким-то як віддалена). Мінуси:
  • Це дорого.
  • Це ненадійно за динамічної природи ПЕК.
3. 1 або (по одному з мережевих інтерфейсів до сервера даних підключився сервер додатків).

Зупинився на варіанті 3, так як він дозволяє досить швидко отримувати шуканий відповідь на питання і правильно
заповнювати дескрипторів процесів як на сервері додатків, так і на сервері даних. Зауважу, що для додаткового прискорення цієї перевірки її позитивний результат для кожного сервера фіксується в глобале.

Деякі висновки
Успішне впровадження API управління процесами у складі регіональної медичної інформаційної системи Красноярського краю показало якщо не ідеальну правильність, то як мінімум життєздатність вибраних підходів. За допомогою даного API нашим фахівцям вдалося вирішити ряд важливих завдань. Перерахую лише деякі з них:
• Виключення дубльованих входів користувачів.
• Отримання общесетевого списку працюючих користувачів.
• Обмін повідомленнями між користувачами.
• Запуск та контроль фонових процесів, які обслуговують лабораторні аналізатори.

На закінчення хочу подякувати моїх колег з СП.АРМ за допомогу в тестуванні коду, оперативну реакцію на помічені помилки і особливо за виправлення деяких з них. Частина методів класу Util.Proc (CCM(), RunJob(), CheckJob()) вдалося зробити незалежними від нашого прикладного ПЗ; їх можна завантажити з репозиторію на гітхабі або користувацької бази коду InterSystems.
Так, я не співробітник InterSystems, проте не без користі і з задоволенням використовую технології цієї компанії, чого бажаю і терплячому читачеві, дочитавшему до цього місця.

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

0 коментарів

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