Команда Trendyol Platform разработала решение проблемы межмикросервисного кэширования в Kubernetes. Приводим перевод статьи, где она делится опытом и рассказывает о создании приложения Sidecache. Мы, команда Trendyol Platform, разработали новую инфраструктуру обнаружения сервисов и после этого отказались от балансировщиков нагрузки при взаимодействии между микросервисами. Но у этого решения мы выявили серьезный недостаток — потеря кэширования.  Вскоре нам снова понадобилось кэшировать ответы в некоторых высоконагруженных сервисах. И поэтому мы стали искать способ, как его реализовать. Мы изучили прокси-сервер Istio/Envoy и выяснили, что у него нет готового решения для кэширования, но есть вот такое. В качестве альтернативного решения оставался только вариант создать промежуточную структуру. Она должна обращаться к конечной точке кэша, которую мы определили в приложении. Кроме того, мы хотели создать промежуточную структуру для java-приложений.  Получилось вот так. После этого мы придумали проект для управления операциями с кэшем. В нём возникла проблема паттерна Sidecar.  Поды, которые мы запускаем в Kubernetes, содержат контейнеры. Обычно мы запускаем один контейнер на под, но kubernetes позволяет нам запускать несколько контейнеров. Из-за того, что у всех разные языки программирования и фреймворки, логично было разработать общее платформенное решение и сделать его доступным для всех. Поэтому мы и сделали кэш-приложение Sidecar. Существует три различных базовых шаблона проектирования pod: Sidecar, Ambassador и Adapter. Подробности вы можете узнать, если изучите тему проектирования многоконтейнерного пода. Мы решили реализовать прокси паттерн и начали разрабатывать наше приложение. Sidecar на самом базовом уровне должен встречать входящие запросы и перенаправлять их в контейнер приложения в той же сети. Таким образом, мы будем запускать более одного контейнера в одном поде и обеспечивать связь через localhost. Как мы все знаем, кэширование нужно, чтобы минимизировать запросы к базе данных и обеспечить большую пропускную способность. Давайте взглянем на некоторые существующие схемы кэширования. Встроенный кэш. Это метод, при котором операции с кэшем управляются из приложения. Кэш «клиент-сервер». В этом случае запросы к кэшу перенаправляются на внешний кэш-сервер из приложения. Sidecar кэш. Этот метод, специфичный для Kubernetes, который можно рассматривать как комбинацию встроенных и клиент-серверных методов. Приложение отвечает на запрос и отправляет запрос в sidecar для операций кэширования. Обратный прокси-кэш. Это метод, при котором операции кэширования управляются обратным прокси-сервером (nginx, haproxy и т.д.). Обратный прокси-кэш в Sidecar. Этот метод мы и реализовали. Здесь sidecar встречает запрос и перенаправляет его на контейнер приложения, если кэш в самом Sidecar отсутствует. В качестве решения мы предпочли применить кэш обратного прокси-сервера. Мы будем направлять входящие GET-запросы на разработанный нами sidecar-кэш, и возвращать на сторону клиента ответ, который мы кэшировали ранее. Некэшированные запросы мы будем пересылать в контейнер, где работает наше основное приложение (проксирование). Поскольку контейнеры в поде работают в одном сетевом нэймспейсе, связь по локальному интерфейсу будет происходить очень быстро и с минимальной задержкой по сравнению с запросом, отправляемым во внешнюю систему. После этого остается только решить, как мы будем направлять входящие запросы и разработать приложение. Итак, нам необходимо направить входящий запрос в кэш-контейнер или контейнер, в котором находится наше основное приложение, в зависимости от предопределенного правила. Как это можно сделать? Так как мы используем Istio для service mesh, то решили проверить можем ли мы с помощью него сделать такой роутинг запросов. На этом этапе давайте рассмотрим некоторые из доступных методов. Мы можем использовать функцию LuaFilter в Istio, которая позволяет перехватывать как входящий запрос, так и возвращаемые ответы. Управлять процессом перехвата можно через вспомогательный прокси-сервер Istio. В качестве примера напишем LuaFilter, который возвращает 400 для каждого входящего запроса и добавляет к ответу собственный заголовок: В этом примере мы видим, что можем перехватывать входящие и исходящие запросы с помощью LuaFilter. Если бы мы хотели, то могли бы направлять входящие запросы другим адресатам или продолжать возвращать поток клиенту. Подробностям о LuaFilter можно найти здесь. Обратите внимание, что транзакции, которые вы будете совершать с помощью LuaFilter, не блокируются. Не блокируйте операции скриптами. Для производительности крайне важно, чтобы API-интерфейсы Envoy использовались для всех операций ввода-вывода. Другой метод — непосредственно через прокси-сервер Envoy вместо Lua-скрипта. Этот способ немного сложнее, чем LuaFilter, но мы можем пересылать запросы на разные адреса. Например, давайте развернём nginx и httpbin. Допустим, мы хотим пересылать запросы с nginx:8080 на httpbin:8080. Эта EnvoyFilter-конфигурация позволит направлять входящие запросы в другой контейнер и модуль. В качестве примера мы добавляем к ответу собственный заголовок. Давайте взглянем на EnvoyFilter: После того как вы применили эту конфигурацию к кластеру из каждого пода выполните команду: Когда мы сделаем запрос, мы увидим, что вместо ответа nginx по умолчанию приходит ответ httpbin. Теперь давайте рассмотрим концепцию VirtualService. В Istio мы определяем маршрутизацию service mesh с помощью пользовательских ресурсов VirtualService. То, что мы можем определить правила сопоставления VirtualService даёт нам некоторую гибкость. Например, мы можем сделать так, чтобы GET-запросы, поступающие по пути xxx/yyy, перенаправлялись на порт 9191, а остальные запросы — на 8084. Реализовать этот способ проще, чем EnvoyFilter, в то же время он позволяет менеджерить injection, retrying и timeout. Подробнее о VirtualService Из-за этих преимуществ мы решили настроить маршрутизацию запросов с помощью VirtualService. Теперь давайте в качестве примера напишем VirtualService и направим входящие GET-запросы на наш sidecar-кэш, а другие запросы — на контейнер приложения. Пример определения VirtualService: Маршрут можно определить двумя способами. Один из них — на порт основного приложения (8080), а другой — на порт cache sidecar (9191). По определению здесь все запросы на получение будут проходить через дополнительный контейнер кэша. Мы разработали приложение для управления всеми этими операциями с кэшем на языке go. Это приложение, которое действует как обратный прокси-сервер, использует Couchbase в качестве хранилища для кэширования ответов и возврата входящих запросов через кэш. Если приходит ранее не кэшированный запрос, то он отправляет запрос в основное приложение. Какое значение ответа будет кэшироваться, определяется пользовательским заголовком, возвращаемым API. Это значение заголовка имеет длительность TTL кэша. Мы реализовали операцию проксирования, используя структуру ReverseProxy в пакете httputil на языке go. Вы можете использовать проект SideCache для своих микросервисов. Ссылка на github. Мы стремимся, чтобы другие команды использовали наше приложение с минимальными изменениями. Поэтому мы создали Mutating Admission Webhook в kubernetes. Команды, которые хотят добавить SideCache в свой проект, могут не переделывать манифесты вручную. Для этого достаточно просто добавить аннотацию к манифестам. С помощью них можно будет убедиться, что контейнер внедрён. С помощью Admission Webhook мы можем отслеживать ресурсы kubernetes и делать validation&mutation. Если мы находим нужную аннотацию в запросах deployments на создание и обновление, то добавляем sidecar кэш-контейнер в deployments. Подробную информацию о структуре Admission Webhook вы можете найти в этой статье. Рассмотрим некоторые метрики проектов с SideCache. В первой из приведенных ниже метрик мы видим изменения в ответах API с момента добавления SideCache. Снижение пропускной способности и снижение скорости поиска показывают, что ответы поступают непосредственно из кэша. Таким образом, приложения могут потреблять меньше ресурсов и выполнять больше запросов. Во второй метрике мы видим, что 26% входящих запросов возвращаются из кэша (это значение может варьироваться, так как API определяет ответ, который нужно кэшировать). Надеюсь наш опыт был полезен. До встречи! Узнайте, как работает Kubernetes изнутри, и научитесь решать стратегические проблемы управления инфраструктурой на курсе Kubernetes: Мега в Слёрме. Вы изучите тонкости установки и конфигурации production-ready кластера («the-not-so-easy-way»), механизмы обеспечения стабильности и безопасности, отказоустойчивости приложений и масштабирование. Чему можно научиться:  создавать отказоустойчивый кластер в ручном режиме; настраивать авторизации в кластере; настраивать autoscaling; делать резервное копирование;  работать с Stateful приложениями в кластере; интегрировать Kubernets и Vault для хранения секретов; делать ротации сертификатов в кластере; альтернативным практикам деплоя; настраивать Service mesh. В курсе 7 онлайн-встреч со спикерами по 1-1,5 часа, более 6 часов практики на стендах, групповой чат с куратором и итоговая сертификация.  Посмотреть программу курса и оставить заявку 👉 на нашем сайте.