Всем привет! На связи Максим Набоких, архитектор и технический руководитель Deckhouse Kubernetes Platform. В ноябре 2023 года я выступил на VK Kubernetes Conf, где рассказал, как мы делаем доставку логов в Kubernetes надёжной и отказоустойчивой с помощью Vector. Эта статья — текстовая версия моего доклада.



Мы уже давно используем инструмент построения observability-пайплайнов Vector для сбора логов в Deckhouse Kubernetes Platform. Успешно применяем его в production-окружениях различных клиентов и получаем огромное удовольствие от работы с этим инструментом. Сегодня я хочу поделиться нашим опытом, чтобы те, кто работает с K8s, почувствовали потенциал Vector и смогли воспользоваться его преимуществами в своей работе.

Начнём мы с краткого обзора того, какие типы информации можно собирать в Kubernetes. Затем поговорим о Vector, его архитектуре и о том, почему он нам так нравится. В конце я поделюсь практическими примерами из реального опыта работы с Vector.

Логи в Kubernetes


Основная задача Kubernetes — запуск контейнеров на узлах и управление ими. При этом контейнеры обычно разрабатываются в соответствии с 12 факторами Heroku (перевод на Хабре, простыми словами о 12 факторах). Разберёмся, как именно ведётся работа с логами в Kubernetes, кто ещё их генерирует и где эти логи хранятся.

1. Логи приложений (подов)


Приложения в K8s пишут логи в stdout или stderr. Далее среда исполнения контейнера собирает и хранит эти логи в директории — обычно это /var/log/pods. Такой процесс можно настроить под конкретные нужды:


Движение логов приложений в K8s

2. Логи сервисов на узлах


Кроме того, в окружении Kubernetes есть сервисы, которые выполняются вне контейнеров, включая containerd и kubelet. Мы также заинтересованы в сборе их журналов. А ещё существуют сервисы, которые направляют свои журналы в syslog, такие как журналы аутентификации, создаваемые демоном SSH.

Ещё в некоторых случаях контейнеры пишут логи не в stdout/stderr, а по определённому пути. Например, так работает kube-apiserver со своими журналами аудита. Следовательно, эти логи также надо собирать с соответствующих узлов.

3. События


События — ещё один пример данных, которые можно считать логами. У них уникальная структура, поскольку они существуют только в etcd. Чтобы собрать их, нужно сделать запрос к API Kubernetes. Пример манифеста события:

apiVersion: v1
kind: Event
count: 1
metadata:
  name: standard-worker-1.178264e1185b006f
  namespace: default
reason: RegisteredNode
firstTimestamp: '2023-09-06T19:08:47Z'
lastTimestamp: '2023-09-06T19:08:47Z'
involvedObject:
  apiVersion: v1
  kind: Node
  name: standard-worker-1
  uid: 50fb55c5-d97e-4851-85c6-187465154db6
message: 'Registered Node standard-worker-1 in Controller'

У событий есть поля reason и count, которые позволяют обрабатывать их как метрики:

  • Поле reason идентифицирует событие.
  • Поле count работает как счетчик и увеличивается по мере регистрации последующих событий.

Но события также можно собирать и как трейсы, потому что есть поля firstTimestamp и lastTimestamp. То есть можно создавать комплексные диаграммы Ганта, которые помогают понять, что происходит в кластере.

Также события содержат понятные человеку сообщения (поле message), то есть их можно собирать в виде логов.

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

Знакомство с Vector


Рассмотрим, что такое Vector по основным составляющим:

  • отличительные особенности,
  • архитектура,
  • язык.

Отличительные особенности Vector


На официальном сайте Vector говорится, что это «лёгкий, сверхбыстрый инструмент для построения observability-пайплайнов». Однако мы, как разработчики платформы, хотим немного переформулировать определение и подчеркнуть те функции, которые для нас наиболее актуальны:

Vector — это эффективный инструмент с открытым исходным кодом для построения пайплайнов для сбора логов.

Важные моменты из этого определения:

  • Открытый исходный код — обязательное условие для того, чтобы на его основе можно было создавать надёжные и долговечные решения и рекомендовать их другим.
  • Другой ключевой фактор — эффективность Vector. Если инструмент легковесен, но не способен обрабатывать значительные объёмы данных, он нам не подходит. Аналогичным образом, если инструмент работает быстро, но потребляет много ресурсов, он нам тоже не подходит. То есть эффективность крайне важна.

Стоит отметить, что возможности Vector по сбору других типов данных нам не особо важны — сейчас нас интересуют только логи.

Одна из крутых особенностей Vector — поддержка многих вендоров. Vector принадлежит платформе мониторинга для облачных приложений Datadog, при этом прекрасно интегрируется с решениями других вендоров, в том числе со Splunk, Grafana Cloud и Elasticsearch Cloud. Такая гибкость позволяет использовать одно программное решение сразу со многими вендорами.

Ещё все мы знаем, что, если нужно ускорить приложение, написанное на Go, стоит переписать его на Rust. Но Vector на шаг впереди — он уже написан на Rust и работает быстро. Кроме того, разработчики тщательно следят за производительностью Vector. Как это достигается? CI-система запускает бенчмаркинг-тесты для всех PR в Vector. Разработчики тщательно анализируют влияние новых фич на производительность Vector. Если проявляются негативные эффекты, они сразу просят авторов PR устранить проблемы, поскольку скорость остаётся ключевым приоритетом для команды Vector.

Наконец, Vector — это гибкий строительный блок (подробнее об этом ниже).

Архитектура Vector


Как инструмент обработки Vector собирает данные из различных источников. Он либо сам делает запросы к другим системам, либо выступает в роли HTTP-сервера, который накапливает данные, полученные от других инструментов. Далее Vector преобразует логи: может изменять сообщения, отбрасывать их или объединять несколько сообщений в одно. Затем он направляет их в систему хранения или очередь:


Схема архитектуры Vector: сбор логов, их преобразование и отправка

Примеры языка VRL


В Vector встроен мощный Vector Remap Language (VRL) — язык, который позволяет осуществлять всевозможные преобразования.

Кратко рассмотрим VRL и начнём с фильтрации логов. Приведённый ниже фильтр на VRL отсекает логи с severity, равной info:

[transforms.filter_severity]
type = "filter"
inputs = ["logs"]
condition = '.severity != "info"'

Когда Vector собирает логи подов, он также дополняет строки лога дополнительными метаданными пода, такими как имя, IP-адрес и лейблы. Однако в лейблы подов могут попадать лейблы, которые используются только контроллерами Kubernetes и, следовательно, не представляют никакой ценности для пользователя. Чтобы не тратить ресурсы на их обработку, мы рекомендуем удалять такие лейблы:

[transforms.sanitize_kubernetes_labels]
type = "remap"
inputs = ["logs"]
source = '''
  if exists(.pod_labels."controller-revision-hash") {
    del(.pod_labels."controller-revision-hash")
  }
  if exists(.pod_labels."pod-template-hash") {
    del(.pod_labels."pod-template-hash")
  }

А вот пример того, как несколько строк лога можно объединить в одну:

[transforms.backslash_multiline]
type = "reduce"
inputs = ["logs"]
group_by = ["file", "stream"]
merge_strategies."message" = "concat_newline"
ends_when = '''
  matched, err = match(.message, r'[^\\]$');
  if err != null {
    false;
  } else {
    matched;
  }

В этом случае поле merge_strategies добавляет символ новой строки в поле сообщения. Кроме того, секция ends_when использует VRL-выражение для проверки того, заканчивается ли строка обратным слешем (подобно тому, как объединяются многострочные комментарии в Bash).

Топологии сбора логов


Теперь поговорим о топологиях сбора логов, которые можно использовать с Vector. 

Распределённая топология. В этом случае агенты Vector развёртываются на всех узлах кластера Kubernetes. Затем они собирают, преобразуют и напрямую отправляют логи в хранилище:


Схема работы распределённой топологии сбора логов

Централизованная топология. Агенты Vector также работают на всех узлах, но не выполняют сложные преобразования: этим занимаются агрегаторы. Преимущество этого типа заключается в предсказуемости нагрузки. Можно развернуть выделенные узлы для агрегаторов и при необходимости масштабировать их, оптимизируя потребление Vector'ом ресурсов на узлах кластера:


Схема работы централизованной топологии сбора логов

Потоковая топология. В ней поды Kubernetes «избавляются» от логов как можно скорее. Но напрямую в Elasticsearch логи писать медленнее, потому что он тратит время на их обработку при записи. Однако если поставить промежуточный буфер, например Kafka, то агенты будут писать логи гораздо быстрее за счёт того, что Kafka не делает никакой обработки. А потом можно отдельным вектором — значит, и отдельным экземпляром Vector — переложить все логи из Kafka в Elasticsearch и уже удобно просматривать их:


Схема работы потоковой топологии сбора логов

В остальной части доклада мы рассмотрим Vector в качестве агента для сборки логов с узлов кластера.

Vector в Kubernetes


Теперь разберёмся, как Vector работает в Kubernetes. Для начала посмотрим на под ниже:


Контейнеры Vector в Kubernetes после развёртывания в формате DaemonSet

Схема может показаться слишком сложной, но на то есть своя причина. В этом поде у нас три контейнера:

  1. В первом работает Vector. Его основная цель — сбор логов.
  2. Второй контейнер — Reloader. У пользователей нашей платформы есть возможность описывать собственные пайплайны сборки логов. Специальный оператор берёт заданные пользователями данные и составляет configmap для Vector. Задача Reloader — проверить, что config правильный, и, если так, перезагрузить Vector.
  3. Третий контейнер — Kube RBAC proxy. Он важен, поскольку Vector выводит различные метрики о собираемых логах. Эта информация может быть конфиденциальной, поэтому важно защитить её надлежащей авторизацией.

Vector развёртывается как DaemonSet — его агенты должны быть на всех узлах кластера Kubernetes:

apiVersion: apps/v1
kind: DaemonSet
volumes:
- name: var-log
  hostPath:
    path: /var/log/
- name: vector-data-dir
  hostPath:
    path: /mnt/vector-data
- name: localtime
  hostPath:
    path: /etc/localtime
volumeMounts:
- name: var-log
  mountPath: /var/log/
  readOnly: true
terminationGracePeriodSeconds: 120
shareProcessNamespace: true

Кроме того, к Vector нужно подключить дополнительные директории, чтобы тот мог собирать логи:

  • /var/log, в которой в том числе хранятся логи подов;
  • /mnt/vector-data — директория на хосте для хранения контрольных точек (а также для хранения буфера). Каждый раз, когда Vector отправляет строку лога, он пишет контрольную точку, чтобы избежать дублирования логов, отправляемых в хранилище;
  • localtime, чтобы получить часовой пояс узла.

Несколько примечаний к листингу выше:

  1. При монтировании директории /var/log важно не забыть включить режим readOnly. Эта мера предосторожности предотвращает несанкционированное изменение файлов логов.
  2. terminationGracePeriodSeconds установлен на 120 секунд. Эта задержка позволяет Vector корректно завершить все текущие задачи перед перезапуском.
  3. Чтобы Reloader мог послать сигнал на перезапуск Vector, ему нужен доступ к пространству имён последнего (shareProcessNamespace: true).

На этом развёртывание Vector в Kubernetes завершено. Переходим к самой увлекательной части — практическому применению. Кейсы ниже описывают реальные проблемы, с которыми мы сталкивались в своей практике.

Практические кейсы


Кейс №1. На устройстве закончилось место


Однажды все поды оказались вытеснены с узла из-за нехватки места на диске. Расследование показало, что Vector держит удалённые файлы открытыми. Что произошло:

  • Vector отслеживает файлы в директории /var/log/pods.
  • По мере того как приложение активно пишет в лог, размер файла достигает предела.
  • kubelet ротирует файл лога, возвращая его к исходному размеру.

В то же время:

  • Vector пытается отправить логи в Loki. 
  • Loki не в состоянии справиться с таким большим объёмом данных.
  • Vector как ответственное приложение продолжает попытки отправить все логи в хранилище.

К сожалению, приложения не ждут, пока пройдут все эти внутренние операции: они просто продолжают работать и писать логи. В результате Vector пытается сохранить все файлы неотправленных логов, а поскольку kubelet продолжает их ротировать, свободное место на узле заканчивается:



Как решить эту проблему:

  • Начать можно с настройки параметров буфера. По умолчанию Vector хранит все логи в памяти, если не может отправить их в хранилище, а вместимость буфера ограничена всего тысячей сообщений, что довольно мало. Её можно увеличить до 10 тысяч.
  • Также можно просто отбрасывать новые логи, которые не помещаются в буфер. Если задать поведение буфера drop newest, Vector будет поступать так со всеми логами.
  • Ещё один способ — использовать дисковый буфер вместо буфера в памяти. Его недостаток в том, что Vector будет тратить больше времени на операции ввода-вывода. В этом случае, принимая решение о том, подходит ли вам такой способ, необходимо учитывать требования к производительности.

Здесь поможет потоковая топология. Позволяя логам покидать узел как можно скорее, можно снизить риск сбоев в работе production-приложений. А самые смелые могут увеличить максимальное число открытых файлов для процесса с помощью sysctl:

sysctl -w fs.file-max=1000

Однако мы не рекомендуем такой подход.

Кейс №2. «Взрыв» Prometheus


Во время работы на узле Vector собирает логи подов и экспортирует метрики, такие как количество собранных строк лога, количество возникших ошибок и другие.

Однако у многих метрик есть лейбл `file`, который является потенциально cardinality-бомбой для экспортеров: перезапуск подов на узле приводит к тому, что метрик становится всё больше. То есть Vector продолжает отдавать метрики по подам, которых уже нет в кластере, что является общей проблемой всех экспортеров.

Чтобы решить эту проблему, мы удалили лишние лейблы `file`, используя правила relabeling:

metric_relabel_configs:
- regex: 'file'
  action: labeldrop

После этого Prometheus функционировал нормально. Но через некоторое время мы столкнулись с другой проблемой. Vector потреблял всё больше и больше памяти для хранения всех метрик, и в итоге она заканчивалась. 

Чтобы исправить это, мы использовали глобальную опцию в Vector expire_metric_secs:

  • Если установить её значение, например, на 60 секунд, Vector каждую минуту будет проверять, продолжает ли он собирать данные с этих подов.
  • Если нет, экспорт метрик для этих файлов будет прекращён.

Хотя это решение оказалось эффективным, оно повлияло и на другие метрики, например на метрику ошибок компонентов Vector. 

Давайте рассмотрим на примере. Как видно из приведённого ниже графика, первоначально было зарегистрировано три ошибки, потом ещё четыре. Через время метрика была удалена из выдачи, так как не было новых ошибок. После периода «пусто!» произошли ещё три ошибки, их мы уже не увидим: 



К сожалению, Prometheus, в частности функция PromQL rate и подобные ей, не умеет справляться с такими пробелами в данных. Вместо этого он ожидает, что метрика будет поступать в течение всего периода.

Чтобы устранить это ограничение, мы модифицировали код Vector, полностью исключив лейбл файла. Такой обходной манёвр оказался успешным.

Кейс № 3. Сбой управляющего слоя Kubernetes


Однажды мы заметили, что управляющий слой Kubernetes даёт сбой при одновременном перезапуске подов Vector. После анализа дашбордов мы выяснили, что проблема связана с чрезмерным использованием памяти, в первую очередь etcd:



Чтобы лучше понять причину, нам пришлось погрузиться во внутреннюю кухню API Kubernetes.

При запуске экземпляра Vector тот делает LIST-запрос к API Kubernetes, чтобы заполнить кэш метаданными подов. Как говорилось ранее, Vector использует эти метаданные для обогащения записей журнала:



Соответственно, каждый экземпляр Vector запрашивал у API Kubernetes метаданные подов на узле, на котором был запущен Vector. При этом для каждого отдельного запроса API Kubernetes считывает данные из etcd.

etcd — это база данных типа «ключ — значение». Ключи содержат тип, пространство имён и имя ресурса:

/registry/<kind>/<namespace>/<name>

Vector использует fieldSelector, чтобы запросить только данные подов (в среднем 110 подов) на своём узле у kube-apiserver. Но так как при запросе в etcd никаких fieldSelector нет, нужно получить данные всех подов в кластере, отфильтровать их и отдать Vector его 110 подов. Это приводит к резкому увеличению потребления памяти как для kube-apiserver, так и для etcd и в итоге заканчивается сбоем:



Решить эту проблему можно двумя способами. Во-первых, можно воспользоваться чтением из кэша. В этом случае kube-apiserver будет извлекать данные из своего кэша, а не из etcd. В некоторых ситуациях могут возникать несоответствия, но для инструментов мониторинга это вполне приемлемо. К сожалению, в клиенте Rust для Kubernetes такой возможности не было, поэтому мы отправили Pull Request в Vector с предложением включить опцию use_apiserver_cache=true.

Второе решение предполагает использование механизма равноправного доступа (Priority and Fairness) к API Kubernetes. Он позволяет задать очередь запросов…

\apiVersion: flowcontrol.apiserver.k8s.io/v1beta1
kind: PriorityLevelConfiguration
metadata:
  name: limit-list-custom
spec:
  type: Limited
  limited:
    assuredConcurrencyShares: 5
    limitResponse:
      queuing:
        handSize: 4
        queueLengthLimit: 50
        queues: 16
      type: Queue

… и привязать её к определённым служебным учётным записям:

apiVersion: flowcontrol.apiserver.k8s.io/v1beta1
kind: FlowSchema
metadata:
  name: limit-list-custom
spec:
  priorityLevelConfiguration:
    name: limit-list-custom
  distinguisherMethod:
    type: ByUser
  rules:
  - resourceRules:
    - apiGroups: [""]
      clusterScope: true
      namespaces: ["*"]
      resources: ["pods"]
      verbs: ["list", "get"]
    subjects:
    - kind: ServiceAccount
      serviceAccount:
        name: ***
        namespace: ***

Такая конфигурация позволяет ограничить количество одновременных preflight-запросов и снизить остроту всплесков, тем самым минимизируя их влияние. (Более подробно мы рассматривали эту тему в другой статье.)

Наконец, вместо того чтобы полагаться на API Kubernetes, метаданные подов можно получать с помощью API kubelet. Для этого достаточно отправить запрос на эндпоинт /pods. Однако в Vector эта функция ещё не реализована.

Заключение


В этой статье мы разобрали, какие типы информации можно собирать в Kubernetes, а также поговорили об архитектуре Vector и рассказали, почему он нам так нравится. Ещё рассмотрели практические примеры с реальным опытом работы с Vector.

Мой вердикт: Vector отлично подходит для разработки платформы благодаря своей гибкости, универсальности и широким возможностям по сбору, обработке и передаче логов. Я рекомендую Vector и призываю вас воспользоваться его богатой функциональностью.

P. S.


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



28 марта VK Cloud проведёт новую VK Kubernetes Conf. Защищенность Kubernetes становится приоритетом №1 и первое K8s-событие этого года будет посвящено отказоустойчивости и аварийному восстановлению, уязвимостям кластеров и как с ними бороться. Обсудят и сценарии применения Kubernetes, которые в последние годы набирают обороты, среди них — управление данными и ML. Регистрируйтесь.

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


  1. vitaly_il1
    15.03.2024 12:45
    +2

    Просмотрел статью, но не увидел самого интересного - сравнения Vector с другими решениями.


    1. nabokihms Автор
      15.03.2024 12:45
      +2

      Это нужна ещё одна статья. К сожалению, время на доклад было ограничено.


      1. nabokihms Автор
        15.03.2024 12:45
        +3

        Пометку, что хотелось бы увидеть сравнение, в блокнотике себе сделал.


  1. DANic
    15.03.2024 12:45
    +1

    Специальный оператор берёт заданные пользователями данные и составляет configmap для Vector

    А можно поподробнее, вы написали свой оператор? Или используете разработанный сообществом, насколько я помню vector не делали своего


    1. nabokihms Автор
      15.03.2024 12:45
      +1

      У нас свой, это часть платформы https://deckhouse.io/
      Про техническое решение почитать можно тут. А здесь есть примеры custom resources.


      1. DANic
        15.03.2024 12:45

        Я правильно понял что отдельно от платформы этот модуль использовать невозможно?


  1. AleksUb
    15.03.2024 12:45
    +2

    Отдельное спасибо за текстовый вариант доклада!