Команда Trendyol Platform разработала решение проблемы межмикросервисного кэширования в Kubernetes. Приводим перевод статьи, где она делится опытом и рассказывает о создании приложения Sidecache.
Зачем нам был нужен этот проект
Мы, команда Trendyol Platform, разработали новую инфраструктуру обнаружения сервисов и после этого отказались от балансировщиков нагрузки при взаимодействии между микросервисами. Но у этого решения мы выявили серьезный недостаток — потеря кэширования.
Вскоре нам снова понадобилось кэшировать ответы в некоторых высоконагруженных сервисах. И поэтому мы стали искать способ, как его реализовать. Мы изучили прокси-сервер Istio/Envoy и выяснили, что у него нет готового решения для кэширования, но есть вот такое.
В качестве альтернативного решения оставался только вариант создать промежуточную структуру. Она должна обращаться к конечной точке кэша, которую мы определили в приложении. Кроме того, мы хотели создать промежуточную структуру для java-приложений.
Получилось вот так.
После этого мы придумали проект для управления операциями с кэшем. В нём возникла проблема паттерна Sidecar.
Что такое проблема паттерна sidecar
Поды, которые мы запускаем в Kubernetes, содержат контейнеры. Обычно мы запускаем один контейнер на под, но kubernetes позволяет нам запускать несколько контейнеров.
Из-за того, что у всех разные языки программирования и фреймворки, логично было разработать общее платформенное решение и сделать его доступным для всех. Поэтому мы и сделали кэш-приложение Sidecar.
Существует три различных базовых шаблона проектирования pod: Sidecar, Ambassador и Adapter.
Подробности вы можете узнать, если изучите тему проектирования многоконтейнерного пода.
Мы решили реализовать прокси паттерн и начали разрабатывать наше приложение. Sidecar на самом базовом уровне должен встречать входящие запросы и перенаправлять их в контейнер приложения в той же сети. Таким образом, мы будем запускать более одного контейнера в одном поде и обеспечивать связь через localhost.
Паттерны кэширования
Как мы все знаем, кэширование нужно, чтобы минимизировать запросы к базе данных и обеспечить большую пропускную способность. Давайте взглянем на некоторые существующие схемы кэширования.
Встроенный кэш. Это метод, при котором операции с кэшем управляются из приложения.
Кэш «клиент-сервер». В этом случае запросы к кэшу перенаправляются на внешний кэш-сервер из приложения.
Sidecar кэш. Этот метод, специфичный для Kubernetes, который можно рассматривать как комбинацию встроенных и клиент-серверных методов. Приложение отвечает на запрос и отправляет запрос в sidecar для операций кэширования.
Обратный прокси-кэш. Это метод, при котором операции кэширования управляются обратным прокси-сервером (nginx, haproxy и т.д.).
Обратный прокси-кэш в Sidecar. Этот метод мы и реализовали. Здесь sidecar встречает запрос и перенаправляет его на контейнер приложения, если кэш в самом Sidecar отсутствует.
В качестве решения мы предпочли применить кэш обратного прокси-сервера. Мы будем направлять входящие GET-запросы на разработанный нами sidecar-кэш, и возвращать на сторону клиента ответ, который мы кэшировали ранее. Некэшированные запросы мы будем пересылать в контейнер, где работает наше основное приложение (проксирование). Поскольку контейнеры в поде работают в одном сетевом нэймспейсе, связь по локальному интерфейсу будет происходить очень быстро и с минимальной задержкой по сравнению с запросом, отправляемым во внешнюю систему. После этого остается только решить, как мы будем направлять входящие запросы и разработать приложение.
Переадресация запросов с помощью Istio
Итак, нам необходимо направить входящий запрос в кэш-контейнер или контейнер, в котором находится наше основное приложение, в зависимости от предопределенного правила. Как это можно сделать? Так как мы используем Istio для service mesh, то решили проверить можем ли мы с помощью него сделать такой роутинг запросов.
На этом этапе давайте рассмотрим некоторые из доступных методов.
Lua-фильтр
Мы можем использовать функцию LuaFilter в Istio, которая позволяет перехватывать как входящий запрос, так и возвращаемые ответы. Управлять процессом перехвата можно через вспомогательный прокси-сервер Istio.
В качестве примера напишем LuaFilter, который возвращает 400 для каждого входящего запроса и добавляет к ответу собственный заголовок:
apiVersion: v1
items:
- apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: sample-lua
namespace: foo
selfLink: /apis/networking.istio.io/v1alpha3/namespaces/foo/envoyfilters/sample-lua
spec:
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
listener:
filterChain:
filter:
name: envoy.http_connection_manager
subFilter:
name: envoy.router
patch:
operation: INSERT_BEFORE
value:
config:
inlineCode: |
function envoy_on_request(request_handle)
request_handle:respond(
{[":status"] = "400"},
"bad request")
end
function envoy_on_response(response_handle)
response_handle:headers():add("foo", "bar")
end
name: envoy.lua
workloadSelector:
labels:
app: nginx
kind: List
metadata:
resourceVersion: ""
selfLink: ""
В этом примере мы видим, что можем перехватывать входящие и исходящие запросы с помощью LuaFilter. Если бы мы хотели, то могли бы направлять входящие запросы другим адресатам или продолжать возвращать поток клиенту.
Подробностям о LuaFilter можно найти здесь.
Обратите внимание, что транзакции, которые вы будете совершать с помощью LuaFilter, не блокируются.
Не блокируйте операции скриптами. Для производительности крайне важно, чтобы API-интерфейсы Envoy использовались для всех операций ввода-вывода.
EnvoyFilter
Другой метод — непосредственно через прокси-сервер Envoy вместо Lua-скрипта. Этот способ немного сложнее, чем LuaFilter, но мы можем пересылать запросы на разные адреса.
Например, давайте развернём nginx и httpbin. Допустим, мы хотим пересылать запросы с nginx:8080
на httpbin:8080
. Эта EnvoyFilter-конфигурация позволит направлять входящие запросы в другой контейнер и модуль. В качестве примера мы добавляем к ответу собственный заголовок.
Давайте взглянем на EnvoyFilter:
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: nginx-routing
namespace: default
spec:
workloadSelector:
labels:
app: nginx
configPatches:
- applyTo: HTTP_ROUTE
match:
context: SIDECAR_INBOUND
routeConfiguration:
name: "inbound|8080||nginx.default.svc.cluster.local"
portNumber: 8080
patch:
operation: MERGE
value:
name: nginx_route
route:
cluster: outbound|8080||httpbin.default.svc.cluster.local
decorator:
operation: "httpbin:8080/*"
response_headers_to_add:
- header:
key: custom-response-header
value: "true"
После того как вы применили эту конфигурацию к кластеру из каждого пода выполните команду:
curl nginx:8080/headers -v
Когда мы сделаем запрос, мы увидим, что вместо ответа nginx по умолчанию приходит ответ httpbin.
VirtualService
Теперь давайте рассмотрим концепцию VirtualService.
В Istio мы определяем маршрутизацию service mesh с помощью пользовательских ресурсов VirtualService. То, что мы можем определить правила сопоставления VirtualService даёт нам некоторую гибкость. Например, мы можем сделать так, чтобы GET-запросы, поступающие по пути xxx/yyy, перенаправлялись на порт 9191, а остальные запросы — на 8084. Реализовать этот способ проще, чем EnvoyFilter, в то же время он позволяет менеджерить injection, retrying и timeout.
Из-за этих преимуществ мы решили настроить маршрутизацию запросов с помощью VirtualService.
Теперь давайте в качестве примера напишем VirtualService и направим входящие GET-запросы на наш sidecar-кэш, а другие запросы — на контейнер приложения.
Пример определения VirtualService:
Маршрут можно определить двумя способами. Один из них — на порт основного приложения (8080), а другой — на порт cache sidecar (9191). По определению здесь все запросы на получение будут проходить через дополнительный контейнер кэша.
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: foo
spec:
gateways:
- foo-gateway
hosts:
- foo
http:
- match:
- method:
exact: GET
route:
- destination:
host: foo
port:
number: 9191
- route:
- destination:
host: foo
port:
number: 8080
Проект SideCache
Мы разработали приложение для управления всеми этими операциями с кэшем на языке go. Это приложение, которое действует как обратный прокси-сервер, использует Couchbase в качестве хранилища для кэширования ответов и возврата входящих запросов через кэш. Если приходит ранее не кэшированный запрос, то он отправляет запрос в основное приложение. Какое значение ответа будет кэшироваться, определяется пользовательским заголовком, возвращаемым API. Это значение заголовка имеет длительность TTL кэша.
Мы реализовали операцию проксирования, используя структуру ReverseProxy в пакете httputil на языке go.
Вы можете использовать проект SideCache для своих микросервисов. Ссылка на github.
Admission Webhook и Dynamic Sidecar Injection
Мы стремимся, чтобы другие команды использовали наше приложение с минимальными изменениями. Поэтому мы создали 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 часов практики на стендах, групповой чат с куратором и итоговая сертификация.
Посмотреть программу курса и оставить заявку ???? на нашем сайте.