Translate

середу, 27 квітня 2022 р.

Istio. Part II: Concepts And Traffic Management

Минулого разу ми познайомились із Service Mesh, поговорили про історичні передумови його появи та встановили Istio. Про особливості його використання згадали лише мимохіть, тож в цій частині спробуємо це виправити.

Для того щоб рухатись далі необхідні робочі Kubernetes та Istio. Версії, котрими я користуюсь, можна знайти в попередніх статтях. Для зручності також можна встановити MetalLB, що реалізує програмний LoadBalancer, хоч він і не обов'язковий в цьому випадку.

Основні додаткові абстракції, котрі привносить Istio в Kubernetes, реалізовані через CRD-розширення:

  • Gateway. Направляє трафік на відповідний VirtualService в залежності від доменного імені. У деякому приближенні про Gateway можна думати як про балансувальник, котрий стоїть перед всіма ресурсами. Разом із VirtualService виступає як більш гнучка заміна Kubernetes Ingress.
  • VirtualService. Абстракція Istio, котра описує, як запити мають потрапляти до кінцевого K8s сервісу. На відміну від Gateway працює із внутрішнім трафіком.
  • DestinationRule. Описує підмножини можливих сервісів та їх мапінги. Їх використовує VirtualService для розрізнення на яку саме версію сервісу має піти трафік. Окрім цього тут також описуються опції Сircuit Вreaker логіки.
  • ServiceEntry. Абстракція, що відповідає за додавання записів до внутрішнього реєстру сервісів Istio (service registry). Після чого Envoy може відправляти трафік на таку адресу, як ніби вона всередині mesh-мережі.

Одразу складно зрозуміти їх призначення, проте далі має бути зрозуміліше.


1. KUBERNETES ISTIO INGRESS

Default профіль
Istio вже має встановлений Ingress-контролер, тож необхідності встановлювати іншу реалізацію немає. Що таке Ingress я писав неодноразово, про це можна прочитати наприклад тут. Перейти до його використання можна за лічені хвилини.


Спочатку задеплоїмо тестовий httpbin сервіс в default неймспейс, адже саме для нього Istio і активований:

$ kubectl apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
  name: httpbin
---
apiVersion: v1
kind: Service
metadata:
  name: httpbin
  labels:
    app: httpbin
    service: httpbin
spec:
  ports:
  - name: http
    port: 8000
    targetPort: 80
  selector:
    app: httpbin
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: httpbin
      version: v1
  template:
    metadata:
      labels:
        app: httpbin
        version: v1
    spec:
      serviceAccountName: httpbin
      containers:
      - image: docker.io/kennethreitz/httpbin
        imagePullPolicy: IfNotPresent
        name: httpbin
        ports:
        - containerPort: 80
EOF


Цей опис також присутній в установочному архіві Іstio за адресою samples/httpbin/httpbin.yaml. Перевіримо чи з'явився сервіс та активуємо Ingress-опис:

$ kubectl get svc
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
httpbin      ClusterIP   10.108.162.227   <none>        8000/TCP   3h29m


$ kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: httpbin-istio-ingress
  annotations:
    kubernetes.io/ingress.class: istio
spec:
  rules:
  - host: httpbin.example.com
    http:
      paths:
      - path: /status
        pathType: Prefix
        backend:
          service:
            name: httpbin
            port:
              number: 8000
  ingressClassName: istio
EOF


Переглянемо чи з'явився Ingress для сервісу httpbin:

$ kubectl get ing
NAME                    CLASS   HOSTS                 ADDRESS        PORTS   AGE
httpbin-istio-ingress   istio   httpbin.example.com   192.168.1.70   80      117m


Нагадаю, що в моєму Kubernetes кластері встановлено MetalLB, тому Ingress доступний по адресам локальної мережі. В іншому разі варто використовувати NodePort. Перевіримо чи працює сам сервіс:

$ curl -s -I -HHost:httpbin.example.com "http://192.168.1.70/status/200"
HTTP/1.1 200 OK
server: istio-envoy
date: Mon, 14 Mar 2022 23:32:00 GMT
content-type: text/html; charset=utf-8
access-control-allow-origin: *
access-control-allow-credentials: true
content-length: 0
x-envoy-upstream-service-time: 6


Офіційна документація пропонує опис який наразі не актуальний, бо змінилось офіційне Ingress API. Також Istio-проект рекомендує використовувати Ingress Gateway, адже він надає більше гнучкості.

Цього разу зупинятись на TLS не буду, приклад його використання наведений в попередній статті про Ingress і цього разу нічим не відрізняється.

Для відсутності подальших конфліктів варто видалити щойно створений Ingress перед переходом до наступних пунктів:

$ kubectl delete ing httpbin-istio-ingress



2. INGRESS GATEWAY

Покриває аналогічні цілі, що і Ingress, але більш функціональний і гнучкий. Istio рекомендує сприймати Gateway як балансувальник, котрий може обслуговувати багато різних віртуальних імен/доменів. За ним VirtualService приймає трафік та направляє його на кінцевий сервіс. Створимо Gateway:

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: httpbin-gateway
spec:
  selector:
    istio: ingressgateway # use Istio default gateway implementation
  servers:
  - port:
      number: 80

      name: http
      protocol: HTTP
    hosts:
    - "httpbin.example.com"

EOF


Гейтвей httpbin-gateway буде приймати HTTP-трафік на 80-му порті та відправлятиме його далі, якщо ім'я хосту в запиті буде httpbin.example.com. Далі запити має прийняти на себе VirtualService:

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
  - "httpbin.example.com"
  gateways:
  - httpbin-gateway

  http:
  - match:
    - uri:
        prefix: /status
    - uri:
        prefix: /delay
    route:
    - destination:
        port:
          number: 8000
        host: httpbin
EOF


Отже, цей VirtualService буде підключено до веществореного гейтвея із іменем httpbin-gateway. Трафік буде перенаправлено на сервіс httpbin (із однойменним хостнеймом), якщо ім'я хосту в запиті буде httpbin.example.com та шлях /status чи /delay. Сервіс/деплой httpbin було вилито раніше, у попередньому пункті. Перевіримо роботу Ingress Gateway:

$ curl -s -I -HHost:httpbin.example.com "http://192.168.1.70/status/200"
HTTP/1.1 200 OK
server: istio-envoy
date: Mon, 14 Mar 2022 22:42:45 GMT
content-type: text/html; charset=utf-8
access-control-allow-origin: *
access-control-allow-credentials: true
content-length: 0
x-envoy-upstream-service-time: 52


TLS-термінування на рівні Gateway також налаштовується без особливих складностей. Цього разу до Gateway, задля демонстрації можливостей, підключимо 2 ресурса: httpbin.example.com та helloworld-v1.example.com.

Згенеруємо рутовий CA сертифікат та ключ:

$ openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt

Після чого згенеруємо для домену httpbin.example.com ключ та запит на сертифікацію, і останній підпишемо щойно створеним CA-ключем:

$ openssl req -out httpbin.example.com.csr -newkey rsa:2048 -nodes -keyout httpbin.example.com.key -subj "/CN=httpbin.example.com/O=httpbin organization"

$ openssl x509 -req -sha256 -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in httpbin.example.com.csr -out httpbin.example.com.crt


Створимо K8s секрет із сертифікатом і ключем до домену httpbin.example.com:

$ kubectl create -n istio-system secret tls httpbin-credential \
                 --key=httpbin.example.com.key \
                 --cert=httpbin.example.com.crt


Аналогічно для домену helloworld-v1.example.com:

$ openssl req -out helloworld-v1.example.com.csr -newkey rsa:2048 -nodes -keyout helloworld-v1.example.com.key -subj "/CN=helloworld-v1.example.com/O=helloworld organization"

$ openssl x509 -req -sha256 -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 1 -in helloworld-v1.example.com.csr -out helloworld-v1.example.com.crt

$ kubectl create -n istio-system secret tls helloworld-credential --key=helloworld-v1.example.com.key --cert=helloworld-v1.example.com.crt


Так як httpbin вже вилито, то лишилось вилити тільки helloworld-v1:

$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: helloworld-v1
  labels:
    app: helloworld-v1
spec:
  ports:
  - name: http
    port: 5000
  selector:
    app: helloworld-v1
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: helloworld-v1
      version: v1
  template:
    metadata:
      labels:
        app: helloworld-v1
        version: v1
    spec:
      containers:
      - name: helloworld
        image: istio/examples-helloworld-v1
        resources:
          requests:
            cpu: "100m"
        imagePullPolicy: IfNotPresent #Always
        ports:
        - containerPort: 5000
EOF


Створимо новий Gateway mygateway, котрий вже буде обслуговувати 2 домени: httpbin.example.com та helloworld-v1.example.com:

$ cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: mygateway
spec:
  selector:
    istio: ingressgateway # use istio default ingress gateway
  servers:
  - port:
      number: 443
      name: https-httpbin
      protocol: HTTPS
    tls:
      mode: SIMPLE
      credentialName: httpbin-credential
    hosts:
    - httpbin.example.com
  - port:
      number: 443
      name: https-helloworld
      protocol: HTTPS
    tls:
      mode: SIMPLE
      credentialName: helloworld-credential
    hosts:
    - helloworld-v1.example.com
EOF


Та два віртуал сервіси, що будуть адресувати на кінцеві K8s сервіси:

$ cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: helloworld-v1
spec:
  hosts:
  - helloworld-v1.example.com
  gateways:
  - mygateway
  http:
  - match:
    - uri:
        exact: /hello
    route:
    - destination:
        host: helloworld-v1
        port:
          number: 5000

EOF


$ cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
  - "httpbin.example.com"
  gateways:
  - mygateway
  http:
  - match:
    - uri:
        prefix: /status
    - uri:
        prefix: /delay
    route:
    - destination:
        port:
          number: 8000
        host: httpbin

EOF


Перевіримо чи все працює:

$ curl -HHost:httpbin.example.com --resolve "httpbin.example.com:443:192.168.1.70" --cacert example.com.crt "https://httpbin.example.com:443/status/418"

    -=[ teapot ]=-

       _...._
     .'  _ _ `.
    | ."` ^ `". _,
    \_;`"---"`|//
      |       ;/
      \_     _/
        `"""`


$ curl -HHost:helloworld-v1.example.com --resolve "helloworld-v1.example.com:443:192.168.1.70" --cacert example.com.crt "https://helloworld-v1.example.com:443/hello"
Hello version: v1, instance: helloworld-v1-5f44dd8565-twjjp

Як бачимо все працює добре, тому видалимо всі створені ресурси, щоб вони не конфліктували із наступними прикладами:

$ kubectl delete virtualservice httpbin helloworld-v1
$ kubectl delete gateway mygateway httpbin-gateway
$ kubectl delete service helloworld-v1 httpbin
$ kubectl delete deploy helloworld-v1 httpbin



3. REQUEST ROUTING

Цього разу ми побачимо як динамічно адресувати трафік на сервіси різних версій. Для цього встановимо згаданий в попередній статті мікросервісний додаток Bookinfo. Там же можна почитати деталі щодо логіки його роботи. Інсталяція, як і раніше, можлива або із архіву установки istio, або із опису на github:

$ cd ~./istio-1.13.2
$ kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml

$ kubectl get pods

NAME                              READY   STATUS    RESTARTS   AGE
details-v1-5498c86cf5-rvks4       2/2     Running   0          9m10s
productpage-v1-65b75f6885-zc6m9   2/2     Running   0          9m9s
ratings-v1-b477cf6cf-p85n6        2/2     Running   0          9m10s
reviews-v1-79d546878f-k2zmn       2/2     Running   0          9m10s
reviews-v2-548c57f459-zps65       2/2     Running   0          9m9s
reviews-v3-6dd79655b9-dtzxd       2/2     Running   0          9m9s


Нагадаю, що Bookinfo складається із 4 окремих мікросервісів: details, productpage, ratings та 3-ох версій reviews. Сервіс reviews випадково адресує запити на 3 різні версії, адже в кожного із цих сервісів однакові лейбли і тому K8s сервіс їх збирає в "одне ціле" селектором:

...
  selector:
    app: reviews
...


У браузері ця "випадковість" також буде помітна: рейтинг, який виглядає як кількість зірочок буде або з'являтись, або зникати, або ж стане червоним:

Всі інші сервіси представлені в одній версії. Отже спершу спробуємо зробити так, щоб запити потрапляли лише на першу версію reviews.

Опишемо Istio віртуал-сервіси для всіх мікросервісів:

$ cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: productpage
spec:
  hosts:
  - productpage
  http:
  - route:
    - destination:
        host: productpage
        subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
  - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ratings
spec:
  hosts:
  - ratings
  http:
  - route:
    - destination:
        host: ratings
        subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: details
spec:
  hosts:
  - details
  http:
  - route:
    - destination:
        host: details
        subset: v1
---
EOF

$ kubectl get virtualservice
NAME          GATEWAYS               HOSTS             AGE
bookinfo      ["bookinfo-gateway"]   ["*"]             2d23h
details                              ["details"]       2d23h
productpage                          ["productpage"]   2d23h
ratings                              ["ratings"]       2d23h
reviews                              ["reviews"]       2d23h


Та всі destination правила:

$ cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: productpage
spec:
  host: productpage
  subsets:
  - name: v1
    labels:
      version: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: reviews
spec:
  host: reviews
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
  - name: v3
    labels:
      version: v3
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: ratings
spec:
  host: ratings
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
  - name: v2-mysql
    labels:
      version: v2-mysql
  - name: v2-mysql-vm
    labels:
      version: v2-mysql-vm
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: details
spec:
  host: details
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
---
EOF

$ kubectl get destinationrules
NAME          HOST          AGE
details       details       21h
productpage   productpage   21h
ratings       ratings       21h
reviews       reviews       21h


Їх варто розуміти наступним чином. Трафік спочатку обслуговується віртуал-сервісом мікросервіса, а вже потім "конкретизується" відповідним destination правилом. Тобто DestinationRule - це відповідність (mapping) імені та лейбла, що призначений на деплойменті K8s сервісу. Наприклад VirtualService reviews вказує, що потрібно відправляти трафік на subset: v1 та host: reviews (доменне ім'я в межах неймспейсу). А DestinationRule reviews описує відповідність лейблів і сабсетів: цього разу їх 3, але трафік буде відправлено лишень на subset v1, що має лейбл version: v1.

Власне це і є реалізація концепції Blue-green деплоя: після виливки нової версії відбувається просто перемикання на неї. Якщо ж що щось пройшло не так - завжди і досить легко можна повернутись на попередню версію.

Тепер можна відкрити сторінку http://192.168.1.70/productpage та пересвідчимось, що трафік справді приходить лише на reviews мікросервіс v1. Цього разу не залежно від кількості оновлень сторінки буде відображатись версія без зірочок.

Віртуал-сервіси дуже гнучкі та надають багато додаткових можливостей, котрі були відсутні чи реалізовувались дещо складніше у ванільному Kubernetes. Наприклад можна в залежності від хедера в HTTP-запиті направляти користувача на іншу версію сервіса:

$ cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
    - reviews
  http:
  - match:
    - headers:
        end-user:
          exact: jason

    route:
    - destination:
        host: reviews
        subset: v2

  - route:
    - destination:
        host: reviews
        subset: v1
---
EOF


Якщо в хедері значення end-user буде рівне jason - запит буде відправлений на v2, інакше - v1. Це також може бути окремою стратегією виливки коду: наприклад на нову версію коду потрапить лише користувачі із особливим значенням якоїсь змінної в хедері.

Коли в браузері знову відкрити http://192.168.1.70/productpage та залогінитись від jason (підійде будь-який пароль) - то буде показані рейтинги із чорними зірочками (v2):


Для переходу до наступної частини видаляти створені ресурси не треба.



4. FAULT DETECTION

Завдяки Envoy проксі в кожному поді Istio також здатний проставляти додаткові затримки для тестування витривалості мікросервісів до збоїв. Цього разу додамо 7 секундну затримку між сервісами reviews:v2 та ratings, при умові, що користувач jason авторизований.

Далі необхідно, щоб Bookinfo перебував у стані, якому він лишився після 3-го пункту. Запити наразі працюють так:

    productpage → reviews:v2 → ratings (тільки для користувача jason)
    productpage → reviews:v1           (для всіх інших випадків)


Із затримкою VirtualService для ratings буде виглядати наступним чином:

$ cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ratings
spec:
  hosts:
  - ratings
  http:
  - match:
    - headers:
        end-user:
          exact: jason
    fault:
      delay:
        percentage:
          value: 100.0
        fixedDelay: 7s
    route:
    - destination:
        host: ratings
        subset: v1
  - route:
    - destination:
        host: ratings
        subset: v1
EOF


Тепер відкриємо /productpage сторінку в браузері і в Developer Tools меню буде помітна затримка лише 6 секунд та повідомлення "Sorry, product reviews are currently unavailable for this book":

Чому 6 секунд і чому постраждав саме review сервіс, адже затримка була введена між ratings та review мікросервісами? В свою чергу timeout по замовчуванню між сервісами в Istio - 10 секунд і це не мало б призвести до подібних результатів. А справа в тому, що між сервісами productpage та reviews в код навмисно було введена затримка в 3 секунди + одна додаткова спроба, що в сумі дорівнює 6 секундам. Саме тому ми і бачимо постійну недоступність reviews, бо він просто "не хоче чекати довше".

Такий функціонал буває корисним в реальному житті, адже деякі сервіси не завжди коректно обробляють випадки, коли залежні від них сервіси недоступні.

Окрім цього на рівні Istio VirtualService можна віддавати якусь помилку, наприклад 500-код, котрий ніби спровокував якийсь сервіс:

$ cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ratings
spec:
  hosts:
  - ratings
  http:
  - match:
    - headers:
        end-user:
          exact: jason
    fault:
      abort:
        percentage:
          value: 100.0
        httpStatus: 500

    route:
    - destination:
        host: ratings
        subset: v1
  - route:
    - destination:
        host: ratings
        subset: v1
EOF


Цього разу сервіс ratings буде навмисно віддавати 500-ту помилку, навіть якщо із самим сервісом буде все добре.



5. HTTP TRAFFIC SHIFTING

VirtualService - дуже потужний інструмент. За допомогою нього також можна розподілити по відсотках запити на різні версії одного сервісу:

$ cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
    - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v1
      weight: 50
    - destination:
        host: reviews
        subset: v3
      weight: 50
EOF


Отже 50% запитів отримає мікросервіс reviews:v1, а іншу половину - reviews:v3. За допомогою цієї можливості і реалізують Canary Deployment, коли в кінцевому рахунку увесь трафік перемикають на останню версію поступово, якщо не було знайдено критичних помилок.

$ cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
    - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v3
EOF


Цього разу всі 100% запитів будуть прямувати на reviews:v3


6. TCP TRAFFIC SHIFTING

Це окремий випадок попереднього пункту - перемикання TCP трафіку між версіями одного сервісу. Можливі причини для цього аналогічні: поступово переводити трафік на сервіс нової версії і повне перемикання у разі відсутності проблем. Попередній випадок був про HTTP-трафік, але це вже протокол вищого рівня ніж TCP і далеко не всі сервіси працюють по HTTP.

Цього разу встановимо новий додаток tcp-echo двох версій, а потім розподілимо трафік між ними. Створимо новий неймспейс для нового експерименту:

$ kubectl create namespace istio-io-tcp-traffic-shifting
$ kubectl label namespace istio-io-tcp-traffic-shifting istio-injection=enabled


Та задеплоїмо дві версії tcp-echo і їх сервіс:

$ cat <<EOF | kubectl -n istio-io-tcp-traffic-shifting apply -f -
apiVersion: v1
kind: Service
metadata:
  name: tcp-echo
  labels:
    app: tcp-echo
    service: tcp-echo
spec:
  ports:
  - name: tcp
    port: 9000
  - name: tcp-other
    port: 9001
  # Port 9002 is omitted intentionally for testing the pass through filter chain.
  selector:
    app: tcp-echo

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tcp-echo-v1
  labels:
    app: tcp-echo
    version: v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tcp-echo
      version: v1
  template:
    metadata:
      labels:
        app: tcp-echo
        version: v1
    spec:
      containers:
      - name: tcp-echo
        image: docker.io/istio/tcp-echo-server:1.2
        imagePullPolicy: IfNotPresent
        args: [ "9000,9001,9002", "one" ]
        ports:
        - containerPort: 9000
        - containerPort: 9001
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tcp-echo-v2
  labels:
    app: tcp-echo
    version: v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tcp-echo
      version: v2
  template:
    metadata:
      labels:
        app: tcp-echo
        version: v2
    spec:
      containers:
      - name: tcp-echo
        image: docker.io/istio/tcp-echo-server:1.2
        imagePullPolicy: IfNotPresent
        args: [ "9000,9001,9002", "two" ]
        ports:
        - containerPort: 9000
        - containerPort: 9001
EOF


Спочатку сконфігуруємо так, щоб 100% трафіку йшло на tcp-echo:v1 сервіс:

$ cat <<EOF | kubectl -n istio-io-tcp-traffic-shifting apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: tcp-echo-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 31400
      name: tcp
      protocol: TCP
    hosts:
    - "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: tcp-echo-destination
spec:
  host: tcp-echo
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: tcp-echo
spec:
  hosts:
  - "*"
  gateways:
  - tcp-echo-gateway
  tcp:
  - match:
    - port: 31400
    route:
    - destination:
        host: tcp-echo
        port:
          number: 9000
        subset: v1
EOF


За замовчуванням Istio Ingress виставляє лише HTTP порти 80 та 443, тому потрібно виставити додатковий, який в даному разі буде обслуговувати лише TCP-протокол:

$ kubectl edit svc istio-ingressgateway -n istio-system
...
  - name: tcp
    nodePort: 31400
    port: 31400
    protocol: TCP
    targetPort: 31400
...

$ kubectl get svc -n istio-system

NAME                   TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)                                                      AGE
...
istio-ingressgateway   LoadBalancer   10.99.81.106     192.168.1.70   15021:31339/TCP,80:30773/TCP,443:32045/TCP,31400:31400/TCP   42d


kubectl edit, як і patch, не варто використовувати для реальних сервісів, адже це погана практика. Цього разу було виставлено публічно порт 31400. Перевіримо чи доступний tcp-echo сервіс:

$ telnet 192.168.1.70 31400
Trying 192.168.1.70...
Connected to 192.168.1.70.
Escape character is '^]'.

one
one
...


Кожен <Enter> призводить до появи напису one, що свідчить про те, що адресація проходить тільки на першу версію сервісу. Задеплоїмо новий опис, де розподіл між сервісами 80 на 20%:

$ cat <<EOF | kubectl -n istio-io-tcp-traffic-shifting apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: tcp-echo
spec:
  hosts:
  - "*"
  gateways:
  - tcp-echo-gateway
  tcp:
  - match:
    - port: 31400
    route:
    - destination:
        host: tcp-echo
        port:
          number: 9000
        subset: v1
      weight: 80
    - destination:
        host: tcp-echo
        port:
          number: 9000
        subset: v2
      weight: 20
EOF


Тепер після підключення до 31400 порту можлива поява напису two, що свідчить про потрапляння трафіку на tcp-echo:v2.

$ for i in {1..20}; do sh -c "(date; sleep 1) | nc -q 1 192.168.1.70 31400"; done
one Sun Apr 24 15:47:40 UTC 2022
two Sun Apr 24 15:47:42 UTC 2022
one Sun Apr 24 15:47:44 UTC 2022
one Sun Apr 24 15:47:46 UTC 2022
one Sun Apr 24 15:47:48 UTC 2022
two Sun Apr 24 15:47:50 UTC 2022
one Sun Apr 24 15:47:52 UTC 2022
one Sun Apr 24 15:47:54 UTC 2022
one Sun Apr 24 15:47:56 UTC 2022


Нагадаю, що 192.168.1.70 та 31400 порт - це моя адреса Istio Gateway в приватній мережі, котру надає MetlalLB.


7. CIRCUIT BREAKING


Шаблон проєктування, що забезпечує обмеження доступу до сервісу у разі, якщо перевищено якісь ліміти, наприклад ліміт по кількості запитів чи по часу очікування. Завдяки цьому, у разі відмови другого сервісу, перший сервіс не витрачає додаткові ресурси на очікування. Цього разу подивимось як він реалізується в Istio.

Спочатку задеплоїмо сам сервіс, на котрому проведемо демонстрацію:

$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: httpbin
---
apiVersion: v1
kind: Service
metadata:
  name: httpbin
  labels:
    app: httpbin
    service: httpbin
spec:
  ports:
  - name: http
    port: 8000
    targetPort: 80
  selector:
    app: httpbin
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: httpbin
      version: v1
  template:
    metadata:
      labels:
        app: httpbin
        version: v1
    spec:
      serviceAccountName: httpbin
      containers:
      - image: docker.io/kennethreitz/httpbin
        imagePullPolicy: IfNotPresent
        name: httpbin
        ports:
        - containerPort: 80
EOF


Після чого виллємо обмеження для цього сервісу у вигляді DestinationRule:

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: httpbin
spec:
  host: httpbin
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 1
      http:
        http1MaxPendingRequests: 1
        maxRequestsPerConnection: 1

    outlierDetection:
      consecutive5xxErrors: 1
      interval: 1s
      baseEjectionTime: 3m
      maxEjectionPercent: 100
EOF


У якості клієнта, із котрого буде відбуватись тест, оберемо Fortio. Він дозволяє задавати кількість з’єднань, паралельність і затримки вихідних викликів HTTP.

$ kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.13/samples/httpbin/sample-client/fortio-deploy.yaml

У налаштуваннях DestinationRule ми вказали maxConnections: 1 та http1MaxPendingRequests: 1. Тобто якщо кількість підключень та одночасних запитів буде більше за одне - istio-proxy "розімкне ланцюг" для наступних підключень:

$ export FORTIO_POD=$(kubectl get pods -l app=fortio -o 'jsonpath={.items[0].metadata.name}')


$ kubectl exec "$FORTIO_POD" -c fortio -- /usr/bin/fortio load -c 3 -qps 0 -n 30 -loglevel Warning http://httpbin:8000/get

19:58:05 I logger.go:127> Log level is now 3 Warning (was 2 Info)
Fortio 1.22.0 running at 0 queries per second, 2->2 procs, for 30 calls: http://httpbin:8000/get
Starting at max qps with 3 thread(s) [gomax 2] for exactly 30 calls (10 per thread + 0)
19:58:05 W http_client.go:810> [1] Non ok http code 503 (HTTP/1.1 503)
...
19:58:05 W http_client.go:810> [0] Non ok http code 503 (HTTP/1.1 503)
19:58:05 W http_client.go:810> [1] Non ok http code 503 (HTTP/1.1 503)
Ended after 51.213349ms : 30 calls. qps=585.78
Aggregated Function Time : count 30 avg 0.0040056222 +/- 0.004012 min 0.000309402 max 0.011254601 sum 0.120168666
# range, mid point, percentile, count
>= 0.000309402 <= 0.001 , 0.000654701 , 40.00, 12
> 0.001 <= 0.002 , 0.0015 , 53.33, 4
> 0.003 <= 0.004 , 0.0035 , 60.00, 2
> 0.004 <= 0.005 , 0.0045 , 66.67, 2
> 0.005 <= 0.006 , 0.0055 , 70.00, 1
> 0.006 <= 0.007 , 0.0065 , 73.33, 1
> 0.008 <= 0.009 , 0.0085 , 76.67, 1
> 0.009 <= 0.01 , 0.0095 , 86.67, 3
> 0.01 <= 0.011 , 0.0105 , 93.33, 2
> 0.011 <= 0.0112546 , 0.0111273 , 100.00, 2
# target 50% 0.00175
# target 75% 0.0085
# target 90% 0.0105
# target 99% 0.0112164
# target 99.9% 0.0112508
Sockets used: 19 (for perfect keepalive, would be 3)
Uniform: false, Jitter: false
Code 200 : 13 (43.3 %)
Code 503 : 17 (56.7 %)
Response Header Sizes : count 30 avg 99.666667 +/- 114 min 0 max 230 sum 2990
Response Body/Total Sizes : count 30 avg 493.63333 +/- 288.9 min 241 max 824 sum 14809
All done 30 calls (plus 0 warmup) 4.006 ms avg, 585.8 qps


І статистика наразі наступна:

Code 200 : 13 (43.3 %)
Code 503 : 17 (56.7 %)


Тобто 13 запитів із 30 завершились успішно, а на останні 17 було віддано 503 помилку.

Можна також проглянути статистику istio-proxy сервісу:

$ kubectl exec "$FORTIO_POD" -c istio-proxy -- pilot-agent request GET stats | grep httpbin | grep pending
cluster.outbound|8000||httpbin.default.svc.cluster.local.circuit_breakers.default.remaining_pending: 1
cluster.outbound|8000||httpbin.default.svc.cluster.local.circuit_breakers.default.rq_pending_open: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.circuit_breakers.high.rq_pending_open: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_active: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_failure_eject: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_overflow: 61
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_total: 94


upstream_rq_pending_overflow вказує, що 61 запит не було оброблено через правило circuit breaking.


8. MIRRORING

Цей приклад продемонструє можливість дзеркалювання трафіку за допомогою Istio. Така концепція дозволяє повторювати трафік, наприклад для копіювання запитів із production на інші вузли для додаткового тестування.

Цього разу спробуємо "скопіювати" трафік, що потрапляє на тестовий сервіс httpbin-v1, на httpbin-v2. Тому створимо їх:

$ cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: httpbin
      version: v1
  template:
    metadata:
      labels:
        app: httpbin
        version: v1
    spec:
      containers:
      - image: docker.io/kennethreitz/httpbin
        imagePullPolicy: IfNotPresent
        name: httpbin
        command: ["gunicorn", "--access-logfile", "-", "-b", "0.0.0.0:80", "httpbin:app"]
        ports:
        - containerPort: 80
EOF

$ cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: httpbin
      version: v2
  template:
    metadata:
      labels:
        app: httpbin
        version: v2
    spec:
      containers:
      - image: docker.io/kennethreitz/httpbin
        imagePullPolicy: IfNotPresent
        name: httpbin
        command: ["gunicorn", "--access-logfile", "-", "-b", "0.0.0.0:80", "httpbin:app"]
        ports:
        - containerPort: 80
EOF

$ kubectl get deploy
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
httpbin-v1   1/1     1            1           3m36s
httpbin-v2   1/1     1            1           2m41s


Та K8s сервіс для них обох:

$ kubectl create -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: httpbin
  labels:
    app: httpbin
spec:
  ports:
  - name: http
    port: 8000
    targetPort: 80
  selector:
    app: httpbin
EOF


Сервіс sleep будемо використовувати для генерації навантаження:

$ cat <<EOF | istioctl kube-inject -f - | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sleep
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sleep
  template:
    metadata:
      labels:
        app: sleep
    spec:
      containers:
      - name: sleep
        image: curlimages/curl
        command: ["/bin/sleep","3650d"]
        imagePullPolicy: IfNotPresent
EOF


За замовчуванням K8s відправляє запити одразу на дві версії httpbin. Виправимо це додавши відповідний VirtualService та DestinationRule:

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
    - httpbin
  http:
  - route:
    - destination:
        host: httpbin
        subset: v1
      weight: 100
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: httpbin
spec:
  host: httpbin
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
EOF


Тепер увесь трафік йде на httpbin:v1 сервіс. Для того щоб відправити копію запитів на сервіс v2 потрібно додати директиву mirror в VirtualService:

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
    - httpbin
  http:
  - route:
    - destination:
        host: httpbin
        subset: v1
      weight: 100
    mirror:
      host: httpbin
      subset: v2
    mirrorPercentage:
      value: 100.0

EOF


Створимо тестові запити та перевіримо логи в кожному поді:

$ export V1_POD=$(kubectl get pod -l app=httpbin,version=v1 -o jsonpath={.items..metadata.name})
$ export V2_POD=$(kubectl get pod -l app=httpbin,version=v2 -o jsonpath={.items..metadata.name})

$ kubectl exec "${SLEEP_POD}" -c sleep -- curl -sS http://httpbin:8000/headers

$ kubectl logs "$V1_POD" -c httpbin
...
127.0.0.6 - - [06/Apr/2022:22:53:10 +0000] "GET /headers HTTP/1.1" 200 529 "-" "curl/7.82.0-DEV"
127.0.0.6 - - [06/Apr/2022:23:01:11 +0000] "GET /headers HTTP/1.1" 200 529 "-" "curl/7.82.0-DEV"


$ kubectl logs "$V2_POD" -c httpbin
...
127.0.0.6 - - [06/Apr/2022:23:01:11 +0000] "GET /headers HTTP/1.1" 200 569 "-" "curl/7.82.0-DEV"



9. WRAP UP

Отже, цього разу ми поглянули на те, що справді може запропонувати Istio і, маю визнати, ці речі дійсно вражаючі. Звісно він додасть ще більшої складності будь-якій інфраструктурі і підійде далеко не кожній команді. Тому перед використанням Istio в production-інфраструктурах варто задуматись чи буде достатньо професіоналізму команди (і не лише DevOps-команді) і чи справді цей функціонал такий потрібний? Не варто також забувати про те, що софт необхідно періодично оновлювати, що часом також не просто зробити не зламавши все можливе.

Наступного разу в заключній статті поговоримо про Egress-правила і як їх використовувати.

Посилання:
https://istio.io/latest/docs/
https://istio.tetratelabs.io/blog/istio-traffic-management-walkthrough-p1
https://www.ibm.com/downloads/cas/XWN1WV9Q

Немає коментарів:

Дописати коментар