Минулого разу ми познайомились із 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
Немає коментарів:
Дописати коментар