Наша команда розробляє бекенд-систему для обробки повідомлень від мобільних пристроїв. Пристрої збирають інформацію про роботу складної техніки і надсилають повідомлення в центр обробки. У цій статті я хочу поділитися підходами до побудови подібних систем. Ідеї досить загальні, їх можна застосовувати для будь-якої системи з наступною архітектурою:
- Визначаймося з цінностями
- 1. Простота створення, зміни та підтримки
- 2. Відмовлення
- 3. Продуктивність
- Налаштовуємо мозок
- 1. Оперуйте чергами та асинхронними обробниками
- 2. Розбивайте обробку на декілька етапів
- 3. Не змішуйте декодування та обробку
- 4. Зробіть зміну налаштувань черг легким
- 5. Обробляйте повідомлення групами
- Створюємо інструменти
- 1. Реалізуйте тотальне спостереження
- 2. Тестуйте все
- 3. Заведіть відстійник для помилкових повідомлень
- 4. Автоматизуйте розгортання
- Замість ув'язнення
Каналами зв'язку пристрої надсилають повідомлення на наш шлюз (gateway) - вхідну точку програми. Завдання програми - розібратися, що саме прийшло, зробити необхідні дії і зберегти інформацію в базі даних для подальшого аналізу. Базу ми будемо розглядати як кінцеву точку обробки. Звучить просто, але зі зростанням кількості і різноманітності повідомлень з'являється кілька нюансів, які я і хочу обговорити.
Трохи про рівень навантаження. Наша система обробляє повідомлення від десятків тисяч пристроїв, при цьому за секунду в середньому ми отримуємо від декількох сотень до тисячі повідомлень. Якщо ваші числа відрізняються на пару порядків в ту чи іншу сторону, можливо, набір ваших проблем і підходів до їх вирішення буде іншим.
Крім числа повідомлень на секунду, існує проблема їх нерівномірного отримання. Додаток має бути готовий до коротких піків навантаження в десятки разів вище середнього. Для вирішення цього завдання система організовується у вигляді набору черг та їх обробників.
Приймальний шлюз не робить ніякої реальної роботи - він просто отримує повідомлення від клієнта і поміщає його в чергу. Це дуже дешева операція, тому шлюз здатний отримувати величезну кількість повідомлень в секунду. Потім, існує окремий процес, який отримує з черги кілька повідомлень - рівно стільки, скільки він хоче і може - і робить важку роботу. Обробка виходить асинхронною, а навантаження - стабільно обмеженим. Можливо, в піку злегка виросте час перебування повідомлень у черзі, ось і все.
Найчастіше, обробка повідомлення нетривіальна і складається з декількох дій. Наступний логічний крок - розбити роботу на кілька етапів: кілька черг і обробників. При цьому фізично різні черги і обробники можуть розташовуватися на різних серверах, кожен з яких можна налаштовувати і масштабувати під його конкретне завдання:
Перша черга містить повідомлення в тому вигляді, в якому вони надійшли від пристрою. Обробник декодує їх і поміщає в другу чергу. Другий обробник може, наприклад, виробляти якусь агрегацію і створювати інформацію, цікаву для бізнесу, а третій обробник - зберігати її в базу даних.
Такий базовий розклад, про що ж ще треба подумати?
Визначаймося з цінностями
1. Простота створення, зміни та підтримки
Асинхронний розподіл повідомлень привносить в програмний продукт додаткову складність. Ми постійно працюємо над зниженням цієї ціни. Код оптимізується, в першу чергу, у бік підвищення читаності, зрозумілості для всіх членів команди, простоти зміни і підтримки. Якщо в підсумку ніхто крім автора не зможе розібратися в коді, ніяка чудова архітектура не допоможе зробити команду щасливою.
Теза здається простою, але нам знадобилося досить багато часу, перш ніж ми не просто озвучили цей принцип, але і стали стабільно і постійно застосовувати її в щоденній роботі. Ми намагаємося постійно робити рефакторинг, якщо відчуваємо, що код можна зробити трохи краще і простіше. Всі вихідники проходять рев'ю, а найбільш критичні частини зазвичай розробляються в парі.
2. Відмовлення
Має сенс одразу визначитися щодо вимог до здатності системи продовжувати функціонувати при виникненні відмов обладнання та підсистем. У всіх вони будуть різними. Можливо, хтось готовий просто викинути всі повідомлення за ті 5 хвилин, що один із серверів перезавантажується.
У нашій системі ми не хочемо втрачати повідомлення. Якщо якийсь сервіс недоступний, виклик до бази закінчується таймаутом, або відбувається випадкова помилка в обробці, це не повинно закінчитися втратою інформації від пристроїв. Залежні повідомлення повинні зберегтися в черзі і будуть оброблені відразу після усунення проблеми.
Припустимо, ваш код на одному сервері синхронно викликає веб сервіс на іншому сервері. Якщо другий сервер недоступний, обробка закінчиться помилкою, а ви зможете хіба що залогувати її. При асинхронній обробці повідомлення буде чекати, коли другий сервер повернеться в робочий стан.
3. Продуктивність
Кількість повідомлень, що обробляються в одиницю часу, затримки, навантаження на сервери - все це важливі параметри продуктивності системи. Саме тому ми закладаємо в проект гнучку архітектуру.
Однак, не зациклюйтеся на оптимізації з самого початку. Зазвичай переважна частина проблем з продуктивністю створюється відносно невеликими шматками коду. На жаль, мало хто здатен заздалегідь передбачити де саме будуть ці проблеми. Ось тут люди пишуть цілі книги про передчасну оптимізацію. Переконайтеся, що ваша архітектура дозволяє швидко налаштовувати систему і забудьте про оптимізацію до перших навантажувальних тестів.
Водночас, навантажувальні тести потрібно починати робити рано, а потім зробити їх частиною стандартної процедури тестування. І тільки тоді, коли тести покажуть конкретну проблему, беріться за оптимізацію.
Налаштовуємо мозок
1. Оперуйте чергами та асинхронними обробниками
Про це я вже писав вище. Наш основний інструментарій - черги та їх обробники. На додаток до класичного стилю організації коду «отримав запит, викликав віддалений код, дочекався відповіді, повернув відповідь нагору», ми отримуємо підхід «отримав повідомлення з черги, відпрацював, відправив повідомлення в іншу чергу». Від того, наскільки вдалий баланс у поєднанні двох підходів ви знайдете, залежить як масштабованість, так і простота розробки системи.
2. Розбивайте обробку на декілька етапів
Якщо обробка повідомлення досить складна і може бути розбита на кілька етапів, передбачте кілька черг і обробників. Майте на увазі, що зайва фрагментація може зробити систему більш складною для розуміння. Тут потрібен баланс. Досить часто існує розбиття, природне і зрозуміле для розробників. Якщо ні, спробуйте подумати про точки відмови. Якщо обробник може завершитися помилкою з кількох незалежних причин, можна подумати про його розбиття.
3. Не змішуйте декодування та обробку
Зазвичай, повідомлення приходить у форматі якогось протоколу взаємодії пристроїв у мережі: бінарний, xml, json тощо. Декодуйте і переводьте їх у свій внутрішній формат якомога раніше. Це дозволить вирішити мінімум два завдання. По-перше, протоколів може бути кілька; після декодування ви зможете уніфікувати формат подальших повідомлень. По-друге, спрощується логування і налагодження.
4. Зробіть зміну налаштувань черг легким
Структуруйте код обробки таким чином, щоб ви могли легко змінити конфігурацію черг. Розбиття обробника на два не повинно призводити до хмари рефакторингу. Не дозволяйте вашому коду залежати занадто сильно від конкретної реалізації черг, завтра ви можете захотіти змінити її.
5. Обробляйте повідомлення групами
Найчастіше має сенс отримувати повідомлення з черги не по одному, а відразу групами. Використовувані вами сервіси можуть приймати масив даних для пакетної обробки, в такому випадку один великий виклик зазвичай буде набагато ефективніше ста маленьких. Вставлення сотні записів до бази за один раз буде швидшим за сто віддалених викликів.
Створюємо інструменти
1. Реалізуйте тотальне спостереження
Вкладайтеся в моніторинг з самого початку. Ви повинні легко і наочно бачити графік пропускної здатності, середнього часу обробки, поточний розмір черги, час з останнього повідомлення з розбивкою по чергах.
Ми використовуємо моніторинг не тільки в бойовому оточенні, але і в тестовому, і навіть на машинах розробників. Правильно налаштовані графіки та повідомлення досить корисні при зневадці та попередньому навантажувальному тестуванні.
2. Тестуйте все
Системи обробки повідомлень - ідеальний полігон для автоматизованого тестування. Протокол вхідних даних визначений і обмежений, ніяких взаємодій з живими людьми. Покривайте код модульними тестами. Передбачте можливість замінити бойові черги на тестові черги в локальній пам'яті і робіть швидкі тести взаємодії обробників. Нарешті, робіть повноцінні інтеграційні тести, які можна ганяти в бета (staging) оточенні (а краще і в продукції).
3. Заведіть відстійник для помилкових повідомлень
Найчастіше, ви не захочете, щоб помилка обробки одного повідомлення зупинила всю чергу. Не менш важлива можливість діагностувати помилку. Поміщайте такі повідомлення в спеціальне сховище і націльте на це сховище всі свої прожектори. Передбачте можливість з легкістю перемістити повідомлення назад в чергу обробки як тільки причина помилки усунена.
Цей же або схожий механізм можна використовувати для зберігання повідомлень, які повинні бути оброблені не раніше якогось моменту в майбутньому. Ми тримаємо їх у відстійнику і періодично перевіряємо, чи не настала година Ч.
4. Автоматизуйте розгортання
Встановлення та оновлення системи має відбуватися в один або кілька кліків. Прагніть до частих оновлень на продукції, в ідеалі - автоматичного розгортання після кожної кімміти в master гілку. Установчий скрипт допоможе розробникам підтримувати їх особисте середовище, а також середовища тестування в актуальному стані.
Замість ув'язнення
Гарна зрозуміла архітектура - це ще й спосіб спрощення комунікації розробників, їхнє спільне бачення і набір понять. У цьому сенсі нам дуже допомогло формулювання метафори системи у вигляді картинки, з якої можна починати багато обговорення в проекті.
Наша метафора схожа на ось цю картинку зі статті дядечка Боба The Clean Architecture:
На нашій схемі ми позначаємо сутності системи і їх залежності, що допомагає в дискусії наблизитися до правильного дизайну, знайти помилки або запланувати рефакторинг.
