[PHP] Отказоустойчивая работа с Redis
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Эта статья — переработанная версия доклада Отказоустойчивая работа с Redis с прошедшего 17 октября 2020 митапа PHP-разработчиков Йошкар-Олы.Мы поговорим о подводных камнях использования Redis в системе, где важна отказоустойчивость — на примере хранения сессий в условном веб-сервисе, написанном на PHP, но многие замечания будут справедливы и для других платформ — например для микросервисов на Go. Статья будет полезна, если вы проектируете микросервисы или монолитные приложения с достаточно большой нагрузкой и интенсивно работаете с Redis либо столкнулись с потребностью в перепроектировании механизмов аутентификации и сессий.Немного о RedisRedis — это NoSQL СУБД с открытым исходным кодом, работающая с данными вида «ключ — значение». Если вы захотите узнать о преимуществах Redis, вы наверняка найдёте примерно такой список:
- Redis хранит все данные в оперативной памяти, что повышает производительность
- В Redis нет SQL и схемы хранилища, что опять же повышает производительность (нет интерпретатора SQL) и даёт гибкость
- Нет никаких ACID транзакций, изменения просто сохраняются на диск в фоне, что тоже повышает производительность
- см. Redis Persistence в документации
Иначе говоря, Redis может быть очень быстрым.Но давайте взглянем на это с точки зрения надёжности и отказоустойчивости:
- Redis хранит все данные в оперативной памяти, и при аварийном завершении процесса или выключении машины данные будут потеряны
- Redis не ориентирован на ACID транзакции, и на практике значит:
- даже при включённом сохранении на диск данные могут быть потеряны частично либо полностью
- в данных может быть нарушена целостность с точки зрения приложения
- фоновое сохранение данных на диск может нанести ущерб производительности и отказоустойчивости
Значит ли это, что Redis плох? Вовсе нет. Наша статья о том, что при внедрении Redis в проект нужно рассматривать разные варианты и принимать взвешенные решения, а не верить слепо, что всё уже предусмотрено умными дядьками и никаких проблем на production не возникнет.Теперь поговорим про это подробнее.Сценарии отказа RedisДопустим, мы используем классический метод развёртывания веб-приложений, и у нас есть две машины — на одной развёрнуто PHP приложение, на другой — Redis.Обычно в PHP приложении на каждый HTTP-запрос создаётся новый контекст PHP-интерпретатора, со своей памятью и ресурсами. Чтобы обратиться к Redis (например, для чтения/записи данных сессии), приложению придётся установить соединение с Redis, а потом отправить одну или несколько команд — например, SETEX key, value, ttl. С точки зрения Redis, есть 4 этапа обработки первой полученной команды:
- Приём соединения
- Приём команды
- Ожидание в очереди
- Обработка команды
Из этих шагов только последний работает с той фантастической скоростью, которую обещают разработчики Redis. Все остальные могут вызывать проблемы.Во-первых, Redis может упасть:
В этом случае приложение попытается установить соединение, но не сможет (сразу либо после произвольного таймаута, установленного для библиотеки Predis). Будет создано исключение, к которому код может быть не готов.Во-вторых, может нарушиться сетевая связность:
В этом случае бесполезное ожидание и отказ по таймауту неизбежны, что может привести к веерным отказам: сначала бесполезные таймауты под нагрузкой приведут к повышению числа активных процессов php-fpm и числа соединений с СУБД, а потом закончится либо одно, либо другое, клиенты начнут получать 500-е и 504-е коды ошибок — сервис перестанет их обслуживать.В-третьих, под высокой нагрузкой Redis может не успеть обработать полученную команду:
С точки зрения приложения произойдёт отказ по таймауту, с рисками веерного отказа. Что ещё хуже, веерные отказы могут произойти и без явных отказов Redis: приложение просто будет ждать дольше обычного и исчерпаются соединения с СУБД либо процессы php-fpm.Наконец, у Redis может кончиться память, и тогда всё зависит от настроек maxmemory и maxmemory-policy. По умолчанию maxmemory-policy имеет значение noeviction, и это означает, что при нехватке памяти Redis перейдёт в режим readonly:
Даже если поменять maxmemory-pocity, вы можете получить ситуацию, когда при нехватке памяти Redis выкидывает ключи, значительно сокращая время жизни сессий, токенов и других полезных данных. Будет ли ваше приложение работать нормально, если контракт на время жизни данных не выполняется?Сценарии отказа Redis в KubernetesДавайте сгустим краски ещё больше: добавим Kubernetes и засунем в него Redis.Kubernetes — это система управления кластером. Он запускает N приложений в контейнерах на меньшем числе машин, распределяя нагрузку такими сложными путями, что для сопровождения проекта в Kubernetes вам потребуется отдельная команда (если у вас всё кросс-функциональное, тогда её размажет ровным слоем по остальным командам).Допустим, у нас есть кластер из 4 worker'ов, управляемых Kubernetes. Допустим, Kubernetes раскидал экземпляры PHP-приложения и Redis так, как показано на картинке:
После этого worker #3 вывели на обслуживание, в результате мы теряем единственный экземпляр Redis и 2 из 4 экземпляров приложения и получаем кратковременный отказ. Если Redis прислал свои данные не на сетевой диск, то получаем ещё и потерю всех его данных. Если же он писал на сетевой диск, то делал это медленно, поскрипывая винчестерами на весь дата-центр и мешая дежурным спать.Как бы развернуть Redis, чтобы потеря одной worker-машины не создавала проблем?У Redis предусмотрено два способа организации отказоустойчивости: Redis Sentinel и Redis Cluster. Прочитав документацию и собрав факты, вы увидите следующее:
- Обе реализации являются скорее кирпичиками для построения отказоустойчивости с автоматическим failover
- Redis Cluster имеет некоторую автоматизацию, но эта разница нивелируется в Kubernetes за счёт Redis Operator (например, spotahome/redis-operator), который основан на Redis Sentinel
- Redis Cluster даёт шардирование, Redis Sentinel его не даст
Если вы хотите производительность выше той космической, что можно выжать из одного инстанса, подумайте о Redis Cluster вне Kubernetes. Если вы хотите засунуть Redis в Kuberntes, используйте Redis + Sentinel + Redis Operator.Отказоустойчивость Sentinel и Cluster имеет свою цену:
- Оба варианта меняют протокол взаимодействия с Redis, и клиент должен поддерживать новый вариант
- Как минимум 3 экземпляра Redis Sentinel нужно, чтобы достигнуть кворума при выборке master
- Сам Redis Sentinel нового мастера не назначит, его надо попросить, что и делает Redis Operator
Взаимодействие PHP-приложения с Redis Sentinel показано на схеме:
Сначала приложение обращается к Redis Sentinel и узнаёт, какой из экземпляров Redis является мастером. Затем приложение обращается к нужному экземпляру Redis.Допустим, в нашем кластере распределено 3 redis и 3 redis-sentinel, причём redis master и один из sentinel оказались на одной машине #3:
После этого worker #3 вывели на обслуживание, в результате мы теряем redis master и один из redis-sentinel. Пока redis-sentinel не перезапустится на другой машине, а redis operator не попросит всех троих выбрать нового master, redis останется недоступен (по крайней мере на запись).Чтобы PHP-приложение (или, например, NGINX/Lua) не обращалось к redis-sentinel на каждый запрос, можно ввести ещё одного игрока: redis-proxy (например, ifwe/twemproxy). Этот proxy является stateless, он может быть запущен внутри контейнера PHP-приложения либо как sidecar-контейнер, чтобы минимизировать задержки сети.Взаимодействие PHP-приложения с Redis Proxy показано на схеме:
Возможен ещё один вариант отказа из статьи Одна история с оператором Redis в K8s и мини-обзор утилит для анализа данных этой БД
- Worker, на котором находится redis master, выводится на обслуживание
- Запускается новый redis
- Посовещавшись, тройка redis sentinel выбирает новый redis мастером
- Новый и абсолютно пустой redis в роли мастера просит реплики удалить все данные
Баг, приводящий к такому сценарию, исправили в spotahome/redis-operator, но кто даст гарантию, что это был последний баг?Избежать проблемы можно путём добавления сетевого диска — например, NFS. Тогда NFS станет ещё одной точкой отказа.Так что же делать?Если вы на этапе проектирования, подумайте:
- Так ли вам нужен Redis в этом классе/модуле/сервисе? избегайте хранения в Redis важных данные или данных, требующих контроля целостности
- Так ли вам нужен Redis в Kubernetes, или подойдёт Redis на отдельной машине?
В целом стоит насторожиться, когда кто-то предлагает засунуть хранилища данных в Kubernetes. Если это предлагает коллега, расскажите ему анекдот про мужика, сено и скафандр.Вы можете столкнутся с несколькими доводами в пользу применения Redis:
- В проекте уже используют Redis
- возражение: важно понимать, для чего именно использован Redis и на какие компромиссы при этом согласились
- Данные имеют time to life
- возражение: это не мешает хранить их в SQL СУБД
- Redis быстрее SQL СУБД
- возражение: перегруженный redis работает медленее, кроме того, взаимодействие с двумя хранилищами вместо одного добавляет сетевые задержки и точки отказа
Достойная причина использовать Redis — высокие нагрузки и данные, которые не так страшно потерять.Как улучшить отказоустойчивостьСложная система порождает новые сценарии катастроф. Что ещё хуже, вы не можете заранее предсказать все риски. Но можно практиковать избыточность, нагрузочное тестирование и тестирование тех сценариев отказа, которые вы можете предсказать.Избыточность может проявляться по-разному:
- В дополнительных сущностях: redis-sentinel, redis-operator, redis-proxy
- В распределении redis по разным машинам или даже разным дата-центрам
- В дополнительной отказоустойчивости со стороны приложения, а не инфраструктуры
Последний пункт почти не упоминается в статьях об отказоустойчивом Redis, а мы поговорим об этом подробнее.Делаем кэширование необязательнымДопустим, наше приложение на PHP использует Redis как кэш. В таком случае мы должны:
- При записи сохранять данные и в Redis, и в основную БД
- При чтении читать сначала из Redis, а при ошибке — читать из БД
- Сохранить небольшой timeout для клиента Redis — например, 100 или 300 миллисекунд
Рассмотрим схему классов для хранения данных сессии:
- Интерфейс KeyValueCacheInterface объявляет обобщённый API для работы с кэшем, за которым скрыт Redis
- Интерфейс SessionStorageInterface объявляет хранилище данных сессии
- реализация DatabaseSessionStorage хранит сессии в таблице в БД
- реализация KeyValueCacheSessionStorage хранит сессии в кэше
- SessionService использует оба варианта хранилища, но работает с ними по-разному
При чтении данных в SessionService сначала пытаемся читать из Redis, в случае ошибки — читаем из БД:
private function doRead(callable $job)
{
try
{
return $job($this->cacheStorage);
}
catch (KeyValueCacheNotAvailableException $e)
{
$this->loggerService->error(
'failed to read from session cache',
['exception' => $e]
);кластере
return $job($this->storage);
}
}
Для обновления данных записываем и Redis, и в БД:
private function doUpdate(callable $job): void
{
try
{
$job($this->cacheStorage);
}
catch (KeyValueCacheNotAvailableException $e)
{
$this->loggerService->error(
'failed to write into session cache',
['exception' => $e]
);
}
$job($this->storage);
}
Такое решение имеет свои ограничения:
- Возрастёт нагрузка на БД — в сравнении с хранением данных только в Redis
- После отказа и восстановления кэша в нём появляются неактуальные данные, записанные в БД в период отказа
Усовершенствование в хранении сессийНа примере всё тех же сессий посмотрим, как можно усовершенствовать механизм отказоустойчивости.Допустим, у нас есть сессии, которые хранятся в трёх хранилищах: session id хранится в Cookie, а данные сессии хранятся одновременно и в Redis, и в MySQL:
Сессию надо периодически обновлять, чтобы при активном использовании TTL (time to life) не истёк и сессия не закончилась. Чтобы уменьшить частоту обновления сессий, мы можем добавить в данные сессии дату последнего обновления, и обновлять сессию в обработчике запроса только в том случае, если с последнего обновления прошло больше определённого числа секунд (например, 60 секунд).Кстати, именно так ведут себя современные PHP фреймворки — например, Symfony.Обновлённое расположение данных будет выглядеть так:
Чтобы решить проблему инвалидации кэша при его недоступности, мы можем добавить в Cookie ещё одно значение: целочисленный generation number данных сессии, который увеличивается на единицу при каждом изменении данных сессии, и предпочитать чтение данных из БД в случае, если generation number в Redis не совпадает с тем, что пришёл из Cookie:
Сессии в Signed CookiesИдею хранения данных в Cookie можно развить, но для этого надо сделать Cookie надёжным хранилищем, в котором нельзя подменить данные. Этого можно достигнуть с помощью Signed Cookies — например, можно в качестве значения хранить JWT (JSON Web Token). В этом случае:
- Секретный ключ для подписи JWT хранится на сервере, и пользователь не может подменить содержимое Cookie, не нарушив целостность подписи JWT.
- Секретные данные в Signed Cookie хранить по-прежнему нельзя, т.к. полезная нагрузка в JWT хранится в открытом виде
С таким подходом основные данные сессии можно переместить в Cookie небольшого размера (не более 1-2 КБ), и до истечения JWT вообще не обращаться к Redis и MySQL. Короткое время жизни такой Cookie обеспечит баланс между временем инвалидации данных и снижением нагрузки на сервера.Состояние сессии, влияющее только на просмотр, можно хранить в открытом виде в Cookie, а можно точно так же поместить в JWT.После этих изменений схема расположения данных выглядит, как показано ниже. Можно убрать из схемы Redis, поскольку Signed Cookie снизит число обращений к данным сессии на сервере.
ПодытожимОтказоустойчивости в работе с Redis или с любым другим вспомогательным хранилищем можно достигнуть не только за счёт схемы развёртывания и инфраструктуры, но и за счёт пересмотра архитектуры или доработок приложения, обеспечивающих устойчивость к отказам.Чтобы это сработало, надо:
- Учесть разные сценарии отказа Redis, в том числе связанные с превышением таймаутов, превышением лимита памяти или сетевых соединений
- Пересмотреть архитектуру и реализацию системы с точки зрения взаимодействия с Redis
- Решить, куда вносить доработки: в схему развёртывания, в реализацию взаимодействия с Redis или даже в архитектуру хранения данных
- Для большей устойчивости к отказам и нагрузкам потребуется применить все три метода
Кроме того, стоит решить, будет ли использование Redis, memcached или иного быстрого хранилища уместным:
- Возможно, требования по нагрузке и отказоустойчивости позволяют не усложнять и хранить всё, включая сессии, в основной БД
- Возможно, идея использовать Redis пришла из-за того, что данные временные; в этом случае данные можно так же хранить в БД, не забывая:
- Добавить в таблицу и во все связанные SQL-запросы колонку expiration date
- Реализовать механизм очистки старых данных фоновыми задачами, запускаемыми через cron или CronJob (в Kubernetes), например, каждую ночь
В микросервисной архитектуре можно выделить работу с сессиями, cookie и токенами в отдельный сервис (который также может взять на себя задачи аутентификации). В монолите можно скрыть работу с Redis за абстракцией, а в реализации этой абстракции Redis рассматривать как вспомогательный кэш, недоступность которого не приводит к отказу.
===========
Источник:
habr.com
===========
Похожие новости:
- [Высокая производительность, Разработка веб-сайтов, Проектирование и рефакторинг] Highload там, где его не ждёшь. Приключение на 20 минут
- [PHP] Избегая автобусов (перевод)
- [Системное администрирование, PHP, MySQL, IT-инфраструктура, Nginx] Установка Ispconfig3 на ubuntu 20.04 (nginx+php-fpm+mysql)
- [PHP, Программирование, Тестирование веб-сервисов] Веб-скрейпинг на PHP (перевод)
- [Разработка веб-сайтов, PHP, Symfony, Микросервисы] Чек-лист: как оставаться отказоустойчивым, переходя на микросервисы на PHP (и как правильно падать)
- [Разработка веб-сайтов, Программирование, Symfony] Главные причины, почему мы разрабатываем веб-приложения на Symfony (перевод)
- [Настройка Linux, Open source, Виртуализация, Визуализация данных] Визуализация данных производительности в RHEL 8
- [Управление продуктом, Управление персоналом, Карьера в IT-индустрии] Как стать product-менеджером: специфика профессии, must-have навыки и вектор развития
- [Информационная безопасность, Системное администрирование, IT-инфраструктура, Виртуализация] АЭРОДИСК vAIR v2. Часть 3. Железный вопрос. Аппаратные платформы, лицензирование, поддержка… и ФСТЭК
- [Разработка веб-сайтов, NoSQL, ReactJS, Serverless] You don't know Redis
Теги для поиска: #_php, #_php, #_redis, #_otkazoustojchivost (отказоустойчивость), #_highload, #_high_availability, #_blog_kompanii_ispring (
Блог компании ispring
), #_php
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 01:12
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Эта статья — переработанная версия доклада Отказоустойчивая работа с Redis с прошедшего 17 октября 2020 митапа PHP-разработчиков Йошкар-Олы.Мы поговорим о подводных камнях использования Redis в системе, где важна отказоустойчивость — на примере хранения сессий в условном веб-сервисе, написанном на PHP, но многие замечания будут справедливы и для других платформ — например для микросервисов на Go. Статья будет полезна, если вы проектируете микросервисы или монолитные приложения с достаточно большой нагрузкой и интенсивно работаете с Redis либо столкнулись с потребностью в перепроектировании механизмов аутентификации и сессий.Немного о RedisRedis — это NoSQL СУБД с открытым исходным кодом, работающая с данными вида «ключ — значение». Если вы захотите узнать о преимуществах Redis, вы наверняка найдёте примерно такой список:
Из этих шагов только последний работает с той фантастической скоростью, которую обещают разработчики Redis. Все остальные могут вызывать проблемы.Во-первых, Redis может упасть: В этом случае приложение попытается установить соединение, но не сможет (сразу либо после произвольного таймаута, установленного для библиотеки Predis). Будет создано исключение, к которому код может быть не готов.Во-вторых, может нарушиться сетевая связность: В этом случае бесполезное ожидание и отказ по таймауту неизбежны, что может привести к веерным отказам: сначала бесполезные таймауты под нагрузкой приведут к повышению числа активных процессов php-fpm и числа соединений с СУБД, а потом закончится либо одно, либо другое, клиенты начнут получать 500-е и 504-е коды ошибок — сервис перестанет их обслуживать.В-третьих, под высокой нагрузкой Redis может не успеть обработать полученную команду: С точки зрения приложения произойдёт отказ по таймауту, с рисками веерного отказа. Что ещё хуже, веерные отказы могут произойти и без явных отказов Redis: приложение просто будет ждать дольше обычного и исчерпаются соединения с СУБД либо процессы php-fpm.Наконец, у Redis может кончиться память, и тогда всё зависит от настроек maxmemory и maxmemory-policy. По умолчанию maxmemory-policy имеет значение noeviction, и это означает, что при нехватке памяти Redis перейдёт в режим readonly: Даже если поменять maxmemory-pocity, вы можете получить ситуацию, когда при нехватке памяти Redis выкидывает ключи, значительно сокращая время жизни сессий, токенов и других полезных данных. Будет ли ваше приложение работать нормально, если контракт на время жизни данных не выполняется?Сценарии отказа Redis в KubernetesДавайте сгустим краски ещё больше: добавим Kubernetes и засунем в него Redis.Kubernetes — это система управления кластером. Он запускает N приложений в контейнерах на меньшем числе машин, распределяя нагрузку такими сложными путями, что для сопровождения проекта в Kubernetes вам потребуется отдельная команда (если у вас всё кросс-функциональное, тогда её размажет ровным слоем по остальным командам).Допустим, у нас есть кластер из 4 worker'ов, управляемых Kubernetes. Допустим, Kubernetes раскидал экземпляры PHP-приложения и Redis так, как показано на картинке: После этого worker #3 вывели на обслуживание, в результате мы теряем единственный экземпляр Redis и 2 из 4 экземпляров приложения и получаем кратковременный отказ. Если Redis прислал свои данные не на сетевой диск, то получаем ещё и потерю всех его данных. Если же он писал на сетевой диск, то делал это медленно, поскрипывая винчестерами на весь дата-центр и мешая дежурным спать.Как бы развернуть Redis, чтобы потеря одной worker-машины не создавала проблем?У Redis предусмотрено два способа организации отказоустойчивости: Redis Sentinel и Redis Cluster. Прочитав документацию и собрав факты, вы увидите следующее:
Сначала приложение обращается к Redis Sentinel и узнаёт, какой из экземпляров Redis является мастером. Затем приложение обращается к нужному экземпляру Redis.Допустим, в нашем кластере распределено 3 redis и 3 redis-sentinel, причём redis master и один из sentinel оказались на одной машине #3: После этого worker #3 вывели на обслуживание, в результате мы теряем redis master и один из redis-sentinel. Пока redis-sentinel не перезапустится на другой машине, а redis operator не попросит всех троих выбрать нового master, redis останется недоступен (по крайней мере на запись).Чтобы PHP-приложение (или, например, NGINX/Lua) не обращалось к redis-sentinel на каждый запрос, можно ввести ещё одного игрока: redis-proxy (например, ifwe/twemproxy). Этот proxy является stateless, он может быть запущен внутри контейнера PHP-приложения либо как sidecar-контейнер, чтобы минимизировать задержки сети.Взаимодействие PHP-приложения с Redis Proxy показано на схеме: Возможен ещё один вариант отказа из статьи Одна история с оператором Redis в K8s и мини-обзор утилит для анализа данных этой БД
private function doRead(callable $job)
{ try { return $job($this->cacheStorage); } catch (KeyValueCacheNotAvailableException $e) { $this->loggerService->error( 'failed to read from session cache', ['exception' => $e] );кластере return $job($this->storage); } } private function doUpdate(callable $job): void
{ try { $job($this->cacheStorage); } catch (KeyValueCacheNotAvailableException $e) { $this->loggerService->error( 'failed to write into session cache', ['exception' => $e] ); } $job($this->storage); }
Сессию надо периодически обновлять, чтобы при активном использовании TTL (time to life) не истёк и сессия не закончилась. Чтобы уменьшить частоту обновления сессий, мы можем добавить в данные сессии дату последнего обновления, и обновлять сессию в обработчике запроса только в том случае, если с последнего обновления прошло больше определённого числа секунд (например, 60 секунд).Кстати, именно так ведут себя современные PHP фреймворки — например, Symfony.Обновлённое расположение данных будет выглядеть так: Чтобы решить проблему инвалидации кэша при его недоступности, мы можем добавить в Cookie ещё одно значение: целочисленный generation number данных сессии, который увеличивается на единицу при каждом изменении данных сессии, и предпочитать чтение данных из БД в случае, если generation number в Redis не совпадает с тем, что пришёл из Cookie: Сессии в Signed CookiesИдею хранения данных в Cookie можно развить, но для этого надо сделать Cookie надёжным хранилищем, в котором нельзя подменить данные. Этого можно достигнуть с помощью Signed Cookies — например, можно в качестве значения хранить JWT (JSON Web Token). В этом случае:
ПодытожимОтказоустойчивости в работе с Redis или с любым другим вспомогательным хранилищем можно достигнуть не только за счёт схемы развёртывания и инфраструктуры, но и за счёт пересмотра архитектуры или доработок приложения, обеспечивающих устойчивость к отказам.Чтобы это сработало, надо:
=========== Источник: habr.com =========== Похожие новости:
Блог компании ispring ), #_php |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 01:12
Часовой пояс: UTC + 5