Перейти до вмісту

Блог ·

Два замовлення, одна машина

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

Автор: Provibr

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

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

Суддею є індекс

Правило про те, що два замовлення ніколи не можуть тримати ту саму машину, живе в частковому унікальному індексі на ідентифікаторі обладнання, обмеженому резервуваннями в стані утримано. Не в застосунку, не в блокуванні, яке бере код, не в коментарі.

Це навмисно та сама форма, що й індекс зайнятості на одиницях стійки. Прикладний шар видає читабельну відмову перед зіткненням, а база даних вирішує, хто справді перемагає. Лише одне з двох може бути владним, і це має бути те, чого не обійде жоден шлях коду, доданий наступного року.

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

Станів чотири, і два з них навмисно утворюють пару: утримано, перетворено, спливло, скасовано. Спокусливим спрощенням є єдине звільнено для двох останніх, і воно лишило б без відповіді питання, чи хтось це скасував, чи воно просто лишилося лежати, а це саме те питання, яке лунає, коли клієнт телефонує про замовлення, що не відбулося. Прибирання, яке мовчки пише той самий стан, що й людина, стирає різницю між рішенням і вичерпаним строком.

Блокування всього набору коштувало резервування

Перша версія добирала машини-кандидати й блокувала їх однією вибіркою з FOR UPDATE SKIP LOCKED, оминаючи те, що вже було зайняте. Читається добре й є хибним, і показав це лише тест на одночасність.

Вісім одночасних замовлень на три придатні машини мали б дати три резервування. Вони дали два. Перша транзакція вхопила всі три рядки, використала один і тримала два інші до свого підтвердження; усі інші проминули всі три й не знайшли нічого. Набір не був вичерпаний, він був заручником.

Ліком є блокування по одному кандидату: бери першого, а якщо його тримає хтось інший, переходь до наступного. Той самий тест ще раз: три резервування на трьох різних машинах і пʼять чесних відмов. Два замовлення на одну машину дають одне резервування та одну відмову.

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

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

Порушення унікальності отруює транзакцію

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

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

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

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

Відмова має сказати, якого вона роду

Розподільник будує список кандидатів двічі, і різниця між цими двома підсумками є повідомленням клієнтові.

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

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

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

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

Резервування стає звʼязком

Коли замовлення завершується, машина має перейти з резервованої на продану. Очевидна реалізація звільняє резервування, а потім пише службовий звʼязок, і містить вікно в кілька мілісекунд, у якому машина вільна. У завантажений день це вікно знаходять.

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

Прибирання є прибиральником, а не вартовим

Резервування спливає через пʼятнадцять хвилин за замовчуванням, і робітник вимітає ті, що сплили, щохвилини.

Це число не є тими чотирма годинами, які отримує резервування стійки, і різниця полягає в тому, чого чекає кожне з них. Резервування стійки чекає на людину з візком: хтось має прийти в залу, поставити напрямні, перекласти кабелі. Резервування машини чекає на купівлю: екран оплати, перевірку на шахрайство, підтвердження. Тим часом найдорожчий запас, який має хостер, не продається. Пʼятнадцять хвилин щедрі для перерваного платежу, який хтось відновлює, і достатньо короткі, щоб буря покинутих замовлень не лишила полицю порожньою пів дня.

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

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

Що ми записали б

  1. Ставте суддю в базу даних. Застосунок має видавати читабельну відмову; індекс має визначати переможця.
  2. Блокуйте по одному кандидату. Блокування набору тримає рядки, яких ви не вживете, і симптомом є втрачений продаж, а не помилка.
  3. Обмежуйте обхід. Блокування по кандидату міняє заручений набір на довгий цикл, і цьому циклу теж потрібна межа.
  4. Порушення унікальності перериває транзакцію. Повторення без точки збереження дає помилку, яка називає не те.
  5. Дивіться на назву обмеження. Одна таблиця часто має кілька унікальностей, і для користувача вони означають різне.
  6. «Нічого немає» і «хтось був швидшим» є двома повідомленнями. Побудувати список кандидатів двічі є способом їх розрізнити.
  7. Підвищуйте, а не звільняйте задля перезапису. Будь-яку послідовність із вільною миттю всередині колись знайдуть.
  8. Дотримуйте строку в точці вжитку. Періодичне прибирання є господарством, а господарство не є правилом.

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

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

Читати далі

Почніть з одного хоста й одного агента

Створіть обліковий запис, встановіть агента однією командою та підключіть свій перший хост Proxmox. Пробний період триває 14 днів і не потребує платіжних даних.