Перейти до основного вмісту
megapromotingПоговорімо

Експертиза · Складні архітектури

Кілька систем, які мають працювати разом, зокрема коли одна з них падає.

Ми проєктуємо та експлуатуємо платформи з кількох сервісів, із message bus, обмеженими retry, idempotency keys, circuit breakers і публікацією з автоматичним відкатом. Кожен шаблон нижче працює в окремій системі, а не в діаграмі.

Вже збудованоМи експлуатуємо чотири власні платформи з різними архітектурами й можемо відкрити всі. Цифри виміряні сьогодні командами, а не взяті з документації: aichat має 28 службових каталогів, 235 моделей даних на чотирьох схемах і шину повідомлень із 297 чергами та 17.525 зв’язками; Kallina має 472 edge-функції, 559 міграцій і 28 запланованих завдань у базі; MEGA CRM має backend на 9.516 рядків із 99 маршрутами та 179 політиками доступу в рядку; Taskin виконує 21 заплановане завдання без жодного Docker. Застереження, яке ми говоримо: у двох місцях власна документація відстала від коду — твердження про кількість рядків було застарілим на 16%, а діаграма інфраструктури описувала сервер, на якому ми вже нічого не запускаємо. Ми використали вимірювання, а не документ.

«Складна архітектура» не означає багато коробок на кресленні. Це означає, що ви знаєте, що станеться, коли одна з них не відповість. Система з трьома сервісами, які викликають один одного синхронно, без ліміту повторних спроб і без idempotence, крихкіша за моноліт — тому що кожен новий зв’язок є новим способом збою, а способи збою множаться швидше, ніж функціональності.

Наша робота починається з розділення відповідальностей і завершується там, де видно, коли щось ламається. В одній із власних платформ повідомлення не переходять прямо між сервісами, а проходять через шину з 297 чергами, 9 обмінниками і 17.525 зв’язками, з яких 278 черг генеруються на клієнта, а не пишуться вручну. Вісім черг мають dead-letter exchange, а вісім — час життя повідомлення п’ять хвилин — тобто повідомлення, яке не може бути оброблене, потрапляє туди, де його можна побачити, а не зникає.

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

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

Що входить

Робота, за складовими

Ми розділяємо сервіси за тим, що може зламатися незалежно

В одній власній платформі є 28 службових директорій, з яких 16 мають власну точку входу, а в production працюють п’ять процесів під process manager. Кожен канал комунікації має власний сервіс і власний порт саме для того, щоб збій в одному не зупинив решту. Дані змодельовані на чотирьох окремих схемах, загалом 235 моделей — розділення є не лише на рівні процесу, а й на рівні схеми.

Ми ставимо магістраль повідомлень, а не ланцюг синхронних викликів

297 черг, 9 обмінів, 17.525 зв’язків. Назва черг є окремою для кожного клієнта і, в деяких випадках, для кожного потоку дискусії — тобто топологія генерується, а не пишеться вручну. Є також обмін із відкладеною доставкою для речей, які мають статися пізніше, не зараз. Вісім черг мають налаштований обмін мертвими листами, а вісім — час життя повідомлення п’ять хвилин.

Повторні спроби з обмеженням, а не без кінця

Три різні обмеження для трьох різних ситуацій, усе записано в коді: п’ять повторних спроб для запланованих повідомлень, з лічильником, що зберігається, а не тримається в пам’яті; три повторні спроби на рівні черги, пораховані в заголовку повідомлення; і три повторні спроби зі зростанням паузи за експонентою для надсилання e-mail, двома шляхами постачальника. Повторна спроба без обмеження — це не стійкість, а цикл.

Ключі ідемпотентності, щоб друге доставлення нічого не зламало

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

Переривник кола на крихкій інтеграції

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

Ворота споживання перед діями, що коштують

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

Обмеження швидкості, що переживає перезапуск

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

Спостережуваність, що розрізняє «відповів» і «спрацював»

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

Публікація з перевіркою та автоматичним поверненням

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

Як це виглядає

Шлях, крок за кроком.

01

Малюємо карту режимів збою, а не коробок

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

02

Ми налагоджуємо магістраль і контракти повідомлень

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

03

Ми додаємо ворота: rate, consumption, вимикач

Обмеження rate на ковзному вікні в основі, consumption gate перед діями, що коштують, і circuit breaker на інтеграціях поза нашим контролем. Ми постачаємо: налаштовані пороги, чітко повернуті причини та тести для кожного рівня відмови.

04

Ми будуємо публікацію та повернення

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

05

Ми передаємо з документацією, перевіреною щодо коду

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

DocumenteРозмовиДжерелаСинтезДіяКомандаКонтекстуалізуєАвторизовані джерела. Перевірені дії.
Системи не викликають одна одну ланцюгом: між ними стоїть шина з чергами на клієнта, окремими обмінниками за типом трафіку і мертвими листами для того, що не можна обробити. Навколо неї — шлюзи: обмеження частоти, шлюз споживання, переривник кола — і проби здоров'я, одна поверхнева і одна глибока.

Дані

Чого торкаємося, де воно лежить і скільки лишається

Запитання, які ставить кожен, у кого є відповідальний за захист даних, — поставлені тут раніше, ніж їх поставить він.

Де зберігаються дані, по платформах
Немає єдиної відповіді, і це добре: одна платформа використовує MySQL на власному сервері, дві використовують PostgreSQL через Supabase — одна розміщена, одна встановлена на нашій інфраструктурі — а одна зберігає вміст як статичні файли, без бази даних узагалі. Вибір робиться за вимогами, а не за звичкою.
Доступ застосовується в базі, а не лише в застосунку
В одній нашій внутрішній системі безпека на рівні рядка увімкнена на 43 таблицях, через 90 інструкцій, із 179 написаними політиками. Правило, якого ми дотримуємося: якщо виклик обходить застосунок і напряму звертається до бази, він усе одно не повинен бачити чужі рядки.
Реєстр споживання
Один рядок на подію з типом, постачальником, окремими кількостями за модальністю — секунди розмови, вхідні токени, вихідні токени, символи — плюс вартість у валюті з шістьма десятковими знаками та списані кредити. З нього виходять денний ліміт витрат, вартість на виклик і проєкція споживання.
Міграції — це історія, а не документація
559 міграцій на одній платформі, 80 — на іншій. Схема змінюється через версіоновані міграції, тож стан бази можна відтворити й читати хронологічно. Коли документ і міграція не збігаються, має рацію міграція.
Заплановані завдання зберігаються в базі або в cron, а не в процесі
28 запланованих завдань запускаються всередині бази даних в одній платформі; в іншій, 21 завдання запускаються через системний cron. Причина записана у файлі: таймери всередині служби скидаються при кожному перезапуску, тож цикл на три години з перевидаваної служби ніколи не спрацьовує.

Випадок

Топологія повідомлень, яка самостійно генерується для кожного клієнта

Ситуація

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

Що ми збудували

Топологія генерується для кожного клієнта, а не пишеться вручну: з 297 черг 278 — це типу «сповіщення для певного облікового запису», а деякі опускаються аж до рівня однієї гілки обговорення. Над ними дев’ять обмінів розділяють типи трафіку — сповіщення, токени, webhookи, канали — і є обмін із відкладеною доставкою для того, що має статися пізніше. Вісім черг мають обмін мертвих листів, вісім мають час життя повідомлення п’ять хвилин. Правила оркестрації можна гаряче перезавантажити через канал публікації, без перезапуску споживачів.

Що вийшло

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

Чого цей випадок не каже

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

Питання

Про що нас питають, перш ніж зателефонувати

Звідки я знаю, що ви не продаєте мені складність, яка мені не потрібна?

Тому що перша рекомендація, яку ми часто даємо, — не розділяти. Кожна нова служба — це новий режим відмови, а режими відмови множаться швидше, ніж функціональності. Один приклад із наших власних систем: backend однієї з них — це один файл із 9.516 рядками та 99 маршрутами. Це не елегантно, і ми це кажемо, але він публікується в один крок і налагоджується в одному місці. Розділення роблять тоді, коли є вимірювана причина — служба, яку треба масштабувати окремо, окрема команда, окремий ритм випуску.

Що відбувається, коли система в ланцюгу не відповідає?

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

Як ви уникаєте того, щоб одна й та сама подія оброблялася двічі?

За допомогою ключа ідемпотентності, а не перевірки «я це вже бачив?», яка має гонку даних. Конкретно: ми вставляємо рядок з ідентифікатором події і, якщо база відхиляє через порушення унікальності, трактуємо цей код помилки як сигнал дубліката, а не як несправність. Для платежів позначка стоїть на транзакції кредитування, тож повторне надсилання того самого webhook не кредитує двічі.

Ви використовуєте Docker чи ні?

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

Як ви знаєте, що заплановане завдання справді було виконане?

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

Наскільки важлива абстракція по постачальнику?

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

Ваша документація актуальна?

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

Що ви не робите?

Ми не обіцяємо цілі доступності без вимірювання — відсоток на кшталт «99,9%» потребує польових даних за певний період, а там, де їх немає, ми цього не стверджуємо. Ми не проєктуємо системи, які не можемо обслуговувати або передати: якщо результат — архітектура, яку ваша команда не може підтримувати, це неправильна архітектура. І ми не робимо сертифікації відповідності — ми можемо побудувати контроль, але не можемо видати сертифікат.

На чому ґрунтуються твердження вище (23 джерел)

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

Що вам хотілося б налагодити?

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

Поговорімо