Блог
Инженерия Автор
18.10.2021

Мониторинг Ceph

Как не пропустить падения и взлёты в жизни кластеров ceph с помощью prometheus или victoriametrics. Теория и практика мониторинга распределенного хранилища.

Теория

Делай алертинг по симптомам, а не по причинам!

(c) Джейсон Стетхем

Возможно вы не в курсе, но ceph — это распределенная система. Система, ключевые сервисы которой хранят состояние. Цеф не просто обрабатывает запросы клиентов, он принимает, хранит и отдаёт данные. При этом строит кворум на протоколе paxos для согласования состояний кластера, использует консистентное хэширование для балансировки нагрузки и данных по частям кластера, использует хартбиты для оценки состояния демонов хранилища.

Мониторинг распределенных систем — это сложно. Распределенные системы сложны, архитектура не тривиальна, так иногда вообще не понятно, работает ли система как надо, или какая-то часть частично сломалась.

Масло в огонь подливают хорошие практики. Мало у кого цеф — внешний сервис, отдаваемый клиентам напрямую как есть. Это инфраструктурный кусок, который используется в глубине продукта. Практики говорят нам: “Мониторьте симптомы, не мониторьте инфраструктуру!”. Хорошо, мы попробуем, спасибо.

Возможно это профессиональная деформация, так как я много работал с цефом для хранения дисков виртуалок, и в меньших масштабах он использовался как бэкенд для самописного S3.

Это правильный подход. Хоть он не всегда применим. Всем давно понятно, что мониторинг делают по симптомам, с точки зрения клиента. Но и мониторить работу системы мониторинга нужно, хотя клиентам абсолютно всё равно работает она или нет. Получать превентивные сообщения о заканчивающемся месте на диске тоже нужно. Как уже было сказано, цеф — это инфраструктура. В этой статье мы посмотрим, что мы можем узнать о ней изнутри.

ExporterDown

Очевидный момент, что первый алерт, который будет настроен — алерт на отвал источника данных. Экспортер работает на всех демонах ceph-mgr.

Активный MGR отдаёт данные, неактивные отдают пустую страницу.

Если экспортер недоступен, то метрики не собираются. Если нет метрик, то мы не можем оценить состояние кластера. Добавляем алерт на такое выражение:

- alert: ExporterDown
  annotations:
    summary: Exporter Down
    details: {{ $labels.job }}
  expr: up == 0
  for: 30s

Здесь и далее подразумевается интервал опроса экспортеров 15 секунд. Система мониторинга — вещь сильно кастомизируемая под процессы в компании, поэтому какие-то моменты могут быть неприменимы. Самое важно в алертах, которые приведены в статье — выражение и причина, по которой выбран for.

Довольно стандартный алерт. Его же можно использовать, например, для node-exporter и для всех экспортеров, которые связаны с критичными сервисами.

Если нет необходимости сообщать о падении одного MGR, можно изменить алерт: count(up{job="ceph-mgr"} == 1) == 0.

PGInactive

Плейсмент группы (placement group, PG) — одна из основных абстракций в ceph. По сути это реплицируемый контейнер с данными. Клиентские данные делятся на кусочки, которые разлетаются в разные PG. Недоступность части PG приводит к недоступности части данных.

Причины недоступности PG могут быть разные. Это могут быть программные ошибки, локальная нехватка ресурсов, флапы демонов OSD, падение большого количества OSD с уменьшением количества реплик ниже минимального. В таких ситуациях кластер не сможет вернуть ответ на запрос к некоторым данным, поэтому нам нужен алерт:

- alert: CephPGInactive
  annotations:
    summary: Ceph PG Inactive
    details: data availability: {{ $value|humanizePercentage }}
  expr: ceph_pg_active/ceph_pg_total < 1
  for: 5m

В алерте мы проверяем не появились ли PG без состояния active. Подробнее о состояниях PG можно почитать тут.

Здесь может появиться желание разбить этот алерт на десяток алертов поменьше. Привязать их к появлению PG в каких-нибудь важных статусах, типа peered, которые не сочетаются с active. Это нахуй не нужно, только лишний шум и больше работы.

Правда в том, что нас волнуют только те состояния, когда PG становится неактивна. Только тогда у клиентов проблемы. Значит и алертинг надо делать на это состояние.

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

Иногда PG могут быть неактивны в течении короткого интервала времени, если проводятся административные операции на кластере. Например, при добавлении OSD в кластер, или в случае падения OSD. На большом кластере количество неактивных PG может быть больше нуля в течении длительного интервала времени. Но в каждый отдельный момент времени неактивны будут разные PG. У нас нет возможности узнать какие именно PG были неактивны, поэтому мы не можем отличить две ситуации:

  1. Одна PG в течении минуты была неактивна.
  2. 1000 PG были неактивны по очереди в течении минуты.

Во второй ситуации клиенты практически не пострадают. Будет кратковременный и малозаметный всплеск латенси на каких-то запросах. В первой же — в течении минуты что-то будет недоступно, а это уже в два раза дольше дефолтного таймаута линукса на дисковую операцию.

Чтобы не получать фалс алармов при втором раскладе, в алерте используется параметр for. Оптимальное значение подбирается на основе статистики. Просто строим график по выражению из алерта и смотрим какой интервал в разных ситуациях приведет к срабатыванию.

HealthError

Кластер ceph сообщает о своем здоровье оператору одной метрикой. Метрика называется ceph_health_status. Это то же самое, что отображается в выводе ceph -s. Здоровье кластера может быть в одном из трёх состояний: здоров, внимание и ошибка:). HEALTH_WARN нас не интересует, потому что он возникает даже тогда, когда ничего не нужно предпринимать. Например, если выставлен флаг noout, запрещающий автоматически выбрасывать OSD из кластера после падения, или флаг nodeep-scrub, который отключает глубокий скраб и помогает избежать проблем с производительностью во время скарббинга в некоторых ситуациях.

Состояние HEALTH_ERR может дать информацию, которую трудно или невозможно собрать иным способом. Ниже список этих событий и их названия в исходниках, которые переводят кластер в ошибку:

  1. Поврежденные PG. PG_DAMAGED
  2. Появление PG в статусе recovery full. PG_RECOVERY_FULL
  3. Ошибки, обнаруженные в результате скраба. OSD_SCRUB_ERRORS
  4. Переполненный пул. POOL_FULL
  5. Переполненный осд. OSD_FULL
  6. Хотя бы у одного ceph-mon процент свободного места меньше, чем параметр mon_data_avail_crit. MON_DISK_CRIT
  7. Ошибка в модуле ceph-mgr. MGR_MODULE_ERROR
  8. Ошибка конфигурации, при которой не соблюдаются неравенства nearfull < backfillfull, backfillfull < full, и full < failsafe_full. OSD_OUT_OF_ORDER_FULL
  9. Коррапт или потеря мета-данных MDS. MDS_HEALTH_DAMAGE
  10. Отсутствие хотя бы одного работающего MDS. MDS_ALL_DOWN
  11. MGR недоступен дольше, чем mon_mgr_inactive_grace. now - first_seen_inactive > g_conf().get_val<int64_t>("mon_mgr_inactive_grace")
  12. Наличие “застрявшего” запроса (REQUEST_STUCK). Не путать с медленным (REQUEST_SLOW)! Застрявшим запрос считается, если он превысил значение произведения двух настроек — mon_osd_warn_op_age * mon_osd_err_op_age_ratio.

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

В цефе периодически случаются баги, из-за которых кластер может нелегитимно переходить в HEALTH_ERR, хотя всё работает нормально. Бороться с этим можно и нужно патчами в апстрим.

Помимо багов, есть и другой неприятный момент. Даже в HEALTH_ERR кластер может переходить в ситуации, которая не достаточно критична. Например, ошибки скраббинга. Стоит ли просыпаться ночью инженеру из-за ошибки скраба? Наверное, нет. Нужно ли отлавливать ошибку скраба, чтобы починить и выяснить причины? Наверное, да. Как отловить эту проблему избегая статуса? Наверное, кастомным скриптом.

Выражение для алерта:

- alert: CephClusterHealthError
  annotations:
    summary: Ceph Cluster Error
  expr: ceph_health_status == 2
  for: 5m

В некоторых случаях такой алерт даёт много фалс алармов. Тогда от него целесообразно отказаться. Замониторить свободное место в пулах и на OSD не составляет труда, как и свободное место под базой у мониторов цефа. Сложнее ситуация с залипшими запросами. Такие запросы не видно в латенси OSD, потому что они ещё находятся в обработке. Получается, что узнать эту информацию можно только сторонним инструментом. Если есть возможность мониторинга запросов на стороне клиента, я рекомендую сфокусироваться на нём.

OSDDown

Упавший OSD-демон — это не всегда авария, но это снижение уровня резервирования и повышение риска отказа в обслуживании. Об этом событии нужно знать:

- alert: CephOSDDown
  annotations:
    summary: Ceph OSD Down
    details: {{ $labels.ceph_daemon }}
  expr: ceph_osd_metadata * on (ceph_daemon,instance)
    group_left() (ceph_osd_up == 0 and ceph_osd_in == 1)

Мы следим не за всеми демонами, а только за теми, которые введены в краш, т.е. находятся в состоянии in. Так отделяется продакшен-окружение в кластере от препродакшена.

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

Почему бы нам не следить за каждым статусом PG и алертить, когда появляются PG в статусе, например, degraded? Причина в том, что на такой алерт невозможно поставить сайленс. Например, при проведении плановых работ, будет невозможно аккуратно притушить алерты.

Плейсмент группы работают на нескольких OSD, а с точки зрения prometheus вообще нет возможности получить информацию о конкретной PG, даже её имя. Сайленс легко ставится на алерт о падении конкретных OSD, после чего проводятся плановые работы. В то же время, алерт на неактивные PG и на возросшую латенси на других OSD, о котором я пишу ниже, останутся в рабочем состоянии. Сайленс работает, кластер мониторится — одни плюсы.

MonQuorum

Мониторы реализуют алгоритм PAXOS. Мониторов должно быть три. Они должны находиться на разных хостах, в разных стойках. Ситуация выхода из строя большей части кворума должна быть минимизирована. Один выпавший монитор — не проблема для клиента. Но с этим надо работать:

- alert: CephMonQuorum
  annotations:
    summary: Ceph Mon Quorum Failed
  expr: ceph_mon_metadata * on (ceph_daemon) group_left()
    ceph_mon_quorum_status < 1

Причины выхода из строя монитора различны и неожиданны.

NearFullOSD

OSD хранят данные на локальном диске и место на нем может закончится. Мы можем высчитать процент, который будет соответствовать порогу near full:

- alert: CephNearFullOSD
  annotations:
    summary: Ceph Near Full OSD
    details: {{ $labels.ceph_daemon }}: {{ printf "%.2f" $value }}% used
  expr: ceph_osd_metadata * on (ceph_daemon,instance) group_left()
    (ceph_osd_stat_bytes_used/ceph_osd_stat_bytes*100) > 85

Этому алерту можно задать for: 15m, потому что показатель может плавать, а получать шквал алертов из-за этого никто не хочет. Развесистое выражение можно вынести в рекорд рул.

Место может закончится либо из-за плохой балансировки, либо из-за плохого планирования. Что делать — использовать балансировщик на upmap, либо срочно добавлять хосты в кластер.

PoolSpace

За местом в пуле тоже нужно следить:

- alert: CephPoolSpace
  annotations:
    summary: Ceph Pool Space
    details: {{ $labels.name }}: {{ printf "%.2f" $value }}% used
  expr: ceph_pool_metadata * on (pool_id,instance) group_left()
    (ceph_pool_bytes_used/
    (ceph_pool_bytes_used+ceph_pool_max_avail)*100) > 60

В примере выставлено 60%, эта величина зависит от процессов в компании, профиля использования кластера и его размера. Срочность такого алерта ниже, чем, например, упавшего демона OSD. Это вполне может Info или Warning или NeSrochno, зависит от вашей системы именования срочности. Выражение можно вынести в рекорд рул и строить по нему красивые графики.

Этому значению также можно задать for больше 0. Потому что не очень интересно наблюдать, как кластер балансирует на пограничном значении.

HighLatency

Самое интересное и сложное — алерты по латенси OSD. Латенси может расти по разным причинам: слишком высокая нагрузка, мисконфиг, проблемы с производительностью железа, потери и ошибки на сети. За латенси нужно следить. И в этот момент вспоминаются красивые рассказы с конференций и из SRE Book о перцентилях и SLA. В контексте нашего хранилища есть нюансы.

Для прома доступно только среднее латенси на запись и чтение с OSD. Но латенси сильно зависит от размера блока данных, который обрабатывается в запросе. Чем больше блок, тем выше будет латенси и тем выше допустимая латенси для такого запроса. К сожалению, у нас нет информации о размерах блока. Ещё мы не знаем худшее значение в кластере на текущий момент. Мы не знаем и лучшее значение и не представляем распределения.

Придётся оперировать средним значением латенси при оценке качества обслуживания. Если у вас есть SLA или SLO на скорость доступа к данным, то вы уже знаете от чего отталкиваться. Придумайте, если его нет. В любом случае, для цефа нужно выбрать какое-то оптимальное значение.

Для этого подготовимся. Нужно оценить профиль нагрузки, возможности бэкенда и требования. Пул под бэкапы на блинах и под метаданные на NVMe. Требования к скорости создания бэкапа довольно низкие. Бэкап делается асинхронно, скорость доступа к каждому конкретному блоку особо не волнует. Важнее максимальная пропускная способность. С метаданными всё наоборот — запросы синхронные и скорость ответа должна быть минимальна.

Латенси HDD на порядки выше, чем NVMe SSD. Профиль нагрузки разный: асинхронная запись большими блоками и единичные случайные запросы к маленьким блокам.

В таком случае очень удобно использовать device class для задания различных лимитов. При этом device class будет учитывать и тип девайса под OSD, и профиль нагрузки, и требования к качеству обслуживания.

Вот пример для ситуации с бэкапами и метаданными:

  1. Пул на NVMe, там лежат какие-то мелкие объекты, доступ к которым должен быть максимально быстрым. Зададим всем OSD в нём класс nvme_meta.
  2. Пул на HDD, используемый под бэкапы, без требований к латенси. Этим OSD зададим класс hdd_backups.

Теоретически это не очень хорошо, что мы расширяем абстракцию. Возможно, для OSD-демонов стоит иметь ещё один параметр для нашей задачи: quality profile. Но это в теории. Сейчас такого нет, поэтому используем device class.

Запишем значение латенси одним правилом:

- expr: ceph_osd_metadata * on (ceph_daemon,instance) group_left()
    (rate(ceph_osd_op_latency_sum[1m])
    /
    rate(ceph_osd_op_latency_count[1m]))
  record: ceph_daemon:ceph_op_latency

Здесь мы осознанно берём rate, а не irate, чтобы сгладить спайки и избежать фалс алармов и флапов.

Дальше пойдем хорошей практикой и создадим метрику-лимит на пару разных девайс-классов:

- expr: 0.015
  labels:
    device_class: nvme_meta
  record: ceph_high_latency:threshold
- expr: 10
  labels:
    device_class: hdd_backups
  record: ceph_high_latency:threshold

Так элегантнейшим образом мы отделим заведомо тормозные OSD от тех, что будут и должны работать быстро.

Пишем алерт:

- alert: CephHighLatency
  annotations:
    summary: Ceph OSD Operations Latency
    details: {{ $labels.ceph_daemon }}: {{ printf "%.2f" $value }}s
  expr: ceph_daemon:ceph_op_latency > on (device_class) group_left()
    (ceph_high_latency:threshold)
  for: 5m

И получаем отличный алертинг по латенси. Кроме одного момента. Если у нас не задан device class, то мы не будем мониторить латенси на этой OSD. Покроем этот кейс:

- alert: CephUndefinedClass
  annotations:
    summary: Undefined Device Class
    details: {{ $labels.hostname }}: {{ $labels.ceph_daemon }}: {{ $labels.device_class }}
  expr: (ceph_osd_up == 1 and ceph_osd_in == 1) * on
    (instance, ceph_daemon) group_left(hostname)
    ceph_osd_metadata{device_class!~"hdd_backups|nvme_meta"}

Теперь всё точно работает.

Как-то сложно всё

Система сложная и во многих моментах приходится идти на компромисс. Мониторинг неактивных PG не даёт представления о том, какой пул затронуло, хотя эту информацию хотелось бы получать легко, чтобы моментально понять кто пострадал.

Мониторинг статуса кластера даёт своеобразные результаты, в документации описано не всё, что он затрагивает. Использовать состояние HEALTH_WARN для мониторинга нельзя, да и HEALTH_ERR не серебряная пуля. Было бы здорово иметь возможность определить что именно приведет к переходу из одного состояния к другому. Так можно было бы оставить только то, что действительно требует вмешательства.

Информации о латенси в том виде, в котором она есть, не всегда достаточно для точечного дебага. Бывают случаи, когда какой-нибудь клиент административными действиями, вроде работы со снапшотами, может зааффектить среднее значение латенси. При этом, рост латенси на самом деле может быть только у этого клиента, остальные не почувствуют проблемы. Мы не можем отделить одно событие от другого.

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

Вместо заключения

Нам удалось построить очень неплохой мониторинг неоднородных кластеров ceph, которые используются под различные задачи. Здесь практически нет информации о cephfs, потому что мы её почти не использовали. Описанных в статье практик должно хватить для хорошего фундамента независимо от кейса.

Если у вас есть возможность инструментировать клиентов кластера – делайте так. Оценивать латенси запросов и количество ошибок на стороне клиента может оказаться гораздо проще и даст более полную картину происходящего, чем использование встроенных в цеф метрик. Избегайте алертинга по причинам, стройте алертинг по симптомам.


₽ с или .