MySQL v roli neschémové databáze

Neschémové databáze (pro které se vžilo označení „NoSQL“) jsou stále populárnější. Vývojáři začínají často narážet na omezení relačních databází, jejichž návrh je sice časem ověřený, ale přece jen poněkud staršího data. Pojďme se podívat na zajímavý příklad netradičního využití MySQL ve službě FriendFeed.

Seriál: Nerelační databáze (11 dílů)

  1. CouchDB – tak trochu jiná databáze (1. část) 24.8.2009
  2. CouchDB – tak trochu jiná databáze (2. část) 31.8.2009
  3. CouchDB – tak trochu jiná databáze (3. část) 7.9.2009
  4. MySQL v roli neschémové databáze 6.1.2010
  5. Základy Amazon SimpleDB 30.3.2010
  6. Návrh databáze – NoSQL vs SQL 31.3.2010
  7. Amazon SimpleDB prakticky v PHP 15.4.2010
  8. Vyskúšajme si Tokyo Cabinet 4.5.2010
  9. Redis: key-value databáze v paměti i na disku 7.10.2010
  10. Přechod z MySQL na CouchDB, část první 17.2.2011
  11. Přechod z MySQL na CouchDB: Druhý díl 24.2.2011

Článek je volným překladem článku Breta Taylora, vydaného pod CC-BY licencí 27.2.2009 na autorově blogu.

Na úvod

Ve FriendFeedu používáme pro ukládání veškerých dat MySQL. Naše databáze rostla zároveň s počtem uživatelů. V současnosti je v databázi přes 250 milionů záznamů („současnost“ je únor 2009, pozn. překl.) a spousta dalších dat, od komentářů a příznaků „líbí se mi“ až po seznamy přátel.

Během růstu databáze jsme se museli vypořádat s problémy škálovatelnosti, které se objevily spolu s růstem. Udělali jsme to, co se dělává: Použili jsme čtení ze slave databází a memcached pro zvýšení výkonu při čtení, a rozdělili jsme databázi (v originále „database sharding“ – technika, při níž jsou záznamy umisťovány do různých fyzických úložišť podle nějakého předem stanoveného kritéria – pozn. překl.) pro zvýšení výkonu při zápisu. Ale škálování existujících funkcí tak, aby stačily zvýšenému přenosu dat, se ukázalo být tím menším problémem v porovnání s přidáváním nových funkcí.

Konkrétněji: změnit strukturu tabulky nebo přidat indexy do databáze s více než 10–20 miliony řádků zaměstná databázi naplno po dobu několika hodin v kuse. Odstranění starých indexů zabere neméně času, a pokud je neodstraníte, sníží to výkon celého stroje, protože databáze bude dál číst a zapisovat do nepoužitých bloků při každém INSERTu, a bude si tak odsouvat z paměti to potřebné. Existují určité postupy operací, které můžete dělat, abyste se těmto problémům vyhnuli (jako nastavit nový index nejprve na slave stroji a pak prohodit slave a master), ale tyto procedury jsou velmi náročné a náchylné k chybám. Což nás v důsledku nutilo vyhýbat se přidávání takových funkcí, které by vyžadovaly změnu struktury nebo indexů. A protože se od doby, co jsme databázi rozdělili, staly „relační“ funkce jako třeba JOIN naprosto nepoužitelné, rozhodli jsme se porozhlédnout mimo svět relačních databází.

Existuje spousta databázových projektů, navržených tak, aby řešily problém ukládání dat s pružnou strukturou a vytváření indexů za běhu (například CouchDB) (psali jsme o ní na Zdrojáku – pozn. překl.). Bohužel žádný z nich není dostatečně používaný na velkých serverech, abychom se mohli někde inspirovat. V testech, které jsme četli a které jsme si udělali, se žádný z těchto projektů neukázal dostatečně stabilním či otestovaným do té míry, jakou potřebujeme. MySQL funguje. Nepoškozuje data. Replikace funguje. Známe její omezení. Máme rádi MySQL jako úložiště dat, ne kvůli tomu, že je to relační databáze.

Po jistých úvahách jsme se rozhodli implementovat „neschémové“ úložiště nad MySQL namísto vytvoření kompletního nového úložiště. V tomto článku se pokusím popsat nejvýraznější rysy tohoto systému. Zajímá nás, jak se s podobnými problémy vypořádávají jiné velké servery, a doufáme, že některé z našich úvah mohou být užitečné ostatním vývojářům.

Pokud vás naopak zajímá to, jak využít správně všech výhod, které MySQL nabízí, a pokud se chcete naučit správně používat MySQL jako relační databázi, přijďte na školení Akademie Root.cz s tématem Návrh a používání MySQL databáze, kde se dozvíte vše potřebné od návrhu až po samotné využití MySQL ve vašich projektech. Školení je vhodné jak pro důkladné seznámení se základy databází, tak pro využití MySQL u náročnějších projektů.

Přehled

Naše úložiště ukládá neschémové seznamy atributů (jako JSON objekty nebo Pythonské slovníky). Jediná vyžadovaná součást uloženého záznamu je identifikátor, který tvoří šestnáctibajtový UUID. Zbytek záznamu je z hlediska úložiště nezajímavý. Můžeme tedy snadno změnit „schéma“ prostým uložením nových atributů. (V originále „properties“, překlad „vlastnost“ by nebyl vhodný, proto budu používat „atributy“ – pozn. překl.)

Uložená data indexujeme pomocí indexových záznamů, uložených v oddělených MySQL tabulkách. Pokud chceme indexovat tři atributy v každém záznamu, budeme mít tři MySQL tabulky, pro každý indexovaný atribut jednu. Když budeme chtít přestat užívat index, prostě přestaneme do této tabulky zapisovat. Tabulku pak můžeme smazat. Když potřebujeme nový index, uděláme pro něj novou MySQL tabulku a spustíme asynchronní proces, který index na pozadí naplní, aniž by to mělo vliv na „živou“ službu.

Nakonec jsme skončili s větším počtem tabulek, než jsme měli předtím, ale přidávání a odstraňování indexů je teď snadné. Máme velmi dobře optimalizovaný proces, který vytváří nové indexy (zvaný „Čistič“). Ten dokáže rychle naplnit indexové tabulky, aniž by přitom „zboural“ celý web. Můžeme přidávat nové atributy a indexovat je v řádu dnů, nikoli v řádu týdnů, a nemusíme kvůli tomu prohazovat MySQL master a slave nebo dělat jiné děsivé akce.

Detaily

Naše entity jsou v MySQL uloženy v tabulce, která vypadá nějak takto:

CREATE TABLE entities (
    added_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    id BINARY(16) NOT NULL,
    updated TIMESTAMP NOT NULL,
    body MEDIUMBLOB,
    UNIQUE KEY (id),
    KEY (updated)
) ENGINE=InnoDB;

Sloupec „ added_id“ je tu proto, že InnoDB ukládá řádky fyzicky v pořadí podle primárního klíče. Primární klíč s AUTO_INCREMENT  zajistí, že nově přidané záznamy jsou na disk zapisovány sekvenčně za starší záznamy, což napomáhá zápisu i čtení (nové záznamy bývají čteny mnohem častěji než starší záznamy, čemuž napomáhá i to, že stránky FriendFeed jsou řazeny v obráceném chronologickém pořadí). Těla záznamů jsou ukládána jako pythonské slovníky, serializované (pickled) a komprimované pomocí zlib.

Indexy jsou ukládány v oddělených tabulkách (v originále sharded – pozn. překl.). Když vytváříme nový index, vytvoříme novou tabulku, která obsahuje atributy, jež chceme indexovat. Kupříkladu typický záznam (entita) ve FriendFeedu může vypadat takto: 

{
    "id": "71f0c4d2291844cca2df6f486e96e37c",
    "user_id": "f48b0440ca0c4f66991c4d5f6a078eaf",
    "feed_id": "f48b0440ca0c4f66991c4d5f6a078eaf",
    "title": "We just launched a new backend system for FriendFeed!",
    "link": "http://friendfeed.com/e/71f0c4d2-2918-44cc-a2df-6f486e96e37c",
    "published": 1235697046,
    "updated": 1235697046,
}

Chceme indexovat atribut „ user_id“ v těchto záznamech, abychom mohli zobrazit stránku všech záznamů daného uživatele. Naše indexová tabulka bude tedy vypadat nějak takto:

CREATE TABLE index_user_id (
    user_id BINARY(16) NOT NULL,
    entity_id BINARY(16) NOT NULL UNIQUE,
    PRIMARY KEY (user_id, entity_id)
) ENGINE=InnoDB;

Naše datové úložiště udržuje indexy automaticky, ve své režii. Pokud chceme se záznamy pracovat, můžeme použít nějaký takový kód (v Pythonu):

user_id_index = friendfeed.datastore.Index(
    table="index_user_id", properties=["user_id"], shard_on="user_id")
datastore = friendfeed.datastore.DataStore(
    mysql_shards=["127.0.0.1:3306", "127.0.0.1:3307"],
    indexes=[user_id_index])

new_entity = {
    "id": binascii.a2b_hex("71f0c4d2291844cca2df6f486e96e37c"),
    "user_id": binascii.a2b_hex("f48b0440ca0c4f66991c4d5f6a078eaf"),
    "feed_id": binascii.a2b_hex("f48b0440ca0c4f66991c4d5f6a078eaf"),
    "title": u"We just launched a new backend system for FriendFeed!",
    "link": u"http://friendfeed.com/e/71f0c4d2-2918-44cc-a2df-6f486e96e37c",
    "published": 1235697046,
    "updated": 1235697046,
}

datastore.put(new_entity)

entity = datastore.get(binascii.a2b_hex("71f0c4d2291844cca2df6f486e96e37c"))
entity = user_id_index.get_all(datastore, user_id=binascii.a2b_hex("f48b0440ca0c4f66991c4d5f6a078eaf"))

Třída Index hledá atribut user_id ve všech záznamech a automaticky spravuje index v tabulce index_user_id. Protože je naše databáze dělená, udává argument shard_on klíč, podle kterého bude rozhodnuto o konkrétním umístění (zde podle čísla  user_id).

Můžete položit dotaz na index pomocí instance této třídy (viz user_id_index.get_all výše). Kód úložiště udělá potřebný „join“ mezi tabulkou „ index_user_id“ a tabulkou entit v Pythonu, a to tak, že nejprve požádá o seznam ID záznamů ze všech tabulek index_user_id ze všech dílčích databází, a pak vyzvedne tyto záznamy z tabulky entities.

Pokud chceme přidat další index, například indexovat odkazy (atribut link), vytvoříme opět novou tabulku:

CREATE TABLE index_link (
    link VARCHAR(735) NOT NULL,
    entity_id BINARY(16) NOT NULL UNIQUE,
    PRIMARY KEY (link, entity_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Změníme inicializační kód datového úložiště tak, aby zohlednil nový index:

user_id_index = friendfeed.datastore.Index(
    table="index_user_id", properties=["user_id"], shard_on="user_id")
link_index = friendfeed.datastore.Index(
    table="index_link", properties=["link"], shard_on="link")

datastore = friendfeed.datastore.DataStore(

    mysql_shards=["127.0.0.1:3306", "127.0.0.1:3307"],
    indexes=[user_id_index, link_index])

Nakonec asynchronně naplníme index (tedy za normálního provozu) pomocí:

./rundatastorecleaner.py --index=index_link

Konzistence a atomicita

Konzistence je v situaci, kdy je databáze je rozdělena na více strojů a indexy pro entity mohou být uloženy na různých oddílech, často na jiných než entita samotná, docela problém. Co když zkolabuje proces předtím, než jsou zapsány všechny záznamy do všech indexových tabulek?

Vytvořit transakční protokol se stalo výzvou pro ty nejambicióznější vývojáře FriendFeedu, ale chtěli jsme zároveň udržet systém tak jednoduchý jak jen to je možné. Rozhodli jsme se porušit některá pravidla konzistence, jako například:

  • Indexy nemusí vždy odrážet aktuální stav záznamů
  • Směrodatný je obsah dat v tabulce entities

V důsledku toho zapisujeme nové záznamy do databáze v těchto krocích:

  1. Zapíšeme záznam do tabulky „ entities“ a využíváme přitom vlastností ACID, které nabízí InnoDB
  2. Zapíšeme indexy do všech indexových tabulek ve všech dílech databáze.

Když hledáme informace v indexu, tak víme, že nemusí být přesné (například mohou obsahovat staré hodnoty atributů, pokud při update neproběhl korektně krok 2). Abychom se ujistili, že nevracíme neplatná data, používáme indexy jen jako vodítko k tomu, abychom zjistili, jaké záznamy načíst, ale patřičné filtry aplikujeme znovu na načtené záznamy; nemůžeme totiž důvěřovat indexům jako autoritativnímu zdroji informací. Tedy:

  1. Přečteme entity_id ze všech tabulek podle dotazu
  2. Načteme záznamy z tabulky entities podle získaných ID
  3. Vyfiltrujeme (v Pythonu) všechny entity, u nichž hodnoty atributů neodpovídají podmínkám v dotazu.

Abychom se ujistili, že v indexech něco nebude chybět napořád a že nesrovnalosti budou opraveny, prochází proces „Čistič“, který jsem zmiňoval v předchozím textu, neustále tabulkou entit, zapisuje chybějící indexy a čistí staré a neplatné záznamy. Při práci postupuje od posledních změněných záznamů, takže nesrovnalosti v indexech jsou opraveny v praxi velmi rychle (během pár sekund).

Výkon

Optimalizovali jsme naše indexy a výsledkem jsme příjemně překvapeni. Výkon stoupl a latence je stabilní. Se systémem je navíc velmi snadné pracovat. (originálním článku jsou grafy, ukazující zlepšení stability výkonu, v kontextu tohoto článku na Zdrojáku nebyly nezbytné – pozn. překl.)

O autorovi: Autor textu, Bret Taylor, je spoluzakladatel služby FriendFeed a její bývalý CEO, nyní (po odkoupení FriendFeedu Facebookem) pracuje ve Facebooku jako produktový ředitel.

Poznámka překladatele

Na výše uvedený článek jsem narazil náhodou, když jsem hledal informace o rozumném, snadno použitelném a netradičním ORM systému pro PHP, který by fungoval jako persistentní úložiště objektů. Zvažoval jsem migraci k nějaké „čisté NoSQL“ databázi, ale problém je s tím, že NoSQL databáze nepatří do standardní nabídky webhostingů. Výše uvedený článek mi připadal jako velmi inspirativní, právě pro vývojáře, kteří by rádi zkusili přechod k NoSQL a využili přitom široce dostupné, známé a relativně spolehlivé MySQL databáze. Navíc přináší pohled autora opravdu velkého webu, což je zkušenost pro většinu vývojářů v ČR neznámá. Proto jsem se rozhodl článek přeložit, i když je už téměř rok starý, a nabídnout čtenářům Zdrojáku.

Začal programovat v roce 1984 s programovatelnou kalkulačkou. Pokračoval k BASICu, assembleru Z80, Forthu, Pascalu, Céčku, dalším assemblerům, před časem v PHP a teď by rád neprogramoval a radši se věnoval starým počítačům.

Komentáře: 155

Přehled komentářů

Vrtak-CZ Další zdroje?
Martin Malý Re: Další zdroje?
keff Díky
keff Re: Díky
Jakub Vrána Re: Verzování
keff Re: Verzování
Jakub Vrána Re: Verzování
Ped Re: Verzování
Inkvizitor Re: Verzování
Michal Krause Re: Díky
Jakub Vrána Neblokující ALTER
aprilchild Re: Neblokující ALTER
Jakub Vrána Re: Neblokující ALTER
Martin Malý Re: Neblokující ALTER
Jan Kodera Re: Neblokující ALTER
Lukas Re: Neblokující ALTER
Martin Malý Re: Neblokující ALTER
aprilchild Klasicky zdroj pro NoSQL
Martin Soušek takže v zásadě nic nového
Palo Re: takže v zásadě nic nového
Martin Malý Re: takže v zásadě nic nového
X NoSQL?
PM Re: NoSQL?
Martin Malý Re: NoSQL?
fos4 Re: NoSQL?
Martin Malý Re: NoSQL?
fos4 Re: NoSQL?
Martin Malý Re: NoSQL?
fos4 Re: NoSQL?
Martin Malý Re: NoSQL?
Ivan Hracka
Martin Malý Re: Hracka
MyOwnClone Fajn clanek
Jiří Knesl Bulvární titulek
krteQ Re: Bulvární titulek
backup Re: Bulvární titulek
Jiří Knesl Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
Jiří Knesl Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
Palo Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
Palo Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
Palo Re: Bulvární titulek
Petr Re: Bulvární titulek
František Kučera Co se učí a co dělá.
Martin Malý Re: Co se učí a co dělá.
backup Re: Co se učí a co dělá.
Jiří Kosek Re: Bulvární titulek
Palo Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
Palo Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
Jiří Kosek Re: Bulvární titulek
Jakub Vrána Re: Bulvární titulek
Jiří Kosek Re: Bulvární titulek
František Kučera Adresy ve fakturách
František Kučera Faktury
Karel Minařík Re: Bulvární titulek
okbob Re: Bulvární titulek
okbob Re: Bulvární titulek
Almad Re: Bulvární titulek
František Kučera Pokrok
Palo Re: Pokrok
František Kučera Re: Pokrok
Palo Re: Pokrok
Lukáš Konarovský Re: Bulvární titulek
František Kučera Fakturoid
Palo Re: Bulvární titulek
František Kučera Re: Bulvární titulek
František Kučera Re: Bulvární titulek
HolyHop Re: Bulvární titulek
Palo Re: Bulvární titulek
mike Re: Bulvární titulek
Palo Re: Bulvární titulek
koles Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
krteQ Re: Bulvární titulek
Jan Kodera Re: Bulvární titulek
backup Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
logik Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
logik Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
Palo Re: Bulvární titulek
Mard Re: Bulvární titulek
A Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
Karel Minařík Re: Bulvární titulek
Jiří Knesl Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
okbob Re: Bulvární titulek
Palo Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
Jiří Knesl Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
Palo Re: Bulvární titulek
David Grudl Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
Jaroslav Moravec Re: Bulvární titulek
Palo Re: Bulvární titulek
Martin Soušek Re: Bulvární titulek
David Grudl Re: Bulvární titulek
Jiří Knesl Re: Bulvární titulek
Jens.cz Re: Bulvární titulek
Jiří Knesl Re: Bulvární titulek
Jens.cz Re: Bulvární titulek
Langi Re: Bulvární titulek
Vojta Re: Bulvární titulek
Karel Minařík Re: Bulvární titulek
Jiří Knesl Re: Bulvární titulek
Aleš Roubíček Re: Bulvární titulek
Karel Minařík Re: Bulvární titulek
Aleš Roubíček Re: Bulvární titulek
Karel Minařík Re: Bulvární titulek
Aleš Roubíček Re: Bulvární titulek
Karel Minařík Re: Bulvární titulek
okbob Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
okbob Re: Bulvární titulek
František Kučera Webové aplikace
Martin Malý Re: Webové aplikace
František Kučera Re: Webové aplikace
Martin Malý Re: Bulvární titulek
okbob Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
Palo Re: Bulvární titulek
Martin Malý Re: Bulvární titulek
Palo Re: Bulvární titulek
backup Re: Bulvární titulek
Palo Re: Bulvární titulek
jos Re: Bulvární titulek
krteQ vytvareni/odstranovani indexu
Martin Malý Re: vytvareni/odstranovani indexu
Lukas NoSql ? To už tu jednou bylo.
Palo Re: NoSql ? To už tu jednou bylo.
Lukas Re: NoSql ? To už tu jednou bylo.
Michal Blaha Know-jow/Technologie/Cas/Zdroje - to je oč tu běží
David Grudl My programátoři si rádi život komplikujeme
Palo Re: My programátoři si rádi život komplikujeme
Bob Kladivem na sroubecek
C Re: Kladivem na sroubecek
Almad Re: Kladivem na sroubecek
Martin Malý Re: Kladivem na sroubecek
Almad Re: Kladivem na sroubecek
František Kučera Re: Kladivem na sroubecek
Almad Re: Kladivem na sroubecek
Vojta Pozor na to!
František Kučera Re: Pozor na to!
Sebastian Hezký článek
Martin Kavalec relační model vs. noSQL
Jakub Vrána Článek k tématu
Zdroj: https://www.zdrojak.cz/?p=3150