ескалація блокувань

трохи теорії

Як правило, в СУБД організація даних являє собою деяку ієрархію. Наприклад, «файл-> база даних-> табліца-> сторінка даних-> запісь-> поле в записі». Деякі рівні цієї ієрархії можуть бути переставлені місцями або ж чисто віртуальними, а то й не бути зовсім. Все це залежить від особливостей конкретної реалізації і навіть використовуваної термінології, і в даному випадку має мало значення, важливо лише, що в тому чи іншому вигляді подібна ієрархія присутній практично завжди. В СУБД реалізують механізм ієрархічних блокувань (multigranularity locking) для забезпечення паралельної обробки транзакцій, накласти блокування можна на об'єкт, що знаходиться на будь-якому рівні цієї ієрархії. При цьому автоматично блокуються відповідні об'єкти на нижчих рівнях. Наприклад, монопольна блокування на сторінку даних, блокує всі записи входять в цю сторінку. Але при цьому, природно, записи на сусідніх сторінках жодним чином не блокуються. Зазвичай, для позначення обсягу заблокованих даних застосовують термін «гранулярность» (granularity). Більша гранулярность означає блокування об'єкта на більш високому рівні ієрархії і, відповідно, всіх назв об'єктів володіють меншою гранулярністю.

Ось тепер ми власне і підібралися до предмету обговорення. Ескалація блокувань - це процес, при якому безліч блокувань з маленькою гранулярністю, конвертуються в одну блокування на більш високому рівні ієрархії з більшою гранулярністю.

Навіщо це треба

Дійсно, на перший погляд не треба. Інтуїтивно хід думок приблизно такий: Блокування на більш високому рівні ієрархії викликає блокування об'єктів на більш низькому рівні, в тому числі і тих, яких, в общем-то, блокувати не треба. А це веде до зменшення ступеня паралелізму транзакцій і в кінцевому підсумку до падіння продуктивності. Але насправді ефект зворотний. Якщо копнути глибше, то з'ясовується, що в деяких випадках вигідніше блокувати об'єкти, з більшою гранулярністю.

Традиційно виділяють три ефекту від накладення блокувань, що призводять до того, що в деяких випадках вигідніше блокувати на більш високому рівні згаданої вище ієрархії.

Locking overhead. Як би швидко не накладалися і знімалися, і скільки б мало місця не займали самі блокування, все одно може наступити момент, коли вигідніше накласти одну блокування з більшою гранулярністю, ніж кілька (або кілька тисяч) більш дрібних. Незважаючи на витрати пов'язані з блокуванням зайвих об'єктів.

Data contention. Накладення однієї блокування - дія атомарному, однак, накладення декількох блокувань вже таким не є. Між накладенням двох блокувань в одній транзакції сервер, керуючись міркуваннями найбільшої ефективності, може виконати одну, або навіть кілька, інших транзакцій цілком. Але якщо перша транзакція виконується відносно довго і накладає багато дрібних блокувань, то можливе виникнення неприємних побічних ефектів. Припустимо, є одна довга транзакція T1, яка блокує безліч записів. У якийсь момент вона стартувала і почала накладати свої блокування. На той час, коли T1 встигла захопити кілька об'єктів, починають виконуватися транзакції Tx, які, в свою чергу блокують записи, які T1 скоро знадобляться, але дістатися до них T1 ще не встигла. В результаті T1 змушена чекати, поки транзакції Tx відпрацюють, при цьому, поки вона чекає одну з Tx, цих транзакцій (захоплюючих записи до яких T1 не встигла дістатися) може стати ще більше. Таким чином, час, що витрачається на виконання T1, значно зростає. Становище ускладнюється тим, що після початку виконання T1 можуть з'явитися транзакції Ty, яким потрібні ті об'єкти, які T1 вже встигла захопити, а оскільки час виконання T1 зростає, то зростає і час очікування транзакцій Ty на об'єктах заблокованих T1.

Resource contention. Блокування, крім того, що просто займають місце, потребують так само і в деякому обслуговуванні, яке витрачає системні ресурси. Наприклад, регулярна перевірка графа очікування транзакцій на наявність взаімоблокіровок (deadlock), заняття досить-таки накладне. І чим більше блокувань, тим більше ресурсів витрачається на подібні перевірки. До того ж, просто існує межа кількості транзакцій, які можуть виконуватися в системі одночасно без шкоди для продуктивності. У певний момент безліч одночасних транзакцій сходяться в боротьбі за процесорний час і інші ресурси системи. Все це знову-таки призводить до збільшення часу відгуку і загального падіння продуктивності.

Таким чином, вплив цих трьох чинників призводить до того, що в деяких випадках, коли число блокувань велике і в системі виконуються досить довгі транзакції, з точки зору продуктивності, стає вигідніше блокувати об'єкти з більшою гранулярністю. Зменшується загальна кількість блокувань, знижується час виконання довгих транзакцій і, як наслідок, коротких, які змушені чекати довгі, витрачається менше системних ресурсів. На малюнку 1 показані якісні графіки залежності продуктивності від гранулярності блокувань для коротких і довгих транзакцій.


Малюнок 1. Графіки залежності продуктивності від гранулярності блокувань для коротких і довгих транзакцій.

Основна проблема в тому, щоб визначити, з якого моменту вважати транзакцію досить «довгою». На жаль, не завжди можна визначити стратегію накладення блокувань на етапі компіляції, тому іноді доводиться виконувати збільшення гранулярності динамічно, прямо в ході виконання запиту.

практична реалізація

В якості практичної реалізації механізму ескалації блокувань можна привести Microsoft SQL Server. В даному випадку цікавить нас ієрархія об'єктів така: «табліца-> сторінка даних-> запис».


Малюнок 2. Ієрархія об'єктів в MSSQL

За замовчуванням, якщо не втручатися в логіку роботи сервера, він намагається накласти блокування на запис, тобто блокування, що володіє найменшою гранулярністю. Але якщо сервер вважатиме, що блокувати на рівні окремого запису не найоптимальніше рішення, то він заблокує більший обсяг. Однак можна вказати явно, з якою гранулярністю блокувати, за допомогою спеціальних підказок оптимізатора (hints).

  • ROWLOCK - блокування на рівні запису.
  • PGLOCK - блокування на рівні сторінки даних.
  • TABLOCK - блокування на рівні таблиці.

Якщо ж в таблиці побудовані індекси, то можна обійтися і без явного вказівки гранулярності в запиті. За допомогою спеціальної системної збереженої процедури, sp_indexoption, можна задати гранулярность блокування індексу. Таким чином, все вибірки з таблиці за цим індексом йтимуть на заданому рівні ієрархії без явних вказівок.

На запити, які не використовують індекси, настройки sp_indxoption не діють. Але якщо в таблиці є кластерний індекс, а в цьому випадку сторінки даних є частиною індексу, то гранулярность, зазначена для кластерного індексу, буде використовуватися і для запитів, що виконуються без участі індексів.

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

Як вже говорилося, сервер може підвищити гранулярность блокування прямо в ході виконання запиту, при цьому ескалація відбувається завжди до рівня таблиці, навіть якщо перед цим блокувалися окремі записи. SQL Server визначає необхідність ескалації виходячи з таких міркувань: Якщо число блокувань накладених однієї транзакцією перевищує 1250, або число блокувань на один індекс або таблицю більше 765, і якщо при цьому більше сорока відсотків пам'яті доступної сервера використовується під блокування, тоді сервер вибирає найбільш підходящу таблицю і намагається ескаліровать блокування до табличного рівня. У разі невдачі, якщо на деякі записи таблиці накладені несумісні блокування інших транзакцій, сервер не чекає, а продовжує працювати на колишньому рівні гранулярності до наступної спроби. Таким чином, виключається можливість взаимоблокировки безпосередньо з вини ескалації.

Чи не складно помітити, що внаслідок ескалації блокувань, ніколи не можна бути впевненим з якою гранулярністю сервер буде блокувати об'єкти. Тому ніколи не слід на це покладатися.

Хоча й існують два способи заборонити ескалацію:

Спосіб перший, «майже хакерський». Якщо ми хочемо, щоб на певній таблиці ніколи не відбувалося ескалації блокувань, то необхідно виконати нижченаведений запит в окремій транзакції, але завершити цю транзакцію треба разом із закриттям програми.

Такий, на перший погляд безглуздий запит, викличе накладення IX блокування на таблицю. Це блокування не сумісна ні з S, ні з U, ні, тим більше, з X блокуваннями. Таким чином, через неможливість накласти блокування на таблицю ескалація на неї не пройде. Однак інші запити, з блокуваннями меншою гранулярності, пройдуть, в общем-то, без проблем. Єдиний побічний ефект полягає в тому, що буде неможливо навіть явно накласти блокування на цю таблицю цілком.

Спосіб другий, «офіційний». Можна також, не використовуючи хитрих обхідних прийомів, заборонити ескалацію взагалі, навіть не в окремій базі, а цілком в окремо взятому екземплярі (instance) SQL Server'а. Для цього існує спеціальний прапор трасування - 1211. Виконання команди DBCC TRACEON (1211), заборонить ескалацію в окремому примірнику всерйоз і надовго, до наступної команди DBCC TRACEOFF (1211).

Але, незважаючи на можливість, користуючись напівлегальними і цілком документально оформлених способами, заборонити ескалацію блокувань, я б дуже не рекомендував діяти таким чином. Ескалація - це добре налагоджений внутрішній механізм сервера, що допомагає домагатися максимальної продуктивності навіть при важкому навантаженні. І лізти в цей механізм руками без крайньої на те необхідності не рекомендується.

Можна так само спробувати розбити довгу транзакцію на безліч дрібних, що, в общем-то, завжди корисно, навіть якщо не брати до уваги ескалацію, так як в більшості випадків це тільки на користь сервера. Та й в цілому, ніж оптимальніше написані транзакції і запити, тим менше ризик ескалації блокувань. Але, я знову повторюся, ніколи не можна бути впевненим, що ескалація не відбудеться. З іншого боку, як правило, ескалація блокування це дзвіночок про те, що в додатку є вузьке місце і його непогано б переписати оптимальніше.

Якщо ж для будь-яких випадків необхідно бути впевненим, що буде заблокований тільки один об'єкт або треба блокувати абстрактні об'єкти типу «документ», то для цього існують призначені для користувача блокування, працювати з якими можна через пару системних збережених процедур, sp_getapplock і sp_releaseapplock. За допомогою цих процедур сервер надає можливість стороннім додаткам користуватися менеджером блокувань сервера в своїх потребах. Реалізовувати ж власний механізм блокувань - заняття клопітка і, внаслідок ескалації, малоперспективне.

Корисні посилання

Використовувана література

[1] Concurrency control and recovery in database systems. Philip A. Bernstein, Vassos Hadzilacos, Nathan Goodman.

Схожі статті