All streams
Search
Write a publication
Pull to refresh
56
0
Стас Выщепан @gandjustas

Пользователь

Send message

Посмотрел ещё раз запрос. Он покрутит счётчик зарезервированного, проигнорировав товарное наличие.

Я что-то пропустил этот момент...
1) Вы можете в таблице добавить check constraint
2) Вы можете после запроса обновления проверять что резервы не превышают запасы и откатывать транзакцию в противном случае

Вычисления должны проходить на приложении, на БД рассматривать в нашем кейсе - смысла нет.

Как так вышло и почему:

Мы упирались в диск на БД при вставке резервов (неожиданно самая дорогая и долгая операция в обработке).

Как это связано с остатками? ну даже если вы храните сами заказы вообще в другом хранилище, просто сгенерируйте в программе запрос вида:

UPDATE stock as s
SET reserved = s.reserved - l.quantity
FROM unnest(ARRAY[1,2], ARRAY[1,2] , ARRAY[10,5]) 
  as l(item_id, warehouse_id, quantity)
where s.item_id = l.item_id and s.warehouse_id = l.warehouse_id;

Массивы можно передать параметрами. Такой запрос вам консистентно и атомарно обновит остатки на уровне изоляции read commited. Это даст ОЧЕНЬ высокую степень конкурентности. Вам не придется вешать pg_advisory_xact_lock. Запросы не будут ждать в очереди на блокировку и будут быстрее освобождать соединения.

Проведя множество экспериментов, нашли неочевидное решение: товары резерва лучше всего сериализовать в proto и класть в резерв как поле с типом bytea (сейчас резерв в БД это 1 запись вида {id uuid, … , items bytea }). С этим не удобно работать вне кода, но скорость перекрыла минусы.

Исходя из примера выше вы можете эти три массива хранить как массивы в заказе и работать с ними из запросов Postgres. Я не думаю что это будет занимать больше места, чем сериализованные в protobuf структуры.

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

Это как раз дает возможность ВСЮ логику, в том числе идемпотентность, выполнить за один запрос к СУБД с уровнем блокировки read commited.

Просто нагенерируйте запрос вида:

WITH 
ord AS (
    INSERT INTO orders(id, items, wharehouses, quantities) 
    VALUES ($1,$2,$3,$4) 
    ON CONFLICT DO NOTHING
    RETURNING *
)
UPDATE stock as s
SET reserved = s.reserved - l.quantity
FROM (
  select l.* FROM ord, LATERAL (
    unnest(items, wharehouses, quantities) 
      as l(item_id, warehouse_id, quantity)
  ) l 
where s.item_id = l.item_id and s.warehouse_id = l.warehouse_id;
-- $1 = 'f32048c5-2e69-441d-bd4b-08fa3c413c46'
-- $2 = ARRAY[1,2], --item_id
-- $3 = ARRAY[1,2], --quantity
-- $4 = ARRAY[10,5], --quantity

Интересующие куски кода из сэмпла

Я несколько раз прочитал ваш пример и не понял чем оно лучше, тем то что я привел. Просто вы собираете "буфер" до отправки в ченнел, а я предложил делать это после чтения из ченнела.

  • После чтения всегда можно узнать есть ли еще элементы в ченнеле

  • Как только вы выбрали все что есть в ченнеле в текущий момент вы это можете в одном соединении с БД обработать

  • Не нужно локи вешать руками

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

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

  • Я уже второй коммент подряд пишу запросы, которые не требуют никаких ретраев

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

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

У вас в статье про счетчик написано. Для счетчика вам не нужна пессимистичная блокировка на advisory lock, там read commited достаточно. Для остальной работы тоже более чем достаточно read commited, так как конкуренции там нет.

Для принятия решения о каждом резерве - требуется

1. Знание о его существовании в БД
2. Наличие свободного стока для всех позиций резерва

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

WITH 
ord AS (
    INSERT INTO orders VALUES ('f32048c5-2e69-441d-bd4b-08fa3c413c46') 
    ON CONFLICT DO NOTHING
    RETURNING *
),
lines AS ( --тут можно использовать ord
    INSERT INTO order_lines 
    VALUES 
    ('f32048c5-2e69-441d-bd4b-08fa3c413c46', 1, 1, 10),
    ('f32048c5-2e69-441d-bd4b-08fa3c413c46', 2, 2, 5) 
    ON CONFLICT DO NOTHING 
    RETURNING *
)
UPDATE stock as s
SET reserved = s.reserved - l.quantity
FROM lines l
where s.item_id = l.item_id and s.warehouse_id = l.warehouse_id;

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

То есть клиент таки ждет пока не заполнится буфер, даже если нагрузки на базу нет и вы можете сразу обработать его запрос? Тогда ваш буфер выглядит еще более сомнительно.

Еслиб можно было крутить только счётчик без всей дополнительной работы - было бы значительно проще :)

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

Теперь понятно откуда такие проблемы с перфомансом. Задача по обновлению остатков при поступлении заказа в postgres решается на уровне Read Commited одним запросом, а вы на advisory сделали медленный вариант Serializable в SQL Server\MySQL и кучу раз бегаете в базу.

  1. Приходит запрос -> кладём его в активный буфер (массив)

  2. Добавляем буфер в очередь на обработку (Channel), если его ещё там нет

  3. …В буфер продолжают добавляться новые запросы

Вам тогда не нужен буфер. Ченнел - уже очередь (LIFO буфер). Каждый запрос пишет в writer, а на стороне reader делаете:

while(await reader.WaitToReadAsync())
{
    List<T> batch = new();
    while(batch.Count < MAX_ITEMS_IN_BATCH 
          && reader.TryRead(out var item)) batch.Add(item);  
    await ProcessBatch(batch);    
}

Если убрать промежуточное звено в виде буфера и класть запросы напрямую в Channel, то на стороне workerа надо будет как-то набирать нужное количество запросов, появятся какой-то искусственный delay при слабом трафике (если трафика мало, то запросы могут уходить в работу слишком поздно, потому кроме счётчика требуется и дедлайн на время накопления)

В том-то и дело, что нет. Код выше выбирает просто все запросы, которые есть сейчас в очереди. Он конечно дает задержку, пока вы наберете свои 50, или сколько там у вас запросов, но она на три порядка меньше чем один раунд-трип в базу.

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

Сделаете пример с транзакционным orleans с 100+ RPS. Даже не на товар, а всего на систему, которая запускается на локальной машине и может масштабироваться?

Single-threaded execution within a grain

У вас больше одного сервера, где выполняются ваши grains

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

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

Там (В методе BookItems у объекта ShopItem) в транзакции

В транзакции база сама проверяет ограничения ПОСЛЕ записи - уникальность (при обновлении индекса), внешние ключи, и прочие check constraints. Вам не нужно придумывать транзакционность в приложении

https://learn.microsoft.com/ru-ru/dotnet/orleans/grains/transactions

А вы сами читали? Транзакционность в Orleans реализована за счет сохранения состояния в транзакционном хранилище, то есть в БД.

То есть вы этим буфером просто батчите обновления БД, чтобы было не одно подключение к базе на каждый запрос пользователя?

Почему тогда не воспользоваться стандартными System.Threading.Channels? Делаете консьюмеров для него столько, сколько вы хотите максимум соединений открывать. Можно даже написать свой монитор количества элементов в канале и спавнить новые задачи для открытия.

При любой деградации БД мы упрёмся в пул подключений на приложении / пуллере БД

Так вы в любом случае упретесь, не? У вас же несколько инстансов делают одно и то же и все они сходятся только в СУБД

В БД вместо 1 записи будет постоянно удалятся и добавляться новые вместо обновления одного поля (IO)

Вы же в курсе, что в postgres обновление строки = пометка удаления (запись xmax) + создание новой строки?

Как вы добьетесь того, чтобы stock >= reserved для всех записей всегда? Если правила нарушается, то резерв надо отменить, а пользователю сказать "простите, товар закончился на складе"?

Я так и не понял: буфер это что? Где он хранится?

Как работает со стороны пользователя? Он кликает оплатить и идет курить пока буфер заполнится?

А если за время заполнения буфера клиент запрос отменит?

примерно 10 сек. В цикле создается n задач, каждая из которых завершается через 10 сек. Поэтому весь код завершится за 10сек+время создания n объектов и помещение n значений в очередь таймера.

Банальный ответ:

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

продвинутый ответ:

Код на C# из статьи вообще не порождает потоков и не использует пул. Delay порождает объект с одним полем, а один поток таймера (один на весь процесс) меняет значение поля через 10 сек и запускает асинхронное продолжение в конце.

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

с другой стороны асинхронность в современном мире это не про параллельность вычислений, а про параллельность ожидания. И c#/dotnet под это очень хорошо заточен.

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

Причем это мнение подано как какое-то важное и нужное для других.

а этот пост - неуклюжая попытка заигрывать с аудиторией с целью оправдаться за свой непрофессионализм или наоборот «укрепить» свои позиции.

хорошо что замисуовали, надеюсь карму в минус собьют. ИМХО такие «специалисты» на Хабре не нужны

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

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

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

А должно быть грустно. Вы не осознаете проблему, которую создаете себе и другим.

Продолжайте в том же духе и скоро заметите как все «срочные вопросы», от которых что-то зависит, пойдут мимо вас.

Так он потом скажет что не видел эти протокол и вообще не говорил такого.

Почему же?


Во-первых для начала наверное стоит прочитать Ильяхова про его правила деловой переписки.

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

И самые лучшие способы добиться того чтобы люди были тебе не рады:

  • Звонить или кидать встречи без предупреждения или согласования

  • Писать письма крупным или красным шрифтом

  • Везде добавлять руководителя в переписку

  1. Верно

  2. Гори в аду

  3. Гори в аду

  4. Гори в аду

  5. Это как вообще? кто и или что кого должно ограничить?

  6. А зачем протокол без ответственных? Это не просто стандартная практика, а это единственное осмысленное действие в протоколе

  7. Очень сомневаюсь что от количества "пожалуйста" хоть что-то зависит

Игнорят письма, потому что людям есть чем заняться вместо ответов на тупые вопросы.

Посоветую бросить работы в ИТ, поискать гденить в другом месте.

1
23 ...

Information

Rating
Does not participate
Location
Москва, Москва и Московская обл., Россия
Date of birth
Registered
Activity