Дедлок? – Просто добавим ретрай, и так сойдёт!
Почему две транзакции, обновляющие таблицы в разном порядке, закольцовывают блокировки и как предотвратить это до того, как упадёт прод? Ирина Лагерь, старший архитектор баз данных и тимлид команды разработки баз данных в Altenar, — о том, почему ретраи не лечат дедлоки и где в коде прячутся настоящие виновники.
Почти все программисты рано или поздно встречаются с дедлоками, особенно если нагрузка в приложении чуть больше двух одновременно подключённых пользователей. А уж если у вас есть многопоточные мультисервисные решения, которые ещё и делят между собой одну базу данных, то вы точно знаете, о чём я. Для остальных счастливчиков дедлок упрощённо выглядит вот так:
Все равны, но никто не может проехать. В мире баз данных в таком случае появляется невидимый регулировщик: он выбирает жертвы и даёт проехать одной счастливой машинке. Ну или на уровне данных:
Дедлок — ситуация в многозадачной среде или СУБД, при которой несколько процессов находятся в состоянии бесконечного ожидания ресурсов, захваченных самими этими процессами.
Обычно в реляционных СУБД типа MS SQL, Postgres, MySQL одна транзакция выбирается жертвой и отменяется. Другая (случайный победитель) — завершается успешно. Самое простое решение — просто повторить попытку в случае ошибки.
Проблемы начинаются, когда число потоков увеличивается, или время исполнения запроса растёт, или обработка повторов начинает самоусиливаться.
Процессы-жертвы выпадают за допустимое время таймаутов, обслуживание ретраев занимает всё больше ресурсов, как, впрочем, и сама обработка дедлока. А ещё могут появляться некорректно обработанные данные и несоответствия в системе хранения. На графиках это выглядит как рост нагрузки, времени ответа и количества ошибок. А в жизни — как алерты и жалобы.
Коротко: почему дедлоки — это плохо
1) Исключения в приложении
• СУБД сама выбирает «жертву» и убивает её. В коде ошибка — deadlock victim.
• Все изменения внутри транзакции откатываются — нужно думать про retry.
2) Непредсказуемость
• Ошибки плавающие, часто единичные.
• При накоплении критической массы (примерно 1 % от общего числа операций над ресурсом) деградация системы может быть лавинообразной.
3) Потеря производительности
• Пока дедлок формируется, ресурсы уже заняты.
• После — откат, повтор, снова нагрузка.
• Чем больше число параллельных процессов, тем выше вероятность формирования лавины.
4) Пользователи страдают, скорость падает
• Пользователь получает ошибку или задержку.
• Время обработки с ретраями, конечно, выше.
5) Дедлоки — часто симптом ошибок архитектуры
• Разный порядок доступа к ресурсам.
• Слишком много данных внутри транзакции обновляется за один раз.
• Разные порядки сортировок при обновлениях.
• Лишние блокировки (например, из-за индексов/сканов).
• Слишком много потоков.
• Нет оркестрации.
Архитектурные антипаттерны, приводящие к дедлокам
1) Cross-entity transactions
• Одна транзакция трогает много таблиц/сущностей — чем проще и короче, тем лучше.
2) Случайный порядок операций
• Порядок зависит от кода, а не от контракта — нужна сортировка или оркестрация.
3) Транзакции, внутри которых множественные операции
• Транзакция принудительно открывается и долго читает, потом обновляет, снова читает, снова обновляет и всё это время держит конфликт — чем проще и короче, тем лучше.
4) Read-modify-write на горячих данных
• Counters, status updates — тут сложнее, к примеру, можно использовать insert-only-модель, или писать обновления одним потоком через общую шину, или использовать шардирование/партиционирование.
5) ORM скрывает порядок (или не задаёт его)
• Разработчик не видит реальный lock order — ORM для сложных и очень нагруженных частей системы подходит обычно плохо.
Дедлок — это не приговор. Но ретрай не лечение, а обезболивающее. «И так сойдёт» работает ровно до первой серьёзной лавины, когда время ответа улетае в потолок, а пользователи засыпают саппорт жалобами. Вместо бесконечных автоповторов гораздо полезнее один раз разобраться, почему транзакции встают в кольцо, кто захватывает блокировки не в том порядке и где архитектура провоцирует конфликты. Не нужно маскировать симптом, когда стоит искать причину.
