Сохранение высокочастотных событий в базе данных с ограничением соединения


13

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

Проблема

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

События приходят быстрее, чем может обработать пул соединений с БД

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

В конечном итоге события складываются в ожидании сохранения и, поскольку в пуле нет доступных подключений, они истекают, и вся система становится неработоспособной.

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

Ограничения

Другие клиенты могут захотеть читать события одновременно

Другие клиенты постоянно запрашивают чтение всех событий с определенным ключом, даже если они еще не сохранены в БД.

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

Есть ли "классные" примеры того, как с этим бороться?

Возможные решения

Поставьте в очередь события на нашем сервере

Мы можем поставить в очередь события на сервере (с максимальным параллелизмом в очереди, равным 400, чтобы пул соединений не заканчивался).

Это плохая идея, потому что:

  • Это съест доступную память сервера. Сложенные в очередь события будут занимать огромное количество оперативной памяти.
  • Наши серверы перезагружаются один раз каждые 24 часа . Это жесткий предел, наложенный Heroku. Сервер может перезапускаться, пока события ставятся в очередь, что приводит к потере событий в очереди.
  • Он вводит состояние на сервере, что ухудшает масштабируемость. Если у нас настроена многосерверная система, и клиент хочет прочитать все события + в очереди + сохраненные, мы не будем знать, на каком сервере находятся события в очереди.

Используйте отдельную очередь сообщений

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

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

Используйте несколько баз данных, каждая из которых сохраняет часть сообщений на центральном сервере БД-координатор для управления ими

Другое решение, которое у нас есть, - это использование нескольких баз данных с центральным «координатором БД / балансировщиком нагрузки». Получив событие, этот координатор выберет одну из баз данных для записи сообщения. Это должно позволить нам использовать несколько баз данных Heroku, тем самым увеличивая ограничение на соединение до 500 x количество баз данных.

По запросу на чтение этот координатор может выдавать SELECTзапросы к каждой базе данных, объединять все результаты и отправлять их обратно клиенту, который запросил чтение.

Это плохая идея, потому что:

  • Эта идея звучит как ... хм .. чрезмерная инженерия? Было бы кошмаром управлять также (резервное копирование и т. Д.). Его сложно построить и поддерживать, и если это не является абсолютно необходимым, это звучит как нарушение KISS .
  • Он жертвует последовательностью . Делать транзакции между несколькими БД не стоит, если мы пойдем с этой идеей.

3
Где ваше узкое место? Вы упоминаете свой пул соединений, но это влияет только на параллелизм, а не скорость на одну вставку. Если у вас 500 соединений и, например, 2000QPS, это должно работать нормально, если каждый запрос выполняется в течение 250 мс, что является длительным временем. Почему это выше 15 мс? Также обратите внимание, что при использовании PaaS вы отказываетесь от значительных возможностей оптимизации, таких как масштабирование оборудования базы данных или использование реплик чтения, чтобы уменьшить нагрузку на основную базу данных. Heroku не стоит того, если развертывание не является вашей самой большой проблемой.
Амон

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

1
При этом я понимаю, что есть микрооптимизации, которые я мог бы сделать, чтобы помочь мне решить текущую проблему. Мне интересно, есть ли масштабируемое архитектурное решение моей проблемы.
Ник Кириакидес

3
Как именно вы убедились, что проблема в пуле соединений? @amon прав в своих расчетах. Попробуйте оформить select nullна 500 подключений. Бьюсь об заклад, вы обнаружите, что пул соединений не проблема там.
USR

1
Если выбрать нуль проблематично, то вы, вероятно, правы. Хотя было бы интересно, где все это время проведено. Нет такой медленной сети.
USR

Ответы:


9

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

Неясно, представляют ли ваши 1000 событий в секунду пики или это непрерывная загрузка:

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

Предложенное решение

Интуитивно понятно, что в обоих случаях я бы использовал поток событий, основанный на Кафке :

  • Все события систематически публикуются на тему кафки
  • Потребитель будет подписываться на события и сохранять их в базе данных.
  • Обработчик запросов будет обрабатывать запросы от клиентов и запрашивать базу данных.

Это отлично масштабируется на всех уровнях:

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

Предлагая события, еще не записанные в БД для клиентов

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

Вариант 1. Использование кеша для дополнения запросов к базе данных.

Я не анализировал подробно, но первая идея, которая приходит мне в голову, - сделать процессор (ы) запросов потребителем (ями) тем kafka, но в другой группе потребителей kafka . Затем обработчик запросов получит все сообщения, которые получит писатель БД, но независимо. Затем он может хранить их в локальном кэше. Затем запросы будут выполняться в БД + кэш (+ устранение дубликатов).

Дизайн будет выглядеть так:

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

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

Вариант 2: дизайн двойного API

ИМХО лучшим подходом было бы предложить двойной API (использовать механизм отдельной группы потребителей):

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

Преимущество в том, что вы позволяете клиенту решать, что интересно. Это может избежать систематического слияния данных БД с недавно кэшированными данными, когда клиент интересуется только новыми входящими событиями. Если деликатное слияние между свежими и заархивированными событиями действительно необходимо, клиент должен будет организовать это.

Варианты

Я предложил kafka, потому что он предназначен для очень больших объемов с постоянными сообщениями, чтобы вы могли перезапустить серверы, если это необходимо.

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


Stellar; Что вы имеете в виду 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?). Но чтобы сделать любой осознанный выбор, потребуется хорошее понимание моделей использования БД (что еще есть в БД, как часто он запрашивается, какие запросы, существуют ли транзакционные ограничения помимо отдельных событий и т. Д ...).
Кристоф

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

11

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

  • Поставьте в очередь события на нашем сервере

Мое предложение состояло бы в том, чтобы начать читать различные статьи, опубликованные об архитектуре LMAX . Им удалось заставить пакетирование больших объемов работать в своем случае, и возможно, что ваши компромиссы будут выглядеть как их.

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

Сервер может перезапускаться, пока события ставятся в очередь, что приводит к потере событий в очереди.

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

  • Используйте несколько баз данных, каждая из которых сохраняет часть сообщений на центральном сервере БД-координатор для управления ими

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

Есть случаи, когда потеря данных является приемлемым компромиссом?

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

То, на что это часто похоже, является моделью на основе извлечения с уведомлениями. Провайдер записывает сообщения в заказанный магазин длительного пользования. Потребитель извлекает сообщения из магазина, отслеживая собственную высокую отметку. Push-уведомления используются в качестве устройства, уменьшающего задержку, но если уведомление потеряно, сообщение по-прежнему выбирается (в конце концов), потому что потребитель выполняет регулярное расписание (разница в том, что если уведомление получено, получение происходит раньше). ).

См. Надежный обмен сообщениями без распределенных транзакций, Udi Dahan (на который уже ссылался Энди ) и Polyglot Data от Greg Young.


In a distributed system, I think you can be pretty confident that messages are going to get lost, В самом деле? Есть случаи, когда потеря данных является приемлемым компромиссом? У меня сложилось впечатление, что потеря данных = неудача.
Ник Кириакидес,

1
@NicholasKyriakides, это обычно неприемлемо, поэтому OP предложил возможность записать в долговременный магазин до начала мероприятия. Проверьте эту статью и это видео Уди Дахана, где он рассматривает проблему более подробно.
Энди

6

Если я правильно понимаю, текущий поток:

  1. Получите и событие (я полагаю, через HTTP?)
  2. Запросить соединение из пула.
  3. Вставьте событие в БД
  4. Освободите соединение с пулом.

Если так, то я думаю, что первое изменение в дизайне состояло бы в том, чтобы прекратить обработку кода, возвращающего соединения с пулом, при каждом событии. Вместо этого создайте пул потоков / процессов вставки, который равен 1: 1 с количеством соединений с БД. Каждый из них будет содержать выделенное соединение с БД.

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

Это должно эффективно устранить накладные расходы пула соединений. Разумеется, вам нужно будет выполнять по крайней мере 1000 событий в секунду для каждого соединения. Возможно, вы захотите попробовать различное количество соединений, поскольку наличие 500 соединений, работающих с одними и теми же таблицами, может привести к конфликту на БД, но это совсем другой вопрос. Еще одна вещь, которую следует учитывать, - это использование пакетных вставок, то есть каждый поток извлекает несколько сообщений и вставляет их сразу. Кроме того, избегайте нескольких подключений, пытающихся обновить одни и те же строки.


5

Предположения

Я собираюсь предположить, что нагрузка, которую вы описываете, постоянна, так как это более сложный сценарий для решения.

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

Решение

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

Чтобы решить проблему задержки, вам нужно работать так, чтобы свести к минимуму величину задержки, возникающей при сохранении события. Это ключевая вещь, которую вам нужно достичь, если вы не желаете или не можете менять оборудование . Поскольку вы работаете с сервисами PaaS и не имеете никакого контроля над оборудованием или сетью, единственный способ уменьшить задержку для каждого события будет заключаться в некоторой пакетной записи событий.

Вам нужно будет локально хранить очередь событий, которая периодически сбрасывается и записывается в вашу базу данных, как только она достигнет заданного размера, или по истечении определенного времени. Процесс должен будет отслеживать эту очередь, чтобы вызвать сброс в хранилище. Вокруг должно быть множество примеров того, как управлять параллельной очередью, которая периодически сбрасывается на выбранном вами языке. Вот пример на C # из периодического приемника пакетирования популярной библиотеки журналов Serilog.

Этот SO-ответ описывает самый быстрый способ сброса данных в Postgres - хотя для этого требуется, чтобы пакетное хранилище сохраняло очередь на диске, и, вероятно, там возникнет проблема, когда ваш диск исчезнет при перезагрузке в Heroku.

скованность

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

Основным преимуществом CQRS является то, что вы отделяете свои концептуальные модели чтения и записи - это необычный способ сказать, что вы пишете в одну модель, а вы читаете из другой совершенно другой модели. Чтобы получить преимущества масштабируемости от CQRS, вы обычно хотите, чтобы каждая модель сохранялась отдельно таким образом, который оптимален для ее моделей использования. В этом случае мы можем использовать модель агрегированного чтения - например, кэш Redis или просто в памяти - чтобы обеспечить быстрое и согласованное чтение, в то время как мы все еще используем нашу транзакционную базу данных для записи наших данных.


3

События приходят быстрее, чем может обработать пул соединений с БД

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

С этим дизайном может использоваться очередь сообщений, вам нужен производитель (и) сообщений, который отправляет события в очередь сообщений, а рабочие (потребители) обрабатывают сообщения из очереди.

Другие клиенты могут захотеть читать события одновременно

Это ограничение возможно только в том случае, если события хранятся в базе данных без какой-либо обработки (необработанные события). Если события обрабатываются перед сохранением в базе данных, то единственный способ получить события из базы данных.

Если клиенты просто хотят запрашивать необработанные события, я бы предложил использовать поисковую систему, например Elastic Search. Вы даже получите API запроса / поиска бесплатно.

Учитывая, что для вас важно запрашивать события перед их сохранением в базе данных, простое решение, такое как Elastic Search, должно работать. Вы просто храните в нем все события и не дублируете одни и те же данные, копируя их в базу данных.

Масштабировать Elastic Search легко, но даже при базовой конфигурации он достаточно высокопроизводителен.

Когда вам нужна обработка, ваш процесс может получать события от ES, обрабатывать и сохранять их в базе данных. Я не знаю, какой уровень производительности вам нужен для этой обработки, но он будет полностью отделен от запроса событий от ES. В любом случае у вас не должно быть проблем с соединением, поскольку у вас может быть фиксированное количество рабочих, и у каждого будет одно соединение с базой данных.


2

1 КБ или 2 КБ событий (5 КБ) в секунду не так много для базы данных, если она имеет соответствующую схему и механизм хранения. По предложению @eddyce мастер с одним или несколькими подчиненными может отделить запросы на чтение от совершения записей. Использование меньшего количества соединений с БД даст вам лучшую пропускную способность.

Другие клиенты могут захотеть читать события одновременно

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

Я использовал (Percona) MySQL с движком TokuDB для очень больших объемов записи. Есть также движок MyRocks, основанный на LSMtrees, который хорош для загрузки записей. Для обоих этих механизмов, а также, вероятно, для PostgreSQL, существуют параметры изоляции транзакций, а также поведения синхронизации при фиксации, которые могут значительно увеличить емкость записи. В прошлом мы принимали потерянные данные до 1 с, которые были переданы клиенту db как подтвержденные. В других случаях были SSD с батарейным питанием, чтобы избежать потерь.

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


TBH любая хорошо администрируемая база данных на достаточном оборудовании должна справиться с этой нагрузкой. Проблема OP не в производительности базы данных, а в задержке соединения; Я предполагаю, что Heroku, как поставщик PaaS, продает им экземпляр Postgres в другом регионе AWS.
Амон

1

Я бы отбросил героку все вместе, то есть отказался бы от централизованного подхода: множественные записи, которые достигают максимума подключения к пулу, являются одной из главных причин, по которым кластеры БД были изобретены, в основном потому, что вы не загружаете запись БД с запросами на чтение, которые могут выполняться другими БД в кластере, я бы попробовал с топологией ведущий-ведомый, более того - как уже упоминал кто-то другой, наличие ваших собственных установок БД позволило бы настроить целое система, чтобы убедиться, что время распространения запроса будет правильно обрабатываться.

Удачи

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.