[Перевод] Кэширование обмена данными между сервисами в Kubernetes и Istio

Команда Trendyol Platform разработала решение проблемы межмикросервисного кэширования в Kubernetes. Приводим перевод статьи, где она делится опытом и рассказывает о создании приложения Sidecache.

9d4000c99d3b30df3561c13b5d36ecf4.png

Зачем нам был нужен этот проект

Мы, команда Trendyol Platform, разработали новую инфраструктуру обнаружения сервисов и после этого отказались от балансировщиков нагрузки при взаимодействии между микросервисами. Но у этого решения мы выявили серьезный недостаток — потеря кэширования. 

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

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

Получилось вот так.

После этого мы придумали проект для управления операциями с кэшем. В нём возникла проблема паттерна Sidecar. 

Что такое проблема паттерна sidecar

Поды, которые мы запускаем в Kubernetes, содержат контейнеры. Обычно мы запускаем один контейнер на под, но kubernetes позволяет нам запускать несколько контейнеров.

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

Существует три различных базовых шаблона проектирования pod: Sidecar, Ambassador и Adapter.

ИсточникИсточник

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

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

Паттерны кэширования

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

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

Кэш «клиент-сервер». В этом случае запросы к кэшу перенаправляются на внешний кэш-сервер из приложения.

Sidecar кэш. Этот метод, специфичный для Kubernetes, который можно рассматривать как комбинацию встроенных и клиент-серверных методов. Приложение отвечает на запрос и отправляет запрос в sidecar для операций кэширования.

Обратный прокси-кэш. Это метод, при котором операции кэширования управляются обратным прокси-сервером (nginx, haproxy и т.д.).

Обратный прокси-кэш в Sidecar. Этот метод мы и реализовали. Здесь sidecar встречает запрос и перенаправляет его на контейнер приложения, если кэш в самом 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.

Теперь давайте в качестве примера напишем 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 часов практики на стендах, групповой чат с куратором и итоговая сертификация. 

Посмотреть программу курса и оставить заявку

© Habrahabr.ru