Database transactions/cs

Databázové transakce využívá MediaWiki k zachování konzistence databáze a tím i ke zlepšení jejího výkonu.

Některé obecné informace o databázových transakcích naleznete zde:


 * Na Wikipedii viz Databázová transakce a ACID
 * Pro MySQL viz transakční výpis a transakční model InnoDB



Rozsah transakce
Nejprve bychom měli rozlišit dva typy metod:


 * S vnějším rozsahem transakce: Metody, které strukturálně jasně zaručují, že nemají žádné volající v řetězci, kteří provádějí transakční operace (kromě zabalení metody okolo transakce jménem uvedené metody). O takových metodách se říká, že vlastní cyklus transakce. Místa, která mají tento rozsah, jsou metoda Skripty údržby, metoda  tříd Job a metoda  tříd DeferrableUpdate. Když jsou tyto metody spuštěny, žádní volající dále v zásobníku volání nebudou mít žádnou transakční aktivitu kromě případného definování počáteční/koncové hranice. O volajícím z vnějšího rozsahu, u kterého je také strukturálně zaručeno, že začne bez deklarovaného cyklu transakce, se říká, že má definující rozsah transakce. To znamená, že metody s vnějším rozsahem transakce jsou volné pro zahájení a ukončení transakcí (s ohledem na některá upozornění popsaná níže). Volající v zásobníku nemají vnější rozsah a očekává se, že tuto skutečnost budou respektovat.
 * S nejasným/vnitřním rozsahem transakce: To jsou metody, u kterých není jasně zaručeno, že mají vnější rozsah transakce. Takové metody nevlastní cyklus transakce, pokud takový existuje. Toto je většina metod v jádru MediaWiki a rozšířeních. Do této kategorie spadají různé metody, jako jsou třídy model/abstrakce, třídy užitných vlastností, objekty DAO, obslužné rutiny háčku, třídy obchodní/kontrolní logiky a tak dále. Tyto metody nesmí volně spouštět/ukončit transakce a musí používat pouze sémantiku transakcí, která podporuje vnořování. Pokud potřebují provést nějaké aktualizace po potvrzení, musí zaregistrovat metodu zpětného volání po potvrzení.

Pokud je kolo transakce zahájeno přes, pak se nazývá explicitní kolo transakce. V opačném případě, pokud  zabalí jakoukoli aktivitu dotazu do transakčního cyklu, jak je tomu obvykle během webových požadavků, pak se to nazývá implicitní cyklus transakce. Takové cykly jsou bez vlastníka a provádí je MediaWiki při vypnutí prostřednictvím. Volající mohou zahájit explicitní cykly uprostřed implicitních cyklů, v takovém případě budou všechny čekající zápisy do databáze potvrzeny, když se explicitní cyklus potvrdí.



Základní použití transakce
MediaWiki používá transakce několika způsoby:


 * 1) Používání "tradičních" párů / k ochraně kritických sekcí a ujištění, že jsou zavázány. Vnořené transakce nejsou podporovány. Toto by mělo být použito pouze u volajících, kteří mají vnější rozsah transakcí a ovlivňují pouze jednu databázi (přičemž také počítají s všemi možnými obslužnými osobami). Platné metody zahrnují zpětná volání na  nebo , kde je aktualizována pouze jedna DB a nejsou aktivovány žádné háčky. Vždy spojte každý  s.
 * 2) Použití párů / k ochraně kritických sekcí, aniž byste věděli, kdy se zadají. Vnořené sekce jsou plně podporovány. Ty lze použít kdekoli, ale musí být správně vnořené (např. neotevírejte sekci a poté ji nezavírejte před příkazem "return"). Ve skriptech údržby, když nejsou otevřené žádné atomické sekce, dojde k potvrzení. Pokud je však nastaven příznak , atomické sekce se připojí k hlavní sadě transakcí  . Uvnitř zpětných volání   nebo  je   pro zadanou databázi vypnuto, což znamená, že  se potvrdí, jakmile v těchto zpětných voláních nebudou žádné sekce.
 * 3) Použití implicitních otočných transakčních cyklů, pokud je povoleno   (toto je výchozí případ u webových požadavků, ale ne pro režim údržby nebo testy jednotek). První zápis při každém připojení databáze bez transakce spustí BEGIN. COMMIT nastane na konci požadavku pro všechna připojení databází s čekajícími zápisy. Pokud je zapsáno více databází, které mají nastaveno , pak všechny provedou svůj krok potvrzení v rychlém sledu na konci požadavku. To maximalizuje atomičnost transakcí napříč DB. Všimněte si, že optimistická kontrola souběžnosti (REPEATABLE-READ nebo SERIALIZABLE v PostgreSQL) to může poněkud podkopat, protože SERIALIZATION FAILURE může nastat na správné podmnožině odevzdání, i když se zdálo, že všechny zápisy byly úspěšné. V každém případě   snižuje počet potvrzení, což může pomoci výkonu webu (snížením  volání) a znamená, že všechny zápisy v požadavku jsou obvykle buď potvrzeny nebo odvolány společně.
 * 4) Použití explicitních pivotovaných transakcí zaokrouhluje přes  a . Tyto cykly jsou účinné v režimu webu i CLI a mají stejnou sémantiku jako jejich implicitní protějšky, s výjimkou následujících aspektů:
 * 5) * Volání z metody, která nezahájila cyklus, vyvolá chybu.
 * 6) * Po dokončení potvrdí všechny prázdné transakce v hlavních databázích a vymažou všechny REPEATABLE-READ snímky. To zajišťuje, že volající, kteří se spoléhají na v nastavení jednoho DB, budou mít stále čerstvé snímky pro připojení na  . Zajišťuje také, že posluchač transakcí nastavený na  vidí všechny databáze ve stavu nečinnosti transakce, což mu umožňuje spouštět odložené aktualizace.
 * 7) Pokud je v kterémkoli okamžiku vyvolána výjimka a není zachycena ničím jiným,   ji zachytí a vrátí zpět všechna databázová spojení s transakcemi. To je velmi užitečné v kombinaci s.



Chyby zneužití transakce
Různá zneužití transakcí způsobí výjimky nebo varování, například:


 * Vnoření volání vyvolá výjimku
 * Volání na transakci jiné metody, která začíná, vyvolá výjimku.
 * Volání nebo, když je atomová sekce aktivní, vyvolá výjimku.
 * Použití a  má analogická omezení jako výše.
 * Volání, když není otevřena žádná transakce, vyvolá varování.
 * a očekávají   jako argument a jejich hodnota se musí shodovat na každé úrovni vnoření atomových sekcí. Pokud se neshoduje, je vyvolána výjimka.
 * Volání nebo, když je nastaveno  , může zaznamenat varování a neoperaci.
 * Volání, když je  nastaveno, vyvolá chybu a spustí rollback všech DB.
 * Volání, zatímco v transakci stále čekají zápisy, bude mít za následek výjimku.
 * Zachycení výjimek,   nebo   bez volání  může vést k výjimce.
 * Pokus o použití nebo  v , která je nastavena na použití podpory transakcí, kterou třída poskytuje, může způsobit výjimky. Tím propadne vnější rozsah, takže více takových aktualizací může být součástí jednoho cyklu transakce.



Vhodné kontexty pro zápisové dotazy
Kromě staršího kódu by se transakce zápisu do databáze (včetně dotazů v režimu automatického potvrzení) v MediaWiki měly dít pouze během provádění:
 * HTTP POST požadavky na SpecialPages, kde ve třídě PHP vrátí hodnotu true
 * HTTP POST požadavky na stránky Action, kde ve třídě PHP vrátí hodnotu true
 * Požadavky HTTP POST na moduly API, kde ve třídě PHP vrací hodnotu true
 * Úlohy v JobRunner (který používá interní požadavky HTTP POST na webu)
 * Skripty údržby se spouštějí z příkazového řádku

Pro zápisy v kontextu požadavků HTTP GET použijte frontu úloh.

Pro zápisy, ke kterým nemusí dojít před odesláním HTTP odpovědi klientovi, mohou být odloženy přes s příslušnou podtřídou   (obvykle   nebo  ) nebo přes  se zpětným voláním. Fronta úloh by se měla pro takové aktualizace používat, když jsou pomalé nebo příliš náročné na prostředky, aby se spouštěly v běžných vláknech požadavků na nevyhrazených serverech.



Určení atomové skupiny zápisů
Když je sada dotazů úzce spojena při určování jednotky zápisů databáze, měli bychom použít atomickou sekci. Například:

Dalším způsobem, jak to udělat, je použít, což je užitečné, pokud existuje mnoho příkazů return.



Rozdělení zápisů do více transakcí


Situace
Předpokládejme, že máte nějaký kód, který aplikuje některé aktualizace databáze. Po dokončení metody můžete chtít:


 * a) Aplikovat některé vysoce sporné aktualizace databáze na konci transakce, aby nedržely uzamčení příliš dlouho
 * b) Aplikovat další aktualizace databáze, které jsou pomalé, neaktuální a nepotřebují 100% atomicitu (např. mohou být obnoveny)

Metody
V některých případech může kód chtít vědět, že data jsou potvrzena, než budete pokračovat k dalším krokům. Jedním ze způsobů, jak toho dosáhnout, je umístit další kroky zpětného volání na,  nebo. Poslední dva jsou, které se poněkud liší v režimu údržby a požadavků na web/úlohu:


 * Ve webových požadavcích a úlohách (včetně úloh v režimu CLI) se odložené aktualizace spouštějí po potvrzení hlavního kola transakce. Každá aktualizace je zabalena do vlastního transakčního cyklu, i když  zakáže   na zadaném popisovači databáze a odevzdá každý dotaz za běhu. Pokud odložené aktualizace zařadí do fronty další odložené aktualizace, jednoduše se přidají další kola transakcí.
 * Ve skriptech údržby se odložené aktualizace spouštějí po potvrzení jakékoli transakce v lokální (např. "aktuální wiki") databáze (nebo okamžitě, pokud neexistuje žádná otevřená transakce). Odložené aktualizace nelze jednoduše automaticky odložit, dokud nebudou aktivní žádné transakce, protože to může vést k chybám z nedostatku paměti u dlouho běžících skriptů, kde nějaká (možná "cizí wiki") databáze má vždy aktivní transakci (což by jinak bylo ideální). To je důvod, proč jsou odložené aktualizace podivně vázány pouze na místní master databáze. Bez ohledu na to, protože má vnější rozsah transakce a   je pro ně vypnuto, obvykle nedává smysl přímo volat  z metody, protože kód by se mohl spustit okamžitě.

Jakákoli metoda s vnějším rozsahem transakcí má možnost zavolat na   singleton k vyprázdnění všech aktivních transakcí ve všech databázích. To zajišťuje, že všechny čekající aktualizace budou potvrzeny před provedením dalších řádků kódu. Také, pokud taková metoda chce zahájit cyklus transakce, může použít na singleton, provést aktualizace a pak zavolat  na singleton. Toto nastaví  na aktuální a nové DB handles během cyklu, což způsobí spuštění implicitních transakcí, dokonce i v CLI režimu. Značka  se po skončení cyklu vrátí do původního stavu. Všimněte si, že podobně jako a  nelze vnořit cykly transakcí.

Všimněte si, že některé databáze, jako ty, které zpracovávají, mají obvykle   vypnuté. To znamená, že zůstávají v režimu automatického potvrzení i během transakčních kol.

Příklad
Pro výše uvedené případy uvádíme několik technik, jak je zvládnout:

Případ A:

Případ B:



Situace
Zápis dotazů (např. operace vytvoření, aktualizace, odstranění), které ovlivňují mnoho řádků nebo mají špatné využití indexu, trvá dlouho, než se dokončí. Horší je, že replikované databáze často používají sériovou replikaci, takže aplikují hlavní transakce jednu po druhé. To znamená, že 10sekundový dotaz UPDATE zablokuje tak dlouho u každé replikované databáze (někdy i více, protože replikové databáze musí zpracovávat provoz čtení a replikovat hlavní zápisy). To vytváří zpoždění, kdy se jiné aktualizace na hlavním serveru chvíli nezobrazují ostatním uživatelům. Také to zpomaluje uživatele provádějící úpravy kvůli, který se snaží čekat, až repliky doběhnou.

Hlavní případy, kdy k tomu může dojít, jsou:


 * a) Pracovní třídy, které provádějí nákladné aktualizace
 * b) Skripty údržby, které provádějí hromadné aktualizace velkých částí tabulek

Další situace, která někdy nastane, je, když aktualizace spustí úlohu a tato úloha musí provést nějaké složité dotazy, aby něco přepočítala. Možná není dobrý nápad provádět dotazy na hlavní databázi, ale replikované databáze mohou být zpožděné a neodrážejí změnu, která spustila úlohu. To vede k následujícímu případu:


 * c) Úlohy, které potřebují čekat, až se dokončí jedna replika DB, aby ji mohli dotazovat a určit aktualizace

Podobný scénář může nastat u externích služeb. Předpokládejme, že služba potřebuje provádět nákladné API dotazy, aby odrážela změny v databázi wiki. Takže zbývá další případ:


 * d) Volající, kteří provádějí aktualizace předtím, než upozorní externí službu, že se potřebuje zeptat na DB, aby se aktualizovala

Metody
Drahé aktualizace, které vytvářejí zpoždění, je třeba přesunout do třídy  a metoda  úlohy by měla dávkovat aktualizace a čekat, až se repliky dokončí mezi každou dávkou.

Příklady
Případ A / B:

Případ C:

Případ D:



Situace
Někdy změny primární datové sady vyžadují aktualizace sekundárních datových úložišť (kterým chybí BEGIN...COMMIT), například:


 * a) Zařadí úlohu, která se bude dotazovat na některé z dotčených řádků, takže koncový uživatel bude čekat na její vložení
 * b) Zařadí do fronty úlohu, která se bude dotazovat na některé z dotčených řádků a vloží ji po vyprázdnění odpovědi MediaWiki koncovému uživateli
 * c) Odešle požadavek službě, která se dotáže na některé z ovlivněných řádků, takže koncový uživatel bude čekat na požadavek služby
 * d) Odešle požadavek službě, která se dotáže na některé z ovlivněných řádků, a to po vyprázdnění odpovědi MediaWiki koncovému uživateli
 * e) Vymaže mezipaměť proxy serveru CDN pro adresy URL, jejichž obsah je založen na ovlivněných řádcích
 * f) Vymaže položku  pro změněný řádek
 * g) Uložení neodvoditelného textu/polostrukturovaného objektu blob do jiného úložiště
 * h) Uložení neodvoditelného souboru do jiného úložiště
 * i) Obslužný program háčku vytvoření účtu vytvářející položku LDAP, která musí nového uživatele doprovázet
 * j) Aktualizace databáze a odeslání e-mailu do schránky uživatele v rámci požadavku uživatele

Metody
Obecně platí, že odvoditelné (např. mohou být regenerovány) aktualizace externích uložení budou používat nějakou třídu  nebo, které se použijí po potvrzení. V případech, kdy jsou externí data neměnná, lze na ně odkazovat pomocí autoinkrementačního ID, UUID nebo hash externě uloženého obsahu. V takových případech je nejlepší uložit data předem. Aktualizace, které nespadají do žádné kategorie, by měly používat, dávkovat všechny aktualizace do externího úložiště do jedné transakce, pokud je to možné, a vyvolat chybu, pokud se aktualizace nezdaří (což spustí vrácení RDBMS). Tím se omezí okno, ve kterém se věci mohou pokazit a vést k nekonzistentním datům.

Příklady
Případ A:

Případ B:

Případ C:

Případ D:

Případ E:

Případ F:

Případ G:

Případ H:

Případ I:

Případ J:



Použití vrácení transakce
Použití by se mělo důrazně vyhnout, protože ovlivňuje to, co dělal veškerý předchozí spuštěný kód před vrácením zpět. Vnější volající mohou stále považovat operaci za úspěšnou a pokusit se o další aktualizace a/nebo zobrazit falešnou stránku úspěchu. Jakýkoli snímek REPEATABLE-READ je obnoven, což způsobuje, že objekty s líným načítáním pravděpodobně nenajdou to, co hledaly, nebo získají neočekávaně novější data než zbytek toho, co bylo načteno. Použití je obzvláště špatné, protože jiné databáze mohou mít související změny a je snadné je zapomenout vrátit zpět.

Místo toho stačí vyvolání výjimky ke spuštění vrácení všech databází kvůli. Toto pravidlo má dva zvláštní případy:


 * Výjimky typu  (používané pro chyby GUI přívětivé pro člověka) nespouštějí rollback u webových požadavků uvnitř, pokud jsou vyvolány před . To umožňuje akcím, speciálním stránkám a odloženým aktualizacím   zobrazovat správné chybové zprávy, zatímco nástroje pro audit a ochranu proti zneužití mohou stále zaznamenávat aktualizace do databáze.
 * Volající mohou zachytit výjimky na úrovni, aniž by je znovu vyvolali nebo vyvolali vlastní verzi chyby. To je extrémně špatný postup a může způsobit nejrůznější problémy od částečných odevzdání až po pouhé chrlení chyb  . Chyby DB zachyťte pouze proto, abyste provedli nějaké vyčištění před opětovným vyvoláním chyby nebo v případě, že je daná databáze používána výhradně kódem zachycujícím chyby.

Takto se normálně používá vrácení zpět, jako zabezpečení proti selhání, které vše přeruší, vrátí se do výchozího stavu a dojde k chybám. Pokud je však skutečně potřeba přímé volání, vždy použijte  na   singleton, abyste se ujistili, že se všechny databáze vrátí do původního stavu jakéhokoli transakčního cyklu.



Protokolování ladění
K protokolování chyb a varování souvisejících s DB se používá několik kanálů (skupin protokolů):


 * DBQuery
 * DBConnection
 * DBPerformance
 * DBReplication
 * exception

Na Wikimedia lze tyto protokoly nalézt dotazem na logstash.wikimedia.org pomocí +channel:.



Staré diskuse
Toto je výsledek nějaké konverzace na wikitech-l mailing listu a následné diskuze na Bugzille. Některé relevantní diskuse jsou:


 * Transakce vnořené databáze
 * Můžeme odmítnout DBO_TRX? Vypadá to špatně!
 * Upozornění na transakci: WikiPage::doDeleteArticleReal
 * Upozornění na transakci: WikiPage::doEdit (User::loadFromDatabase) (TranslateMetadata::get)

V jednom mailu Tim Starling vysvětlil důvody systému DBO_TRX. Zde je upravená verze jeho vysvětlení:

DBO_TRX poskytuje následující výhody:


 * It provides improved consistency of write operations for code which is not transaction-aware, for example rollback-on-error.


 * It provides a snapshot for consistent reads, which improves application correctness when concurrent writes are occurring.

DBO_TRX was introduced when we switched over to InnoDB, along with the introduction of Database::begin and Database::commit.

[...]

Initially, I set up a scheme where transactions were "nested", in the sense that begin incremented the transaction level and commit decremented it. When it was decremented to zero, an actual COMMIT was issued. So you would have a call sequence like:


 * begin -- sends BEGIN
 * begin -- does nothing
 * commit -- does nothing
 * commit -- sends COMMIT

This scheme soon proved to be inappropriate, since it turned out that the most important thing for performance and correctness is for an application to be able to commit the current transaction after some particular query has completed. Database::immediateCommit was introduced to support this use case -- its function was to immediately reduce the transaction level to zero and commit the underlying transaction.

When it became obvious that that every Database::commit call should really be Database::immediateCommit, I changed the semantics, effectively renaming Database::immediateCommit to Database::commit. I removed the idea of nested transactions in favour of a model of cooperative transaction length management:


 * Database::begin became effectively a no-op for web requests and was sometimes omitted for brevity.
 * Database::commit should be called after completion of a sequence of write operations where atomicity is desired, or at the earliest opportunity when contended locks are held.

[...]

When transactions too long, you hit performance problems due to lock contention. When transactions are too short, you hit consistency problems when requests fail. The scheme I introduced favours performance over consistency. It resolves conflicts between callers and callees by using the shortest transaction time. I think was an appropriate choice for Wikipedia, both then and now, and I think it is probably appropriate for many other medium to high traffic wikis.

Savepoints were not available at the time the scheme was introduced. But they are a refinement of the abandoned transaction nesting scheme, not a refinement of the current scheme which is optimised for reducing lock contention.

In terms of performance, perhaps it would be feasible to use short transactions with an explicit begin with savepoints for nesting. But then you would lose the consistency benefits of DBO_TRX that I mentioned at the start of this post.

-- Tim Starling