Від оркестрування до хореографії та назад [ukr]
Презентація доповіді
Це історія про десятирічний шлях еволюції архітектури високонавантаженого конвеєра обробки даних, через який проходить 500 мільйонів повідомлень щодня, від дизайну на основі хореографії до повністю централізованої оркестрації та про важливі уроки, отримані на цьому шляху.
Транскрипція доповіді
Привіт всім, добрий ранок. Сподіваюсь, що ранкова кава вже вас розбудила. Проснулись? Гарно. Надіюсь, що технічна інформація не перевантажила ваш мозок і буде цікавою. Дозвольте розповісти трошки про себе. Зараз я працюю на посаді Staff Engineer в компанії Letyshops. До цього я був технічним лідером і software architect в стартапі Yuskan, про який я розповім пізніше. Також я займався та продовжую займатися розробкою відкритих проектів. Ви можете знайти мене на Github чи Twitter за ім'ям "Євген". Тепер трошки про компанію Yuskan.
Yuskan – це відомий український стартап, який розробляє онлайн-сервіс для аналізу та моніторингу соціальних мереж. Сервіс агрегує дані з публічних мереж, таких як Facebook і Twitter. Кількість даних, яку Yuskan обробляє, вражає – більше півмільйона різноманітних джерел та півмільярда повідомлень кожен день. Yuskan аналізує та обробляє кожне повідомлення, визначаючи його тональність, розпізнаючи об'єкти на картинках та застосовуючи багато інших алгоритмів машинного навчання та обробки природної мови. Пайплайн обробки повідомлень, що використовує Yuskan, схожий на конвеєр на фабриці, де кожен етап додає або модифікує дані. Сьогодні я розкажу про функціональність та патерни побудови такого пайплайну, інструменти, які можна використовувати, і проблеми, які виникають при його керуванні.
Для цього я розповім історію еволюції пайплайна в U-Scan та проблеми, з якими ми стикалися. Окей, рушаємо далі. Трошки звернемося від історії і говоримо про те, що таке високоефективний пайплайн. Обробка півмільярда повідомлень в день вимагає більш складного підходу, ніж проста послідовність кроків. Пайплайни можуть бути синхронними, де кожен крок передає дані наступному і чекає на його завершення. Проте це не є ефективним, оскільки пропускна здатність такого пайплайна низька.
Більш поширеним є використання асинхронних пайплайнів, де кожен крок виконується незалежно від інших, поки попередній може приймати нові завдання. Це підвищує пропускну здатність через незалежне виконання кроків. Ще однією популярною формою асинхронних пайплайнів є розподілені пайплайни, де кроки можуть бути розташовані на різних машинах та взаємодіяти через буфер чи чергу. Розглянемо основні характеристики ефективного пайплайна. Пропускна здатність та низька затримка проходження повідомлень від початку до кінця є важливими. Також важливо мати можливість блокувати паралелізму, щоб уникнути перенавантаження.
Контроль паралелізму на кожному етапі, пакетна обробка для оптимізації роботи сервісів, локальний буфер та гнучка маршрутизація повідомлень – це також ключові характеристики. Тепер про інструменти. На ринку існує багато різноманітних інструментів, таких як Apache Nifi та інші. Однак, на початку нашої роботи в 2013 році таких інструментів для .NET було обмаль. Ми використали DataFlow (раніше відомий як TPL DataFlow) для побудови пайплайну. Це дозволило нам легко створювати асинхронні обробники та ланцюги їх зв'язку. Згодом виникли більш потужні інструменти, такі як Apache Nifi, але ми інтегрували DataFlow вже на той час. Загалом, важливо обирати інструменти відповідно до потреб та характеристик системи.
І при цьому там вказувати якісь умови маршрутизації. Ну, достатньо. Така собі відома і дуже популярна бібліотека в .NET-і. В принципі, із коробки вона вирішує багато того, про що ми говорили. Тобто там є можливість лімітувати розміри буферів в P-плані. Таким чином робити бекпреша. Вона достатньо... За рахунок того, що вона низькорівнева, вона дає достатньо велику пропускну здатність. Низьку затримку. Ну, робить... Там із коробки є batching. І... Таке різне. Окей. З якою проблемою ми столкнулися? Колись ми... Декілька років після того, як ми створили цей P-план наш базовий, у нас стала така проблема. Треба було змінити архітектуру. Треба було додавати нову функціональність. При нас повстав вибір. Як це зробити? Тобто, або ми нову функціональність розширюємо, існуючий P-план, існуючий сервіс, в якому нас... Цей P-план містився. Або ми розділяємо і робимо новий сервіс. Тобто, як насправді зробити краще?
Ми почали думати. А думати... Ми почали в термінах того, а які є фактори взагалі для дезінтеграції цих сервісів. Ніл Форд, який, до речі, виступав на онлайн-конференції, в своїй презентації архітектурної згідно з інформацією, з Hard Parts, виділяє п'ять основних факторів розподілення на сервіси. В нашому випадку це було 4 з 5. Тобто, це була одна функціональність. Там був достатньо волатільний код. Тобто, змінювався постійно, незалежно від основного P-плана. В нього були зовсім інші вимоги до масштабованості. Ну, і від... Старий сервіс там був трошечки ненадійний. Тому ми вирішили, що це дуже гарний набір факторів, для якого ми зможемо зараз... Тобто, для вибору окремого сервісу. Окей.
Як це робити? Була така колись класна книжка. Вона називає супер-біблею для розробки систем обміну повідомлень. Хтось таку книжку бачив колись? О-о-о! Уклон, привіт! Я знаю, що у вас ця вся книжка повністю, в повному мірі використовується. Добре. Ось ми подивилися цю книжку. І там перший паттерн, який є, це Pipes and Filters. Ну, я думаю, що вам всім він відомий. Він відомий всім по філософії Unix. Коли у нас є пайпи, ми щось там кудись передаємо. Дані просто йдуть з одного пайплу в інший пайпл. Окей.
Наступний паттерн — це Content Enricher. Тобто, це про... То, що ми... Якби назначаємо компонент, це його роль. Він займається збагаченням даних. Тобто, до нього поступають дані, він їх якимось чином збагачує, передає далі. Наступний паттерн — це Document Messages. Це те, що збагачує Content Enricher. Це такий як єдина канонічна модель даних, яка курсує між компонентами, і кожен з неї щось може робити. І останні — це, звичайно, як це комунікується. Ну... Це такий собі звичайний канал точка-точка, де в нас просто є якась черга і гомогенна черга, в яку складається повідомлення, і потім з неї забираються.
Ось так ми і зробили. Тобто, так виглядала архітектура після створення нового сервісу. В цьому новому сервісі хвасталась новачка кількість кроків нових, які були там притаманні для цього сервісу. Але трошечки... І з'явилася ще одна умова. Тобто, якщо у нас, наприклад, повідомлення там вже старе, ніж 30 днів, то ми не мали роутити, тобто не мали його відсилати на новий сервіс, а мали його там десь скіпати. Ну, окей. Наче все нормально. Потім виникла потреба зробити нову функціональність теж. Хтось подивився такий і думає, ага, ну, типу, архітектура понятна, додам-ка я теж новий сервіс. Додав новий сервіс — з'явились нові кроки.
А... Потім... З'явився ще один сервіс для роспізнаних картинок. Ну, подивилось — архітектура наша теж сама. Теж додали таку ж саму... Також самий дизайн зробили і з новим сервісом. Та там теж з'явилась невеличка умова. Потім додався ще один сервіс. Архітектура трошечки ускладнилась. Але в цілому все було консистентно в рамках первоначальної концепції. Там теж з'явилась своя кількість кроків. І теж з'явилась своя умова. Потім трошки ускладнилася маршрутизація. І там теж з'явились умови. А потім це вигляділо отак. Та це такий собі, я називаю, мікромес.
Архітектура, яка була повністю... Яка навіть в діаграмі виглядала дуже монструозно. Окей. Проблема зрозуміла. Що з цим месом в нас? Які були проблеми? Ну, по-перше, ця проблема була... Я називаю її підтримка діаграми. Тобто в діаграмі було дуже важко підтримувати. Ну, в принципі, архітектура була незрозуміла. Тому для того, щоб якось це зробити нормальним, у нас була велика діаграма, на якій було все розмальовано. Як і всі діаграми, коли щось додавали у системі. Ну, тобто в нас ніхто її не оновлював. І вона дуже швидко застарівала і була неактуальна. Потім... Але мені здається, що найголовніша проблема була все це діаграми. От так.
Головна проблема з діаграми була в тому, що коли я на онбордингу її показував новачкам, то вони, звичайно, робили отакі глаза. Та казали, ого, а чому так все складно? А можна тут якось простіше було б зробити? Казав, оце ж зрозуміло. Гляньте, по діаграмі йде повідомлення. Ось так, ось так і ось так. Ну, що ж тут непонятного? Словна проблема була якраз в тому, що це було непонятно. Тобто за 36 років, де це повідомлення курсує по цьому архітектурі, було дуже складно. Треба було робити якісь там корреляціони, ділити кожне повідомлення, потім десь його відслідковувати в різних індексах, на еластику, там, де це логалось.
В общем, це була прямо біль. Але в нас на це було рішення дуже просте. Я називаю його племінне знання. Тобто якщо ти щось не знав, де потірялося повідомлення, тобі треба було просто, як би, вчепити найстарішого аборігена, спитати його, типу, як це. Ваня, підкажи. Ваня, привіт, між запрошенням. Підкажи, будь ласка, чому повідомлення X не йшло через сервіс N. І він міг на це собі відповісти. Єдине, що погано, що Ваня дуже погано масштабувався. Ну, це таке. Третя проблема була в тому, що була велика кількість залежностей, тобто кожен сервіс і дублювання коду, тому що кожен сервіс, він, ну, мав ті ж самий набор бібліотек для роботи з чергами, він мав ті ж самі полір в кожному сервісі був, ну, коротше, було купа дублювання коду, який був дуже схожий між сервісами.
Також дуже неприємною проблемою була проблема така, що, тому що ми використовували оцей document message, який курсував між різними сервісами, це була така канонічна схема, яку шеріли між собою всі сервіси, то кожного разу, коли ми там щось змінювали, треба було редеплоїти всі сервіси. Вони просто не розпізнавали того, що там з'явилися якісь нові дані, якщо я щось записав там десь в першому сервісі, то треба було його протягнути через всі, бо вона просто тіряється. Окей.
Ще одна із проблем була, я так називаю її, американські гірки по сіріалізації. Тобто кожного разу, коли нам треба було щось кудись далі передати, треба було це сіріалізувати, покласти в чергу, а потім знову десіріалізувати. І навіть, якщо сервіс використовував там тільки три атрибути з 200, там було десь близько 200 атрибутів було в нашому цьому сервісі, в цьому великому документі. Все рівно треба було все сіріалізувати, а потім знову десіріалізувати. Ну, називати це рішення як би економним, в мене не повертається язик. Окей. Цікаво, якщо ви знаєте про мікросервіси. Хто пише про мікросервіси взагалі? Так, я пішов, нічого собі. Я не пишу, я не знаю. Шутку.
Який в нас є дуже класний тест на мікросервіс? Це реально мікросервіс чи не мікросервіс? База даних. Що ще? Ну, тест на автономність. Тож так я чув вже, правильно? Оце десь схоже. Тест на автономність це десь близько. Тобто є такий, називаю, лакмусовий тест мікросервісів. Це тест на автономність. Наприклад, якщо у вас один сервіс лежить, то інший повинен працювати, правильно? Оце, типу, є у нас, типу, ідеальний мікросервіс. Ось. Але так ми собі насправді і думали, що якщо в нас не працює розпізнування картинок, то ми, в принципі, можемо все інше покласти в тему клієнтам і все буде класно.
А одна проблемка. Клієнтам було на це все рівно. Вони хотіли отримати повне повідомлення. Вони не хотіли бачити повідомлення, в якому є все, але картинки не розпізнані. Тому насправді в реалі було таке, що якщо в нас один сервіс лежить, то лежить абсолютно все. Окей. Здається, мені останнім цвяхом стало те, що коли в нас вже... Це у нас різні контексти. У нас був контекст Core, в якому відбувалась продуктова розробка. Тобто цей pipeline весь був, хостився. І pipeline, вибачте, і контекст, який ми називали Artificials Intellivision. В принципі, це просто якісь там ML-сервіси, Data Science нашу команду. Хтось вирішив, що буде класна ідея, насправді, це продовжити цю хореографію ще й між сервісами, між контекстами. Це у нас був мес в одному контексті, а став тепер між всіми контекстами.
Я називаю такий контекст-меседж. І ось тут вже це була остання крапля. На цьому етапі треба було нагадати собі, що архітектура це ж не тільки у нас про масштабованість, про доступність, про швидкість, про надійність. Ще є така класна штука, вибачте, така важлива штука, як мейнтенебіліті, тобто легкість підтримки коду. Якщо у нас архітектура складна, то це теж є проблема, яку потрібно вирішувати. А як її вирішувати? Що можна зробити? Якщо у вас є щось, що вам заважає, тому що у вас система розподілена, то мабуть треба її якось з'єднати і це буде рішення, яке допоможе вирішити цю проблему. Повертаючись до того ж самого Ніла Форда у своїй той же презентації з архітектурою The Hard Parts, одні з причин якраз це проблеми оркестрації та хореографії. Тобто якщо у вас є проблеми з хореографією, переходьте до оркестрування.
І тут важливо сказати, що таке оркестрування. Всі знають, що таке оркестрування? Стоїть дирижер і оркеструє. Дійсно так. Оркестрація це процес, коли в нас все, що стосується до флоу, то до потіка, воно хоститься десь в одному місці. Як це виглядало би в наші так як нам зробити з нашого недолугої архітектури перейти до оркестрації? Що справді в нас було? В нас був в кожному сервіс, були окремі сервіси, які між собою комунікували через буфер, це була якась черга. По суті кожен сервіс, він мав собі якусь частину флоу, яка контролювала маршрутизацію в середині, плюс ще виконував якусь функцію. По суті, щоб нам перейти до оркестрації треба було б примінити такий відомий принцип як separation of concerns, тобто розділити ці два концерна, щоб у нас були функції, тобто функція окрема, а флоу окрема. Окей, добре.
Що будемо робити далі? Ну от ми це розділили. Як це імплементувати? Знову повертаємось до нашої супербіблії. В супербіблії є такий паттерн, який називається message route. Тобто він каже про те, щоб ми якби определили один із компонентів як центральний і він повністю хостить весь флоу і займався тільки роутингом, більше нічого не робив. Його різновидом є інший паттерн, називається він content-based route. Тобто це маршрутизація, яка залежна від змісту того, що є в даних. Тобто ми дивимось на зміст даних і приймаємо якісь умови, куди далі це повідомлення маршрутізувати. Що в нас лишило в кінці кінців? В нас в кінці кінців вийшло два типи сервісів. Тобто в нас, я називаю їх типа pure function, тобто чиста функція, сервіс-функція. Це просто сервіс, який виконує якусь роботу, наприклад, там розпізнує картинку, або там матчить текст, або щось таке. І інший сервіс це сервіс-аркістрація, який робить тільки те, що хостить собі флоу і викликає інші сервіси.
Ось дуже, мені здається, простіше, ніж те, що виглядало до цього. Тут буде гарне питання, а як це масштабувати? Що ми ж казали про високонавантажений pipeline. В попередньому архітектурі розподіленій ми використовували дуже простий паттерн, називається competing consumers. Тобто, по суті, ми ще знаємо його як worker pattern, мовне. Тобто, в нас є одна черга, і ми просто, якщо хочемо змасштабувати її обробку, ми просто створюємо репліки нашого воркера, і він починає швидше цю чергу розгрібати. Тут ми вирішили нічого не мудрувати, і просто зробили те ж саме з нашим оркістратором.
Тобто, якщо нам треба швидше розгрібати цю чергу, наприклад, нам насипали більше краулери даних, ми просто створюємо репліки сервісу, і кожна репліка хостить собі повний воркфлоу. Тобто, ну, немає там нічого складного в цьому. Окей. Якщо б це було, було б класно, але в нас після того, як ми змарзли усе в одне місце, виявилось, що є, з'явилися нові проблеми. Цікавою властивістю попереднього розподіленого pipeline була така штука, як розподілення складності. Тобто, в цілому, що ти казав, вибачте, а, я... Було розподілено складності. Тобто, в цілому архітектура була достатньо складна, для зрозуміння, глобальна, але коли ти вже знаходишся десь всередині якогось сервера, все дуже просто. Там, ну, маленький pipeline, все зрозуміло, що до чого, все понятно. Але коли ми це все з'єднали в одне місце, ми отримали таку ковбасу із, там, 30 плюс різних кроків, які треба виконати, плюс таку достатньо приправлену великою кількістю умовних і різних кондішенів.
І якщо б це була..., насправді, можна знати, що пригадати. Тут є такий, може чули, хтось, закон збереження складності. Хто чув про таке? Не чули? Зараз спробую перевести. За законом збереження складності він каже, що будь-яка програмна система має складність, яка притамана вирішеної проблемі, яку неможливо прибрати або приховати. Тобто, якщо ви знайомі з працею Брукса «No silver bullet», там він ще й сказав про суттєву складність і додану складність. Тобто, суттєва складність це та, що притамана проблемі, а додана та, що притаманна рішенню. Тобто, по суті, ми прибрали складність притаманну рішенню, а складність притаманну проблемі, тобто складної маршрутизації, вона залишилась, як і була.
Окей, повертаємось до... Якщо б це була така проста кавбаса, ще б це було пів біди. Але, насправді, це було щось схоже на таку схему лондонського метро в мініатюрному метрі, бо там було дуже багато різних умов, по яким різні кроки десь стрибали, десь або в кінець, або десь в середину. І щоб ще це якось задебажити головою, там навіть доходило до такого, що в коді були із ASCII вибудовані маршрути, і це так було. Тобто, хотілося би, щоб ми від такого не дуже зрозумілого стрибаючого там всюди pipeline перейшли до цього більш лінійного. Але як це зробити? Чи можливо це, в принципі, спростити? Заглядаємо в супербіблію Interest Initiation Patterns. Там є такий паттерн, називається він Selective Consumer. Тобто, про що цей паттерн? Він каже, що замість того, щоб той, хто продюсер повідомлення вирішував, куди йому далі піти по навігації, він може просто дати меседж наступному консумеру, а консумер вже сам вирішує, чи хоче він цей меседж опрацьовувати, чи ні.
Наче проста річ, казалось би, тобто, для чого це призводить? Це призводить до того, що умовні кондішн, які були до цього на продюсері і робили оці стрибки, конвертуються, тобто, інвертуються вже в фільтри, які є, ну, фільтри, які на кожному області свої. Тобто, по суті, ми можемо отримати достатньо лінійний pipeline, в якому немає вже ніяких складних маршрутів. Тобто, ось це ми і зробили. Це те, що вийшло в кінці кінців. Тобто, ну, це, мабуть, ще дуже контекстна штука, бо в нас було дуже багато кроків, які, наприклад, виконувалось, а є картинка в повідомленні, чи немає? Немає картинки, то немає сенсу цей крок робити. Тобто, ми просто перенесли, це зробили фільтрами і просто кидали ті повідомлення по pipeline. Фільтр сам, господи, цей, вибачте, консумер, крок сам вирішував, чи треба йому це опрацювувати чи ні. Якщо ні, то просто він це передавав далі. Ось така дуже проста система. Тобто, з точки зору коду це виглядало десь, ну, приблизно отак. Давайте перейдемо відразу, просто часу у нас вже не залишається. Давайте ми перейдемо відразу до результатів.
Тобто, що ми отримали? В результаті ми отримали досить простий pipeline, який дуже легко розширювати, легкий у розумінні, щодо чого взагалі. Ми трошки знизили витрати на інфраструктуру, оскільки замість ниряння в 5 черг, ви можете уявити, що півмільярда повідомлень рухається туди-сюди в 5 черг. Це приблизно 2,5 мільярда транзакцій щодня. Вибачте за неточність. Тобто, ми трошки знизили витрати, бо ми вже маємо одну чергу, і ми працюємо все в пам'яті. Не треба було ніякого утримання, отже, немає необхідності у документації або діаграмах утримання. Все зрозуміло просто з коду. Ми викинули приблизно 2000 рядків завдяки цьому, отримали позначку, що ми вже екологічно чисті продукти. Добре, давайте перейдемо до висновків. Тобто, які висновки можна зробити?
По-перше, оркестрація працює набагато краще, ніж хореографія, особливо коли це стосується одного граничного контексту. Якщо в цьому різні контексти, то використання хореографії може бути неефективним. Хореографія чудово працює, коли це є частиною конкретного потоку, але необов'язково, якщо це різні контексти. Це може призвести до проблем, які я описав раніше. По-друге, якість має значення. Якщо архітектура системи має проблеми, це так само важливо, як і інші характеристики. Якщо інженери щоденно стикаються з проблемами управління системою, це вимагає уваги.
Нарешті, вам не потрібен складний фреймворк для побудови pipeline. Хоча ви можете використовувати його, на кожній платформі вже є абстракції, які дозволяють будувати синхронні pipeline. Їх можна використовувати просто і зрозуміло. І це ще не все. Є такий феномен, який називається підтвердженням-упередженням (confirmation bias), коли люди шукають інформацію, яка підтверджує їхні гіпотези. Це може призводити до ілюзорних кореляцій, коли фокусуємося на схожостях між різними речами, ігноруючи їхні різниці. Щодо мікросервісів, якщо ви хочете їх бачити, ви їх побачите. Це все, дякую.