Re: Задачка на проектирование
От: Pzz Россия https://github.com/alexpevzner
Дата: 24.04.20 22:20
Оценка: +1
Здравствуйте, Ватакуси, Вы писали:

В>Как сделать / спроектировать систему так, чтобы она работала на 100k или даже миллионе rps и не более 50 мс отклик.


Очевидно, что никакая база тебе не даст "100к или даже миллион" записей в секунду. Однако требование на 50 мс на отклик внушает надежду — 20 записей в секунду можно потянуть, даже если хранить данные в текстовом файле, который каждый раз обновляется.

Значит, тебе надо собирать запросы в кучку, суммировать их эффект, сохранять их в базу одной записью на много запросов, и если она прошла уачно, всем везунчикам отвечать "200 ОК".
Re: Задачка на проектирование
От: mik1  
Дата: 24.04.20 22:37
Оценка: 5 (1) +2
Здравствуйте, Ватакуси, Вы писали:

В>REST-сервис


В>inc n — увеличить глобальное значение на n. После того как запись в базу сделана возвращается код 200. Это гарантия, что счётчик увеличился.

В>read — получить текущее глобальное значение

В>сервис на сервере, который пишет и читает из постгреса.


В>Как сделать / спроектировать систему так, чтобы она работала на 100k или даже миллионе rps и не более 50 мс отклик.


В>Ваши предложения.

В>Бюджет относительно небольшой, поэтому всякие супер-космические решения не годятся.

Какая разбивка между reads and increments? Одинаковый rps или сильный перевес с одну сторону?
Какая точность нужна для read (если ожидаем latency < 50 ms, то насколько старое значение можно вернуть, т.к очевидно что "текущее" не имеет смысла в такой системе)
И самое главное — зачем postgres, если мы по сути имеем всего несколько байт данных (ну или скаляр по одному числу на линию кэша)?

Как только вы ответите себе на эти вопросы, остальное станет просто.
Для 100К огород вообще городить не надо, один 1-сокетный сервер справится без проблем. На миллион тоже можно поколдовать.
Re[8]: Задачка на проектирование
От: Ватакуси Россия  
Дата: 25.04.20 06:39
Оценка:
LK>А вообще, о каком хайлоде может идти речь? Если ты искать и читать не умеешь...

И вам не кашлять.
Все будет Украина!
Re[4]: Задачка на проектирование
От: Ватакуси Россия  
Дата: 25.04.20 06:39
Оценка:
В>>Именно в этом и заключается подвох задачи. Увеличивать мощность сервера приложений или даже сервера ДБ нельзя до бесконечности (да и деньги кончатся).
В>>Поскольку все запросы inc будут биться за одну глобальную переменную, то они сразу станут в очередь.
В>>Но решение (типа) есть.

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

G>Потому что CAP-теорему (будь она неладна) обмануть нельзя. Тут или согласованностью чтения надо жертвовать, или время ответа не жестко фиксировать.

G>Но в реальности нет такого, что количество запросов растет бесконечно. Более того, 90% проектов не преодолеют возможности одной машины.

Не бесконечно, конечно же. Кол-во запросов просто большое.
Все будет Украина!
Re[2]: Задачка на проектирование
От: Ватакуси Россия  
Дата: 25.04.20 06:40
Оценка:
В>>read — получить текущее глобальное значение

L>А что такое "текущее" значение? Если у тебя дофига запросов на запись, то оно будет устаревать практически сразу после чтения.


Согласен.
Но интуитивно, это значение, которое успело уже записаться.
Все будет Украина!
Re[2]: Задачка на проектирование
От: Ватакуси Россия  
Дата: 25.04.20 06:42
Оценка:
В>>Как сделать / спроектировать систему так, чтобы она работала на 100k или даже миллионе rps и не более 50 мс отклик.

МГ>держать счётчик в памяти, какие ещё могут быть решения? Гарантировать сохранение в базу будет raid-массив и дизель-генератор. Вероятность выхода из строя raid-массива с дизель-генератором считать много меньшей вероятности банкротства компании.


Смешно
Все будет Украина!
Re[4]: Задачка на проектирование
От: Ватакуси Россия  
Дата: 25.04.20 06:43
Оценка:
В>>Хочешь на 1 увеличить или на 10, например.
K>Мне не понятно зачем тебе счётчик такой нужен.
Просто поболтать, это же не настоящая задача, ты понимашь.
Все будет Украина!
Re[2]: Задачка на проектирование
От: Ватакуси Россия  
Дата: 25.04.20 06:44
Оценка:
В>>Как сделать / спроектировать систему так, чтобы она работала на 100k или даже миллионе rps и не более 50 мс отклик.

Pzz>Очевидно, что никакая база тебе не даст "100к или даже миллион" записей в секунду. Однако требование на 50 мс на отклик внушает надежду — 20 записей в секунду можно потянуть, даже если хранить данные в текстовом файле, который каждый раз обновляется.


Pzz>Значит, тебе надо собирать запросы в кучку, суммировать их эффект, сохранять их в базу одной записью на много запросов, и если она прошла уачно, всем везунчикам отвечать "200 ОК".


Да. "Правильное решение" примерно так и должно выглядеть.
Как это осуществить?
Все будет Украина!
Re[2]: Задачка на проектирование
От: Ватакуси Россия  
Дата: 25.04.20 06:46
Оценка:
M>Какая разбивка между reads and increments? Одинаковый rps или сильный перевес с одну сторону?
M>Какая точность нужна для read (если ожидаем latency < 50 ms, то насколько старое значение можно вернуть, т.к очевидно что "текущее" не имеет смысла в такой системе)
Это заранее неизвестно, скажем так. Т.е. может быть любое соотношение.

M>И самое главное — зачем postgres, если мы по сути имеем всего несколько байт данных (ну или скаляр по одному числу на линию кэша)?

Можно предлагать, что угодно. Можно и отказаться от него, если есть решение.

M>Как только вы ответите себе на эти вопросы, остальное станет просто.

M>Для 100К огород вообще городить не надо, один 1-сокетный сервер справится без проблем. На миллион тоже можно поколдовать.
Очевидно, что узким местом тут является запись в глобальную переменную. Как это распарралелить?
Все будет Украина!
Re[3]: Задачка на проектирование
От: Ватакуси Россия  
Дата: 25.04.20 06:48
Оценка:
Z>ну можно взять какую-нибудь быструю ПЗУ, типа Intel Optane,
Z>и писать туда напрямую, а TCP/IP и Ethernet обрабатывать почти полностью
Z>в user-space или наоборот все в kernel space включаю логику инкремента и записи optane,
Z>тогда я думаю 100k должно выдавать без проблем, даже на относительно слабом железе.

Довольно необычное решение. Спасибо.
Все будет Украина!
Re[3]: Задачка на проектирование
От: Sharowarsheg  
Дата: 25.04.20 07:28
Оценка:
Здравствуйте, Ватакуси, Вы писали:

В>>>read — получить текущее глобальное значение


L>>А что такое "текущее" значение? Если у тебя дофига запросов на запись, то оно будет устаревать практически сразу после чтения.


В>Согласен.

В>Но интуитивно, это значение, которое успело уже записаться.

В смысле, с гарантией, что при повторном запросе нельзя получить ответ меньше, чем при первом запросе. Никакой другой гарантии особо не получится.
Re[3]: Задачка на проектирование
От: Pzz Россия https://github.com/alexpevzner
Дата: 25.04.20 08:24
Оценка: 10 (3) +1
Здравствуйте, Ватакуси, Вы писали:

В>Да. "Правильное решение" примерно так и должно выглядеть.

В>Как это осуществить?

Ну для начала, надо бы побенчмаркать, на чем вообще можно сделать HTTP-сервер, способный держать миллион пустых запросов в секунду. Скажем, если запрос и ответ укладываются в 1 килобайт (а это довольно близкое приближение), то там чисто сетевого трафика набегает по 8 гигабит в каждую сторону. Одна писишка, боюсь, не потянет, а десяток писишек потянут с легкостью, на чем не пиши. Т.е. понадобится какой-нибудь load balancer и сетевое хранилище, скорострельность которого нам не очень важна, но оно должено обладать одним хитрым свойством, о котором чуть позже.

Далее, вопрос о своевременности чтения, как многие уже заметили, не стоит, но вопрос о монотонности счетчика все же стоит упомянуть. В последовательности read-inc-read второй read должен возвращать число, которое больше, чем вернул первый read. Ну, хотя бы для приличия. И никакие два клиента, даже сговорившись, не должны увидеть одинаковый ответ на read, если в промежутке проскочил инкремент.

Поскольку для конструкции из сети, load balancer'а и нескольких писишек, обслуживание запросов будет наверняка происходить не всегда в порядке поступления, часть забот о монотонности нам придется переложить на клиентов. А именно, мы возьмем с них обязательство, что они не будут посылать несколько запросов одновременно, иначе мы не гарантируем, что их запросы будут обслужены в правильном порядке.

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

А что до хранилища, нам от него понадобится один единственный запрос, но хитрый: он должен прибавлять к счетчику, что сказали, и возвращать старое значение. Назовем его read-increment.

Ну а дальше все относительно просто. Нам понадобится очередь запросов. Изначально очередь пустая. Первый поступивший запрос посылает хранилищу запрос read-increment(0), если это запрос на чтение, и read-incrememt(1), если это запрос на увеличение счетчика, и становится в очередь. Остальные просто становятся в очередь. Запрос в хранилище посылается асинхронно. И еще, мы запоминаем, сколько запросов было в очереди, когда мы послали запрос в хранилище.

Когда из хранилища придет ответ, мы выгребаем из очереди столько запросов, сколько было на момент обращения к хранилищу — не зря мы запомнили это число. У нас есть вся информация, чтобы их обслужить: актуальный счетчик из базы и сколько к нему прибавилось по мере продвижения по очереди. Если у нас после этого в очереди остались запросы, мы считаем, сколько из них инкременты, и посылаем очередной read-increment(), указав в качестве параметра количество оставшихся инкрементов. Ну и так далее.

Еще было бы не лишне поставить какое-то ограничение на максимальную длинну очереди. Если оно превышено, очередной запрос сразу получает HTTP 503, и в очередь не становится.

По-моему, так (c) Винни Пух.
Re: Задачка на проектирование
От: v.a.v СССР  
Дата: 25.04.20 10:55
Оценка:
Здравствуйте, Ватакуси, Вы писали:

В>REST-сервис


В>inc n — увеличить глобальное значение на n. После того как запись в базу сделана возвращается код 200. Это гарантия, что счётчик увеличился.

В>read — получить текущее глобальное значение

В>сервис на сервере, который пишет и читает из постгреса.


В>Как сделать / спроектировать систему так, чтобы она работала на 100k или даже миллионе rps и не более 50 мс отклик.


В>Ваши предложения.

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

Абстрактная проблема — абстрактное решение.

Несколько серверов с сервисами для обслуживания запросов "inc n".
Каждый сервер со своим экземпляром СУБД(PostgreSQL по условию задачи), на локальном диске/дисковом массиве(никаких внешних СХД). В каждом экземпляре PostgreSQL собственный счетчик-накопитель. Сервисы обрабатывающие запросы "inc n" кешируют текущее значение счетчика-накопителя в памяти(изменение кеша после успешного изменения в БД), и периодически отправляют(push) его сервису-агрегатору(собственный протокол поверх UDP, или даже RDMA).
Балансировка, например посредством DNS. Получаем практически неограниченное горизонтальное масштабирование по запросу "inc n".

Сервис-агрегатор периодически суммирует push-и от вышеописанных сервисов и отправляет(push) результат множеству сервисов "read"(собственный протокол поверх multicast UDP, или RDMA, если RDMA умеет multicast). Сервис-агрегатор не нуждается в хранилище данных.

Несколько сервисов "read" котрые отправляют суммарное значение счетчиков-накопителей клиентам по запросу. Получаем практически неограниченное горизонтальное масштабирование по запросу "read". Сервис "read" не нуждается в хранилище данных.

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

Стоимость реализации сравнительно не высока, так как специальнго оборудования не требуется. Сервисам-агрегаторам и сервисам "read" даже HDD после старта не нужен. Для сервисов "inc n" важны только накопители с низкой задержкой.

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

Можно обойтись и без "собственный протокол поверх UDP, или даже RDMA", но тогда возможно придется увеличить количество экземпляров сервисов, да и лаги станут больше.
Отредактировано 25.04.2020 11:08 v.a.v . Предыдущая версия .
Re: Задачка на проектирование
От: elmal  
Дата: 26.04.20 06:14
Оценка: 5 (1)
Здравствуйте, Ватакуси, Вы писали:

В>Бюджет относительно небольшой, поэтому всякие супер-космические решения не годятся.

Тестовое задание чтоль делаешь ?

Тривиально. Получил событие inc n — положил в очередь. Другие потоки, а то и сервера — они вычитывают из очереди и уже атомарно инкрементят счетчик, который либо в памяти, либо это распределенный кеш. Операция read тривиальная, просто достать значение. 100к держать будет даже на одной машине навскидку. Если нужен постгрес, и он оказывается узким местом (нужно проверять) — периодически иожно сбрасывать значение текущего счетчика в базу. По идее задача адекватно решается даже на одном серваке без резервирования.

Подобная концепция вроде как называется CQRS.
Re[4]: Задачка на проектирование
От: Sharov Россия  
Дата: 04.05.20 19:16
Оценка:
Здравствуйте, Pzz, Вы писали:

Pzz>Здравствуйте, Ватакуси, Вы писали:


В>>Да. "Правильное решение" примерно так и должно выглядеть.

В>>Как это осуществить?

Pzz> Т.е. понадобится какой-нибудь load balancer и сетевое хранилище, скорострельность которого нам не очень важна, но оно должено обладать одним хитрым свойством, о котором чуть позже.


А lb то каким должен быть, ибо все эти 8Гб через него же ходят?


Pzz>А что до хранилища, нам от него понадобится один единственный запрос, но хитрый: он должен прибавлять к счетчику, что сказали, и возвращать старое значение. Назовем его read-increment.


Я правильно понимаю, что чтение из хранилища надо в сл. нескольких машин, а не одной?

Pzz>Ну а дальше все относительно просто. Нам понадобится очередь запросов. Изначально очередь пустая. Первый поступивший запрос посылает хранилищу запрос read-increment(0), если это запрос на чтение, и read-incrememt(1), если это запрос на увеличение счетчика, и становится в очередь. Остальные просто становятся в очередь. Запрос в хранилище посылается асинхронно. И еще, мы запоминаем, сколько запросов было в очереди, когда мы послали запрос в хранилище.


Смысл асинхронно, что мы будем делать в этот момент? Мы даже чтение обслужить без результата запроса не можем.
Кодом людям нужно помогать!
Re: Задачка на проектирование
От: scf  
Дата: 04.05.20 19:31
Оценка:
Здравствуйте, Ватакуси, Вы писали:

В>REST-сервис


В>inc n — увеличить глобальное значение на n. После того как запись в базу сделана возвращается код 200. Это гарантия, что счётчик увеличился.

В>read — получить текущее глобальное значение

В>сервис на сервере, который пишет и читает из постгреса.


В>Как сделать / спроектировать систему так, чтобы она работала на 100k или даже миллионе rps и не более 50 мс отклик.


В>Ваши предложения.

В>Бюджет относительно небольшой, поэтому всякие супер-космические решения не годятся.

Надежность какая нужна? Судя по неидемпотентному API, абсолютная точность не требуется. Так что оптимально — синхронизированный счетчик в памяти, который периодически, каждые 10 мс, например, пишется в постгрес.

Для масштабирования разворачиваются несколько нод, каждая со своим собственным счетчиком. read читает данные всех счетчиков и суммирует.
Re[5]: Задачка на проектирование
От: Pzz Россия https://github.com/alexpevzner
Дата: 04.05.20 19:51
Оценка: 5 (1)
Здравствуйте, Sharov, Вы писали:

Pzz>> Т.е. понадобится какой-нибудь load balancer и сетевое хранилище, скорострельность которого нам не очень важна, но оно должено обладать одним хитрым свойством, о котором чуть позже.


S>А lb то каким должен быть, ибо все эти 8Гб через него же ходят?


Вот не знаю, честно. Возможно, у них внутри что-нибудь более легковесное, чем linux TCP/IP stack.

Pzz>>А что до хранилища, нам от него понадобится один единственный запрос, но хитрый: он должен прибавлять к счетчику, что сказали, и возвращать старое значение. Назовем его read-increment.


S>Я правильно понимаю, что чтение из хранилища надо в сл. нескольких машин, а не одной?


Я не понял вопрос.

Pzz>>Ну а дальше все относительно просто. Нам понадобится очередь запросов. Изначально очередь пустая. Первый поступивший запрос посылает хранилищу запрос read-increment(0), если это запрос на чтение, и read-incrememt(1), если это запрос на увеличение счетчика, и становится в очередь. Остальные просто становятся в очередь. Запрос в хранилище посылается асинхронно. И еще, мы запоминаем, сколько запросов было в очереди, когда мы послали запрос в хранилище.


S>Смысл асинхронно, что мы будем делать в этот момент? Мы даже чтение обслужить без результата запроса не можем.


Выгребать запросы из сокетов, парсить их, ставить в очередь, рассылать ответы.

У нас основная нагрузка — это именно HTTP. Затраты на содержательную обработку запросов очень низкие.
Re[6]: Задачка на проектирование
От: Sharov Россия  
Дата: 04.05.20 20:38
Оценка:
Здравствуйте, Pzz, Вы писали:

Pzz>>>А что до хранилища, нам от него понадобится один единственный запрос, но хитрый: он должен прибавлять к счетчику, что сказали, и возвращать старое значение. Назовем его read-increment.

S>>Я правильно понимаю, что чтение из хранилища надо в сл. нескольких машин, а не одной?
Pzz>Я не понял вопрос.

В случае одной машины зачем в базу лезть? Т.е. писать надо, а отвечать можно по памяти.
Кодом людям нужно помогать!
Re[7]: Задачка на проектирование
От: Pzz Россия https://github.com/alexpevzner
Дата: 04.05.20 20:41
Оценка:
Здравствуйте, Sharov, Вы писали:

S>В случае одной машины зачем в базу лезть? Т.е. писать надо, а отвечать можно по памяти.


Если отвечать по памяти, и програмка упадет в процессе записи, не успев ничего записать, то кто-то получит одну и ту же чиселку два раза.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.