Привет! Меня зовут Григорий, я техлид в 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 месяца данное решение должно окупиться.
Возможно, в вашем случае результаты могут отличаться, потому что количество метрик, частота поиска, задачи, да и инфраструктура от проекта к проекту не идентичны. Надеюсь, наш опыт будет вам полезен.