Hangfire — планувальник завдань .NET

Hangfire design
Зображення hangfire.io

Hangfire — багатопотоковий і масштабований планувальник завдань, побудований на клієнт-серверній архітектурі на стеку технологій .NET (в першу чергу Task Parallel Library і Reflection), з проміжним зберіганням завдань в БД. Повністю функціональний безоплатної (LGPL v3) версії з відкритим вихідним кодом. У статті розповідається, як користуватися Hangfire.

План статті:

Принципи роботи
В чому суть? Як ви можете бачити на КДПВ, яку я чесно скопіював з офіційної документації, процес-клієнт додає завдання в БД, процес-сервер періодично опитує БД і виконує завдання. Важливі моменти:
  • Все, що пов'язує клієнта і сервера — це доступ до загальної БД і загальним збірок, в яких оголошені класи-завдання.
  • Масштабування навантаження (збільшення кількості серверів) — є!
  • Без БД (сховища завдань) Hangfire не працює і працювати не може. За замовчуванням підтримується SQL Server, є розширення для ряду популярних СУБД. У платній версії додається підтримка Redis.
  • В якості хоста для Hangfire може виступати що завгодно: ASP.NET-додаток, Windows Service, консольний додаток і т. д. аж до Azure Worker Role.
З точки зору клієнта, робота з завданням відбувається за принципом «fire-and-forget», а якщо точніше — «додав в чергу і забув» — на клієнті не відбувається нічого, крім збереження в БД. Наприклад, ми хочемо виконати метод MethodToRun в окремому процесі:
BackgroundJob.Enqueue(() => MethodToRun(42, "foo"));

Ця задача буде сериализована разом зі значеннями вхідних параметрів і збережена в БД:
{
"Type": "HangClient.BackgroundJobClient_Tests, HangClient, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"Method": "MethodToRun",
"ParameterTypes": "(\"System.Int32 mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\",\"System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\")",
"Arguments": "(\"42\",\"\\\"foo\\\"\")"
}

Цієї інформації достатньо, щоб викликати метод MethodToRun в окремому процесі через Reflection, за умови доступу до складання HangClient, в якій він оголошений. Природно, зовсім необов'язково тримати код для фонового виконання в одній збірці з клієнтом, які в загальному випадку схема залежностей така:
module dependency
Клієнт і сервер повинні мати доступ до загальної збірці, при цьому для вбудованого веб-інтерфейсу (про нього трохи нижче) доступ необов'язковий. При необхідності можливо замінити реалізацію вже зберігається в БД завдання — шляхом заміни збірки, на яку посилається додаток-сервер. Це зручно для повторюваних за розкладом завдань, але, звичайно ж, працює за умови повного збігу контракту MethodToRun в старій і новій збірках. Єдине обмеження на метод — наявність public модифікатора.
Необхідно створити об'єкт і викликати його метод? Hangfire зробить це за нас:
BackgroundJob.Enqueue<EmailSender>(x => x.Send(13, "Hello!"));

І навіть отримає примірник EmailSender через DI-контейнер при необхідності.

Розгорнути сервер (наприклад в окремому Windows Service) простіше нікуди:
public partial class Service1 : ServiceBase
{
private BackgroundJobServer _server;

public Service1()
{
InitializeComponent();
GlobalConfiguration.Configuration.UseSqlServerStorage("connection_string");
}

protected override void OnStart(string() args)
{
_server = new BackgroundJobServer();
}

protected override void OnStop()
{
_server.Dispose();
}
}

Після старту сервісу наш Hangfire-сервер почне підтягувати завдання з БД і виконувати їх.

Необов'язковим для використання, але корисним і дуже приємним є вбудований web dashboard, який дозволяє керувати обробкою завдань:

dashboard

Нутрощі і можливості Hangfire-сервера
Перш за все, сервер містить свій пул потоків, реалізований через Task Parallel Library. А в основі лежить всім відомий Task.WaitAll (див. клас BackgroundProcessingServer).

Горизонтальне масштабування? Web Farm? Web Garden? Підтримується:
You don't want to consume additional Thread Pool threads with background processing – Hangfire Server uses custom, and separate limited thread pool.
You are using Web Farm or Web Garden and don't want to face with synchronization issues – Hangfire Server is Web Garden/Web Farm friendly by default.
Ми можемо створити довільну кількість Hangfire-серверів і не думати про їх синхронізації — Hangfire гарантує, що одна задача буде виконана одним і тільки одним сервером. Приклад реалізації — використання sp_getapplock (див. клас SqlServerDistributedLock).
Як вже зазначалося, Hangfire-сервер не вимогливий до процесу-хосту і може бути встановлений де завгодно від Console App до Azure Web Site. Однак, він не всемогутній, тому при хостингу в ASP.NET слід враховувати ряд загальних особливостей IIS, таких як process recycling, авто-старт (startMode=«AlwaysRunning» ) і т. п. Втім, документація планувальника надає вичерпну інформацію і на цей випадок.
До речі! Не можу не відзначити якість документації — воно вище всяких похвал і знаходиться десь у районі ідеального. Вихідний код Hangfire окрыт і якісно оформлений, немає ніяких перешкод до того, щоб підняти локальний сервер і походити по коду відладчиком.

Повторювані та відстрочені завдання
Hangfire дозволяє створювати повторювані завдання з мінімальним інтервалом у хвилину:
RecurringJob.AddOrUpdate(() => MethodToRun(42, "foo"), Cron.Minutely);

Запустити задачу вручну або видалити:
RecurringJob.Trigger("task-id");
RecurringJob.RemoveIfExists("task-id");

Відкласти виконання завдання:
BackgroundJob.Schedule(() => MethodToRun(42, "foo"), TimeSpan.FromDays(7));

Створення повторюваної І відкладеної завдання можливо за допомогою CRON expressions (підтримка реалізована через проект NCrontab). Наприклад, наступне завдання буде виконуватися кожен день в 2:15 ночі:
RecurringJob.AddOrUpdate("task-id", () => MethodToRun(42, "foo"), "15 2 * * *");


Микрообзор Quartz.NET
Розповідь про конкретний планувальнику завдань був би неповним без згадки гідних альтернатив. На платформі .NET такою альтернативою є Quartz.NET — порт планувальника Quartz з світу Java. Quartz.NET вирішує подібні задачі, як і Hangfire — підтримує довільну кількість «клієнтів» (додавання завдання) і «серверів» (виконання завдання), які використовують загальну БД. Але виконання різне.
Моє перше знайомство з Quartz.NET не можна було назвати вдалим — взятий з офіційно GitHub-репозиторію вихідний код просто не компилировался, поки я не поправив посилання на декілька відсутніх файлів і збірок (disclaimer: просто розповідаю, як було). Поділу на клієнтську і серверну частину в проекті немає — Quartz.NET поширюється у вигляді єдиної DLL. Для того, щоб конкретний екземпляр програми дозволяв тільки додавати завдання, а не виконувати їх необхідно його настроить.
Quartz.NET повністю безкоштовний, «з коробки» пропонує зберігання задач in-memory, так і з використанням багатьох популярних СУБД (SQL Server, Oracle, MySQL, SQLite тощо). Зберігання in-memory являє собою по суті звичайний словник в пам'яті одного єдиного процесу-сервера, що виконує завдання. Реалізувати кілька процесів-серверів стає можливим тільки при збереженні завдань в БД. Для синхронізації, Quartz.NET не покладається на специфічні особливості реалізації конкретної СУБД (ті ж Application Lock в SQL Server), а використовує один узагальнений алгоритм. Наприклад, шляхом реєстрації в таблиці QRTZ_LOCKS гарантується одноразова робота не більше ніж одного процесу-планувальника з конкретним унікальним id, видача завдання «на виконання» здійснюється простим зміною статусу в таблиці QRTZ_TRIGGERS.

Клас-завдання Quartz.NET повинен реалізовувати інтерфейс IJob:
public interface IJob
{
void Execute(IJobExecutionContext context);
}

З подібним обмеженням, дуже просто сериализации завдання: у БД зберігається повне ім'я класу, що достатньо для подальшого отримання типу класу-завдання через Type.GetType(name). Для передачі параметрів у завдання використовується клас JobDataMap, при цьому допускається зміна параметрів вже збереженої завдання.
Що стосується багатопоточності, то Quartz.NET використовує класи з простору імен System.Threading: new Thread() (див. клас QuartzThread)свої пули потоків, синхронізація через Monitor.Wait/Monitor.PulseAll.
Чималою ложкою дьогтю є якість офіційній документації. Приміром, ось матеріал по кластеризації: Lesson 11: Advanced (Enterprise) Features. Так-так, це все, що є на офіційному сайті по даній темі. Десь на просторах SO зустрічався феєричний рада переглядати гайди по оригінальному Quartz, там тема розкрита докладніше. Бажання розробників підтримувати схоже API в обох світах — Java і .NET — не може не позначатися на швидкості розробки. Релізи та оновлення у Quartz.NET нечасто.
Приклад клієнтського API: реєстрація повторюваної задачі HelloJob.
IScheduler scheduler = GetSqlServerScheduler();
scheduler.Start();

IJobDetail job = JobBuilder.Create<HelloJob>()
.Build();

ITrigger trigger = TriggerBuilder.Create()
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInSeconds(10)
.RepeatForever())
.Build();

scheduler.ScheduleJob(job, trigger);

Основні характеристики двох розглянутих планувальників зведені в таблицю:
Характеристика Hangfire Quartz.NET
Необмежену кількість клієнтів і серверів Так Так
Вихідний код github.com/HangfireIO github.com/quartznet/quartznet
NuGet-пакет Hangfire Quartz
Ліцензія LGPL v3 Apache License 2.0
Де хостим Web, Windows Azure Web, Windows Azure
Сховище завдань SQL Server (за замовчуванням), ряд СУБД через розширення, Redis (в платній версії) In-memory, ряд БД (SQL Server, MySQL, Oracle...)
Реалізація багатопоточності TPL Thread, Monitor
Web-інтерфейс Так Немає. Планується в майбутніх версіях.
Відстрочені завдання Так Так
Повторювані завдання Так (мінімальний інтервал 1 хвилина) Так (мінімальний інтервал 1 мілісекунда)
Cron Expressions Так Так
Про (не)навантажувальне тестування
Необхідно було перевірити, як впорається Hangfire з великою кількістю завдань. Сказано-зроблено, і я написав найпростішого клієнта, додає завдання з інтервалом 0,2 с. Кожна завдання записує рядок з налагоджувальною інформацією в БД. Поставивши на клієнті обмеження в 100К завдань, я запустив 2 примірника клієнта і один сервер, причому сервер — з профайлером (dotMemory). Через 6 годин, мене вже чекало 200К успішно виконаних завдань в Hangfire і 200К доданих рядків в БД. На скріншоті наведено результати профілювання — 2 знімка стану пам'яті «до» і «після» виконання:
snapshots
На наступних етапах працювало вже 20 процесів-клієнтів і процесів 20-серверів, а час виконання завдання було збільшено і стало випадковою величиною. Ось тільки на Hangfire це не відбивалося взагалі ніяк:
dashboard-2kk

Висновки. Опитування.
Особисто мені сподобався Hangfire. Безкоштовний, відкритий продукт, скорочує витрати на розробку та підтримку розподілених систем. Чи використовуєте ви що-небудь подібне? Запрошую взяти участь в опитуванні і розповісти свою точку зору в коментарях.

Які планувальники завдань ви використовуєте при розробці .NET?

/>
/>


<input type=«checkbox» id=«vv72404»
class=«checkbox js-field-data»
name=«variant[]»
value=«72404» />
Не пишемо .NET
<input type=«checkbox» id=«vv72406»
class=«checkbox js-field-data»
name=«variant[]»
value=«72406» />
Реалізуємо подібну функціональність самі
<input type=«checkbox» id=«vv72408»
class=«checkbox js-field-data»
name=«variant[]»
value=«72408» />
Quartz.NET
<input type=«checkbox» id=«vv72410»
class=«checkbox js-field-data»
name=«variant[]»
value=«72410» />
Hangfire
<input type=«checkbox» id=«vv72412»
class=«checkbox js-field-data»
name=«variant[]»
value=«72412» />
FluentScheduler
<input type=«checkbox» id=«vv72414»
class=«checkbox js-field-data»
name=«variant[]»
value=«72414» />
Інше
<input type=«checkbox» id=«vv72452»
class=«checkbox js-field-data»
name=«variant[]»
value=«72452» />
Не використовую нічого подібного (нецікаво, не потрібне)

Проголосувало 64 людини. Утрималося 22 людини.


Тільки зареєстровані користувачі можуть брати участь в опитуванні. Увійдіть, будь ласка.


Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

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