Сегодня рассмотрим, что такое Istio, и как его использовать.
Для начала разберемся, что такое Service Mesh, и кому он нужен.
Service Mesh (с англ. «сервисная сетка») — слой архитектуры, отвечающий за надежную доставку запросов через сложную сеть микросервисов.
Когда ваше приложение выросло из монолита в микросервисную архитектуру, то с каждым днем становится все сложнее ею управлять и мониторить. В таком случае вам необходимо переходить на решения, которые решают часть проблем, связанных с микросервисами:
- балансировка нагрузки внутри микросервисной сетки;
- обнаружение сервисов (Service discovery);
- восстановление после сбоев (Failure recovery);
- метрики;
- мониторинг.
Также решают более сложные задачи:
- A/B тестирование;
- канареечные выкатки (Canary rollouts);
- контроль доступа (Access control);
- сквозная аутентификация (end-to-end authentication).
Тут на помощь приходит Istio, созданный компаниями Google, IBM и Lyft.
Основные возможности Istio:
- управление трафиком: таймауты, повторные попытки, балансировка нагрузки;
- безопасность: аутентификация и авторизация;
- наблюдаемость: трассировка, мониторинг;
Архитектура Istio:
Istio Service Mesh логически разделен на data plane и control plane.
- Data plane — состоит из набора «умных» прокси, развернутых как sidecars. Эти прокси обеспечивают и контролируют всю сетевую связь между сервисами вместе c Mixer центром политик и телеметрии;
- Control Plane — настраивает прокси для маршрутизации трафика. Также control plane настраивает Mixer для применения политик и сбора телеметрии.
Общая схема архитектуры Istio:
Компоненты:
- Envoy — это высокопроизводительный прокси, разработанный на C ++ для передачи всего входящего и исходящего трафика для всех сервисов;
- Mixer — обеспечивает контроль доступа и политики использования в сети сервисов и собирает данные телеметрии с прокси-сервера Envoy и других сервисов;
- Pilot — обеспечивает service discovery для Envoy sidecar, дает возможности для интеллектуальной маршрутизации трафика (например, A/B-тесты, канареечные развертывания) и отказоустойчивость (тайм-ауты, повторные попытки, circuit breakers);
- Citadel — обеспечивает надежную service-to-service и end-user аутентификацию;
- Galley — является компонентом валидации конфигурации Istio. Он отвечает за изоляцию остальных компонентов Istio от конфигурации пользователя базовой платформы.
Маршрутизация и конфигурация трафика
Модель маршрутизации и конфигурации трафика Istio использует следующие ресурсы API:
- Virtual services — настраивает правила для маршрутизации трафика Envoy внтури нашего service mesh;
- Destination rules — настраивает политики для после применений правил маршрутизаций в Virtual services;
- Gateways — для настройки способ распределения нагрузки Envoy (HTTP, TCP или gRPC);
- Service entries — для настройки внешних зависимостей сетки.
Установка и настройка istio
Будем запускать istio в Google Kubernetes Engine (GKE). Cоздаем кластер:
1 2 3 4 5 |
gcloud container clusters create <cluster-name> \ --cluster-version latest \ --num-nodes 4 \ --zone <zone> \ --project <project-id> |
Получаем ключи доступа (далее credentials):
1 2 3 |
gcloud container clusters get-credentials <cluster-name> \ --zone <zone> \ --project <project-id> |
Даем права администратора нашему пользователю:
1 2 3 |
kubectl create clusterrolebinding cluster-admin-binding \ --clusterrole=cluster-admin \ --user=$(gcloud config get-value core/account) |
После подготовки кластера, переходим к установке компонентов Istio. Скачиваем последнюю версию, на момент написания статьи версия Istio 1.3.0:
1 |
curl -L https://git.io/getLatestIstio | ISTIO_VERSION=1.3.0 sh - |
Переходим в директорию с Istio:
1 |
cd istio-1.3.0 |
Устанавливаем Custom Resource Definitions (CRDs) для Istio с помощью kubectl
1 |
for i in install/kubernetes/helm/istio-init/files/crd*yaml; do kubectl apply -f $i; done |
После установки CRD, установим сам Isito cotrol plane c mutual TLS аутентификацией:
1 |
kubectl apply -f install/kubernetes/istio-demo-auth.yaml |
Проверяем сервисы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE grafana ClusterIP 10.43.242.92 <none> 3000/TCP 4m1s istio-citadel ClusterIP 10.43.252.216 <none> 8060/TCP,15014/TCP 3m58s istio-egressgateway ClusterIP 10.43.254.22 <none> 80/TCP,443/TCP,15443/TCP 4m2s istio-galley ClusterIP 10.43.244.7 <none> 443/TCP,15014/TCP,9901/TCP 4m3s istio-ingressgateway LoadBalancer 10.43.253.1 34.69.43.198 15020:31384/TCP,80:31380/TCP,443:31390/TCP,31400:31400/TCP,15029:30472/TCP,15030:32532/TCP,15031:30101/TCP,15032:30948/TCP,15443:30384/TCP 4m1s istio-pilot ClusterIP 10.43.250.244 <none> 15010/TCP,15011/TCP,8080/TCP,15014/TCP 3m59s istio-policy ClusterIP 10.43.242.33 <none> 9091/TCP,15004/TCP,15014/TCP 4m istio-sidecar-injector ClusterIP 10.43.244.233 <none> 443/TCP,15014/TCP 3m58s istio-telemetry ClusterIP 10.43.253.8 <none> 9091/TCP,15004/TCP,15014/TCP,42422/TCP 3m59s jaeger-agent ClusterIP None <none> 5775/UDP,6831/UDP,6832/UDP 3m43s jaeger-collector ClusterIP 10.43.250.60 <none> 14267/TCP,14268/TCP 3m43s jaeger-query ClusterIP 10.43.242.192 <none> 16686/TCP 3m43s kiali ClusterIP 10.43.242.83 <none> 20001/TCP 4m prometheus ClusterIP 10.43.241.166 <none> 9090/TCP 3m59s tracing ClusterIP 10.43.245.22 <none> 80/TCP 3m42s zipkin ClusterIP 10.43.248.101 <none> 9411/TCP 3m42s |
и поды:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
NAME READY STATUS RESTARTS AGE grafana-6fc987bd95-t4pwj 1/1 Running 0 4m54s istio-citadel-679b7c9b5b-tktt7 1/1 Running 0 4m48s istio-cleanup-secrets-1.3.0-q9xrb 0/1 Completed 0 5m16s istio-egressgateway-5db67796d5-pmcr2 1/1 Running 0 4m58s istio-galley-7ff97f98b5-jn796 1/1 Running 0 4m59s istio-grafana-post-install-1.3.0-blqtb 0/1 Completed 0 5m19s istio-ingressgateway-859bb7b4-ms2zr 1/1 Running 0 4m56s istio-pilot-9b9f7f5c8-7h4j7 2/2 Running 0 4m49s istio-policy-754cbf67fb-5vk9f 2/2 Running 2 4m52s istio-security-post-install-1.3.0-7wffc 0/1 Completed 0 5m15s istio-sidecar-injector-68f4668959-ql975 1/1 Running 0 4m47s istio-telemetry-7cf8dcfd54-crd9w 2/2 Running 2 4m50s istio-tracing-669fd4b9f8-c8ptq 1/1 Running 0 4m46s kiali-94f8cbd99-h4b5z 1/1 Running 0 4m53s prometheus-776fdf7479-krzqm 1/1 Running 0 4m48s |
Запуск приложения
Будем использовать облегченную версию bookinfo, написаную в нашей компании для тестирования Istio. UI пока использовать не будем (он не работает достаточно хорошо для презентации).
Архитектура приложения:
- ui
- Gateway — API
- Books
- Ratings
Отличия версий приложений:
- books
- v1 — нету описания (description)
- v2 — есть описание
- ratings
- v1 — presentation «:-)»
- v2 — presentation «¯\\_(ツ)_/¯»
Создаем namespace для приложения:
1 2 |
kubectl create ns mesh kubectl label namespace mesh istio-injection=enabled |
Создаем файл books-deployment.yaml с содержимим.
Это набор стандартных api deployments и services для развертывания обычного приложения в kubernetes. В данном примере мы применяем одну версию Gateway и по 2 версии books и ratings. В service прописан selector только на имя приложения, а не на версию; маршрутизацию в определенную версию будем настраивать средствами Istio
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# =========== # = Gateway = # =========== --- apiVersion: apps/v1 kind: Deployment metadata: name: gw labels: app: gw version: v1 spec: replicas: 1 selector: matchLabels: app: gw version: v1 template: metadata: labels: app: gw version: v1 spec: containers: - name: gw image: kruschecompany/mesh:gateway ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: gw spec: selector: app: gw ports: - name: http port: 8080 # ========= # = Books = # ========= --- apiVersion: apps/v1 kind: Deployment metadata: name: books-v1 labels: app: books version: v1 spec: replicas: 1 selector: matchLabels: app: books version: v1 template: metadata: labels: app: books version: v1 spec: containers: - name: books image: kruschecompany/mesh:books_v1 ports: - containerPort: 8080 --- apiVersion: apps/v1 kind: Deployment metadata: name: books-v2 labels: app: books version: v2 spec: replicas: 1 selector: matchLabels: app: books version: v2 template: metadata: labels: app: books version: v2 spec: containers: - name: books image: kruschecompany/mesh:books_v2 ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: books spec: selector: app: books ports: - name: http port: 80 targetPort: 8080 # =========== # = Ratings = # =========== --- apiVersion: apps/v1 kind: Deployment metadata: name: ratings-v1 labels: app: ratings version: v1 spec: replicas: 1 selector: matchLabels: app: ratings version: v1 template: metadata: labels: app: ratings version: v1 spec: containers: - name: ratings image: kruschecompany/mesh:ratings_v1 ports: - containerPort: 8080 --- apiVersion: apps/v1 kind: Deployment metadata: name: ratings-v2 labels: app: ratings version: v2 spec: replicas: 1 selector: matchLabels: app: ratings version: v2 template: metadata: labels: app: ratings version: v2 spec: containers: - name: ratings image: kruschecompany/mesh:ratings_v2 ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: ratings spec: selector: app: ratings ports: - name: http port: 80 targetPort: 8080 |
Применяем:
1 |
kubectl -n mesh apply -f books-deployment.yaml |
Проверяем:
1 |
kubectl -n mesh get pod |
1 2 3 4 5 6 |
NAME READY STATUS RESTARTS AGE books-v1-758875cb99-sj4wm 2/2 Running 0 26m books-v2-64c4889569-jjpnt 2/2 Running 0 26m gw-7488b5dcbd-2t9xr 2/2 Running 0 26m ratings-v1-57f7d99c55-kxnm7 2/2 Running 0 26m ratings-v2-5d856c95d5-dm2tk 2/2 Running 0 26m |
В выводе мы видим в каждом поде запущенно по 2 контейнера, Istio при деплое сделал inject контейнера с Envoy, в дальнейшем весь трафик будем проходить через эти контейнеры.
Создаем файл istio-gateway.yaml c содержимим. Istio не разрешает в VirtualService использовать wildcard, поэтому заменим ‘*’ на ip балансировщика:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: mesh spec: selector: istio: ingressgateway # use Istio default gateway implementation servers: - port: number: 80 name: http protocol: HTTP hosts: - "*" --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: mesh-gw spec: hosts: - "*" # <- replace on load balancer ip gateways: - mesh http: - match: - uri: exact: /gateway/books route: - destination: port: number: 8080 host: gw |
Применяем:
1 |
kubectl -n mesh apply -f istio-gateway.yaml |
Мы определили точку входа в наше приложение, весь входящий трафик с uri/gateway/books будет маршрутизироваться на сервис gateway(он же gw).
Теперь создадим файл istio-destinationrule.yaml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: gw spec: host: gw trafficPolicy: tls: mode: ISTIO_MUTUAL subsets: - name: v1 labels: version: v1 --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: books spec: host: books trafficPolicy: tls: mode: ISTIO_MUTUAL subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2 --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: ratings spec: host: ratings trafficPolicy: tls: mode: ISTIO_MUTUAL subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2 |
В subset мы определили куда направлять трафик, для gw трафик будет идти в под c версией 1, books и ratings на обе версии по очереди.
Применяем:
1 |
kubectl -n mesh apply -f istio-destinationrule.yaml |
Открываем в браузере http://<load_balancer_ip>/gateway/books это наше API. Получаем JSON с книгами и их рейтингом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[ { "id": 1, "name": "War and Piece", "rating": 455.45, "presentation": ":-)", "description": "Historical" }, { "id": 2, "name": "Anna Karenina", "rating": 666.4, "presentation": ":-)", "description": "Drama" }, ... ] |
Попробуйте пообновлять страницу — вывод будет отличаться, так как приложение будет обращаться каждый раз к разным сервисам.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[ { "id": 1, "name": "War and Piece", "rating": 455.45, "presentation": "¯\\_(ツ)_/¯", "description": "Historical" }, { "id": 2, "name": "Anna Karenina", "rating": 666.4, "presentation": "¯\\_(ツ)_/¯", "description": "Drama" }, ... ] |
И еще пару раз:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[ { "id": 1, "name": "War and Piece", "rating": 455.45, "presentation": ":-)", "description": null }, { "id": 2, "name": "Anna Karenina", "rating": 666.4, "presentation": ":-)", "description": null }, ... ] |
Топологию приложения также можно посмотреть через под Kiali, который был установлен вместе с другими компонентами Istio. Для этого с помощью port-forward пробрасывем сервис себе на машину:
1 |
kubectl -n istio-system port-forward $(kubectl -n istio-system get pod -l app=kiali -o jsonpath='{.items[0].metadata.name}') 20001:20001 |
Kiali будет доступен на http://localhost:20001 admin/admin. На вкладке Graph видим:
Направление трафика
Все красиво на картинке, но в реальной жизни нужно, чтобы трафик ходил на определенные версии сервисов. Для этого создадим еще один файл istio-virtual-service-all-v1.yaml, в котором определяем, что все запросы будут идти на books и rating версии 1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: books spec: hosts: - books http: - route: - destination: host: books subset: v1 --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: ratings spec: hosts: - ratings http: - route: - destination: host: ratings subset: v1 |
И применяем:
1 |
kubectl -n mesh apply -f istio-virtual-service-all-v1.yaml |
Проверяем, в браузере должны видеть один и тот же вывод:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[ { "id": 1, "name": "War and Piece", "rating": 455.45, "presentation": ":-)", "description": null }, { "id": 2, "name": "Anna Karenina", "rating": 666.4, "presentation": ":-)", "description": null }, ... ] |
В данном примере в subset мы указали только v1 для books и ratings, и весь трафик пошел только в первую версию.
Переключение трафика
Применяем weight-based маршрутизацию. Это означает, что мы выставляем веса на сервисы, в примере мы ставим на сервис ratings первую версию 50 и на вторую версию так же. Теперь весь трафик будет балансироваться 50/50 между версиями. Так же мы можем поставить 10/90, в таком случае 10% трафика пойдет на первую версию и 90% на вторую.
Создаем файл virtual-service-ratings-50.yaml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: ratings spec: hosts: - ratings http: - route: - destination: host: ratings subset: v1 weight: 50 - destination: host: ratings subset: v2 weight: 50 |
Применяем:
1 |
kubectl -n mesh apply -f virtual-service-ratings-50.yaml |
Проверим в браузере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[ { "id": 1, "name": "War and Piece", "rating": 455.45, "presentation": ":-)", "description": null }, { "id": 2, "name": "Anna Karenina", "rating": 666.4, "presentation": ":-)", "description": null }, ... ] |
Обновим пару раз страницу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[ { "id": 1, "name": "War and Piece", "rating": 455.45, "presentation": "¯\\_(ツ)_/¯", "description": null }, { "id": 2, "name": "Anna Karenina", "rating": 666.4, "presentation": "¯\\_(ツ)_/¯", "description": null }, ... ] |
Почистим и перейдем к следующему примеру:
1 |
kubectl -n mesh apply -f istio-virtual-service-all-v1.yaml |
Таймауты и Повторные попытки
Istio позволяет включать таймауты для сервисов, таким образом можно искусственно имитировать долгий отклик микросервисов.
Создаем файл istio-delay.yaml, в котором задаем задержку в 2 секунды для всех запросов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: ratings spec: hosts: - ratings http: - fault: delay: percent: 100 fixedDelay: 2s route: - destination: host: ratings subset: v1 |
Применяем:
1 |
kubectl -n mesh apply -f istio-delay.yaml |
Проверяем в браузере — приложение работает, но с задержкой. Повысим таймаут до 5 секунд.
Применим и проверим:
1 |
kubectl -n mesh apply -f istio-delay.yaml |
Получаем в ответ ошибку, теперь мы знаем что приложение упадет, если один из микросервисов будет долго отвечать:
Так же можно добавить повторные попытки и и таймаут на повторную попытку:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: ratings spec: hosts: - ratings http: route: - destination: host: ratings subset: v1 retries: attempts: 3 perTryTimeout: 2s |
Почистим и перейдем к следующему примеру:
1 |
kubectl -n mesh apply -f istio-virtual-service-all-v1.yaml |
Зеркалирование трафика
Иногда нужно проверить новую версию на большем количестве пользователей, но выкатывать в прод нельзя. Для этого в Istio есть функционал зеркалирование трафика, мы запускаем параллельно новую версию сервиса и направляем туда трафик, не задевая при этом рабочую версию сервиса.
Для этого создаем файл istio-mirroring.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: ratings spec: hosts: - ratings http: - route: - destination: host: ratings subset: v1 weight: 100 mirror: host: ratings subset: v2 |
Применяем:
1 |
kubectl -n mesh apply -f istio-mirroring.yaml |
Проверяем:
1 |
while true;do curl http://<load_balancer_ip>/gateway/books; sleep 2;done |
Получаем ответ от ratings первой версии:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[ { "id": 1, "name": "War and Piece", "rating": 455.45, "presentation": ":-)", "description": null }, { "id": 2, "name": "Anna Karenina", "rating": 666.4, "presentation": ":-)", "description": null }, ... ] |
В логах контейнера ratings второй версии видим что трафик зеркалируться и на него :
1 2 3 4 5 |
2019-09-18 11:19:04.574 INFO 1 --- [nio-8080-exec-8] c.m.r.controller.BooksRatingsController : [1, 2, 3, 4, 5, 6] 2019-09-18 11:19:06.686 INFO 1 --- [nio-8080-exec-9] c.m.r.controller.BooksRatingsController : [1, 2, 3, 4, 5, 6] 2019-09-18 11:19:08.801 INFO 1 --- [io-8080-exec-10] c.m.r.controller.BooksRatingsController : [1, 2, 3, 4, 5, 6] 2019-09-18 11:19:10.918 INFO 1 --- [nio-8080-exec-1] c.m.r.controller.BooksRatingsController : [1, 2, 3, 4, 5, 6] 2019-09-18 11:19:13.065 INFO 1 --- [nio-8080-exec-2] c.m.r.controller.BooksRatingsController : [1, 2, 3, 4, 5, 6] |
Почистим и перейдем к следующему примеру:
1 |
kubectl -n mesh apply -f istio-virtual-service-all-v1.yaml |
Circuit Breaker
Очень важно, чтобы наши запросы гарантировано доходили до адресата. В istio реализован механизм Circuit Breaking. Прокси внутри кластера опрашивают сервисы и в случае поломки или медленного ответа выключают инстанс (под) сервиса из сети и направляют нагрузку на другие реплики сервиса.
Для сервиса books применим следующие правила:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: books spec: host: books trafficPolicy: connectionPool: tcp: maxConnections: 1 http: http1MaxPendingRequests: 1 maxRequestsPerConnection: 1 outlierDetection: consecutiveErrors: 1 interval: 1s baseEjectionTime: 3m maxEjectionPercent: 100 tls: mode: ISTIO_MUTUAL subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2 |
- maxConnections — Максимальное количество подключений к сервису. Любое избыточное соединение будет в очереди.
- http1MaxPendingRequests — максимальное количество ожидающих запросов к сервису. Любые лишние ожидающие запросы будут отклонены.
- maxRequestsPerConnection — максимальное количество запросов в кластере.
- BaseEjectionTime — максимальная продолжительность извлечения для пода. Под будет извлечен на 20 секунд.
- ConsecutiveErrors — количество ошибок до того, как под будет удален из пула. Например, если у вас есть три последовательные ошибки при взаимодействии со службой, Istio помечает под как нездоровый.
- Interval — интервал времени для анализа выброса. Например, сервис проверяются каждые 10 секунд.
- MaxEjectionPercent — максимальный процент подов, которые могут быть извлечены из пула балансировки нагрузки. Например, установка этого поля в 100 подразумевает, что любые нездоровые поды, выдающие последовательные ошибки, могут быть извлечены, и запрос будет перенаправлен на исправные поды.