Как найти deadlocked
Подскажите куда по умолчанию пишется информация о дедлоках?
DBCC TRACEON (1204)
DBCC TRACEON (1222)
надо ли еще что-то делать чтобы в лог записался дедлок?
зачем?
чтобы сессии подольше висели во взаимоблокировке?
DBCC TRACEON (1204)
DBCC TRACEON (1222)
надо ли еще что-то делать чтобы в лог записался дедлок?
если потрудишься прочитать ссылку, которая приведена выше, то станет ясно, что включать надо так:
Глобальный дедлок и его обнаружение в локальной базе данных PostgreSQL
В настоящее время типовые базы данных продакшена состоят не только из одной базы. Как правило, это несколько баз, соединенных между собой потоковой, логической репликацией, BDR, FDW и другими специфическими для приложений способами репликации и распределения данных.
Сейчас большинство рабочих нагрузок приложений нацелены на одну базу данных одновременно, и поэтому, к счастью, поведение распределенных транзакций не является серьезной проблемой.
В будущем ожидается, что больше рабочих нагрузок будет направлено на несколько баз данных, и ядру базы, возможно, потребуется предоставить дополнительные возможности.
Глобальный дедлок - это явление, которое нынешние единичные базы данных не могут обнаружить. В этой новой серии блогов будет представлена информация о том, что такое дедлок, как он обрабатывается в PostgreSQL сейчас, и какие дополнительные возможности следует предусмотреть в такой распределенной среде.
Для начала в этом посте блога описывается, что такое глобальный дедлок и как он обрабатывается в существующем сейчас PostgreSQL. После этого будет рассмотрена обработка дедлока в распределенной среде.
Что такое дедлок?
Для защиты данных, с которыми работает транзакция (или процесс), можно использовать блокировку. Блокировка - это механизм, предотвращающий обновление или удаление целевых данных нежелательным образом. Если другие транзакции хотят воспользоваться блокировкой и при этом вступают в конфликт, транзакция ждет, пока блокировка не будет освобождена, как показано на рисунке 1.
Рис.1 Блокировка обеспечивает последовательную работу каждой транзакции с объектами базы данных
Обычно блокировка имеет более одного режима, и некоторые из них позволяют нескольким транзакциям использовать ее одновременно. Другие режимы позволяют получить блокировку только одной транзакции. В типовых базах данных, включая PostgreSQL, такие блокировки снимаются только тогда, когда удерживающая транзакция завершается фиксацией или прерыванием. Это называется двухфазной блокировкой (2PL, обратите внимание, что она отличается от двухфазной фиксации, 2PC) и необходима для обеспечения сериализации транзакций.
В некоторых ситуациях существует вероятность того, что две транзакции будут ждать друг друга.
Рис.2 Как может возникнуть дедлок
Как показано на рисунке 2, транзакция (T1) получает эксклюзивную блокировку на таблицу (TabA), а другая транзакция (T2) - на таблицу (TabB). Далее, T1 пытается получить эксклюзивную блокировку на TabB. Затем T1 начинает ждать завершения T2. После этого предположим, что T2 пытается получить эксклюзивную блокировку на TabA.
Рис.3 Дедлок-ситуация
В этой ситуации T1 ждет T2, а T2 ждет T1 (Рисунок 3). Нет никакого способа продолжить работу, пока вы не прервете T1 или T2 извне.
Такая ситуация называется дедлоком. На рисунке 3 представлен простейший сценарий дедлока, в реальности ситуация может быть гораздо сложнее. В ядре базы данных стратегия внутренних блокировок тщательно разработана для предотвращения подобных сценариев. Однако дедлок может возникнуть только при нагрузке на приложение, и предотвратить возникновение такого сценария в целом не представляется возможным.
Почему необходимо обнаружение ситуаций с дедлоками
Похоже, что тайм-аут транзакции может помочь разрешить дедлок, так что транзакции, выполняющиеся слишком долго, будут прерваны. Установить эффективный тайм-аут довольно сложно, особенно для большого пакета. Продолжительность транзакции зависит от других рабочих нагрузок, и это не очень удобная ситуация - пакетная транзакция может быть прервана после нескольких часов работы без каких-либо дедлоков или потенциальных проблем. Лучше всего своевременно обнаруживать дедлоки и поддерживать выполнение других работоспособных транзакций. Даже если дедлоки обнаружены, только одна транзакция будет уничтожена, а все остальные транзакции будут продолжать работу.
Шаги по обнаружению дедлока
Во многих учебниках по базам данных описывается, как обнаружить дедлок. Для его обнаружения мы используем подход "граф ожидания" (wait-for-graph). Это простой и проверенный способ.
Рис.4 Граф ожидания
Чтобы обнаружить дедлок, необходимо проанализировать, что ожидает та или иная транзакция. Рисунок 4 - это очень простой пример графа ожидания.
Рис.5 При наступлении дедлока граф ожидания содержит цикл
Также известно, что граф ожидания содержит "цикл", когда возникает дедлок в виде [1][2][3][4], как показано на рисунке 5. Вы можете использовать этот цикл в графе ожидания для обнаружения дедлока.
В PostgreSQL, когда транзакция не может получить запрошенную блокировку в течение определенного времени (задается параметром `deadlock_timeout`, значение по умолчанию 1 секунда), то запускается обнаружение дедлока.
Начиная с транзакции, которая не смогла получить блокировку, PG проверяет (более чем одну) другие транзакции, которые могут удерживать ожидаемую блокировку. Затем PG проверяет, ожидают ли транзакции другой блокировки. Повторяя это, можно построить граф ожидания. PG перебирает все возможные сегменты графа ожидания, пока он не завершится транзакцией, не ожидающей блокировки, или не найдет "цикл", когда граф достигает той же транзакции в самом начале.
Рис.6 Архитектура обнаружения дедлока в PostgreSQL
На рисунке 6 показано простое и схематичное описание этого.
Сценарий глобального дедлока
Подобные ситуации могут возникать в средах с несколькими базами данных (или распределенными базами данных), когда транзакция охватывает более одной базы. Это называется "глобальным дедлоком".
Рис.7 Сценарий дедлока с участием более чем одной базы данных
На рисунке 7 показан немного более сложный сценарий глобального дедлока. Обратите внимание, что сценарий глобального дедлока может включать три или более баз данных, и граф ожидания может уходить в другую базу данных, возвращаться обратно, а затем уходить дальше в следующую базу. Сценарий бывает очень сложным.
В подобной ситуации при использовании настоящей PG транзакция, ожидающая удаленные транзакции, не ждет никакой блокировки, и у вас нет средств для отслеживания такого внутрикластерного графа ожидания.
Также известно, что даже в такой распределенной транзакции вы можете использовать внутрикластерный граф ожидания для обнаружения глобального дедлока [2][4].
Что дальше?
В следующем посте этой серии статей я покажу, как можно использовать и расширить обнаружение дедлоков в текущем PostgreSQL для обнаружения таких глобальных дедлоков. Оставайтесь с нами!
Типичные взаимные блокировки в MS SQL и способы борьбы с ними
Чаще всего deadlock описывают примерно следующим образом:
Процесс 1 блокирует ресурс А.
Процесс 2 блокирует ресурс Б.
Процесс 1 пытается получить доступ к ресурсу Б.
Процесс 2 пытается получить доступ к ресурсу А.
В итоге один из процессов должен быть прерван, чтобы другой мог продолжить выполнение.
Но это простейший вариант взаимной блокировки, в реальности приходится сталкиваться с более сложными случаями. В этой статье мы расскажем с какими взаимными блокировками в MS SQL нам приходилось встречаться и как мы с ними боремся.
Немного теории
Выбор уровня изоляции транзакции
При использовании транзакций с уровнем изоляции serializable могут происходить любые взаимные блокировки. При использовании уровня изоляции repeatable read некоторые из описанных ниже взаимных блокировок не могут произойти. У транзакций с уровнем изоляции read committed могут возникнуть только простейшие взаимные блокировки. Транзакция с уровнем изоляции read uncommitted практически не влияет на скорость работы других транзакций и в ней не могут возникнуть взаимные блокировки из-за чтения, так как она не накладывает shared блокировки (правда могут быть взаимные блокировки с транзакциями изменяющими схему БД).
- Если транзакция изменяет данные в БД и при этом проверяет, чтобы эти данные не противоречили уже существующим записям в БД, то для нее скорее всего нужен уровень изоляции serializable. Но если вставка новых записей в параллельных транзакциях никак не может повлиять на результат текущей транзакции то можно использовать уровень изоляции repeatable read.
- Для чтения данных обычно достаточно использовать уровень изоляции по умолчанию (read committed) без какой либо транзакции. Однако при чтении агрегатов, части которых могут быть изменены во время чтения, может понадобится использовать транзакцию с уровнем изоляции repeatable read или даже serializable, иначе можно получить из базы агрегат в некорректном состоянии, в котором он может быть только в процессе выполнения транзакции изменения.
- Если необходимо отображать real time статистику по постоянно изменяющимся данным, то зачастую лучше использовать уровень изоляции read uncommitted. В этом случае в статистике будет некоторое количество грязных данных (хотя вряд ли это будет заметно), но зато построение отчетов практически не будет влиять на скорость работы системы.
Retry on deadlock
В достаточно сложной системе, насчитывающей десятки разнообразных типов бизнес транзакций, вряд ли получится спроектировать все транзакции таким образом, чтобы deadlock не мог возникнуть ни при каких условиях. Не стоит тратить время на предотвращение взаимных блокировок, вероятность возникновения которых крайне мала. Но, чтобы не портить user experience, в случае, когда операция прерывается из-за взаимной блокировки, ее нужно повторить. Для того, чтобы операцию можно было безопасно повторить, она не должна изменять входные данные и должна быть обернута в одну транзакцию (либо вместо всей операции, надо оборачивать в свой RetryOnDeadlock каждую SQL транзакцию в операции).
Важно понимать, что функция RetryOnDeadlock всего лишь улучшает user experience при изредка возникающих взаимных блокировках. Если они возникают очень часто, она лишь ухудшит ситуацию, в разы увеличив нагрузку на систему.
Борьба с простейшими взаимными блокировками
Если взаимная блокировка возникает из-за того, что два процесса обращаются к одним и тем же ресурсам но в разном порядке (как это описано в начале статьи), то достаточно поменять порядок блокировки ресурсов. В принципе, если в разных операциях блокируется определенный набор ресурсов, блокироваться первым всегда должен один и тот же ресурс, если это возможно. Этот совет применим не только к реляционным БД, но и вообще к любым системам, в которых возникают взаимные блокировки.
В применении к MS SQL этот совет, немного упрощая, можно выразить следующим образом: в разных транзакциях, изменяющих несколько таблиц, первой должна изменяться одна и та же таблица.
Shared->Exclusive lock escalation
- Транзакция 1 читает запись (накладывается S-блокировка).
- Транзакция 2 читает эту же запись (накладывается вторая S-блокировка).
- Транзакция 1 пытается изменить запись и ждет, когда транзакция 2 закончится и отпустит свою S-блокировку.
- Транзакция 2 пытается изменить эту же запись и ждет, когда транзакция 1 закончится и отпустит свою S-блокировку
Чтобы избежать такой взаимной блокировки, необходимо, чтобы из двух транзакций, собирающихся изменить запись, прочитать ее могла только одна. Специально для этого была введена update блокировка. Ее можно наложить следующим образом:
Если вы используете ORM и не можете управлять тем, как запрашивается сущность из БД, то вам придется выполнить отдельный запрос на чистом SQL для блокировки записи прежде чем запрашивать ее из БД. Важно, что накладывающий update блокировку запрос должен быть первым запросом, обращающимся к этой записи в данной транзакции, иначе будет возникать все та же взаимная блокировка, но при попытке наложить update блокировку, а не при изменении записи.
Накладывая update блокировку мы заставляем все транзакции, обращающиеся к одному ресурсу, выполняться по очереди, но обычно транзакции изменяющие один и тот же ресурс в принципе нельзя делать параллельно, так что это нормально.
Такая взаимная блокировка может возникнуть в любой транзакции, которая проверяет данные перед их изменением, но для редко изменяющихся сущностей, можно использовать RetryOnDeadlock. Подход с предварительной update блокировкой достаточно использовать только для сущностей, которые часто меняются разными процессами параллельно.
Пример
Пользователи заказывают призы за баллы. Количество призов каждого вида ограниченно. Система не должна позволить заказать больше призов, чем есть в наличии. Из-за особенностей промоакции периодически происходят набеги пользователей, желающих заказать один и тот же приз. Если использовать RetryOnDeadlock в данной ситуации, то во время набега пользователей заказ приза в большинстве случаев будет падать по web таймауту.
- Получаем запись о виде приза, накладывая update блокировку.
- Проверяем количество оставшихся призов. Если оно равно 0, завершаем транзакцию и возвращаем соответствующий ответ пользователю.
- Если призы еще есть, уменьшаем количество оставшихся призов на 1.
- Добавляем запись о заказанном призе.
Большинство взаимных блокировок, описанных далее, происходят похожим образом — мы пытаемся изменить данные после того как наложили на них Shared блокировку. Но в каждом из этих случаев есть свои нюансы.
Выборки по неиндексируемым полям
Если мы в serializable транзакции ищем запись по полю не входящему ни в один индекс, то shared блокировка будет наложена на всю таблицу. По другому нельзя убедиться, что ни одна другая транзакция не сможет вставить запись с таким же значением до завершения текущей транзакции. В итоге любая транзакция делающая выборку по этому полю, а потом изменяющая эту таблицу, будет взаимно блокироваться с любой подобной же транзакцией.
Если же добавить индекс по этому полю (или индекс по нескольким полям, первым из которых является поле, по которому мы ищем), то блокироваться будет ключ в этом индексе. Так что в serializable транзакциях еще более важно задумываться есть ли индекс по колонкам, по которым вы ищете записи.
Есть еще один нюанс, о котором важно помнить: если индекс уникален, то блокировка накладывается только на запрашиваемый ключ, а если неуникален, то также блокируются cледующее за этим ключом значение. Две транзакции, запрашивающие разные записи по неуникальному индексу, а потом изменяющие их, могут взаимно блокироваться, если запрашиваются соседние значения ключей. Обычно это редкая ситуация и достаточно использовать RetryOnDeadlock, чтобы избежать проблем, но в некоторых случаях может потребоваться накладывать update блокировку при вытаскивании записей по неуникальному ключу.
Проверка на наличие перед вставкой
Пример
Нам необходимо проверить, есть ли в БД пользователь с таким Id в Facebook, перед тем как его добавлять. Так как мы работаем с одной строчкой в БД, создается ощущение, что будет блокироваться только она и вероятность взаимной блокировки невелика. Однако если в транзакции с уровнем изоляции Serializable попытаться выбрать несуществующее значение (и эта колонка входит в индекс), то будет наложена shared блокировка на все ключи между двумя ближайшими значениям, которые есть в таблице. Например, если в базе есть Id 15 и Id 1025, и нет ни одного значения между ними, то при выполнении SELECT * FROM Users WHERE FacebookId = 500 будет наложена Shared блокировка на ключи с 15 до 1025. Если до вставки другая транзакция проверит есть ли пользователь с FacebookId = 600 и попытается его вставить, то произойдёт взаимная блокировка. Если в БД уже много потребителей, у которых заполнен FacebookId, то вероятность взаимной блокировки будет невелика и нам достаточно использовать RetryOnDeadlock. Но если выполнять множество таких транзакций на почти пустой базе, то взаимные блокировки будут возникать достаточно часто, чтобы это сильно сказалось на производительности.
У нас эта проблема возникла при параллельном импорте потребителей от новых клиентов (для каждого клиента мы создаем новую пустую БД). Так как нас на данный момент устраивает скорость однопоточного импорта, мы просто отключили параллелизм. Но в принципе проблема решается также как и в выше описанном примере, надо использовать update блокировку:
В этом случае при многопоточном импорте в пустую базу по началу потоки будут простаивать, ожидая пока освободится блокировка, но по мере заполнения базы степень параллелизма будет возрастать. Хотя если импортируемые данные упорядочены по FacebookId, то параллельно импортировать их не получится. При импорте в пустую базу такого упорядочивания стоит избегать (либо не проверять наличие пользователей в БД по FacebookId при первом импорте).
Взаимные блокировки на сложных агрегатах
Если у вас в системе есть сложный агрегат, данные которого хранятся в нескольких таблицах, и есть множество транзакций изменяющих разные части этого агрегата параллельно, то необходимо выстроить все эти транзакции таким образом, чтобы в них не возникали взаимные блокировки.
Пример
В БД хранится персональные данные потребителя, его идентификаторы в соц сетях, заказы в интернет магазине, записи об отправленных ему письмах.
Транзакции, добавляющие идентификатор в соц сети, отправляющие письма и регистриующие покупки, также могут изменять технические поля в основной записи о потребителе. В любой из этих транзакций присутствует Id потребителя.
Для избежания взаимных блокировок нужно начинать любую транзакцию со следующего запроса:
В этом случае в один момент только одна транзакция сможет изменять данные, относящиеся к конкретному потребителю и взаимные блокировки не будут возникать в независимости от того насколько сложен агрегат потребителя.
Можно попробовать изменить схему хранения данных так, чтобы транзакции, отправляющие письма и регистрирующие покупки не меняли технические пометки в потребителе. Тогда информацию о заказах и отправленных письмах можно будет изменять параллельно с изменением потребителя. В этом случае мы фактически выносим эти данные за рамки агрегата «потребитель».
- надо стремится к тому, чтобы любая транзакция в системе не изменяла более одного агрегата
- в любой транзакции, изменяющей агрегат, первый запрос, обращающийся к данным агрегата, должен накладывать exclusive или update блокировку на корень агрегата
- для увеличения степени параллелизма системы надо делать агрегаты настолько маленькими насколько это возможно
Взаимные блокировки на последовательно идущих записях
Подобные взаимные блокировки возникают при очень специфичных условиях, но пару раз мы с ними все-таки сталкивались, так что о них тоже стоит рассказать.
Пример
Как найти deadlocked
Если два пользователя выполняют 2 операции в таблицах A и B
1 изменяет данные в таблице A и начинает изменять в B
2 изменяет данные в таблице B и начинает изменять в A
Ну скажем с некоторой вероятностью создается взаимная блокировка (deadlock)
IMHO Oracle может и не порулить такую ситуацию, ну или если ситуация будет более сложная.
Если два пользователя выполняют 2 операции в таблицах A и B
1 изменяет данные в таблице A и начинает изменять в B
2 изменяет данные в таблице B и начинает изменять в A
Ну скажем с некоторой вероятностью создается взаимная блокировка (deadlock)
Читайте также: