Трафік в AWS взагалі досить цікава та місцями складна штука, колись писав окремо про це у пості AWS: Cost optimization – обзор расходов на сервисы и стоимость трафика в AWS – прийшов час трохи повернутися до цієї теми.
Отже, в чьому проблема: в AWS Cost Explorer помітив, що кілька днів поспіль маємо зростання витрат на EC2-Other:
А що у нас входить в EC2-Other? Всякі Load Balancers, IP, EBS та трафік, див. Tips and Tricks for Exploring your Data in AWS Cost Explorer.
Щоб перевірити, на шо саме виросли трати – переключаємо Dimension на Usage Type та у Service вибираємо EC2-Other:
Бачимо, що виросли кости на DataTransfer-Regional-Bytes, які є “This is Amazon EC2 traffic that moves between AZs but stays within the same region.” – див. Understand AWS Data transfer details in depth from cost and usage report using Athena query and QuickSight та Understanding data transfer charges.
Можемо переключити на API Operation, і побачити який саме трафік почав використовуватись:
InterZone-In та InterZone-Out.
Як раз на минулому тижні запустив моніторинг з VictoriaMetrics Kubernetes Stack з Grafana Loki і налаштовував збір логів з CloudWatch Logs та додавав алерти з Loki Ruler – мабуть воно і вплинуло на трафік. Давайте розбиратись.
Зміст
VPC Flow Logs
Що нам треба, це додати Flow Logs для VPC нашого Kubernetes кластеру – тоді побачимо, які саме Kubernetes-поди або Lambda-фунції в AWS почали активно “їсти” трафік. Детальніше є в пості AWS: VPC Flow Logs – знайомство та аналітика з CloudWatch Logs Insights.
Створюємо CloudWatch Log Group з кастомними полями щоб мати pkt_srcaddr
та pkt_dstaddr
, які містять у собі IP Kubernetes подів, див. Using VPC Flow Logs to capture and query EKS network communications.
В Log Group налаштовуємо наступні поля:
region vpc-id az-id subnet-id instance-id interface-id flow-direction srcaddr dstaddr srcport dstport pkt-srcaddr pkt-dstaddr pkt-src-aws-service pkt-dst-aws-service traffic-path packets bytes action
Далі, налаштовуємо Flow Logs для VPC нашого кластеру:
І йдемо дивитсь на логи.
CloudWatch Logs Insigts
Беремо запит із прикладів:
І переписуємо його під свій формат – взяв з того ж поста у Примеры Logs Insights:
parse @message "* * * * * * * * * * * * * * * * * * *" | as region, vpc_id, az_id, subnet_id, instance_id, interface_id, | flow_direction, srcaddr, dstaddr, srcport, dstport, | pkt_srcaddr, pkt_dstaddr, pkt_src_aws_service, pkt_dst_aws_service, | traffic_path, packets, bytes, action | stats sum(bytes) as bytesTransferred by pkt_srcaddr, pkt_dstaddr | sort bytesTransferred desc | limit 10
Та отримуємо цікаву картину:
Де в топі з великим відривом бачимо дві адреси – 10.0.3.111 та 10.0.2.135, які нагнали аж 28995460061 байт трафіку.
Loki components та трафік
Перевіряємо, що ж це за поди в Kubernetes, і заодно знаходимо відповідні WorkerNodes/EC2.
Спершу 10.0.3.111:
[simterm]
$ kk -n dev-monitoring-ns get pod -o wide | grep 10.0.3.111 loki-backend-0 1/1 Running 0 22h 10.0.3.111 ip-10-0-3-53.ec2.internal <none> <none>
[/simterm]
Та 10.0.2.135:
[simterm]
$ kk -n dev-monitoring-ns get pod -o wide | grep 10.0.2.135 loki-read-748fdb976d-grflm 1/1 Running 0 22h 10.0.2.135 ip-10-0-2-173.ec2.internal <none> <none>
[/simterm]
І вже тут я згадав, що саме 31-го липня включив алерти в Loki, які обробляються як раз в поді backend
, де крутиться компонент Ruler (раніше він був у поді read
).
Тобто левова частина трфіку відбувається саме між Read та Backend подами.
Окреме питання що саме там в такій кількості передається, але поки треба вирішити проблему с витратами на трафік.
Перевіримо в яких AvailabilityZones знаходяться Kubernetes WorkerNodes.
Інстанс ip-10-0-3-53.ec2.internal, де крутиться под з Backend:
[simterm]
$ kk get node ip-10-0-3-53.ec2.internal -o json | jq -r '.metadata.labels["topology.kubernetes.io/zone"]' us-east-1b
[/simterm]
Та ip-10-0-2-173.ec2.internal, де знаходиться под з Read:
[simterm]
$ kk get node ip-10-0-2-173.ec2.internal -o json | jq -r '.metadata.labels["topology.kubernetes.io/zone"]' us-east-1a
[/simterm]
Ось і маємо cross-AvailabilityZones трафік.
Kubernetes podAffinity
та nodeAffinity
Що можемо спробувати – це додати Affinity для подів, щоб вони запускались в одній AvailabilityZone. Див. Assigning Pods to Nodes та Kubernetes Multi-AZ deployments Using Pod Anti-Affinity.
Для подів у Helm-чарті вже маємо affinity
:
... affinity: | podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: {{- include "loki.readSelectorLabels" . | nindent 10 }} topologyKey: kubernetes.io/hostname ...
Перший варіант – це вказати Kubernetes Scheduler, що ми хочемо поді Read розташовувати на тій самій WorkerNode, де є поди з Backend. Для цього можемо використати podAffinity
.
Перевірямо лейбли Backend:
[simterm]
$ kk -n dev-monitoring-ns get pod loki-backend-0 --show-labels NAME READY STATUS RESTARTS AGE LABELS loki-backend-0 1/1 Running 0 23h app.kubernetes.io/component=backend,app.kubernetes.io/instance=atlas-victoriametrics,app.kubernetes.io/name=loki,app.kubernetes.io/part-of=memberlist,controller-revision-hash=loki-backend-8554f5f9f4,statefulset.kubernetes.io/pod-name=loki-backend-0
[/simterm]
Тож для Reader можемо задати podAntiAffinity
з labelSelector=app.kubernetes.io/component=backend
– тоді Reader буде “тягнутись” до тії ж AvailabilityZone, де запущено Backend.
Інший варіант – через nodeAffinity
, і в Expressions для обох Read та Backend вказати лейблу з бажаною AvailabilityZone.
Спробуємо з preferredDuringSchedulingIgnoredDuringExecution
, тобто “soft limit”:
... read: replicas: 2 affinity: | nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 1 preference: matchExpressions: - key: topology.kubernetes.io/zone operator: In values: - us-east-1a ... backend: replicas: 1 affinity: | nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 1 preference: matchExpressions: - key: topology.kubernetes.io/zone operator: In values: - us-east-1a ...
Деплоїмо, перевіряємо Read-поди:
[simterm]
$ kk -n dev-monitoring-ns get pod -l app.kubernetes.io/component=read -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES loki-read-d699d885c-cztj7 1/1 Running 0 50s 10.0.2.181 ip-10-0-2-220.ec2.internal <none> <none> loki-read-d699d885c-h9hpq 0/1 Running 0 20s 10.0.2.212 ip-10-0-2-173.ec2.internal <none> <none>
[/simterm]
Та зони інстансів:
[simterm]
$ kk get node ip-10-0-2-220.ec2.internal -o json | jq -r '.metadata.labels["topology.kubernetes.io/zone"]' us-east-1a $ kk get node ip-10-0-2-173.ec2.internal -o json | jq -r '.metadata.labels["topology.kubernetes.io/zone"]' us-east-1a
[/simterm]
Окей, тут все є, а що там Backend?
[simterm]
$ kk get nod-n dev-monitoring-ns get pod -l app.kubernetes.io/component=backend -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES loki-backend-0 1/1 Running 0 75s 10.0.3.254 ip-10-0-3-53.ec2.internal <none> <none>
[/simterm]
І його нода:
[simterm]
$ kk -n dev-get node ip-10-0-3-53.ec2.internal -o json | jq -r '.metadata.labels["topology.ebs.csi.aws.com/zone"]' us-east-1b
[/simterm]
А чому в 1b, коли ми вказали 1a?? Глянемо StatefulSet – чи додались наші affinity:
[simterm]
$ kk -n dev-monitoring-ns get sts loki-backend -o yaml apiVersion: apps/v1 kind: StatefulSet ... spec: affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - preference: matchExpressions: - key: topology.kubernetes.io/zone operator: In values: - us-east-1a weight: 1 ...
[/simterm]
Все є.
Добре – давайте використаємо “hard limit”, тобто requiredDuringSchedulingIgnoredDuringExecution
:
... backend: replicas: 1 affinity: | nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: topology.kubernetes.io/zone operator: In values: - us-east-1a ...
Деплоїмо ще раз, і тепер под з Бекендом застряг у статусі Pending:
[simterm]
$ kk -n dev-monitoring-ns get pod -l app.kubernetes.io/component=backend -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES loki-backend-0 0/1 Pending 0 3m39s <none> <none> <none> <none>
[/simterm]
Чому? Дивимось Events:
[simterm]
$ kk -n dev-monitoring-ns describe pod loki-backend-0 ... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 34s default-scheduler 0/3 nodes are available: 1 node(s) didn't match Pod's node affinity/selector, 2 node(s) had volume node affinity conflict. preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling..
[/simterm]
Спешу подумав, що на WokrerkNdoes вже маємо максимум подів – 17 штук на t3.medium.
Перевіримо:
[simterm]
$ kubectl -n dev-monitoring-ns get pods -A -o jsonpath='{range .items[?(@.spec.nodeName)]}{.spec.nodeName}{"\n"}{end}' | sort | uniq -c | sort -rn 16 ip-10-0-2-220.ec2.internal 14 ip-10-0-2-173.ec2.internal 13 ip-10-0-3-53.ec2.internal
[/simterm]
Але ні – місця ще є.
Тоді що – EBS? Часта проблема, коли EBS в одній AvailabilityZone, а Pod запускається в іншій.
Знаходимо Volume Бекенду – там йому підключаються алерт-рули для Ruler:
[simterm]
... Volumes: ... data: Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace) ClaimName: data-loki-backend-0 ReadOnly: false ...
[/simterm]
Знаходимо відповідний Persistent Volume:
[simterm]
$ kubectl k -n dev-monitoring-ns get pvc data-loki-backend-0 NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE data-loki-backend-0 Bound pvc-b62bee0b-995e-486b-9f97-f2508f07a591 10Gi RWO gp2 15d
[/simterm]
І AvailabilityZone цього EBS:
[simterm]
$ kk -n dev-monitoring-ns get pv pvc-b62bee0b-995e-486b-9f97-f2508f07a591 -o json | jq -r '.metadata.labels["topology.kubernetes.io/zone"]' us-east-1b
[/simterm]
Так і є – диск у нас в зоні us-east-1b, а под намагаємось запустити под в зоні us-east-1a.
Що можемо зробити – це або Readers запускати в зоні 1b, або видалити PVC для Backend, і тоді при деплої він створить новий PV та EBS в зоні 1a.
Так як в волюмі ніяк даних нема і для Ruler правила створються з ConfigMap, то простіше просто видалити PVC:
[simterm]
$ kubectl k -n dev-monitoring-ns delete pvc data-loki-backend-0 persistentvolumeclaim "data-loki-backend-0" deleted
[/simterm]
Видаляємо под, щоб він перестворився:
[simterm]
$ kk -n dev-monitoring-ns delete pod loki-backend-0 pod "loki-backend-0" deleted
[/simterm]
Перевіряємо, що PVC створений:
[simterm]
$ kk -n dev-monitoring-ns get pvc data-loki-backend-0 NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE data-loki-backend-0 Bound pvc-5b690c18-ba63-44fd-9626-8221e1750c98 10Gi RWO gp2 14s
[/simterm]
І його локація тепер:
[simterm]
$ kk -n dek -n dev-monitoring-ns get pv pvc-5b690c18-ba63-44fd-9626-8221e1750c98 -o json | jq -r '.metadata.labels["topology.kubernetes.io/zone"]' us-east-1a
[/simterm]
І сам под теж запустився:
[simterm]
$ kk -n dev-monitoring-ns get pod loki-backend-0 NAME READY STATUS RESTARTS AGE loki-backend-0 1/1 Running 0 2m11s
[/simterm]
Результати трафіку
Робив це в п’ятницю, і на понеділок маємо результат:
Все вийшло, як і планувалось – Cross AvailabilityZone трафік тепер майже на нулі.