Накопление значения с конкурентным доступом

Thamerlan
Дата: 08.01.2015 00:16:28
Всем привет. Помогите решить такую задачу.

Имеется общий фонд (пул) для накопления джекпота. С каждой ставки в игре этот фонд увеличивается на какую-то сумму (процент от ставки). В табличном виде что-то типа:
-jackpot_id INTEGER
-amount NUMERIC
,где с каждой ставки идёт
UPDATE table SET amount=amount+<%_от_ставки> WHERE jackpot_id = <тут_id>;

Когда это всё делали, то игроков было немного и борьба за захват записи была небольшой. Но с увеличением кол-ва игроков само-собой началась борьба за этот ресурс. Попробовали добавить дополнительное поле part_id - разбить джекпот на части, но это не дало ощутимой разницы, что очень удивило, так как при, скажем, 50 игроках и без part_id задержек практически нет, а при
800 игроках и 64 part_id задержки появляются, при том, что борьба за общую запись должна была снизиться.

Есть определенные требования, которые необходимо соблюсти:
1) Нельзя пропускать ни одной ставки. То есть все финансы должны точно совпадать
2) Сумма обновленного джекпота должна быть возвращена сразу после обновления (это, так скажем, крайне желательное условие)
3) Нельзя аккумулировать джекпот отдельно для каждого пользователя (то есть нельзя добавить user_id в эту таблицу), так как пользователей, учавствующих в розыгрыше джекпота, может быть сотни тысяч, и:
а) выборка общей суммы джекпота становится тяжелой.
б) джекпот могут часто выигрывать, и необходимо лочить все записи.

Есть ли какие-то идеи как лучше (правильнее) организовать накопление джекпота?
Maxim Boguk
Дата: 08.01.2015 03:11:08
Thamerlan,

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

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

PS: включите log_lock_waits в конфиге, может что то интересное увидите.

--Maxim Boguk
www.postgresql-consulting.ru
Ivan Durak
Дата: 08.01.2015 09:36:02
Thamerlan
Всем привет. Помогите решить такую задачу.

Имеется общий фонд (пул) для накопления джекпота. С каждой ставки в игре этот фонд увеличивается на какую-то сумму (процент от ставки). В табличном виде что-то типа:
-jackpot_id INTEGER
-amount NUMERIC
,где с каждой ставки идёт
UPDATE table SET amount=amount+<%_от_ставки> WHERE jackpot_id = <тут_id>;

Когда это всё делали, то игроков было немного и борьба за захват записи была небольшой. Но с увеличением кол-ва игроков само-собой началась борьба за этот ресурс. Попробовали добавить дополнительное поле part_id - разбить джекпот на части, но это не дало ощутимой разницы, что очень удивило, так как при, скажем, 50 игроках и без part_id задержек практически нет, а при
800 игроках и 64 part_id задержки появляются, при том, что борьба за общую запись должна была снизиться.

Есть определенные требования, которые необходимо соблюсти:
1) Нельзя пропускать ни одной ставки. То есть все финансы должны точно совпадать
2) Сумма обновленного джекпота должна быть возвращена сразу после обновления (это, так скажем, крайне желательное условие)
3) Нельзя аккумулировать джекпот отдельно для каждого пользователя (то есть нельзя добавить user_id в эту таблицу), так как пользователей, учавствующих в розыгрыше джекпота, может быть сотни тысяч, и:
а) выборка общей суммы джекпота становится тяжелой.
б) джекпот могут часто выигрывать, и необходимо лочить все записи.

Есть ли какие-то идеи как лучше (правильнее) организовать накопление джекпота?

имхо, асинхронно.
т.е. отдельный процесс, который постоянно пробегает по новым ставкам и апдейтит джекпот.
Он один - никаких ожиданий бокировок.
думаецца
Дата: 08.01.2015 11:49:13
Ivan Durak
Thamerlan
Всем привет. Помогите решить такую задачу.

Имеется общий фонд (пул) для накопления джекпота. С каждой ставки в игре этот фонд увеличивается на какую-то сумму (процент от ставки). В табличном виде что-то типа:
-jackpot_id INTEGER
-amount NUMERIC
,где с каждой ставки идёт
UPDATE table SET amount=amount+<%_от_ставки> WHERE jackpot_id = <тут_id>;

Когда это всё делали, то игроков было немного и борьба за захват записи была небольшой. Но с увеличением кол-ва игроков само-собой началась борьба за этот ресурс. Попробовали добавить дополнительное поле part_id - разбить джекпот на части, но это не дало ощутимой разницы, что очень удивило, так как при, скажем, 50 игроках и без part_id задержек практически нет, а при
800 игроках и 64 part_id задержки появляются, при том, что борьба за общую запись должна была снизиться.

Есть определенные требования, которые необходимо соблюсти:
1) Нельзя пропускать ни одной ставки. То есть все финансы должны точно совпадать
2) Сумма обновленного джекпота должна быть возвращена сразу после обновления (это, так скажем, крайне желательное условие)
3) Нельзя аккумулировать джекпот отдельно для каждого пользователя (то есть нельзя добавить user_id в эту таблицу), так как пользователей, учавствующих в розыгрыше джекпота, может быть сотни тысяч, и:
а) выборка общей суммы джекпота становится тяжелой.
б) джекпот могут часто выигрывать, и необходимо лочить все записи.

Есть ли какие-то идеи как лучше (правильнее) организовать накопление джекпота?

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

первый ответ миши очень в тему

а про разделение и прочие бубны -- я делал так: табличка без уникью по jackpot_id, с суррогатом id, когда обновляем -- пытаемся захватить nowait любую с данным джекпотом, и отапдейтить. а агрегацию можно и джобом. (я захватывал FOR UPDATE тут же в цикле массив id, все их кидал с новой суммой в одну запись, остальные тут же удалял).
думаецца
Дата: 08.01.2015 12:31:01
ps
1. перепутал макса с мишей, извиняюсь

2. в длине цепи из N конкурентов участвует еще и длина единичной "бесконкурентной" транзакции. если она простая -- пишем только данные и сумму -- то это, скорее всего [там должно быть шаманство с записью не напрямую в таблицы, а послежовательно в "журналки", чтобы не позиционировать головки], время последовательного доступа на запись, и, если в память таблица сумм не ложится [что вряд ли] -- время произвольного [-- что плохо] доступа на чтение. для 1000 доступов к суммам 1000 джекпотов, которые почему-то на разных страницах и может быть нехорошо, но если все суммы в памяти -- время рендом-доступа выпадет и все станет много приличнее. т.е. предлагаю сначала пошерстить длину единичной транзакции. если там много левой логики -- попытаться всю левизну скинуть в асинхрон. ну а параллельное не блокирующее синхронное накопление я описывал выше
Thamerlan
Дата: 08.01.2015 12:40:32
Maxim Boguk
Thamerlan,

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

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

PS: включите log_lock_waits в конфиге, может что то интересное увидите.

--Maxim Boguk
www.postgresql-consulting.ru


Максим, я специально не стал указывать про железо, логи и прочее. Просто мне, в идеале, хотелось бы найти альтернативные решения задачи. Но если их нет, тогда конечно последний выход это оптимизация имеющегося решения.
Так вот по железу:
1) С дисковой проблем нет (ssd в 5 или 6 раиде)
2) Нехватка CPU имеет место быть. E5-2620 0 @ 2.00GHz с 1 cpu c 12 ядрами, причем на vmware. Но как я уже писал, я ожидал улучшения после разбивки фонда джекпота на части, но его не получил. То есть понятно, что перейдя на машинку с 40 ядрами я улучшу ситуацию, но проблему в принципе не решу.

По логам:
log_lock_waits на 1сек включал. При "50 игроках и без part_id" ничего не получал. Ну может пару-тройку раз было за 10 минутный stress test.
При "800 игроках и 64 part_id" засыпало wait'ами по 4-8 секунд при пополнении фонда, и временами до 30 секунд при выигрыше джекпота.

А вот насчёт этого:
Maxim Boguk
Там например возможен вариант конкуренции да, но вызванной не собственно обновлением строки а долгой транзакцией охватывающей этот update.

наверное буду думать. Транзакцией управляет приложение на java. Хотя, ява-разрабы говорят, что сразу после вызова хранимки на увеличение джекпота делают коммит. Но наверное стоит попробовать самому вызывать через dblink функцию пополнения с моментальным коммитом.


Ivan Durak
пропущено...
имхо, асинхронно.
т.е. отдельный процесс, который постоянно пробегает по новым ставкам и апдейтит джекпот.
Он один - никаких ожиданий бокировок.

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

думаецца
ps
2. в длине цепи из N конкурентов участвует еще и длина единичной "бесконкурентной" транзакции. если она простая -- пишем только данные и сумму -- то это, скорее всего [там должно быть шаманство с записью не напрямую в таблицы, а послежовательно в "журналки", чтобы не позиционировать головки], время последовательного доступа на запись, и, если в память таблица сумм не ложится [что вряд ли] -- время произвольного [-- что плохо] доступа на чтение. для 1000 доступов к суммам 1000 джекпотов, которые почему-то на разных страницах и может быть нехорошо, но если все суммы в памяти -- время рендом-доступа выпадет и все станет много приличнее. т.е. предлагаю сначала пошерстить длину единичной транзакции. если там много левой логики -- попытаться всю левизну скинуть в асинхрон. ну а параллельное не блокирующее синхронное накопление я описывал выше

... много левой логики. Да с этим буду разбираться. Как уже написал выше, попробую внешний вызов через SQL/MED, чтобы понять, что лишняя логика АПП не является тут причиной.

И всё же, если есть какие-то другие архитектурные решения для данной задачи, буду рад услышать.
Спасибо.
Ivan Durak
Дата: 08.01.2015 13:19:09
на тебе еще одну идею - вместо
UPDATE table SET amount=amount+<%_от_ставки> WHERE jackpot_id = <тут_id>;

1. добавь id как PK в таблицу джекпотов (еще лучше разбить на таблицы для каждого джекпота, но можно и не разбивать, тогда правда всесто подзапроса будет другой - с limit 1)

2. делай Insert into table (id, jackpot_id, amount) select sequence.nextval, <тут_id>, <%_от_ставки> + (select amount from table
where jackpot_id = <тут_id> and id = sequence.curval)

3. асинхронно чистим неакуальную историю.
Maxim Boguk
Дата: 08.01.2015 13:32:57
Thamerlan,

автор
При "800 игроках и 64 part_id" засыпало wait'ами по 4-8 секунд при пополнении фонда, и временами до 30 секунд при выигрыше джекпота.


из этого скорее всего следует что где то образуются 30 секундные транзакции...
вообще совет для тестирования
1)dealock_timeout=100ms + log_lock_waits
+
2)log_min_duration_statement=0 + log_line_prefix=%m %p %u@%d from %h [vxid:%v txid:%x] [%i]

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

PS: обязательно разнести для тестирования базу и приложение на разное железо... так как если приложение сьедает все CPU то базе будет очень грустно жить.

--Maxim Boguk
www.postgresql-consulting.ru
NikolayV81
Дата: 08.01.2015 15:15:10
Thamerlan
...

Есть ли какие-то идеи как лучше (правильнее) организовать накопление джекпота?


Есть простое решение, джепот в память, в базу штамп последнего розыгрыша, в случае падения сервера джекпот пересчитывать при старте с момента после штампа розыгрыша.
Thamerlan
Дата: 08.01.2015 15:44:13
Ivan Durak
на тебе еще одну идею - вместо
UPDATE table SET amount=amount+<%_от_ставки> WHERE jackpot_id = <тут_id>;
2. делай Insert into table (id, jackpot_id, amount) select sequence.nextval, <тут_id>, <%_от_ставки> + (select amount from table
where jackpot_id = <тут_id> and id = sequence.curval)


Интересная версия хранить актуальную сумму в последней записи, только я не уверен, что sequence.curval всегда даст последнее правильное значение фонда при большой параллельной нагрузке. Тогда лучше, наверное , advisory lock использовать для организации последовательного доступа к последней записи. И фрагментация таблицы из-за удалений неактуальных значений будет огого. Но я возьму эту идею на подумать. Спасибо.

Maxim Boguk
Thamerlan,

вообще совет для тестирования
1)dealock_timeout=100ms + log_lock_waits
+
2)log_min_duration_statement=0 + log_line_prefix=%m %p %u@%d from %h [vxid:%v txid:%x] [%i]

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

--Maxim Boguk
www.postgresql-consulting.ru


В данном случае я и так знаю, что lock'и идут из-за борьбы за этот update. Уменьшив "dealock_timeout=100ms + log_lock_waits" я узнаю ещё много нового, но хотелось бы сначала побороть главный bottleneck :)

NikolayV81

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


Так как я явно упираюсь в CPU, а не в IO, то джекпот в памяти не даст в данном случае никакого прироста производительности, так как там тоже надо организовывать последовательный доступ к общему ресурсу. Разве что только сам Postgresql медленно ставит/снимает lock'и с ресурса и другое "хранилище" будет просто это делать быстрее.