Збереження високочастотних подій до бази даних з обмеженням зв'язку


13

У нас виникла ситуація, коли мені доводиться стикатися з масовим напливом подій, що надходять на наш сервер, в середньому близько 1000 подій в секунду (пік може бути ~ 2000).

Проблема

Наша система розміщується на Heroku та використовує відносно дорогий БД Heroku Postgres , який дозволяє максимум 500 підключень до БД. Ми використовуємо пул з'єднань для підключення з сервера до БД.

Події надходять швидше, ніж може працювати пул з'єднань з БД

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

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

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

Обмеження

Інші клієнти можуть одночасно читати події

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

Клієнт може запитувати GET api/v1/events?clientId=1та отримувати всі події, надіслані клієнтом 1, навіть якщо ці події ще не виконані, зберігаючи в БД.

Чи є приклади «класної кімнати», як з цим боротися?

Можливі рішення

Замовте події на нашому сервері

Ми можемо передбачити події на сервері (з чергою максимальна сумісність 400, щоб пул зв’язків не закінчувався).

Це погана ідея, оскільки:

  • Він з'їсть доступну пам'ять сервера. Складені захоплені події будуть використовувати велику кількість оперативної пам’яті.
  • Наші сервери перезапускаються раз на 24 години . Це жорстка межа, нав'язана Героку. Сервер може перезапуститись, коли події вмикаються, внаслідок чого ми втрачаємо події, що перебувають у спаді
  • Він вводить стан на сервері, тим самим шкода масштабованості. Якщо у нас є налаштування декількох серверів, і клієнт хоче прочитати всі зафіксовані + збережені події, ми не будемо знати, на якому сервері живуть події, котрі стосуються.

Скористайтеся окремою чергою повідомлень

Я припускаю, що ми могли б використовувати чергу черг (наприклад, RabbitMQ ?), Де ми перекачуємо повідомлення в ній, а на іншому кінці є ще один сервер, який займається збереженням подій у БД.

Я не впевнений, чи дозволяють черги повідомлень запитувати події, пов’язані з запитом (які ще не збереглися), тому якщо інший клієнт хоче прочитати повідомлення іншого клієнта, я можу просто отримати збережені повідомлення з БД та очікувані повідомлення з черги і з'єднати їх разом, щоб я міг відправити їх назад клієнту із запитом на читання.

Використовуйте кілька баз даних, кожна з яких зберігає частину повідомлень на центральному сервері координатора БД для управління ними

Ще одне рішення, яке ми маємо, - це використання декількох баз даних з центральним "координатором БД / балансиром завантаження". Отримавши подію, цей координатор вибере одну з баз даних, на яку написати повідомлення. Це повинно дозволяти нам використовувати декілька баз даних Heroku, таким чином збільшуючи межу з'єднання до 500 x кількості баз даних.

Після запиту читання, цей координатор може видати SELECTзапити до кожної бази даних, об'єднати всі результати та відправити їх тому клієнту, який запросив прочитане.

Це погана ідея, оскільки:

  • Ця ідея звучить як ... гм .. надмірна інженерія? Був би кошмаром і для керування (резервне копіювання тощо). Складно створювати та підтримувати, і якщо це абсолютно не потрібно, це звучить як порушення KISS .
  • Він жертвує послідовністю . Здійснення транзакцій у кількох БД - це безрезультатно, якщо ми з цією ідеєю.

3
Де ваше вузьке вузьке місце? Ви згадуєте свій пул з'єднань, але це впливає лише на паралелізм, а не на швидкість вставки. Якщо у вас 500 підключень і, наприклад, 2000QPS, це має спрацювати нормально, якщо кожен запит завершиться протягом 250 мс, що є довгим часом. Чому це вище 15 мс? Також зауважте, що використовуючи PaaS, ви відмовляєтесь від значних можливостей оптимізації, таких як масштабування апаратного забезпечення бази даних або використання репліка читання для зменшення навантаження на первинну базу даних. Heroku не варто, якщо розгортання не є вашою найбільшою проблемою.
амон

@amon Вузьке місце - це справді пул зв’язків. Я вже працював ANALYZEнад запитами, і вони не є проблемою. Я також створив прототип, щоб перевірити гіпотезу пулу підключень і переконався, що це справді проблема. База даних і сам сервер живуть на різних машинах, отже, і затримка. Крім того, ми не хочемо відмовлятися від Heroku, якщо це абсолютно не потрібно, не турбуючись про розгортання - це величезний плюс для нас.
Нік Кіріакідес

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

3
Як саме ви переконалися, що проблема пулу з'єднань є проблемою? @amon правильний у своїх розрахунках. Спробуйте видати select nullна 500 підключень. Б'юсь об заклад, ви виявите, що пул з'єднань тут не проблема.
usr

1
Якщо вибрати null проблематично, то ви, мабуть, праві. Хоча було б цікаво, де весь цей час проводиться. Жодна мережа не така повільна.
usr

Відповіді:


9

Вхідний потік

Не ясно, чи ваші 1000 подій в секунду представляють піки чи це безперервне навантаження:

  • якщо це пік, ви можете використовувати чергу повідомлень як буфер для поширення навантаження на сервер БД на довший час;
  • якщо це постійне завантаження, одна лише черга повідомлень недостатня, оскільки сервер БД ніколи не зможе наздогнати. Тоді вам потрібно буде подумати про розподілену базу даних.

Пропоноване рішення

Інтуїтивно, в обох випадках я б пішов на події на основі Kafka :

  • Усі події систематично публікуються на тему кафки
  • Споживач підписується на події та зберігає їх у базі даних.
  • Процесор запитів буде обробляти запити клієнтів і запитувати БД.

Це можна легко масштабувати на всіх рівнях:

  • Якщо сервер БД є вузьким місцем, просто додайте декількох споживачів. Кожен міг підписатися на тему та записатись на інший сервер БД. Однак, якщо розподіл відбувається випадковим чином на серверах БД, процесор запитів не зможе передбачити, як прийме сервер БД, і доведеться запитувати кілька серверів БД. Це може призвести до нового вузького місця на стороні запиту.
  • Таким чином, схему розподілу БД можна було б передбачити, організувавши потік подій у декілька тем (наприклад, використовуючи групи ключів або властивостей для розділення БД згідно з передбачуваною логікою).
  • Якщо одного сервера повідомлень недостатньо, щоб обробити зростаючий потік вхідних подій, ви можете додати розділи kafka для поширення тем kafka на декількох фізичних серверах.

Пропонуючи клієнтам події, ще не записані в БД

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

Варіант 1: Використання кешу для доповнення db-запитів

Я не проаналізував глибоко, але першою ідеєю, яка мені спадає на думку, було б зробити процесор (и) запитів споживачами теми кафки, але в іншій групі споживачів кафки . Потім процесор запитів отримає всі повідомлення, які отримає записувач БД, але незалежно. Потім він може тримати їх у локальному кеші. Потім запити виконували б кеш DB + (+ усунення дублікатів).

Потім дизайн виглядатиме так:

введіть тут опис зображення

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

Варіант 2: розробити подвійний API

Кращим підходом IMHO було б запропонувати подвійний API (використовувати механізм окремої групи споживачів):

  • API запиту для доступу до подій у БД та / або здійснення аналітики
  • API потокового передавання, який просто пересилає повідомлення безпосередньо з теми

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

Варіанти

Я запропонував kafka, оскільки він розроблений для дуже великих обсягів з постійними повідомленнями, щоб ви могли перезапустити сервери при необхідності.

Ви можете побудувати подібну архітектуру з RabbitMQ. Однак якщо вам потрібні стійкі черги, це може знизити продуктивність . Крім того, наскільки я знаю, єдиний спосіб досягти паралельного споживання одних і тих же повідомлень декількома читачами (наприклад, автором + кешем) за допомогою RabbitMQ - це клонувати черги . Таким чином, більша масштабованість може бути вищою ціною.


Зоряний; Що ви маєте на увазі під a distributed database (for example using a specialization of the server by group of keys)? Також чому Кафка замість RabbitMQ? Чи є певна причина вибору одного над іншим?
Нік Киріакідес

@NicholasKyriakides Дякую! 1) Я просто думав про декілька незалежних серверів баз даних, але з чіткою схемою розподілу (ключ, географія тощо), яка могла бути використана для ефективного відправлення команд. 2) Інтуїтивно , можливо, тому, що Kafka розроблений для дуже високої пропускної спроможності з постійними повідомленнями, потрібно перезапустити ваші сервери?). Я не впевнений, що RabbitMQ настільки гнучка для розподілених сценаріїв, а стійкі черги знижують продуктивність
Крістоф

Для 1) Таким чином, це дуже схоже на мою Use multiple databasesідею, але ви говорите, що я не повинен просто випадковим чином (або кругообігом) поширювати повідомлення в кожній із будь-яких баз даних. Правильно?
Нік Кіріакідес

Так. Першою моєю думкою було б не піти на випадковий розподіл, оскільки це може збільшити навантаження на обробку для запитів (тобто запит обох декількох БД більшу частину часу). Ви також можете розглянути механізми розподілених БД (наприклад, Ignite?). Але для будь-якого обґрунтованого вибору потрібно буде добре розуміти шаблони використання БД (що ще є в db, як часто він запитується, які запити, чи є трансакційні обмеження поза окремими подіями тощо).
Крістоф

3
Просто хочу сказати, що, хоча кафка може дати дуже високу пропускну здатність, це, мабуть, перевищує потреби більшості людей. Я виявив, що поводження з kafka та його API було для нас великою помилкою. RabbitMQ - це не сутуль, і він має інтерфейс, який можна очікувати від MQ
imela96

11

Я здогадуюсь, що вам потрібно уважніше вивчити підхід, який ви відкинули

  • Замовте події на нашому сервері

Моя пропозиція - почати читати різні статті, опубліковані про архітектуру LMAX . Їм вдалося зробити об'ємну партійну роботу для їх використання, і можливо, ви зможете зробити ваші торги схожішими на їхні.

Крім того, ви можете дізнатися, чи зможете ви зчитувати зчитувані способи - в ідеалі ви хочете мати можливість їх масштабування незалежно від запису. Це може означати розгляд CQRS (розділення відповідальності за запит команди).

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

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

  • Використовуйте кілька баз даних, кожна з яких зберігає частину повідомлень на центральному сервері координатора БД для управління ними

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

Є випадки, коли втрата даних є прийнятною компромісом?

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

Як часто це виглядає, це модель на основі потягу з повідомленнями. Постачальник записує повідомлення в замовлений довговічний магазин. Споживач витягує повідомлення з магазину, відстежуючи власний знак високої води. Push-сповіщення використовуються як пристрій зменшення затримки - але якщо повідомлення втрачено, повідомлення все одно виймається (врешті-решт), оскільки споживач працює за звичайним графіком (різниця полягає в тому, що якщо повідомлення отримано, витяг відбувається раніше ).

Дивіться Надійні повідомлення без розподілених транзакцій Уді Дахана (на які вже посилається Енді ) та дані про поліглот Грега Янг.


In a distributed system, I think you can be pretty confident that messages are going to get lost. Дійсно? Є випадки, коли втрата даних є прийнятною компромісом? У мене було враження, що втрата даних = збій.
Нік Кіріакідес

1
@NicholasKyriakides, як правило, це не прийнятно, тому ОП запропонував можливість написати в міцний магазин, перш ніж випускати подію. Перегляньте цю статтю та це відео Уді Дахана, де він детальніше розглядає проблему.
Енді

6

Якщо я правильно розумію поточний потік:

  1. Отримання та подія (я припускаю через HTTP?)
  2. Попросити з'єднання з пулу.
  3. Вставте подію в БД
  4. Відпустіть з'єднання з пулом.

Якщо так, я думаю, що першою зміною дизайну було б перестати мати рівномірні підключення для повернення коду до пулу на кожному заході. Замість цього створіть пул потоків / процесів вставки, який 1 на 1 із кількістю з'єднань БД. Кожен з них матиме виділене з'єднання БД.

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

Це повинно ефективно усунути накладні пули з'єднань. Вам, звичайно, потрібно мати можливість робити принаймні 1000 / подій підключення в секунду на кожному з'єднанні. Ви можете спробувати різні кількості з'єднань, оскільки 500 підключень, що працюють на одних і тих же таблицях, можуть створити суперечки в БД, але це зовсім інше питання. Інша річ, яку слід врахувати, - це використання пакетних вставок, тобто кожна нитка витягує ряд повідомлень і одночасно проштовхує їх. Крім того, уникайте кількох з'єднань, намагаючись оновити одні й ті ж рядки.


5

Припущення

Я припускаю, що навантаження, яке ви описуєте, є постійним, тому що це складніше вирішити.

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

Рішення

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

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

Вам потрібно буде зберігати чергу подій на локальному рівні, яка періодично видається та записується на ваш db, як тільки вона досягне заданого розміру, або після закінчення проміжок часу. Процес повинен буде контролювати цю чергу, щоб викликати потоки в магазин. Навколо має бути безліч прикладів того, як керувати паралельною чергою, яка періодично виводиться на вашу обрану мову - ось приклад на C # , з періодичної пакетної мийки популярної бібліотеки журналів Serilog.

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

Обмеження

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

Ключовою перевагою CQRS є те, що ти роз'єднав свої концептуальні моделі читання і запису - що є фантастичним способом сказати, що ти пишеш в одну модель і читаєш з іншої абсолютно іншої моделі. Щоб отримати переваги від масштабування від CQRS, ви, як правило, бажаєте забезпечити збереження кожної моделі окремо таким чином, що є оптимальним для її використання. У цьому випадку ми можемо використовувати сукупну модель читання - наприклад, кеш Redis, або просто пам'ять - щоб забезпечити швидке та послідовне зчитування, в той час як ми все ще використовуємо нашу транзакційну базу даних для запису наших даних.


3

Події надходять швидше, ніж може працювати пул з'єднань з БД

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

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

Інші клієнти можуть одночасно читати події

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

Якщо клієнти просто хочуть запитувати вихідні події, то я б запропонував використовувати пошукову систему на зразок Elastic Search. Ви навіть отримаєте API запиту / пошуку безкоштовно.

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

Еластичний пошук масштабування простий, але навіть при базовій конфігурації він досить високий.

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


2

1k або 2k події (5KB) в секунду - це не так багато для бази даних, якщо вона має відповідну схему і механізм зберігання даних. Як запропонував @eddyce, майстер з одним або декількома рабами може відокремити запити читання від запису. Використання меншої кількості підключень до БД допоможе вам зробити загальну пропускну здатність.

Інші клієнти можуть одночасно читати події

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

Я використовував (Percona) MySQL з двигуном TokuDB для дуже високого обсягу записів. Також є двигун MyRocks на базі LSMtrees, який добре підходить для завантаження записів. Для обох цих двигунів, і, ймовірно, і для PostgreSQL, є налаштування для ізоляції транзакцій, а також здійснення поведінки синхронізації, що може різко збільшити ємність запису. Раніше ми приймали до 1-х втрачених даних, про які було повідомлено клієнту db як здійснене. В інших випадках існували захищені від акумулятора SSD, щоб уникнути втрат.

Як стверджується, Amazon RDS Aurora в ароматі MySQL має 6-кратну пропускну спроможність запису з реплікацією з нульовою вартістю (схожа на рабів, що обмінюються файловою системою з master). Аромат Aurora PostgreSQL також має інший вдосконалений механізм реплікації.


TBH будь-яка добре керована база даних на достатньому рівні апаратного забезпечення повинна мати можливість справлятися з цим навантаженням. Здається, проблема ОП не в продуктивності бази даних, а в затримці підключення; я думаю, що Heroku як постачальник PaaS продає їм екземпляр Postgres в іншому регіоні AWS.
амон

1

Я б кинув heroku всі разом, тобто я б відмовився від централізованого підходу: кілька записів про те, що максимальне з'єднання в пулі є однією з головних причин, чому db кластери там, де винайдено, в основному викликають не завантаження запису db (s) з запитами на читання, які можуть виконувати інші db в кластері, я б спробував з топологією master-slave, крім того - як хтось уже згадував, наявність власних установок db дозволить налаштувати ціле система, щоб переконатися, що час поширення запиту буде правильно оброблений.

Удачі

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.