Scalability review using AI (Claude) #70
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Аудит масштабируемости Vitastor
Дата: 2026-05-16
Версия кода: master @
e136a1df("Remove extra assert in heap after reworking recheck")Контекст: Аудит проведён для оценки готовности Vitastor к роли storage-слоя публичного коммерческого облака (IaaS): тысячи OSD, сотни серверов, десятки тысяч VM/клиентов, RDMA RoCEv2 как основной транспорт.
Метод: 12 параллельных агентов, каждый аудитировал свою подсистему путём полного построчного чтения исходников (без grep, который давал ложные срабатывания в прошлых аудитах). Все находки верифицированы прямым чтением кода. Дополнительно проанализирована официальная документация Vitastor (docs/intro/architecture.ru.md, features.ru.md, config/.ru.md, performance/.ru.md) для отделения "багов" от "архитектурных by-design ограничений".
Что не вошло: корректность данных и performance в hot path (разобраны в
VITASTOR_AUDIT_REPORT.md,VITASTOR_NEW_FINDINGS.md,VITASTOR_ROUND2_FINDINGS.md,VITASTOR_PERFORMANCE_AUDIT.md).Архитектурный контекст из официальной документации
Перед чтением находок важно понимать: многие проблемы масштабируемости — это не баги, а сознательные архитектурные решения автора Vitastor (Виталия Филиппова), которые работают для целевой аудитории (среднемасштабные кластеры ≤ нескольких сотен OSD), но не подходят для IaaS. Это нужно учитывать при формировании плана действий и при обращении к разработчику — некоторые "недостатки" нельзя исправить мелкими фиксами, требуется обсуждение целевой архитектуры.
Прямые цитаты из документации
Из
docs/intro/architecture.ru.md(раздел "Отличия от Ceph"):→ Следствие для нас: 24 NVMe сервер = 96+ OSD per server. Это многократно усугубляет все проблемы full-mesh (Часть 3.2) и количества соединений (Часть 5). Также по архитектурной гарантии "и всегда такими останутся" — multi-threaded OSD ждать не стоит.
→ Прямое признание автора: Vitastor рассчитан "вплоть до нескольких сотен OSD". IaaS с 5000+ OSD выходит за официально поддерживаемые пределы. Все находки части 2 (LP optimizer) — это закономерное следствие подхода, рассчитанного на меньший масштаб.
→ By-design: метаданные всегда в RAM. Для 16 ТБ NVMe это ~8.7 ГБ только метаданных, плюс +1 ГБ на 1 ТБ при включённых crc32c (см.
docs/config/layout-osd.ru.md) = 24+ ГБ RAM на OSD при типичных дефолтах. С 96 OSD на сервер = 2+ ТБ RAM на сервер только под Vitastor метаданные. Наши находки в Части 11 (Blockstore RAM) — это документированное архитектурное свойство.Из
docs/intro/architecture.ru.md(раздел "Клиентские компоненты"):→ By-design: full-mesh клиент → ВСЕ OSD. Наши находки Части 7 (cluster_client per VM = независимый клиент со всеми соединениями) — это архитектурный фундамент, не bug. Per-host proxy daemon, который мы рекомендуем — это сменa архитектурной парадигмы, не "фикс".
Из
docs/config/network.ru.md(разделrdma_max_recv):→ Официально документированный расход: 2 МБ × N_OSD на одного клиента. Для 5000 OSD = 10 ГБ pinned RAM на одного клиента. Это полностью совпадает с нашей Частью 4.2 (находка #R2) — но это документированная by-design характеристика, не баг.
Из
docs/intro/features.ru.md(раздел "Планы развития"):→ Автор сам признаёт, что эти направления — TODO. Наш аудит подтверждает: эти направления критичны для IaaS.
Из
docs/intro/architecture.ru.md(раздел "Принципы разработки"):→ Это объясняет findings: ручной HTTP-клиент к etcd (Часть 6.5 — single keepalive_client, HOL blocking); ручной messenger (Часть 5 — std::map для FD-handlers, vector::erase в hot path); отсутствие RAII (Часть 1 — race в updating_total_stats флаге).
Карта "by-design" vs "bug/fixable" для наших находок
client_queue_depthне применяется (3.1)force_stop(1)при изменении pg_count (3.3)shutdown()метод существует, но не используется в SIGTERM handler.update_total_statsгигантская TXN (1.1)stop_all_pgscompare на каждый OSD (1.2)target_historyбез GC (8.1)returnвместоcap.exit(1)на любую ошибку (4.4)stop_client().node_placementесть, но default плохой.Соотношение по нашим топ-30 находкам:
Что это значит для вашей задачи
Если идёте на IaaS с RDMA RoCEv2 на Vitastor — вы выходите за официально поддерживаемые пределы по архитектуре, и должны быть готовы к серьёзной собственной разработке или к финансированию разработки Vitastor.
Без архитектурных изменений (proxy daemon, multi-thread, шардирование mon) Vitastor подходит максимум для 500-1000 OSD кластеров.
С мелкими fixes (закрытие 23 bug-находок) и тщательным тюнингом конфигурации можно дойти до 1500-2500 OSD, но всё ещё с низкой VM-density (10-50 VM/host без proxy).
Для полноценного hyperscaler (5000+ OSD, 100+ VM/host, RDMA) необходимо:
Альтернатива: использовать Vitastor только как подкомпонент (например, для одного rack/AZ из 200-500 OSD), а на уровне облака над ним делать собственный шардирующий слой. В этой модели каждый Vitastor-кластер обслуживает 100-500 хостов, и облако имеет 50-100 таких независимых кластеров.
Положительные особенности архитектуры
Не всё плохо. Дизайн Vitastor имеет сильные стороны, релевантные для облака:
local_readsдля multi-AZ оптимизации.Эти преимущества делают Vitastor очень привлекательным для on-prem private cloud с десятками-сотнями серверов, и нишево привлекательным как компонент IaaS, разделённого на изолированные shard-кластеры.
TL;DR — оценка готовности
Краткий вердикт
Vitastor в текущем виде НЕ ГОТОВ к IaaS-уровню (5000+ OSD, 50000+ клиентов, RoCEv2). Архитектурные решения, принятые для среднего масштаба (десятки-сотни OSD, единичные тысячи клиентов), создают непреодолимые узкие места:
/pg/config,/stats,last_clean_pgsмонолитны; любое изменение → перезапись 10-50 МБ.Безопасные пределы текущей реализации (без рефакторинга)
Топ-30 критических находок для IaaS
msgr_rdma.cpp:662-680msgr_rdma.cpp:405-461msgr_rdma.cpp:566, 656, 670exit(1)на любой RDMA error → кластер коллапсирует от одного OOM на pinned RAMosd.h:120+osd.cpp:240client_queue_depthНЕ применяется → OOM при loadosd_cluster.cpp:777force_stop(1)при изменении pg_count если есть активные PG → суицид OSDosd_main.cpp:12-21exit()без drain → тысячи timeout у клиентов при rolling restartosd_cluster.cpp:992mon.js:744-803update_total_statsшлёт 100K+ inode-stats в одной etcd-транзакции каждую секундуmon.js:466-486stop_all_pgs— compare на каждый OSD → fails при стандартномmax-txn-ops=128mon.js:398-432load_cluster_stateгрузит всё одним range → монитор не стартует при > 1.5 МБ statemon.js:518-519Promise.allзапускает N lp_solve процессов одновременно — 16 ГБ RAM при 16 пулахdsl_pgs.js:244-292random_custom_combinations8-24 мин на 5000 OSD × 16 пулов × EC 10+4dsl_pgs.js:294-340filter_tree_by_rulessplice в расширяющемся массиве — часы CPUpg_gen.js:121-148save_new_pgs_txn— 2 op per PG, гарантированный отказ etcd при 16 пулов × 8192 PGetcd_state_client.cpp:635-740load_pgsтащит 7 range-запросов ~35 МБ JSON × 55K клиентов = 2.5 ТБ при reconnect stormetcd_state_client.cpp:480-491etcd_state_client.cpp:512-520/pg/— etcd watchserver bottleneck при ~50 PG events/secosd_peering.cpp:700-712target_historyне GC при degraded → OOM + etcd падаетosd_peering_pg.cpp:407-449osd_flush.cpp:420-423nfs_proxy.cpp:256-322nfs_kv_write.cpp:56-145cur_shared_inode— точка сериализации для small-file writescontrollerserver.go:115-123vitastor-cliна каждый CSI вызов + watch всех inodes в etcdDockerfile:31nbds_max=128на узелblockstore_impl.cpp:36-40+ heapinmemory_metadata=true+ heap entries malloc → 20-40 ГБ RAM на 16TB NVMeЧасть 1. Monitor / etcd
Подсистема:
mon/mon.js,etcd_adapter.js,antietcd_adapter.js,stats.js,prometheus.js.1.1. CRITICAL —
update_total_stats()шлёт всю inode-статистику в одной etcd-транзакцииФайл:
mon/mon.js:744-803Каждые
mon_stats_timeout=1000мс в одной etcd-транзакции пишутся: глобальный/stats, одинrequestPutНА КАЖДЫЙ inode для всех пулов, один на каждыйpool/stats/<pool_id>, плюсrequestDeleteRangeдля исчезнувших.Сценарий: Облако с 100 пулами × 1000 томов = 100 000 inode. Каждую секунду одна транзакция с 100K+ puts:
--max-request-bytesetcd (по умолчанию 1.5 МБ; даже vitastor-скрипт ставит 100 МБ)max-txn-ops=128(default etcd!) транзакция ВСЕГДА фейлитЧто сделать: Разбить на батчи ≤100 ops. Писать только изменившиеся inode (хранить prev_inode_stats). Адаптивный
mon_stats_timeoutпри N>1000.1.2. CRITICAL —
stop_all_pgs()создаёт compare на КАЖДЫЙ OSDФайл:
mon/mon.js:466-486При 5000 OSD + изменении pg_count пула → 5002 compare-операции в одной TXN. С
max-txn-ops=128(стандартный etcd) никогда не сработает — pg_count невозможно изменить, кластер не масштабируется.Что сделать: Использовать одну compare на общий
osd_revisionepoch-ключ, а не проверку каждого OSD.1.3. CRITICAL —
load_cluster_state()грузит всё дерево одним range-запросомФайл:
mon/mon.js:398-432При 5000 OSD × (state+stats+inodestats+space) + 50000 PG (state+history+stats) + 100K inodes — dump 10-50 МБ.
--max-request-bytes(1.5 МБ default)etcd_start_timeout=5sнедостаточноЧто сделать: Пагинировать с
limit=1000, либо bootstrap черезwatch start_revision=0.1.4. CRITICAL —
recheck_pgs()пересчитывает все пулы при ЛЮБОМ изменении OSD treeФайл:
mon/mon.js:506-549Promise.allзапускает ВСЕ пулы параллельно, каждый =child_process.spawn('lp_solve'). При 100 пулах × 4096 PG = 100 параллельных lp_solve. На 8 CPU cores → load average 100+, swap, throttling. Каждое добавление OSD меняет tree_hash → новая итерация → стейбилизация при auto-deploy 100 OSD занимает 30-300 секунд.Что сделать:
1.5. CRITICAL —
recheck_pgs_activeблокирует обновления статистикиФайл:
mon/mon.js:491-568, 777-810Флаг
recheck_pgs_activeблокирует новые recheck и stats updates. При большом кластере recheck работает минуты. В этом окне все события о падении OSD не обрабатываются → primary не переключаются (recheck_primary не вызывается). Окно "глухоты" монитора 5-10 минут.1.6. HIGH — Prometheus dump может вырасти до сотен МБ
Файл:
mon/prometheus.js:58-213,mon.js:87-109export_prometheus_metricsстроит строку черезres += .... Для каждого OSD генерируются метрики op_stats × subop_stats × {count, usec, bytes}. Для каждого inode — 3 op × 3 поля. Для каждого пула × pg_count — 17 состояний.При 5000 OSD + 100K inodes + 50K PG → ~2 миллиона строк, dump 100-500 МБ. Node.js конкатенация строк O(N²). 5-30 секунд блокировки event loop → lease expires (etcd_mon_ttl=5 сек) → монитор die → cascade.
Что сделать: Стримить через
res.write(). ИспользоватьBuffer.concat()(линейный). Скипать high-cardinality (per-inode) метрики.1.7. HIGH —
save_last_clean()записывает весь pg.config.items на каждое pg/state-событиеФайл:
mon/mon.js:265-311, line 236В
on_messageприpg_states_changedвызываетсяsave_last_clean()напрямую (безschedule_save_last_clean()который существует, но не используется). Каждый вызов — JSON.stringify ~20 МБ + write в etcd. При rebalance 50 PG state changes → 50 синхронных save_last_clean.1.8. HIGH — Один ключ
/pg/configна ВСЕ пулыФайл:
mon/mon.js:678-693,etcd_schema.js:282-296Структура
{ items: { <pool_id>: { <pg_id>: ... } } }— 100 пулов × 4096 PG × ~50 байт = 20 МБ JSON. Любое изменение primary одного PG → перезапись 20 МБ. Каждый OSD читает этот ключ через watch → 5000 × 20 МБ = 100 ГБ network burst при flapping.Что сделать: Per-pool ключи
/pg/config/<pool_id>.1.9. HIGH — WebSocket watch без backpressure → OOM при flood
Файл:
mon/etcd_adapter.js:190-229При flapping 1000 OSD: 10000 watch events/sec,
parse_kv+ регулярка +JSON.parseкаждого = ~100 мкс × 10000 = 1 сек обработки на 1 сек flooding → очередь растёт. Lease expires, монитор die, рестарт во время flapping = катастрофа.1.10. HIGH —
sum_object_counts()O(pools × pg × pg_size) на BigInt'ах каждую секундуФайл:
mon/stats.js:114-202Вложенный цикл 100 пулов × 4096 PG × 6 (pg_size) = 2.4M итераций, каждая с BigInt-арифметикой (10-100x медленнее Number). 50-200 мс CPU блокировки на каждое вычисление; вызывается каждую секунду + из
apply_pool_pgs().1.11. MEDIUM —
etcd_mon_ttl=5 сек, lease renew интервал = TTL (а не TTL/3)Файл:
mon/mon.js:153-157, 332-341При любой блокировке event loop >5 сек (см. 1.6, 1.10) lease истекает → монитор die. Best practice: renew каждые TTL/3.
Часть 2. PG generation / LP optimizer
Подсистема:
mon/pg_gen.js,mon/pg_utils.js,mon/lp_optimizer/*.2.1. CRITICAL —
random_custom_combinations×select_murmur3×filter_tree_by_rulesФайл:
mon/lp_optimizer/dsl_pgs.js:244-292Алгоритм:
first(все top-level OSD), для каждого вызываетfilter_tree_by_rulespg_size-1раз +select_murmur3(сам O(N))count = max_osd_combinations = 10000итераций поrules.length(= pg_size), каждая сноваfilter_tree_by_rules+select_murmur3select_murmur3(murmur3.js:9-18) проходит весь массивcountраз, для каждого считает murmur3 от строкового пути в дереве.Сценарий 5000 OSD, EC 10+4 (pg_size=14), 16 пулов, max=10000:
(5000 + 10000) × 14 × 5000 × ~20 ops≈ 2.1 миллиарда операций murmur3 + столько же string concatPromise.all, но V8 однопоточный → 8-24 минуты на генерацию входа LPЭффект:
recheck_pgs_active = trueблокирует monitor десятки минут. Любое изменение OSD за это время → setImmediate рестартует с полным пересчётом → может никогда не сойтись.2.2. CRITICAL —
filter_tree_by_rulesмутирует массив через splice в расширяющейся итерацииФайл:
mon/lp_optimizer/dsl_pgs.js:294-340Сценарий 4-уровневой иерархии (dc → rack → host → osd), 5000 OSD = ~5556 nodes. Каждый
filter_tree_by_rulesспускается до OSD-уровня → O(5556) splice'ов в одном проходе. Splice в массиве длиной 5000 — O(N) каждый. O(N²) = 30M ops на один вызов. Per pool:(5000 + 10000) × 14= 15000×14 = 210K вызовов × 30M = 6.3 × 10¹² ops — часы CPU.Что сделать: заменить splice на построение нового массива через push.
2.3. CRITICAL — Размер LP-задачи и сборка строки через
+=Файл:
mon/lp_optimizer/lp_optimizer.js:57-109, 240-330, 77-91, 293-328LP-задача
optimize_initial:max_osd_combinations + |first|= до 15000optimize_change— 30000 переменныхРазмер LP-файла: ~100 МБ-1 ГБ в строковом виде. Сборка через
lp += ...(O(N²) memory copy) — минуты CPU + GC pressure. Передача вcp.stdin.write(text)блокирующая.lp_solveс integer constraints (sec) на 150K переменных — MILP NP-hard, может никогда не вернуть результат.Что сделать:
max_osd_combinationsпо формуле, чувствительной кpg_size.2.4. CRITICAL —
save_new_pgs_txn— 2 etcd-op per PG → превышаетmax_txn_ops=128Файл:
mon/pg_gen.js:121-148(FIXME уже в коде)На каждую PG: history compare + put/delete = 2 op. Для 16 пулов × 8192 PG = 262144 etcd-операций в одном txn. С default
max_txn_ops=128→ отказ → recheck бесконечный retry → пул stuck.Даже если поднять max до 1М — single txn такого размера развалит etcd raft (
max-request-bytes=1.5 МБ).Что сделать: chunked transactions; key-per-pool вместо key-per-PG для истории.
2.5. CRITICAL —
Promise.allзапускает N lp_solve процессов одновременноФайл:
mon/mon.js:518-519Каждый lp_solve процесс кушает 100-1000+ МБ RAM. 16 пулов с EC 10+4 → 16 × 1 ГБ = 16 ГБ RAM + Node-процесс со строками LP → OOM на 32 ГБ monitor.
Что сделать: Семафор max 2-4 параллельных lp_solve.
2.6. HIGH —
recheck_pgsблокирует event loop черезJSON.parse(JSON.stringify())в циклеФайл:
mon/pg_gen.js:70, 85; mon.js:602Deep clone всего
pg.config(≈20 МБ JSON) делается внутри двойного цикла по пулам × pg_num. Reweight всех OSD одновременно → primary меняется массово → clone выполняется на каждой PG → 131072 × 300 мс = ~11 часов блока event loop.Что сделать: clone один раз перед циклом;
structuredClone(быстрее в Node 17+).2.7. HIGH —
recheck_primary+pick_primary— O(pools × pg × pg_size × osd_count)Файл:
mon/pg_gen.js:44-95При 5000 OSD × 8192 PG × pg_size=14 × 16 пулов = 9 миллиардов ops на recheck. 8-24 минуты per полный recheck primaries. Вызывается каждый
mon_change_timeoutпри любом event.2.8. HIGH —
calc_intersect_weightsпри больших pg_sizeФайл:
mon/lp_optimizer/lp_optimizer.js:163-237Для EC 10+4 (pg_size=14):
2^14 = 16384. Приpg_count = 8192попадаем в ELSE-ветку: для каждогоpg ∈ all_pgs(15000+) пробегает по всемprev_weights(8000+). ~1.68 × 10⁹ ops per pool, ~3-10 минут per pool. 16 пулов → час+ суммарно.2.9. HIGH —
make_cyclicбаг:pg.sizeвместоpg.lengthФайл:
mon/lp_optimizer/lp_optimizer.js:111-125Цикл никогда не выполняется → для EC циклические перестановки не создаются → LP получает только одну ориентацию каждой PG → менее оптимальное распределение чанков. При исправлении:
all_pgsвырастет вpg_sizeраз → 150K переменных для EC 10+4 → LP взрывается. Парадокс: баг защищает от взрыва.Что сделать: Исправить + ввести строгий лимит
max_osd_combinations / pg_sizeдля EC.2.10. HIGH —
fold_failure_domains— O(N²×iterations)Файл:
mon/lp_optimizer/fold.js:10-67While-loop пересборки
per_parentна каждой итерации. 5000 OSD × depth=4 = 123M ops + GC pressure × 16 пулов = 2 млрд ops, 5-15 минут.2.11. HIGH —
scale_pg_countshared-ref + push вgenerate_pool_pgsФайл:
mon/pg_utils.js:98-112,pg_gen.js:222-223При
prev_pgs=[[1,2,3]], scale до 3 PG → все три PG = одна ссылка. При pg_size=5 push добавит 2 нуля три раза →[1,2,3,0,0,0,0,0,0]вместо ожидаемых 2 нулей. LP получает мусорные prev_pgs при scale-up.2.12. MEDIUM-HIGH —
orderedне передаётся в CombinatorФайл:
mon/pg_gen.js:199-208Для EC pools combinator не генерирует ротации ролей → меньшее число valid combinations → неравномерное распределение data/parity по OSD → разный износ дисков.
Часть 3. OSD cluster / discovery / peers
Подсистема:
src/osd/osd.cpp/h,osd_cluster.cpp,osd_main.cpp,osd_peering.cpp,osd_peering_pg.cpp/h.3.1. CRITICAL —
client_queue_depthне применяетсяФайл:
src/osd/osd.h:120-121,osd.cpp:240-245, 456При 5000 OSD и pg_size=3 каждый primary получает SEC_WRITE/SEC_READ от всех соседей. При 10000 VM × iodepth=64 → до 640K параллельных запросов на один primary.
inflight_ops++вexec_opне имеет потолка. Secondary OSD получают ещё больше за счёт fan-out.Эффект: OOM из-за неограниченного
osd_op_t/bs_op/iov-буферов; зависаниеshutdown().Что сделать: Backpressure — если
inflight_ops > client_queue_depth × N_clients, отложить read из сокета или возвращать-EBUSY.3.2. CRITICAL — Full-mesh O(N²) TCP/RDMA соединений
Файлы:
src/osd/osd_cluster.cpp:992-1000(apply_pg_config),osd_peering.cpp:204-214(start_pg_peering)5000 OSD, 100 PG per OSD, pg_size=3 → каждый OSD держит ~5000 peer connections. На одном OSD:
osd_client_t(~300 байт + sent_ops + outbox) = 1.5+ МБЧто сделать: Lazy connection (только когда есть I/O). Минимум — поднять ulimit в systemd до 1М.
3.3. CRITICAL —
apply_pg_count()self-kill при изменении pg_countФайл:
src/osd/osd_cluster.cpp:757-778Все 5000 OSD получают update через etcd watch одновременно. Если хоть один не успел остановить PG к моменту обработки —
exit(1)→ lost writes (in-flight ops terminated). Каскадная гибель.Что сделать: Заменить
force_stop(1)на ожидание PG → drain → retry.3.4. CRITICAL — SIGTERM =
force_stop(0)=exit()без drainФайл:
src/osd/osd_main.cpp:12-21+osd_cluster.cpp:656-677shutdown()(с проверкой inflight_ops) есть, но вызывается только из init.loop. Приsystemctl stop/ k8s SIGTERM OSD выходит мгновенно, теряя in-flight client ops. Cluster-wide rolling restart 1000 OSD = 1000 одновременных hiccup'ов клиентов — неприемлемо для production.Что сделать: В
handle_sigintпоставитьstopping=true, дождаться drain, потом exit.3.5. CRITICAL —
report_pg_states()шлёт огромную etcd транзакциюФайл:
src/osd/osd_cluster.cpp:1019-11445000 OSD × 100 PG = 500000 PG-state updates единовременно при рестарте. На одном OSD при
repeer_pgs(peer)все PG данного OSD становятся dirty одновременно → 200-500 PG в одной TXN, payload 100+ КБ. etcd сMaxTxnOps=128отбросит. Cluster paralysis.3.6. CRITICAL —
report_statistics()шлёт O(N_pg) keys каждые 30sФайл:
src/osd/osd_cluster.cpp:266-411500 PG на OSD × 5000 OSD × interval 30s + возможное совпадение фаз → 2.5M ops на etcd каждые 30s — distributed DoS-стиль.
Что сделать: jitter на report (±5s); рассмотреть statsd/prometheus вместо etcd для часто меняющейся статистики.
3.7. HIGH —
repeer_pgs()O(N_pg × |all_peers|) на каждый disconnect peerФайл:
src/osd/osd_peering.cpp:93-118500 PG на OSD × replication 3 + history(5) = 2500 проверок на каждый disconnect. При network blip 100 peers → 250000 проверок + side effects через repeer_pg.
Что сделать: Обратный индекс
osd_num → set<pg_t*>. O(N_affected).3.8. HIGH —
pgsиpg_locks—std::map(RB-tree)Файл:
src/osd/osd.h:173-174pgs.find()вызывается 2-5 раз на каждый client IO. 500 PG → O(log) ≈ 10 сравнений. На 100k IOPS — 10-50% CPU overhead.Что сделать:
robin_hood::unordered_flat_map, либоstd::vector<pg_t>с прямой индексацией.3.9. HIGH — Peer connection retry без jitter — connect storm
Файл:
src/client/messenger.cpp:615-622Network partition: 1000 OSD теряют связь к 500 OSD. Через 5 сек ВСЕ 1000 одновременно reconnect → 500000 SYN packets. После accept — каждое connection делает
OSD_OP_SHOW_CONFIGhandshake.Что сделать: Exponential backoff + jitter (
peer_connect_interval * (1 + random()/2)).3.10. HIGH — При неудачном lock — fixed 100ms retry до бесконечности
Файл:
src/osd/osd_peering.cpp:500-511Split-brain → два OSD пытаются взять lock на одной PG → бесконечный pingpong каждые 100мс, спам в логе.
Что сделать: Exponential backoff с max wait time.
3.11. HIGH —
on_change_etcd_state_hookзапускаетapply_pg_configна любое etcd-событиеФайл:
src/osd/osd_cluster.cpp:465-486Любое изменение
/osd/state/N(даже далёкого OSD без общих PG) триггерит full walkapply_pg_config(). Wasted CPU на 5000 OSD кластере.3.12. HIGH —
start_pg_peeringблокирует PG как INCOMPLETE при ANY disconnectedall_peersФайл:
src/osd/osd_peering.cpp:202-221all_peersвключаетtarget_set + all_peers + history. История может содержать десятки OSD когда-либо participating. Если 2 из них permanently dead → PG в PG_INCOMPLETE → IO unavailability.3.13. MEDIUM —
print_slow()O(N_clients × queue_per_client) каждые 10sФайл:
src/osd/osd.cpp:604-7081000 clients × 128 queue = 128000 итераций каждые 10 сек. snprintf+printf блокирует main thread на 1-10мс.
3.14. MEDIUM —
unstable_writesглобальныйstd::map, очищается линейно приreset_pgФайл:
src/osd/osd.h:202-203,osd_peering.cpp:164-1711M unstable writes (immediate_commit != ALL), repeer на одного peer → full scan 1M × N_repeering_PGs.
Что сделать: Per-PG
unstable_writes(хранить вpg_t), не глобально.3.15. CRITICAL — Keepalive timer обходит ВСЕ clients каждую секунду
Файл:
src/client/messenger.cpp:183-2615000 OSD-peer + 10000 client connections = 15000 итераций/сек. Cache-unfriendly random access по
robin_hood::unordered_flat_map. Main thread block на ~мс ежесекундно.Что сделать: Отдельный список только активных peer-connections (regular clients ping не нужны).
3.16. MEDIUM — Recovery auto-tune — единственная глобальная переменная
Файл:
src/osd/osd_flush.cpp:368-448recovery_target_sleep_usне per-pool, не per-priority. Один горячий пул выкрутит до 10s → recovery в "тихих" пулах надолго пайзится.Часть 4. RDMA / RoCEv2 (КРИТИЧНО ДЛЯ ВАШЕЙ ЗАДАЧИ)
Подсистема:
src/client/msgr_rdma.cpp/h,msgr_rdmacm.cpp,src/nfs/rdma_alloc.cpp,nfs_proxy_rdma.cpp.4.1. CRITICAL #R1 — Один QP на каждое соединение, без QP-pooling
Файлы:
msgr_rdma.cpp:405-461(create()),msgr_rdmacm.cpp:235-263(rdmacm_create_qp)Строка 430:
.qp_type = IBV_QPT_RC. Для каждого нового пира — отдельный QP черезibv_create_qp. Нет DC, нет UD, нет XRC, нет SRQ.QP_count(OSD) = N_peers. На каждом популярном OSD (10K пиров):
Что сделать:
rdma_max_connectionsс fallback на TCP.4.2. CRITICAL #R2 — Eager-аллокация recv-буферов:
max_msg * max_recvна КАЖДОЕ соединениеФайл:
msgr_rdma.cpp:662-680(init_recv_rdma)Эта память pinned + registered — нельзя свопировать, занимает HCA TLB entries.
Что сделать:
rdma_max_recvдефолт (16 буферов × 129 КБ — пережиток "1 клиент к 1 OSD").4.3. CRITICAL #R3 — Отсутствие SRQ → квадратичный рост памяти
Не найдено ни одного
ibv_create_srqв коде. Каждый QP имеет персональную recv-очередь. Без SRQ память на recv = O(N_connections). С SRQ — O(1) на device.4.4. CRITICAL #R16 —
exit(1)на любую RDMA ошибкуФайлы:
msgr_rdma.cpp:566-567(send fail → exit):656-658(recv fail → exit):604-606(reg_mr fail → exit):670-672(init recv reg_mr fail → exit)nfs_proxy_rdma.cpp:498-501, 758-760, 1046-1049, 473-475Любая ошибка
ibv_post_*/ibv_reg_mr→ kill всего процесса OSD. Под нагрузкой при достижении HCA limits (max QPs, max MRs, pinned memory limitulimit -l) OSD умирает целиком. Один сбойный клиент / переполнение pinned memory → kill OSD → repeer всего пула → каскад.Что сделать:
stop_client(client_id)для per-connection ошибок; retry с backoff на временные; алерт + graceful degradation.4.5. HIGH #R8 — RDMA RC retry parameters захардкожены, нет fallback при потере
Файл:
msgr_rdma.cpp:484-491При обнаружении
wc.status != IBV_WC_SUCCESS→stop_client(нет recovery RC-state, нет fallback на TCP). В RoCEv2 RC после 7×134ms ≈ 1 сек неудач → QP →IBV_QPS_ERR→ жесткий disconnect → пересоединение с нуля. В hyperscaler под congestion = постоянный flap-storm.Что сделать:
IBV_WC_RNR_RETRY_EXC_ERR— reset QP (RESET → INIT → RTR → RTS).4.6. HIGH #R6 — Нет PFC/ECN/Service Level/ToS конфигурации
Все коннекшены идут через
sl = 0(msgr_rdma.cpp:479). Нет упоминаний PFC, ECN, DCQCN, priority groups, traffic class, ToS. В rdmacm.cpp нетrdma_set_option(RDMA_OPTION_ID_TOS,...).RoCEv2 ТРЕБУЕТ lossless network через PFC (priority 3 typical) или DCQCN. Без них — incast collapse при сотнях клиентов.
Что сделать:
rdma_traffic_class,rdma_service_level,rdma_tos.ah_attr.slиrdma_set_option(RDMA_OPTION_ID_TOS,...).4.7. HIGH #R5 — MTU не валидируется, нет path-MTU обмена
Файл:
msgr_rdma.cpp:267, 468MTU берётся из
portinfo.active_mtuили конфига. Вrdma_address(msgr_rdma.cpp:38) MTU не передаётся — каждая сторона использует свойctx->mtu. При asymmetric MTU (jumbo 9000 ↔ 1500) connection поднимется, но пакеты будут drop'аться на коммутаторе → silent black hole, очень тяжёлый debug.Что сделать: Обмен MTU при handshake; использовать min(local, remote, configured).
4.8. HIGH #R13 —
ibv_reg_mrв hot path — blockingФайлы:
msgr_rdma.cpp:601(lazy alloc send_out — 1 МБ)msgr_rdma.cpp:667(init_recv_rdma — 2.1 МБ)nfs/rdma_alloc.cpp:114—ibv_reg_mrвrdma_malloc_allocна каждую новую rdma_region при обработке RPC!nfs_proxy_rdma.cpp:470—ibv_reg_mrна каждый recv-bufferibv_reg_mr— syscall + pinning + DMA-mapping. 5-50 µs для 1 МБ, миллисекунды для больших регионов. В NFS — постоянно блокирует hot path обработки RPC.Что сделать: Pre-register больших MR при init; ODP (
IBV_ACCESS_ON_DEMAND); pool заранее.4.9. HIGH #R22 —
is_globalлогика небезопасна для RoCEv2Файл:
msgr_rdma.cpp:481Для RoCEv2
is_globalвсегда должен быть 1 (RoCEv2 = всегда GRH/UDP). Текущий код выставит 0 если GID нулевой. Такжеhop_limit = 64захардкожено.Что сделать: Для RoCEv2 (
link_layer == IBV_LINK_LAYER_ETHERNET) принудительноis_global = 1. Сделать hop_limit конфигурируемым (для RoCE через несколько L3 hops).4.10. HIGH #R18 — Bounce buffer + memcpy убивает zero-copy
Файл:
msgr_rdma.cpp:571-637Каждое сообщение копируется из
cl->send_list[](user iovecs) вrc->send_out.bufчерез memcpy. RDMA фактически в режиме copy + send. На high-throughput recovery (4 МБ объекты): bandwidth bottleneck = memcpy ≈ 10-15 ГБ/с per core.4.11. HIGH #R9 — DC (Dynamic Connected) не используется
Только
IBV_QPT_RC. НетIBV_QPT_DRIVER/DC. См. #R1 — без DC невозможно scale > 30K connections.4.12. HIGH #R4 — Один общий CQ на context, blocking resize
Файлы:
msgr_rdma.cpp:368-374, 684-796max_cqe = 4096, ресайз черезibv_resize_cq— blocking call, миллисекунды на больших значениях. Под burst-нагрузкой подключения блокирует event-loop OSD на 5-50 мс.Что сделать:
iothread_count > 0.4.13. MEDIUM #R7 — ARP/ND storm при cold start
Файлы:
msgr_rdma.cpp:149-179, msgr_rdmacm.cpp:384, 419, 45210000 пиров одновременно делают
rdma_resolve_addr→ 10000 параллельных ARP'ов в fabric. Большие switch tables (ARP/ND aging) могут throttle до 200 pps. RFC 8014 рекомендует pre-population. При cold-start cluster connect storm длится десятки секунд.4.14. MEDIUM #R10 — Нет NUMA-awareness
Файл:
msgr_rdma.cpp:182-319create_allитерирует по всемibv_get_device_list(). Никакой проверки NUMA-узла HCA. На двухсокетных серверах с 2 HCA трафик может идти через QPI → halve bandwidth, +500ns latency. Send/recv буферы черезmalloc_or_dieбезnuma_alloc_onnode.4.15. MEDIUM #R11 — Multipath не реализован
Файл:
msgr_rdma.cpp:269-301match_port_gidвыбирает один best GID per port. Если несколько подсетей —choose_rdma_context(messenger.cpp:806) выбирает один контекст поpeer_addrmatching. Bandwidth = 1×HCA-speed, нет ECMP balancing.Для 2×200G HCA per node, ECMP полагается на UDP src port; единый UDP src port = single ECMP path = 200G max вместо 400G.
4.16. MEDIUM #R12 — Нет
IBV_SEND_INLINEдля small opsФайлы:
msgr_rdma.cpp:552-569, nfs_proxy_rdma.cpp:739-744Каждое сообщение (включая PING ≈ 128 байт) DMA'ится из buffer. Inline до
max_inline_data(typically 64-220 байт) экономит PCIe transaction, -200ns/op.4.17. MEDIUM #R30 — Asymmetric multi-HCA → silent fail
В
msgr_rdma.cpp:442addr.gid = ctx->my_gid— используется GID СВОЕГО ctx. Если у пира нет route к этому GID — fail. При asymmetric network (multi-HCA на одной стороне, single на другой) RDMA не поднимается, тихо переключается на TCP.4.18. MEDIUM #R25 —
rdma_listen(listener, 128)— захардкоженный backlogФайлы:
msgr_rdmacm.cpp:46,nfs_proxy_rdma.cpp:208При cluster cold-start 10000 клиентов могут одновременно
rdma_connect→ backlog overflow → REJECTED → exponential backoff retry → задержка.Готовность к RoCEv2 для IaaS
Текущая оценка: НЕ ГОТОВ.
ОБЯЗАТЕЛЬНО для запуска (5000+ OSD):
exit(1)на RDMA errors — главная причина потенциального flap storm.Желательно:
Что уже работает разумно:
osd_network_masks.Часть 5. TCP messenger
Подсистема:
src/client/messenger.cpp/h,msgr_*.cpp,src/util/epoll_manager.cpp,timerfd_manager.cpp.5.1. CRITICAL —
in_buf=64KBвсегда выделяется per-connectionФайл:
src/client/messenger.cpp:514, 79050000 клиентов × 64 КБ = 3.2 ГБ RAM только на recv-staging, независимо от активности. Idle VM/клиент держит соединение → занимает 64 КБ всё время.
Что сделать:
receive_buffer_size(start small).recvmsgнапрямую в OP-buffer без staging.5.2. CRITICAL — Ringloop SQE = 1024, exhaustion при 5k+ соединений
Файл:
src/util/ringloop.h:20(RINGLOOP_DEFAULT_SIZE 1024)55000 соединений × (1 recv + 1 send) = 110000 SQE требуется. Доступно 1024.
get_sqe()возвращает NULL → fallback to next iteration. Под высокой нагрузкой OSD постоянно спинит без прогресса.Что сделать: sizing ringloop под N клиентов (хотя бы 65536 SQE).
5.3. CRITICAL —
epoll_handlers=std::map<int, std::function>Файл:
src/util/epoll_manager.h:17RB-tree lookup на каждое событие. 50k FD → log2(50000) ≈ 16 узлов cache miss на каждом. Batch 64 events × O(log) ≈ 1024 random memory lookups per epoll_wait.
Что сделать:
robin_hood::unordered_flat_map, либо FD-indexed array, либоepoll_event.data.ptrуказывает наosd_client_t*напрямую.5.4. CRITICAL —
MAX_EPOLL_EVENTS = 64+ блокирующий acceptФайл:
src/util/epoll_manager.cpp:12, 142-15364 события — слишком маленький batch. При 50k активных клиентов в одном тике может быть 5000+ ready FD → 80 syscalls
epoll_wait× 500ns = 40us simply на syscalls. Поднять до 1024-4096.5.5. CRITICAL —
accept()бросаетstd::runtime_errorпри EMFILEФайл:
src/client/messenger.cpp:799-802OSD достигает RLIMIT_NOFILE (1024 в systemd default или 4096 в bash) → EMFILE/ENFILE → uncaught exception → OSD crash → restart → connect-storm → recovery storm.
Что сделать: Catch EMFILE отдельно: накопить «спящий FD», на EMFILE close+accept+close нового (TCP RST). Явно поднимать RLIMIT_NOFILE до 1M в init.
5.6. HIGH — Регулярные TCP клиенты БЕЗ keepalive
Файл:
src/client/messenger.cpp:192-196И НЕТ
setsockopt(SO_KEEPALIVE). VM делает iSCSI/NBD connection, NIC отваливается тихо (ECMP/firewall reset) без RST → соединение в kernel TCP-stack открыто сутками. За неделю набегает 100k+ "зомби" соединений → FD-utilization растёт → ulimit → crash.Что сделать:
setsockopt(SO_KEEPALIVE)+ TCP_KEEPIDLE=60, TCP_KEEPINTVL=30, TCP_KEEPCNT=3. L7 idle-timeout для regular clients.5.7. HIGH —
listen_backlog = 128хардкод, не из etcd configФайл:
src/util/addr_util.cpp:240,src/osd/osd.h:117OSD рестартует → все 5000 OSD-peers + 50000 клиентов connect одновременно → Linux SYN-queue=128 → 49000+ ECONNREFUSED. Connect-storm с 5-сек периодом (peer_connect_interval).
Что сделать: backlog 4096-65536 по дефолту; парсить из etcd;
net.core.somaxconn.5.8. HIGH — SO_REUSEPORT не используется
Файл:
src/util/addr_util.cpp:209-215Все 50000 accept() через один listen_fd в одном epoll-event-loop. Max ~100k connections/sec. При burst 50k клиентов — 0.5 sec wall time на accept.
Что сделать: SO_REUSEPORT + N accept-fd + N worker threads.
5.9. HIGH — Unbounded
outboxper client → OOM via slow clientФайл:
src/client/messenger.h:99-101,msgr_send.cpp:10-115Медленный клиент не читает → kernel TCP буфер 4MB полон → sendmsg EAGAIN → тысячи
osd_op_t(каждый со своимbufдо 4MB read data) висят в outbox. Один зловредный клиент может задосить OSD до OOM.Что сделать: max in-flight ops per client; drop client после threshold.
5.10. HIGH —
received_ops—std::vector+ O(N) erase вoutbox_pushФайл:
src/client/msgr_send.cpp:29-37Клиент с QD=1000 → O(N²) = 1M операций на replies одного клиента.
Что сделать:
unordered_set<osd_op_t*>илиrobin_hood::unordered_flat_set.5.11. HIGH —
read_ready_clients/write_ready_clients— vector с linear eraseФайл:
src/client/msgr_stop.cpp:149-164Failover cascade: 100 OSD умирает → 5000 клиентов одновременно
stop_client→ каждый O(N) scan → 25M операций + cascading failures.Что сделать: Маркировать как stopped (skip-flag в deque).
5.12. MEDIUM —
timerfd_managerO(N) поиск таймераФайл:
src/util/timerfd_manager.cpp:85-103, 127-135При connect-storm рестарта 5000 connect_timeout таймеров создаются/удаляются.
set_nearest()+clear_timerкаждый O(N). 25M ops на этап рестарта.Что сделать: min-heap +
unordered_map<id, index>.5.13. MEDIUM —
MSG_WAITALLвio_uring_prep_sendmsgФайл:
src/client/msgr_send.cpp:227, 231При SQE pool=1024, 1024 slow клиентов с MSG_WAITALL → SQE-slot занят минутами → полная остановка messenger.
5.14. MEDIUM — Connect-storm без jitter
Файл:
src/client/messenger.cpp:618, 446Все клиенты ждут ровно
peer_connect_interval=5s→ пилообразная нагрузка с 5-сек периодом.Что сделать: Jittered backoff
peer_connect_interval * (0.5 + random()).5.15. MEDIUM — SO_RCVBUF/SO_SNDBUF не настраиваются
Дефолтные kernel TCP буфера ~200KB-4MB autotuned. 50000 connections × autoscaled = гигабайты kernel memory.
Максимум соединений на один OSD без переписывания
LimitNOFILE=1048576→ ~1M)Реалистичный потолок: 5000-10000 одновременных TCP соединений на один OSD.
Часть 6. etcd_state_client (как клиент etcd watch'ит state)
Подсистема:
src/client/etcd_state_client.cpp/h,http_client.cpp/h.6.1. CRITICAL —
load_pgsтащит 7 range-запросов одним TXN ~35 МБ JSONФайл:
etcd_state_client.cpp:635-740В одной транзакции:
/config/pools(100 pools × 500 байт = 50 КБ)/config/pgs(legacy)/pg/config(50000 PG × 200 байт = ~10 МБ)/config/inode/(10000 × 300 байт = ~3 МБ)/pg/history/(50000 × 200 байт = ~10 МБ)/pg/state/(50000 × 150 байт = ~7.5 МБ)/osd/state/(5000 × 500 байт = ~2.5 МБ)Итого: ~35 МБ JSON в одном HTTP-ответе на КАЖДЫЙ клиент. С base64 + 30%. ~45 МБ × 55000 клиентов = 2.5 ТБ трафика на reconnect storm. etcd default
--max-request-bytes=1.5 МБ— транзакция просто не пройдёт без специального тюнинга.Что сделать:
limit:(cursor поmod_revision)./pg/history/на клиентах — FIXME уже стоит, но история всё равно запрашивается (line 678-681)./pg/state/целиком на клиентах.6.2. CRITICAL — Reconnect storm без jitter
Файл:
etcd_state_client.cpp:480-491, 294-322При падении etcd-master все 55k клиентов одновременно увидят EOF:
etcd_watches_initialised > 0→ немедленный reconnect без задержки (// Connection was live, retry immediately)==0→ fixedetcd_quick_timeout(1000мс), без jitterSYN flood в новый master. Каждый запускает
load_pgs()(35МБ TXN!). etcd может уйти в cascading failover.Что сделать: Randomized jitter; exponential backoff на повторных EOF.
6.3. CRITICAL — Все 55K клиентов watch'ат
/pg/Файл:
etcd_state_client.cpp:512-520Healthy кластер с recovery → ~10-50 PG state updates/sec. Watcher на
/pg/(includes/pg/state/,/pg/history/,/pg/config). Каждое изменение → push на ВСЕ 55000 watcher'ов. 50 updates/sec × 55000 = 2.75M push/sec через etcd. CPU bottleneck.Что сделать:
6.4. HIGH — Watch payload включает
/pg/history/— на клиентах не нуженФайл:
etcd_state_client.cpp:521(FIXME), 1088-1142FIXME отмечено:
// FIXME: Do not watch /pg/history/ at all in client code. Клиент парсит и сохраняет всю history, но для I/O нужен толькоcur_primary. При peering 1000 PG → 55M ненужных push.6.5. HIGH — Один
keepalive_client, HOL blockingФайл:
etcd_state_client.cpp:166-170+http_client.cpp:148-152keepalive_clientсоздаётся один раз. Все etcd-запросы черезkeepalive_queueпоследовательно. HTTP/1.1 pipelining не работает. Тяжёлаяload_pgs(35МБ, 10 сек) → все остальные операции stuck на 10 сек.Что сделать: Connection pool; HTTP/2 (но etcd v3 JSON gateway не поддерживает); нативный gRPC.
6.6. HIGH — WS keepalive 5 сек fixed, drop после одного miss
Файл:
etcd_state_client.cpp:262-264, 538-57055000 × 1 ping/5sec = 11000 ping/sec на etcd + 11000 progress_notify reply. Если
ws_alive == 0(ответ не пришёл за 5 сек) — немедленный disconnect. При нагруженном etcd 5 сек недостаточно → cascading reconnects.Что сделать: Default 15-30 сек; 2 пропущенных подряд; jitter.
6.7. HIGH —
peer_statesхранит полныйjson11::Json× 5000 OSDФайл:
etcd_state_client.h:139json11::Jsonshared_ptr-based. Per OSD ~1-2 КБ heap. 5000 OSD × 1.5 КБ = ~7.5 МБ только peer_states.pool_config× 50K PG = ~12.5 МБinode_config× 10K = ~3 МБinode_by_nameдубликат имёнSteady state: ~25-30 МБ на клиента. Peak при reconnect: ~100 МБ. 1000 VM на хост = 30 ГБ RAM-overhead.
Что сделать: POD-структуры вместо JSON (-80% RAM); lazy-load inode_config по имени.
6.8. HIGH — JSON parsing синхронный, blocking, O(payload)
Файл:
etcd_state_client.cpp:359, 445-451json11::Json::parse~10 МБ/с single-threaded. Если приходит batched event с 1000 PG changes (5 МБ payload) — 500мс CPU stall клиента → блокирует все I/O.Что сделать: Нативный gRPC + protobuf (10× быстрее); streaming JSON parser.
6.9. HIGH — HTTP READ_BUFFER_SIZE = 9000 + O(N²) конкатенация
Файл:
http_client.cpp:23, 502При чтении 35МБ ответа: 35МБ/9000 = ~3900 recvmsg + 3900
response += std::string(rbuf.data(), 9000)= аллокация + копия + конкатенация. Worst case квадратичное копирование 35МБ. 5-10 сек CPU.Что сделать: READ_BUFFER 64-256 КБ;
response.append(); pre-reserve по Content-Length.6.10. MEDIUM —
pick_next_etcdround-robin прилипает к одному узлуФайл:
etcd_state_client.cpp:294-322selected_etcd_addressсбрасывается только при ошибках. 55000 клиентов прилипают к одному etcd-узлу. etcd HTTP gateway single-process, max ~10k connections. Дисбаланс нагрузки.6.11. MEDIUM —
load_pgsretry indefinitely без backoffФайл:
etcd_state_client.cpp:701-712(// Retry indefinitely)55000 клиентов ретраются каждые 5 сек, гарантированный DDoS на recovering etcd. Никакого backoff/jitter.
Ответы
Максимум клиентов для 3-node etcd cluster: ~3000 одновременных (OSD + клиенты).
RAM на клиента при 5000 OSD: ~25-30 МБ steady state, peak ~100 МБ при reconnect.
Нужен ли proxy: ДА, АБСОЛЮТНО НЕОБХОДИМ. Без него Vitastor не масштабируется выше ~1000-3000 VM/clients на cluster.
Часть 7. cluster_client + VM density
Подсистема:
src/client/cluster_client.cpp/h,cluster_client_impl.h,cluster_client_wb.cpp,vitastor_c.cpp,qemu_driver.c,ublk_server.cpp,nbd_proxy.cpp,fio_cluster.cpp.7.1. CRITICAL — Каждый процесс — независимый cluster_client (S20)
Файлы:
vitastor_c.cpp:107,129,153,194,209,qemu_driver.c:555,583,ublk_server.cpp:244,nbd_proxy.cpp:517,fio_cluster.cpp:296,304Каждый
vitastor_c_create_*()создаётnew cluster_client_t(...)с собственнымиring_loop,epoll_manager,messenger, копиями метаданных.200 VM на хосте:
rdma_contextс MR — очень дорого по pinned memory и HW resources (NIC лимит на QP/MR)Эффект:
/proc/sys/kernel/io_uring)load_pgs()с полным range-scanЧто сделать: ОБЯЗАТЕЛЬНО ввести per-host
vitastor-proxydaemon, к которому VM подключаются через unix socket/shared memory ring. Один процесс — одно соединение на OSD.7.2. CRITICAL — Полная загрузка ВСЕХ inode_config
Файл:
etcd_state_client.cpp:670-675, 1283-1293load_pgs()тащит все/config/inode/ключи. Для облака с 1М образов: 1M × 300 байт = 300МБ+ на каждый клиент + дублирование вinode_by_name. 200 VM × 300МБ = 60ГБ RAM на хост.Что сделать: Lazy-load inode по требованию (watch_inode по имени → точечный запрос).
7.3. CRITICAL — Per-PG state в
pool_configФайл:
etcd_state_client.h:66,etcd_state_client.cpp:1045, 1081Полная копия
pool_configсо всеми PG,target_set,target_history,all_peers. На PG: ~200-500 байт + vector. 100 пулов × 1024 PG × ~500 байт ≈ 50МБ на клиента. При 5000 OSD × 100 PG = 500K PG → ~250МБ на одного клиента. 200 VM × 250МБ = 50ГБ RAM.Что сделать: Shared pool_config через per-host proxy.
7.4. CRITICAL — Один TCP/RDMA на (client × OSD), НЕ shared между клиентами
Файл:
messenger.cpp:501-515,messenger.h:223-224osd_messenger_t— приватный член cluster_client_t. Каждый клиент держит своиclientsиosd_peers. Один OSD = одно соединение с каждого клиента.200 VM × 5000 OSD = до 1M TCP-соединений на хост. С kernel sk_buff/recv/send buffers (default 256КБ+256КБ) = 128ГБ kernel memory.
При RDMA:
recv_buf= 2МБ +send_out= 1МБ = ~3МБ pinned на RDMA-соединение. 200 × 5000 × 3МБ = 3ТБ pinned RAM — физически невозможно.Что сделать: ОБЯЗАТЕЛЬНЫЙ per-host proxy для multiplexing RDMA QP и shared MR.
7.5. CRITICAL — НЕТ rate-limiting на client side
Файл: Весь
cluster_client.cppиvitastor_c.cppВ коде нет ни одного упоминания throttle/rate/qos/iops_limit/bandwidth_limit. Клиент может посылать любое количество ops. Один tenant DoS-ит весь кластер.
Что сделать:
client_max_iops,client_max_bw_bytes, token bucket в cluster_client_t.7.6. HIGH — Лимиты dirty/buffered "soft";
writeback_overflowunboundedФайл:
cluster_client.h:9-13,cluster_client.cpp:802-813, 784-787Defaults:
DEFAULT_CLIENT_MAX_DIRTY_BYTES=32MB,DIRTY_OPS=1024,WRITEBACK_IODEPTH=256на ОДНОГО клиента. 200 VM × ~64МБ = 12.8 ГБ writeback memory на хост.При превышении
writeback_iodepth— операции вwriteback_overflow(unbounded vector!). Misbehaving VM пишет быстрее сети → vector растёт без ограничения по памяти → OOM kill процесса.7.7. HIGH —
op_queueрастёт без явного лимитаФайл:
cluster_client.cpp:836-852, 118Двусвязный список
op_queue_head/tailнеограничен. При network partition клиент продолжает принимать операции от QEMU/UBLK. На 100K ops ≈ сотни МБ → OOM при длительной недоступности.7.8. HIGH — При смерти OSD весь dirty writeback переотправляется
Файл:
cluster_client.cpp:39-45,cluster_client_wb.cpp:266-298repeat_ops_for(peer_osd)сканирует всюdirty_buffersmap (O(N) где N = все dirty buffers по всем inode), на каждом вызове. Каждый вызов делаетpool_config.at(INODE_POOL(inode))lookup вstd::map. Failover одного OSD из 5000 → 200 VM × O(N=1024) обходов = высокий transient CPU. При rack power loss → 50 OSD одновременно → CPU pinning.7.9. HIGH — Failover triggers per-client work
Файл:
cluster_client.cpp:639-647(on_change_osd_state_hook),:613-624(on_change_pg_state_hook)Каждый клиент получает уведомление через etcd watch. При flapping OSD — шторм событий. При reshuffle 1024 PG поменяли primary = 1024 × O(dirty_buffers) операций.
7.10. HIGH — VM с 5 дисками = 5 cluster_client (нет sharing внутри QEMU)
Файл:
qemu_driver.c:71-102, 555, 583VM с 5 виртуальными дисками = 5×~600МБ = 3ГБ на одну VM только для клиента.
Что сделать: Singleton vitastor_c per (config, etcd_addr) внутри одного QEMU процесса.
7.11. HIGH —
list_inodeдержит ВСЕ объекты в памяти до завершения PGФайл:
cluster_client_list.cpp:38, 413, 444pg->objects—std::set<object_id>накапливает ВСЕ объекты PG. 16 байт + set overhead ~50 байт/элемент. Pool с миллиардами файлов (VitastorFS) → десятки миллионов объектов → полностью съесть RAM клиента.Что сделать: Streaming listing (batched callback во время приёма).
7.12. HIGH — UBLK pinned 256МБ buffers/устройство
Файл:
ublk_server.cpp:86-114, 189-357buffers(vector<uint8_t*>) —queue_depth=256×max_io_buf_bytes=1MB= 256МБ pinned buffers на устройство. 200 UBLK-устройств × 256МБ = 50ГБ pinned memory только на I/O буферы.7.13. HIGH — NBD = отдельный процесс на каждое устройство
Файл:
nbd_proxy.cpp:475-707Каждое NBD-устройство =
nbd_proxyпроцесс с полным cluster_client (~600МБ base + per-OSD соединения). 200 NBD = 120ГБ RAM. Плюс лимитnbds_max=64по умолчанию.7.14. HIGH — При timeout in-flight ops НЕ отменяются
Файл:
cluster_client.cpp:1060-1093OSD_OP_WAIT_UP_TIMEOUTтолько если PG inactive. Если OSD принял запрос и завис — клиент будет ждать ВЕЧНО.qemu_co_flush(qemu_driver.c:1102-1120) блочно ждёт без таймаута → VM hang in D-state.7.15. HIGH —
vitastor_c_destroy()безflush()теряет writebackФайл:
vitastor_c.cpp:218-228Destructor не дожидается inflight ops. Callbacks теряются. fio_cluster.cpp:347-363 — не делает flush перед destroy → потеря данных в fio с writeback.
Ответы
Сколько VM на гипервизоре без proxy:
Нужен ли per-host vitastor-proxy: ДА, БЕЗУСЛОВНО. Без него IaaS с RDMA RoCEv2 физически нереализуем.
Какие защиты отсутствуют:
Часть 8. Recovery / peering
Подсистема:
src/osd/osd_peering.cpp,osd_peering_pg.cpp/h,osd_flush.cpp,osd_scrub.cpp.8.1. CRITICAL —
target_historyне GC при degraded → OOMФайлы:
osd_peering.cpp:700-712,osd_primary_write.cpp:206-215В состоянии
PG_ACTIVE|PG_DEGRADEDилиPG_HAS_MISPLACEDtarget_historyникогда не очищается (условие GCpg.state == PG_ACTIVEбез флагов). Каждыйpg.epoch++(osd_primary_write.cpp:194) при degraded состоянии добавляет запись вtarget_history.Сценарий:
target_history.size() = 10000, ~400 КБ на 1 PGreport_pg_state→ etcd получит JSON value 400 КБ × 50000 = невозможноЭффект: OOM на OSD; etcd value overflow; кластер мёртв.
Что сделать:
target_history.size()(FIFO 1024)pg.stateвключаетPG_ACTIVEнезависимо от degraded/misplaced8.2. CRITICAL —
calc_object_states— std::sort всего списка + 24-40 байт × NФайл:
osd_peering_pg.cpp:407-449st.listobj_ver_role≈ 40 байт (с alignment) → 30M × 40 = 1.2 ГБ на одну PGstd::sort30M элементов ≈ 30M × 25 × 100ns = 75 секунд CPU на 1 PGЧто сделать: Стриминг + k-way merge (BS_OP_LIST blockstore возвращает stable + unstable раздельно, требовать sorted output).
8.3. CRITICAL — Auto-tune cutoff bug → recovery НЕ throttle'ится при перегрузке
Файл:
osd_flush.cpp:420-423При перегрузке (target_lat огромный) sleep > 10s →
returnбез обновления. При перегрузке recovery НЕ замедляется, продолжает использовать предыдущее (низкое) значение sleep, усугубляя перегрузку.Что сделать: Заменить на
sleep_us = recovery_tune_sleep_cutoff_us;(cap).8.4. CRITICAL —
flush_actions: std::map с O(N) memoryФайл:
osd_peering_pg.h:124,osd_peering_pg.cpp:199-234PG с 10M объектов где половина unclean → 15M
obj_piece_id_t → flush_action_tзаписей. std::map: каждый node ~64 байт + key 24 + value 24 = ~112 байт/запись. 15M × 112 = 1.68 ГБ на ОДНУ PG.Эффект: При массовом crash recovery — OSD умирает от OOM до flush.
Что сделать:
btree::btree_map(уже используется дляinconsistent_objects); стриминг flush_actions.8.5. CRITICAL —
submit_pg_flush_opsO(N²) при больших flushФайл:
osd_flush.cpp:9-65, 67-157FLUSH_BATCH=512на peer. 15M actions / 512 / N_peers ≈ 10K батчей. Каждыйsubmit_pg_flush_opsлинейно ищет с begin.handle_flush_opтоже walks from begin → O(N²) для всего flush! Для 15M: O(225 × 10¹²) = дни CPU.Что сделать: Cursor/iterator в pg; handle_flush_op erase из map.
8.6. HIGH —
handle_peersобходит ВСЕ PG на каждой итерации event-loopФайл:
osd_peering.cpp:15-7450000 PG,
loop()~миллионы раз/сек. 50k указательных операций × log = 750K ops × 10kHz = 7.5 GOPS только на пустой обход. Cache miss на std::map узлах.Что сделать: Отдельные
std::list<pg_t*>для разных состояний; итерировать только нужные подмножества.8.7. HIGH — Нет concurrency limit на параллельный peering
Файл:
osd_peering.cpp:15-40Restart OSD → 50000 PG × 3 peers = 150000 одновременных list operations. BS_OP_LIST держит heap mutex → locks contention. TCP/RDMA backlog заполнится → retry storm.
Что сделать: Семафор max_concurrent_peering = 32.
8.8. HIGH — Stuck PG не имеет detection
Файл:
osd_peering.cpp:332-377PG в
PG_PEERINGждётlist_results[peer]. Если peer "тихо мёртв" — peering висит бесконечно. Часть данных недоступна, мониторинг не показывает причины.Что сделать: Timeout на каждый list_op; watchdog для PG в peering > N сек.
8.9. HIGH —
state_dict— до 192 МБ per 1 ТБ worst caseФайл:
osd_peering_pg.h:121(комментарий!)Заявлено
~ (raw storage / object size) * 24 bytes. Для 100 ТБ OSD при block_size=128КБ: 100×10¹²/128000 × 24 = 18.75 ГБ на OSD.8.10. HIGH — Нет log-based / delta recovery: всегда full object copy
Файл:
osd_flush.cpp:283-326submit_recovery_opвсегда триггерит full READ+WRITE. Object 4 МБ изменился 1 байт → recover пишет полные 4 МБ. EC 5+3: 1.25 МБ I/O на восстановление одного объекта.Что сделать: Diff-based recovery через сравнение версий + block-level dirty tracking.
8.11. HIGH — Mass-rebalance: pg_count не пересоздаёт PG инкрементально
Файл:
mon/pg_utils.js:75-89Non-multiple pg_count change → все PG получают merged history of ALL previous. При 50000 PG: каждый получит osd_sets = объединение всех 50000 = 2 МБ на каждый PG = 100 ГБ суммарно. etcd value overflow.
Что сделать: Multiple-only pg_count changes; incremental migration с GC.
8.12. MEDIUM —
pick_next_recoveryне проверяетscrub_ops/write_queueФайл:
osd_flush.cpp:233-289Только
recovery_ops. Сравните сosd_scrub.cpp:144-146(три проверки). Recovery + scrub конфликт → ложное LOC_CORRUPTED.Ответы
Peering time после рестарта 100 OSD на 5000 OSD:
Безопасный максимум PG на OSD: 100-200 (текущий код без правок: ~256-1024)
Scrub PB/сутки: Идеально ~55 PB raw; реально ~15-25 PB (с auto-tune); при крупных PG < 1 PB/сутки.
Часть 9. NFS / KV
Подсистема:
src/nfs/*,src/kv/kv_db.cpp,vitastor_kv.h.9.1. CRITICAL — Single-threaded event loop на все клиенты
Файл:
nfs_proxy.cpp:256-258, 318-322Один proxy = один
ring_loop_t+ одинepoll_manager_t. Все RPC выполняются в одном event loop, все state (block_cache, list_cookies, idgen, touch_queue, used_buffers, allocating_shared) в std::map/std::set/std::vector. На 64+ ядрах используется одно.Эффект: При 10K NFS-клиентов CPU-bound 1 ядро становится bottleneck'ом.
Что сделать: Sharded multi-proxy с L3 LB; либо multi-threaded event loop.
9.2. CRITICAL — JSON-сериализация всех inode-операций
Файлы:
nfs_kv.cpp:355-410, nfs_kv_getattr.cpp:23-46Каждый GETATTR/SETATTR/LOOKUP/READ/WRITE/CREATE/REMOVE/RENAME/LINK выполняет
json11::Json::parseиJson(ientry).dump(). JSON-парсинг ~10-100x медленнее бинарной структуры. На 100K req/sec — несколько ядер только на JSON.9.3. CRITICAL — KV block cache unbounded под write-pressure
Файл:
kv_db.cpp:923-977try_evictпропускает блоки сupdating > 0. При большой write-нагрузке все блоки на горячем пути updating → cache превышаетcache_max_blocks. Defaultmemory_limit=128 MiB.9.4. CRITICAL — KV B-tree глубина растёт с N entries
Файл:
kv_db.cpp:1144-1183, 1545-1602При
kv_block_size=4096и средней value ~150 байт: leaf ~22 entries, fan-out ~80. 100M direntries: depth = 5 levels. Cold-cache LOOKUP = 5 RDMA round-trips = 5-10ms. NFS LOOKUP = 2 KV gets = до 10 round-trips. Под массовымfind— unusable.9.5. CRITICAL — Global
cur_shared_inode— single-point bottleneck для small writesФайл:
nfs_kv_write.cpp:56-145Все маленькие файлы (<
pool_block_size) пишутся в один глобальныйcur_shared_inodeс растущимcur_shared_offset. Все клиенты конкурируют за один inode. При создании 1000 мелких файлов параллельно — все попадают в одну shared inode → write contention в OSD pool, последовательная аллокация offset.9.6. CRITICAL —
list_cookiesmap растёт без ограниченияФайл:
nfs_kv_readdir.cpp:294,nfs_kv.h:71Каждый READDIR сохраняет в
list_cookies{dir_ino, cookieverf, cookie} → keyдля каждой прочитанной entry. Очистка только когда клиент пришлёт READDIR с новым cookieverf. Если клиент уходит/timeout → записи остаются навсегда. 1000 клиентов × 100K entries = 100M записей × 100 байт = десятки ГБ в std::map.9.7. HIGH —
read_hack_cacheflush каждую секунду на ВСЕ inodesФайл:
nfs_kv.cpp:412-446(touch_inodes, каждые 1000мс)Для RDMA-клиентов
read_hack_cache(fattr3 hack).touch_inodes()делаетread_hack_cache.clear()раз в секунду. После clear — все RDMA READ снова делают KVget. Периодические "lag spikes" каждую секунду на RDMA-нагрузке.9.8. HIGH —
touch_queueсинхронный цикл по всем dirty inodesФайл:
nfs_kv.cpp:416-424Каждую секунду
touch_inodes()обходит ВСЕ inodes изtouch_queue. На 100K writes/sec → 100K записей × 2 round-trip KV op = блокирует event loop.9.9. HIGH — Mass-rename/rmdir — нет атомарности и параллелизма
Файл:
nfs_kv_rename.cpp:31-382, nfs_kv_remove.cpp:30-305RENAME каталога — non-atomic (комментарий line 33). При rename каталога с миллионом файлов —
parent_inoкаждого подкаталога рекурсивно НЕ обновляется. RMDIR с миллионом — минуты, невозможно abort.9.10. HIGH — Каждый WRITE генерирует отдельный
write_inodeдля shared_verФайл:
nfs_kv_write.cpp:893-924, 1018-1022При записи 100КБ через 100 NFS writes по 1КБ — 100 KV-set, каждый = 1-2 OSD write 4 КБ.
9.11. HIGH —
volume_removed,extends,free_buffers— unbounded growthФайлы:
nfs_kv.cpp:429-435, nfs_kv_write.cpp:1002-1028, nfs_proxy.cpp:998-1018volume_removedmap → O(unique_shared_inodes);kvfs->extends→ O(active extending inodes);xdr_pool/free_buffersper-client (RPC_INIT_BUF_SIZE=32 КБ, MAX_REQUEST_SIZE=128 МБ) — никогда не уменьшается.9.12. HIGH — RDMA: один CQ на устройство, mandatory memcpy на reply
Файл:
nfs_proxy_rdma.cpp:67-85, 312-411, 659-678Linux NFS RDMA bug workaround:
rop->buffer = rdma_malloc_alloc(...)+memcpyкаждого reply.9.13. HIGH —
nfs_rdma_credit = 16слишком низкоФайл:
nfs_proxy.h:41,nfs_proxy.cpp:222-224Один RoCEv2-клиент ограничен 16 outstanding RPC. При 10µs RTT + 50µs processing = ~16/66µs = 240K iops max. Сотни тысяч ops per VM нет.
9.14. HIGH —
id_alloc_batch_size=200глобальный — mass-create bottleneckФайл:
nfs_kv.cpp:270-272, nfs_kv_create.cpp:34-85При mass-create CAS на
idkey — все CREATE стоят в очереди. Несколько тысяч/сек не выше.9.15. MEDIUM —
fsync= global fsync на весь кластерФайл:
nfs_mount.cpp:34-50// fsync. we don't know how to fsync a single inode, so just fsync everything. OSD_OP_SYNC синхронит все pgs кластера → сотни ms. Один тяжёлый COMMIT от одного клиента стопит ВЕСЬ кластер.9.16. MEDIUM — Per-mount/per-fs state: один proxy = один FS
Файл:
nfs_proxy.h:51, nfs_proxy.cpp:294-298Для отдачи M filesystem'ов нужно M процессов. 100 FS на хосте → 100 процессов, 100 cluster_client'ов, 100 etcd-watches, сотни МБ RAM.
9.17. MEDIUM —
readdir_getattr_parallel=8фиксированныйФайл:
nfs_kv.cpp:267-269, nfs_kv_readdir.cpp:49-71ls -laв каталоге 100K файлов = 100K KV gets через 8 parallel = 12500 sequential rounds. На RDMA 10µs — 125ms. На TCP 1ms — 12 секунд.Ответы
NFS клиентов на proxy без переписывания: 100-500 активных.
Максимум inode в KV-FS:
Готов ли NFS для production hyperscaler: НЕТ.
Vitastor NFS — это бета-quality для лимитированной нагрузки, никак не аналог Ceph CephFS MDS (который сам spans десятки MDS shards).
Часть 10. CSI / Kubernetes
Подсистема:
csi/src/*.go.10.1. CRITICAL — Каждый CSI вызов = fork+exec
vitastor-cli+ watch всех inodesФайл:
csi/src/controllerserver.go:115-123(invokeCLI)Вызывается из
CreateVolume,DeleteVolume,CreateSnapshot,DeleteSnapshot,ListSnapshots,ControllerExpandVolume. Каждый запуск создаётring_loop_t,epoll_manager_t,cluster_client_t— а он подписывается watch на/config/inode/,/config/pgs,/osd/stateи ждёт INIT-снапшот.Сценарий: 1000 PV в кластере = 1000 inode записей. На каждый
CreateVolume:--timeout=150sистечёт → retry stormЧто сделать: Persistent connection к etcd прямо в Go (
go.etcd.io/etcd/client/v3), убрать exec. Кэшироватьcluster_client_t.10.2. CRITICAL — NBD nbds_max=128 hardcoded в Dockerfile
Файл:
csi/Dockerfile:31, Dockerfile.local:31Жёсткий лимит = 128 NBD устройств на узел. Hyperscaler: 200-500 pod'ов на узле → physical lock'нется на 128. После загрузки модуля
nbds_maxНЕ МЕНЯЕТСЯ — только перезагрузка ядра.10.3. CRITICAL — Только 1 active controller (leader election)
Файл:
csi/src/server.go:31-36, grpc.go:56-115,007-csi-provisioner.yaml:25(replicas: 3)3 реплики через
csi-provisioner --leader-election=true— фактически работает только 1. Latency запуска vitastor-cli ≈ 500ms-2s. Максимум 0.5-2 PV/sec на кластер. 1000 PV = 8-30 минут.10.4. HIGH —
Probe()всегда возвращает success без проверкиФайл:
csi/src/identityserver.go:64-67Нет проверки etcd connectivity, vitastor-client, дисков. Зомби-CSI не будет рестартован.
10.5. HIGH —
volumeLocks— глобальный mutex с broadcast cond, thundering herdФайл:
csi/src/nodeserver.go:42-44, 115-133200 pod'ов на узле, kubelet делает параллельно 200 NodeStageVolume → 200 горутин. На каждом unlock — все 200 пробуждаются, конкурируют за
mu.Lock.10.6. HIGH — VolumeId = JSON, раздутый и хрупкий
Файл:
csi/src/controllerserver.go:183, 187, 242, 246, 513, 610JSON
{"configPath":"/etc/vitastor/vitastor.conf","name":"...","pool":"testpool"}уже ~85 байт. CSI spec: volume_id ≤ 128 байт. Сvitastorfs/префиксом/snapshot — легко перевалит. Storage class изменения сломают существующие PV.10.7. HIGH —
ListVolumesНЕ имплементирован, но capability публикуетсяФайл:
csi/src/controllerserver.go:404-413, 430-437CSI spec требует для health-monitor sidecar. Не работает CSI Volume Health.
10.8. HIGH —
DeleteVolumeНЕ идемпотентен → бесконечный retryФайл:
csi/src/controllerserver.go:282-287Если предыдущий DeleteVolume успел удалить, но не успел ответить (например, был убит) → retry получит "image not found" → InternalError → kubelet бесконечный retry. CSI spec: DeleteVolume должен возвращать SUCCESS если volume не существует.
10.9. HIGH — При смерти узла device остаётся "залочен" в Vitastor
Файл:
csi/src/nodeserver.go:415-615, state-файлы/run/vitastor-csi/State в
/run(tmpfs!). Node умирает →/runочищается. Lock в etcd НЕ ОЧИЩАЕТСЯ. Failover пода на другой узел → "image is already locked".Что сделать:
vitastor-cli rm-osd-stateили явно очистить старые lock'и при boot; передавать NodeID для идемпотентного освобождения.10.10. HIGH — UBLK ~64 устройств на узел (kernel default)
Файл:
src/client/ublk_server.cpp:355-374Не передаются параметры
ublks_max. Дефолт вublk_drv= 64 (UBLK_MAX_UBLKS). 65-й Stage упадёт.10.11. MEDIUM —
os.WriteFile(stateFile)без SyncФайлы:
csi/src/utils.go:253, 411Файлы в tmpfs
/run/vitastor-csi. При kernel panic state теряется → все mappings потеряны.10.12. MEDIUM —
NodeGetVolumeStatsиNodeExpandVolumeНЕ реализованыФайл:
csi/src/nodeserver.go:1027-1037K8s метрика
kubelet_volume_stats_*пустая, мониторинг ёмкости не работает (важно для billing!). Expansion требует pod restart.10.13. MEDIUM — Outdated sidecars
csi-attacher:v3.1.0(текущая 4.x),csi-snapshotter:v4.0.0(текущая 7.x),csi-resizer:v1.1.0(текущая 1.10+),csi-provisioner:v3.0.0(текущая 5.x). На K8s 1.30+ возможны несовместимости.Ответы
PV на K8s узел:
Practical safe: ~50 PV на узел. Для hyperscaler (300-500 pod'ов на узел) — недостаточно.
Одновременных PV creations: Safe rate ~5-10 PV/sec. Burst 1000 PV — НЕ ПОДДЕРЖИВАЕТСЯ.
Готов ли CSI к production hyperscaler: НЕТ, КАТЕГОРИЧЕСКИ.
Минимальные блокеры: F-01 (persistent etcd), F-02/F-13 (поднять лимиты NBD/UBLK), F-09 (идемпотентность Delete), F-12 (cleanup state при node death), F-04 (реальный health-check), F-23 (online expand + volume stats).
Часть 11. Blockstore (RAM на OSD)
Подсистема:
src/blockstore/*,src/util/allocator.cpp.11.1. CRITICAL —
inmemory_metadata=trueзагружает ВСЁ meta-area в RAMФайлы:
blockstore_init.cpp:165-189,blockstore_impl.cpp:36-39,blockstore_heap.cpp:240-255По умолчанию
inmemory_meta=trueиinmemory_journal=true.Для 16 ТБ NVMe / 1MB block / crc32c:
clean_dyn_size= 2×bitmap(32) + 4×256 (csum) = 1088 байт/объектmin_meta_len ≈ 17.6 ГБметаданных НА ДИСКЕblock_info.resize(meta_block_count)= 4.6M × ~80 байт = ~370 МБ только block_infoИтого 20-40 ГБ RAM на OSD при типичной конфигурации с 16 ТБ NVMe.
При data_block_size=128KB (более гранулярное хранилище): 16ТБ/128К = 128M entries → ~150 ГБ RAM — невозможно.
11.2. CRITICAL —
heap_list_item_tчерезmalloc()per entryФайл:
blockstore_heap.cpp:432-438, 1527-1529malloc_or_die(wr_orig->size + sizeof(heap_list_item_t) - sizeof(heap_entry_t))на КАЖДЫЙ entry. Big entry с csum: 1128 байт. 16M объектов × 1144 байт = ~18.3 ГБ только на entries + glibc fragmentation 10-15% = +2 ГБ.При длительной работе heap фрагментируется. Multiple circle write/compact циклы оставляют дырки.
Что сделать: Pool/slab allocator для
heap_list_item_t(3-4 размерных класса).11.3. CRITICAL —
inflight_lsnstd::dequeбез bound при slow fsyncФайл:
blockstore_heap.h:210, 214-217, blockstore_heap.cpp:2446-2466При
journal_trim_interval=4096между trim'ами 4096 inflight lsn ≈ 64 КБ — OK. Но если fsync задержан (медленный диск, RAID rebuild) —push_inflight_lsnresizeмассива → миллионы записей → сотни МБ.11.4. CRITICAL — Cold start blocks 20-60 сек без heartbeat
Файл:
blockstore_init.cppДля 16M объектов:
Итог: 30-60 секунд. Vitastor monitor может пометить такой OSD как failed → ребалансировка.
Что сделать: Early heartbeat в monitor; параллелизовать парсинг; lazy-load.
11.5. CRITICAL —
iterate_with_stablewalks ВСЮ chain на каждом readФайл:
blockstore_heap.cpp:2028-2067При активной записи в один объект (БД), chain длинный. Каждый read делает full walk → microseconds на read. В EC + scrub — multiplicative.
Что сделать: Cache pointer на stable entry; сокращать chain в
apply_inflight.11.6. CRITICAL —
block_index— двойной hash map: PG → inode → stripesФайл:
blockstore_heap.h:173-189robin_hood::unordered_flat_map<uint64_t, robin_hood::unordered_flat_map<inode_t, void*>>. Внутренний map: 56 байт пустой + bucket arrays. 1M inodes × 56 байт = 56 МБ только заголовки. 16M объектов: ~500 МБ только индекс.11.7. HIGH —
block_infoмассив O(meta_block_count)Файл:
blockstore_heap.cpp:251, blockstore_heap.h:127-128heap_block_info_t≈ 64 байта. Для 16 ТБ + 1MB + crc32c: 4.6M × 64 = ~290 МБ. + entries 16M × 8 = +128 МБ. С vector capacity overhead: ~500-800 МБ.Что сделать: Sparse representation: hash map block_num → block_info вместо плотного vector.
11.8. HIGH —
data_allocbitmap O(data_block_count), 2G блоков лимитФайл:
src/util/allocator.cpp:12Жёсткое ограничение
blocks < 0x80000000(2^31 = 2.1G блоков). При 4K block max data = 8 TB — HDD с малым block_size не работают.11.9. HIGH —
inode_space_statsstd::mapФайл:
blockstore_heap.h:196std::map node ~64 байт. 10M inodes → ~700 МБ.
Что сделать:
robin_hood::unordered_flat_map.11.10. HIGH —
recheck_full_gc()при старте — O(meta_block_count)Файл:
blockstore_heap.cpp:816-854При gc_on_start=true: на каждом блоке O(entries × remove_list_item). 16M объектов с 30% garbage → ~5M entries → минуты.
11.11. HIGH —
list_objectsчерезrealloc()с doublingФайл:
blockstore_heap.cpp:2173-2250Listing 1M объектов:
obj_ver_id(24 байта) × 1M = 24 МБ single allocation per LIST. При doubling peak 48 МБ. Полная блокировка event loop на время listing.11.12. HIGH — Reshard копирует ВСЕ entries
Файл:
blockstore_heap.cpp:1182-1296Для 16M объектов: ~16M операций move = минуты.
11.13. HIGH — Нет hard cap на live_memory; compactor не успевает
Файл:
blockstore_heap.cpp:1527-1546На каждый write —
malloc()нового heap_list_item_t. Старые держатся до compact. Если compactor не успевает → unbounded growth. Backpressure только при ENOSPC.11.14. HIGH —
submit_queuestd::vector<blockstore_op_t*>unboundedФайл:
blockstore_impl.h:111При OSD overload: storage не успевает, OSD client продолжает enqueue → 100K pending = 25 МБ ops + 800K байт pointers.
11.15. CRITICAL — V1 (legacy):
dirty_db,used_sectors,clean_bitmapsФайлы:
v1/impl.h:85, v1/journal.h:53, v1/open.cpp:159V1 формат:
dirty_dbstd::map— ~96 байт/entry, неограниченный рост при больших журналахused_sectors— 80 МБ на 1 ГБ journal в worst caseclean_bitmapsmalloc(block_count × 2 × 32)= 1 ГБ для 16 ТБ + 1MB block, 8 ГБ для 128K blockV1 не рекомендовать для больших OSD.
Ответы
RAM на 16 ТБ NVMe в типичной конфигурации:
При data_block_size=128KB: ~150 ГБ RAM — невозможно.
Максимальный размер OSD:
Холодный старт 16M объектов: 30-60 секунд (single-threaded init) — критично для авто-failover.
Часть 12. Deployment / disk_tool (массовый roll-out)
Подсистема:
src/disk_tool/*,src/cmd/*,mon/scripts/*.12.1. CRITICAL — Race на shared SSD при parallel prepare
Файл:
src/disk_tool/disk_tool_prepare.cpp:651, 447В hybrid режиме 24 HDD + 2 NVMe для journal/meta. Параллельный
vitastor-disk prepare: два независимых процесса читают partition table → оба считают свободное место → оба вызываютsfdisk(line 447). Sfdisk перезаписывает GPT каждый раз с нуля → второй НЕ ВИДИТ partition'ов первого → разрушение GPT.Никакого flock на parent_dev при sfdisk.
Что сделать: flock на parent_device перед
read_parttable+sfdisk.12.2. CRITICAL — Sequential обработка дисков в prepare
Файл:
src/disk_tool/disk_tool_prepare.cpp:794-82824 NVMe ×
osd_per_disk=4= 96 OSD на сервер. Циклfor (auto & dev: devinfo)строго последовательный. Для каждого OSD:vitastor-cli alloc-osd— etcd round-trip ~10-50 мсsfdisk+partprobe+ ожидание/dev/disk/by-partuuid/...до 30 секudevadm trigger --settle— блокирует на все pending udev eventssystemctl enable --now vitastor-osd@N— блокирующий~5-15 минут на сервер с 96 OSD.
12.3. CRITICAL —
alloc-osdO(N²) etcd traffic при mass allocФайл:
src/cmd/cli_alloc_osd.cpp:30-981000 OSD создаются параллельно:
O(N²) etcd transactions. Для N=1000 это ~500 000 транзакций × 5 мс latency = ~40 минут только на alloc-osd.
Что сделать: Атомарная инкрементная схема (etcd lease/sequence) или batched alloc-osd
--count N.12.4. CRITICAL —
vitastor-disk purgeНЕ параллелитсяФайл:
src/disk_tool/disk_tool_udev.cpp:430-557Покупаем bare-metal recovery для сервера с 96 OSD.
vitastor-disk purge /dev/nvme*:rm-osd— единая команда (хорошо для etcd) ✓systemctl disable --now vitastor-osd@N— синхронное ожидание stop каждого. Для 96 OSD при stop_timeout 90 сек теоретически до 144 минутДесятки минут на purge сервера.
12.5. HIGH — clear_osd_superblock без fsync, неатомарный purge
Файл:
src/disk_tool/disk_tool_udev.cpp:402-428Между
rm-osd(line 487) иclear_osd_superblock(line 500) нет атомарности. Если процесс убит — OSD исчез из etcd, но superblock на диске остался валидным → при следующем boot udev rule запуститsystemctl enable vitastor-osd@Nс уже несуществующим N → loop рестартов.12.6. HIGH — Boot storm 96 OSD одновременно
Файлы:
mon/scripts/90-vitastor.rules:6,vitastor-osd@.service:3After=network-online.target local-fs.target time-sync.target— все 96 ждут одно и то же → запустятся "залпом":12.7. HIGH — hostname как failure domain
Файл:
src/osd/osd_cluster.cpp:171,mon/osd_tree.js:31OSD регистрируется с
host = gethostname(). Дублирование hostname (defaultlocalhost.localdomainпри misconfigured kickstart) → ВСЕ OSD из разных серверов в одном host node → EC pool может разместить данные на одной машине → потеря данных при отказе.Что сделать:
node_placementили config-driven host (по UUID/serial).12.8. HIGH — Pool create НЕ атомарен с node_placement
Файл:
src/cmd/cli_pool_create.cpp:225-277CAS только на
config/pools, НЕ наnode_placement. Между read snapshot и put pool — мог быть изменёнnode_placement→ pool создаётся с поломанным размещением.12.9. HIGH —
rm-osdretry loop без атомарностиФайл:
src/cmd/cli_rm_osd.cpp:300-326При retry exhaustion
removed OSDs from etcdУЖЕ ПРОИЗОШЛО. OSD удалён, но история PG может содержать его какall_peers→ "zombie" вall_peers,left_on_dead.12.10. HIGH —
cli_modify_osdне делает retry при CAS failureФайл:
src/cmd/cli_modify_osd.cpp:172-177Просто
return EAGAIN. Никакого автоматического retry. В отличие отcli_pool_modify(line 172-177) сgoto resume_0— API не единообразен.12.11. HIGH — Purge неатомарный (dry-run → stop → rm)
Файл:
src/disk_tool/disk_tool_udev.cpp:469-490Между шагами
rm-osd --dry-runи реальнымrm-osdпроходит много времени (минуты). За это время состояние pool могло измениться (другой OSD упал) → теперь удаление приводит к data loss. OSD в "ничейном" состоянии.12.12. MEDIUM —
add_partitionsждёт partuuid до 30 сек per partitionФайл:
src/disk_tool/disk_tool_prepare.cpp:470-522Создание 96 OSD на сервере: 1.5-5 минут только на partprobe wait.
12.13. MEDIUM —
meta_reserve_min_size=1GBфиксирован на каждый OSDФайл:
src/disk_tool/disk_tool.h:5796 OSD × 1 ГБ = 96 ГБ wasted места только на резерв.
12.14. LOW — Нет global scrub schedule throttling
Файл:
src/osd/osd_scrub.cpp:326-336Все PG активированные одновременно (после mass deployment) получат одинаковый
next_scrubчерез 30 дней → одновременный scrub-шторм. Нет cluster-wide scrub limit.Ответы
Можно ли deploy'ить 1000 OSD за час: Нет. Реалистично ~1.5-3 часа на 1000 OSD.
alloc-osdсам по себе становится bottleneck'ом.Максимум OSD на сервере:
inmemory_metadata=0Rolling upgrade без downtime: Частично — с ограничениями. Нет встроенного mechanism'а "set noout, stop one at a time, wait active, continue" как
ceph orch upgrade. Нетversion checkпри mixed deployment. Возможно с custom orchestrator.Архитектурные рекомендации для IaaS
A. Обязательные изменения перед production hyperscaler (5000+ OSD, 50000+ VM)
A.1. Сетевой слой
Per-host vitastor-proxy daemon (КРИТИЧНО):
etcd grpc-proxy на каждом гипервизоре (КРИТИЧНО):
RDMA RoCEv2 переписать:
exit(1)на RDMA errorsA.2. OSD слой
Backpressure на client_queue_depth (CRITICAL bug):
// Implement client queue depth limit-EBUSYGraceful shutdown:
handle_sigintпоставитьstopping=true, дождатьсяshutdown(), потом exitLazy peer connections:
Заменить std::map на robin_hood:
pgs,pg_locks,unstable_writes,dirty_osdsA.3. Monitor слой
Шардирование etcd-схемы:
/pg/config/<pool_id>вместо одного/pg/config/statsChunked etcd transactions:
update_total_stats,save_new_pgs_txn,report_pg_statesConcurrency control для lp_solve:
Инкрементальный recheck:
A.4. etcd слой
Адаптивные таймауты:
etcd_mon_ttlот 5 до 30 сек для крупных кластеровПагинированный snapshot:
load_cluster_stateчерез range с limitload_pgsчерез cursor по mod_revisionHTTP/gRPC оптимизации:
A.5. NFS / CSI / Deployment
NFS multi-thread:
CSI persistent etcd connection:
Mass-deploy:
A.6. Жёсткие лимиты конфигурации
B. Прогноз готовности
C. Минимальный safe path к production hyperscaler
Фаза 1 (3 месяца) — устранение блокеров:
Фаза 2 (3 месяца) — масштабирование:
Фаза 3 (6 месяцев) — переписывание узких компонентов:
Без этих изменений Vitastor подходит только для приватных кластеров среднего масштаба (50-500 OSD, единичные тысячи клиентов).
Приложение. Что было проверено
Аудит охватил полное чтение и анализ файлов (12 параллельных агентов):
Подсистема Monitor/etcd (Часть 1):
mon/mon.js,mon-main.js,etcd_adapter.js,antietcd_adapter.js,etcd_schema.js,stats.js,prometheus.js,http_server.js,osd_tree.js,pool_config.js,utils.js,vitastor_persist_filter.jsПодсистема PG/LP (Часть 2):
mon/pg_gen.js,pg_utils.js,lp_optimizer/lp_optimizer.js,dsl_pgs.js,fold.js,simple_pgs.js,murmur3.js,stable-stringify.jsПодсистема OSD cluster (Часть 3):
src/osd/osd.cpp/h,osd_cluster.cpp,osd_main.cpp,osd_peering.cpp,osd_peering_pg.cpp/hПодсистема RDMA (Часть 4):
src/client/msgr_rdma.cpp/h,msgr_rdmacm.cpp,src/nfs/rdma_alloc.cpp/h,nfs_proxy_rdma.cppПодсистема TCP messenger (Часть 5):
src/client/messenger.cpp/h,msgr_op.cpp/h,msgr_receive.cpp,msgr_send.cpp,msgr_stop.cpp,src/util/epoll_manager.cpp/h,timerfd_manager.cpp/hПодсистема etcd_state_client (Часть 6):
src/client/etcd_state_client.cpp/h,http_client.cpp/hПодсистема cluster_client (Часть 7):
src/client/cluster_client.cpp/h,cluster_client_impl.h,cluster_client_list.cpp,cluster_client_wb.cpp,vitastor_c.cpp/h,ublk_server.cpp,nbd_proxy.cpp,fio_cluster.cpp,qemu_driver.cПодсистема Peering/Recovery (Часть 8):
src/osd/osd_peering.cpp,osd_peering_pg.cpp/h,osd_flush.cpp,osd_scrub.cppПодсистема NFS/KV (Часть 9):
src/nfs/nfs_proxy.cpp/h,nfs_kv.cpp/h,nfs_block.cpp/h,nfs_kv_*.cpp(10 файлов),nfs_mount.cpp,nfs_portmap.cpp/h,src/kv/kv_db.cpp,vitastor_kv.hПодсистема CSI (Часть 10):
csi/src/*.go(8 файлов),csi/Dockerfile, deploy yaml файлыПодсистема Blockstore (Часть 11):
src/blockstore/blockstore_*.cpp/h(полный набор),multilist.cpp/h,src/util/allocator.cpp/hПодсистема Deployment (Часть 12):
src/disk_tool/disk_tool_*.cpp/h(полный набор),src/cmd/cli_*.cpp(alloc_osd, modify_osd, rm_osd, pool_create, pool_modify, status, osd_tree, pg_ls, ls),mon/scripts/*Объём проанализированного кода: ~80 K строк C++ + ~10 K строк JS + ~3 K строк Go.
Все номера строк актуальны для коммита
e136a1df.Переделаю с помощью Opus 4.8 + ultracode