* MySQL и memcached
Публикувано на 09 февруари 2013 в раздел Бази от Данни.
Memcached е приложение, което се използва основно за кеширане на данни и работи принципа ключ-стойност (хеш таблица). Обикновено записва данните само в рам паметта. Правейки аналогия с релационните бази от данни, може да си го представите като една таблица, в която има две колони - в първата записвате уникални идентификатори на данни, а във втората записвате самите данни. Освен това тази таблица може да е разделена между множество физически сървъри (sharding). А работата с този софтуер реално е много лесна. Имате няколко команди:
- add: записва информация ако запис за подадения ключ не съществува;
- replace: ако на дадения ключ съществува стойност, я презаписва с подадената
- set: записва информация на даден ключ (add), а ако такъв вече съществува, го презаписва (replace).
- append: добавя подадената информация в края на съществуваща по даден ключ;
- prepend: добавя подадената информация в началото на съществуваща по даден ключ;
- cas: запаметява информацията ако няма друга връзка, която да я е променила след последния достъп до нея;
- get: взима информация по подаден ключ;
- gets: взима информация по подаден ключ, като освен това добавя уникален идентификатор, който се използва от cas;
- incr и decr: увеличава или намалява с единица стойността по даден ключ, когато тя е числова естествено;
- flush: изтрива целия кеш;
- delete: изтрива даден ключ и съответната му стойност от кеша.
За практическа употреба, най-вероятно ще използвате само "set" и "get" командите, за които ще бъде даден пример по-надолу. Стандартно memcached използва принципа на LRU (least recently used) - ако паметта свърши, най-малко използваните данни се премахват, за да бъде освободена памет за по-новите. Използването на такъв кеш обикновено се свежда до следния алгоритъм (приложен от приложението, което използва база от данни):
- Когато се наложи да бъде прочетено нещо от базата от данни, приложението първо се свързва с memcached и търси получените данни в кеша;
- Ако данните ги има в memcached, те се използват директно и въобще не се генерира SQL заявка към базата от данни;
- Ако данните ги няма в memcached, се прави SQL заявка към базата от данни, резултатът от която се използва както от приложението, така и се записва в memcached.
Ето един прост пример с PHP. Представете си, че искаме да изпишем на сайта кой е последния посетител (неговия IP и потребителско име). Да, но сайта е прекалено натоварен - непрекъснато идват нови потребители. Затова ще предпочетем да кешираме информацията за определено време - например в рамките на една минута ще показваме един и същи потребител, взет от кеша:
<?php $memc = new Memcache(); // добавяме memcached сървъра // сървърът може да е повече от един: // просто изпълнявате командата повече пъти $memc->addServer('localhost','11211'); // проверяваме дали има запис в кеша $memcached_result = $memc>get('Latest user'); if ($memcached_result) { // отпечатваме резултата на екрана echo $memcached_result['ip'].", "; echo $memcached_result['user']; } else { // ако няма нищо в кеша // свързваме се с MySQL сървъра $mysqli = new mysqli("cphpvb.net", "usr", "pass", "db"); if (mysqli_connect_errno()) { echo "Проблем с базата от данни"; exit(); } // заявката, която ще се изпълнява // в примера извеждаме последен потребител (*) $query= "SELECT ip, user FROM logs ORDER BY last_login_date DESC LIMIT 1"; // изпълняваме я $result = $mysqli->query($query); // взимаме резултата // понеже имаме LIMIT 1 ще има само един ред // и няма да се налага да обхождаме с цикъл $row = $result->fetch_assoc(); // отпечатваме резултата на екрана echo $row['ip'].", "; echo $row['user']; // записваме резултата в кеша // MEMCACHE_COMPRESSED компресира данните със zlib // 60 означава, че кеша ще е валиден до 60 секунди $memc>set('Latest user', $row, MEMCACHE_COMPRESSED, 60); // затваряме връзката с MySQL $result->free(); $mysqli->close(); } ?>
* Много по-разумно ще е да си складирате специален запис в базата от данни с последния посетител, вместо при всяка заявка да сортирате целия log файл!
Какви са последствията от горното решение?
Бързина!
Прочитането на информация от хеш таблицата на memcache е много по-бързо отколкото от базата от данни MySQL. Причините са основно три:
- Чете се винаги от рам паметта, а не от хард диска. При базата от данни понякога се налага да се чете от хард диска;
- Не се прави "парсване" на SQL код, съставяне на query plan и т.н. Заявките към memcache се изпълняват много по-бързо;
- Няма процес на автентикиране с име и парола при всяка връзка.
Загуба на консистентност
Ако от примера имаме стотици посещения в дадена минута, ние ще извършваме само една единствена SQL заявка при само за едно от тях. Останалите посещения ще изваждат информацията от кеша. Да, но информацията от кеша в рамките на една минута ще бъде една и съща. Тоест ние няма да изкарваме истинското "последно ip и потребителско име" на посетител. С други думи може да се каже, че кеша пази копие на информацията, което не е 100% синхронизирано с истинската база от данни. Това е т.нар. "евентуална консистентност" - данните са с точност до определен времеви интервал (в примера 60 секунди).
Този принцип се използва често при много натоварени приложения. Например във Фейсбук в повечето случаи вие изкарвате най-новите съобщения адресирани до вас от кеша (фейсбук използва memcached в комбинация с MySQL). Ваши приятели може да напишат нещо ново, но то няма да се появи моментално на вашата страница, дори да правите опресняване чрез браузъра (refresh). Ще е нужно първо вашия кеш да "изтече" (expire) и чак тогава вие ще достъпите информацията от основната база от данни. Така, че загубата на консистентност не е нещо лошо, даже напротив. Когато приложението стане прекалено натоварено и основния сървър започне да не успява да се справи, това може да е много евтин начин да отложите обновяването на скъпа компютърна техника.
Memcached и Query Cache - прилики и разлики
Идеята на Memcached и Query Cache е много сходна - вие вадите информация от рам паметта, вместо да я четете от хард диска. При това и при двете не се прави "парсване" на SQL код, както и при двете, ако няма наличен резултат в кеша, е била направена една излишна допълнителна проверка. Разликата е свързана именно с консистентността. При Query Cache консистентността е гарантирана: "При промяна на таблица (изпълнение на insert, update или delete заявка върху нея) се премахват от кеша всички заявки, които зависят от нея". При memcached както казахме вече - не е.
Тогава за кои цели да използваме Query Cache и за кои Memcached?
Очевидно, че ако таблицата ще бъде обновявана изключително често (както в примера с потребителски статистики), то Query Cache непрекъснато ще става невалиден и дефакто полза от него не само, че няма да я има, а ще има директна вреда (непрекъснати безсмислени проверки за наличие на информация в кеша). В случая трябва да се използва "евентуално консистентен кеш". Обратно, ако консистентността е много важна за вас, но въпреки това искате да се възползвате от бързо кеширане - използвайте Query Cache (представете си финансова система от тип "аукцион", където потребителите непрекъснато "рефрешват", за да търсят нови залагания за дадена стока - в нея вие ще искате да давате реалната информация на потребителите, но от друга страна тя често ще бъде една и съща и ще се променя само при получаване на нов "bid").
Какво ще се случи, ако сървъра с кеша "изчезне"?
Практически нищо, освен че кеша няма да може да бъде използван. Който и от memcached сървърите да изчезне, включително всичките, това няма да спре приложението (разбира се ако е написано правилно). В такива случаи се очаква, че натоварването на основния сървър ще стане по-голямо, т.е. системата ще се забави. Същото важи за добавяне на нови сървъри - добавя се ред "$memc->addServer(<IP>,'11211');" в кода на приложението и сървъра се включва в системата незабавно след запазването на файла, без това да прави "downtime" (период, в който приложението не работи).
Понякога, когато кешовете в Memcached са огромни (по няколко гибибайта) и се използват множество сървъри, при включване на нов memcached сървър се прави т.нар. "pre-warm", т.е. запълване на кеша с предвартелни актуални данни, за да се избегне огромно количество на "cache misses" (заявки с несъществуващи ключове).
Задължително ли е кеша да е винаги в оперативната памет (volatile)?
Не, спокойно може да конфигурирате memcached да записва не в рам паметта, а на постоянен носител. Ползата е, че при рестартиране или проблем на сървъра с memcached, информацията няма да се губи. От гледна точка на употребата на memcached като "кеш за заявки", ползата е малка (няма нужда от "pre-warm", но и обикновено рестартирания и прекофигурации на такива сървъри не се очаква да се прави често), загубата голяма - четенето от диска е много по-бавно. Възможната употреба е когато ви е нужен наистина много голям кеш, за който става непрактично да се инвестира в сървър с огромно количество рам памет - в такива случаи може да се изгради кеш например с SSD дискове в RAID 0. Отново няма да е толкова бърз, колкото кеша в оперативната памет, но въпреки това ще бъде много по-бърз от изпълнение на SQL заявки към СУБД.
Друга възможност за употреба на подобно решение (с хард дискове) може да е самия memcached да се използва като основна "база от данни" във вид на разпределена между няколко сървъра хеш таблица. В този случай не бихте искали да губите информация.
Възможно ли е кеша да се обновява директно от MySQL?
Да, вместо да се обновява чрез приложението ($memc>set в примера), може да стане чрез тригери в самата базата от данни. За целта обаче трябва да може самия MySQL да може комуникира с Memcached. Използва се т.нар. "MySQL UDF API" и приложението с отворен код libmemcached. Трябва да свалите memcached_functions_mysql и да го инсталирате ("make install" в линукс). След това трябва да изпълните всички команди от файла "sql/install_functions.sql" (с команда SOURCE в MySQL). От тук нататък:
- За да се свържете с Memcached сървър през MySQL, изпълнявате команда: SELECT memc_servers_set('127.0.0.1:11211'); - ако искате да добавите повече сървъри, разделете ги със запетайка вътре в кавичките;
- За да подадете команда set изпълнете: SELECT memc_set('<key>', '<value>');
- За да подадете команда get изпълнете: SELECT memc_get('<key>');
- Останалите команди към memcached се използват аналогично.
Ясно е, че много лесно за дадения по-горе пример може да направите тригер за команда INSERT, в който самия MySQL да се свързва и да обновява кеша при идване на нов посетител.
MySQL 5.6 и Memcached API
С въвеждането на версия 5.6 (която съвсем скоро излезе официално) се въвежда и т.нар. "Memcached API" за MySQL. Практически погледнато това е интегриране на memcached приложението като част от MySQL СУБД. По този начин от Oracle целят да интегрират двата често употребявани в комбинация продукта в един общ. При това те дават допълнителни възможности. Memcached може да работи в един от три възможни режима:
- Традиционен - като стандартната хеш таблица в паметта за реализиране на "кеш с евентуална консистентност". В този режим memcached няма отношение към данните в самата база от данни, а работи като съвсем отделно приложение от нея;
- Автоматичен - прави това, което направихме в приложението по-горе. При get извлича се информацията кеша, а ако в него няма запис, информацията се извлича директно от таблицата в базата от данни, като същевременно с това се запълва и кеша. При изпълнение на команда set от външен източник, memcached ще записва както в кеша си, така и директно в таблицата на диска (извършва uncommitted transaction);
- NoSQL интерфейс - директен достъп до InnoDB таблица (без кеш), в която да се чете и пише без да се "парсва" SQL код.
Ако включите Memcached API в MySQL 5.6, практически вашият MySQL ще отвори още един порт за "слушане" (стандартно 11211). Когато (например през PHP) подавате заявки към този memcached сървър, той ще изпълнява действията в зависимост от режима, в който работи. Ако работи във втория или третия режим, вие може да си спестите много писане на код в приложението, защото реално ще се нуждаете само и единствено от подаване на заявки към memcached и въобще няма да се свързвате, за да подавате SQL код към базата от данни. И в двата случая подобрението в скоростта ще е гарантирано. Разбира се, че вие няма да може да изпълнявате сложни SQL заявки - за изпълнението на такива ще трябва да се свързвате по традиционния начин.
Memcached API например предоставя един чудесен и работещ вариант, в който вече можете да използвате MySQL за запазване на потребителски сесии на уеб сайт. Ето един пример за това. Без това API това би било непрактично, понеже комуникацията с базата от данни би била доста по-бавна.
Кеша е много полезен подход, но на мен винаги ми е било трудно инвалидирането му.
Инвалидирането по време е вид `търговия` с консистентността - по-дългия период за инвалидиране увеличава ползата от кеша, но намалява консистентността.
За бизнес приложенията консистентността е от съществено значение. При тях `печалбата` от пре-използването на изчислени вече данни идва не толкова от големия брой потребители на които ще бъдат сервирани наготово, а от това, че за изчисляването им се използват значителни ресурси. С други думи, бизнес приложенията се ползват от по-малко потребители, но релациите които използват са по-сложни.
По-добрия начин спрямо инвалидирането по време, при бизнес приложенията е инвалидиране при промяна на данните, които се кешират. Тук обаче става много сложно, защото логиката на получаване на данните, които се кешират може да зависи от много модели и от конкретни записи при тях.
Нека си представим, че имаме няколко отчета в една бизнес система: R1, R2, R3, .....Rn
Поради това, че те се генерират бавно, а са относително постоянни (не се променят много динамично) решаваме да ги кешираме. Някой от тези отчети зависят от някакъв модел данни (таблица в MySQL), например `sales_Invoices`. Това, което ми е интересно е има ли общоприет начин, при промяната на `sales_Invoices` да инвалидираме всички отчети, които зависят от от този модел. Разбира се можем да поддържаме външен списък с отчетите, зависещи от `sales_Invoices`, но това изисква допълнителен модел данни и усилия. Имам предвид има ли лесно решение, например всички ключове на кеширани данни, зависещи от `sales_Invoices` да имат префикс името на модела, а след това с една заявка към Memcached да можем да изтрием всички данни, в чийто ключове има този префикс?
На въпроса ти "има ли общоприет начин, при промяната на `sales_Invoices` да инвалидираме всички отчети, които зависят от от този модел" има еднозначен отговор - ДА, ИМА! Технологично това прави именно Query Cache! При него има гарантирана консистентност.
П.П. С новия "NoSQL режим" на MySQL 5.6 с Memcached API се получава почти същото. Реално често ще се използва кеш в паметта (InnoDB буферите), а информацията в него ще е винаги консистентна.
П.П.2. Изобщо тук няма спор "неконсистентeн срещу консистентен кеш". Нормално трябва да използваме и двата вида! В почти всяко приложение си има "важни операции", които е задължително да са консистентни (и тях оптимизираме с консистентен кеш, който както отбеляза ти е добре да се "инвалидира" автоматично, защото иначе става голяма беля), но има и операции, които търпят неконситентност (за които използваме технологии като Memcached).
`Query Cache` не кешира ли само резултата от изпълнението на SQL заявки?
Ако е така, не ми върши работа, защото аз кеширам резултати, получени от заявки към MySQL, но след значителна обработка в PHP. Всъщност времето за обработка в PHP е много по-голямо от колкото времето необходимо за MySQL.
В момента за кеширането на подобни данни ползваме обикновена MyISAM таблица, в която ключа е двукомпонентен (две полета) - префикс и име. Един рипорт се генерира примерно за 10 секунди, а като се кешира и се извади от кеша това отнема няколкостотин пъти по-малко време.
За този случай, дори и да използваме Memcached, няма да увеличим производителността много. Но в същото време имаме за кеширане и множество малки, плитко извлечени данни, затова ако преминем на Memcached ще има положителен ефект като цяло.
Да, само за резултат от заявки са. Но зависи какво разбираш под "обработка в PHP". Ако например теглиш някакво количество информация, а после му извършваш сортирания, групирания, премахваш излишества и т.н., веднага ще запитам защо не правиш обработката с по-сложна заявка към MySQL?
Принципът "винаги извършвай обработката на информацията възможно най-близко до първоизточника й, за да не трансферираш излишни количества данни" винаги води до положителни резултати :)
Това, което наистина вреди на query cache е, че не може да се използва за съхранени процедури. Ако можеше, той щеше да ти върши перфектна работа.
П.П. Не забравяй за ENGINE=Memory - временните таблици в паметта са чудесни за подобни данни. Можеш да си създадеш сложни тригери, които ги обновяват при обновяване на информацията в основните таблици, а приложението да чете само готовите данни от MEMORY таблицата, имитирайки кеш.
Относно: `защо не правиш обработката с по-сложна заявка към MySQL?`
По две причини.
Първата причина е, че аз (и хората с които работя) познаваме значително по-добре PHP от колкото SQL. Аз съм сигурен, че SQL е много мощен език, на който могат да бъдат написани цели бизнес приложения. За мен най-важното нещо на едно приложение е не колко е функционално и производително, колко е "приспособимо", т.е. колко лесно може да се променя и настройва според околната среда (бизнес практики, закони, ИТ култура на потребителите ...). За това за мен е важно приложението да е написано на лесен за разбиране език, използван от много хора. Колкото пъти досега (с помощта на приятели) сме писали сложни SQL заявки, след няколко години сме имали проблем, че не можем да разберем как действат и да ги променим. За PHP кода проблема е значително по-малък.
Втората причина е, че трябва да се съобразяваме с `Cloud computing`. Убеден съм, че след време стартирането на приложение в облак, ще стане толкова обичайно, колкото е сега светването на ел. лампа с енергия от националната електрическа мрежа. В това отношение релационните бази данни имат съществени проблеми. MySQL инстанцията например или трябва да има цялата база данни (независимо дали е master или sleeve), което прави невъзможно разпределянето на голяма база върху 2 или повече машини. Има възможност за разпределяне (клъстер) но с големи ограничения от към избора на енджин за съхраняване на данните. Ние използваме голяма част от MySQL данните по начин характерен за NoSQL базите. Стремим се, частта, която използваме по напълно релационен начин да бъде минимална.
Както се казва в такива случаи - ваша работа :)
MilenG, като цяло съм съгласен с написаното от теб. Лично аз до толкова съм свикнал с употребата и настройката на Query Cache, че напълно елиминирам нуждата от кеширането на каквото и да е в memcached - излишно е правенето на едно нещо с две приложения, при положение, че може да се постигне същия резултат (а и по-добър) само с едно.
Не съм съгласен само с пред последното ти изречение относно скалируемостта на MySQL. Предлагам ти да разглеждаш MySQL Cluster, при който данните се шардват на ниво база данни - това, което до сега се налагаше да правим на ниво application. Принципа е много подобен на работата на MongoDB.