18.33
Rating
Uma.Tech
Uma.Tech создает IT-инновации для медиабизнеса.

Как быстро и просто ускорить доступ к API приложениям?

Uma.Tech corporate blogMachine learning
Ответ прост: используя проверенные инструменты, такие как кэширование и горизонтальное масштабирование. Сразу скажем, что это инструменты не единственные, но чаще всего именно проверенные классические подходы оказываются наиболее действенные даже в современных условиях. Рассмотрим практический пример.

Об исходной задаче


Видеоплатформа PREMIER, как и положено современному ресурсу, создала для своих клиентов рекомендательный сервис, построенный на машинном обучении. К видеоплатформе обращается много пользователей — порядка миллиона в день, PREMIER очень популярен — причем обращения идут как через веб-форму, с приложений для мобильных устройств и со Smart TV.

Исходные данные, на основании которых работает машинное обучение нашего сервиса, хранятся в колончатой СУБД ClickHouse. По расписанию в фоновом режиме идет обработка данных для построения моделей (которые будут применены для выдачи финальных рекомендаций). Результаты расчетов сохраняются в реляционной СУБД PostgreSQL.

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



О кэшировании


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

Емкость кэша ограничена — чем быстрее хранилище, тем оно дороже — поэтому его нужно использовать эффективно. Конечно, теоретически можно обойтись и вовсе без кэширования, если ваше основное хранилище достаточно быстрое. Но это будет экономически невыгодно, т.к. придется провести значительный апгрейд оборудования, часто сводящийся к увеличению оперативной памяти и/или замене дисков с HDD на SSD. Т.е. значительно повысить требования к инфраструктуре, что повлияет на экономические параметры всего создаваемого приложения. Без проверенного временем кэширования в большинстве случаев создать массовый продукт вряд ли не получится.

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

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

Кэширование в рекомендательном приложении




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

Redis — это быстрое хранилище значений ключей. Оно повышает эффективность работы с данными, т.к. становится возможным задание структуры. обеспечивает детальный контроль над инвалидацией и вытеснением, позволяя выбирать из шести различных политик. Redis поддерживает как ленивое, так и активное вытеснение, а также вытеснение по времени. Использование структур данных Redis может дать ощутимую оптимизацию, в зависимости от бизнес-сущностей. Например, вместо хранения объектов в виде сериализованных строк разработчики могут использовать структуру данных “хэш” для хранения полей и значений и управления ими по ключу. Hash избавляет от необходимости извлекать всю строку, десериализовать ее и заменять в кэше новым значением при каждом обновлении, что означает снижение потребления ресурсов и повышение производительности. Другие структуры данных, предлагаемые Redis (“листы”, “сеты”, “сортированные сеты”, “гиперлоги”, “битовые карты” и “геоиндексы”), могут использоваться для реализации еще более сложных сценариев. Sorted sets для анализа данных временных рядов предлагают сниженную сложность и объем при обработке и передаче данных. Структура данных HyperLogLog может использоваться для подсчета уникальных элементов в наборе, используя только небольшой постоянный объем памяти, в частности 12 КБ для каждого HyperLogLog (плюс несколько байтов для самого ключа). Значительная часть из около 200 команд, доступных в Redis, посвящена операциям обработки данных и встраиванию логики в саму базу данных с помощью скриптов на Lua. Встроенные команды и возможность скриптинга обеспечивают гибкость в задачах обработки данных непосредственно в Redis без необходимости пересылки данных по сети в ваше приложение, снижая накладные расходы при реализации дополнительной логики кэширования. Доступность данных кэша сразу после перезапуска позволяет значительно сократить время “прогрева кэша” и снять нагрузку, связанную с повторным перерасчетом содержимого кэша из основного хранилища данных. Об особенностях конфигурации Redis и перспективах кластеризации мы поговорим в следующих статьях.

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

Базовый и распространенный способ поддержания актуальности данных — это устаревание по времени. Этот способ, учитывая периодическое централизованное обновление данных рекомендательного приложения, используем и мы. Однако, тут не все так просто как кажется на первый взгляд: в этом случае крайне важно следить за ранжированием, чтобы в кэш попадали только действительно популярные данные. Это становится возможным благодаря сбору статистики запросов и реализации «предварительного прогрева данных», т.е. предзагрузки данных в кэш на старте приложения. Управление размером кэша также является важным аспектом кэширования. В нашем приложении генерируется порядка миллионов рекомендаций, соответственно, нереально сохранить все эти данные в кэше. Управление размером кеша выполняется путем удаления данных из кеша, чтобы освободить место для новых данных. Есть несколько стандартных методов: TTL, FIFO, LIFO, last accessed. Пока мы используем TTL, т.к. инстанс Redis не выходит за границы выделенных ресурсов памяти и диска.

Напомним, что кэш бывает разный. Чаще всего, выделяют две категории: со сквозной записью и отложенной. В первом случае запись выполняется синхронно как в кэш, так и в основное хранилище. Во втором — изначально запись выполняется только в кэш, а запись в основное хранилище будет отложена до тех пор, пока измененные данные не будут заменены другим блоком кэша. Кэш с обратной записью сложнее в реализации, поскольку тут нужно отслеживать данные в кэше для последующей записи в основное хранилище при удалении из кэша. Мы используем первый вариант совместно с процедурой «прогрева кэша». Заметим, что «прогрев» сам по себе является важной и сложной задачей, о нашем решении которой поговорим в следующих статьях.

Горизонтальное масштабирование в рекомендательном приложении


Для организации доступа PREMIER к приложению рекомендаций мы используем протокол HTTP, который часто является основной опцией при взаимодействии приложений. Он подходит для организации взаимодействия между приложениями, особенно, если в качестве инфраструктурного окружения используется Kubernetes и Ingress Controller. Применение Kubernetes позволяет упростить масштабирование. Инструмент способен автоматически балансировать запрос между подами в кластере для равномерной работы, упрощая задачу масштабирования для разработчиков. За это отвечает модуль Ingress Controller, который определяет правила внешнего подключения к приложениям в Kubernetes. По умолчанию приложения в Kubernetes не доступны из внешней сети. Для предоставления внешнего доступа к приложениям необходимо объявить ресурс Ingress, который поддерживает автоматическую балансировку. Мы используем Nginx Ingress Controller, который поддерживает SSL / TLS, URI rewrite rules, а также VirtualServer и VirtualServerRoute для маршрутизации запросов в разные приложения в зависимости от URI и заголовка хоста.

Базовая конфигурация в Ingress Controller позволяет использовать только основные функции Nginx — это маршрутизация на основе хоста и пути, а дополнительные функции, такие как URI rewrite rules, дополнительные заголовки ответа, время ожидания соединения недоступны. Аннотации, применяемые к ресурсу Ingress, позволяют использовать функции самого Nginx (обычно, доступные через конфигурацию самого приложения) и менять поведение Nginx для каждого ресурса Ingress.

Мы планируем применять Nginx Ingress Controller не только в рассматриваемом сейчас проекте, но и в ряде других приложений, про которые еще расскажем. Про это поговорим в следующих статьях.



Риски и последствия применения кэширования


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

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

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

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

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

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

Ссылки:

Tags:машинное обучениесубдclickhousepostgresqlкэшированиеredismemcachedhyperloglogluattlfifokubernetesingress controllerssl/tls
Hubs: Uma.Tech corporate blog Machine learning
+4
1.4k 9
Leave a comment

Information

Location
Россия
Website
uma.tech
Employees
Unknown
Registered