Блог ·
Два замовлення, одна машина
Виділений сервер продається рівно один раз. Блокування всього набору кандидатів мовчки коштувало нам резервування, і ліком стало блокування по одній.
Автор: Provibr
Тариф на віртуальний сервер навмисно продає більше, ніж має, і ніхто через це не втрачає сну. Виділена машина так не може: вона одна, і якщо два замовлення обидва вважають, що дістали її, то в понеділок уранці один клієнт почує, що річ, за яку він заплатив у пʼятницю, належить комусь іншому.
Вимога вміщується в одне речення. Резервуй перед побічними діями та повертай потужність транзакційно після падіння чи невдачі. Її втілення дало два вимірювання, які змінили задум, і одне з них коштувало нам машини, що просто стояла вільною.
Суддею є індекс
Правило про те, що два замовлення ніколи не можуть тримати ту саму машину, живе в частковому унікальному індексі на ідентифікаторі обладнання, обмеженому резервуваннями в стані утримано. Не в застосунку, не в блокуванні, яке бере код, не в коментарі.
Це навмисно та сама форма, що й індекс зайнятості на одиницях стійки. Прикладний шар видає читабельну відмову перед зіткненням, а база даних вирішує, хто справді перемагає. Лише одне з двох може бути владним, і це має бути те, чого не обійде жоден шлях коду, доданий наступного року.
Утримано є єдиним станом, який щось утримує, і це записано таблицею поруч з індексом, а не логічним стовпцем, бо логічне значення поруч зі станом є другою правдою, вірною лише доти, доки кожен, хто пише, про нього памʼятає. Тест звіряє таблицю з індексом.
Станів чотири, і два з них навмисно утворюють пару: утримано, перетворено, спливло, скасовано. Спокусливим спрощенням є єдине звільнено для двох останніх, і воно лишило б без відповіді питання, чи хтось це скасував, чи воно просто лишилося лежати, а це саме те питання, яке лунає, коли клієнт телефонує про замовлення, що не відбулося. Прибирання, яке мовчки пише той самий стан, що й людина, стирає різницю між рішенням і вичерпаним строком.
Блокування всього набору коштувало резервування
Перша версія добирала машини-кандидати й блокувала їх однією вибіркою з FOR UPDATE SKIP LOCKED, оминаючи те, що вже було зайняте. Читається добре й є хибним, і показав це лише тест на одночасність.
Вісім одночасних замовлень на три придатні машини мали б дати три резервування. Вони дали два. Перша транзакція вхопила всі три рядки, використала один і тримала два інші до свого підтвердження; усі інші проминули всі три й не знайшли нічого. Набір не був вичерпаний, він був заручником.
Ліком є блокування по одному кандидату: бери першого, а якщо його тримає хтось інший, переходь до наступного. Той самий тест ще раз: три резервування на трьох різних машинах і пʼять чесних відмов. Два замовлення на одну машину дають одне резервування та одну відмову.
Той лік несе власний ризик, а саме довжину циклу. За ста одночасних замовлень остання транзакція обійшла б сто кандидатів, перш ніж перемогти, а довга транзакція затримує всі інші. Тому обхід спиняється на двадцяти пʼяти кандидатах, а понад це чесною відповіддю є «спробуйте ще за мить».
Зауважте, що пропуск і невдачу рахують окремо. Кандидата, якого тримає хтось інший, пропускають, і це не спроба, бо нічого не пішло не так. Кандидат, який програв перегони вставляння, є спробою, і таких щонайбільше пʼять.
Порушення унікальності отруює транзакцію
Це та пастка, довкола якої збудовано цикл повторення, і легко написати код, що виглядає правильним і падає під час першого справжнього зіткнення.
Усередині транзакції PostgreSQL порушення унікального ключа перериває транзакцію. Кожна наступна вказівка тоді відхиляється з іншою помилкою, «поточну транзакцію перервано», доки хтось не відкотиться. Отже, цикл, який ловить порушення й пробує наступного кандидата, не працює зовсім: вставляння другого кандидата зазнає невдачі з причини, що не має до нього жодного стосунку.
Тому кожна спроба виконується всередині точки збереження, що в нашому будівнику запитів є просто вкладеною транзакцією. Відкат до точки збереження, зовнішня транзакція знову здорова, наступний кандидат.
До цього належить подробиця про те, як ловлять порушення. Наш будівник запитів загортає помилки драйвера у власний тип і чіпляє справжню до причини, тож перевірка коду на верхньому рівні не бачить нічого. Треба обійти ланцюг причин, а ще треба дивитися на назву обмеження, бо ця таблиця має два унікальні обмеження, і вони кажуть протилежне: одне каже, що машину зайнято, друге каже, що це замовлення вже має резервування, і ось воно.
Відмова має сказати, якого вона роду
Розподільник будує список кандидатів двічі, і різниця між цими двома підсумками є повідомленням клієнтові.
Перший прохід є повним списком із причиною для кожної машини, яка відпала: не той майданчик, не той виробник, не готова тощо. Другий прохід є тим, що в ту мить справді вдалося заблокувати. Якщо повний список дав переможця, а придатний до блокування ні, то продати було що, і хтось випередив на частку секунди. Це інше речення, ніж «нічого немає»: одне означає спробуйте ще, друге означає купіть більше заліза. Вони заслуговують на різні кнопки.
Заморожена причина обраної машини потрапляє в рядок резервування, бо через місяць кожне обладнання стоїть інакше й уже не відтворити, чому розподільник узяв саме цю. Тоді рядок каже це сам.
Про ключ ідемпотентності питають двічі, і той другий раз є ще однією річчю, яку знайшов лише тест на одночасність. Пʼять одночасних запитів з тим самим ключем на чотирьох машинах дали одне резервування та чотири відмови, зокрема одну від транзакції, яка не змогла нічого заблокувати й тому ніколи не дійшла ані до вставляння, ані до гілки повторення, тоді як у ту мить резервування з її ключем просто існувало. Повторне питання безпосередньо перед відмовою це виправляє, і воно діє, бо на нашому рівні ізоляції кожна вказівка бере свіжий знімок: сусід, що щойно підтвердився, отже, видимий.
Гарантія, яка переживає все це, вузька й варта запису саме так: пʼять запитів з тим самим ключем дають одне резервування та чотири рази «хтось був швидшим». Ті, хто програв, ще не бачать переможця, бо в ту мить він ще не підтвердився. Немає версії, у якій це було б інакше без того, щоб усі чекали одне на одного, а чекання перетворює кожну завантажену хвилину на чергу.
Резервування стає звʼязком
Коли замовлення завершується, машина має перейти з резервованої на продану. Очевидна реалізація звільняє резервування, а потім пише службовий звʼязок, і містить вікно в кілька мілісекунд, у якому машина вільна. У завантажений день це вікно знаходять.
Тому перетворення пише звʼязок і переводить резервування в стан перетворено однією транзакцією, а рядок лишається утриманим, доки звʼязок не зʼявиться. У кожну мить цієї транзакції друге замовлення бачить або резервування, або звʼязок, і обидва його виключають. Вільної миті немає, бо жодна вказівка її не творить. Та сама форма, що й підвищення резервування стійки до позиції, на рівень вище.
Прибирання є прибиральником, а не вартовим
Резервування спливає через пʼятнадцять хвилин за замовчуванням, і робітник вимітає ті, що сплили, щохвилини.
Це число не є тими чотирма годинами, які отримує резервування стійки, і різниця полягає в тому, чого чекає кожне з них. Резервування стійки чекає на людину з візком: хтось має прийти в залу, поставити напрямні, перекласти кабелі. Резервування машини чекає на купівлю: екран оплати, перевірку на шахрайство, підтвердження. Тим часом найдорожчий запас, який має хостер, не продається. Пʼятнадцять хвилин щедрі для перерваного платежу, який хтось відновлює, і достатньо короткі, щоб буря покинутих замовлень не лишила полицю порожньою пів дня.
Важливо, де саме дотримується строк. Його перевіряють усередині перетворення, а не в прибиранні. Якби прибирання було єдиним місцем, резервування, що сплило між двома проходами, все одно перетворилося б, а вартовий, що приходить раз на хвилину, не вартує нічого. Прибирання впорядковує облік і повертає машину в запас; це прибиральна робота.
До того ж воно не має перемикача функції, і це навмисний виняток. Над усім модулем центру даних стоїть перемикач розгортання, і повторити його тут означало б, що вимкнення модуля лишає машини назавжди зарезервованими за замовленнями, яких уже ніхто не бачить. Перемикач, побічною дією якого є вимкнений строк, не є перемикачем, а є витоком.
Що ми записали б
- Ставте суддю в базу даних. Застосунок має видавати читабельну відмову; індекс має визначати переможця.
- Блокуйте по одному кандидату. Блокування набору тримає рядки, яких ви не вживете, і симптомом є втрачений продаж, а не помилка.
- Обмежуйте обхід. Блокування по кандидату міняє заручений набір на довгий цикл, і цьому циклу теж потрібна межа.
- Порушення унікальності перериває транзакцію. Повторення без точки збереження дає помилку, яка називає не те.
- Дивіться на назву обмеження. Одна таблиця часто має кілька унікальностей, і для користувача вони означають різне.
- «Нічого немає» і «хтось був швидшим» є двома повідомленнями. Побудувати список кандидатів двічі є способом їх розрізнити.
- Підвищуйте, а не звільняйте задля перезапису. Будь-яку послідовність із вільною миттю всередині колись знайдуть.
- Дотримуйте строку в точці вжитку. Періодичне прибирання є господарством, а господарство не є правилом.
Для ясності щодо того, чим це є: робота з логістики центру даних лежить за перемикачем розгортання, вимкненим за замовчуванням, із налаштуванням на кожну організацію поверх нього, тож нічого з цього сьогодні не продається. Дві речі лишаються недоведеними. Прибирання за строком виміряно як функцію, а не як робітника в роботі. І сирого запиту до серверних дій із закритим перемикачем не пробували.
Із платформної роботи Provibr. Те саме підвищення замість перезапису, застосоване до одиниць стійки, описано в резервування стає позицією, а чому вільні одиниці в стійці не є продажною потужністю, описано в місце у стійці не є ємністю.