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

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

Поиск готовых 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.

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

Комментарии (1)


  1. lexore
    16.12.2022 06:18

    При постоянных (keepalive) соединениях, идея "соединение"~="запрос" перестает работать. Но подсчет байтов продолжит работать.

    Можно получить приблизительное количество запросов из объема трафика, если запросы не сильно различаются по размеру. Делим трафик на средний размер запроса, получаем некоторое количество попугаев.