Дизайн системы: стратегии работы с большими объемами операций записи в БД

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

Для простоты предположим, что таблица представляет собой инвентарную таблицу для продуктов и имеет столбец «Название продукта» и столбец «Количество», и он просто увеличивается на +1 каждый раз, когда новый продукт покупается в системе. И есть миллионы пользователей, покупающих разные продукты каждую секунду, и мы должны отслеживать последний счет каждого продукта, но это не обязательно должно быть строго в реальном времени, может быть приемлемо 5-минутное отставание.

Мои варианты:

1) Репликация Master-Slave, где главная БД обрабатывает все записи, а подчиненные — чтение. Но это не решает проблему записи

2) Разделение БД на основе диапазона названий продуктов или его хэш-значения. Но что, если есть конкретный продукт (например, Apple), который получает большое количество обновлений за короткое время, он все равно попадет в одну и ту же базу данных.

3) Пакетные обновления? Использовать какое-то кэширование и записывать в таблицу каждые X секунд с кумулятивным подсчетом того, что мы получили за эти X секунд? Является ли это допустимым вариантом и какой механизм кэширования я использую? А что, если произойдет сбой между последним чтением и следующей записью? Как восстановить потерянный счет?

4) Есть ли другие очевидные варианты, о которых я забыл?

Любое понимание ценится!


person user1008636    schedule 29.10.2018    source источник
comment
вы ограничены конкретной БД? Известно, что Cassandra обладает высокой пропускной способностью записи и распространяется по дизайну для обеспечения масштабируемости/высокой доступности.   -  person bsapaka    schedule 30.10.2018
comment
Меня больше интересуют методы и принципы, обеспечивающие высокую пропускную способность записи, а не то, какой фреймворк или БД.   -  person user1008636    schedule 31.10.2018


Ответы (2)


Я бы сказал, что решение будет сильно зависеть от того, что именно вам нужно сделать. Решение для записи тысяч записей в секунду может сильно отличаться от увеличения счетчика в приведенном вами примере. Более того, tables вообще не могло быть такой нагрузки. В вашем вопросе также отсутствуют требования Consistency/availability, и в зависимости от них вся архитектура может сильно отличаться.

В любом случае, вернемся к вашему конкретному упрощенному случаю и вашим вариантам.

Вариант 1 (репликация Master-Slave)

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

Вариант 2 (шардинг БД)

Ваше предположение верно, мало чем отличается от п.1

Вариант 3 (пакетные обновления)

Очень близко. Уровень кэширования, предоставляемый облегченным хранилищем, обеспечивающим одновременное атомарное увеличение/уменьшение с постоянством, чтобы не потерять ваши данные. Мы использовали redis для аналогичной цели, хотя любая другая база данных "ключ-значение" подойдет как ну - вокруг буквально десятки таких баз данных.

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

Решение будет выглядеть следующим образом:

incoming requests → your backend server -> kv_storage (atomic increment(product_id))

И у вас будет запущен скрипт "промывки", то есть */5, который делает следующее (упрощенно):

  1. для каждого product_id в kv_storage читать текущее value
  2. обновить счетчик БД (+= value)
  3. уменьшить value в kv_storage

Дальнейшее масштабирование

  • в случае сбоя скрипта ничего страшного не произойдет - обновления поступят при следующем запуске
  • если ваши серверные блоки не справляются с нагрузкой - вы можете легко добавить больше блоков
  • если один ключ-значение db не может справиться с нагрузкой - большинство из них поддерживают масштабирование по нескольким полям, или простая стратегия сегментирования в ваших внутренних сценариях будет работать нормально
  • если один сценарий «промывки» не поспевает за приращениями — вы можете масштабировать их до нескольких полей и решать, какие диапазоны ключей обрабатываются каждым из них.
person ffeast    schedule 01.11.2018
comment
Спасибо за подробный ответ! Дополнение: этому kv_storage, например, Redis, нужна отдельная БД для сохранения? Итак, у меня будет сервер-›redis-›db store? Если это так: входящие запросы → ваш внутренний сервер -> kv_storage (атомный инкремент (product_id)) Это на самом деле не записывает ни в какую базу данных, просто обновляет кеш Redis в памяти, верно? Постоянство выполняется сценарием очистки каждые 5 минут? - person user1008636; 03.11.2018
comment
Это уже произошло. Вы можете настроить, насколько он должен быть надежным — есть несколько вариантов, см. redis.io/topics/persistence Обычно существует компромисс между производительностью и надежностью - person ffeast; 03.11.2018
comment
Могу ли я использовать Redis с другим внутренним хранилищем баз данных, например, с традиционным MySQL? - person user1008636; 04.11.2018
comment
Нельзя, у него своя файловая система хранения и все его операции строятся вокруг нее. Это намного проще, чем у mysql, так что это не должно вызывать беспокойства. - person ffeast; 04.11.2018

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

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

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

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

person Tengiz    schedule 07.11.2018
comment
1. Было сказано, что параллельное обновление одной и той же записи из нескольких процессов заставит процессы ждать друг друга => при умеренной нагрузке система рухнет, а не то, что очередь приведет к остановке всей вашей системы. 2. Какую бы систему очередей вы бы ни использовали, вам нужно будет решить, что важнее — долговечность или скорость, потому что все равно вам нужно будет где-то сохранять элементы, и не всегда возможно и разумно сбрасывать каждый запрос на диск. сразу, а не накапливать их в памяти какое-то время, поэтому всегда есть шанс потерять некоторые данные - person ffeast; 08.11.2018
comment
3. Я не предлагаю только «кэш в памяти» - пожалуйста, прочитайте еще раз 4. Правильно ли, что в конкретном случае, предложенном автором, вы бы использовали промежуточную очередь и создавали новое сообщение для каждого запроса на увеличение? - person ffeast; 08.11.2018
comment
Спасибо за разъяснение вашего ответа. Да, вы правильно поняли мой ответ. Чтобы обеспечить максимальную надежность обработки каждого запроса на приращение, вам нужна очередь. - person Tengiz; 08.11.2018
comment
В своем ответе вы предложили, чтобы каждый запрос обновлял кеш в памяти. Это ненадежно и будет медленнее, чем ставить команду в очередь. Кроме того, я надеюсь, вы заметили, что вопрос прояснил доступность и согласованность. 5-минутная задержка означает, что окончательная согласованность приемлема. Я не рекомендую делать что-либо на каждый запрос, если количество команд намного превышает количество запросов. Во всяком случае, это, по крайней мере, проясняет различия между вашим предложением и моим. - person Tengiz; 08.11.2018