Как мы пытались подружить VictoriaMetrics и Thanos (и у нас почти получилось) +4


Источник изображения — СоюзМультфильм ©
Источник изображения — СоюзМультфильм ©

Привет! Меня зовут Григорий, я техлид в Cloud Infrastructure Team в Сравни. Моя команда отвечает за observability системы и облачную инфраструктуру. Не так давно мы полностью обновили наш стек мониторинга. Хочу рассказать, как у нас организовано хранение long-term метрик без использования Object Storage.

Мы в Сравни долгое время использовали связку Prometheus + Thanos для мониторинга и хранения данных. Для Thanos мы использовали схему с sidecar’ом. Эта схема работала довольно неплохо, но с ростом проекта — росло и потребление ресурсов. Со временем задачи по scrape samples уже потребляли значительные ресурсы. Когда только на Prometheus стало уходить больше 30 ядер vCPU и 100 гигабайт RAM, мы начали искать способы оптимизации потребления ресурсов. 

Первым делом определили требования, которые необходимы для системы мониторинга:

  • должно поддерживаться развертывание в Kubernetes;

  • система должна быть способна переезжать из одного Kubernetes-кластера в другой без потери данных;

  • нужна поддержка downsampling;

  • возможность построить high availability систему;

  • в идеале, чтобы система требовала очень мало внимания на обслуживание ;)

Мы поизучали варианты, и сперва показалось, что будет хорошей идеей взять стек VMAgent + Thanos receiver. Как несложно угадать из названия статьи, этого у нас не получилось. Недавно я увидел в одном профессиональном чате, что коллеги захотели использовать такой же стек и по тем же причинам, что и мы. Поэтому решил поделиться нашим опытом и рассказать, к чему мы в итоге пришли.

Причины выбора VMAgent + Thanos receiver

Начнем с агента, осуществляющего сбор samples. В Prometheus для каждого remote write создается in-memory очередь, в которую записываются samples из журнала (WAL). Данные в очереди хранятся в течении двух часов, и это поведение не изменить. Но есть issue на изменение такого поведения. 

Забегая вперед, при выборе данного стека, каким-то образом мы упустили один, казалось бы, очевидный момент в схеме с Thanos Receiver. То есть по факту мы бы просто перешли бы из pull на push модель (о плюсах и минусах можно посмотреть здесь), получили возможность поддержки мультитенантности с global view, а также исключили ситуацию, когда sidecar может перестать заливать time series в object storage. И вряд ли бы получили бы экономию в ресурсах (как записывали изначально в своих целях). 

Когда поняли это, мы приняли решение всё же продолжить тестирование данной схемы, но уже параллельно продумывая другое решение.

VMAgent умеет кешировать samples на диск в случае проблем в сети или с удаленным стораджем, и потом отправлять их, как только связь с удаленным стораджем восстановится — это очень удобно. Размер буфера ограничивается размером диска. Также есть опция -memory.allowedPercent – позволяет регулировать допустимый процент системной памяти, которую могут занимать кэши метрик (по умолчанию 60% от доступной памяти). И да, VictoriaMetrics умеет в автоматическое определение лимитов памяти и процессора под управлением cgroupv2.

Протестировав Prometheus и VMAgent на одинаковых количествах samples, мы увидели, что VMAgent потребляет меньше ресурсов, чем Prometheus (к сожалению, результаты тестов не дожили до написания данной статьи).

В связи с вышесказанным, для наших задач VMAgent подходил больше, поэтому мы остановились на нем, как на агенте, осуществляющем сбор samples и сохраняющем их в удаленный сторадж.

Thanos же выбрали по следующим причинам:

  • Community edition версия поддерживает downsampling (VMStorage поддерживает только в Enterprise-версии);

  • есть поддержка работы с Object Storage (изначально нам казалось, что это единственный способ не терять данные без downtime при переезде стораджа между кластерами).

Начало тестирования

Начали мы, конечно же, с поиска issue и чтения документации. Вооружившись этими сакральными знаниями и с помощью helm chart’ов, все необходимые компоненты развернули в Kubernetes, и схема довольно быстро заработала (подозрительно!).

Схема работы c Thanos Receiver мало чем отличается от Thanos с sidecar. Если вам интересно более подробно разобраться в компонентах Thanos и VictoriaMetrics, рекомендую прочитать две статьи Александра Валялкина — на хабре, и на medium. Я кратко пройдусь по компонентам, останавливаясь на тех частях, которых нет в статьях Александра.

VMAgent собирает samples из таргетов по pull-модели. После этого VMAgent отправляет samples в Thanos Receiver (на самом деле, отправляет на L7 балансировщик нагрузки, а тот – уже по round robin на Thanos Receiver). Thanos Receiver может состоять из одной или нескольких реплик (далее буду называть их эндпоинтами). Для балансировки нагрузки, репликации данных и service discovery используется файл hashring configuration. Выглядит он так – внутри перечислены эндпоинты, tenant ID и порты. 

Thanos Receiver бывает soft tenancy и hard tenancy. 

  • Soft tenancy – запросы, tenant ID которых явно не совпадает ни с одним другим эндпоинтом в конфигурационном файле, автоматически попадают в это хэш-кольцо soft tenancy; то есть по факту это тенант по умолчанию, если не найдется нужный эндпоинт. К слову, id данного тенанта задается через переменную receive.default-tenant-id. А еще данный тенант может выступать как балансировщик (в документации значится как Load distribution) к hard tenancy. 

  • Hard tenancy – если просто, это те эндпоинты, у которых установлен tenant ID в каждом HTTP-запросе. Данный механизм нужен для мультитенантной архитектуры Thanos. В нашем случае не было таких задач, и мы везде использовали Soft tenancy Thanos Receiver.

На тестовым стенде Thanos Receiver состоял из четырех реплик (по две реплики в каждой из зон доступности) и хранил данные в течении трёх дней (-tsdbRetention=3d). Почему решили хранить данные на Thanos Receiver три дня? Наш анализ показал, что чаще всего используются метрики в течении трех дней (скажем так, горячие метрики) и чтобы ускорить их выдачу пользователям, было принято решение оставить метрики до трех дней на SSD-дисках. 

Thanos Receiver, получив samples от VMAgent, отправляет данные time series на другие эндпоинты, которые указаны в hashring configuration файле. Если какой-либо time series в запросе на запись на эндпоинт Thanos Receiver не был успешно записан по крайней мере на (ReplicationFactor / 2) + 1, то Thanos Receiver отвечает ошибкой к VMAgent. Например, при replication-factor=3 Thanos Receiver убедится, что каждый time series успешно записан по крайней мере на двух Thanos receivers в эндпоинтах hashring файла ( 3/2 + 1 = 2,5 = 2).

Еще одно отличие от схемы с sidecar – в том, что Thanos Query делает запросы вместо sidecar в receiver (помимо Store Gateway), и агенту (Prometheus или VMAgent) необходимо отправить данные в Thanos Receiver (в схеме с sidecar, сам Thanos Sidecar сохранял данные в Object Storage).

Но есть нюанс…

В целом, вышеописанная схема сработала для нас. Но дьявол кроется в мелочах, и для нас они стали критичными.

Нюансы номер раз и два

Проблемы начались при удалении пода одного из эндпоинтов Thanos Receiver — тестировали случаи, когда под с Thanos Receiver находился на ноде, в которой произошел сбой. Например, инженер ошибся и удалил ноду из кластера, сработал дешедуллер, и нода удалилась, или по каким-то причинам нода с подом Thanos Receiver упала. 

Мы стали периодически получать 409 ошибки в VMAgent, а в логах Thanos Receiver увидели: hashring has changed; server is not ready to receive web requests.

Затем мы решили проверить случай отказа кластера или большей его части – отключили три ноды, на которых были поды с Thanos Receiver и логи перестали писаться. Тут все логично. 

Вернули все три ноды в работу, но логи не появились в VMAgent, и были видны 409 ошибки. В логах: Error on ingesting samples that are too old or are too far into the future. В документации есть описание данной проблемы и ее причины, но нет решения.

Обе проблемы помог решить thanos-receive-controller, который отслеживает количество running подов с лейблом controller.receive.thanos.io=thanos-receive-controller (можно поставить и свой, разумеется) и обновляет configMap c hashring configuration файлом, и с задержкой (по нашим тестам, примерно через 120 секунд) обновляется в контейнере. После этого Thanos Receiver перечитывает конфиг. Важно — не монтировать configMap как subPath, иначе не обновится файл в контейнере; подробнее о нюансах configMap можно почитать в блоге Фланта

Учтите, что у данного подхода с контроллером есть и минусы.

Нюанс номер три

Отключили все ноды с подами Thanos Receiver больше, чем на 2 часа; помним, что Thanos Receiver – это реализация над Prometheus Remote Write API, со всеми вытекающими. Получили проблему с тем, что Thanos Receiver отбрасывает логи. 

У этой проблемы пока что нет решения; это нужно учитывать при эксплуатации.

Нюанс номер четыре

Решили добавить реплик VMAgent (мы же все за высокую доступность всех компонентов, да ведь?)

Но для дедупликации Thanos Querier использует query.replica-label. И есть требование, чтобы для каждого агента должен отличаться extrnal_labels. Значит, на каждой реплике VMAgent он должен отличаться. 

Решение нашлось довольно быстро: в конфиге VMAgent проставляем переменную окружения POD_NAME:

external_labels:
  replica: %{POD_NAME}

И в statefulSet VMagent пробросить имя пода в переменную окружения:

- name: POD_NAME
  valueFrom:
    fieldRef:
      fieldPath: metadata.name

Все заработало.

Нюанс номер пять

Для двух реплик VMAgent и сбоев в сети была ещё одна проблема – уезжали метрики. Иногда на час, иногда на два часа — с дельтой во времени. 

Мы так и не смогли понять причину этого явления. Но при использовании одной реплики VMAgent такой проблемы не наблюдалось. 

Если вы знаете возможную причину такого поведения, буду рад прочитать об этом в комментариях).

Суммируем нюансы

На основании результатов тестирования мы приняли решение не тащить связку VMAgent + Thanos receiver в прод. 

Вышеописанные нюансы в Thanos проявляются из-за того, что данный вид стораджа не поддерживает backfilling – вставку данных, соответствующих временной метке в прошлом. Тут есть некоторые вещи, которые можно сделать, начиная с версии Prometheus v2.24.0. Если интересны объяснения на примерах, как это работает, можно посмотреть тут и тут

А что с VictoriaMetrics stack?

Как я говорил выше, параллельно с тестированием Thanos Receiver, мы начали изучать другие варианты и решили сконцентрироваться на VictoriaMetrics.

Одной из фич VictoriaMetrics является поддержка Backfilling из коробки. Поэтому проблемы с out-of-order samples при использовании стека VictoriaMetrics нам не страшны. Это же позволяет использовать параллельные очереди для каждого remote write стораджа, если одной очереди недостаточно для отправки большого объема собранных данных в удаленное хранилище. При этом в Thanos мы всегда должны использовать только одну очередь, иначе получим out-of-order samples

За время тестирования мы выяснили, что VictoriaMetrics больше подходит под наши задачи. Поэтому нам осталось только решить, как перенести данные из одного кластера k8s в другой. 

Первый и очевидный вариант — использовать vmbackup. Но это подразумевает временный простой, когда будут переноситься данные из одного кластера в другой. А нам нужна была возможность переноса VMStorage между кластерами по очереди и с последующим переключением stateless-сущностей (vmagent, vmselect, vminsert).

Мы используем managed kubernetes в облаках, поэтому нам нужно было, чтобы решение поддерживалось обоими нашими облачными провайдерами (у нас это Яндекс.Облако и Azure). Изучив документацию наших облачных провайдеров, обнаружили, что оба вендора поддерживают возможность добавления static PersistentVolume (подробнее – для Azure тут, для ЯО тут). В итоге мы пришли к следующей архитектуре:

  • через terraform создаем диски в облаке;

  • создаем объект PersistentVolume в Kubernetes под каждый созданный диск. 

Так как StatefulSet при создании пода имеет уникальный упорядоченный идентификатор, то при использовании volumeClaimTemplates мы можем описать, какое имя нашего тома будет использоваться с указанием claimName (оно тоже упорядоченное, и будет использоваться тот же идентификатор что в поде), и таким образом, не будет запускаться провижинер для создания новых томов PersistentVolume. 

В этой схеме правильное наименование claimRef очень важно. Ниже — немного упрощенный конфиг, для понимания. В примере созданы две PersistentVolume с отличающимися claimRef и volumeHandle (индентификатор нашего диска) и один kind StatefulSet c volumeClaimTemplates.

---
apiVersion: v1
kind: PersistentVolume
metadata:
 name: prod-yc-vm-storage-0
spec:
 capacity:
   storage: 100Gi
 accessModes:
   - ReadWriteOnce
 persistentVolumeReclaimPolicy: Retain
 storageClassName: yc-network-ssd
 csi:
   driver: disk-csi-driver.mks.ycloud.io
   fsType: ext4
   # See it in terraform.
   volumeHandle: qwerty0
 claimRef:
   namespace: monitoring-system
   name: pvc-vm-vmstorage-0
---
apiVersion: v1
kind: PersistentVolume
metadata:
 name: prod-yc-vm-storage-1
spec:
 capacity:
   storage: 100Gi
 accessModes:
   - ReadWriteOnce
 persistentVolumeReclaimPolicy: Retain
 storageClassName: yc-network-ssd
 csi:
   driver: disk-csi-driver.mks.ycloud.io
   fsType: ext4
   # See it in terraform.
   volumeHandle: qwerty1
 claimRef:
   namespace: monitoring-system
   name: pvc-vm-vmstorage-1
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
 name: vmstorage
 namespace: monitoring-system
spec:
 selector:
   matchLabels:
     app: vmstorage
 serviceName: "vmstorage"
 replicas: 2
 podManagementPolicy: OrderedReady
 template:
   metadata:
     labels:
       app: vmstorage
       app.kubernetes.io/name: vmstorage
       app.kubernetes.io/instance: vmstorage-b
       app.kubernetes.io/version: "v1.74.0"
   spec:
     containers:
     - name: vmstorage
       image: "victoriametrics/vmstorage:v1.74.0-cluster"
       volumeMounts:
       - name: pvc-vm
         mountPath: /storage
 volumeClaimTemplates:
 - metadata:
     name: pvc-vm
   spec:
     accessModes: [ "ReadWriteOnce" ]
     resources:
       requests:
         storage: 100Gi
     storageClassName: yc-network-ssd

Теперь мы можем переносить VMStorage из одного кластера Kubernetes в другой, поочередно уменьшая реплики StatefulSet в одном кластере с удалением PersistentVolume и добавляя PersistentVolume с увеличением реплик в другом, сопровождая это изменением конфигурации в VMSelect и VMInsert. 

В этой схеме есть два нюанса: 

  • Автоматизацию этого переключения мы пока не сделали, отложив эту задачу в бэклог — до времени, когда такая ситуация понадобится. Данное решение — скорее задел на будущее.

  • Чтобы схема заработала, необходимо между кластерами обеспечить pod-to-pod связанность без NAT (в наших облаках такая возможность есть), либо при переезде на каждый под прокинуть по сетевому балансировщику, создавая таким образом сетевую доступность между VMSelect/VMInsert и VMStorage в разных кластерах.

Взяв за основу статью Resizing StatefulSet Persistent Volumes with zero downtime, мы отработали схему с задачей по увеличению размера диска для VMStorage. 

Ниже – упрощенный процесс без использования terraform в Яндекс Облаке:

  • удаляем kind StatefulSet VMStorage оставляя “сиротские” поды без управления контроллера StatefulSet.

k delete sts --cascade=orphan vmstorage

  • удаляем pod с самым высоким id (в нашем пример два пода, у одного имя vmstorage-0, у второго vmstorage-1). Удалять под приходится из-за того, что наше облако не дает возможность увеличить размер диска, если он примаунчен.

k delete pod vmstorage-1

  • увеличиваем размер диска в облаке до 110G.

yc compute disk update --size 110G --id qwerty1

  • патчим PVC, увеличивая сторадж до 110G.

k patch pvc pvc-vm-vmstorage-1 --patch '{"spec": {"resources": {"requests": {"storage": "110Gi"}}}}'

  • деплоим StatefulSet не изменяя размер storage в storageClassName.

cat <<EOF | kubectl apply -f -
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
 name: vmstorage
 namespace: monitoring-system
spec:
 selector:
   matchLabels:
     app: vmstorage
 serviceName: "vmstorage"
 replicas: 2
 podManagementPolicy: OrderedReady
 template:
   metadata:
     labels:
       app: vmstorage
       app.kubernetes.io/name: vmstorage
       app.kubernetes.io/instance: vmstorage-b
       app.kubernetes.io/version: "v1.74.0"
   spec:
     containers:
     - name: vmstorage
       image: "victoriametrics/vmstorage:v1.74.0-cluster"
       volumeMounts:
       - name: pvc-vm
         mountPath: /storage
 volumeClaimTemplates:
 - metadata:
     name: pvc-vm
   spec:
     accessModes: [ "ReadWriteOnce" ]
     resources:
       requests:
         storage: 100Gi
     storageClassName: yc-network-ssd
EOF
  • повторяем пункты 1-4 д1я vmstorage-1.

  • После чего деплоим манифест с увеличенным storage в storageClassName.

cat <<EOF | kubectl apply -f -
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
 name: vmstorage
 namespace: monitoring-system
spec:
 selector:
   matchLabels:
     app: vmstorage
 serviceName: "vmstorage"
 replicas: 2
 podManagementPolicy: OrderedReady
 template:
   metadata:
     labels:
       app: vmstorage
       app.kubernetes.io/name: vmstorage
       app.kubernetes.io/instance: vmstorage-b
       app.kubernetes.io/version: "v1.74.0"
   spec:
     containers:
     - name: vmstorage
       image: "victoriametrics/vmstorage:v1.74.0-cluster"
       volumeMounts:
       - name: pvc-vm
         mountPath: /storage
 volumeClaimTemplates:
 - metadata:
     name: pvc-vm
   spec:
     accessModes: [ "ReadWriteOnce" ]
     resources:
       requests:
         storage: 110Gi
     storageClassName: yc-network-ssd
EOF

Downsampling? 

Как было сказано выше, downsampling в VictoriaMetrics используется в enterprise-версии.

Хотя разработчики оставили хак для этой фичи, мы придумали очередной костыль пошли своим путем: добавили отдельные группы VMAgent со scrape Interval в 5 минут (для графиков до 3х месяцев) и сохраняем в отдельную группу VMStorage c соответствующим dedup.minScrapeInterval (о причинах наличия отдельных VMStorage ниже). 

Таким образом, мы делаем некое подобие downsampling. А с учетом того, что VictoriaMetrics довольно хорошо сжимает time series — это позволяет экономить на утилизации места на диске. 

Вообще, по поводу downsampling и как оно работает — есть хорошее выступление у команды Thanos.

Нюансы VictoriaMetrics

  • Если в Thanos для дедупликации важно, чтобы у агентов был разный external label, то для VictoriaMetrics он должен быть идентичным.

  • При использовании двух VMAgent, которые скрейпят одни и те же данные, и пишут тот же набор VMStorage (точнее в VMInsert'ы, а те уже в VMStorage), стали замечать, что графики при обновлении странички начинают “скакать”. Это связано это с тем, что два VMAgent'а скрейпят одни и те же samples с дельтой по времени (т.е. не одновременно). Одно из решений - шардирование. Деплоя VMAgent как statefulset и указывания уникальное значение promscrape.cluster.memberNum=%{POD_NAME}. Второе решение - при использовании дедупликации в VMStorage и VMSelect dedup.minScrapeInterval желательно выставлять большее значение чем ScrapeInterval в VMAgent. Так же есть issue при работе VMAgent в разных AZ делающих scrape samples идентичных эндпоинтов (это кстати, наш случай). Данная issue решается с выставлением уникального значения promscrape.cluster.name с версии v1.78.0.

Более подробно про дедупликацию описано в документации.

  • Репликация:

    • В отличии от Thanos Receiver, VMStorage не находятся в одном кластере, и вообще ничего не знаю друг о друге. Репликация time series происходит на стороне VMInsert, который и сохраняет эти данные в VMStorage. Вычисляется репликация по формуле 2N-1, где N – это replicationFactor. То есть репликация на несколько хостов будет происходить, если replicationFactor будет больше единицы. Например: replicationFactor=1 (21-1=1) – сохраняем данные на один VMStorage, replicationFactor=2 (2*2-1=3) –  сохраняем данные на три VMStorage. 

    • VMInsert реплицирует данные синхронно на VMStorage (ожидает, пока данные запишутся во все VMStorage). Так что если не наберется необходимое количества нод на запись, запись не будет совершена.

    • VMSelect отправляет запросы на все VMStorage, которые указаны в параметре storageNode и ждет ответа от <количество VMStorage> - replicationFactor + 1. Такая настройка позволяет ускорить отдачу ответа, не дожидаясь ответа от VMStorage. Но replicationFactor в VMSelect работает хорошо, когда все хорошо на VMStorage нодах хранятся равное количество копий всех данных выставленных при replicationFactor. Но если, например, вы вывели на обслуживание ноды с VMStorage или добавили новые – вы можете получать пропуски данных на графиках. На это есть issue. Поэтому в таких случаях лучше убрать параметр replicationFactor из VMSelect, чтобы VMSelect дожидался ответа от всех VMStorage нод перед отправкой ответа на запрос.

    • Чем больше replicationFactor, тем больше потребуется ресурсов CPU, RAM и места на диске

  • VMInsert и VMStorage сжимают данные перед отправкой их, что естественно, сказывается на утилизации CPU. Если вы сильно ограничены в ресурсах CPU, отключить это поведение можно через аргумент -rpc.disableCompression.

  • Для деплоя в Kubernetes в компании используются как кастомные написанные helm-чарты, так и вендорные, в которых используется kind ServiceMonitor для service discovery в prometheus. VMAgent не умеет работать с данным типом CRD. Мы не стали переписывать наши кастомные чарты (и тем более вендорные), а просто задеплоили VMOpertor который умеет конвертировать CR ServiceMonitor в сущности VictoriaMetrics. Это не отменяет того факта, что CRD ServiceMonitor необходимо задеплоить в Kubernetes, так как данное CRD не идет в поставке с VictoriaMetrics). Данная функциональность облегчает переезд с Prometheus на VictoriaMetrics.

Профит от миграции 

Изначально для переезда мы рассматривали стек VMAgent + Thanos Receiver, но он не подошел нам по ряду причин – тех самых нюансов, которые в нашем случае были важны. Критичным оказалось наличие механизма backfilling, который есть из коробки в VictoriaMetrics, куда мы в итоге и мигрировали. VictoriaMetrics подкупила еще более простой архитектурой.

В результате миграции мы снизили затраты на ресурсы по RAM примерно в два раза; по CPU — примерно в полтора раза; на сторадж — примерно в три раза. Это произошло по той причине, что в Object Storage взимается плата не только за хранение данных, но и за количество запросов. В нашем случае, переход на диски привел к экономии ресурсов.  

Также бонусом мы получили отдачу метрик быстрее — примерно на секунды 2-3 (проверяли на графиках нашей инфраструктурной команды), в сравнении со схемой с Thanos Sidecar.

На данный момент экономия составила порядка 60 000 рублей в месяц. По времени исследование заняло около двух с половиной недель. Так что примерно через 3-4 месяца данное решение должно окупиться.

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




Комментарии (0):