Проблемы с конкуренцией в Google App Engine

У меня проблемы с конкуренцией в Google App Engine, и я пытаюсь понять, что происходит.

У меня есть обработчик запросов, помеченный:

@ndb.transactional(xg=True, retries=5) 

..и в этом коде я получаю кое-что, кое-что обновляю и т. д. Но иногда такая ошибка появляется в журнале во время запроса:

16:06:20.930 suspended generator _get_tasklet(context.py:329) raised TransactionFailedError(too much contention on these datastore entities. please try again. entity group key: app: "s~my-appname"
path <
  Element {
    type: "PlayerGameStates"
    name: "hannes2"
  }
>
)
16:06:20.930 suspended generator get(context.py:744) raised TransactionFailedError(too much contention on these datastore entities. please try again. entity group key: app: "s~my-appname"
  path <
    Element {
      type: "PlayerGameStates"
      name: "hannes2"
    }
  >
  )
16:06:20.930 suspended generator get(context.py:744) raised TransactionFailedError(too much contention on these datastore entities. please try again. entity group key: app: "s~my-appname"
  path <
    Element {
      type: "PlayerGameStates"
      name: "hannes2"
    }
  >
  )
16:06:20.936 suspended generator transaction(context.py:1004) raised TransactionFailedError(too much contention on these datastore entities. please try again. entity group key: app: "s~my-appname"
  path <
    Element {
      type: "PlayerGameStates"
      name: "hannes2"
    }
  >
  )

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

Я не понимаю, почему это происходит. Глядя на строку в моем коде, возникает исключение, я запускаю get_by_id на совершенно другом объекте (Round). «PlayerGameStates», имя «hannes2», которое упоминается в сообщениях об ошибках, является родительским элементом другого объекта GameState, который был get_async: ed из базы данных несколькими строками ранее;

# GameState is read by get_async
gamestate_future = GameState.get_by_id_async(id, ndb.Key('PlayerGameStates', player_key))
...
gamestate = gamestate_future.get_result()
...

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

Но может ли это случиться и при чтении сущностей? ("приостановленный генератор получить .." ??) И это происходит после 5 попыток транзакции ndb.transaction ..? Я не вижу в журнале ничего, что указывало бы на то, что были сделаны какие-либо повторные попытки.

Любая помощь приветствуется.


person boffman    schedule 03.10.2015    source источник
comment
Я бы посмотрел на вашу ключевую структуру. Разногласия не только на уровне сущностей. Также нужно обследовать родителей. Посмотрите на масштабы своих групп сущностей, чтобы понять, почему у вас возникают разногласия.   -  person Tim Hoffman    schedule 04.10.2015
comment
Спасибо. Я старался, чтобы группы сущностей были небольшими и избегали разногласий, но я изучу больше. В этом случае возникла конкуренция в группе сущностей с родительским ndb.Key("PlayerGameStates", "hannes2"), верно? Я до сих пор не понимаю, почему чтение из него вызывает исключение / конфликт? Где я могу узнать об этом подробнее ..?   -  person boffman    schedule 04.10.2015
comment
@TimHoffman Хорошо, только что нашел это в документации по межгрупповым транзакциям: Примечание. Первое чтение группы сущностей в транзакции XG может вызвать исключение TransactionFailedError, если существует конфликт с другими транзакциями, обращающимися к той же группе сущностей. Это означает, что даже транзакция XG, которая выполняет только чтение, может завершиться ошибкой из-за исключения параллелизма.   -  person boffman    schedule 04.10.2015
comment
Попробуйте создать архитектуру своего приложения, чтобы использовать очереди задач (вы можете ставить задачи в очередь переходно) для обновления корневых сущностей и / или использовать сегментирование. В большинстве случаев возможно иметь решение с независимыми объектами и обновлять агрегаты / корни / соседи через очереди задач. Не начинайте транзакцию, пока не измените данные. Это хороший способ сначала прочитать объект, проверить, нужно ли его изменить, и если да, то начать транзакцию.   -  person Alexander Trakhimenok    schedule 05.10.2015


Ответы (1)


Да, конкуренция может происходить как при чтении, так и при записи.

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

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

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

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

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

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

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

Еще одна вещь, которая может помочь (но только немного), - это использование более быстрого (также более дорогого) типа экземпляра - более короткое время выполнения приводит к немного меньшему риску перекрытия транзакций. Я заметил это, так как мне нужен был экземпляр с большим объемом памяти, который тоже оказался быстрее :)

person Dan Cornilescu    schedule 03.08.2017