Часто возникает ситуация, когда в кластере работает много взаимодействующих между собой сервисов, но из-за спонтанности разработки эти взаимодействия могут быть нигде не документированы. То есть ни команды разработки, ни команды эксплуатации доподлинно не знают, какие приложения куда обращаются, как часто, и какую нагрузку создают эти обращения. И когда возникает проблема с производительностью какого-то сервиса, не совсем понятно, на что нужно обратить внимание.
В идеале хотелось бы иметь какую-то карту взаимодействия сервисов в Kubernetes, которая сама автоматически обновляется. Такую карту можно построить с помощью инструментов типа Istio и Cilium. Но иногда можно обойтись и более простыми решениями — например, NetFlow.
Поиск готовых Open Source-решений
Вообще Istio — это огромный комбайн, который решает множество задач: авторизация сервисов и шифрование трафика между ними, маршрутизация запросов, управление балансировкой запросов, трассировка и т. д.
Но минус Istio в том, что он внедряет свои sidecar-контейнеры с envoy между приложениями, то есть по сути вмешивается в трафик. В результате этого могут возникать проблемы. Они, безусловно, имеют свои решения, но использовать Istio в режиме «включил и ничего не сломалось» практически никогда не получается. Что-нибудь обязательно начинает работать не так, как работало ранее. Это не говоря уже о повышенной нагрузке — каждый sidecar потребляет ресурсы CPU и памяти, а также привносит дополнительные, пусть и небольшие, задержки.
Нам хотелось получить какую-то систему мониторинга межсервисного взаимодействия, которая стоит «сбоку» от приложений. Чтобы в случае, если эта система не работает или не успевает обрабатывать трафик и так далее, это никак не влияло на трафик между приложениями, и они продолжили работать.
Что-то подобное на просторах Open Source найти не удалось. Некоторые CNI частично предоставляют такую функциональность, но не в полном объеме. К примеру, у Cilium есть Hubble, показывающий взаимодействие контейнеров. Но, во-первых, он потребляет просто огромное количество ресурсов (об этом мы упоминали в нашей статье), во-вторых, не хранит историю — посмотреть взаимодействие можно только «здесь и сейчас», без ретроспективы.
В Calico есть подобная функция, но в enterprise-версии.
В любом случае не хотелось бы привязываться к CNI. Хотелось бы чего-то более универсального.
Изобретаем свой велосипед
В голове вертелась мысль про NetFlow и модуль ядра ipt-netflow, который обрабатывает соединения и отправляет статистику по ним по протоколу NetFlow в коллектор.
NetFlow — сетевой протокол, предназначенный для учёта сетевого трафика, разработанный компанией Cisco Systems. Является фактическим промышленным стандартом и поддерживается не только оборудованием Cisco, но и многими другими устройствами, включая свободные реализации для UNIX-подобных систем.
Так как модуль работает на уровне ядра, то может обрабатывать огромный поток трафика, как PPS так и BPS, что практически незаметно на фоне остальной нагрузки на узле. Этот инструмент выглядел как отличное решение по снятию статистики трафика с узлов.
Но ipt-netflow содержит только информацию про IP-адреса (и порты) источника и назначения, но ничего не знает про сервисы, Pod'ы, пространства имён и остальные сущности Kubernetes. Да и что делать с огромным числом данных о соединениях, которые будут отправлять сенсоры ipt-netflow?
Мысль не давала покоя, и в итоге было решено «скрестить ужа с ежом». С помощью библиотеки goflow2 был написан собственный коллектор для NetFlow, который экспортировал информацию о межсервисном взаимодействии в формате Prometheus.
Во-первых, при запуске коллектор подписывается через API Kubernetes на информацию от всех Pod'ов: запущенных, новых и удаляемых. Это позволяет знать, какой IP-адрес соответствует какому Pod'у или приложению (по лейблам), а также в каком пространстве имён запущено это приложение.
Во-вторых, получая пакеты NetFlow с сенсора ipt-netflow с каждого узла, коллектор разбирает эти пакеты по IP-адресу источника и назначения, производит маппинг адресов в имен сервисов, а затем обновляет счетчики Prometheus с соответствующими лейблами.
В NetFlow для каждого соединения нас интересовало количество байт в соединении, а также общее количество соединений, которые приближенно можно считать количеством обращений одного сервиса к другому.
То есть мы собираем 2 метрики: netflow_connection_count
и netflow_connection_bytes
. Можно экспортировать еще netflow_connection_packets
, но полезного применения этой метрике не нашлось.
Получилось что-то подобное:
netflow_connection_count{dsthost="task-scheduler",dstnamespace="stage",srchost="kube-dns",srcnamespace="kube-system"} 5710
netflow_connection_count{dsthost="task-scheduler",dstnamespace="stage",srchost="opentelemetry-collector",srcnamespace="telemetry"} 11063
netflow_connection_bytes{dsthost="kube-dns",dstnamespace="kube-system",srchost="task-scheduler",srcnamespace="stage"} 1.126274e+06
netflow_connection_bytes{dsthost="opentelemetry-collector",dstnamespace="telemetry",srchost="task-scheduler",srcnamespace="stage"} 7.231581e+06
Тут надо понимать, что по данным из NetFlow непонятно, какое приложение инициировало подключение. То есть на каждое соединение у вас будет 2 записи в NetFlow — трафик из приложения А в приложение В и трафик из приложения В в приложение А.
Так как NetFlow фиксирует вообще весь трафик, проходящий через узел, там присутствует информация о соединениях, ушедших из кластера наружу, и пришедших снаружи. Все соединения с неизвестных IP-адресов (тех, которые не принадлежат ни одному Pod'у в API Kubernetes) маркируются меткой unknown*
.
Примечание
В перспективе можно сделать какой-то дополнительный маппинг для таких внешних адресов. Например, подставлять AS (Autonomous System) или AS Name — тогда вместо unknown
в лейблах будут записи вроде AS13238
или YANDEX LLC
.
Для доставки модуля ipt-netflow на все узлы кластера мы использовали custom resource NodeGroupConfiguration
в Deckhouse, под управлением которого работает наш кластер:
apiVersion: deckhouse.io/v1alpha1
kind: NodeGroupConfiguration
metadata:
name: iptnetflow.sh
spec:
weight: 100
bundles:
- "*"
nodeGroups:
- "*"
content: |
dpkg -s iptables-netflow-dkms || ( apt-get update && apt-get install -y iptables-netflow-dkms )
lsmod | grep ipt_NETFLOW || modprobe ipt_NETFLOW protocol=9
[ "$(sysctl -n net.netflow.destination)" = 10.222.2.222:2055" ] || sysctl -w net.netflow.destination=10.222.2.222:2055
[ "$(sysctl -n net.netflow.protocol)" = "9" ] || sysctl -w net.netflow.protocol=9
iptables -C FORWARD -i cni0 -j NETFLOW || iptables -I FORWARD -i cni0 -j NETFLOW
Из описания CR видно, что на узле выполняются:
-
установка пакета
iptables-netflow-dkms
(вариант для Ubuntu); -
указание целевого адреса доставки NetFlow;
-
выбор необходимой 9-й версии NetFlow;
-
отправка при помощи iptables всех пакетов с интерфейса
cni0
в цепочкуNETFLOW
, где их обработает модуль.
Интерфейс cni0
может отличаться в зависимости от используемой версии CNI.
Важно отметить, что перехватывается только входящий в интерфейс трафик. Так как взаимодействующие приложения находятся на разных узлах, при учете всего трафика он будет удваиваться из-за одновременного учета на обоих узлах: на каждом из узлов сенсор будет отправлять его в коллектор.
Далее мы запустили наше приложение и сделали для него сервис ClusterIP с адресом 10.222.2.222, который указан в манифесте выше:
apiVersion: v1
kind: Service
metadata:
name: iptnetflow
labels:
prometheus.deckhouse.io/custom-target: iptnetflow
annotations:
prometheus.deckhouse.io/sample-limit: "30000"
spec:
type: ClusterIP
clusterIP: 10.222.2.222
selector:
app: iptnetflow
ports:
- name: udp-netflow
port: 2055
protocol: UDP
- name: http-metrics
port: 8080
protocol: TCP
Сервис принимает UDP-поток с NetFlow на порту 2055, обрабатывает его, формирует метрики и лейблы, которые получает для Pod'ов из API Kubernetes, и затем экспортирует метрики на порту 8080.
Также нам понадобился ServiceAccount с правами get watch list
на все Pod'ы в кластере.
Сначала была идея выкатить коллектор на каждый узел кластера с помощью DaemonSet’а, чтобы каждый узел отправлял NetFlow в свой коллектор. Но оказалось, что в кластере из 60 узлов и 4000+ Pod'ов «NetFlow-коллектор+prometheu-exporter» потребляет менее 1 CPU. Пришлось даже сгенерировать повышенный flow-rate, чтобы убедиться, что сервис не будет упираться в одно ядро при обработке трафика.
DaemonSet или Deployment в нескольких репликах запустить тоже возможно, если кластер очень большой, и один Pod не сможет обработать весь поток данных. Но здесь нужно понимать, что при этом кратно увеличится количество собираемых метрик (а их и так будет довольно много), которые нужно потом агрегировать.
Исходные коды приложения-коллектора на Go и сервиса-конвертера на Python, а также все необходимое для развертывания доступно в репозитории.
Для построения более надежной системы можно запустить несколько коллекторов на разных адресах. Модуль ipt-netflow может дублировать данные для нескольких получателей: для этого нужно указать несколько destination
'ов. Подробнее об этом и о многих других возможностях модуля можно прочитать в официальной документации.
Что получилось в итоге
Для визуализации сделали дашборд в Grafana и карту взаимодействия сервисов с помощью плагина hamedkarbasi93-nodegraphapi-datasource. Мы написали небольшое приложение на Python, которое, используя ServiceAccount, получает данные из Prometheus и отдает их в формате, требуемом для Nodegraph:
В результате у нас получился очень легковесный инструмент, с помощью которого можно автоматически построить карту взаимодействия между сервисами в Kubernetes. Причем его работа абсолютно никак не влияет на работоспособность самих сервисов и никак не вмешивается в трафик. Также он позволяет мониторить объем передаваемых между сервисами данных и количество открываемых соединений, что условно можно рассматривать как количество запросов к сервису.
P.S.
Читайте также в нашем блоге:
Автор:
trublast