Русский
Русский
English
Статистика
Реклама

Кэширование

Перевод Уход от проблемы TTL или Стратегии корректного и быстрого кэширования

23.03.2021 18:09:53 | Автор: admin

Определение TTL для некоторых кэшированных данных (time to live время существования или длительность хранения) может стать своего рода шарлатанской нумерологией для программистов. При кэшировании по значениям TTL корректность жертвуется ради достижения скорости. Но до какой степени можно отказаться от корректности? Как долго можно показывать где-то неправильное значение, прежде чем пользователь будет сбит с толку? Как скоро такие пользователи заподозрят наличие у себя проблемы и лягут бременем на службы поддержки клиентов? Давайте разбираться.


Кэширование имеет значение, потому что оно обеспечивает большую скорость. Программа с наивным кодом, но которая хорошо использует кэш, обычно работает быстрее программы с умным кодом, которая этого не делает. Использование кеша часто меняет неявный класс сложности системы вниз и в сторону постоянного времени.

Но будет справедливо сказать, что кэширование это не круто. Существует снобизм в отношении кэша уровня приложений. Респектабельнее переписать программу на языке системного программирования (читайте: быстрая, скомпилированная программа для серьёзных целей), чем широко применять Memcache к PHP-приложению (читай: временное решение, apache2, используется только шутниками).

Это досадно, особенно потому, что часто переписывание не начинается (или никогда не заканчивается), а приложение PHP остается в рабочем состоянии, устойчиво и неподвижно, с упущенной простотой оптимизации.

Далее я подробно расскажу о стратегиях кэширования без отказа от корректности в целях достижения скорости. Во всех таких стратегиях вместо значений TTL используется активное аннулирование.

Стратегия 1: Просто никогда не аннулируйте

Самая простая стратегия - никогда не аннулировать. Некоторые данные не теряют актуальности. Содержимое CSV-файла upload #42345 не изменится. Как и HTML-преобразование некоторых данных продукта в формате markdown. Одна из причин, по которой люди не рассматривают эту стратегию, заключается в том, что они ошибочно опасаются, что кэш "заполнится". Краткосрочный кэш так не работает.

Вам не нужно управлять содержимым краткосрочного кэша с помощью TTL. Краткосрочный кэш будет управлять собственным содержимым с помощью вытеснения.

Кэши приложений, будь то Memcache или соответствующим образом настроенный Redis (см. ниже), работают по алгоритму LRU (least recently used). Редко используемые (холодные) данные вытесняются по мере необходимости, чтобы освободить место для более часто используемых (горячих) данных. Благодаря этому кэш в целом остаётся максимально горячим с учётом доступного пространства. Цель состоит в том, чтобы всегда иметь полный кеш.

При поступлении новых данных старые (более холодные) данные перемещаются вниз по иерархии. Если кэш заполнен (как это и должно быть), самые холодные данные грубо вытесняются. Я добавил клубы дыма для акцента.

В результате получается кэш, битком набитый данными, которые, скорее всего, понадобятся вашему приложению.

Не путайте вытеснение (часть алгоритма удаления наиболее давно использованных элементов) с истечением срока действия (часть TTL). Данные могут быть вытеснены до истечения срока их действия. И так будет часто: в большинстве случаев в кэше не учитываются значения TTL при поиске данных, которые следует вытеснить.

Стратегия 2: Обновление при записи

Иногда требуется кэшировать данные, которые будут изменяться. Например, возьмём объект пользователя вашего приложения: имя пользователя, адрес электронной почты, почтовый адрес и т. д. Эти данные неизбежно изменяются с течением времени, но к ним также обращаются часто, в каждом запросе или в каждой операции.

Лучший подход здесь обновлять кеш всякий раз, когда вы обновляете данные в своём постоянном хранилище. Таким образом, кеш остается в актуальном состоянии, не требуя каких-либо нечетких значений TTL и всех связанных предположений о том, как долго что-то будет оставаться в силе.

В соответствии с этой стратегией чтение преимущественно осуществляется из кэша, и только запись должна идти в постоянное хранилище или базу данных. Вытеснение данных из кэша не проблема: когда в кэше нет нужных данных, вы возвращаетесь к чтению из базы данных (требование для каждой стратегии из этой статьи).

Небольшой бонус в том, что многие базы данных могут записывать данные быстрее, когда модули записи не борются с модулями чтения за блокировки.

Стратегия 3: Аннулирование при записи

Иногда изменение данных может повлиять на несколько объектов в кэше. Например: объект User может содержаться в кэше как в переменной user_id, так и в переменной email_address. При каждом изменении пользователя все данные уже будет в памяти, так что вы можете легко обновить оба кэшированных значения. Вы можете красиво инкапсулировать это в одном месте своей базы кода, где редактируются пользователи. Счастливые дни.

Проблема возникает, когда нет всех данных, необходимых для этих обновлений. Например, если пользователь лайкает какую-то статью, то кэш его списка наиболее понравившихся авторов статей необходимо обновить, но при обработке одного лишь лайка в памяти нет всех необходимых данных. Можно каждый раз извлекать все эти данные, чтобы пересчитать этот список в кэше, или просто аннулировать список в кэше и восстановить его, когда он понадобится в следующий раз.

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

Эта стратегия обычно применяется к связанным данным, но очень часто к агрегированным данным. Аннулирование при записи может применяться довольно широко, но имеет большое ограничение: необходимо знать ключи кэша, чтобы их можно было аннулировать. Это не всегда возможно.

Стратегия 4: Использование пространства имен

Когда нет конечного, узнаваемого списка ключей, которые исходя из изменения требуется аннулировать с эффектом домино, лучшим решением является кэширование пространства имен.

Пространства имён работают следующим образом: у вас есть специальное значение из пространства имён, которое следует включить как часть ключа в другие кэшированные данные. Часто, но не всегда значение из пространства имён представляет собой монотонно возрастающую метку времени последнего изменения.

Рабочий пример:

В /namespaces/user657 сохраняется монотонно возрастающая метка времени момента, когда пользователь 657 в последний раз изменил свой профиль, например, 1514937600.

Затем эта метка времени вносится в ключи, относящиеся к этому пользователю, например:

  • /user657/1514937600/likes/article-12312

  • /user657/1514937600/comments/article-12315

  • /user657/1514937600/something-else-completely

Теперь для каждого логического поиска в кэше требуются два реальных поиска: сначала ключа пространства имён, а затем фактического ключа. Когда пользователи что-то меняют, вы обновляете содержимое соответствующих ключей пространства имён, и все ключи, подчинённые старому ключу пространства имён, теперь недоступны и поэтому фактически недействительны. Достаточно остывшие данные вытесняются.

Очевидный недостаток: для каждого доступа к кэшу с пространством имён теперь требуются две двусторонние передачи сигналов вместо одной. Часто это того стоит. Во-первых, потому что с пространствами имён можно гораздо шире использовать кэширование, чем без них. Вы кэшируете данные, которые иначе не могли бы использовать. Вторая причина заключается в том, что две двусторонние передачи данных из кэша обычно всё ещё намного быстрее регенерируют или извлекают всё, что есть. Попадание в кэш, как мы надеемся, происходит намного более чем в два раза чаще промаха кэша.

Будьте осторожны: есть много способов наивно улучшить эту стратегию способами, которые не работают. Я не буду вдаваться в подробности сомнительно улучшенных версий, лишь укажу два варианта, которые необходимы для правильной работы такой схемы:

1. Ключ пространства имён должен быть не менее горячим, чем самый горячий вложенный в него ключ, в противном случае он может постоянно вытесняться перед этими ключами. Остерегайтесь оптимизаций, которые избегают необходимости извлекать ключ. Это приведёт к тому, что кэш LRU будет считать его холодным.

2. Фактически при аннулированиях такого рода предполагается, что нет других способов доступа к устаревшим подключам. Остерегайтесь попыток заменить иерархическое пространство имён подходами на основе тегов, которые позволяют продолжать извлекать такие устаревшие подключи после намеренного аннулирования.

Стратегия 5: HTTP PUSH и PURGE

HTTP имеет встроенную систему кэширования, но эта система основана только на значениях TTL. Однако в самом широком смысле стратегия 1 продолжает работать: просто установите абсурдно большое значение TTL, и браузеры и CDN будут следовать ей в той мере, в какой им это нужно.

Стратегия 4 также хорошо работает в HTTP, за исключением того, что в этом контексте она обычно называется перебором кэша. Общее использование заключается в том, чтобы поместить строки версий или метки времени выпусков в URL-пути или строки-запросы, чтобы браузер загружал только последний пакет JavaScript.

Иногда CDN предоставляют явные API очистки, хотя это в основном удобно тем, кто развёртывает изменения в статических файлах. Традиционные операции очистки CDN обычно занимают несколько минут, поэтому, вероятно, не стоит полагаться на них на уровне приложений, хотя что-то вроде сервиса fly.io, где вы выполняете часть своего кода в CDN, может работать лучше.

При кэшировании инвертированных прокси-серверов, которые вы размещаете самостоятельно, например Varnish и Apache Traffic Server, могут использоваться нестандартные команды PUSH и PURGE, которые позволяют явно контролировать содержимое кэша. Если возможно явное аннулирование, можно использовать стратегии 2 и 3. Если под рукой есть нужный файл, почему бы также не заполнить кэш инвертированного прокси-сервера?

В целом хорошее свойство кэширования на уровне HTTP заключается в том, что оно снимает нагрузку с сервера приложений.

Советы и подводные камни

Кэширование не такое уж солнечное и приветливое. Вот некоторые проблемы, которых следует избегать, и некоторые советы, которые следует учитывать.

Не помещайте в кэш некэшируемые данные

Одна из проблем, которая встречается на практике, заключается в том, что иногда программисты становятся жертвой архитектур Baser Software и начинают использовать кэш в качестве базы данных и помещать туда реальные данные.

Некоторые люди совершают этот грех, так как получить новое хранилище данных, одобренное Центральным галактическим техническим консультативным советом организации, это бюрократический кошмар. Тем временем кэш-сервер сидит там и выглядит как потрясающая хэш-таблица, в которую можно просто втиснуть что-то. Следовательно, это отличное место, чтобы хранить некоторые статические данные, не спрашивая ни у кого разрешения. Наверняка такие данные всегда будут достаточно "горячими", чтобы не вытесняться, подумает кто-то. Но, в конце концов, специальные данные остынут, и будет великий плач и скрежет зубов, когда Memcache выбросит чьи-то данные в космос. Глупый кэш! Как ты смеешь!

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

Настройка максимально доступной памяти Redis по умолчанию (maxmemory) не ограничена. Это означает, что экземпляр всегда расширяется даже за пределами физической памяти без какого-либо вытеснения. Для кэширования задайте максимальный предел памяти, который строго меньше объёма физической памяти. Политика выселения по умолчанию (maxmemory-policy) заключается не в вытеснении, а в вызове ошибок в случае заполнения памяти. Опять же, если требуется вытеснение, это не хорошо для кэширования. В этом случае задайте значение allkeys-lru.

Большинство управляемых сервисов, таких как ElastiCache компании AWS, имеет другое значение по умолчанию для политики вытеснения: volatile-lru. При этой настройке для вытеснения рассматриваются только ключи, для которых задано значение TTL. Это умный подход, который позволяет дополнительно использовать экземпляр кэша для некэшируемых данных путём перегрузки флага TTL. По сути, volatile-lru это мина-ловушка, у которой есть множество неожиданных видов отказов. Во-первых, она побуждает программистов размещать некэшируемые данные на сервере, который сисадмины считают изменчивым и временным. Во-вторых, программисты, которые не знают об этой специальной настройке, склонны заполнять экземпляр по ошибке, многократно вставляя данные в кэше без TTL.

Redis можно настроить как сервер структур данных или непосредственно как кэш. Но нельзя сделать и то, и другое. Решив использовать Redis в качестве кэша, убедитесь, что экземпляр кэша служит только в качестве кэша. Ваша межсистемная шина сообщений должна быть в другом экземпляре Redis с другой конфигурацией.

Никогда не требовать попадания в кэш

Важно никогда не требовать попадания в кэш, даже в качестве мягкого требования. Вытеснение может происходить в неудобное время, например, когда какая-то другая часть системы находится под нагрузкой, и не должно быть никаких негативных последствий промаха кэша.

Одна из особенно сомнительных вещей хранить веб-сессии только в кэше. Тогда промах кэша невольно инициирует выход из системы незадачливого пользователя, который не сделал ничего плохого и ничего не нарушил. Вместо этого используйте стратегию 2, описанную выше, и храните веб-сессии в своей базе данных, используя кэш лишь для ускорения.

Чтобы избавиться от подобных проблем, стоит попробовать запустить автоматизированные тесты со специальными кэшами: тесты с частотой попадания 0 % или со случайной частотой попадания. Промах кэша ничего не должен нарушать, поэтому любые провальные тесты заслуживают изучения.

API в стиле декоратора

В стране Python люди любят декораторы. Например:

from functools import lru_cache@lru_cache(maxsize=1000)def get_slow_thing_v1(thing_id):    thing = get_slow_thing_from_a_database_layer(thing_id)    return thing

В приведённом выше коде нет ничего плохого, но он допускает только стратегию 1: никогда не аннулировать. Если есть необходимость в аннулировании/обновлении, код следует усложнить:

from pyappcache import RedisCachemy_cache = RedisCache()def get_slow_thing_v2(thing_id):    thing = my_cache.get_by_str(thing_id)    if thing is None:        thing = get_slow_thing_from_a_database_layer()        my_cache.set_by_str(thing_id, thing)    return thingdef update_something_slow(thing_id, new_thing):    set_thing_in_a_database_layer(new_thing)    my_cache.set_by_str(thing_id, new_thing)

Вторая версия более многословна, но зато позволяет обновлять кэш при настройке чего-либо.

Другое преимущество заключается в том, что значение из кэша можно получать в других местах по ключу оно больше не привязано к get_something_slow_v1. Это не важно в тривиальных случаях, но приобретает значение в более крупных системах. Если вы можете кэшировать по аргументам, почему бы и нет, но лучше не зацикливаться на кешировании исключительно на основе аргументов функций, поскольку это может быть немного ограничивающим.

Упоминание о собственной библиотеке

Если вы работаете с Python и хотите кэшировать данные, подумайте об использовании моей библиотеки: pyappcache (код здесь, документация здесь).

В ней много потрясающих функций, в том числе:

Есть и другие хорошие библиотеки для Python, в том числе dogpile.cache, cachew и, конечно, вы можете использовать Redis-py или pylibmc напрямую.

Иногда оно того стоит

Ничто из этого не означает, что кэш всегда стоит использовать. Это дополнительная работа по программированию, дополнительный сервис резервного копирования, который вам нужно запустить, и, конечно, больше драгоценного пространства для скрытия ошибок. Эффект Расёмона в смысле ошибок кэша делает их особенно неприятными как с точки зрения опыта, так и для отладки.

Тем не менее существует огромное количество систем, которые могли бы уменьшиться на пару размеров экземпляров AWS при обнаружении следов применения Memcache. Думайте об окружении, если это вас мотивирует.

Один совет: лучше всего начать с поиска вариантов аннулирования. Перефразируя кого-то: спрашивайте не о том, что можно кэшировать, а о том, что можно аннулировать. Если невозможно что-то надёжно аннулировать, то маловероятно, что удастся кэшировать это в первую очередь. Прежде чем пытаться что-то кэшировать, убедитесь, что это можно аннулировать везде, где необходимо.

Смотрите также

Мне нравится вот это объяснение, почему идея хранить сеансы в своем кэше не очень хорошая.

Apache (ne Inktomi) Traffic Server поддерживает PUSH и PURGE.

Я думаю, что Varnish поддерживает оба варианта, но вы должны их настроить.

На двух страницах, которые мне нравятся, описывается (слегка устаревшая, общая картина) конструкция Memcache: Memcache для чайников, автор Тиноу (Tinou), и Внутренние компоненты Memcache, автор Джошуа Тийссен (Joshua Thijssen). Документация memcache хороша и, хоть она и явно не закончена, включает некоторые трюки, о которых я не упоминал.

Документы системы Redis по её использованию в качестве кэша LRU необходимо прочитать, прежде чем пытаться использовать её таким образом. Как у большинства вещей у Redis, есть острые края. Важно знать, что при использовании кэширования значения по умолчанию вызывают проблемы.

Узнайте, как прокачаться в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Redis на практических примерах

18.06.2020 10:17:09 | Автор: admin
Redis достаточно популярный инструмент, который из коробки поддерживает большое количество различных типов данных и методов работы с ними. Во многих проектах он используется в качестве кэшируещего слоя, но его возможности намного шире. Мы в ManyChat очень любим Redis и активно используем его в нашем продукте для решения огромного количества задач. Про некоторые интересные кейсы использования этой in-memory key-value базы данных я расскажу на примерах. Надеюсь, вам они будут полезны, и вы сможете применить что-то в своих проектах.

Рассмотрим следующие кейсы:
  • Кэширование данных (да, банально и скучно, но это классный инструмент для кэширования и обойти стороной этот кейс, кажется будет не правильно)
  • Работа с очередями на базе redis
  • Организация блокировок (mutex)
  • Делаем систему rate-limit
  • Pubsub делаем рассылки сообщений на клиенты

Буду работать с сырыми redis командами, чтобы не завязываться на какую-либо конкретную библиотеку, предоставляющую обертку над этими командами. Код буду писать на PHP с использованием ext-redis, но он здесь для наглядности, использовать представленные подходы можно в связке с любым другим языком программирования.



Кэширование данных


Давайте начнем с самого простого, один из самых популярных кейсов использования Redis кэширование данных. Будет полезно для тех, кто не работал с Redis. Для тех, кто уже давно пользуется этим инструментом можно смело переходить к следующему кейсу. Для того, чтобы снизить нагрузку на БД, иметь возможность запрашивать часто используемые данные максимально быстро, используется кэш. Redis это in-memory хранилище, то есть данные хранятся в оперативной памяти. Ещё это key-value хранилище, где доступ к данным по их ключу имеет сложность O(1) поэтому данные мы получаем очень быстро.

Получение данных из хранилища выглядит следующим образом:

public function getValueFromCache(string $key){    return $this->getRedis()->rawCommand('GET', $key);}

Но для того, чтобы данные из кэша получить, их нужно сначала туда положить. Простой пример записи:

public function setValueToCache(string $key, $value){    $this->getRedis()->rawCommand('SET', $key, $value);} 

Таким образом, мы запишем данные в Redis и сможем их считать по тому же самому ключу в любой нужный нам момент. Но если мы будем все время писать в Redis, данные в нем будут занимать все больше и больше места в оперативной памяти. Нам нужно удалять нерелевантные данные, контролировать это вручную достаточно проблематично, поэтому пускай redis занимается этим самостоятельно. Добавим к нашему ключу TTL (время жизни ключа):

public function setValueToCache(string $key, $value, int $ttl = 3600){    $this->getRedis()->rawCommand('SET', $key, $value, 'EX', $ttl);}

По истечении времени ttl (в секундах) данные по этому ключу будут автоматически удалены.

Как говорят, в программировании существует две самых сложных вещи: придумывание названий переменных и инвалидация кэша. Для того, чтобы принудительно удалить значение из Redis по ключу, достаточно выполнить следующую команду:

public function dropValueFromCache(string $key){    $this->getRedis()->rawCommand('DEL', $key);}


Также редис позволяет получить массив значений по списку ключей:

public function getValuesFromCache(array $keys){    return $this->getRedis()->rawCommand('MGET', ...$keys);}

И соответственно массовое удаление данных по массиву ключей:

public function dropValuesFromCache(array $keys){    $this->getRedis()->rawCommand('MDEL', ...$keys);}


Очереди


Используя имеющиеся в Redis структуры данных, мы можем запросто реализовать стандартные очереди FIFO или LIFO. Для этого используем структуру List и методы по работе с ней. Работа с очередями состоит из двух основных действий: отправить задачу в очередь, и взять задачу из очереди. Отправлять задачи в очередь мы можем из любой части системы. Получением задачи из очереди и ее обработкой обычно занимается выделенный процесс, который называется консьюмером (consumer).

Итак, для того, чтобы отправить нашу задачу в очередь, нам достаточно использовать следующий метод:

public function pushToQueue(string $queueName, $payload){    $this->getRedis()->rawCommand('RPUSH', $queueName, serialize($payload));}

Тем самым мы добавим в конец листа с названием $queueName некий $payload, который может представлять из себя JSON для инициализации нужной нам бизнес логики (например данные по денежной транзакции, данные для инициализации отправки письма пользователю, etc.). Если же в нашем хранилище не существует листа с именем $queueName, он будет автоматически создан, и туда попадет первый элемент $payload.

Со стороны консьюмера нам необходимо обеспечить получение задач из очереди, это реализуется простой командой чтения из листа. Для реализации FIFO очереди мы используем чтение с обратной записи стороны (в нашем случае мы писали через RPUSH), то есть читать будем через LPOP:

public function popFromQueue(string $queueName){    return $this->getRedis()->rawCommand('LPOP', $queueName);}

Для реализации LIFO очереди, нам нужно будет читать лист с той же стороны, с которой мы в него пишем, то есть через RPOP.



Тем самым мы вычитываем по одному сообщению из очереди. В случае если листа не существует (он пустой), то мы получим NULL. Каркас консьюмера мог бы выглядеть так:

class Consumer {    private string $queueName;    public function __construct(string $queueName)    {        $this->queueName = $queueName;    }    public function run()    {        while (true) { //Вычитываем в бесконечном цикле нашу очередь            $payload = $this->popFromQueue();            if ($payload === null) { //Если мы получили NULL, значит очередь пустая, сделаем небольшую паузу в ожидании новых сообщений                sleep(1);                continue;            }            //Если очередь не пустая и мы получили $payload, то запускаем обработку этого $payload            $this->process($payload);        }    }    private function popFromQueue()    {        return $this->getRedis()->rawCommand('LPOP', $this->queueName);    }}

Для того, чтобы получить информацию о глубине очереди (сколько значений хранится в нашем листе), можем воспользоваться следующей командой:

public function getQueueLength(string $queueName){    return $this->getRedis()->rawCommand('LLEN', $queueName);}

Мы рассмотрели базовую реализацию простых очередей, но Redis позволяет строить более сложные очереди. Например, мы хотим знать о времени последней активности наших пользователей на сайте. Нам не важно знать это с точностью вплоть до секунды, приемлемая погрешность 3 минуты. Мы можем обновлять поле last_visit пользователя при каждом запросе на наш бэкенд от этого пользователя. Но если этих пользователей большое количество в онлайне 10,000 или 100,000? А если у нас еще и SPA, которое отправляет много асинхронных запросов? Если на каждый такой запрос обновлять поле в бд, мы получим большое количество тупых запросов к нашей БД. Эту задачу можно решать разными способами, один из вариантов это сделать некую отложенную очередь, в рамках которой мы будем схлопывать одинаковые задачи в одну в определенном промежутке времени. Здесь на помощь нам придет такая структура, как Sorted SET. Это взвешенное множество, каждый элемент которого имеет свой вес (score). А что если в качестве score мы будем использовать timestamp добавления элемента в этот sorted set? Тогда мы сможем организовать очередь, в которой можно будет откладывать некоторые события на определенное время. Для этого используем следующую функцию:

public function pushToDelayedQueue(string $queueName, $payload, int $delay = 180){    $this->getRedis()->rawCommand('ZADD', $queueName, 'NX', time() + $delay, serialize($payload))}

В такой схеме идентификатор пользователя, зашедшего на сайт, попадет в очередь $queueName и будет висеть там в течение 180 секунд. Все другие запросы в рамках этого времени будут также отправляться в эту очередь, но они не будут туда добавлены, так как идентификатор этого пользователя уже существует в этой очереди и продублирован он не будет (за это отвечает параметр 'NX'). Так мы отсекаем всю лишнюю нагрузку и каждый пользователь будет генерить не более одного запроса в 3 минуты на обновление поля last_visit.

Теперь возникает вопрос о том, как читать эту очередь. Если методы LPOP и RPOP для листа читают значение и удаляют его из листа атомарно (это значит, что одно и тоже значение не может быть взято несколькими консьюмерами), то sorted set такого метода из коробки не имеет. Мы можем сделать чтение и удаление элемента только двумя последовательными командами. Но мы можем выполнить эти команды атомарно, используя простой LUA скрипт!

public function popFromDelayedQueue(string $queueName){    $command = 'eval "        local val = redis.call(\'ZRANGEBYSCORE\', KEYS[1], 0, ARGV[1], \'LIMIT\', 0, 1)[1]        if val then            redis.call(\'ZREM\', KEYS[1], val)        end        return val"';    return $this->getRedis()->rawCommand($command, 1, $queueName, time());}

В этом LUA скрипте мы пытаемся получить первое значение с весом в диапазоне от 0 до текущего timestamp в переменную val с помощью команды ZRANGEBYSCORE, если нам удалось получить это значение, то удаляем его из sorted set командой ZREM и возвращаем само значение val. Все эти операции выполняются атомарно. Таким образом мы можем вычитывать нашу очередь в консьюмере, аналогично с примером очереди построенной на структуре LIST.

Я рассказал про несколько базовых паттернов очередей, реализованных в нашей системе. На текущий момент у нас в продакшене существуют более сложные механизмы построения очередей линейных, составных, шардированных. При этом Redis позволяет все это делать при помощи смекалки и готовых круто работающих структур из коробки, без сложного программирования.

Блокировки (Mutex)


Mutex (блокировка) это механизм синхронизации доступа к shared ресурсу нескольких процессов, тем самым гарантируя, что только один процесс будет взаимодействовать с этим ресурсом в единицу времени. Этот механизм часто применяется в биллинге и других системах, где важно соблюдать потоковую безопасность (thread safety).

Для реализации mutex на базе Redis прекрасно подойдет стандартный метод SET с дополнительными параметрами:

public function lock(string $key, string $hash, int $ttl = 10): bool{    return (bool)$this->getRedis()->rawCommand('SET', $key, $hash, 'NX', 'EX', $ttl);}

где параметрами для установки mutex являются:
  • $key ключ идентифицирующий mutex;
  • $hash генерируем некую подпись, которая идентифицирует того, кто поставил mutex. Мы же не хотим, чтобы кто-то в другом месте случайно снял блокировку и вся наша логика рассыпалась.
  • $ttl время в секундах, которое мы отводим на блокировку (на тот случай, если что-то пойдет не так, например процесс, поставивший блокировку, по какой-то причине умер и не снял ее, чтобы это блокировка не висела бесконечно).


Основное отличие от метода SET, используемого в механизме кэширования это параметр NX, который говорит Redis о том, что значение, которое уже хранится в Redis по ключу $key, не будет записано повторно. В результате, если в Redis нет значения по ключу $key, туда произведется запись и в ответе мы получим 'OK', если значение по ключу уже есть в Redis, оно не будет туда добавлено (обновлено) и в ответе мы получим NULL. Результат метода lock(): bool, где true блокировка поставлена, false уже есть активная блокировка, создать новую невозможно.

Чаще всего, когда мы пишем код, который пытается работать с shared ресурсом, который заблокирован, мы хотим дождаться его разблокировки и продолжить работу с этим ресурсом. Для этого можем реализовать простой метод для ожидания освободившегося ресурса:

public function tryLock(string $key, string $hash, int $timeout, int $ttl = 10): bool{    $startTime = microtime(true);    while (!this->lock($key, $hash, $ttl)) {        if ((microtime(true) - $startTime) > $timeout) {            return false; // не удалось взять shared ресурс под блокировку за указанный $timeout}usleep(500 * 1000) //ждем 500 миллисекунд до следующей попытки поставить блокировку    }    return true; //блокировка успешно поставлена}

Мы разобрались как ставить блокировку, теперь нам нужно научиться ее снимать. Для того, чтобы гарантировать снятие блокировки тем процессом, который ее установил, нам понадобится перед удалением значения из хранилища Redis, сверить хранимый хэш по этому ключу. Для того, чтобы сделать это атомарно, воспользуемся LUA скриптом:

public function releaseLock(string $key, string $hash): bool{    $command = 'eval "        if redis.call("GET",KEYS[1])==ARGV[1] then            return redis.call("DEL",KEYS[1])        else            return 0        end"';    return (bool) $this->getRedis()->rawCommand($command, 1, $key, $hash);}

Здесь мы пытаемся найти с помощью команды GET значение по ключу $key, если оно равно значению $hash, то удаляем его при помощи команды DEL, которая вернет нам количество удаленных ключей, если же значения по ключу $key не существует, или оно не равно значению $hash, то мы возвращаем 0, что значит блокировку снять не удалось. Базовый пример использования mutex:

class Billing {    public function charge(int $userId, int $amount){        $mutexName = sprintf('billing_%d', $userId);        $hash = sha1(sprintf('billing_%d_%d'), $userId, mt_rand()); //генерим некий хэш запущенного потока        if (!$this->tryLock($mutexName, $hash, 10)) { //пытаемся поставить блокировку в течение 10 секунд            throw new Exception('Не получилось поставить lock, shared ресурс занят');}        //lock получен, процессим бизнес-логику        $this->doSomeLogick();        //освобождаем shared ресурс, снимаем блокировку        $this->releaseLock($mutexName, $hash);}}


Rate limiter


Достаточно частая задача, когда мы хотим ограничить количество запросов к нашему апи. Например на один API endpoint от одного аккаунта мы хотим принимать не более 100 запросов в минуту. Эта задача легко решается с помощью нашего любимого Redis:

public function isLimitReached(string $method, int $userId, int $limit): bool{    $currentTime = time();    $timeWindow = $currentTime - ($currentTime % 60); //Так как наш rate limit имеет ограничение 100 запросов в минуту, //то округляем текущий timestamp до начала минуты  это будет частью нашего ключа,//по которому мы будем считать количество запросов    $key = sprintf('api_%s_%d_%d', $method, $userId, $timeWindow); //генерируем ключ для счетчика, соответственно каждую минуту он будет меняться исходя из $timeWindow    $count = $this->getRedis()->rawCommand('INCR', $key); //метод INCR увеличивает значение по указанному ключу, и возвращает новое значение. //Если ключа не существует, он будут инициализирован со значением 0 и после этого увеличен    if ($count > $limit) { //limit достигнут        return true;    }    return false;} 

Таким простым методом мы можем лимитировать количество запросов к нашему API, базовый каркас нашего контроллера мог бы выглядеть следующим образом:

class FooController {    public function actionBar()    {        if ($this->isLimitReached(__METHOD__, $this->getUserId(), 100)) {            throw new Exception('API method max limit reached');        }        $this->doSomeLogick();    }}


Pub/sub


Pub/sub интересный механизм, который позволяет, с одной стороны, подписаться на канал и получать сообщения из него, с другой стороны отправлять в этот канал сообщение, которое будет получено всеми подписчиками. Наверное у многих, кто работал с вебсокетами, возникла аналогия с этим механизмом, они действительно очень похожи. Механизм pub/sub не гарантирует доставки сообщений, он не гарантирует консистентности, поэтому не стоит его использовать в системах, для которых важны эти критерии. Однако рассмотрим этот механизм на практическом примере. Предположим, что у нас есть большое количество демонизированных команд, которыми мы хотим централизованно управлять. При инициализации нашей команды мы подписываемся на канал, через который будем получать сообщения с инструкциями. С другой стороны у нас есть управляющий скрипт, который отправляет сообщения с инструкциям в указанный канал. К сожалению, стандартный PHP работает в одном блокирующем потоке; для того, чтобы реализовать задуманное, используем ReactPHP и реализованный под него клиент Redis.

Подписка на канал:
class FooDaemon {    private $throttleParam = 10;    public function run()    {        $loop = React\EventLoop\Factory::create(); //инициализируем event-loop ReactPHP        $redisClient = $this->getRedis($loop); //инициализируем клиента Redis для ReactPHP        $redisClient->subscribe(__CLASS__); // подписываемся на нужный нам канал в Redis, в нашем примере название канала соответствует названию класса        $redisClient->on('message', static function($channel, $payload) { //слушаем события message, при возникновении такого события, получаем channel и payload            switch (true) { // Здесь может быть любая логика обработки сообщений, в качестве примера пускай будет так:                case \is_int($payload): //Если к нам пришло число  обновим параметр $throttleParam на полученное значение                    $this->throttleParam = $payload;                    break;                case $payload === 'exit': //Если к нам пришла команда 'exit'  завершим выполнение скрипта                    exit;                default: //Если пришло что-то другое, то просто залогируем это                    $this->log($payload);                    break;            }        });        $loop->addPeriodicTimer(0, function() {            $this->doSomeLogick(); // Здесь в бесконечном цикле может выполняться какая-то логика, например чтение задач из очереди и их процессинг        });        $loop->run(); //Запускаем наш event-loop    }}

Отправка сообщения в канал более простое действие, мы можем сделать это абсолютно из любого места системы одной командой:

public function publishMessage($channel, $message){    $this->getRedis()->publish($channel, $message);}

В результате такой отправки сообщения в канал, все клиенты, которые подписаны на данный канал, получат это сообщение.



Итог


Мы рассмотрели 5 примеров использования Redis на практике, надеюсь что каждый найдет для себя что-то интересное. В нашем стэке технологий Redis занимает важное место, мы любим этот инструмент за его скорость и гибкость. Мы используем Redis в продакшене уже много лет, и он зарекомендовал себя как очень крутой и надежный инструмент, который лежит в основе многих частей нашего продукта. Наш небольшой кластер Redis серверов обрабатывает около 1 миллиона запросов в секунду. А как вы используете Redis в своем проекте? Делитесь опытом в комментариях!
Подробнее..

EPY4еский сервер ASUS с процессором AMD и RAID на DC1000M что ты можешь?

25.01.2021 10:05:20 | Автор: admin
Привет, Хабр! Трудности в выборе сервера для задач компаний, как правило, возникают и у опытных и у начинающих системных администраторов. Ассортимент поставок комплектующих внутри готового решения зачастую едва умещается на нескольких листах. А сервера на базе процессоров AMD EPYC и вовсе считаются диковинными зверями в стойках. Давайте посмотрим, что из себя представляет сервер ASUS RS500A-E10-RS12U в тандеме с накопителями Kingston DC1000M/960G.



Цели Задача Определение




Привычная последовательность при поиске подходящей платформы для пользователей обычно выражается вышенаписанным словосочетанием. Есть база данных, стоящая рядом с сервисом 1С, с виртуализацией или без, в 9 из 10 случаев вам предложат машину на процессоре Intel. Причем для 1С это будет не многоядерная сборка с максимальной частотой. Базу MSQL традиционно отправят на виртуальную машину с несколькими десятками ядер, сотнями гигабайт памяти. Но на этом этапе все забывают о пироге составляющих успеха функционирования такой модели. SQL требователен к системе хранения данных и это в идеале должно быть защищенное место. Самый доступный вариант из серии серверного оборудования это RAID массив из SAS дисков. Неважно, какой уровень вы выберите (естественно, из числа подходящих), SAS диски всегда будут оставаться узким местом, тормозящем SQL. Для десятка пользователей такой подход имеет право на существование, но по мере подключения новых людей, ядра и объем памяти вам не заменят IOPS. Поэтому вместо смены всего модуля хранения в рамках экономии бюджета предпочитают доустановить несколько SSD дисков, поместив их на Temp.db, или, как их еще называют кеш-SQL. И снова модель изживет себя по мере роста количества и сложности запросов со стороны 1С. Правильный, пусть и не дешевый подход это создание массива RAID 10 из SSD дисков под временные файлы и основные базы SQL. В качестве примера возьмем объективно лучший SSD на рынке по цене/скорости NVMe PCIe SSD DC1000M.



Этот накопитель поддерживает шину PCIe Gen 3.0 x4 и создан для условий работы в смешанных нагрузках. Значит в перечень сред его обитания входят:
Виртуализация
Высокопроизводительные облачные сервисы
Кэширование веб-хостинга
Запись и передача медиафайлов высокого разрешения
Рабочие нагрузки ERP, CRM, GL, OLAP, OLTP, ERM, BI и EDW

DC1000M U.2 NVMe компании Kingston выпускается в различных объемах от 960 Гбайт до 7,68 ТБ. Одна из отличительных черт это высокая емкость для хранения данных совместно с лучшей в своем классе производительностью для корпоративного применения. Он способен выдавать до 540 000 IOPS в режиме произвольного чтения (до 3100 МБ/с для операций чтения, 2600 МБ/с для операций записи). Конечно, это Вам не бытовой SSD, в DC1000M соблюдены все высокие требования к качеству обслуживания (QoS) и способен обеспечить предсказуемую производительность при выполнении произвольных операций ввода-вывода, а также предсказуемые значения задержки при выполнении широкого спектра рабочих нагрузок. Особенно стоит отметить предсказуемо низкие значения задержки, стабильно высокую производительность ввода/вывода и встроенную защиту от потери питания (PLP).



Четыре диска DC1000M мы установили в сервер RS500A-E10-RS12U, оснащенный процессором AMD EPYC 7542 (2.9Ггц, 32 ядра, L3 128Мб, 225 ватт), но подключим их не напрямую к процессору, а через контроллер Broadcom MegaRAID 9560-16i. Данная плата имеет ряд преимуществ над встроенными в материнскую плату контроллерами.

Стандартный перечень характеристик включает в себя поддержку 12Gb/s SAS и 6Gb/s SATA дисков. Контроллер подключается через шину PCIe Gen 4.0, что уже соответствует современным стандартам. Было бы странно, если бы производитель не задействовал новый интерфейс, т.к. он действительно необходим серверам. MegaRAID 9560-16i поддерживает Hardware Secure Boot и Universal Backplane Management. На нем создаются RAID уровня 0, 1, 5, 6, 10, 50, и 60 с расширением томов в режиме реального времени без остановки. Во время работы с SSD можно использовать SSD GuardTM технологию, которая постепенно появляется в серверных SSD. С полным перечнем поддерживаемых технологий можно ознакомиться на сайте производителя, мы же доукомплектуем контроллер батарейкой и приступим к тестированию

Тестовая конфигурация




Сервер: ASUS RS500A-E10-RS12U
Процессор: AMD EPYC 7542 (2.9Ггц, 32 ядра, L3 128Мб, 225 ватт);
Память: Kingston Server Premier Registered DDR4 DIMM 32 Гб PC4-25600 х 8 шт. (KSM32RD4 / 32MEI), суммарный объем 256 ГБ;
Загрузочный диск: SSD Kingston DC1000B 240ГБ, оснащенный функцией встроенной защиты от потери питания (PLP);
Контроллер Broadcom MegaRAID 9560-16i + 4 Kingston NVMe PCIe SSD DC1000M 960ГБ объединенные в RAID 10;
Операционная система: Windows Server 2016 + 1C + MSSQL 2017;

Результаты тестов





Для определения входных параметров и правильной настройки программного обеспечения в связке SQL+1C использовался Нагрузочный тест TPC-1C Гилева. Т.к. виртуализация все же влияет на производительность, но без нее не обходится практически ни одна компания, то сервер был введен в состав кластера средствами VMware. Создана одна виртуальная машина, объединившая в себе функции сервера 1С и SQL. MS SQL с базами и кешем был выведен на RAID 10 из DC1000M.



Быстрый тест выдал средние результаты, которыми вряд ли кого-нибудь удивишь. Однако, есть одно большое Но, в представленной конфигурации ASUS RS500A-E10-RS12U, при увеличении количества пользователей до 200 человек, практически не терял отзывчивость на клиентских устройствах (подключение осуществлялось тонким клиентом). Более подробно о субъективных впечатлениях работы сервера с 1С и MS SQL чуть ниже. Мы же плавно переходим к многопоточному тесту производительности 1С.



Данный тест (http://personeltest.ru/away/fragster.ru/perfomanceTest/results.php) создан для оценки производительности связки сервер 1с сервер СУБД в различных вариантах, а также масштабируемости этой связки в режиме многопоточной работы. Он создает множество фоновых сеансов и выполняет ими одинаковые действия, например, создание элементов справочников или запись наборов записей регистров. Это позволяет оценить, насколько производительна связка 1С СУБД, а также насколько она масштабируема, т.е. количество активных пользователей, при котором система еще будет работать, но скорость существенно снизится. На первом графике тест показывает сводную информацию и падение производительности в зависимости от количества активных пользователей. Нормальная скорость, при которой отзывчивость и быстрота выполнения запросов остается комфортной, расположена в диапазоне 400-500 единиц на поток (не учитывая временные таблицы). Потоки генерируются фоновыми заданиями. В нашем случае благодаря сбалансированной связке CPU + Память + быстрый RAID сервер легко переваривает более 112 пользователей. А в однопользовательском режиме результат и вовсе отличный:



Данный тест демонстрирует, насколько меняется распределение времени выполнения одной операции в зависимости от количества потоков, т.е. насколько влияют блокировки и обслуживание служебных таблиц типа итогов, нумераторов и т.п. Хороший показатель выше 500 единиц. Фактически видимое и ощущаемое падение происходит только при 64 потоках, но психологический рубеж в 400-500 единиц не падает и на 112 потоках.



Таким образом, формально не самый лучший процессор для 1С выдерживает более 100 клиентов выдавая хорошую скорость для них. Тесты с фоновыми заданиями все же не отражают объективную реальность, потому как лучшая проверка это запустить пользователей на сервер, что мы и опробовали. Естественно, далее речь пойдет о субъективном ощущении от работы, потому как измерить производительность на настоящих людях невозможно. Но на 240 ГБ базе ERP отличная скорость выполнения запросов сохранялась вплоть до 100-120 пользователей. Далее, при достижении 160-170 человек в он-лайн отзывчивость падала, но не критическим образом. Даже 200 тонких клиентов чувствовали себя хорошо. Настоящая беда пришла с закрытием месяца, когда 200 пользователей ощутили местами заторможенность 1С. Опять же, это субъективные ощущения пользователей, привыкших к весьма быстрым серверам, специально сконфигурированных под задачи 1С+SQL. Платформу AMD EPIC в нашем случае лучше адресовать на задачу MSSQL, не объединяя вместе с сервером 1С. Хотя и в данном примере работа связки стабильно хорошая до 200 пользователей. Причем, ограничивающим фактором далее становится низкочастотный процессор, поднимающийся в бусте всего до 2,9 ГГц. Пропускная способность памяти и системы хранения RAID 10 из 4 SSD Kingston DC1000M выдержит гораздо больше пользователей. Кстати, предлагаю вам посмотреть, насколько шустро работает система хранения



Как всегда, для оценки производительности дисковой системы задействуем сначала простейшие тесты, распространенные среди обзоров SSS это CrystalDiskMark, AS SSD и скорость линейного чтения. Эти данные позволят нам сравнить конфигурацию, состоящую из контроллера Broadcom MegaRAID 9560-16i + 4 Kingston NVMe PCIe SSD DC1000M 960ГБ, объединенные в RAID 10 и бытовых SSD.







Даже при беглом осмотре данных наш сервер вроде бы выдал цифры аналогичные простейшему RAID 0 из пары SSD, но разница скрыта в другом! Типичный пользователь оценивает скорость по операциям последовательного чтения и записи в начале диска с учетом SLC-кеша, а серверу с базой данных важны показатели на всем объеме дисковой системы случайного чтения/записи мелкими блоками.





Квартет SSD DC1000M показывает стабильно высокую скорость линейной и случайной записи, а также чтения, на всем объеме массива. И ему не важно, будете вы занимать 10% или 99% доступного места на диске. В этом и заключается отличие от бытовых накопителей, дополнительно Kingston гарантирует исправную работу SSD. Срок гарантии составляет пять лет, что вполне типично для накопителя, но нагрузка записи заявлена до 1681 ТBW (1 DWPD/5 лет), что весьма немало для памяти TLC.

Итог





Даже если вы отлично разбираетесь в компьютерных комплектующих, понимаете их предназначение, то при переходе на серверные решения у вас обязательно возникнет состояние героя Доброго Ээха из мультфильма Ух ты, говорящая рыба!. Происходящее хорошо выражено фразой: Какой заяц, какой орел, какая блоха!!!. Но не стоит отчаиваться, в интернете полно правильных и неправильных советов. Мы же дадим вам правильный. Подбор сервера и его комплектующих нужно делать под ваши задачи. ASUS RS500A-E10-RS12U с процессорами AMD EPYC интересный комбайн для множества сценариев. Расширяя конфигурацию с помощью контроллера и SSD дисков DC1000M, вы закроете до 80% типичных серверных задач. В данной конфигурации раскрывается еще один приятный момент, а именно умеренное потребление системы в пересчете на производительность. Странно, что серверные процессоры AMD EPYC часто обходят стороной, ссылаясь на недостаточно оптимизированный софт. Но, как показала практика, более 3 месяцев сервер прекрасно выполнял возложенные на него задачи, даже с неподходящим софтом в виде 1С. По части MSSQL/скорости дисковой системы квартет SSD DC1000M превзошел наши требования. Всегда приятно иметь запас прочности в части накопителей ввиду постоянно растущей базы ERP и количества пользователей. Второе распространенное заблуждение совместимость памяти с процессорами AMD EPYC. Ведь для десктопной платформы AM4 выбор и настройка DDR4 памяти дело нелегкое из-за архитектуры работы контроллера в CPU. Вот и кочует мнение, что раз в домашних ПК на это тратится много времени, то в серверном исполнении проблем еще больше



К счастью, с ECC DDR4 памятью Kingston и отточенному BIOS серверу ASUS, все настройки подхватываются автоматически из микросхем SPD. Для этого даже не нужно изучать настройки BIOS. Память встает на свою законную частоту при первом включении, дальнейшие манипуляции с настройками от специалиста не требуются.



И наконец последний, часто задаваемый вопрос от людей, впервые сталкивающихся с серверами: А зачем устанавливать дополнительный SSD DC1000B M.2 NVMe, когда используется RAID 10 из четырех Kingston NVMe PCIe SSD DC1000M?. Ответ лежит на поверхности. На него устанавливается рабочая операционная система, будь то продукт Microsoft, Linux или VMWare. Его отличие от потребительских SSD заключается в поддержке специфических функций, нужных для работы в серверах: встроенная защита от потери питания (PLP), повышенная износоустойчивость (при коэффициенте перезаписи всего объема диска в день (DWPD) 0,5 на 5 лет), привлекательная цена.

Автор: Дмитрий Владимирович aka Rasamaha

Для получения дополнительной информации о продуктах Kingston Technology обращайтесь на официальный сайт компании.
Подробнее..

От монолита к микросервисам ускорили банковские релизы в 15 раз

24.07.2020 12:09:40 | Автор: admin
Бывает, что компания использует устаревшую монолитную IT-систему, с которой сложно быстро выпускать обновления и решать свои бизнес-задачи. Как правило, рано или поздно владелец продукта начинает проектировать новое, более гибкое архитектурное решение.

Недавно мы писали о том, как работают IT-архитекторы, а теперь расскажем подробности об одном из наших кейсов и покажем схему работы системы. В этом проекте мы помогли заменить коробочное банковское приложение на микросервисное ДБО, при этом наладив быстрый выпуск релизов в среднем 1 раз в неделю.



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

Проблемы коробочного монолита


Среди средних и малых компаний востребованы готовые IT-решения, которые можно кастомизировать и выпустить со своим логотипом. Минусы функции приложений из коробки бывают ограничены, а обновления зачастую приходится вносить долго и исключительно через вендора.

Именно такую коробочную систему дистанционного банковского обслуживания (ДБО) использовал один из наших клиентов. Онлайн-банк представлял собой монолитное приложение с достаточно небольшим набором функций.

Для того, чтобы не уступать конкурентам, банку нужно было постоянно внедрять улучшения и новые фичи. Однако, даже для того, чтобы просто передвинуть кнопки в приложении, приходилось обращаться к вендору. Обновления выходили в среднем 1 раз в квартал.

Таким образом, развивать продукт было сложно из-за ряда ограничений:

  1. Вендор вносил желаемые изменения неприемлемо долго, а переписать мобильный интерфейс в конкретной коробке и вовсе было практически невозможно.
  2. Из-за сбоев в шине данных или коробке пользователи зачастую не могли войти в онлайн-банк.
  3. Работать с приложением пользователи могли только в том случае, если у них была установлена самая свежая версия.
  4. Для распределения нагрузки инстансы монолита были развернуты на нескольких серверах, каждый из которых обслуживал свою группу абонентов. При падении одного из серверов пропадал доступ у всех абонентов соответствующей группы.
  5. Вся экспертиза хранилась у производителя коробочного решения, а не в банке.
  6. Из-за того, что банк не мог оперативно внести изменения по просьбам пользователей, а функциональность была недостаточной по сравнению с конкурентами, появился риск оттока клиентов.

В результате банк принял решение постепенно отказаться от коробки и разработать собственное ДБО, с микросервисной архитектурой, чтобы ускорить разработку функций и обеспечить удобство и безопасность для пользователей.

С чего мы начинали


Создание онлайн-банка началось с проектирования верхнеуровневой архитектуры, найма разработчиков в штат и подключения нашей выделенной команды. При этом архитектуру нужно было заложить с запасом прочности, в расчете на будущее расширение.

На старте наша команда Backend-разработчиков занималась реализацией отдельных базовых функций: например, денежными переводами. Однако, у нас был достаточно большой опыт работы с онлайн-банками, один из наших проектов к этому моменту вошел в отраслевой рейтинг Markswebb, поэтому мы предложили банку помощь в проектировании архитектуры и получили зеленый свет.

Архитектура

Вместе с владельцем продукта мы приняли решение использовать Spring Cloud, который предоставляет все необходимые функции для реализации микросервисной архитектуры: это и Service discovery Eureka, Api Gateway Zuul, Config server и многое другое. В качестве системы контейнеров для Docker-образов выбрали OpenShift, потому что инфраструктура банка была заточена под этот инструмент.

Также мы проанализировали, какие особенности старой коробки могут осложнять работу пользователей. Один из основных недостатков заключался в том, что система синхронно работала через шину данных, и каждое действие пользователей вызывало обращение к шине. Из-за больших нагрузок часто происходил отказ шины, при этом переставало работать все приложение. Кроме того, как и во многих старых банковских продуктах, накопилось легаси наследство в виде старого и тяжеловесного CORE АБС, переписывать которое было бы сложно и дорого.

Мы предложили ряд улучшений:

  • Версионирование

Ранее коробка поддерживала только одну версию, а в новом онлайн-банке мы предложили новую архитектуру, которая позволит поддерживать несколько различных версий одновременно и заменять их при необходимости.

Схема версионирования следующая: если минорная версия в сервисе изменилась, то сервис автоматически пересобирается и деплоится, заменяя устаревшую версию. Если мы ставим мажорную версию, то разворачивается новая копия сервиса с новой версией. Таким образом, разработка новых функций с изменением API сервиса не влияет на мобильное приложение, при этом сокращаются сроки тестирования. Такая система позволила обеспечить поддержку даже устаревших версий мобильного приложения, если у пользователя нет возможности обновиться.

  • Асинхронность

Мы реализовали библиотеку, которую можно использовать в сервисах, где требуется асинхронная работа. Библиотека реализует выполнение асинхронных задач, подходит для использования на любом количестве копий сервисов, и они не мешают друг другу. Синхронизация между различными копиями производится с помощью очереди сообщений Kafka.

Это решение помогло повысить стабильность работы приложения. Мобильное приложение теперь не зависит от шины, необходимые пользователю данные мы дублируем в наших сервисах и обновляем их в фоне, когда есть доступ к банковской шине. Все действия пользователя ставятся в очередь на исполнение, по мере готовности приходят PUSH-уведомления о результатах.

  • Кэширование

Чтобы ускорить работу приложения и снизить нагрузку на внутренние банковские ресурсы, организовано кэширование данных с помощью Reddis. В качестве кэша служит KeyDB, который показывает хорошие результаты и совместим со многими системами, использующими Reddis. Данные кэшируются не после запроса пользователя, а при изменении пользовательских данных, что позволяет иметь к ним доступ независимо от внутренних банковских систем.

Что изменилось в системе


Как было отмечено выше, в старом онлайн-банке backend был выполнен в монолитной архитектуре, приложение разворачивалось на отдельной машине. При увеличении нагрузки приходилось разворачивать новые сервера. При падении сервера у некоторых пользователей полностью не работало мобильное приложение.





В новом решении использована микросервисная архитектура, что позволяет разворачивать столько копий сервисов, сколько требуется для того или иного функционала. Мы добавили кэширование данных на основе KeyDB, чтобы не только увеличить скорость получения информации, но и снизить нагрузку на базу данных.



Давайте рассмотрим пример получения данных по счетам пользователя в новой архитектуре. Запрос пользователя из мобильного приложения попадает через балансировщик на Gateway, который понимает, на какой из сервисов направить запрос. Далее попадаем на API сервис счетов. Сервис сперва проверяет, есть ли актуальные данные пользователя в Cache. При успешном исходе возвращает данные, в ином случае отправляет запрос в Middle сервис счетов.

Обновление данных в сервисе может происходить в различных случаях. Например, при непосредственном запросе от пользователя сервис создает задачу на обновление данных, которая выполняется асинхронно, и полученные из ESB данные обновляются. Также сервис получает сообщения из очереди сообщений и реагирует на них. Например, сервис получает сообщение о входе пользователя в приложение и сразу же обновляет данные по счетам, получает данные об операциях со счетом от других сервисов и обновляет у себя данные. Таким образом, пользователь всегда видит актуальные данные по своим счетам.

Среди наиболее важных изменений можно отметить следующие:

  • Гибкость масштабирования

Для увеличения отказоустойчивости системы сервисы распределены по разным серверам. Это позволяет сохранять систему в работоспособном состоянии в случае падения одного из них. Для своевременного реагирования на нестандартные ситуации мы внедрили систему мониторинга, это помогло при необходимости вовремя масштабировать сервисы. Подключили DevOps для настройки с нуля CI/CD на серверах клиента, выстроили процессы развертывания и поддержки будущего приложения на нескольких серверах.

  • Ускорение релизов за счет версионирования

Ранее при обновлении мобильной версии одни пользователи уже применяли новую версию, а другие нет, но число последних было минимально. При разработке нового ДБО мы реализовали версионирование и возможность выпускать релизы без риска, что приложение перестанет работать у большой части клиентов. Сейчас при реализации отдельных функциональностей в новых версиях мы не ломаем старые версии, а значит, нет необходимости проводить регресс. Это помогло ускорить частоту релизов минимум в 15 раз теперь релизы выходят в среднем 1 раз в неделю. Команды Backend и Mobile могут одновременно и независимо работать над новыми функциональностями.

Подводя итоги


В этом примере мы рассказали о проектировании микросервисной архитектуры банка, которая пришла на смену коробочному монолитному решению. Над этим проектом работала распределенная команда, которая включала как штатных разработчиков, так и аутсорсеров.
При разработке онлайн-банка мы стремились к тому, чтобы реализовать в новом приложении такой же объем функций, как в коробочном, и постепенно развивать его.

Для того, чтобы не допустить оттока клиентов, нужно было наладить безотказную работу приложения, без сбоев и простоев, и регулярный выход релизов и обновлений. Осуществить это удалось благодаря улучшениям в архитектуре. В частности, после снижения нагрузки на базу данных мы добились постоянной доступности приложения, а за счет версионирования сократили сроки тестирования, обеспечили возможность независимой работы над функциональностями и выпуска релизов 1 раз в неделю.

Архитектура приложения заложена с учетом дальнейшего роста продукта и инструментов разработки, которые использует банковская инхаус-команда, чтобы владелец продукта мог самостоятельно вносить любые доработки в ДБО. Сроки активной разработки альфа-версии составили около года, еще через 3 месяца вышла бета-версия для всех пользователей.
Надеемся, что описанная схема работы может быть полезна при создании других финтех-продуктов.

Спасибо за внимание!
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru