
Привет, Хабр! Меня зовут Игорь Шишкин, я руковожу отделом разработки облачной платформы Рег.облака и архитектором SDS в Рунити.
Ранее я уже рассказывал про то, как мы выбирали SDS (Software Defined Storage), почему остановились на Ceph, а также о наших процессах в R&D. В этой статье , поделюсь, что мы поймали за год в продакшене, какие решения в дизайне кластеров оказались ошибочными, как это изменило нашу референсную архитектуру и к чему мы пришли в итоге.
Скрытый текст
Эта статья написана по мотивам моего доклада на конференции DevOpsConf 2026.
Навигация по тексту:
Сразу контекст:
Большая часть кейсов в этой статье — про S3/RGW.
У нас несколько кластеров Ceph: в каждом регионе есть инфраструктурный кластер для внутренних задач, публичный кластер, плюс ситуативно появляются кластеры поменьше.
Легаси нет совсем — на текущий момент все наши кластеры уже обновлены доSquid. Прямо сейчас Reef не осталось нигде, но некоторые кейсы в статье были пойманы еще на Reef, и мы прицеливаемся на следующий релиз — Tentacle.
Во многих случаях мы имеем дело с труднопрогнозируемой нагрузкой: она привязана к росту аудитории и к ее спросу на наши услуги, а не к внутренним кейсам, где мы могли бы сами прикинуть сколько будет RPS/bandwidth.
The Референсная Архитектура
Прежде чем перейти к кейсам — пара слов о том, что мы называем референсной архитектурой.
Референсная архитектура — это набор верифицированных конфигураций кластеров Ceph: топология, применяемое оборудование, настройки базовой ОС и самого Ceph. Она живёт как документация со своим собственным версионированием по CalVer, которую мы периодически или ситуативно обновляем, регулярно пересматриваем и актуализируем по мере эксплуатации Ceph и новых исследований.
Например, одна из референсных конфигураций выглядит так:
2×40 Гбит/с+, LACP поощряется, 4+ ядра CPU, 256+ ГБ RAM, 8×HDD 22 ТБ, 4×NVMe SSD 15 ТБ, HBA. HSD OSD: 2,5 ТБ NVMe под DB+WAL, 100% HDD под данные.
Для S3: два storage class — STANDARD на EC4+2 @ NVMe, STANDARD_IA на rf=3 @ HSD.
Бюллетени обновления. Инструкции по миграции между версиями — как самого Ceph, так и нашей конфигурации. Каждый из них и представляет собой новую версию по CalVer: когда что-то в референсной архитектуре получает изменение, выходит бюллетень с описанием: что применить, куда записать, на что обратить внимание.
Known issues. Описания известных проблем в Ceph и/или наших конфигураций, способы как с ними жить, как не допустить (если возможно) и как чинить, если что-то пойдёт не так.
Мы поддерживаем (в большинстве случаев) 2 версии референсной архитектуры одновременно. Всё, что старше, либо самостоятельно поддерживают команды эксплуатации, либо последовательно догоняют до актуальной конфигурации. Это сделано ради унификации — чтобы не плодить выродившиеся конструкции, которые живут своей жизнью и никто не знает, что с ними делать.
Кейсы: HSD
HSD (Hybrid Storage Device) — так мы называем OSD с вынесенными разделами DB и WAL на отдельные устройства, в нашем случае, на NVMe SSD, а сами данные — на обычные вращающиеся HDD (но, естественно, не десктопные, а enterprise grade). Таким образом мы выносим RocksDB на быстрый диск, хорошо справляющийся со случайной записью и случайным чтением, а также WAL для быстрых операций записи.
Наш кейс: берем сервер с 12 дисками, из которых 4 слота гибридные (умеют и SATA, и NVMe), кладём WAL и DB на NVMe, данные — на шпиндели, получаем плотную конфигурацию с хорошим объемом
Наш первый вариант конфигурации: 2×NVMe SSD + 10×HDD, то есть 5 HDD на каждый NVMe, делим LVM’ом — и всё работает. Какое-то время.


При росте нагрузки очередь на NVMe-дисках начинает набиваться, и появляется SLOW_OPS из-за флуктуацией латентности на OSD.
Маленький оффтоп: как мы вообще ожидаем увидеть проблему нагрузки с диском? Наверное самое ожидаемое место поиска — %util в iostat или dstat. Но это мимо: %util показывает суммарное время, которое процессы провели в состоянии D с использованием IO этого диска, — и с заполненностью очереди может не коррелировать практически никак.
За размер очереди отвечает параметр aqu-sz в iostat — он усредненный, но позволяет оценить заполняемость. Если видите значения в районе 6–8–10, очередь уже неплохо набита. Такая ситуация называется Queue contention: несколько процессов ceph-osd конкурируют за однопоточную очередь, операции накапливаются — возникает эффект бутылочного горлышка.
В новых версиях референсной архитектуры мы перешли на NVMe-неймспейсы вместо LVM — для распараллеливания IO разных OSD — и ограничили количество HDD до 2–3 на каждый NVMe-диск. Это нам позволило полностью исправить данную проблему.
Кейсы: EC @ HSD
Erasure coding существенно снижает накладные расходы по месту, но ценой большего write- и read-amplification. Давайте посчитаем для, например, 100 OSD — это простой и тривиальный расчёт для демонстрации порядков на самом низком слое, то есть со стороны самих дисков. Реальная производительность кластера на каждом уровне будет отличаться.
HSD-диски выдают порядка 100–200 IOPS каждый, как собственно и обычные HDD, возьмем среднее — 150 IOPS. Тогда:
EC4+2:
Write = 100 OSD × 150 IOPS / 6 ≤ 2 500 IOPS
Read = 100 OSD × 150 IOPS / 4 ≤ 3 750 IOPSrf=3:
Write = 100 OSD × 150 IOPS / 3 ≤ 5 000 IOPS
Read = 100 OSD × 150 IOPS / 1 ≤ 15 000 IOPS
Важный нюанс: read и write нельзя суммировать — это взаимоисключающие сценарии в пределах расчета, а коэффициент их связанности не определяется так просто. Разница по чтению между EC4+2 и rf=3 — четырехкратная, по записи — двукратная. Это существенно.
И это еще до учета того, что одна операция чтения объекта в RGW на самом деле раскладывается в несколько операций под капотом. Конкретно — для GetObject флоу выглядит следующим:
аутентификация и авторизация (meta pool);
поиск объекта по индексу бакета (index pool);
чтение самого объекта (data pool);
запись в op log (log pool) — опционально (если включен op log).
При этом EC-amplification применяется только к data pool — все метаданные мы держим в replicated-пуле. Но нагрузка на index pool тоже высока: в реальном кластере в спокойное время суток — 1,4 ГБ/с на чтение из индекса против 2,7 ГБ/с из данных. Это не статистика, а реальная картина из нашего кластера: смотрите, кто реально «ест» ресурс.

Вывод: erasure coding на HDD существенно снижает доступную ёмкость по IOPS. Если спрос на IOPS непредсказуем или просто велик — это не лучший выбор для такого оборудования.
Кейсы: Cache tiering
Идея Cache tiering простая: поставить быстрый SSD-пул в качестве кэша перед более медленным HDD/HSD-пулом. Получив кластер с EC @ HSD и понимая, что IOPS там немного, мы задались вопросом: а не поможет ли cache tiering? Спойлер: да, но нет.
Хорошо работает при частой перезаписи одного объекта, пока он ещё в кэше, или при повторном чтении сразу после записи. При append-only паттерне — плохо. У нас 80% по объему — бэкапы, подавляющее большинство операций — добавление новых объектов. При таком паттерне кэш-пул набивается, данные нужно flush'ить в base tier, и если там не хватает производительности — операции встают.
Есть и особый нюанс: при достижении неявного порога размера кэш-пула происходит заморозка IO-операций. RGW как честный RADOS-клиент ждёт завершения операции. Таймаута в этом месте по сути нет — есть только HTTP-таймаут снаружи. В плохом случае клиент просто висит, а cache manager не успевает разгребать.
И главное: начиная с Reef, cache tiering официально deprecated. В документации так и написано — стоит верить документации. Баги никто чинить, скорее всего, уже не будет.
Кейсы: Exhausted sockets
У нас классическая схема: nginx как reverse proxy с proxy_buffering off, за ним RGW. На каждый входящий запрос nginx создает новое соединение к RGW.
В пиках накапливалось больше 30 тысяч сокетов в состоянии FIN_WAIT. Это штатный TCP-статус — завершение запроса и ожидание освобождения ресурсов. При большом количестве запросов они не успевают освобождаться, и nginx не может открыть новый исходящий сокет в сторону RGW. Увеличивать количество нод с RGW смысла не было, так как текущее количество фронтендов еще не исчерпало себя ни по сети, ни по памяти, ни по CPU. От nginx избавляться — тоже, потому что мы хотим чтобы там был прокси, дающий нам дополнительное логирование, всегда отвечающий клиентам и при этом позволяющим добавлять новые механики в сервис (например, rate limiter’ы и локальные кеши).
Что помогло:
Расширили диапазон ephemeral-портов — немного, но стало легче.
Уменьшили time-wait timeout для сокетов — сократилось время ожидания освобождения.
Включили upstream keepalive в nginx — сработало наиболее эффективно. Соединения перестали пересоздаваться на каждый запрос, сокеты перестали «жечь».
Используем Auth API и Cache API в RGW — для кэширования данные локально на дисках RGW-нод средствами самого nginx, снижая нагрузку на RGW.
Кейсы: количество доменов отказа
Простая арифметика говорит: для кластера с EC4+2 нужно минимум 6 доменов отказа (по умолчанию хостов). На практике это работающая конфигурация, но любой отказ выведет кластер из штатного режима — деградация и повышенный риск потери данных без возможности восстановиться за конечное время.
Мы стали закладывать +1 домен отказа (т.е. ноду или стойку — в зависимости от конкретной инсталляции): k+m+1, то есть 7 нод при EC4+2. Потому что суровая действительность нам говорит, что существуют праздники, логистические окна, сложные поломки, связанные отказы, потерянный SLA.

Лишняя нода позволяет кластеру автоматически оставаться в штатном режиме-ребалансе без участия человека. Идеально делать доменом отказа не ноду, а стойку, но это не всегда возможно.
Есть корнер-кейс: all-HDD-кластеры с небольшим общим количеством IOPS. Там ребаланс может заметно ухудшить ситуацию. Здесь нужно аккуратно подбирать параметры recovery.
Кейсы: usage с тысяч клиентов
В личном кабинете Рег.облака есть плашка с объемом занятого места. Казалось бы — просто собрать и показать. Но у нас публичный S3: десятки тысяч клиентов, сотни тысяч бакетов. Один запрос usage на бакет — примерно 10–50 м/с. Умножаем на сотни тысяч бакетов — получаем числа, которые не вписываются в разумный интервал сбора.
Глобально существует два подхода: собирать per user через admin API RGW — он умеет выгружать все бакеты по user ID, что сокращает количество запросов на порядок — или собирать одну большую JSON'ину разом: один запрос вместо десятков тысяч.
Немаловажной оказалась минимизация паразитной латентности. Если ходить в кластер Ceph из внешней системы, к времени обработки добавляется round-trip по сети. Мы перешли на локальный сбор: round-trip почти нулевой. Плюс используем утилиты RGW напрямую через локальный интерфейс — получаем данные в нужном формате одним чтением без HTTP-оверхеда.
Кейсы: HEALTH_OK != все OK
HEALTH_OK показывает общее состояние кластера из того, что он умеет оценивать. Но это не значит, что всё в порядке. Мы встречали такую ситуацию: при большой нагрузке во время ребаланса кластер в HEALTH_OK, но отдельные OSD зависли — их main thread заблокирован и не обслуживает входящие запросы, peering с другими OSD не происходит. Если такой OSD оказывается primary для какого-то PG, RGW ждёт ответа — и клиент тоже ждёт. Единственное решение — перезапустить конкретный OSD. При этом тред, отвечающий за ответы ceph-mon’у может отвечать и говорить, что все ок и все работает.
Практический вывод: мониторить нужно не только внутренние метрики кластера, но и внешние проявления работы кластера:
Коды ответов через nginx: всплеск 499-х говорит о чём-то нездоровом. У нас таймаут выставлен примерно в 10 минут — 499 означает, что уже совсем плохо.
Тайминги ответов через nginx: позволяют оценить, насколько текущая конфигурация справляется с нагрузкой.
Метрики со всего, что есть: у нас есть отдельный инстанс VictoriaMetrics, в который сливаются метрики со всех кластеров Ceph. Это не мониторинг в классическом смысле — это аналитика для поиска корреляций. Удобно делать это отдельно от продакшн-мониторинга.
Кейсы: rgw_gc_obj_min_wait
rgw_gc_obj_min_wait — параметр, который говорит GC-коллектору, через какое время он может прийти удалять объекты, помеченные как удаленные.
Баг BZ#1892644 существовал во времена Nautilus–Octopus, но может быть актуален и в Reef. Проявляется так: объект может остаться в индексе бакета, но отсутствует в пуле с данными. «Лечится» только удалением из индекса — то есть фактической потерей объекта.
Официальный workaround — поднять rgw_gc_obj_min_wait. Мы доходили до значения в неделю: с таким значением данные вычищаются редко, но кластер не страдает от незакрытых объектов. Рабочий вариант, но не подходит для маленьких кластеров с ограниченным местом. Прямого решения нет. В текущих конфигурациях, мы снизили значение до нескольких часов — этого, с учетом решения всех ранее описанных проблем уже хватает для нормальной работы кластера Ceph.
Кейсы: JBOD vs HBA
У нас достаточно много контроллеров MegaRAID, которые заявляют поддержку JBOD. Ceph не стоит использовать поверх RAID-массивов — это известно и понятно. Но по идее JBOD-режим же должен быть эквивалентен прямому доступу к диску? На практике — нет. Что мы получали:
неполный SMART — часть метрик контроллер просто не пропускает;
зачастую нет trim/discard/unmap;
повышенная латентность операций — по нашим тестам около 10% на SATA SSD;
очередь NCQ «вроде» на месте;
кэш контроллера «вроде» тоже не задействован (но это не точно);.
Из-за неполного SMART мы получаем ошибки в dmesg, но это пол беды — мы не знаем, что было вырезано. Ну а 10% latency, особенно для HDD — это существенно. Решение очевидное: не использовать JBOD (или RAID0 из одного диска — ну вдруг), а всегда брать HBA. Это еще и упрощает закупки: у команд, которым нужны аппаратные RAID, свой пул железа, у нас — свой, четко описанный.
Итоги года
Не все конфигурации оказались жизнеспособными. От EC @ HSD — самого проблемного сочетания — пришлось отказаться. Плотность — немного сократить. Несколько кластеров на 2 ПБ мы мигрировали на новую референсную архитектуру без потерь данных и потери доступности — клиенты этого даже не особо заметили. И даже проработали решения для запуска Ceph на отечественных ОС.
Ну а в целом:
Стандартизация конфигураций работает. Команды эксплуатации четко понимают, что их ждёт при следующем обновлении. Референсная архитектура заведомо отвечает на вопрос «что можно, что нельзя» — и это снижает энтропию.
Конкретные требования к оборудованию упрощают закупки. Чёткий пул железа, описанные требования — и никаких вариаций «возьмём что осталось».
Пойманные кейсы улучшили мониторинг. Теперь отлавливаем проблемы на самых ранних этапах — буквально как только они начинают проявляться.
За год запустилось больше 10 кластеров Ceph — объемом от 160 ТБ до почти 2 ПБ под разные задачи. Публичный S3 вырос в объеме в более чем в 2 раза. Запустились новые сервисы: сетевые диски на базе Ceph RBD и управление бэкапами в DBaaS — там под капотом так же находится S3 на базе Ceph.
С вами был Игорь Шишкин, руководитель отдела разработки облачной платформы и архитектор SDS в Рунити. Если у вас остались каки-то вопросы — оставляйте их в комментариях, обсудим!
