Розшифровка доповіді 2020 Брюса Момжіана "Unlocking the Postgres Lock Manager".
(Примітка: Всі SQL запити зі слайдів ви можете отримати за цим посиланням:
Вітання! Чудово знову бути тут у Росії. Я перепрошую, що я не зміг приїхати торік, але цього року в Івана і в мене великі плани. Я сподіваюся, що буду тут набагато частіше. Я обожнюю приїжджати до Росії. Я відвідуватиму Тюмень, Твер. Я дуже радий, що мені пощастить побувати в цих містах.
Мене звуть Брюс Момжіан. Я працюю в EnterpriseDB і працюю з Postgres понад 23 роки. Я живу у Філадельфії, США. Подорожую приблизно 90 днів на рік. І відвідую близько 40 конференцій. Мій
Я раніше був викладачем, професором, перш ніж почав працювати з Postgres. І я дуже радий, що мені вдасться розповісти вам те, що я збираюся вам розповісти. Це одна із найцікавіших моїх презентацій. І ця презентація містить 110 слайдів. Говорити ми почнемо з простих речей, а до кінця доповідь стане все складнішою і складнішою, і стане досить складною.
Це досить неприємна розмова. Блокування – це найпопулярніший предмет. Ми хочемо, щоб це кудись зникло. Це як ходити до стоматолога.
- Блокування є проблемою для великої кількості людей, які працюють у базах даних та у яких працюють одночасно кілька процесів. Їм необхідне блокування. Т. е. сьогодні я вам дам базові знання з блокування.
- Ідентифікатори транзакцій. Це досить нудна частина презентації, але їх потрібно зрозуміти.
- Далі ми поговоримо про типи блокування. Це досить механічна частина.
- І далі ми наведемо деякі приклади блокувань. І це буде досить складно для сприйняття.
Давайте поговоримо про блокування.
Термінологія у нас досить складна. Скільки знають, звідки цей уривок? Дві людини. Це з гри, яка називається «Колосальна пригода в печері». Це була текстова комп'ютерна гра у 80-х роках, мені здається. Там треба було зайти в печеру, в лабіринт і текст змінювався, але при цьому зміст був приблизно однаковий щоразу. Отак я пам'ятаю цю гру.
І тут ми бачимо найменування блокувань, які прийшли до нас із Oracle. Ми використовуємо їх.
Тут ми бачимо терміни, які мене бентежать. Наприклад, SHARE UPDATE ECXLUSIVE. Далі SHARE RAW ECXLUSIVE. Щиро кажучи, ці назви не дуже зрозумілі. Ми намагатимемося їх детальніше розглянути. Деякі містять слово «share», яке означає – відокремитися. Деякі містять слово "exclusive" - ексклюзивний. Деякі містять обидва ці слова. Я хотів би почати з того, як ці блокування працюють.
І також дуже важливим є слово «доступ» — access. І слова "row" - рядок. Т. е. розподіл доступу, розподіл рядків.
Ще одна проблема, яку необхідно зрозуміти у Postgres, я, на жаль, не зможу розповісти про неї у своєму виступі, це MVCC. У мене є окрема презентація на цю тему на моєму веб-сайті. І якщо ви думаєте, що ця презентація складна, то MVCC – це, мабуть, найскладніша. І якщо вам цікаво, можете подивитися її на сайті. Подивитися можете відео.
Ще один момент, який нам необхідно зрозуміти, – це ідентифікатори транзакції. Багато транзакцій не можуть працювати без унікальних ідентифікаторів. І тут ми пояснюємо те, що таке транзакція. У Postgres є дві системи нумерації транзакцій. Я знаю, це не дуже гарне рішення.
Також майте на увазі, що слайди будуть досить складними для сприйняття, тому на те, що виділено червоним кольором, саме на це треба звернути увагу.
Дивимося. Червоним кольором виділено номер транзакції. Тут показано функцію SELECT pg_back. Вона повертає мою транзакцію та ID цієї транзакції.
Ще один момент, якщо вам подобається ця презентація і ви хочете запустити її у своїй базі даних, то ви можете пройти по цьому посиланню, виділеному рожевим кольором, і завантажити SQL для цієї презентації. І можете просто запустити її у вашому PSQL і вся презентація виявиться у вас на екрані негайно. Вона не міститиме квітів, але принаймні ми зможете її побачити.
У разі ми бачимо ID транзакції. Це номер, який ми їй надали. І є ще один тип ID транзакції в Postgres, який називається віртуальний ID транзакція
І ми маємо зрозуміти це. Це дуже важливо, інакше ми не зможемо зрозуміти блокування у Postgres.
Віртуальний ID транзакція – це ID транзакції, яка містить постійних значень. Наприклад, якщо я запускаю команду SELECT, то я, швидше за все, не змінюватиму базу даних, я нічого не блокуватиму. Тому коли ми запускаємо простий SELECT, ми не даємо цієї транзакції постійний ID. Ми там даємо їй лише віртуальний ID.
І це підвищує продуктивність Postgres, покращує можливості для очищення, тому віртуальний ID транзакції складається із двох чисел. Перше число перед слешем – це ID бекенда. А праворуч ми бачимо просто лічильник.
Тому якщо запускаю запит, він каже, що ID бекенд – 2.
А якщо я запускаю серію таких транзакцій, то бачимо, що лічильник щоразу збільшується, коли я запускаю запит. Наприклад, коли я запускаю запит 2/10, 2/11, 2/12 і т.д.
Майте на увазі, що тут є дві колонки. Зліва ми бачимо віртуальний ID транзакції – 2/12. А праворуч у нас є постійний ID транзакції. І це поле пусте. І ця транзакція не модифікує бази даних. Тому я не надаю їй постійного ID транзакції.
Як тільки я запускаю команду аналізувати ( (ANALYZE)), той самий запит видає мені постійний ID транзакції. Подивіться, як це змінилося. Раніше я не мав цього ID, тепер з'явився.
Отже, тут є ще один запит, ще одна транзакція. Віртуальний номер транзакції – 2/13. І якщо я попрошу постійний ID транзакції, то коли я запущу запит, я його отримаю.
Отже, ще раз. У нас є віртуальний ID транзакції та постійний ID транзакції. Просто зрозумійте цей момент, щоб зрозуміти поведінку Postgres.
Ми переходимо до третього розділу. Тут ми просто пройдемо через різні типи блокувань у Postgres. Це дуже цікаво. Останній розділ буде набагато цікавішим. Але ми маємо розглянути базові речі, бо інакше ми не зрозуміємо того, що буде далі.
Ми пройдемо через цей розділ, подивимося на кожен тип блокувань. І я вам покажу приклади, як вони встановлюються, як вони працюють, покажу вам деякі запити, які можна використовувати, щоб переглянути, як працює блокування в Postgres.
Щоб створити запит та подивитися, що відбувається в Postgres, нам потрібно випустити запит у system view. У цьому випадку червоним кольором у нас виділено pg_lock. Pg_lock – це системна таблиця, яка говорить нам, які блокування зараз використовуються у Postgres.
Тим не менш, мені дуже складно показати вам pg_lock сам по собі, тому що це досить складно. Тому я створив перегляд, який показує pg_locks. І він також виконує для мене деяку роботу, яка дозволяє мені краще зрозуміти. Т. е. він виключає мої блокування, мою власну сесію і т. д. Це просто стандартний SQL і він дозволяє краще показати, що відбувається.
Ще одна проблема в тому, що цей view дуже широкий, тому мені доводиться створити другий lockview2.
І він показує мені ще стовпчики з таблиці. І ще один, який показує мені решту колонок. Це досить складно, тому я постарався уявити це якомога простіше.
Отже, ми створили таблицю, яка називається Lockdemo. І ми створили там один рядок. Це наша зразкова таблиця. І ми створюватимемо розділи, щоб просто показати вам приклади блокувань.
Отже, один рядок, один стовпчик. Перший тип блокування називається ACCESS SHARE. Це найменше забороняюче блокування. Це означає, що вона практично не конфліктує з іншими блокуваннями.
І якщо хочемо експліцитно визначити блокування, ми запускаємо команду «lock table». І вона явно заблокує, тобто у режимі ACCESS SHARE ми запускаємо lock table. І якщо я запущу PSQL у фоновому режимі, я запускаю таким чином другу сесію з моєї першої сесії. Т. е. що я зроблю тут? Я переходжу до іншої сесії та кажу їй «покажи мені lockview для цього запиту». І тут у мене AccessShareLock у цій таблиці. Це саме те, що я просив. І він каже, що блокування було присвоєно. Дуже просто.
Далі, якщо ми дивимось у другу колонку, то там нічого немає. Вони порожні.
І якщо я запускаю команду SELECT, то це імпліцитний (явний) спосіб запросити AccessShareLock. Тому я випускаю свою таблицю та запускаю запит, і запит повертає кілька рядків. І в одному з рядків ми бачимо AccessShareLock. Таким чином, SELECT викликає AccessShareLock в таблиці. І він не конфліктує практично ні з чим, бо це є блокування низького рівня.
Що якщо я запущу SELECT і маю три різні таблиці? Раніше я запускав лише одну таблицю, тепер я запускаю три: pg_class, pg_namespace та pg_attribute.
І тепер, коли я дивлюся на запит, я бачу 9 AccessShareLocks у трьох таблицях. Чому? Синім кольором виділено три таблиці: pg_attribute, pg_class, pg_namespace. Але ви також можете бачити, що всі індекси, які визначені через ці таблиці також мають AccessShareLock.
І це блокування, яке практично не конфліктує з іншими. А все, що вона робить, це просто не дає нам скинути таблицю, доки ми її вибираємо. Це має сенс. Т. е. якщо ми вибираємо таблицю, вона в цей момент зникає, то це неправильно, тому AccessShare – це блокування низького рівня, яке говорить нам "не видаляйте цю таблицю, доки я працюю". По суті це все, що вона робить.
ROW SHARE – це блокування трохи відрізняється.
Візьмемо приклад. SELECT ROW SHARE спосіб блокування кожного рядка окремо. Таким чином, ніхто не може видалити їх або змінити їх, поки ми їх дивимося.
Отже, що робить SHARE LOCK? Ми бачимо, що ID транзакції 681 для SELECT'а. І це цікаво. Що тут у нас сталося? Перший раз ми бачимо номер у полі Lock. Ми беремо ID транзакції і він каже, що блокує її в ексклюзивному режимі. Все, що він робить, він каже, що я маю рядок, який технічно заблокований десь у таблиці. Але не каже, де конкретно. Трохи згодом ми докладніше це розглянемо.
Тут говоримо, що блокування використовується нами.
Отже, ексклюзивне блокування експліцитно(явно) каже, що воно ексклюзивне. І якщо ви видаляєте рядок у цій таблиці, це і станеться, як ви можете бачити.
SHARE EXCLUSIVE – це довше блокування.
Це (ANALYZE) команда аналізатора, яка використовуватиметься.
SHARE LOCK – ви можете заблокувати експліцитно в режимі share.
Ви також можете створити унікальний індекс. І там ви можете побачити SHARE LOCK, який є їхньою частиною. І він блокує таблицю та встановлює на неї блокування SHARE LOCK.
За замовчуванням SHARE LOCK на таблиці означає, що інші люди можуть читати таблицю, але не може її модифікувати. І саме це відбувається, коли ви створюєте унікальний індекс.
Якщо я створюю унікальний concurrently індекс, то у мене буде інший тип блокування, тому що, як ви пам'ятаєте, використання concurrently індексів знижує вимогу до блокування. І якщо я використовую нормальне блокування, нормальний індекс, то я таким чином запобігу запису в індекс таблиці під час його створення. Якщо я використовую поточний індекс, то мені потрібно використовувати інший тип блокування.
SHARE ROW EXCLUSIVE – знову можна задати експліцитно (явно).
Або можемо створити правило, тобто взяти певний випадок, при якому вона буде використовуватися.
EXCLUSIVE блокування означає, що ніхто інший змінювати таблицю не зможе.
Тут бачимо різні типи блокувань.
ACCESS EXCLUSIVE, наприклад, команда блокування. Наприклад, якщо ви робите CLUSTER table
, то це означатиме, що ніхто не зможе записувати туди. І вона блокує не тільки саму таблицю, а й індекси також.
Це друга сторінка блокування ACCESS EXCLUSIVE, де бачимо конкретно, що вона блокує у таблиці. Вона блокує окремі рядки таблиці, що досить цікаво.
Це вся базова інформація, яку я хотів надати. Ми говорили про блокування, про ID транзакцій, говорили про віртуальні ID транзакції, про постійні ID транзакції.
І тепер ми пройдемося за прикладами блокувань. Це найцікавіша частина. Ми подивимося дуже цікаві випадки. І моє завдання у цій презентації – дати вам найкраще уявлення про те, що Postgres насправді робить, коли він намагається блокувати ті чи інші речі. Мені здається, що він добре вміє блокувати окремі частини.
Давайте розглянемо певні приклади.
Ми почнемо з таблиць та з одного рядка в таблиці. Коли я вставляю щось, у мене відображається ExclusiveLock, ID транзакції та ExclusiveLock на таблиці.
А що буде, якщо я вставлю ще два ряди? І тепер у нашій таблиці три ряди. І я вставив один ряд і одержав ось це на виході. І якщо я вставляю ще два ряди, що тут дивного? Тут є дивина, тому що я додав три ряди до цієї таблиці, але я все ще два ряди в таблиці блокування. І це, по суті, основна поведінка Postgres.
Багато хто думаю, що якщо в базі даних, ви блокуєте 100 рядів, то вам буде потрібно створити 100 вводів блокувань. Якщо я блокуватиму відразу 1 000 рядів, то тоді мені потрібна буде 1 000 таких запитів. І якщо мені потрібно мільйон чи мільярд заблокувати. Але якщо ми так робитимемо, то це буде не дуже добре працювати. Якщо ви використовували систему, яка створює введення блокування для кожного окремого ряду, ви бачите, що це складно. Тому що вам потрібно визначити відразу таблицю блокування, яка може переповнитись, але Postgres так не робить.
І на цьому слайді дуже важливо, що тут очевидно демонструється, що є ще одна система, яка працює всередині MVCC, яка блокує окремі рядки. Тому коли ви блокуєте мільярди рядів, то Postgres не створює мільярд окремих команд на блокування. І це дуже добре позначається на продуктивності.
А як щодо оновлення? Я зараз оновлюю ряд, і ви можете помітити, що він одразу виконав дві різні операції. Він одночасно заблокував таблицю, але також заблокував індекс. І йому потрібно було блокувати індекс, бо є унікальні обмеження на цій таблиці. І ми хочемо переконатись, що ніхто його не змінює, тому ми його блокуємо.
А що буде, якщо я хочу оновити два ряди? І ми бачимо, що він поводиться також. Ми проводимо вдвічі більше оновлень, але така сама кількість рядків блокування.
Якщо вам цікаво, як Postgres це робить, вам потрібно послухати мої виступи з MVCC, щоб дізнатися, як Postgres внутрішньо маркує ці рядки, які він змінює. І у Postgres є спосіб, за допомогою якого він це робить, але це не робить на рівні блокування таблиць, він робить на більш низькому і на більш ефективному рівні.
А якщо хочу щось видалити? Якщо я видаляю, наприклад, один ряд і у мене все ще є мої два вступні блокування, і навіть якщо я захочу видалити їх усі, то вони все одно там присутні.
І, наприклад, я хочу вставити 1 рядків, а потім або видалити, або додати 000 рядків, то ті індивідуальні рядки, які я додаю або змінюю, вони не записуються тут. Вони записуються на нижчому рівні всередині ряду. І під час виступу з MVCC я говорив про це у деталях. Але дуже важливо, коли ви аналізуєте блокування, переконатися, що у вас блокування на рівні таблиці і що ви не бачите, як ведеться запис окремих рядів.
А як щодо експліцитного блокування?
Якщо я натисну «оновити», то маю два заблоковані ряди. І якщо виокремлю їх все і натисну «оновити скрізь», то в мене все одно залишаються два записи блокування.
Ми не створюємо окремих записів на кожен окремий ряд. Тому що тоді падає продуктивність, там може бути надто багато. І ми можемо опинитися у неприємній ситуації.
І те ж саме, якщо ми робимо shared, ми можемо робити на всі 30 разів.
Ми відновлюємо нашу таблицю, видаляємо все, потім знову вставляємо один ряд.
Ще один вид поведінки, який ви бачите в Postgres, це дуже добре відома та бажана поведінка – це те, що ви можете проводити update або select. І ви можете це робити одночасно. І select не блокують update і теж у зворотний бік. Ми говоримо читачеві не блокувати того, хто пише, а той, хто пише, не блокував читача.
Я вам покажу цей приклад. Я зараз зроблю вибір. Ми потім зробимо INSERT. І потім зможете побачити – 694. Ви зможете побачити ID транзакції, яка провела цю вставку. І це те, як це працює.
І якщо я зараз подивлюся на свій бекенд ID, він став – 695.
І я можу побачити, що 695 з'являється у моїй таблиці.
І якщо я проводжу оновлення тут так, то я отримую інший кейс. У цьому випадку 695 – ексклюзивне блокування, а у update така сама поведінка, але між ними не виникає конфлікту, що досить незвичайно.
І ви можете помітити, що вгорі – це ShareLock, а внизу – це ExclusiveLock. І обидві транзакції вийшли.
І треба послухати мій виступ у MVCC, аби зрозуміти, як це відбувається. Але це ілюстрація того, що ви можете робити це одночасно, тобто одночасно робити SELECT та UPDATE.
Давайте ми скинемо і вкотре зробимо одну операцію.
Якщо ви спробуєте запустити одночасно два update на тому самому ряду, то воно заблокується. І пам'ятайте, я говорив, що читач не блокує письменника, а письменник читача, але один письменник блокує іншого письменника. Т. е. ми не можемо робити так, щоб дві людини одночасно оновлювали один і той же ряд. Потрібно чекати, поки один із них закінчить.
І для того, щоб це проілюструвати, я подивлюся на Lockdemo таблицю. І ми подивимось на один ряд. На транзакцію 698.
Ми це оновили до 2-х. 699 – це перше оновлення. І воно пройшло успішно або воно перебуває в транзакції, що очікує, і чекає, коли ми підтвердимо або скасуємо.
Але подивіться інше – 2/51 – це наша перша транзакція, наша перша сесія. 3/112 – це другий запит, який з'явився зверху і який змінив це значення на 3. І якщо ви помітите, верхній заблокував сам себе, який 699. Але 3/112 не надали блокування. У колонці Lock_mode написано, що чекає. Він очікує 699. І якщо ви подивіться, де 699, він вищий. І що зробила перша сесія? Вона створила ексклюзивне блокування на власному транзакційному ID. Це те, як Postgres це робить. Він блокує свій транзакційний ID. І якщо ви хочете чекати, поки хтось підтвердить або скасує, то потрібно чекати, поки є транзакція, що очікується. І тому ми можемо побачити дивний рядок.
Погляньмо ще раз. Зліва ми бачимо наше процесингове ID. У другій колонці ми бачимо наш віртуальний ID транзакції, а третьої бачимо lock_type. Що це означає? По суті вона каже, що блокує транзакційний ID. Але зауважте, що у всіх рядах унизу написано relation. І тому у вас два види блокування у таблиці. Існує блокування relation. А також є блокування transactionid, де ви блокуєте самостійно, це саме те, що відбувається на першому ряду або в самому низу, де transationid, де ми очікуємо, щоб 699 закінчив свою операцію.
Я дивлюся, що тут виходить. І тут одночасно трапляються дві речі. Ви дивитеся на блокування по транзакційному ID у першому ряду, яке блокує сама себе. І вона блокує сама себе, щоб змусити людей чекати.
Якщо ви подивіться на шосту сходинку, то той самий запис, що й перший. І тому транзакція 6 блокується. 699 також самоблокується. І потім у нижньому ряду ви побачите, що ми чекаємо, коли закінчить свою операцію.
І у lock_type, tuple ви бачите цифри.
Ви можете побачити, що це 0/10. І це номер сторінки, а також offset цього конкретного ряду.
І ви бачите, що стає 0/11, коли ми оновлюємо.
Але насправді – це 0/10, тому що відбувається очікування на цю операцію. У нас є можливість подивитися, що це той ряд, який я чекаю на підтвердження.
Як тільки ми його підтвердили і натиснули на commit, і коли оновлення закінчилося, це те, що ми отримуємо знову. Транзакція 700 – це єдине блокування, воно більше нікого не чекає, бо його закоммітували. Вона просто чекає, щоб транзакція завершилась. Як тільки 699 закінчується, ми більше нічого не чекаємо. І тепер транзакція 700 каже, що все добре, що всі блокування, які потрібні, має у всіх дозволених таблицях.
І щоб ще ускладнити всю цю справу, ми створюємо ще один view, який цього разу надасть нам ієрархію. Я не чекаю, що ви зрозумієте цей запит. Але це дасть нам більш чіткий вигляд того, що відбувається.
Це рекурсивний вид, який також має ще одну секцію. І вона потім знову повертає все разом. Давайте використати це.
Що, якщо ми зробимо три одночасні оновлення і скажемо, що ряд зараз дорівнює трьом. І ми змінимо 3 на 4.
І ось ми бачимо 4. І транзакційний ID 702.
І потім я поміняю 4 на 5. А 5 на 6, а 6 на 7. І я вибудовую в чергу низку людей, які чекатимуть на те, щоб ця одна транзакція закінчилася.
І все стає зрозумілим. Який перший ряд? Це 702. Це транзакційний ID, який спочатку поставив це значення. А що в мене написано у колонці Granted? У мене є позначки f
. Це ті мої оновлення, які не можуть бути схвалені (5, 6, 7), тому що ми чекаємо, щоб транзакційний ID 702 закінчився. Там ми маємо блокування транзакційного ID. І виходить 5 транзакційних блокувань ID.
І якщо ви подивіться на 704, 705, то там ще нічого не написано, тому що вони ще не знають, що відбувається. Вони просто пишуть, що без уявлення, що відбувається. І вони просто підуть у сон, тому що вони чекають, поки хтось закінчить і їх розбудять, коли з'явиться можливість змінити низку.
Це те, як це виглядає. Зрозуміло, що вони всі чекають 12-го рядка.
Це те, що ми бачили тут. Ось 0/12.
Отже, як тільки перша транзакція схвалена, ви можете тут побачити, як працює ієрархія. І тепер стає зрозуміло. Вони усі стають чистими. І вони насправді все ще чекають.
Ось що відбувається. 702 комітується. І тепер 703 отримує це блокування ряду, а потім 704 починає чекати, коли 703 закоммітується. І 705 теж цього чекає. І коли все це завершується, вони самі себе зачищають. І я хотів би вказати на те, що всі вишиковуються в чергу. І це дуже схоже на ситуацію з пробкою, коли всі чекають на першу машину. Перша машина зупинилася, і всі вишиковуються в довгу лінію. Потім вона рухається, потім наступна машина може проїхати вперед і отримати своє блокування тощо.
І якщо вам це здалося недостатньо складним, ми зараз поговоримо з вами про deadlocks. Я не знаю, хто з вас із ними стикався. Це досить поширена проблема у системах баз даних. Але deadlocks – це той випадок, коли одна сесія чекає, щоб щось виконала інша сесія. А зараз інша сесія очікує, щоб перша сесія виконала щось.
І, наприклад, якщо Іван каже: "Дай мені щось", а я кажу: "Ні, я тобі це дам тільки, якщо ти мені даси щось інше". А він каже: "Ні, я не дам тобі це, якщо ти мені не даси". І ми виходить у ситуації мертвого блокування. Я впевнений, що Іван так не зробить, але ви розумієте сенс, що у нас дві людини хочуть щось отримати і вони не готові це віддати, доки інша людина їм не віддасть, що вони хочуть. І тут немає рішення.
І, по суті, вашій базі даних це потрібно виявляти. І потім необхідно видаляти або закривати одну із сесій, тому що у протилежному випадку вони там залишаться назавжди. І ми це бачимо у базах даних, ми це бачимо в операційних системах. І в усіх місцях, де ми маємо паралельні процеси, таке може бути.
Та ми доставимо зараз два deadlocks. Ми поставимо 50 та 80. У перший ряд я проведу оновлення з 50 на 50. У мене вийде номер транзакції 710.
І потім я поміняю 80 на 81 і 50 на 51.
І ось, як це виглядатиме. І тому 710 має блокування ряду, а 711 чекає підтвердження. Ми бачили це, коли оновлювали. 710 – власник нашого ряду. А 711 очікує, щоб 710 закінчив транзакцію.
І там навіть написано, на якому саме ряді у нас відбувається deadlocks. І ось де це починає стає дивним.
Тепер ми оновлюємо 80-80.
І ось де починається deadlocks. 710 очікує відгуку від 711, а 711 чекає 710. І це погано скінчиться. І із цього немає виходу. І вони чекатимуть на відгук один від одного.
І це просто все затримуватиме. І ми цього не хочемо.
І у Postgres є способи помічати, коли це відбувається. І коли це відбувається, то ви отримуєте таку помилку. І з цього ясно, що такий процес чекає SHARE LOCK'а від іншого процесу, тобто який блокується 711 процесом. А той процес очікував, щоб було дано SHARE LOCK на такий транзакційний ID і був заблокований таким процесом. Тому тут ситуація мертвого блокування.
Чи буває тристоронній deadlocks? Чи це можливо? Так.
Ми вбиваємо ці цифри у таблицю. Ми змінюємо 40 на 40, ми робимо блокування.
Змінюємо 60 на 61, 80 на 81.
А потім ми міняємо 80, а потім бум!
І 714 тепер чекає 715. 716-ий 715-го чекає. І із цим уже нічого не зробити.
Тут уже не дві людини, тут уже три особи. Я хочу щось від тебе, цей хоче щось від третьої людини, а третя людина хоче щось від мене. І ми виходить у тристоронньому очікуванні, тому що ми всі чекаємо, поки інша людина завершить те, що вона має зробити.
І Postgres знає, на якому ряді це відбувається. І тому він видасть вам наступне повідомлення, яке показує, що у вас є проблема, де три вступні блокують один одного. І тут нема обмежень. Це може бути у випадку, де 20 записів блокують один одного.
Наступна проблема – це serializable.
Якщо спеціальне послідовне блокування.
І повертаємось до 719. У нього цілком нормальна видача.
І ви можете натиснути, щоб зробити транзакцію з serializable.
І ви розумієте, що у вас тепер є інший вид блокування SA це означає serializable.
І тому ми маємо новий вид блокування, який називається SARieadLock, який є серійним блокуванням і дозволяє вводити серійники.
Також ви можете вставляти унікальні індекси.
У цій таблиці ми маємо унікальні індекси.
Тому, якщо я сюди введу число 2, тому я маю 2. Але в самомугорі я вставляю ще одне 2. І ви можете бачити, що у 721-го ексклюзивне блокування. Але тепер 722 очікує, щоб 721 завершив свою операцію, тому що він не може вставити 2 до тих пір, поки не знає, що станеться з 721.
І якщо ми робимо subtransaction.
Ось у нас 723.
І якщо ми зберігаємо крапку і потім її оновлюємо, то ми отримуємо новий транзакційний ID. Це ще один характер поведінки, який вам потрібно знати. Якщо ми повертаємо це, то транзакційний ID йде. 724 йде. Але тепер ми маємо 725.
І що я намагаюся тут зробити? Я намагаюся показати вам приклади незвичайних блокувань, які ви можете знайти: будь то serializable блокування або SAVEPOINT - це різні види блокувань, які з'являтимуться в таблиці блокувань.
Це створення експліцитних (явних) блокувань, які мають pg_advisory_lock.
І ви бачите, що тип блокування тут вважається як advisory. І тут червоним написано advisory. І можна одночасно так заблокувати з pg_advisory_unlock.
І завершуючи я хотів би вам показати ще одну дахісну річ. Я створю ще один вид. Але я поєднаю таблицю pg_locks з таблицею pg_stat_activity. І навіщо хочу це зробити? Тому що це дозволить мені подивитися та побачити всі поточні сесій та побачити, яких саме блокувань вони очікують. І це досить цікаво, коли ми збираємо воєдино таблицю блокувань та таблицю запитів.
І тут ми створюємо pg_stat_view.
І ми оновлюємо ряд на один. І тут бачимо 724. А потім ми оновлюємо наш ряд до трьох. І що ви бачите тут зараз? Це запити, тобто ви бачите весь список запитів, які перелічені у лівій колонці. А потім праворуч ви можете бачити блокування і те, що вони створюють. І це може бути більш зрозумілим для вас, щоб вам не було потреби щоразу повертатися до кожної сесії та дивитися – чи потрібно до неї приєднатися чи ні. За нас це роблять.
Ще одна функція, яка дуже корисна – це pg_blocking_pids
. Ви, мабуть, ніколи не чули про неї. Що вона робить? Вона дозволяє нам сказати, що для цієї сесії 11740, які саме ID-процесів вона очікує. І ви можете бачити, що 11740 очікує 724. І 724 знаходиться на вершині. А 11306 є вашим ID процесом. По суті, ця функція йде за таблицею блокувань. І я знаю, що це трохи складно, але у вас це виходить розуміти. По суті, ця функція проходить через цю таблицю блокувань і намагається знайти, де цей процес ID, враховуючи ті блокування, на які вона чекає. І також намагається обчислити, який саме процес ID, у того процесу, який чекає на блокування. Тому ви можете запустити цю функцію pg_blocking_pids
.
І це дуже корисно. Ми це додали тільки з версії 9.6, тому цієї функції лише 5 років, але вона дуже і дуже корисна. І те саме стосується другого запиту. Він показує те, що нам треба бачити.
Це те, про що я хотів поговорити з вами. І як я очікував, ми використовували весь наш час, тому що була така велика кількість слайдів. І слайди доступні для завантаження. Я хотів би подякувати вам за те, що ви були тут. Я впевнений, що вам сподобається залишок конференції, дякую!
Питання:
Наприклад, якщо я намагаюся оновити рядки, а друга сесія намагається видалити всю таблицю. Наскільки я розумію, що там має бути щось на зразок intent lock'а. Чи є таке у Postgres?
Повертаємось до самого початку. Можливо, ви пам'ятаєте, що коли ви робите будь-що, наприклад, коли ви робите SELECT, то ми видаємо AccessShareLock. І це запобігає видаленню таблиці. Тому якщо ви, наприклад, хочете оновити ряд у таблиці або видалити ряд, то хтось не може видалити всю таблицю одночасно з цим, тому що ви тримає цей AccessShareLock над всією таблицею і над рядом. І коли ви закінчили, вони можуть це видалити. Але поки що ви безпосередньо щось там міняєте, вони не зможуть зробити.
Давайте ще раз. Давайте перейдемо, наприклад, з видаленням. І ви бачите, як на ряді є ексклюзивний lock над усією таблицею.
Це буде схоже на lock exclusive, правильно?
Так, це схоже на це. Я розумію, про що ви кажете. Ви кажете, що, якщо я виконаю SELECT, то у мене буде ShareExclusive, а потім я перекладаю це в стан Row Exclusive, чи це стає проблемою? Але напрочуд це не створює проблему. Це схоже на збільшення ступеня блокування, але, по суті, я lock, який запобігає видалення. І зараз, коли я роблю цей замок потужнішим, він все ще запобігає видаленню. Тому це не те, щоб я піднімаюсь нагору. Т. е. він запобігав це і коли він був на нижчому рівні, тому, коли я підвищую його рівень, він все ще запобігає видаленню таблиці.
Я розумію, про що ви кажете. Тут немає випадку збільшення ступеня блокування, де ви намагаєтеся відмовитись від одного блокування, щоб ввести більш потужне. Тут це просто повсюдно збільшує це запобігання, тому це не викликає жодного конфлікту. Але це гарне питання. Дуже дякую, що його поставили!
Що нам необхідно робити для того, щоб уникнути ситуації deadlock'у, коли у нас є багато сесій, велика кількість користувачів?
Postgres автоматично помічає ситуації deadlock'а. І автоматично видалятиме одну із сесій. Єдиний спосіб, який допоможе уникнути ситуації з мертвими блокуваннями, якщо блокуватимете людей в тому самому порядку. Тому, коли ви подивіться на вашу програму, то часто причина deadlocks… Давайте уявимо, що я хочу заблокувати дві різні речі. Один додаток блокує таблицю 1, а інший додаток блокує 2, а потім таблицю 1. І найпростіший спосіб уникнути deadlocks – це подивитися на вашу програму і постаратися переконатися в тому, що блокування відбувається в одній тій же черговості у всіх додатках. І це, як правило, прибирає 80% проблем, тому що різні люди пишуть ці додатки. І якщо ви блокуєте їх в тому самому порядку, то ви не стикаєтеся з ситуацією deadlock'а.
Дуже дякую за ваш виступ! Ви говорили про vacuum full і, якщо я правильно розумію, vacuum full деформує порядок записів в окремому зберіганні, тому зберігають поточні записи незмінними. А чому vacuum full бере доступ до ексклюзивного блокування і чому він конфліктує з операціями із запису?
Це гарне питання. Причина у тому, що vacuum full бере таблицю. І ми по суті створюємо нову версію таблиці. І таблиця буде новою. Виходить, це буде зовсім нова версія таблиці. І проблема полягає в тому, що коли ми це робимо, ми не хочемо, щоб люди читали це, тому що нам потрібно, щоб вони побачили нову таблицю. І тому це поєднується з попереднім питанням. Якби ми могли одночасно читати, то ми не змогли б його перемістити і направляти людей на нову таблицю. Нам слід було б очікувати, щоб кожен перестав читати цю таблицю, і тому, по суті, це ситуація lock exclusive.
Ми просто говоримо, що ми блокуємо з самого початку, тому що ми знаємо, що наприкінці нам буде потрібно ексклюзивне блокування для того, щоб пересунути всіх на нову копію. Тому потенційно ми можемо це дозволити. І ми так робимо це з одночасним індексуванням. Але це набагато складніше зробити. І це дуже сильно стосується вашого попереднього питання про lock exclusive.
Чи можливо додавати locking timeout до Postgres? У Oracle я можу, наприклад, написати «вибрати оновлення» і чекати 50 секунд до оновлення. Це було добре для застосування. Але в Postgres у мене або потрібно це робити відразу і зовсім не чекати, або чекати до якогось часу.
Так, ви можете вибрати тайм-аут на блокування, на ваші locks. Ви також можете видати команду no way, яка буде … якщо ви не зможете відразу отримати блокування. Тому або lock timeout, або інше, що дозволить вам це зробити. Це не робиться на синтаксичному рівні. Це робиться як змінна на сервері. Іноді це не можна використовувати.
Ви можете відкрити 75 слайдів?
Так.
І моє питання наступне. Чому очікують обидва процеси оновлення 703?
І це чудове питання. Я не розумію, до речі, чому Postgres це робить. Але коли 703 був створений, він чекав 702. І коли 704 і 705 з'являються, то здається, що вони не знають того, що вони чекають, тому що там ще нічого немає. І Postgres це робить так: коли ви не можете отримати блокування, то він пише «А сенс вас обробляти?», тому що ви й так чекаєте на когось. Тому просто дамо йому повисіти у повітрі, він взагалі не оновлює це. Але що сталося тут? Як тільки 702 завершив процес і 703 отримав своє блокування, система повернулася назад. І сказала, що тепер у нас є дві людини, які чекають. А потім давайте ми їх поновимо разом. І вкажемо, що обоє чекають.
Я не знаю чому Postgres так робить. Але є проблема, яка називається f…. Мені здається, що це не термін російською. Це коли всі чекають на один замк, навіть якщо є 20 інстанцій, на який чекають замку. І раптом вони всі прокидаються одночасно. І всі починають намагатися зреагувати. Але система робить так, що всі очікують 703. Тому що вони всі чекають, і ми їх одразу всіх вишикуємо в чергу. І якщо з'являється будь-який новий запит, який був сформований після цього, наприклад, 707, то там знову буде порожнеча.
І мені здається, що це робиться для того, щоб можна було сказати, що на цьому етапі 702 чекає 703, а всі ті, хто прийдуть після цього, у них не буде жодного запису в цьому полі. Але щойно перший очікуючий залишає, і всі ті, хто чекав у цей момент до оновлення, отримують той самий маркер. І тому, на мою думку, це зроблено для того, щоб ми могли обробляти по порядку, щоб вони були правильно впорядковані.
Я завжди дивився на це як на досить дивний феномен. Бо тут, наприклад, їх взагалі не перераховуємо. Але, мені здається, щоразу, коли ми даємо новий lock, ми дивимося на всіх тих, хто перебуває в процесі очікування. Тоді ми їх усіх вишиковуємо в чергу. А потім будь-яка нова, яка приходить, потрапляє в чергу лише тоді, коли наступна людина перестала оброблятися. Дуже добре питання. Дякую вам велике за питання!
Мені здається, набагато логічніше, коли 705 очікує 704.
Але проблема тут така. Технічно ви можете розбудити або той чи той. І тому ми розбудимо той чи інший. Але що відбувається у роботі системи? Ви бачите, як 703 у самому верху заблокував свій власний транзакційний ID. Це те, як Postgres працює. І 703 блокується власним транзакційним ID, тому якщо хтось хоче почекати, то він чекатиме 703. І, по суті, 703 завершує. І лише після його завершення якийсь із процесів прокидається. І ми не знаємо, який саме цей процес буде. Потім ми поступово все опрацьовуємо. Але не ясно який саме процес прокидається першим, тому що це може бути будь-який із цих процесів. По суті, у нас був планувальник, який казав, що ми тепер можемо будити будь-який із цих процесів. Ми просто вибираємо один випадковим чином. Тому обидва з них слід зазначити, тому що ми можемо пробуджувати будь-який із них.
І проблема в тому, що у нас CP-нескінченність. І тому цілком можливо можемо розбудити пізніший. І якщо, наприклад, ми будитимемо пізніший, то чекатимемо того, хто щойно отримав блокування, тому ми не визначаємо, хто саме буде пробуджений першим. Ми створюємо просто таку ситуацію, і система їх будитиме в довільному порядку.
є
Джерело: habr.com