Pyramid REST API: как безопасно обрабатывать одновременный доступ к данным?

Я работаю над REST API для веб-сервиса, использующего Pyramid и Cornice; данные на стороне сервера обрабатываются с использованием SQLAlchemy и MySQL. Веб-сервер nginx использует uwsgi, и он настроен для запуска нескольких процессов Python:

[uwsgi]
socket = localhost:6542
plugins = python34
...
processes = 2 # spawn the specified number of workers/processes
threads = 2 # run each worker in prethreaded mode with the specified number of threads

проблема

Предположим, стол customers на стороне сервера. Используя API, можно читать данные клиентов, изменять их или удалять. В дополнение к этому есть другие функции API, которые читают данные клиента.

Я мог бы сделать несколько вызовов API одновременно, которые затем конкурируют за один и тот же ресурс клиента:

# Write/modify the customer {id} data
curl --request POST ... https://some.host/api/customer/{id}
# Delete customer {id} and all of its associated data
curl --request DELETE https://some.host/api/customer/{id}
# Perform some function which reads customer {id}
curl --request GET ... https://some.host/api/do-work

По сути, это проблема Readers-Writers, но поскольку задействовано более одного процесса, традиционная синхронизация потоков с использованием блокировок / мьютексов / семафоров здесь не будет работать.

Вопрос

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

Решения (?)

  • Я не думаю, что имеет смысл отмечать / отмечать клиента {id} как locked потому что SQLAlchemy кеширует такие модификации, и flush() не кажется достаточно атомным в этом контексте?
  • В этой статье описывается использование HTTP ETag для управления общими ресурсами.
  • Можно также использовать Redis в качестве распределенного диспетчера блокировки для спин-блокировки, чтобы обернуть функцию просмотра?
  • А как насчет менеджера транзакций Pyramid?

2 ответа

Я предполагаю, что вы имеете дело с одной базой данных MySQL, и ваши блокировки не должны покрывать другие ресурсы (Redis, сторонние API и т. Д.). Я также предполагаю, что вашим функциям на стороне клиента не нужно работать с данными транзакций (поддерживать сеанс через несколько вызовов API), вы просто хотите предотвратить одновременный доступ API к ошибкам в вашей базе данных.

Существует два вида блокировок: пессимистическая и оптимистическая.

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

Оптимистическая блокировка - это то, что вы можете легко избежать с помощью баз данных SQL. Если две транзакции конкурируют из одного и того же ресурса, база данных фактически обрекает одну из транзакций, и среда приложения (в данном случае Pyramid + pyramid_tm) может повторить транзакцию N раз, прежде чем отказаться.

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

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

  • pyramid_tm связывает жизненный цикл транзакции с HTTP-запросом, что очень естественно с точки зрения веб-разработчика

  • pyramid_tm может связать другие события с успешными транзакциями, например pyramid_mailer отправляет электронную почту пользователям, только если транзакции совершают

  • pyramid_tm хорошо протестирован и основан на ZODB transaction менеджер транзакций, который используется в производстве с начала 2000 года

  • Убедитесь, что в сеансе SQLAlchemy установлен уровень SERIALIZABLE SQL - вы начинаете с самой высокой модели согласованности. Вы можете снизить это требование к производительности, если знаете, что вызовы API допускают это - например, вызовы, выполняющие статистику только для чтения.

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

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

дальнейшее чтение

Обычно вы начинаете с определения того, какая модель согласованности является приемлемой. Чем слабее ваши требования согласованности, тем легче становится эта проблема на стороне сервера.

Например:

Можно ли сойти с оптимистичного параллелизма? Т.е. предположить, что у вас есть блокировка, выполнить свою операцию, но определить, когда возникает ситуация с параллелизмом, чтобы вы могли правильно восстановиться? Это может быть хорошим вариантом, если вы не ожидаете много столкновений. Sqlalchemy должен быть в состоянии обнаружить, что он обновляет строку, которая уже была изменена, например.

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

Другие вопросы по тегам