Як ми будуємо систему обробки повідомлень

Наша команда розробляє бекэнд-систему для обробки повідомлень від мобільних пристроїв. Пристрої збирають інформацію про роботу складної техніки і посилають повідомлення в центр обробки. У цій статті я хочу поділитися підходами до побудови подібних систем. Ідеї досить загальні, їх можна застосовувати для будь-якої системи з наступною архітектурою:



По каналах зв'язку пристрої надсилають повідомлення на наш шлюз (gateway) — вхідні точку програми. Завдання програми — розібратися, що саме прийшло, зробити необхідні дії і зберегти інформацію в базі даних для подальшого аналізу. Базу ми будемо розглядати як кінцеву точку обробки. Звучить просто, але зі зростанням кількості та різноманітності повідомлень з'являється кілька нюансів, які я і хочу обговорити.

Трохи про рівень навантаження. Наша система обробляє повідомлення від десятків тисяч пристроїв, при цьому за секунду в середньому ми отримуємо від декількох сотень до тисячі повідомлень. Якщо ваші числа відрізняються на кілька порядків в ту або іншу сторону, можливо, набір ваших проблем і підходів до їх вирішення буде іншим.

Крім власне числа повідомлень в секунду, існує проблема нерівномірного отримання. Додаток має бути готове до коротким піків навантаження в десятки разів вище середнього. Для вирішення цієї задачі система організується у вигляді набору черг та їх обробників.

Приймальний шлюз не робить ніякої реальної роботи — він просто отримує повідомлення від клієнта і поміщає його в чергу. Це дуже дешева операція, тому шлюз здатний отримувати величезну кількість повідомлень в секунду. Потім, існує окремий процес, який отримує з черги кілька повідомлень — рівно стільки, скільки він хоче і може — і робить важку роботу. Обробка виходить асинхронної, а навантаження — стабільно обмеженою. Можливо, в піку злегка виросте час перебування повідомлень в черзі, от і все.

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



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

Такий базовий розклад, про що ж ще треба подумати?

Визначаємося з цінностями

1. Простота створення, зміни та підтримки
Асинхронна розподілена обробка повідомлень привносить в програмний продукт додаткову складність. Ми постійно працюємо над зниженням цієї ціни. Код оптимізується, в першу чергу, в бік підвищення доступності, зрозумілості для всіх членів команди, простоти зміни і підтримки. Якщо в результаті ніхто, крім автора не зможе розібратися в коді, ніяка чудова архітектура не допоможе зробити команду щасливою.

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

2. Відмовостійкість
Має сенс відразу визначитися щодо вимог до здатності системи продовжувати функціонувати при виникненні відмов обладнання і підсистем. У всіх вони будуть різними. Можливо, хтось готовий просто викинути всі повідомлення за ті 5 хвилин, що один з серверів перезавантажується.

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

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

3. Продуктивність
Число повідомлень, оброблюваних в одиницю часу, затримки, навантаження на сервера — все це важливі параметри продуктивності системи. Саме тому ми закладаємо в проект гнучку архітектуру.

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

У той же час, навантажувальні тести потрібно починати робити рано, а потім зробити їх частиною стандартної процедури тестування. І тільки тоді, коли тести покажуть конкретну проблему, беріться за оптимізацію.

Налаштовуємо мозок

1. Оперуйте чергами і асинхронними обробниками
Про це я вже писав вище. Наш основний інструментарій — черги та їх обробники. На додаток до класичного стилю організації коду «отримав запит, викликав віддалений код, дочекався відповіді, повернув відповідь наверх», ми отримуємо підхід «отримав повідомлення з черги, відпрацював, відправив повідомлення в іншу чергу». Від того, наскільки вдалий баланс в поєднанні двох підходів ви знайдете, залежить як масштабованість, так і простота розробки системи.

2. Розбивайте обробку на кілька етапів
Якщо обробка повідомлення достатньо складна і може бути розбита на декілька етапів, передбачте кілька черг і обробників. Майте на увазі, що надмірна фрагментація може зробити систему більш складною для розуміння. Тут потрібен баланс. Досить часто існує розбиття, природне і зрозуміле для розробників. Якщо ні, то спробуйте подумати про точки відмови. Якщо обробник може завершитися помилкою з кількох незалежних причин, можна подумати про його розбиття.

3. Не змішуйте декодування і обробку
Зазвичай, повідомлення приходить у форматі якогось протоколу взаємодії пристроїв у мережі: бінарному, xml, json і т. д. Декодируйте і переводьте їх у свій внутрішній формат якомога раніше. Це дозволить вирішити щонайменше два завдання. По-перше, протоколів може бути кілька; після декодування ви зможете уніфікувати формат подальших повідомлень. По-друге, спрощується логування та налагодження.

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

5. Обробляйте повідомлення групами
Часто має сенс отримувати повідомлення з черги не по одному, а відразу групами. Використовувані вами сервіси можуть приймати масив даних для пакетної обробки, у такому випадку один великий виклик зазвичай буде набагато ефективніше ста маленьких. Вставка сотні записів у базу за один раз буде швидше ста віддалених викликів.

Створюємо інструменти

1. Реалізуйте тотальне спостереження
Вкладайтеся в моніторинг з самого початку. Ви повинні легко і наочно бачити графік пропускної здатності, середнього часу обробки, поточний розмір черги, час з останнього повідомлення з розбивкою по чергах.



Ми використовуємо моніторинг не тільки в бойовому оточенні, але й у тестовому, і навіть на машинах розробників. Правильно налаштовані графіки та повідомлення вельми корисні при налагодженні і попередньому навантажувальному тестуванні.

2. Тестуйте
Системи обробки повідомлень — ідеальний полігон для автоматизованого тестування. Протокол вхідних даних визначений і обмежений, жодних взаємодій з живими людьми. Покривайте код модульними тестами. Передбачте можливість замінити бойові черзі на тестові черзі в локальній пам'яті і робіть швидкі тести взаємодії обробників. Нарешті, робіть повноцінні інтеграційні тести, які можна ганяти в бета (staging) оточенні (а краще і в продукції).

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

Цей же або подібний механізм можна використовувати для зберігання повідомлень, які повинні бути оброблені не раніше якогось моменту в майбутньому. Ми тримаємо їх у відстійнику і періодично перевіряємо, чи не настав годину Ч.

4. Автоматизуйте розгортання
Установка і оновлення системи повинна відбуватися в один або кілька кліків. Прагніть до частих оновлень на продукції, в ідеалі — автоматичного розгортання після кожного коміта в master гілку. Інсталяційний скрипт допоможе розробникам підтримувати їх особисту середовище, а також середовища тестування в актуальному стані.

Замість висновку

Хороша зрозуміла архітектура — це ще і спосіб спрощення комунікації розробників, їх спільне бачення і набір понять. В цьому сенсі нам дуже допомогло формулювання метафори системи у вигляді картинки, з якої можна починати багато обговорення в проекті.

Наша метафора схожа на ось цю картинку з статті дядечка Боба The Clean Architecture:



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

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

0 коментарів

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