Тримаємо 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 , передбачалося вбити відразу двох зайців:

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 . Кожній з яких ми знайшли застосування .

Звичайно , як і у кожного рішення у редису є своя темна сторона :

Незважаючи на недоліки , редис повністю підходив для всіх наших бізнес завдань .

Оптимізації

Незважаючи на досить хороший результат у 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 )