Тримаємо 11k req / s
Зазвичай я пишу технічні статті на Хабре , але у зв'язку з останніми подіями заграли патріотичні нотки , і я вирішив зробити виняток. На ДОУ частенько виникали срач суперечки про сумовитість проектів в аутсорс і безвихідь буття. Мені з цим завжди щастило, і я потрапляв в більш-менш цікаві проекти.
У більшості випадків бізнес-ідеї продуктів та архітектура проектів закриті NDA , тому на просторах инета важкувато знайти щось цікаве і нове , щодо архітектури якихось готових працюючих рішень . На щастя , мені вдалося вмовити нашого замовника дати добро на розголошення інформації про структуру нашого проекту (за винятком імені клієнта нашого замовника =)) .
Тому представляю Вашій увазі пост про архітектуру одного рекламного движка.
Близько 8 місяців тому до одного з клієнтів Cogniance звернулася одна досить велика й відома компанія . Назвемо її « Х». У компанії « Х» вже досить давно існує безкоштовне мобільний додаток з величезною користувача базою ( на поточний момент - 85 млн активних користувачів) . Проблема « Х» була в тому , що вони ніяк не монетизували додаток . Ну і цілком очевидно , що настав момент , коли з'явилася необхідність отримання прибутку. Який найпростіший і очевидний спосіб заробити на додатку? Правильно - банери. І , як це часто буває , «Х» захотів своє рішення зі своїм блекджек і ... ну ви зрозуміли.
Вимоги
Для нас як виконавців вимога виглядало приблизно так:
Потрібен UI , де можна конфігурувати рекламні компанії. Наприклад , показувати банер туфель дівчатам , які захоплюються музикою , старше 14 років у Каліфорнії не частіше ніж раз на день. «Х » у свою чергу смикає наші сервера з клієнтських додатків , запитуючи рекламу для конкретного користувача , передаючи всю наявну інформацію для таргетингу . + Більш-менш real time репорти з інфою про те - кому , скільки і якої реклами було віддано .
Очевидно , що це десятки сторінок спеки , стислі в 3 пропозиції . Але от віддалено все виглядало якось так . Окремі вимоги були щодо продуктивності системи :
Request rate : 1000 req/sec
Protocol : https
Response time : 99 % No downtime
Hosting : Amazon
Після 3 -х місяців розробки вимоги по навантаженню в 1000 річок/сек змінилися ( а як же без цього =)) , і з'явилася цифра в 11000 річок/cек . Через що нам згодом довелося трохи змінити вектор розвитку продукту.
Архітектура
З спеки відразу стало ясним , що систему можна умовно розділити на 3 підсистеми , що ми власне і зробили :
UI ( CRUD + інтеграція з сервісами клієнта) , AdServer ( high - load ) , Reporting ( big data ) .
Поділ на модулі на початковому етапі було необхідно як повітря :
- перше, це дозволило відразу вести розробку паралельно ;
- друге , відразу ж були максимально знижені залежності між модулями ;
- третє, підвищувалася відмовостійкість всієї системи в цілому. Так як падіння якогось вузла ніяк не впливало на роботу інших частин системи .
UI
Завдання модуля - надати зручний , дружній інтерфейс для створення рекламної компанії.
Технічна . стек : JS , Spring , Hibernate , Tomcat , MySQL.
Детальний опис UI модуля я опущу , так як в ньому все досить банально , і це звичайний CRUD модуль + інтеграція з сервісами замовника. Упевнений , що 90 % проектів робить таке ж , тільки в різних доменних областях.
Одне з найважливіших завдань , яка вирішувалася на етапі розробки UI модуля , - як поєднати UI модуль з AdServer модулем. Справа в тому , що можливих рішень було дуже багато. Ну , наприклад, це проблему можна було б вирішити через RMI , RESTfull API , JMS , звичайну master - slave реплікацію СУБД , розподілений ehCache , розподілені датагріди і ще +100500 різними способами. Особисто я зупинив свій вибір на master - slave реплікації СУБД , так як це рішення не вимагало коду і виглядало досить простим. На жаль , наш замовник також володів технічною експертизою . І після обговорення запропонованих варіантів , маючи в багажі схожі запущені проекти (як основний аргумент - це рішення вже є , і воно працює) - клієнт наполіг на Solr .
Використовуючи Solr , передбачалося вбити відразу двох зайців:
- перше, всі потрібні для доставки реклами дані будуть локально зберігатися на кожному з деливери серверів. Таким чином , підвищуємо відмовостійкість системи і зменшуємо час доступу до даних;
- друге , всю складну логіку з підбору реклами для конкретного користувача можна буде винести в Solr .
AdServer
Завдання модуля - на основі вхідного запиту та його параметрів підбирати найбільш релевантну рекламу для користувача в конкретний момент часу.
Технічна . стек : Spring , Tomcat , Solr , Redis .
Solr
Після 3 -х місяців розробки , базовий функціонал був реалізований і ми почали перші тестування навантаження . Результат виявився плачевним. Один с1.xLarge сервер зміг обробляти всього 200 реквестов в секунду при часу відповіді наближається до 100 мс.
Швидкий профайлинг відразу дозволив виявити вузьке місце - SOLR . Вся справа в тому , що SOLR - це http сервер , тому на кожен реквест від користувача доводилося робити http запит на localhost до солру . Що , звичайно ж , не могло бути дешево. На жаль , існуючий Embedded Solr працював ще гірше. Кеш на рівні додатку допоміг підняти рейт до 400 річок/сек. Але нас це теж не влаштовувало , оскільки, крім усього іншого , ми впритул підійшли до вимоги часу відповіді сервера 99 %
Index sizeЗрештою було прийнято рішення відмовитися від вибірки з солра . І вся логіка вибірки перекочувала на рівень додатки. SOLR , проте , залишився - виключно як сховище delivery індексу . Весь код роботи з солром зводився до наступного:
volatile DeliveryData cache ; Cron Job : DeliveryData tempCache = loadAllDataFromSolr (); cache = tempCache ;Як результат , швидкість роботи одного сервера виросла до 600 річок/сек c часом відповіді ~ 15ms при LA 4 .
No - SQL
В світі веб- реклами існує така фіча , як frequency capping . Якщо коротко - це ліміт показів однієї і тієї ж реклами для одного і того ж користувача . Наприклад , якщо ви не хочете , щоб користувач бачив ваш банер частіше , ніж раз на день , або якщо Ви хочете , щоб вже після показаного банера показувався інший, наприклад , зі знижкою , у разі , якщо після показу перших баннера ви не отримали клік. Зазвичай у веб світі ця проблема вирішується через куки. І подібна інфа зберігається на стороні клієнта. «Х » чомусь не захотів реалізовувати куку на стороні мобільного клієнта і переклав це завдання на нас.
Для нас же це означало зберігати фактично всю користувача базу (коли , кому і яка реклама була віддана ) і мати швидкий доступ до неї під час кожного реквеста від користувача. Потрібно було рішення , яке б зберігало загальний стан для всіх деливери серверів - з максимально коротким часом відповіді і максимально можливою продуктивністю . Ідеальна ситуація для no - sql рішення.
DynamoDB
Тепер виникла необхідність вибрати потрібне рішення. Спочатку ми вирішили спробувати DynamoDB . Передбачалося , що ми знизимо вартість AdOps підтримки + за описом , рішення від амазона виглядало дуже привабливо. Перші навантажувальні тести показали , що DynamoDB дуже далекий від ідеалу. Як кажуть, я просто залишу це тут :
Крім великої вартості і великого часу відповіді , виявилося , що динамо що не масштабується автоматично. Є властивість, яка потрібно задавати при старті кластеру , що при різкому зростанні навантаження небезпечне відмовою в обслуговуванні частини запитів.
Redis
Після грунтовного гуглінга і досвіду роботи з деякими no - sql рішеннями в минулому , ми зупинили свій вибір на Redis . Редис показував просто казкове середній час відповіді - 0.2ms в приватній мережі амазона . При цьому він цілком собі тримав навантаження в 50к get річок/сек на одній с3.xLarge ноді . Ну і нарешті , у редиски є пачка смачних фіч - atomic increments , sets, hashes . Кожній з яких ми знайшли застосування .
Звичайно , як і у кожного рішення у редису є своя темна сторона :
- Усе зберігається в пам'яті , диск використовується тільки для бекапу і відновлення даних ;
- Кластерний рішення ще не готове до продакшену , так як вийшло кілька місяців тому ;
- Для масштабованості доведеться використовувати sharding . А значить закладати відразу в архітектуру при розробці ;
- Використання диска впливає на час відповіді , так само можливий page swapping , який теж може збільшити час відповіді ;
- Можлива втрата даних.
Незважаючи на недоліки , редис повністю підходив для всіх наших бізнес завдань .
Оптимізації
Незважаючи на досить хороший результат у 600 річок/сек і часу відповіді 15ms , було зрозуміло , що можна вичавити більше. Оптимізувати всі - починаючи від банальних очевидних речей , про які соромно писати , і закінчуючи деякими алгоритмами . Оптимізація коду в яві - це окрема тема для поста . Власне , кілька таких постів я вже написав на Хабре . Тому не буду повторяться , лише залишу на почуття цікавляться - невеликі трюки і ще, скільки коштує виділити об'єкт , одна маленька оптимізація , оптимізуємо ще, зміни в String .
В результаті оптимізацій ми дійшли до 1000 річок/сек і часу овтета 1.2 ms при LA 4 на с3.xLarge . Після всіх наших старань вузьким місцем став редис , який в середньому на 1 користувальницький запит виконував 1.5 реквеста по мережі , а також використовував синхронізацію при зверненні до пулу коннекшенов . На що йшло ~ 50-60 % часу обробки запиту .
Reporting
Ясна річ , що кожен клієнт хоче бачити ефективність рекламної компанії , бачити свою аудиторію , її чуйність на ту чи іншу рекламу. Для цього існує reporting модуль . Він раз на годину збирає всю інформацію з деливери серверів , обробляє і віддає в спрощеному вигляді кінцевому користувачеві.
Технічна . стек : Hadoop ( ) , MySQL.
Кожна відповідь сервера клієнту логіруется . Під час першого ж тестування було виявлено, що генерітся досить велика кількість логів . Один запис в лог була в середньому ~ 700 байт. Отже , при навантаженні в 11k річок/сек в секунду генерувалося близько 7 мб логів в секунду або близько 25 ГБ на годину. Структура логу :
{ " uid ": " test " , " platform ": " android " , " app ": " xxx " , " ts " : 1375952275223 , " adId " : 1 , " education ": " Some - Highschool - or - less " , " type ": " new " , " sh " : 1280 , " appver ": " 6.4.34 " , " country ": " AU " , " time ": " Sat , 03 August 2013 10:30:39 +0200 " , " deviceGroup " : 7 , " rid ": " fc389d966438478e9554ed15d27713f51 " , " responseCode " : 200 , " event ": " ad " , " device ": " N95 " , " sw " : 768 , " ageGroup ": " 18-24 " , " preferences " : [" beer " , " girls "] }
Hadoop
Було ясно , що при генерації 25 ГБ логів на годину ми ніяк не можемо безпосередньо зберігати їх в базі , так як це варто було б не дешево. Необхідно було якось скоротити обсяги . На щастя , замовник точно знав , якого роду репорти йому потрібні. Тому з наявних полів логу :
device , os , osVer , sreenWidth , screenHeight , country , region , city , carrier , advertisingId , preferences , gender , age , income , sector , company , language , ...
ми визначили набір таблиць необхідних клієнту , наприклад :
Geo table : Country , City , Region , CampaignId , Date , counters ; Device table : Device , Carrier , Platform , CampaignId , Date , counters ; Uniques table : CampaignId , UID ...
Потрібно було рішення , яке б легко масштабувати у разі збільшення трафіку або в разі , якщо кластер хадупа впав і не був доступний кілька годин , щоб ми могли швидко обробити накопичилися логи . Ми зупинили вибір на Hadoop . Здебільшого через те , що у нас вже був досвід роботи з ним , а часу на експерименти з іншими рішеннями не було.
Так , агрегація вхідних даних по певної сукупності полів призводить до втрати більшої частини інформації (після агрегації не можна дізнатися наприклад , скільки людина з айфоном подивилися рекламу в Сан- Франциско) , але бізнес задачу рішення вирішувало , а значить влаштовувало і нас (хоча деякі сирі дані ми все ж зберігаємо =)) . У результаті цього підходу обсяг даних вдалося скоротити на 3 порядку , а саме до 40Гб даних на місяць . Які ну ніяк не страшні навіть самим кволеньким СУБД.
У уважного читача напевно виникло питання : «А навіщо використовувати хадуп ? ». Питання , насправді , дуже правильний і цікавий . Справа в тому , що обсяг в 25 ГБ на годину - це зовсім не багато ( для нашої задачі ) , і навіть якщо треба обробити кілька десятків таких файлів - написаний на коліні агрегатор впорається з цим завданням дуже швидко. Але в нашому випадку хадуп виконує не тільки агрегацію , але і певну валідацію , яка є досить ресурсномісткою (на жаль , ця частина закрита NDA ) . Власне через цю валідації нам і необхідно було рішення , яке легко можна було масштабувати у разі потреби , що на коліні реалізувати вже набагато складніше.
Висновок
Проект , звичайно ж, не вийшов ідеальним ( хоча дуже хотілося) , але своє завдання ми виконали. Вже близько 2 місяців проект успішно запущений в робочому середовищі.
Зараз , озираючись назад , якби я був в тих же умовах сьогодні , я б однозначно наполіг на повному позбавленні від Solr . Solr для нас виявився явно overengineering solution . Не маючи в проекті солр , ми цілком змогли б позбавиться і від Tomcat , банально замінивши його HttpServer, що спростило б процес деплоя і написання інтеграційних тестів. Ну і щодо репортінгу - я б вже напевно подивився у бік більш перспективних технологій , а саме - Spark , Storm , Redshift . У них , звичайно , своя специфіка , але вирішити нашу проблему можна і на них. І цілком можливо , що вийшло б дешевше.
Спасибі всім , хто дочитав . Буду радий будь-якої конструктивної критиці. Сподіваюся пост вам сподобався.
Опубліковано: 18/06/14 @ 12:39
Розділ Різне
Рекомендуємо:
Дайджест цікавих вакансій № 141
Дуальне освіта: навчання і робота - два в одному
Дуальне освіта: навчання і робота - два в одному
Бесіда з Максом Бурцевим , креативним директором Arriba (частина 2 )
Бесіда з Максом Бурцевим , креативним директором Arriba (частина 2 )