Мы в 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:
Спасибо 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:
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.
Pilot подключается к кластеру Kubernetes, читает состояние кластера и ждет обновлений. Он следит за подами, сервисами и конечными точками в кластере Kubernetes, чтобы потом дать нужную конфигурацию всем sidecar-ам Envoy, подключенным к Pilot. Это мост между Kubernetes и Envoy.
Когда в 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 <none> 80/TCP
accounts-grpc-public ClusterIP 10.3.0.202 <none> 50051/TCP
addressvalidator ClusterIP 10.3.0.56 <none> 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 за помощь в написании поста.