Мониторинг межсервисного взаимодействия Kubernetes с помощью протокола NetFlow

в 7:12, , рубрики: devops, Go, Grafana, kubernetes, netflow, Блог компании Флант, Сетевые технологии, сети, системное администрирование

Часто возникает ситуация, когда в кластере работает много взаимодействующих между собой сервисов, но из-за спонтанности разработки эти взаимодействия могут быть нигде не документированы. То есть ни команды разработки, ни команды эксплуатации доподлинно не знают, какие приложения куда обращаются, как часто, и какую нагрузку создают эти обращения. И когда возникает проблема с производительностью какого-то сервиса, не совсем понятно, на что нужно обратить внимание.

В идеале хотелось бы иметь какую-то карту взаимодействия сервисов в Kubernetes, которая сама автоматически обновляется. Такую карту можно построить с помощью инструментов типа Istio и Cilium. Но иногда можно обойтись и более простыми решениями — например, NetFlow.

Мониторинг межсервисного взаимодействия Kubernetes с помощью протокола NetFlow - 1

Поиск готовых Open Source-решений

Вообще Istio — это огромный комбайн, который решает множество задач: авторизация сервисов и шифрование трафика между ними, маршрутизация запросов, управление балансировкой запросов, трассировка и т. д.

Пример отображения межсервисного взаимодействия в Kiali — Istio
Пример отображения межсервисного взаимодействия в Kiali — Istio

Но минус Istio в том, что он внедряет свои sidecar-контейнеры с envoy между приложениями, то есть по сути вмешивается в трафик. В результате этого могут возникать проблемы. Они, безусловно, имеют свои решения, но использовать Istio в режиме «включил и ничего не сломалось» практически никогда не получается. Что-нибудь обязательно начинает работать не так, как работало ранее. Это не говоря уже о повышенной нагрузке — каждый sidecar потребляет ресурсы CPU и памяти, а также привносит дополнительные, пусть и небольшие, задержки.

Нам хотелось получить какую-то систему мониторинга межсервисного взаимодействия, которая стоит «сбоку» от приложений. Чтобы в случае, если эта система не работает или не успевает обрабатывать трафик и так далее, это никак не влияло на трафик между приложениями, и они продолжили работать.

Что-то подобное на просторах Open Source найти не удалось. Некоторые CNI частично предоставляют такую функциональность, но не в полном объеме. К примеру, у Cilium есть Hubble, показывающий взаимодействие контейнеров. Но, во-первых, он потребляет просто огромное количество ресурсов (об этом мы упоминали в нашей статье), во-вторых, не хранит историю — посмотреть взаимодействие можно только «здесь и сейчас», без ретроспективы.

Так выглядит карта межсервисного взаимодействия в Hubble, модуле Cilium
Так выглядит карта межсервисного взаимодействия в Hubble, модуле Cilium

В Calico есть подобная функция, но в enterprise-версии

График «Network Visibility» в Calico Enterprise
График «Network Visibility» в 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:

Так выглядит дашборд нашего приложения, показывающий взаимодействие между сервисами в пространствах имён
Так выглядит дашборд нашего приложения, показывающий взаимодействие между сервисами в пространствах имён
Карта взаимодействия сервисов на базе Nodegraphapi
Карта взаимодействия сервисов на базе Nodegraphapi

В результате у нас получился очень легковесный инструмент, с помощью которого можно автоматически построить карту взаимодействия между сервисами в Kubernetes. Причем его работа абсолютно никак не влияет на работоспособность самих сервисов и никак не вмешивается в трафик. Также он позволяет мониторить объем передаваемых между сервисами данных и количество открываемых соединений, что условно можно рассматривать как количество запросов к сервису. 

P.S.

Читайте также в нашем блоге:

Автор:
trublast

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js