Ерланг на практиці
Трохи теорії
Минулого уроку ми з'ясували, що стратегія ерланг - розділити потоки на робочі (worker) і системні (supervisor), і доручити системним потокам обробляти падіння робочих потоків.
Існують наукові роботи, які доводять, що значна частина помилок у серверних системах викликані тимчасовими умовами, і перевантаження частини системи у відомий стабільний стан дозволяє впоратися з ними. Серед таких робіт докторська дисертація Джо Армстронга, одного із творців ерланг.
Систему на ерланг рекомендується будувати так, щоб будь-який потік був під наглядом супервізора, а супервізори були організовані в дерево.
На малюнку намальовано таке дерево. Вузли в ньому – супервізори, а листя – робочі процеси. Падіння будь-якого потоку та будь-якої частини системи не залишиться непоміченим.
Дерево супервізорів розгортається на старті системи. Кожен супервізор відповідає за те, щоб запустити своїх нащадків, спостерігати за їхнім станом, рестартувати та коректно завершувати, якщо треба.
Ерланг має стандартну реалізацію супервізора. Він працює аналогічно до gen_server. Ви повинні написати кастомний модуль, що реалізує поведінку supervisor, куди входить одна функція зворотного викликуinit/1. З одного боку це просто - лише один callback. З іншого бокуinit повинен повернути досить складну структуру даних, з якою потрібно добре розібратися.
Запуск супервізора
Запуск supervisor схожий на запуск gen_server. Ось картинка, аналогічна до тієї, що ми бачили в 10-му уроці:
Нагадаю, що два ліві квадрати (верхній та нижній), відповідають нашому модулю. Два праві квадрати відповідають коду OTP. Два верхні квадрати виконуються в потоці батька, два нижніхквадрати виконуються в потоці нащадка.
Починаємо з функціїstart_link/0 :
Тут ми просимо supervisor запустити новий потік.
Перший аргумент - це ім'я, під яким потрібно зареєструвати потік. Є варіант supervisor:start_link/2 на випадок, якщо ми не хочемо реєструвати потік.
Другий аргумент,?MODULE - це ім'я модуля, callback-функції якого викликатиме supervisor.
Третій аргумент - це набір параметрів, які потрібні для ініціалізації.
Далі відбувається якась магія в надрах OTP, у результаті якої створюється дочірній потік, і викликається callbackinit/1.
Зinit/1 потрібно повернути структуру даних, що містить всю необхідну інформацію для роботи супервізора.
Налаштування супервізора
Нам потрібно описати специфікацію самого супервізора та дочірніх процесів, за якими він спостерігатиме.
Специфікація супервізора - це кортеж із трьох значень:
RestartStrategy описує політику перезапуску дочірніх потоків. Є 4 варіанти стратегії:
one_for_one -- при падінні одного потоку перезапускається лише цей потік, інші продовжують працювати.
one_for_all -- під час падіння одного потоку перезапускаються всі дочірні потоки.
rest_for_one - проміжний варіант між двома першими стратегіями. Суть у тому, що спочатку потоки запущені один за одним, у певній послідовності. І при падінні одного потоку, перезапускається він, і ті потоки, які були запущені пізніше за нього. Ті, що були запущені раніше, продовжують працювати.
simple_one_for_one - це особливий варіант, що буде розглянуто нижче.
Чимало проблем можна вирішити рестартом, але не всі. Супервізор повинен якось справлятися із ситуацією,коли рестарт не допомагає. Для цього є ще два налаштування:Intensity - максимальна кількість рестартів, іPeriod - за проміжок часу.
Наприклад, якщо Intensity = 10, а Period = 1000, це означає, що дозволено не більше 10 рестартів за 1000 мілісекунд. Якщо потік падає 11 раз, то супервізор розуміє, що він не може виправити проблему. Тоді супервізор завершується сам, а проблему намагається вирішити його батько – супервізор рівнем вище.
У 18-й версії ерланг замість кортежу:
Але кортеж підтримується для зворотної сумісності.
child specifications
Тепер розберемо, як описуються дочірні потоки. Кожен з них описується кортежем із 6-ти елементів:
ChildID - ідентифікатор потоку. Тут може бути будь-яке значення. Супервізор не використовує Pid дочірнього потоку, тому що Pid змінюватиметься під час рестарту.
Start - кортеж, що описує, з якої функції стартує новий потік.
Restart - атом, що вказує на необхідність рестарту дочірнього потоку. Можливі 3 варіанти:
- permanent – потік потрібно рестартувати завжди.
- transient – потік потрібно рестартувати, якщо він завершився аварійно. За нормального завершення рестартувати не потрібно.
- temporary - потік не потрібно рестартувати.
Shutdown - визначає, скільки часу супервізор дає дочірньому потоку на нормальне завершення роботи.
Коли супервізор хоче зупинити дочірній потік, він шле сигнал shutdown, і чекає на заданий час. Якщо дочірній потік не завершився, супервізор зупиняє його сигналом kill.
Shutdown може бути вказаний як час у мілісекунах, або атомами:
- brutal_kill - не давати час, завершувати примусово відразуж.
- infinity - не обмежувати час, нехай дочірній потік завершується скільки йому потрібно.
Зазвичай для worker-потоків вказують час у мілісекундах, а для supervisor-потоків вказують на infinity.
Type - тип дочірнього потоку. Можливо або worker, або supervisor.
Modules - модулі, у яких виконується дочірній потік. Зазвичай це один модуль, і він збігається із зазначеним у кортежі Start.
Приклад child specitication:
У 18 версії ерланг використовується map:
Приклад функції init:
Те саме для 18-ї версії ерланг:
З map це виглядає зрозуміліше і лаконічніше.
Динамічне створення воркерів
Дерево супервізорів не обов'язково має стати статичним. При необхідності його можна змінювати: додавати/видаляти нові робочі потоки, і навіть нові гілки супервізорів. Є два способи зробити це: або викликамиstart_child або використаннямsimple_one_for_one стратегії.
start_child
4 функції супервізора дозволяють додавати та прибирати дочірні потоки.
start_child/2
Функція дозволяє додати новий дочірній потік, не описаний уinit. Вона приймає 2 аргументи: ім'я/pid супервізора та специфікацію дочірнього потоку.
terminate_child/2
Функція дозволяє зупинити працюючий дочірній потік. Вона приймає 2 аргументи: ім'я/pid супервізора, та Id дочірнього потоку.
Після того, як потік зупинено, його можна або рестартувати викликомrestart_child/2, або взагалі прибрати його специфікацію зі списку дочірніх потоків викликомdelete_child/2.
simple_one_for_one стратегія
Використанняsimple_one_for_one стратегії - це особливий випадок, коли нам потрібно мати великекількість потоків: десятки та сотні.
При використанні цієї стратегії супервізор може мати нащадків лише одного типу. І, відповідно, повинен вказати лише одну дитину specitication.
Дочірні потоки потрібно запускати явно викликомstart_child/2. Причому тут змінюється роль другого аргументу. Це тепер не child specification, а додаткові аргументи дочірньому потоку.
І дочірній потік своєї функції start_link отримає аргументи і з child specification, і з start_child.
Зупинка супервізора
В АПА супервізора не передбачено функції для його зупинки. Він зупиняється або за своєю стратегією, або за сигналом батька.
При цьому він завершує всі свої дочірні потоки в черговості, зворотній запуску, потім зупиняється сам.