Диагностика блокировок в SQL Server 2005

Мы наблюдаем некоторые вредные, но редкие, тупиковые условия в базе данных Qaru SQL Server 2005.

Я подключил профилировщик, настроил профиль трассировки, используя эту отличную статью об устранении неполадок тупиков и захватил кучу примеров. Странно, что запирающая запись всегда одна и та же:

UPDATE [dbo].[Posts]
SET [AnswerCount] = @p1, [LastActivityDate] = @p2, [LastActivityUserId] = @p3
WHERE [Id] = @p0

Другой оператор взаимоблокировки меняется, но обычно это своего рода тривиальная, простая прочитанная таблица сообщений. Этот человек всегда убивается в тупике. Вот пример

SELECT
[t0].[Id], [t0].[PostTypeId], [t0].[Score], [t0].[Views], [t0].[AnswerCount], 
[t0].[AcceptedAnswerId], [t0].[IsLocked], [t0].[IsLockedEdit], [t0].[ParentId], 
[t0].[CurrentRevisionId], [t0].[FirstRevisionId], [t0].[LockedReason],
[t0].[LastActivityDate], [t0].[LastActivityUserId]
FROM [dbo].[Posts] AS [t0]
WHERE [t0].[ParentId] = @p0

Чтобы быть совершенно ясным, мы не видим взаимоблокировки записи/записи, но читаем/пишем.

На данный момент мы имеем смесь LINQ и параметризованных SQL-запросов. Мы добавили with (nolock) ко всем SQL-запросам. Возможно, это помогло. У нас также был один (очень) плохо написанный запрос к значкам, который я исправил вчера, который занимал более 20 секунд, чтобы запускать каждый раз, и выполнял каждую минуту поверх этого. Я надеялся, что это послужило источником некоторых проблем с блокировкой!

К сожалению, я получил еще одну тупиковую ошибку около 2 часов назад. Одинаковые точные симптомы, один и тот же самый критик пишут.

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

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

Использование NOLOCK с Linq немного сложнее, чем Скотт Гензельман обсуждает здесь.

Мы заигрываем с идеей использования

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

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

Думаю, я немного расстроен тем, что тривиальные чтения в SQL 2005 могут затормозить при записи. Я видел, что блокировки записи/записи являются огромной проблемой, но читает? У нас нет банковского сайта здесь, мы не нуждаемся в полной точности каждый раз.

Идеи? Мысли?


Создается ли экземпляр нового объекта LINQ to SQL DataContext для каждой операции или, возможно, вы используете один и тот же статический контекст для всех ваших вызовов?

Джереми, мы разделяем один статический datacontext в базовом контроллере по большей части:

private DBContext _db;
/// <summary>
/// Gets the DataContext to be used by a Request controllers.
/// </summary>
public DBContext DB
{
    get
    {
        if (_db == null)
        {
            _db = new DBContext() { SessionName = GetType().Name };
            //_db.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
        }
        return _db;
    }
}

Рекомендуете ли вы создавать новый контекст для каждого контроллера или на странице или чаще?

Ответ 1

В соответствии с MSDN:

http://msdn.microsoft.com/en-us/library/ms191242.aspx

Когда либо ПРОЧИТАЙТЕ СООТВЕТСТВИЕ База данных ALLOW SNAPSHOT ISOLATION опции включены, логические копии (версии) поддерживаются для всех данных изменения, выполненные в база данных. Каждый раз, когда строка изменяется по конкретной сделке экземпляр хранилищ Database Engine версия ранее совершенной образ строки в tempdb. каждый версия отмечена транзакцией порядковый номер транзакции которые внесли изменения. Варианты измененные строки привязаны по ссылке список. Новейшее значение строки всегда хранятся в текущей базе данных и привязан к сохраненным версиям строк в tempdb.

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

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

Попробуйте установить этот параметр и УДАЛИТЕ все NOLOCK из кодовых запросов, если это действительно необходимо. NOLOCK или использование глобальных методов в обработчике контекста базы данных для борьбы с уровнями изоляции транзакций базы данных - это проблемы с Band-Aids. NOLOCKS будет маскировать фундаментальные проблемы с нашим слоем данных и, возможно, приведет к выбору недостоверных данных, в которых решение для автоматического выбора/обновления строк является решением.

ALTER Database [StackOverflow.Beta] SET READ_COMMITTED_SNAPSHOT ON

Ответ 2

NOLOCK и READ UNCOMMITTED - это скользкий наклон. Вы никогда не должны использовать их, если не понимаете, почему происходит тупик. Меня беспокоит, что вы говорите: "Мы добавили (nolock) ко всем SQL-запросам". Необходимость добавления WITH NOLOCK во всем мире - верный признак того, что у вас проблемы с вашим уровнем данных.

Сам процесс обновления выглядит немного проблематичным. Вы определяете счет раньше в транзакции или просто извлекаете его из объекта? AnswerCount = AnswerCount+1, когда вопрос добавлен, вероятно, лучший способ справиться с этим. Тогда вам не нужна транзакция, чтобы получить правильный счет, и вам не нужно беспокоиться о проблеме concurrency, которую вы потенциально подвергли себя.

Один простой способ обойти эту проблему с тупиковой ситуацией без большой работы и без включения грязных чтений - использовать "Snapshot Isolation Mode" (новый в SQL 2005), который всегда даст вам чистую информацию о последних немодифицированных данных. Вы также можете легко поймать и затормозить запоздалые заявления, если хотите обработать их изящно.

Ответ 3

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

Это, вероятно, проблема, связанная с индексом. Например, допустим, что в таблице Posts имеется некластеризованный индекс X, который содержит ParentID и один (или несколько) обновляемых полей (AnswerCount, LastActivityDate, LastActivityUserId).

Заключительный тупик возникнет, если SELECT cmd сделает блокировку с разделяемым чтением в индексе X для поиска в ParentId и затем должен выполнить блокировку с разделяемым чтением в кластерном индексе, чтобы получить оставшиеся столбцы, в то время как CMD UPDATE выполняет write-exclusive lock для кластерного индекса и для его исправления необходимо создать блокировку с записью по индексу X.

Теперь у вас есть ситуация, когда A заблокирован X и пытается получить Y, тогда как B заблокирован Y и пытается получить X.

Конечно, нам понадобится OP для обновления вашего сообщения с дополнительной информацией о том, какие индексы играют в игру, чтобы подтвердить, действительно ли это является причиной.

Ответ 4

Мне очень неудобно об этом вопросе и сопутствующих ответах. Там много "попробуй эту волшебную пыль! Нет этой волшебной пыли!"

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

Все, что вы указали, это то, что происходят некоторые блокировки, а не тупиковая ситуация.

В SQL 2005 вы можете получить больше информации о том, какие блокировки извлекаются с помощью:

DBCC TRACEON (1222, -1)

так что, когда произойдет тупик, у вас будет лучшая диагностика.

Ответ 5

Вы создаете экземпляр нового объекта LINQ to SQL DataContext для каждой операции или, возможно, используете один и тот же статический контекст для всех ваших вызовов? Первоначально я испробовал последний подход, и, насколько я помню, это вызвало нежелательную блокировку в БД. Теперь я создаю новый контекст для каждой атомной операции.

Ответ 6

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

Помните, что в тупике требуется (по крайней мере) 2 замка. Соединение 1 имеет Lock A, хочет Lock B - и наоборот для соединения 2. Это неразрешимая ситуация, и кто-то должен дать.

То, что вы показали до сих пор, решается простой блокировкой, которую Sql Server рад сделать в течение всего дня.

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

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

Ответ 7

Вам будет интересно, если ваш профиль пользователя за несколько секунд устарел?

Нет - это совершенно приемлемо. Установка уровня изоляции базовой транзакции, вероятно, является лучшим/самым чистым способом.

Ответ 8

Типичный тупик чтения/записи происходит из доступа к порядку индекса. Read (T1) находит строку в индексе A, а затем просматривает проецируемый столбец на индекс B (обычно кластеризованный). Запись (T2) изменяет индекс B (кластер), затем необходимо обновить индекс A. T1 имеет S-Lck на A, хочет S-Lck на B, T2 имеет X-Lck на B, хочет U-Lck на A. Тупик, слойка. T1 убит. Это распространено в средах с тяжелым OLTP-трафиком и чуть-чуть слишком много индексов:). Решение состоит в том, чтобы заставить чтение не переходить с A на B (то есть включенный столбец в или удалять столбцы из проецируемого списка), или T2 не нужно переходить с B на A (не обновлять индексированный столбец). К сожалению, linq здесь не ваш друг...

Ответ 9

@Jeff - я определенно не эксперт в этом, но у меня были хорошие результаты с созданием нового контекста почти на каждом звонке. Я думаю, что это похоже на создание нового объекта Connection при каждом вызове с ADO. Накладные расходы не так плохи, как вы думаете, поскольку пул соединений все равно будет использоваться.

Я просто использую глобальный статический помощник:

public static class AppData
{
    /// <summary>
    /// Gets a new database context
    /// </summary>
    public static CoreDataContext DB
    {
        get
        {
            var dataContext = new CoreDataContext
            {
                DeferredLoadingEnabled = true
            };
            return dataContext;
        }
    }
}

а затем я делаю что-то вроде этого:

var db = AppData.DB;

var results = from p in db.Posts where p.ID = id select p;

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

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

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

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

Ответ 10

Настройка вашего значения по умолчанию для чтения uncommitted не является хорошей идеей. Ваша воля, несомненно, приведет к несогласованности и в конечном итоге приведет к проблеме, которая хуже, чем у вас сейчас. Изоляция снимков может работать хорошо, но это радикальное изменение в том, как работает Sql Server и создает огромную нагрузку на tempdb.

Вот что вы должны сделать: используйте try-catch (в T-SQL), чтобы обнаружить условие взаимоблокировки. Когда это произойдет, просто запустите запрос. Это стандартная практика программирования баз данных.

Есть хорошие примеры этой техники в Paul Nielson Sql Server 2005 Bible.

Вот быстрый шаблон, который я использую:

-- Deadlock retry template

declare @lastError int;
declare @numErrors int;

set @numErrors = 0;

LockTimeoutRetry:

begin try;

-- The query goes here

return; -- this is the normal end of the procedure

end try begin catch
    set @[email protected]@error
    if @lastError = 1222 or @lastError = 1205 -- Lock timeout or deadlock
    begin;
        if @numErrors >= 3 -- We hit the retry limit
        begin;
            raiserror('Could not get a lock after 3 attempts', 16, 1);
            return -100;
        end;

        -- Wait and then try the transaction again
        waitfor delay '00:00:00.25';
        set @numErrors = @numErrors + 1;
        goto LockTimeoutRetry;

    end;

    -- Some other error occurred
    declare @errorMessage nvarchar(4000), @errorSeverity int
    select    @errorMessage = error_message(),
            @errorSeverity = error_severity()

    raiserror(@errorMessage, @errorSeverity, 1)

    return -100
end catch;    

Ответ 11

Q. Почему вы в первую очередь сохраняете таблицу AnswerCount в таблице Posts?

Альтернативный подход заключается в том, чтобы исключить "запись назад" в таблицу Posts, не сохраняя AnswerCount в таблице, а динамически вычислять количество ответов на сообщение по мере необходимости.

Да, это будет означать, что вы выполняете дополнительный запрос:

SELECT COUNT(*) FROM Answers WHERE post_id = @id

или более типично (если вы показываете это для домашней страницы):

SELECT p.post_id, 
     p.<additional post fields>,
     a.AnswerCount
FROM Posts p
    INNER JOIN AnswersCount_view a
    ON <join criteria>
WHERE <home page criteria>

но это обычно приводит к INDEX SCAN и может быть более эффективным при использовании ресурсов, чем при использовании READ ISOLATION.

Там более одного способа скинуть кошку. Преждевременная де-нормализация схемы базы данных может привести к проблемам с масштабируемостью.

Ответ 12

Вы обязательно хотите, чтобы READ_COMMITTED_SNAPSHOT был включен, что не по умолчанию. Это дает вам семантику MVCC. Это то же самое, что Oracle использует по умолчанию. Наличие базы данных MVCC настолько невероятно полезно, что НЕ использовать ее безумно. Это позволяет вам выполнить следующее внутри транзакции:

Обновить ПОЛЬЗОВАТЕЛИ Установите FirstName = 'foobar'; // решаем спать в течение года.

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

Ответ 13

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

То есть, если один запрос обновляется в порядке, указанном в Таблице 1, Таблице 2, а другой запрос обновляет его в соответствии с Таблицей 2, Таблица1, тогда вы можете видеть взаимоблокировки.

Не уверен, что вы можете изменить порядок обновлений, так как используете LINQ. Но это на что посмотреть.

Ответ 14

Вам будет интересно, если ваш профиль пользователя за несколько секунд устарел?

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

Ответ 15

Я согласен с Джереми в этом вопросе. Вы спрашиваете, следует ли создавать новый контекст данных для каждого контроллера или для каждой страницы. Я стараюсь создать новый для каждого независимого запроса.

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

Как только я изменил свою стратегию использования другого контекста данных на уровне LINQ для каждого запроса и доверял тому, что SQL-сервер мог работать с магией пула соединений, блокировки, казалось, исчезли.

Конечно, я был под некоторым давлением времени, поэтому, пробуя несколько вещей примерно в одно и то же время, я не могу быть на 100% уверенным, что это исправлено, но у меня высокий уровень уверенности - пусть это так.

Ответ 16

Теперь, когда я вижу ответ Джереми, я думаю, что я помню, что лучше всего использовать новый DataContext для каждой операции с данными. Роб Конейри написал несколько сообщений о DataContext, и он всегда публикует новости, а не использует синглтон.

Вот шаблон, который мы использовали для ссылки Video.Show(в исходное представление в CodePlex):

using System.Configuration;
namespace VideoShow.Data
{
  public class DataContextFactory
  {
    public static VideoShowDataContext DataContext()
    {
        return new VideoShowDataContext(ConfigurationManager.ConnectionStrings["VideoShowConnectionString"].ConnectionString);
    }
    public static VideoShowDataContext DataContext(string connectionString)
    {
        return new VideoShowDataContext(connectionString);
    }
  }
}

Затем на уровне обслуживания (или даже более подробно, для обновлений):

private VideoShowDataContext dataContext = DataContextFactory.DataContext();

public VideoSearchResult GetVideos(int pageSize, int pageNumber, string sortType)
{
  var videos =
  from video in DataContext.Videos
  where video.StatusId == (int)VideoServices.VideoStatus.Complete
  orderby video.DatePublished descending
  select video;
  return GetSearchResult(videos, pageSize, pageNumber);
}

Ответ 17

Вы должны выполнять грязные чтения.

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

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

Это может дать вам так называемый "phantom reads", который возникает, когда ваш запрос воздействует на данные из транзакции, которая не была выполнена.

У нас нет банковского сайта здесь, мы не нуждаемся в совершенной точности каждый раз

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

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

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

Ответ 18

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

Не будут ли, по крайней мере, некоторые из других вариантов вводить штрафы за производительность, которые принимаются все время, когда система повторных попыток будет пинать редко?

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

Ответ 19

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

Мне было бы интересно узнать, Джефф, как настроить его на уровне базы данных повлияет на такой запрос, как:

Begin Tran
Insert into Table (Columns) Values (Values)
Select Max(ID) From Table
Commit Tran

Ответ 20

Это хорошо, если мой профиль истекает даже на несколько минут.

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

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

Ответ 21

Я буду продолжать настраивать все; как работает дисковая подсистема? Какова средняя длина очереди на диск? Если резервное копирование ввода-вывода, реальной проблемой могут быть не эти два запроса, которые являются взаимоблокировками, это может быть другой запрос, который является узким местом в системе; вы упомянули запрос, набравший 20 секунд, которые были настроены, есть ли другие?

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

Ответ 22

Имела ту же проблему и не может использовать "IsolationLevel = IsolationLevel.ReadUncommitted" на TransactionScope, потому что сервер не поддерживает DTS (!).

Вот что я сделал с помощью метода расширения:

public static void SetNoLock(this MyDataContext myDS)
{
    myDS.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
}

Итак, для тех, кто использует критические таблицы concurrency, мы включаем "nolock" следующим образом:

using (MyDataContext myDS = new MyDataContext())
{
   myDS.SetNoLock();

   //  var query = from ...my dirty querys here...
}

Приглашаем к участию!