[Перевод] Ликбез по запуску Istio

shm2jainigjyhqewoqvi4cxdlg0.jpeg
Istio Service Mesh

Мы в Namely уже год как юзаем Istio. Он тогда только-только вышел. У нас здорово упала производительность в кластере Kubernetes, мы хотели распределенную трассировку и взяли Istio, чтобы запустить Jaeger и разобраться. Service mesh так здорово вписалась в нашу инфраструктуру, что мы решили вложиться в этот инструмент.

Пришлось помучиться, но мы изучили его вдоль и поперек. Это первый пост из серии, где я расскажу, как Istio интегрируется с Kubernetes и что мы узнали о его работе. Иногда будем забредать в технические дебри, но не сильно далеко. Дальше будут еще посты.


Что такое Istio?

Istio — это инструмент конфигурации service mesh. Он читает состояние кластера Kubernetes и делает обновление до прокси L7 (HTTP и gRPC), которые реализуются как sidecar-ы в подах Kubernetes. Эти sidecar-ы — контейнеры Envoy, которые читают конфигурацию из Istio Pilot API (и сервиса gRPC) и маршрутизируют по ней трафик. С мощным прокси L7 под капотом мы можем использовать метрики, трассировки, логику повтора, размыкатель цепи, балансировку нагрузки и канареечные деплои.


Начнем с начала: Kubernetes

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

Этапы жизненного цикла API:

dldct5fzrxubc5c1z3zt2m7lchu.png
Спасибо Banzai Cloud за крутую картинку.

Один из этапов — модифицирующие вебхуки допуска. Это отдельная часть жизненного цикла в Kubernetes, где ресурсы кастомизируются до коммита в хранилище etcd — источнике истины для конфигурации Kubernetes. И здесь Istio творит свою магию.


Модифицирующие вебхуки допуска

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

Во время установки Istio добавляется istio-sidecar-injector как ресурс конфигурации модифицирующих вебхуков:

$ kubectl get mutatingwebhookconfiguration
NAME                     AGE
istio-sidecar-injector   87d

И конфигурация:

apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
  labels:
    app: istio-sidecar-injector
    chart: sidecarInjectorWebhook-1.0.4
    heritage: Tiller
  name: istio-sidecar-injector
webhooks:
- clientConfig:
    caBundle: redacted
    service:
      name: istio-sidecar-injector
      namespace: istio-system
      path: /inject
  failurePolicy: Fail
  name: sidecar-injector.istio.io
  namespaceSelector:
    matchLabels:
      istio-injection: enabled
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    resources:
    - pods

Тут написано, что Kubernetes должен отправлять все события создания подов в сервис istio-sidecar-injector в неймспейс istio-system, если в неймспейсе есть ярлык istio-injection=enabled. Инжектор включает в PodSpec еще два контейнера: один временный для настройки правил прокси и один — собственно для проксирования. Sidecar-инжектор вставляет эти контейнеры по шаблону из карты конфигурации istio-sidecar-injector. Этот процесс еще называется sidecaring.


Sidecar-поды

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


Init- и прокси-контейнеры

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

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

@jimmysongio нарисовал отличную схему связи между правилами iptables и прокси Envoy:

yl3myca7qv4esodpj8a7iwsp6bc.jpeg
Трафик Envoy–Envoy

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

Процесс Envoy обнаруживает все маршруты через Envoy v2 API, который реализует Istio.


Envoy и Pilot

У самого Envoy нет никакой логики, чтобы обнаруживать поды и сервисы в кластере. Это плоскость данных и ей нужна плоскость контроля, чтобы руководить. Параметр конфигурации Envoy запрашивает хост или порт сервиса, чтобы получить эту конфигурацию через gRPC API. Istio, через свой сервис Pilot, выполняет требования для gRPC API. Envoy подключается к этому API на основе sidecar-конфигурации, внедренной через модифицирующий вебхук. В API есть все правила трафика, которые нужны Envoy для обнаружения и маршрутизации для кластера. Это и есть service mesh.

lrknbzkkjqescnkyh6k1voy_nyk.png
Обмен данными «под <-> Pilot»

Pilot подключается к кластеру Kubernetes, читает состояние кластера и ждет обновлений. Он следит за подами, сервисами и конечными точками в кластере Kubernetes, чтобы потом дать нужную конфигурацию всем sidecar-ам Envoy, подключенным к Pilot. Это мост между Kubernetes и Envoy.

fgnohjdngxbyn8kj5u_udcvl6x0.png
Из Pilot в Kubernetes

Когда в Kubernetes создаются или обновляются поды, сервисы или конечные точки, Pilot узнает об этом и отправляет нужную конфигурацию всем подключенным экземплярам Envoy.


Какая конфигурация отправляется?

Какую конфигурацию получает Envoy от Istio Pilot?

По умолчанию Kubernetes решает ваши сетевые вопросы с помощью sevice (сервис), который управляет endpoint (конечные точки). Список конечных точек можно открыть командой:

kubectl get endpoints

Это список всех IP и портов в кластере и их адресатов (обычно это поды, созданные из деплоя). Istio важно это знать, чтобы настраивать и отправлять данные о маршрутах в Envoy.


Сервисы, прослушиватели и маршруты

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

curl my-service.default.svc.cluster.local:3000

сначала найдет виртуальный IP, назначенный сервису my-service в неймспейсе default, и этот IP перешлет трафик в под, который соответствует ярлыку сервиса.

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

Термины Kubernetes, Istio и Envoy немного отличаются, и не сразу понятно, что с чем едят.


Сервисы

Сервис в Kubernetes сопоставляется с кластером в Envoy. Кластер Envoy содержит список конечных точек, то есть IP (или имена хостов) экземпляров для обработки запросов. Чтобы увидеть список кластеров, настроенных в sidecar-поде Istio, запустите istioctl proxy-config cluster <имя пода>. Эта команда показывает текущее положение дел с точки зрения пода. Вот пример из одной нашей среды:

$ istioctl proxy-config cluster taxparams-6777cf899c-wwhr7 -n applications
SERVICE FQDN                                                              PORT      SUBSET     DIRECTION     TYPE
BlackHoleCluster                                                          -         -          -             STATIC
accounts-grpc-gw.applications.svc.cluster.local                           80        -          outbound      EDS
accounts-grpc-public.applications.svc.cluster.local                       50051     -          outbound      EDS
addressvalidator.applications.svc.cluster.local                           50051     -          outbound      EDS

Все те же сервисы есть в этом пространстве имен:

$ kubectl get services
NAME                                     TYPE           CLUSTER-IP   EXTERNAL-IP        PORT(S)                         
accounts-grpc-gw                         ClusterIP      10.3.0.91                 80/TCP                          
accounts-grpc-public                     ClusterIP      10.3.0.202                50051/TCP                       
addressvalidator                         ClusterIP      10.3.0.56                 50051/TCP

Как Istio узнает, какой протокол использует сервис? Настраивает протоколы для манифестов сервисов по полю name в записи порта.

$ kubectl get service accounts-grpc-public -o yaml
apiVersion: v1
kind: Service
metadata:
  name: accounts-grpc-public
spec:
  ports:
  - name: grpc
    port: 50051
    protocol: TCP
    targetPort: 50051

Если там grpc или префикс grpc-, Istio настроит для сервиса протокол HTTP2. Мы на горьком опыте узнали, как Istio использует имя порта, когда запороли конфиги прокси, потому что не указали префиксы http или grpc…

Если использовать kubectl и админскую страницу переадресации портов в Envoy, видно, что конечные точки account-grpc-public реализуются Pilot как кластер в Envoy с протоколом HTTP2. Это подтверждает наши предположения:

$ kubectl -n applications port-forward otherpod-dc56885ff-dqc6t 15000:15000 &
$ curl http://localhost:15000/config_dump | yq r -
...
    - cluster:
        circuit_breakers:
          thresholds:
          - {}
        connect_timeout: 1s
        eds_cluster_config:
          eds_config:
            ads: {}
          service_name: outbound|50051||accounts-grpc-public.applications.svc.cluster.local
        http2_protocol_options:
          max_concurrent_streams: 1073741824
        name: outbound|50051||accounts-grpc-public.applications.svc.cluster.local
        type: EDS
...

Порт 15000 — это админская страница Envoy, доступная в каждом sidecar.


Прослушиватели

Прослушиватели узнают конечные точки Kubernetes, чтобы пропускать трафик в поды. У сервиса проверки адреса здесь одна конечная точка:

$ kubectl get ep addressvalidator -o yaml
apiVersion: v1
kind: Endpoints
metadata:
  name: addressvalidator
subsets:
- addresses:
  - ip: 10.2.26.243
    nodeName: ip-10-205-35-230.ec2.internal
    targetRef:
      kind: Pod
      name: addressvalidator-64885ccb76-87l4d
      namespace: applications
  ports:
  - name: grpc
    port: 50051
    protocol: TCP

Поэтому у пода проверки адреса один прослушиватель на порте 50051:

$ kubectl -n applications port-forward addressvalidator-64885ccb76-87l4d 15000:15000 &
$ curl http://localhost:15000/config_dump | yq r -
...
    dynamic_active_listeners:
    - version_info: 2019-01-13T18:39:43Z/651
      listener:
        name: 10.2.26.243_50051
        address:
          socket_address:
            address: 10.2.26.243
            port_value: 50051
        filter_chains:
        - filter_chain_match:
            transport_protocol: raw_buffer
...


Маршруты

В Istio вместо стандартного объекта Kubernetes Ingress берется более абстрактный и эффективный кастомный ресурс — VirtualService. VirtualService сопоставляет маршруты с апстрим-кластерами, привязывая их к шлюзу. Это как использовать Kubernetes Ingress с Ingress-контроллером.

В Namely мы используем Istio Ingress-Gateway для всего внутреннего GRPC-трафика:

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: grpc-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    - '*'
    port:
      name: http2
      number: 80
      protocol: HTTP2
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: grpc-gateway
spec:
  gateways:
  - grpc-gateway
  hosts:
  - '*'
  http:
  - match:
    - uri:
        prefix: /namely.address_validator.AddressValidator
    retries:
      attempts: 3
      perTryTimeout: 2s
    route:
    - destination:
        host: addressvalidator
        port:
          number: 50051

На первый взгляд в примере ничего не разберешь. Тут не видно, но деплой Istio-IngressGateway записывает, какие конечные точки нужны, на основе селектора istio: ingressgateway. В этом примере IngressGateway направляет трафик для всех доменов через порт 80 по протоколу HTTP2. VirtualService реализует маршруты для этого шлюза, сопоставляет по префиксу /namely.address_validator.AddressValidator и передает в апстрим-сервис addressvalidator через порт 50051 с правилом повтора через две секунды.

Если переадресовать порт пода Istio-IngressGateway и посмотреть конфигурацию Envoy, мы увидим, что делает VirtualService:

$ kubectl -n istio-system port-forward istio-ingressgateway-7477597868-rldb5 15000
...
          - match:
              prefix: /namely.address_validator.AddressValidator
            route:
              cluster: outbound|50051||addressvalidator.applications.svc.cluster.local
              timeout: 0s
              retry_policy:
                retry_on: 5xx,connect-failure,refused-stream
                num_retries: 3
                per_try_timeout: 2s
              max_grpc_timeout: 0s
            decorator:
              operation: addressvalidator.applications.svc.cluster.local:50051/namely.address_validator.AddressValidator*
...


Что мы гуглили, копаясь в Istio

Возникает ошибка 503 или 404

Причины разные, но обычно такие:


  • Sidecar-ы приложения не могут связаться с Pilot (проверьте, что Pilot запущен).
  • В манифесте сервиса Kubernetes указан неправильный протокол.
  • Конфигурация VirtualService/Envoy записывает маршрут не в том апстрим-кластере. Начните с edge-сервиса, где ожидаете входящий трафик, и изучите логи Envoy. Или используйте что-то типа Jaeger, чтобы найти ошибки.

Что означает NR/UH/UF в логах прокси Istio?


  • NR — No Route (нет маршрута).
  • UH — Upstream Unhealthy (неработоспособный апстрим).
  • UF — Upstream Failure (сбой апстрима).

Подробности читайте на сайте Envoy.

По поводу высокой доступности с Istio


  • Добавьте NodeAffinity в компоненты Istio для равномерного распределения подов по разным зонам доступности и увеличьте минимальное количество реплик.
  • Запустите новую версию Kubernetes с функцией горизонтального автомасштабирования подов (Horizontal Pod Autoscaling). Самые важные поды будут масштабироваться в зависимости от нагрузки.

Почему Cronjob не завершается?

Когда основная рабочая нагрузка выполнена, sidecar-контейнер продолжает работать. Чтобы обойти проблему, отключите sidecar в cronjobs, добавив аннотацию sidecar.istio.io/inject: "false” в PodSpec.

Как установить Istio?

Мы используем Spinnaker для деплоев, но обычно берем последние Helm-чарты, колдуем над ними, используем helm template -f values.yml и коммитим файлы в Github, чтобы посмотреть изменения, прежде чем применить их через kubectl apply -f -. Это для того, чтобы нечаянно не изменить CRD или API в разных версиях.

Спасибо Bobby Tables и Michael Hamrah за помощь в написании поста.

© Habrahabr.ru