Pull to refresh

Comments 31

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

Решение немного обработать напильником и можно пилить код.

Цель: обработать запросы клиентов так, чтобы время ожидания было справедливым, учитывая количества транзакций в запросе. По крайней мере, я так понял задачу.

… результат обработки записывают в таблицу истории обработки транзакций…

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

Что значит время ожидания должно быть справедливым? За счет рационального выбора порядка обслуживания можно сократить суммарное время ожидания обслуживания клиентов, но никак не время собственно обслуживания.

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

Есть большое отличие от задачи одного станка. В нашем случае не учитываются такие значение как: «Время исполнения транзакции», «значение накладных ресурсов, для исполнения транзакции», «Важность транзакции». Все транзакции для системы условно равны.

Что значит время ожидания должно быть справедливым?

Критерии «справедливости» я описал в самом начале задачи:

Необходимо исключить ситуации, когда:

Тысячи клиентов по несколько транзакции ожидают двух, у которых по 10000 транзакций

Несколько клиентов по 10000 ждут тысячи клиентов по несколько транзакций.

Если общий пул транзакций не подходит, сделайте параллельные вычисления в отдельных потоках.

А как насчет ситуации, когда из за постоянного наплыва мелких транзакций, большая никогда не выполнится?

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

Такой сценарий невозможен. Если речь про запросов с менее 100 транзакций то они выполняются в отдельном инстансе (он может быть в другом сервере…).

Допустим, у нас 10 тысяч запросов с 100-300 транзакциями в запросе, и несколько запросов с 1000 транзакциями. Тогда получается, мы объединяем транзакции в пачки из 10 тысяч транзакций, по одной транзакции с каждого запроса. И одна транзакция из большого запроса выполнится раз из 10к транзакций. Да, большому запросу придется ожидать дольше, так как каждая транзакция выполняется в цикле из 10к транзакций (пока не закончатся пачки из мелких транзакций), но ресурсы ограничены, по другому нельзя. Возможно, я не прав, можете предложить более релевантный сценарий.

При желании, можно указать в docker compose, что для транзакций с 100-300 транзакциями в запросе, нужно выделить отдельный инстанс, как для запросов с менее 100 транзакций

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

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

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

В терминах операционных систем разделяемый ресурс - это процессорное время. В терминах вашей задачи - это количество обрабатываемых транзакций.

Все пачки придется разделить на блоки по 1000 транзакций, блоки обрабатывать последовательно для каждой пачки.

В итоге получается т.н. планировщик, и это тоже термин из области многозадачности.

Да, именно. Одна выполненная транзакция запроса – точка синхронизации, после которой переходим на выполнение транзакции со следующего запроса.

Перечитал статью, посмотрел исходники:
когда вы в промежуточном сервисе ProcessExecutor создали список запросов - вы создали у своего сервиса state, он как-бы перестал быть "чисто-рестовым".
public static List<RequestDTO> Requests { get; set; } = new List<RequestDTO>();
Теперь при отключении инстанса (а при масштабировании на это надо закладываться) этот state потеряется. Клиенты должны будут самостоятельно решать, какие транзакции отправлять повторно, какие признать выполненными.

Как вы будете бороться с этим тонким местом?

Мелочи не трогаю.

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

Кстати вполне практическое приложение: я заехал в мак вкусно-и-точка взять кофе. Но в ресторане было много заказов и кофе я дожидался минут 15. Хотя сама операция наливания кофе занимает от силы минуту, но передо мной было много "семейных" заказов, которые не умещались на один поднос.

Очевидно в маке (буду так называть) используется обычная FIFO очередь. Без потери общности можно считать, что обработчик очереди тоже один. Чтобы улучшить обслуживание они могли бы каждому заказу назначать некоторый вес Wi. В случае мака это время приготовления (они могут вычислить), а в случае топикстартера - количество транзакций в запросе.

  • Далее надо ввести функцию долга D(w), такую что D(Wi) < Wi. Для примера D(w) = 0.5 * w

  • После обработки каждого заказа считать значение долга d = (d - Wi + D(Wi)) max 0.

  • Следующим заказом брать первым из очереди у которого Wi <= d, или первый из очереди если под условие ни один не подходит, в начале обработки d = 0

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

Если брать принцип вкусно и точка, тогда надо посмотреть как работает бэк офис. Во вкусно и точно есть касса - место где посетитель озвучивает и оплачивает свой заказ. Далее есть бэк офис где один сотрудник все время жарит картошку, другой котлеты, третий режит булки и т.д. А есть ещё стол сборки заказа где человек, по имеющейся информации, собирает заказы. Так если первый человек заказал комбо меню, а второй только кофе. Второй получит свой заказ быстрее потому что задействовано меньше человек. С точки зрение распределения производственных сил это хорошо. С точки зрения потребителя это несправедливо потому, что первый человек сделавший заказ должен получить его первым. Но понятие справедливости здесь условное. Человек сделав большой заказ понимает что ждать его дольше чем мелкий заказ.

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

Что касается функции долга: если 2 посетителя заказали по три семейных комбо, а третий посетитель только колу, тогда при распределенной работе множество людей будут готовить компоненты семейного комбо, а для третьего посетителя один человек нальет колу и он уйдёт. А если оценивать вес заказа, тогда семейное комбо будет весомее с точки зрения бизнеса, чем посетитель заказавший колу. Однако распределение ресурсов позволяет максимально быстро удовлетворить потребности всех посетителей.

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

Что касается функции долга: если 2 посетителя заказали по три семейных комбо, а третий посетитель только колу, тогда при распределенной работе множество людей будут готовить компоненты семейного комбо, а для третьего посетителя один человек нальет колу и он уйдёт.

Предположим сборщик один. В текущем виде от соберет сначала первый крупный заказ, потом второй, а потом только отдаст вам колу.

Если будет действовать по моему алгоритму, то после первого крупного заказа появится "долг" в размере Х, а кола гарантированно будет иметь вес меньше Х (если нет, то неправильно функцию долга подобрали). И вы сразу после обработки крупного заказа её получите.

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

В том то и прикол, что не будет. Самые большие прибыли рестораны быстрого питания делают на напитках. Там наценка на себестоимость выше 100%. В вот кухня сильно дороже.

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

Не могли бы пояснить, пожалуйста. Выглядит так, что d <= 0, а Wi > 0. Условие Wi <= d никогда не выполнится.

Неверно формулу записал, надо было так: d = ((d - Wi) max 0) + D(Wi)

Спасибо.

Разве у них все так и не устроено? В БК, во всяком случае, если кто-то перет просто лимонад, то ему наливают почти без очереди.

Что касается кода из статьи, то автор не учел, что когда у всех клиентов будет меньшее 100 транзакций в заказе, то второй инстанс будет отдыхать. Или наоборот, все элементы в очереди окажутся чуть больше порога. Этот алгоритм хоть и "справедливый", но нифига не эффективный.

Если оба инстанса бегут в одной машине, то даже если второй инстанс будет простаивать, лишний инстанс не использует ресурсы машины (я не имею ввиду память и т.д. я про ресурсы на "обработку транзакций"). Можно считать, что работает один инстанс. А так, в идеале, нужно придумать какое-нибудь решение, чтобы создавать/удалять инстансы релевантно, по росту нагрузки.

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

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

Почему не использует? Вы заранее знаете что у вас все инстансы на одной физической машине развернуты? Масштабировать не собираетесь?

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

Я может чего не понял в постановке, но что мешает получить сообщение с транзакциями (без учёта их количества) и по одной записывать в ту же кафку? Тогда сообщения от всех клиентов будут равномерно перемешаны с учётом времени прихода. И также горизонтально масштабируется путем создания нужных партий и увеличения консьюмеров

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

Грубо говоря - получили 10к транзакций. Присваиваем каждой из них случайное число в диапазоне 0..100к и пишем их в БД.

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

То есть вычитали скажем одну, самую низкоранговую транзакцию с рангом 157. Следующая транзакция например 301. Тогда у всех остальных ранг понизили на 301. И транзация 301 стала транзакцией с нулевым рангом.

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

PS только обратил внимание, что gandjustas предложил в принципе то же самое со своей функцией долга

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

Генератор случайных чисел не подходит, так как кукушата, скорее всего, будут равномерно распределены не в начале ранга.

Вы предлагаете запросу, с одной транзакцией случайно сгенерировать ранг 7539 и ждать, пока 7538 транзакций отработают, пока кукушонку не дойдет очередь. Согласитесь, мой вариант справедливее.

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

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

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

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

Есть ещё вариант: реализовать список из клиентов, где у каждого клиента своя очередь транзакций. И можно выбирать поочередно из каждой очереди сообщения. Этот вариант более лоялен к клиентам с мелкими транзакциями. А крупняки получат могут получить приличную задержку.

Вариант 3 - это вариация варианта 2: оценивать при вычитывании размер очереди. Чем больше размер очереди, тем больше у этой очереди приоритет вычитывания транзакций. И после каждого вычитывания пересчитывать приоритеты

Получится примерно так-2 клиента: 10к транзакций и 100 транзакций. И на каждую 1000 транзакций из первой очереди считаем 10 транзакций из второй. Этот вариант позволяет уровнять время обслуживания всех клиентов.

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

Я разве не реализовал тоже самое?

Кажется, не то же самое. Отдельный инстанс на мелкие транзакции неравно отдельному инстансу на клиента

Рашили похожую задачу используя hash exchange который равномерно размазывает пользователей на N очередей и сервис обработчики с подпиской на все очереди и лимитом в количество обрабатываемых сообщений из каждой очереди (грубо говоря до 8 потоков вообще и не более двух потоков на одну очередь).

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

Придумать функцию двух параметров: время прихода сообщения и количество транзакций. Фунция монотонно убывает от времени и растёт от количества транзакций. Сообщения балансировщих скидывает в кучу. На вершине всегда будет наиболее приоритетное, его балансировщих и будет скармливать обработчикам. Осталось поковыряться с арифметикой и придумать функцию.

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

если у вас N клиентом которые шлют транзакции, назовём нормальными словами хотят провести платежи через M банков.

При этом в Росси около 200 банков, т.е. N >> M.

Очередь должна быть на каждый банк, так как это API банка который будет тормозить.

Далее не ясно мы списываем деньги или зачисляем.

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

ещё одна проблема- если не прошла 1001 -Аня транзакция на 1000000 млн рублей, то 1002 на 1 рубль может пройти. Скорее всего данных от остатках на счете у клиента у вас нет, так что в случае получения отказа на на транзакцию -надо попытаться пропихнуть транзакции с меньшей суммой.

соответственно вы не можете просто перемешать платежи клиента с платежами других клиентов.

В общем- надо глубже вникать в бизнес процесс а не в микросервисы.

И последнее: закон Мура - экспоненциальный , а микросервисы масштабируются линейно, т.е. выгоднее купить более производительный Помпею но и не париться с микросервисами.

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

```C#

//No

HttpClient client = new HttpClient();```

//Yes

public void ctor()

{

_client = new HttpClient();

}

Sign up to leave a comment.

Articles