Наш досвід створення програми на микросервисах в сфері рекламних технологій

У серпні 2015 року ми запустили новий adtech-проект — Atuko.
Це система керування мобільною рекламою, орієнтована на професіоналів.


У Atuko ми сфокусувалися на управлінні одним каналом трафіку — myTarget, основний рекламною системою Mail.ru, що об'єднала в собі рекламу на Однокласниках, мобільному VK та деяких інших ресурсах Mail.ru і охоплює >90% аудиторії Рунета. І природно рекламодавцям, потрібні інструменти створення кампаній, аналізу результатів і управління.


Хочемо розповісти, як саме ми підійшли до створення цих інструментів та архітектури системи.

Для нашої команди це не перший проект в області рекламних технологій. Ми займалися розробкою систем управління рекламою з 2009 року, створюючи інструменти для Яндекс.Директ, Google Adwords, Google Analytics, VK, Target@mail.ru та інших каналів. Застали навіть Begun і часи, коли він був актуальний :)



За цей час ми зіткнулися з безліччю підводних каменів і несподіванок, пов'язаних з особливістю роботи рекламних майданчиків, їх API, так і незвичайних задач самих рекламодавців — і встигли накопичити чимало досвіду! Про все розповісти в одній статті не вийде, так що, якщо буде інтерес, ми напишемо серію статей, в яких постараюся поділитися отриманими знаннями.

У цій статті я хочу розповісти ключові речі про архітектуру та інфраструктуру Atuko — і чому ми зробили саме так, а не інакше.

Минулий досвід і важливі уроки
З нашого минулого досвіду ми, серед іншого, винесли наступні важливі уроки:
  • Необхідно гнучке масштабування у всіх вузлах системи. Не можна заздалегідь передбачити, на яку частину системи зросте навантаження: аналіз, створення, перегляд і т. д.
    Наведу невеликий приклад. При управлінні контекстною рекламою (Яндекс.Директ і Google AdWords) спочатку все йшло добре, зростання був плавним. В якийсь момент з'являється дійсно великий клієнт — і для реалізації його завдань потрібно управляти 9 млн ключових слів — і це в рази більше, ніж інші клієнти, разом узяті. Деякі частини системи (наприклад, отримання конверсій з Google Analytics) спокійно впоралися зі збільшенням навантаження, але інші (наприклад, отримання статистики за ключовими словами) вимагав сильної оптимізації. А найнеприємніше, що обсяги цього клієнта позначилися на роботі всієї системи в цілому — і, відповідно, на інших клієнтів.
    Це навчило нас ізолювання і гнучкості окремих частин системи, і можливості ізолювати клієнтів один від одного.
  • Необхідна налаштування функціональності під конкретного клієнта.
    Часто у окремих клієнтів є свої унікальні вимоги, і поєднати завдання різних клієнтів в одному універсальному вирішенні буває важко або неможливо — і більш ефективним рішенням виявляється кастомізація функціоналу для конкретного клієнта. При цьому, зрозуміло, ця кастомізація не повинна торкнутися інших користувачів.
    Таким чином, потрібна можливість запускати вдосконалену функціональність для окремих клієнтів, і при цьому — не піднімати окрему копію всієї системи для кожного клієнта.
  • Мінімізація залежності від фреймворків та мов програмування.
    Наприклад, один із створених нами проектів існує довше, ніж фреймворк, на якому він побудований — підтримка і розвиток фреймворку зупинилися. Крім того, зав'язка всього проекту на одну мову програмування знижує ефективність — немає можливості використовувати оптимальний мова для кожного завдання, і немає можливості використовувати нові мови програмування.
Тепер я розповім, як ми постаралися передбачити ці моменти в архітектурі Atuko.

Загальну схему проекту можна зобразити так:


Микросервисы
Перш за все, ми вирішили все будувати на микросервисах. Кожен микросервис надає HTTP API і може бути реалізований на будь-якому стеку техологий. Це дає нам можливість масштабувати кожен сервіс непомітно для інших, так як за HTTP API може ховатися як одна копія сервісу, так і цілий кластер зі своїм балансировщиком.

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

Один із частих питань, що виникають при використанні микросервисов — як ділити функціональність на микросервисы. Для себе ми вирішили, що ми виділяємо в окремі микросервисы деякий функціональний блок, який ділити на дрібніші частини не має сенсу.
Наведу приклад: у Atuko є можливість створити на рекламному майданчику велика кількість рекламних кампаній і оголошень, завантаживши спеціальним чином сформований Excel-файл. Крім інтерфейсу, у цьому процесі задіяні три микросервиса:
  • розбір Excel файлу і створення набору даних для завантаження в myTarget
  • перевірка набору даних на відповідність правилами рекламної площадки
  • надсилання даних в myTarget


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

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

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

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

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

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

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

При цьому деякі речі ніколи не будуть продубльовані в різних юнітів, наприклад, взаємодію із зовнішніми системами. У разі Atuko, наприклад, є ліміти використання API myTarget — і тому зовнішня комунікація йде через один микросервис, контролює частоту запитів.

До речі, всередині юніта ми можемо змінювати не тільки роботу окремих сервісів, але і кількість сервісів — наприклад, додаючи нові кроки в процедуру обробки або перевірки даних.

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


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

Docker

В цілому описаний вище підхід дозволив нам підготуватися до можливих труднощів. Але виникла й інша задача, пов'язана не стільки з процесом розробки, скільки з процесом експлуатації. Як всім цим господарством керувати? Кожен микросервис може бути реалізований з використанням будь-яких технологій, на якому фреймворку і мати свої залежно від різних бібліотек. Наприклад, на даний момент у нас є микросервисы і на golang, і на python, і на php.

І для вирішення цієї задачі ми використовуємо Docker. На основі кожного микросервиса створюється Docker-образ (image), на базі якого вже може бути запущено необмежену кількість сервісів. При цьому вони можуть розташовуватися на різних машинах, що теж спрощує масштабування.

Reverse proxy
Всі звернення йдуть через reverse proxy. Це дозволяє при піднятті ще одного контейнера з сервісом просто додати потрібну запис в потрібний upstream, і reverse proxy сам розподілить трафік.

Як reverse proxy ми зараз використовуємо nginx — але продовжуємо розглядати й інші варіанти.

Крім того, для деплоя ми використовуємо техніку Blue-Green Deployment — це означає, що одночасно можуть працювати сервіси як з новим, так і зі старою функціональністю. І в цьому випадку reverse proxy знову ж виручає, надаючи можливість розподіляти трафік в потрібних пропорціях між двома версіями, і остаточно переходити на нову, тільки переконавшись в її повної працездатності.

DNS
Коли ми стартували розробку, Docker не мав достатньо розвинених можливостей по організації мережі. При цьому ми хотіли зручного звернення до микросервисам, недоступності бек-ендом ззовні, і винесення кожного юніта в свою підмережа.

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

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

Висновок
Хочу сказати про результати застосування такого підходу.

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

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

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

Ми ж, якщо буде інтерес, готові і далі ділитися тими знаннями, які придбали за цей час: про микросервисах на golang, моніторингу та тестуванні, інтерфейс на базі ReactJS + Flux і в чому іншому.

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

0 коментарів

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