Використання memcached і Redis в високонавантажених проектах



В'ячеслав Москаленко (Ленвендо)
Я буду розповідати вам про інструменти високонавантажених проектів, кешуванні, зокрема, про memcached, Redis-е і про сервіс черг RabbitMQ або в простолюдді «кролика».

У першій частині доповіді я розповім про те, що таке memcached — базові поняття, що таке Redis, про його особливості, і чим ці два сервісу відрізняються. Розповім про практичне застосування в наших проектах обох сервісів.

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

Про кешуванні. Думаю, багато хто з вас хоч щось кэшировали в своїх веб-проектах, тому все буде дуже просто і доступно.


Що таке кеш? Це посередник між клієнтом, який запитує дані, і основним, як правило, повільним, сховищем. Такий посередник дозволяє отримувати наші дані дуже швидко. Як правило, дані зберігаються в оперативній пам'яті у разі memcached і Redis-a. Ефективне використання кеша дозволяє нам знизити навантаження на наші сервера БД.

Виникає питання — коли і що нам кешувати? Зрозуміло, що корисно кешувати часто запитувані дані, тобто якщо у нас якийсь маленький садок і там 100 користувачів у годину, то велике навантаження на базу не буде, і кешування буде малоефективно. Ось, у великих високонавантажених проектах, коли ми хочемо знизити навантаження на БД, ми використовуємо кеш.

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

Інструменти кешування, про яких піде мова, це memcached і Redis.

Почнемо з першого. Мemcached — сервіс для кешування даних в оперативній пам'яті, що володіє високою продуктивністю. Його історія починається в 2003 р. Brad Fitzpatrick розробив його для Livejournal, де його успішно впровадили і прискорили свій сервіс.

Можливості memcached: він дуже швидкий, незалежно від кількості даних, які ми зберігаємо, у нього простий інтерфейс — можна засетить (set) дані, можна отримати, через час життя видалити ключ і т. д.; в memcached підтримуються атомарні операції — incr/decr, append/prepend; дозволяє легко розширювати кількість серверів, і навіть падіння одного з серверів просто розраховується як невлучення до кешу, тобто просто немає даних в кеші.

Обмеження memcached: довжина ключів максимум 250 байт, обсяг даних, який можна зберігати під одним ключем, обмежується 1 Мб. Втрата ключів в memcached може траплятися по часу життя, по ліміту пам'яті, або при відмову сервера.

Т. к. я є PHP-програмістом, я навів приклад, як PHP використовувати memcached.



Для PHP є 2 розширення — одне використовує бібліотеку libmemcached, а другий — просто розширення php-memcache. Libmemcached має більше можливостей реалізовувати дані, за замовчуванням стерилізують їх за допомогою PHP, але можна визначити стерилізатор JSON, наприклад. Ми створюємо об'єкт memcached, додаємо наш сервер на localhost-е, дефолтний порт у нас 11211. Ми можемо засетить якусь рядок, засетить якийсь масив під ключ «array», виставити час життя, коли дані повинні «протухнуть». Простою операцією get ми отримуємо свої дані. Тобто нічого складного. Попросіть своїх системних адміністраторів, хто ще не використав, поставити memcached, php-розширення, написати свою грамотну «обгортку» для memcached, і використовувати для кешування часто запитуваних даних, які навантажують вашу БД і які можна кешувати.

Далі я розповім про те, що таке Redis, чим він відрізняється від memcached.

У Redis-е є підтримка великої кількості типів даних, серед яких рядка, хеши, списки, множини і сортовані множини. Також Redis вміє періодично скидати свої дані на диск — можна після, наприклад, тисячі оновлень наших даних скидати на диск. Redis підтримує LRU очищення, там можна визначати різні стратегії очищення ключів, можна рандомно, наприклад, очищати, можна давно не використовуються, або видаляти тільки ті ключі, у яких виставлено час життя і т. д. Також можна зробити так, щоб він, взагалі, не очищав свою пам'ять, але тоді при відновленні даних ваш клієнт Redis-а буде видавати помилку. Краще нехай він очищає самі невикористовувані дані.

Redis підтримує master-slave реплікацію, підтримує найпростіші черги, тобто може створювати канали, на них підписуватися, публікувати в них якісь повідомлення, читати. Підтримує транзакції за допомогою команди MULTI/EXEC, LUA-скрипти. Ще у Redis-а відмінна документація — заходимо на сайт redis.io і там все доступно, з прикладами, все розписано.

Далі я хочу розповісти про типи даних, і які команди у нас є в Redis-е.



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

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

Для роботи з хэшами використовуються команди HSET, HMSE. Щоб отримати значення полів хеша, ми використовуємо команду HGETALL і отримуємо всі властивості нашого хеша.

Приватний приклад — в хэшах можна зберігати сесію користувача.



Наступний тип даних — це множини (Sets). Redis дозволяє зберігати за конкретного ключа певна множина елементів, при цьому багато в Redis відповідає за його унікальність. Тобто якщо ми додамо в безліч два однакових елемента, то він завжди там буде один. Для додавання елемента в безліч використовується команда SADD для отримання всіх елементів множини використовується команда SMEMBERS. Ключам ми можемо виставляти за допомогою команди EXPIRE час життя, видаляти ключі і т. д.

Ще один тип даних — це сортовані множини. Вони працюють майже так само, як Sets, але за тим винятком, що ми можемо виставити так звані «окуляри» кожного елемента в нашому множині. Як очок у прикладі на слайді вище — передається TIMESTAMP в коментарях до статті 13, тобто ми називаємо свій ключ так, щоб було зрозуміло, що це коментар до статті 13.

Ми додаємо 2 елемента в безліч і можемо отримати відсортований список за зменшенням або зростанням наших очок.

Є ще такий тип даних в Redis-е списки. Тут можна пушити в наш безліч зліва, справа, брати елементи з конкретного індексу до конкретного індексу.

Які є клієнти для PHP?

Є розширення phpredis, він написаний на С. Також є бібліотеки Predis, написана на PHP, Rediska, RedisServer — клас, який відкриває сокет на конкретний порт і просто спілкується з Redis-му, Resident — форк RedisServer-а, але він ще використовує розширення phpredis для прискорення і для одержання більш високої продуктивності.

Мої рекомендації — використовувати, звичайно, phpredis, тому що він за всіма бенчмарками і тестів швидше, він написаний на С, і всі php-реалізації часто повільніше.

На наступному слайді представлені відмінності між memcached і Redis.



Насправді і Redis, і memcached потрібно використовувати під конкретні завдання, тобто якщо нам потрібно зберігати якісь свої структури даних, якісь безлічі, сортовані безлічі, і нам не можна втратити дані, то, звичайно, тут на допомогу приходить Redis. Потрібно враховувати те, що Redis однопотоковий.

Тепер про прикладах.



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

Ми вирішили таким способом:



Тобто коли прилітає AJAX-запит на наш фронтенд Ngnix, у Ngnix стоїть модуль, який вміє працювати з memcached, тобто ми спочатку запитуємо дані в memcached по ключу, і якщо дані є (а там зберігається JSON у нас для продуктів), то ми відразу ж повертаємо цей JSON. Це працює дуже швидко.

Якщо даних немає, наш запит проксируется на PHP і там у нас дві ситуації — картка товару може лежати в Redis-е, також у вигляді JSON, тоді ми беремо з Redis-а, зберігаємо в memcached і віддаємо її далі клієнту.

Якщо у нас немає ні там, ні там   ні в Redis-е, ні в memcached, ми запитуємо нашу картку товару з MySQL, зберігаємо її в Redis, дублюємо ці дані в memcached і так само повертаємо. При наступних запитах наші картки товарів видаються вже безпосередньо з memcached.

Наступним, більш складним прикладом, де використовуються більшість типів даних Redis-а, — це параметричний пошук:



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

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



Наприклад, у категорії «Телефони», у якій ID=100, у нас є дві групи фільтрів — «Виробники» і «Колір».

Припустимо, у нас в Redis-е для Samsung-а є ось товару 4 — 201, 202, 203, 204, а для Philips — 301, 302, 303. Ключ, відповідно, містить ID рубрики, фільтр і варіант фільтра. Якщо користувач вибирає в панелі фільтрів Samsung, ми запитуємо в Redis, отримуємо ID товарів, віддаємо їх у компонент списку товарів, і наш компонент відображає 4 товару. Якщо користувач вибирає ще виробника Philips, ми і робимо два запиту в Redis, беремо об'єднання цих множин і, відповідно, показуємо сім елементів в нашому каталозі.

Далі, якщо користувач вибирає варіант фільтра з іншої групи фільтрів, ми дістаємо, наприклад, колір червоний, тобто ми хочемо подивитися усі Samsung-і і Philips-и червоного кольору, ми беремо безліч об'єднання перших двох варіантів, перетинаємо, відповідно, з червоними телефонами і отримуємо два товари — 202, 303.

Особливості реалізації даної — вона набагато швидше, ніж робота з MySQL, але є одне але: необхідно при кожних зміни наших сутностей, тобто коли змінюються властивості товару — товар переходить з рубрики рубрику, у товару змінюється виробник, товар активується/деактивується, створюються, змінюються, видаляються фільтри, — ми повинні ці множини все перераховувати. Тобто це основна робота, яку треба робити, і у нас це реалізовано через черги, тобто ми наші безлічі перераховуємо, використовуючи брокер повідомлень RabbitMQ, про яке мова піде далі.

Говорячи про чергах, я буду говорити про «кролика». RabbitMQ — це платформа, що реалізує систему обміну повідомленнями за допомогою протоколу AMQP. Особливості даного сервісу — це надійність, гнучка система маршрутизації повідомлень, підтримка кластеризації, підтримка різних плагінів, які дозволяють нам дивитися на стан наших черг, дивитися, скільки в них повідомлень, які консьюмеры їх обробляють, також можна писати кастомні плагіни, щоб змінити поведінку нашого брокера. Написаний RabbitMQ на Erlang. І клієнти для роботи з «кроликом» є для більшості мов — для Java, Ruby, Python, .NET, PHP, Perl, C/C++ та ін. На сайті «кролика» є відмінні tutorial-и, добре задокументовані можливості. Для кожної мови є навіть приклади, все доступною мовою описано.

Які ж є основні поняття при роботі з «кроликом»?

Я розповім про те, які є типи обмінників в «кролика», як відбувається маршрутизація, і буду показувати діаграми, як відбуваються у workflow повідомлення від продюсера до консьюмеру.



Продюсер (Р) — це програма, яка надсилає повідомлення. Повідомлення не потрапляють безпосередньо в чергу, а через обмінник — Exchange (Х), і вже обмінник вирішує, в яку чергу ми повинні послати те чи інше повідомлення. Зрозуміло, що є черги, в яких зберігаються повідомлення, а також є консьюмеры — програми, які ці черзі читають і якимось чином обробляють.

Найпростіший workflow — продюсер посилає повідомлення в обмінник, обмінник вирішує, в яку чергу допомогою binding-ключа послати повідомлення, і консьюмер вже читає конкретну чергу. Отримавши повідомлення з черги, консьюмер може підтверджувати, що так, я це отримав повідомлення, обробив, повідомлення можна з черги прибирати, тобто є механізм підтвердження того, що повідомлення опрацьовано в «кролика». Якщо консьюмер взяв повідомлення і раптово впав, повідомлення залишиться в черзі, поки ми і не підтвердимо, що ми його обробили.

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

Які ж у нас бувають обмінники?



Перший тип — це Direct Exchange, і суть його в наступному: обмінник може бути забинден з чергою різними правилами. Наприклад, тут верхня чергу забиндена з Exchange допомогою binding-ключа error, а нижня чергу забиндена з обмінником через ключ warning. Продюсер, посилаючи повідомлення в обмінник, передає routing-ключ повідомлення і, ось наприклад, на слайді красненьке повідомлення має routing-ключ error, якщо воно збігається з binding-ключем, то вона потрапляє у верхню чергу. Якщо повідомлення має routing-ключ warning, тобто суворе відповідність routing-ключ з binding-ключем конкретної черги. Ми можемо різні повідомлення віддавати в різні консьюмеры.

Наступний тип обмінника — це Fanout.



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

Наступний тип — обмінники з типом Topic.



Обмінники цього типу можуть биндиться з чергами за допомогою ключів, які можуть містити такі спецсимволи як зірочка (*) і решітка (#). Що це означає? Зірочка в binding-ключі може замінюватися рівно на одне слово, а решітка може замінюватися на 0 і більше слів.

В даному прикладі чергу Q1 забиндена допомогою ключа *.orange.*, а друга черга забиндена двома ключами, тобто вона буде приймати повідомлення, які починаються з lazy, а інше неважливо — там може бути скільки завгодно інших слів, і слова розділяються крапкою, і другий binding-ключ — це складається з трьох слів: перші два неважливо які, головне щоб в кінці стояв «кролик».

Якщо у нас продюсер послав повідомлення з таким routig-ключем, як перший приклад на слайді, воно потрапляє в обидві черги, тому що воно складається з трьох слів, другим словом містить orange, а на кінці у нього слово rabbit.

RabbitMQ на практиці.

Ми використовуємо «кролика» багато де, наприклад, для перерахунку даних в нашому Redis-e (див. вище). Тобто у нас є багато процесів-продюсерів, які змінюють якісь дані, ми посилаємо ці команди на зміни в наші черги, там у нас не дві черги, їх більше, і консьюмеры вже обробляють конкретні повідомлення. Наприклад, товар А деактивировался, ми повинні його видалити. Ми надсилаємо повідомлення у вигляді JSON-а в нашу чергу, консьюмер його читає, коннектітся до Redis-у, там у нас master-slave реплікація, ми пишемо, відповідно, майстер, парсим цей JSON, бачимо, що необхідно видалити такий-то товар, з такою-то рубрики, таких множин, видаляємо його, і наш фронтенд вже читає зі slave Redis-а і показує оновлені дані.

Власне, це все, що я хотів розповісти про ці прості інструменти — кешування і черги. Є питання?

Питання з залу: Могли б Ви детальніше розповісти, як у вас реалізовано «протухание» кешу, тобто як ви його вираховує, які значення виставляєте? Наприклад, коли ви показуєте картку товару і, припустимо, змінилася ціна — як швидко у вас оновиться кеш?

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

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

Відповідь: Залишки ми не кешуємо. У випадку з карткою товару, насправді, можна просто дописати якусь логіку, яка при зміні ціни товару буде брати з memcached відповідну картку товару, знаючи його ID, та оновлювати в ній ціну.

Питання з залу: Ось тут як раз це детальніше — про логіки оновлення кешу в різних ситуаціях?

Відповідь: Якщо нам необхідно оновити дані в кеші, у нас є обробники на зміну товарів. Якщо товар змінює ціну, ми просто в цьому процесорі коннектимся до memcached, просто формуємо нову картку товару і зберігаємо її в memcached. Моментально ціна буде оновлено для клієнтів.

Питання з залу: чи Є якісь вбудовані засоби розуміння частоти використання даних в кеші, щоб зрозуміти, які рідко використовуються і в разі переповнення їх очищати? Або це все вручну треба робити?

Відповідь: Я не знаю таких інструментів.

Питання з залу: Яку максимальну якість черг ви використовували в одному проекті і для чого?

Відповідь: У нас близько 20-ти черг в «кролика». Там є черги, які перераховують наші дані Redis, є черга, яка стежить за тим, які смс яким клієнтам відіслати, є черга за статусної схемою, життєвий цикл замовлення теж через черзі проходить, відбувається зміна статусів… Близько 20 черг, не так багато. Наші консьюмеры встигають обробляти всі повідомлення, більше впираємося в запити до MySQL. Можна кілька консьюмеров, якщо у вас черга велика, нацькувати, і він розбере вже цю чергу швидше в два-три рази.

Питання з залу: Як у вас влаштована пагинация в Redis-е. коли фільтр вибирає значення?

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

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

Відповідь: Мemcached там використовується, тому що у нас вже є в ngnix модуль, який з memcached вміє працювати, а Redis є в бэкенде, тому що у нас є ще й інші процеси, які можуть складати картку товару в Redis. Грубо кажучи, клієнт заходить на картку товару, в звичайну, не popup. Там вже кешується картка товару в Redis і в наступний раз, якщо користувач прийде з іншої сторінки, але вже, наприклад, відкриє popup, у нас немає даних memcached, і наш запит долетів до PHP, то ми там, не підключаючи важкий framework, безпосередньо спочатку звертаємося до Redis. Якщо дані в ньому є, то дуже швидко віддаємо. Відповідно, якщо даних в Redis-е ні, то ми підключаємо важкий framework, беремо дані з MySQL і складаємо їх у Redis і memcached.

Питання з залу: Скажіть, ви як-небудь моніторите роботу черг?

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

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

Відповідь: Таке завдання ми не вирішували, підтримки я такої не бачив, але думаю, тут треба вже на рівні вашого додатки якісь

Питання з залу: чи Є у RabbitMQ система подій, щоб не ганяти кожен раз обробник черг по колу, а щоб «кролик» смикав за якусь подію сам конкретний скрипт?

Відповідь: Так, це на рівні врапперов. У консьюмер можна передати callback, і коли приходить повідомлення, воно там автоматом виконується.

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

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

Питання з залу: Ви говорили про те, що в RabbitMQ реалізований механізм гарантованої доставки, а чи існує механізм повідомлення продюсера про те, що повідомлення доставлено до консьюмера?

Відповідь: Ми такого не робили, але думаю, можна реалізувати такий механізм на рівні вашого додатка.

Питання з залу: Ви говорите, що Redis за всіма параметрами краще, ніж memcached. А навіщо тоді memcached використовувати?

Відповідь: Я не кажу, що Redis за всіма параметрами краще, ніж memcached, просто потім під кожну свою задачу можна використовувати або Redis, або memcached. Останній швидше, але в Redis-e більше можливостей. Якщо що-то просто треба кешувати, і ці дані не так важливі, навіть якщо вони пропадуть, якщо ви використовуєте memcached, то нічого страшного. Його можна використовувати, він дуже надійний, високопродуктивний сервер.

Питання з залу: А наскільки в середньому швидше виходить memcached? 10%, 20%, 50%?

Відповідь: Ну, я дивився різні бенчмарки, скрізь різні дані   хто-то в декілька потоків робить записи читання, хтось показує, що Redis місцями на 10% краще, ніж memcached, або навпаки. Сам бенчмарків не проводив, в наших програмах це не найвужче місце, у нас інші проблеми. Але такі бенчмарки є, і дані різняться. Але швидкості порівнянні.

Ця доповідь — розшифровка одного з кращих виступів на конференції розробників високонавантажених систем HighLoad++.

Також деякі з цих матеріалів використовуються нами в навчальному онлайн-курс по розробці високонавантажених систем HighLoad.Guide — це ланцюжок спеціально підібраних листів, статей, матеріалів, відео. Вже зараз у нашому підручнику понад 30 унікальних матеріалів. Підключайтеся!

Ну і головна новина — ми почали підготовку весняного фестивалю "Російські інтернет-технології", в який входить вісім конференцій, включаючи HighLoad++ Junior. Ми, звичайно, жадібні коммерсы, але зараз продаємо квитки за собівартістю — можна встигнути до підвищення цін
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.