Scalability review using AI (Claude) #70

Closed
opened 2026-05-17 03:43:17 +03:00 by maxpain · 1 comment

Аудит масштабируемости 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"):

"Vitastor OSD однопоточные и всегда такими останутся." Если вам нужно выделить больше 1 ядра на 1 диск — создайте несколько OSD на разделах этого диска.

→ Следствие для нас: 24 NVMe сервер = 96+ OSD per server. Это многократно усугубляет все проблемы full-mesh (Часть 3.2) и количества соединений (Часть 5). Также по архитектурной гарантии "и всегда такими останутся" — multi-threaded OSD ждать не стоит.

"Распределение PG не основано на консистентных хешах. Вместо этого все маппинги PG хранятся прямо в etcd... Перераспределение PG по OSD выполняется через математическую оптимизацию... В таком подходе есть и потенциальный недостаток — есть предположение, что в очень большом кластере он может сломаться — однако вплоть до нескольких сотен OSD подход точно работает нормально."

→ Прямое признание автора: Vitastor рассчитан "вплоть до нескольких сотен OSD". IaaS с 5000+ OSD выходит за официально поддерживаемые пределы. Все находки части 2 (LP optimizer) — это закономерное следствие подхода, рассчитанного на меньший масштаб.

"Метаданные всегда размещаются в памяти, благодаря чему никогда не тратится лишнее время на чтение метаданных с диска. Объём метаданных линейно зависит от ёмкости диска и размера блока хранилища (block_size, по умолчанию 128 КБ). С 128 КБ блоком потребление памяти составляет примерно 512 МБ на 1 ТБ данных."

→ 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 (раздел "Клиентские компоненты"):

"Клиентская библиотека — инкапсулирует логику на стороне клиента. Соединяется с etcd и со всеми OSD, от etcd получает состояние кластера, команды чтения и записи отправляет на все OSD напрямую. В силу архитектуры все отдельные блоки данных (по умолчанию по 128 КБ) располагается на разных OSD, но клиент устроен так, что всегда точно знает, к какому OSD обращаться, и подключается к нему напрямую."

→ By-design: full-mesh клиент → ВСЕ OSD. Наши находки Части 7 (cluster_client per VM = независимый клиент со всеми соединениями) — это архитектурный фундамент, не bug. Per-host proxy daemon, который мы рекомендуем — это сменa архитектурной парадигмы, не "фикс".

Из docs/config/network.ru.md (раздел rdma_max_recv):

"Один Vitastor-клиент с RDMA использует rdma_max_recv * rdma_max_msg * ЧИСЛО_OSD байт памяти, по умолчанию — примерно 2 МБ * число OSD."

→ Официально документированный расход: 2 МБ × N_OSD на одного клиента. Для 5000 OSD = 10 ГБ pinned RAM на одного клиента. Это полностью совпадает с нашей Частью 4.2 (находка #R2) — но это документированная by-design характеристика, не баг.

Из docs/intro/features.ru.md (раздел "Планы развития"):

Планы развития включают:

  • Многопоточный клиент
  • Более быстрое переключение при отказах
  • Оптимизация слоя управления
  • iSCSI и NVMeoF прокси
  • SSD-кэширование (tiered storage)

→ Автор сам признаёт, что эти направления — TODO. Наш аудит подтверждает: эти направления критичны для IaaS.

Из docs/intro/architecture.ru.md (раздел "Принципы разработки"):

"Я люблю простые решения. Поэтому Vitastor простой и быстрый и всегда будет таковым. Я не против иногда изобрести велосипед, например, собственный простенький HTTP-клиент для работы с etcd, вместо того, чтобы взять тяжёлую готовую библиотеку. Общепринятые практики написания C++ кода с RAII, наследованием, умными указателями и так далее меня также не волнуют, поэтому если вы хотели увидеть здесь красивый идиоматичный C++ код, вы, вероятно, пришли не по адресу."

→ Это объясняет 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" для наших находок

Находка Категория Комментарий
Full-mesh O(N²) соединений (S9, 3.2) BY-DESIGN Архитектурная цитата: "клиент устроен так, что всегда точно знает, к какому OSD обращаться, и подключается к нему напрямую". Решается только per-host proxy.
Однопоточный OSD (3.3, 5, 8) BY-DESIGN "Vitastor OSD однопоточные и всегда такими останутся". Решается через "несколько OSD на разделах" — что усугубляет деплоймент и количество соединений.
Метаданные в RAM (11.1, 11.6) BY-DESIGN "Метаданные всегда размещаются в памяти". Решается только sharding на много мелких OSD.
Множественные cluster_client per VM (7.1) BY-DESIGN Документированно. Решается только per-host vitastor-proxy.
LP optimizer для PG distribution (2) BY-DESIGN (с признанием ограничения) Автор признаёт "вплоть до нескольких сотен OSD". Для тысяч — необходимо переходить на consistent hashing (CRUSH-like).
2 МБ × N_OSD RAM на клиента (RDMA) (4.2) BY-DESIGN (документировано) SRQ необходимо реализовать.
NFS proxy single-thread (9.1) BY-DESIGN (упомянуто как TODO) "Многопоточный клиент" в планах развития.
CSI fork+exec на каждый вызов (10.1) BUG/FIXABLE Технический долг, не by-design.
client_queue_depth не применяется (3.1) BUG В коде FIXME комментарий: "Implement client queue depth limit".
force_stop(1) при изменении pg_count (3.3) BUG Должен ждать drain.
SIGTERM = exit() без drain (3.4) BUG/FIXABLE shutdown() метод существует, но не используется в SIGTERM handler.
update_total_stats гигантская TXN (1.1) BUG/FIXABLE Нужна пагинация.
stop_all_pgs compare на каждый OSD (1.2) BUG/FIXABLE Можно epoch-based.
target_history без GC (8.1) BUG Должен GC независимо от degraded флага.
Auto-tune cutoff bug (8.3) BUG return вместо cap.
RDMA exit(1) на любую ошибку (4.4) BUG/FIXABLE Должен stop_client().
Nbds_max=128 hardcoded (10.2) BUG/FIXABLE Поднять до 1024+.
Hostname как failure domain (12.7) BUG/CONFIG-FIXABLE Опция node_placement есть, но default плохой.

Соотношение по нашим топ-30 находкам:

  • BY-DESIGN (требует архитектурного решения): ~7-8 находок. Их нельзя исправить мелким PR, нужно переходить на per-host proxy daemon, consistent hashing для PG, multi-thread architecture.
  • BUG/FIXABLE (можно фиксить): ~22-23 находки. Большинство — за вечер-неделю работы каждая.

Что это значит для вашей задачи

  1. Если идёте на IaaS с RDMA RoCEv2 на Vitastor — вы выходите за официально поддерживаемые пределы по архитектуре, и должны быть готовы к серьёзной собственной разработке или к финансированию разработки Vitastor.

  2. Без архитектурных изменений (proxy daemon, multi-thread, шардирование mon) Vitastor подходит максимум для 500-1000 OSD кластеров.

  3. С мелкими fixes (закрытие 23 bug-находок) и тщательным тюнингом конфигурации можно дойти до 1500-2500 OSD, но всё ещё с низкой VM-density (10-50 VM/host без proxy).

  4. Для полноценного hyperscaler (5000+ OSD, 100+ VM/host, RDMA) необходимо:

    • Per-host vitastor-proxy daemon (новый компонент)
    • etcd grpc-proxy слой в deployment
    • Реализация SRQ в RDMA транспорте
    • Возможно — переход на consistent hashing для PG distribution
    • Реализация multi-threaded NFS proxy
    • Backplane для управления — отдельный orchestrator вместо текущего CSI плагина
  5. Альтернатива: использовать Vitastor только как подкомпонент (например, для одного rack/AZ из 200-500 OSD), а на уровне облака над ним делать собственный шардирующий слой. В этой модели каждый Vitastor-кластер обслуживает 100-500 хостов, и облако имеет 50-100 таких независимых кластеров.

Положительные особенности архитектуры

Не всё плохо. Дизайн Vitastor имеет сильные стороны, релевантные для облака:

  • PG эфемерны — не хранятся на дисках. Это позволяет быстрый reshuffle PG без disk I/O.
  • Метаданные в памяти — мгновенное чтение, нет latency overhead.
  • immediate_commit=all — для серверных SSD с конденсаторами оптимальный режим (без накладных fsync).
  • Atomic write support — Write Amplification ≈ 1 на NVMe с поддержкой атомарной записи.
  • Лёгкий монитор — JSON в etcd, не критичный компонент (как MGR в Ceph).
  • Простой код — относительно лёгко аудитировать и патчить, нет over-engineering.
  • Локальные чтения — local_reads для multi-AZ оптимизации.
  • Поддержка multiple pool schemes — replication + EC в одном кластере.

Эти преимущества делают Vitastor очень привлекательным для on-prem private cloud с десятками-сотнями серверов, и нишево привлекательным как компонент IaaS, разделённого на изолированные shard-кластеры.


TL;DR — оценка готовности

Краткий вердикт

Vitastor в текущем виде НЕ ГОТОВ к IaaS-уровню (5000+ OSD, 50000+ клиентов, RoCEv2). Архитектурные решения, принятые для среднего масштаба (десятки-сотни OSD, единичные тысячи клиентов), создают непреодолимые узкие места:

  1. Full mesh TCP/RDMA соединений между всеми OSD — O(N²) ресурсов сети.
  2. Один cluster_client_t на каждый процесс-клиент — каждая VM держит свои копии метаданных кластера + полный набор соединений ко всем OSD.
  3. Гигантские etcd-транзакции — отдельные ключи /pg/config, /stats, last_clean_pgs монолитны; любое изменение → перезапись 10-50 МБ.
  4. Один global mon процесс с однопоточной сериализованной обработкой; lp_solve через child_process на каждый recheck с параллельным запуском N процессов.
  5. RDMA реализован как RC QP per peer без SRQ/DC → 2.1 МБ pinned RAM на каждое соединение, нет масштабирования > 1000 пиров на HCA.
  6. NFS proxy / CSI / disk_tool однопоточные и блокирующие — массовый deploy и операции не параллелятся.

Безопасные пределы текущей реализации (без рефакторинга)

Параметр Безопасный потолок Жёсткий предел до коллапса
OSD в одном кластере 500-1000 ~2000 (с тюнингом etcd, ulimit)
PG на OSD 100-200 500 (потом OOM на target_history)
VM/клиентов на хост (без proxy) 10-20 (RDMA), 50-80 (TCP) 100 (FD/RAM exhaustion)
Параллельных соединений на OSD 5000-10000 50000 (нужен переписанный messenger)
PG count на пул ≤ 256 8192 (потом max_txn_ops error)
Пулов в кластере ≤ 8 16-32 (потом lp_solve OOM/timeout)
EC pg_size k+m ≤ 6 (например, 4+2) 8+3 (затем LP-задача не сходится)
Inode в одном NFS-FS 1-10M 100M (B-tree depth, latency unusable)
PV на K8s узел ~50 64 (UBLK) / 128 (NBD), затем hard fail
Размер OSD (raw) 16 ТБ 64 ТБ (потом RAM > 100 ГБ)
Объектов на OSD 5-10M 16M (потом OOM/long init)

Топ-30 критических находок для IaaS

# Подсистема Файл:строка Severity Эффект на scale
S1 RDMA msgr_rdma.cpp:662-680 CRITICAL Eager recv-буфер 2.1 МБ × N соединений = 21 ГБ pinned RAM на популярном OSD при 10K пиров
S2 RDMA весь msgr_rdma CRITICAL Нет SRQ — RAM квадратично по числу соединений
S3 RDMA msgr_rdma.cpp:405-461 CRITICAL RC QP per peer, нет DC — HCA TLB miss при >16K QP
S4 RDMA msgr_rdma.cpp:566, 656, 670 CRITICAL exit(1) на любой RDMA error → кластер коллапсирует от одного OOM на pinned RAM
S5 RDMA весь HIGH Нет PFC/ECN/SL — RoCEv2 в congestion collapse без lossless network
S6 OSD cluster osd.h:120 + osd.cpp:240 CRITICAL client_queue_depth НЕ применяется → OOM при load
S7 OSD cluster osd_cluster.cpp:777 CRITICAL force_stop(1) при изменении pg_count если есть активные PG → суицид OSD
S8 OSD cluster osd_main.cpp:12-21 CRITICAL SIGTERM = моментальный exit() без drain → тысячи timeout у клиентов при rolling restart
S9 OSD cluster osd_cluster.cpp:992 CRITICAL Full-mesh O(N²) соединений, lazy connect отсутствует
S10 Monitor mon.js:744-803 CRITICAL update_total_stats шлёт 100K+ inode-stats в одной etcd-транзакции каждую секунду
S11 Monitor mon.js:466-486 CRITICAL stop_all_pgs — compare на каждый OSD → fails при стандартном max-txn-ops=128
S12 Monitor mon.js:398-432 CRITICAL load_cluster_state грузит всё одним range → монитор не стартует при > 1.5 МБ state
S13 Monitor mon.js:518-519 CRITICAL Promise.all запускает N lp_solve процессов одновременно — 16 ГБ RAM при 16 пулах
S14 LP optimizer dsl_pgs.js:244-292 CRITICAL random_custom_combinations 8-24 мин на 5000 OSD × 16 пулов × EC 10+4
S15 LP optimizer dsl_pgs.js:294-340 CRITICAL filter_tree_by_rules splice в расширяющемся массиве — часы CPU
S16 LP optimizer pg_gen.js:121-148 CRITICAL save_new_pgs_txn — 2 op per PG, гарантированный отказ etcd при 16 пулов × 8192 PG
S17 etcd_state_client etcd_state_client.cpp:635-740 CRITICAL load_pgs тащит 7 range-запросов ~35 МБ JSON × 55K клиентов = 2.5 ТБ при reconnect storm
S18 etcd_state_client etcd_state_client.cpp:480-491 CRITICAL Reconnect без jitter — thundering herd при etcd failover
S19 etcd_state_client etcd_state_client.cpp:512-520 CRITICAL Все 55K клиентов watch'ат /pg/ — etcd watchserver bottleneck при ~50 PG events/sec
S20 cluster_client весь vitastor_c.cpp CRITICAL Каждый процесс — независимый cluster_client → невозможен hyperscaler VM-density
S21 cluster_client весь CRITICAL Нет rate-limiting/throttling → один tenant DoS-ит весь кластер
S22 cluster_client etcd_state_client.cpp:670 CRITICAL Полный load всех inode_config при старте каждого клиента — etcd query storm
S23 Peering osd_peering.cpp:700-712 CRITICAL target_history не GC при degraded → OOM + etcd падает
S24 Peering osd_peering_pg.cpp:407-449 CRITICAL Peering 10M объектов = 1.2 ГБ RAM + 75с CPU на 1 PG
S25 Peering osd_flush.cpp:420-423 CRITICAL Auto-tune cutoff bug: при перегрузке recovery НЕ throttle'ится
S26 NFS nfs_proxy.cpp:256-322 CRITICAL Single-thread event loop = 1 ядро на все NFS клиенты
S27 NFS nfs_kv_write.cpp:56-145 CRITICAL Single global cur_shared_inode — точка сериализации для small-file writes
S28 CSI controllerserver.go:115-123 CRITICAL fork+exec vitastor-cli на каждый CSI вызов + watch всех inodes в etcd
S29 CSI Dockerfile:31 CRITICAL NBD захардкожен nbds_max=128 на узел
S30 Blockstore blockstore_impl.cpp:36-40 + heap CRITICAL inmemory_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:

  • 100K × ~500 байт ≈ 50 МБ JSON в одной TXN
  • Превышает --max-request-bytes etcd (по умолчанию 1.5 МБ; даже vitastor-скрипт ставит 100 МБ)
  • При max-txn-ops=128 (default etcd!) транзакция ВСЕГДА фейлит
  • JSON-сериализация 50 МБ блокирует Node.js event loop на сотни мс → lease истекает

Что сделать: Разбить на батчи ≤100 ops. Писать только изменившиеся inode (хранить prev_inode_stats). Адаптивный mon_stats_timeout при N>1000.

1.2. CRITICAL — stop_all_pgs() создаёт compare на КАЖДЫЙ OSD

Файл: mon/mon.js:466-486

for (const osd_num of this.all_osds())
{
    const key = b64(this.config.etcd_prefix+'/osd/state/'+osd_num);
    checks.push({ key, target: 'MOD', result: 'LESS', mod_revision: ''+this.etcd_watch_revision });
}

При 5000 OSD + изменении pg_count пула → 5002 compare-операции в одной TXN. С max-txn-ops=128 (стандартный etcd) никогда не сработает — pg_count невозможно изменить, кластер не масштабируется.

Что сделать: Использовать одну compare на общий osd_revision epoch-ключ, а не проверку каждого OSD.

1.3. CRITICAL — load_cluster_state() грузит всё дерево одним range-запросом

Файл: mon/mon.js:398-432

const res = await this.etcd.etcd_call('/kv/txn', { success: [
    { requestRange: { key: b64(this.config.etcd_prefix+'/'), range_end: b64(this.config.etcd_prefix+'0') } },
] }, this.config.etcd_start_timeout, -1);

При 5000 OSD × (state+stats+inodestats+space) + 50000 PG (state+history+stats) + 100K inodes — dump 10-50 МБ.

  • Превышает --max-request-bytes (1.5 МБ default)
  • При failover monitor не стартует
  • JSON.parse 50 МБ = 2-5 секунд блокировки event loop
  • etcd_start_timeout=5s недостаточно

Что сделать: Пагинировать с limit=1000, либо bootstrap через watch start_revision=0.

1.4. CRITICAL — recheck_pgs() пересчитывает все пулы при ЛЮБОМ изменении OSD tree

Файл: mon/mon.js:506-549

const optimize_results = (await Promise.all(Object.keys(this.state.config.pools)
    .map(pool_id => generate_pool_pgs(this.state, this.config, pool_id, osd_tree, levels)))).filter(r => r);

Promise.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. Recheck только изменившихся пулов (incremental hash per pool).
  2. Лимитировать concurrency lp_solve через семафор (2-4 параллельных).
  3. Debounce при burst-добавлении OSD.

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-109

export_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

Алгоритм:

  • Строки 250-265: первый цикл по first (все top-level OSD), для каждого вызывает filter_tree_by_rules pg_size-1 раз + select_murmur3 (сам O(N))
  • Строки 267-282: ещё count = max_osd_combinations = 10000 итераций по rules.length (= pg_size), каждая снова filter_tree_by_rules + select_murmur3

select_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 concat
  • ~30-90 секунд per pool на одном ядре
  • 16 пулов через Promise.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

cur.splice(i+1, 0, ...item.children);  // strung 324, в середине итерации

Сценарий 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-328

LP-задача optimize_initial:

  • Переменных = max_osd_combinations + |first| = до 15000
  • Constraints: 1 objective + |osd_count| неравенств + |all_pgs| границ + sec → 20000 строк
  • objective string содержит все 15000 переменных = сотни КБ
  • optimize_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, может никогда не вернуть результат.

Что сделать:

  1. Таймаут на lp_solve (60-300 сек) + fallback.
  2. Лимит max_osd_combinations по формуле, чувствительной к pg_size.
  3. array.push + .join('') или streaming pipe в stdin.
  4. Запретить EC k+m > 6 при pg_count > 256 без явного override.

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:602

Deep 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

for (let i = 1; i < pg.size; i++)  // у массива нет .size!

Цикл никогда не выполняется → для 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-67

While-loop пересборки per_parent на каждой итерации. 5000 OSD × depth=4 = 123M ops + GC pressure × 16 пулов = 2 млрд ops, 5-15 минут.

2.11. HIGH — scale_pg_count shared-ref + push в generate_pool_pgs

Файл: mon/pg_utils.js:98-112, pg_gen.js:222-223

prev_pgs[i] = prev_pgs[i % prev_pgs.length];  // shared ref
// затем pg.push(0) мутирует разделяемый массив

При 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

combinator: use_rules
    ? new RuleCombinator(folded_tree, rules, pool_cfg.max_osd_combinations)  // ordered=undefined
    : new SimpleCombinator(...),  // ordered=undefined
ordered: pool_cfg.scheme != 'replicated',  // только для самого optimizer

Для 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

// FIXME: Implement client queue depth limit
int client_queue_depth = 128;

При 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)

for (auto pg_osd: all_peers) {
    if (pg_osd != this->osd_num && msgr.osd_peers.find(pg_osd) == msgr.osd_peers.end())
        msgr.connect_peer(pg_osd, st_cli.peer_states[pg_osd]);
}

5000 OSD, 100 PG per OSD, pg_size=3 → каждый OSD держит ~5000 peer connections. На одном OSD:

  • 5000 TCP sockets × 65 KiB (receive_buffer_size) = 325 МБ только in_buf
  • 5000 file descriptors (стандартный ulimit 1024 — OSD не стартует)
  • 5000 × osd_client_t (~300 байт + sent_ops + outbox) = 1.5+ МБ
  • При RDMA: 5000 × 2.1 МБ recv-буфер = 10.5 ГБ pinned

Что сделать: Lazy connection (только когда есть I/O). Минимум — поднять ulimit в systemd до 1М.

3.3. CRITICAL — apply_pg_count() self-kill при изменении pg_count

Файл: src/osd/osd_cluster.cpp:757-778

if (still_active_primary > 0 || still_active_secondary > 0) {
    printf("... This is not allowed. Exiting\n");
    force_stop(1);
    return;
}

Все 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-677

static void handle_sigint(int sig) {
    if (osd && !force_stopping) {
        force_stopping = true;
        osd->force_stop(0);  // -> exit() через etcd_call callback
        return;
    }
    exit(0);
}

shutdown() (с проверкой 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-1144

5000 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-411

500 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-118

for (auto & p: pgs) {
    for (osd_num_t pg_osd: pg.all_peers) {
        if (pg_osd == peer_osd) { repeer = true; break; }
    }
}

500 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-174

std::map<pool_pg_num_t, osd_pg_lock_t> pg_locks;
std::map<pool_pg_num_t, pg_t> pgs;

pgs.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-622

tfd->set_timer(1000*peer_connect_interval, false, [this, peer_osd](int) {
    try_connect_peer(peer_osd);
});

Network partition: 1000 OSD теряют связь к 500 OSD. Через 5 сек ВСЕ 1000 одновременно reconnect → 500000 SYN packets. После accept — каждое connection делает OSD_OP_SHOW_CONFIG handshake.

Что сделать: Exponential backoff + jitter (peer_connect_interval * (1 + random()/2)).

3.10. HIGH — При неудачном lock — fixed 100ms retry до бесконечности

Файл: src/osd/osd_peering.cpp:500-511

Split-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 walk apply_pg_config(). Wasted CPU на 5000 OSD кластере.

3.12. HIGH — start_pg_peering блокирует PG как INCOMPLETE при ANY disconnected all_peers

Файл: src/osd/osd_peering.cpp:202-221

all_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-708

1000 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-171

1M 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-261

keepalive_timer_id = tfd->set_timer(1000, true, [this](int) {
    auto cl_it = clients.begin();
    while (cl_it != clients.end()) {
        if (!cl->osd_num && !cl->in_osd_num || ...) continue;
        ...
    }
});

5000 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-448

recovery_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 пиров):

  • QP context: ~16-64 КБ + WQE-память (8 send + 16 recv × ~256 байт) ≈ 80 КБ на QP × 10000 = ~800 МБ
  • Mellanox ConnectX-6 теоретически 262K QP, но при >16-32K QP начинаются проблемы с TLB-кэшем UAR и mem registration cache HCA → 5-10x latency.
  • Для ConnectX-5 практический потолок ~10-20K QP per device; ConnectX-6 ~50K.

Что сделать:

  1. DC (Dynamic Connected) на ConnectX-5+: один DCI на исходящий поток, DCT на приёмный.
  2. Альтернативно: SRQ + XRC.
  3. Минимально: ограничить rdma_max_connections с fallback на TCP.

4.2. CRITICAL #R2 — Eager-аллокация recv-буферов: max_msg * max_recv на КАЖДОЕ соединение

Файл: msgr_rdma.cpp:662-680 (init_recv_rdma)

rc->recv_buf.buf = (uint8_t*)malloc_or_die(rc->max_msg * rc->max_recv);
rc->recv_buf.mr = ibv_reg_mr(rc->ctx->pd, rc->recv_buf.buf, rc->max_msg * rc->max_recv, IBV_ACCESS_LOCAL_WRITE);
  • Per connection: 16 × 132096 ≈ 2.1 МБ pinned RAM, registered в HCA
  • 10000 соединений (популярный OSD): ~21 ГБ pinned RAM на один OSD только под recv
  • 5000 OSD кластер с full-mesh + клиентами: ~100 ТБ pinned RAM глобально. Нереалистично.

Эта память pinned + registered — нельзя свопировать, занимает HCA TLB entries.

Что сделать:

  1. SRQ (Shared Receive Queue) — один пул recv-буферов на все QP одного device. Стандартная практика для RDMA без неё RDMA не масштабируется.
  2. Уменьшить rdma_max_recv дефолт (16 буферов × 129 КБ — пережиток "1 клиент к 1 OSD").
  3. Документировать предел.

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 limit ulimit -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

.min_rnr_timer  = 1,    // ~16us
.timeout        = 14,   // ~134ms
.retry_cnt      = 7,
.rnr_retry      = 7,

При обнаружении 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.

Что сделать:

  1. Сделать timeout/retry конфигурируемыми.
  2. При IBV_WC_RNR_RETRY_EXC_ERR — reset QP (RESET → INIT → RTR → RTS).
  3. Fallback на TCP при многократных RDMA сбоях с одним пиром.

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 при сотнях клиентов.

Что сделать:

  1. Конфиги rdma_traffic_class, rdma_service_level, rdma_tos.
  2. Передавать в ah_attr.sl и rdma_set_option(RDMA_OPTION_ID_TOS,...).
  3. Документировать требование PFC priority N + ECN на коммутаторах.

4.7. HIGH #R5 — MTU не валидируется, нет path-MTU обмена

Файл: msgr_rdma.cpp:267, 468

MTU берётся из 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-buffer

ibv_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

.is_global = (uint8_t)(dest->gid.global.subnet_prefix || dest->gid.global.interface_id ? 1 : 0)

Для 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-796

max_cqe = 4096, ресайз через ibv_resize_cq — blocking call, миллисекунды на больших значениях. Под burst-нагрузкой подключения блокирует event-loop OSD на 5-50 мс.

Что сделать:

  • Заранее sizing CQ под expected max connections.
  • Несколько CQ per context, привязанных к разным iothread.
  • Сейчас RDMA event handler сидит в main thread — не использует iothread_count > 0.

4.13. MEDIUM #R7 — ARP/ND storm при cold start

Файлы: msgr_rdma.cpp:149-179, msgr_rdmacm.cpp:384, 419, 452

10000 пиров одновременно делают 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-319

create_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-301

match_port_gid выбирает один best GID per port. Если несколько подсетей — choose_rdma_context (messenger.cpp:806) выбирает один контекст по peer_addr matching. 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:442 addr.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):

  1. SRQ — без него RAM растёт квадратично, scale невозможен.
  2. DC QP (Mellanox CX-5+) либо жёсткий лимит RDMA peers с fallback на TCP.
  3. Configurable PFC/ECN/SL/ToS — без них production RoCEv2 в congestion collapse.
  4. Recovery вместо exit(1) на RDMA errors — главная причина потенциального flap storm.
  5. Recovery state QP при RNR/RETRY_EXC вместо drop клиента.
  6. MTU handshake между пирами.
  7. Pre-registration больших MR + per-iothread CQ.

Желательно:

  1. NUMA-awareness.
  2. Inline data для small ops.
  3. Multipath на несколько HCA.
  4. Zero-copy через MR cache (registry per user buffer).
  5. Rate-limit на connect burst.

Что уже работает разумно:

  • Credit-based flow control в NFS RDMA proxy.
  • Auto-detect RoCEv2 vs RoCEv1.
  • Auto-discovery GID по osd_network_masks.
  • Application-level keepalive через PING.
  • Fallback на TCP при неудаче RDMA-CM.

Часть 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, 790

cl->in_buf = malloc_or_die(receive_buffer_size);   // 64 КБ

50000 клиентов × 64 КБ = 3.2 ГБ RAM только на recv-staging, независимо от активности. Idle VM/клиент держит соединение → занимает 64 КБ всё время.

Что сделать:

  • Lazy in_buf, освобождать после idle timeout.
  • Adaptive receive_buffer_size (start small).
  • Использовать recvmsg напрямую в OP-buffer без staging.
  • 4-16 КБ достаточно для header-only трафика.

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:17

RB-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-153

64 события — слишком маленький 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-802

if (peer_fd == -1 && errno != EAGAIN)
{
    throw std::runtime_error(std::string("accept: ") + strerror(errno));
}

OSD достигает 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

if (!cl->osd_num && !cl->in_osd_num || ...)
{
    // Do not run keepalive on regular clients
    continue;
}

И НЕТ 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:117

OSD рестартует → все 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 outbox per 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-164

Failover cascade: 100 OSD умирает → 5000 клиентов одновременно stop_client → каждый O(N) scan → 25M операций + cascading failures.

Что сделать: Маркировать как stopped (skip-flag в deque).

5.12. MEDIUM — timerfd_manager O(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 без переписывания

Bottleneck Лимит
RAM (in_buf 64KB) ~50k @ 16GB OSD; ~150k @ 32GB
io_uring SQE ~512-5000 concurrent active
FD ulimit Default 1024-4096 (с LimitNOFILE=1048576 → ~1M)
Connect-storm Practical ceiling ~500 reconnect/sec
O(N²) data structures Stable до ~10-20k clients

Реалистичный потолок: 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

В одной транзакции:

  1. /config/pools (100 pools × 500 байт = 50 КБ)
  2. /config/pgs (legacy)
  3. /pg/config (50000 PG × 200 байт = ~10 МБ)
  4. /config/inode/ (10000 × 300 байт = ~3 МБ)
  5. /pg/history/ (50000 × 200 байт = ~10 МБ)
  6. /pg/state/ (50000 × 150 байт = ~7.5 МБ)
  7. /osd/state/ (5000 × 500 байт = ~2.5 МБ)

Итого: ~35 МБ JSON в одном HTTP-ответе на КАЖДЫЙ клиент. С base64 + 30%. ~45 МБ × 55000 клиентов = 2.5 ТБ трафика на reconnect storm. etcd default --max-request-bytes=1.5 МБ — транзакция просто не пройдёт без специального тюнинга.

Что сделать:

  1. Пагинировать с limit: (cursor по mod_revision).
  2. Не грузить /pg/history/ на клиентах — FIXME уже стоит, но история всё равно запрашивается (line 678-681).
  3. Не грузить /pg/state/ целиком на клиентах.
  4. Дельта-снапшот через snapshot+watch-replay с локального кэша.

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 → fixed etcd_quick_timeout (1000мс), без jitter

SYN 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-520

Healthy кластер с 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.

Что сделать:

  1. Развернуть etcd grpc-proxy на каждом гипервизоре — 1000 VM на хост = 1 watch к etcd. Снижение нагрузки на 3 порядка.
  2. Не грузить полное состояние на клиенте.
  3. Использовать узкие префиксы.

6.4. HIGH — Watch payload включает /pg/history/ — на клиентах не нужен

Файл: etcd_state_client.cpp:521 (FIXME), 1088-1142

FIXME отмечено: // 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-152

keepalive_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-570

55000 × 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:139

json11::Json shared_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-451

json11::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_etcd round-robin прилипает к одному узлу

Файл: etcd_state_client.cpp:294-322

selected_etcd_address сбрасывается только при ошибках. 55000 клиентов прилипают к одному etcd-узлу. etcd HTTP gateway single-process, max ~10k connections. Дисбаланс нагрузки.

6.11. MEDIUM — load_pgs retry 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 на хосте:

  • 200 cluster_client_t = 200 × (250МБ pool_config + 300МБ inodes + RDMA buffers + connections)
  • При RDMA каждый QEMU инициализирует свой rdma_context с MR — очень дорого по pinned memory и HW resources (NIC лимит на QP/MR)
  • 200 × 5000 OSD = до 1M соединений на хост — превышает любые ulimit, kernel state, conntrack

Эффект:

  • FD exhaustion
  • TCP port exhaustion (ephemeral ports 30K)
  • RDMA QP exhaustion (NIC обычно 64K-256K)
  • io_uring instances exhaustion (/proc/sys/kernel/io_uring)
  • Memory ballooning (дублирование metadata)
  • VM startup time: каждая делает load_pgs() с полным range-scan

Что сделать: ОБЯЗАТЕЛЬНО ввести per-host vitastor-proxy daemon, к которому VM подключаются через unix socket/shared memory ring. Один процесс — одно соединение на OSD.

7.2. CRITICAL — Полная загрузка ВСЕХ inode_config

Файл: etcd_state_client.cpp:670-675, 1283-1293

load_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-224

osd_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_overflow unbounded

Файл: cluster_client.h:9-13, cluster_client.cpp:802-813, 784-787

Defaults: 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-298

repeat_ops_for(peer_osd) сканирует всю dirty_buffers map (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, 583

VM с 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, 444

pg->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-357

buffers (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-1093

OSD_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-228

Destructor не дожидается inflight ops. Callbacks теряются. fio_cluster.cpp:347-363 — не делает flush перед destroy → потеря данных в fio с writeback.

Ответы

Сколько VM на гипервизоре без proxy:

  • Типичная конфигурация (1000 OSD, без RDMA): 50-80 VM/host
  • IaaS (5000 OSD, RDMA): 10-20 VM/host

Нужен ли per-host vitastor-proxy: ДА, БЕЗУСЛОВНО. Без него IaaS с RDMA RoCEv2 физически нереализуем.

Какие защиты отсутствуют:

  1. Rate-limiting (IOPS/BW)
  2. op_queue cap
  3. writeback_overflow cap (OOM возможен)
  4. offline_ops cap
  5. Cancellation/timeout client-side
  6. Защита от misbehaving listing (миллионы объектов в памяти)
  7. Per-inode/per-tenant fairness
  8. Проверка reasonable parameters (1TB read)
  9. Защита от reconnect storm
  10. CAS не делает internal retry

Часть 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_MISPLACED target_history никогда не очищается (условие GC pg.state == PG_ACTIVE без флагов). Каждый pg.epoch++ (osd_primary_write.cpp:194) при degraded состоянии добавляет запись в target_history.

Сценарий:

  • Кластер с долго-живущим degraded состоянием (длинный rebalance) — каждый write на новой epoch добавляет запись
  • После 10000 writes: target_history.size() = 10000, ~400 КБ на 1 PG
  • 50000 PG → до 20 ГБ RAM. И эта история улетает в etcd при report_pg_state → etcd получит JSON value 400 КБ × 50000 = невозможно

Эффект: OOM на OSD; etcd value overflow; кластер мёртв.

Что сделать:

  • Ограничить target_history.size() (FIFO 1024)
  • Чистить history когда pg.state включает PG_ACTIVE независимо от degraded/misplaced
  • Дедуплицировать через consistent identifier

8.2. CRITICAL — calc_object_states — std::sort всего списка + 24-40 байт × N

Файл: osd_peering_pg.cpp:407-449

  • PG с 10M объектов (replication x3) → 30M записей в st.list
  • obj_ver_role ≈ 40 байт (с alignment) → 30M × 40 = 1.2 ГБ на одну PG
  • Для 100 PG в peering одновременно — 120 ГБ
  • std::sort 30M элементов ≈ 30M × 25 × 100ns = 75 секунд CPU на 1 PG
  • При EC 5+3, 1 OSD с 100 PG × 10M = 1 млрд объектов: невыполнимо

Что сделать: Стриминг + 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

if (sleep_us > recovery_tune_sleep_cutoff_us)
{
    return;  // НЕ обновляет recovery_target_sleep_us!
}

При перегрузке (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-234

PG с 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_ops O(N²) при больших flush

Файл: osd_flush.cpp:9-65, 67-157

FLUSH_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-74

50000 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-40

Restart 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-377

PG в 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-326

submit_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-89

Non-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 (~10k объектов): 30-120 секунд
  • Большие PG (~1M объектов): 5-30 минут
  • Огромные (~10M объектов): OOM до завершения

Безопасный максимум 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-977

try_evict пропускает блоки с updating > 0. При большой write-нагрузке все блоки на горячем пути updating → cache превышает cache_max_blocks. Default memory_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_cookies map растёт без ограничения

Файл: 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_cache flush каждую секунду на ВСЕ inodes

Файл: nfs_kv.cpp:412-446 (touch_inodes, каждые 1000мс)

Для RDMA-клиентов read_hack_cache (fattr3 hack). touch_inodes() делает read_hack_cache.clear() раз в секунду. После clear — все RDMA READ снова делают KV get. Периодические "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-305

RENAME каталога — 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-1018

volume_removed map → O(unique_shared_inodes); kvfs->extends → O(active extending inodes); xdr_pool/free_buffers per-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-678

Linux 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 на id key — все 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-71

ls -la в каталоге 100K файлов = 100K KV gets через 8 parallel = 12500 sequential rounds. На RDMA 10µs — 125ms. На TCP 1ms — 12 секунд.

Ответы

NFS клиентов на proxy без переписывания: 100-500 активных.

Максимум inode в KV-FS:

  • Архитектурный: 2^56
  • Практический: 10-100M (выше — операции unusable)

Готов ли 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:

  • etcd выдаёт snapshot из ~1000 inode-конфигов (100-200 КБ)
  • Watch establishment latency 50-200 мс
  • При 1000 одновременных PV → 1000 параллельных watch'ей на одну etcd → etcd либо лопнет, либо CSI calls таймаутят
  • 30-60 сек на запуск vitastor-cli → --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

(echo options nbd nbds_max=128 > /etc/modprobe.d/nbd.conf)

Жёсткий лимит = 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

func (is *IdentityServer) Probe(...) {
    return &csi.ProbeResponse{}, nil
}

Нет проверки etcd connectivity, vitastor-client, дисков. Зомби-CSI не будет рестартован.

10.5. HIGH — volumeLocks — глобальный mutex с broadcast cond, thundering herd

Файл: csi/src/nodeserver.go:42-44, 115-133

200 pod'ов на узле, kubelet делает параллельно 200 NodeStageVolume → 200 горутин. На каждом unlock — все 200 пробуждаются, конкурируют за mu.Lock.

10.6. HIGH — VolumeId = JSON, раздутый и хрупкий

Файл: csi/src/controllerserver.go:183, 187, 242, 246, 513, 610

volumeIdJson, _ := json.Marshal(ctxVars)
VolumeId: string(volumeIdJson)

JSON {"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-437

csi.ControllerServiceCapability_RPC_LIST_VOLUMES,     // объявлено, но не реализовано
csi.ControllerServiceCapability_RPC_LIST_SNAPSHOTS,

CSI 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-1037

K8s метрика 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 узел:

  • NBD: 128 (hardcoded)
  • UBLK: ~64 (kernel default)
  • VDUSE: ~16-32

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 ГБ метаданных НА ДИСКЕ
  • В RAM: block_info.resize(meta_block_count) = 4.6M × ~80 байт = ~370 МБ только block_info
    • buffer_area (inmemory_journal): + journal_len

Итого 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-1529

malloc_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_lsn std::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_lsn resize массива → миллионы записей → сотни МБ.

11.4. CRITICAL — Cold start blocks 20-60 сек без heartbeat

Файл: blockstore_init.cpp

Для 16M объектов:

  1. Read 18 ГБ metadata от NVMe (3 ГБ/с) = ~6 сек I/O
  2. Parse + CRC + malloc + insert: 16M × ~500 ns = ~8 сек CPU
  3. mark_used_blocks (16M × 300ns) = ~5 сек CPU
  4. recheck_full_gc (при 10% garbage): 5-10 сек
  5. finish_recheck (sort compact_queue): 2-5 сек

Итог: 30-60 секунд. Vitastor monitor может пометить такой OSD как failed → ребалансировка.

Что сделать: Early heartbeat в monitor; параллелизовать парсинг; lazy-load.

11.5. CRITICAL — iterate_with_stable walks ВСЮ 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-189

robin_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-128

heap_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_alloc bitmap 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_stats std::map

Файл: blockstore_heap.h:196

std::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-2250

Listing 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_queue std::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:159

V1 формат:

  • dirty_db std::map — ~96 байт/entry, неограниченный рост при больших журналах
  • used_sectors — 80 МБ на 1 ГБ journal в worst case
  • clean_bitmaps malloc(block_count × 2 × 32) = 1 ГБ для 16 ТБ + 1MB block, 8 ГБ для 128K block

V1 не рекомендовать для больших OSD.

Ответы

RAM на 16 ТБ NVMe в типичной конфигурации:

Компонент Размер
heap_list_item_t × 16M (big + csum, ~1.2 КБ/entry) ~20 ГБ
block_info (4.6M × 64 + vectors) ~500 МБ
block_index (PG → inode → stripe) ~500 МБ
inmemory_journal 10 ГБ
inode_space_stats ~70-700 МБ
Прочее + glibc fragmentation 10% ~3-5 ГБ
ИТОГО ~32-35 ГБ RAM на 1 OSD

При data_block_size=128KB: ~150 ГБ RAM — невозможно.

Максимальный размер OSD:

  • 4K block: 8 ТБ (hard limit)
  • 128K block: 256 ТБ
  • 1M block: 2 ПБ (теория); практически ~64 ТБ при 100 ГБ 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-828

24 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 events
  • systemctl enable --now vitastor-osd@N — блокирующий

~5-15 минут на сервер с 96 OSD.

12.3. CRITICAL — alloc-osd O(N²) etcd traffic при mass alloc

Файл: src/cmd/cli_alloc_osd.cpp:30-98

1000 OSD создаются параллельно:

  1. Все N процессов одновременно пытаются взять id=1 → 999 fail
  2. 999 процессов читают весь список (range request → большой ответ при 10K OSD), все определяют id=2 → 998 fail
  3. Goto resume_1

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 минут
  • Удаление partition: 96 sfdisk + 96 partprobe последовательно

Десятки минут на 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:3

After=network-online.target local-fs.target time-sync.target — все 96 ждут одно и то же → запустятся "залпом":

  • 96 OSD × LimitNOFILE=1048576 = ~100M FD; ulimit может не успеть подняться
  • etcd получает шторм
  • При recovery — etcd-storm

12.7. HIGH — hostname как failure domain

Файл: src/osd/osd_cluster.cpp:171, mon/osd_tree.js:31

OSD регистрируется с host = gethostname(). Дублирование hostname (default localhost.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-277

CAS только на config/pools, НЕ на node_placement. Между read snapshot и put pool — мог быть изменён node_placement → pool создаётся с поломанным размещением.

12.9. HIGH — rm-osd retry 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:57

96 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 на сервере:

  • Софт лимитов нет
  • Практический: 48-96 OSD при тщательной настройке inmemory_metadata=0
  • Без настройки: ~24 OSD стабильно

Rolling 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. Сетевой слой

  1. Per-host vitastor-proxy daemon (КРИТИЧНО):

    • Один cluster_client_t на хост → одно соединение к каждому OSD
    • Shared pool_config / inode_config между всеми VM на хосте
    • Shared RDMA MR (memory regions)
    • Multiplexing через unix socket / shared memory ring (типа vhost-user-blk)
    • Per-VM/per-tenant rate-limiting
    • Без этого — фундаментально невозможен hyperscaler на RDMA
  2. etcd grpc-proxy на каждом гипервизоре (КРИТИЧНО):

    • Coalesces watches: 1000 VM на хосте = 1 watch к etcd
    • Кэширует snapshot для load_pgs без хождения в etcd
    • 100 хостов × 1 proxy vs 100000 connections к etcd — снижение на 3 порядка
  3. RDMA RoCEv2 переписать:

    • SRQ (Shared Receive Queue) — обязательно
    • DC QP (Mellanox CX-5+) либо жёсткий лимит RDMA peers с fallback на TCP
    • Configurable PFC/ECN/SL/ToS
    • Recovery вместо exit(1) на RDMA errors
    • QP reset вместо drop клиента при RNR/RETRY_EXC
    • MTU handshake между пирами
    • Pre-registration больших MR

A.2. OSD слой

  1. Backpressure на client_queue_depth (CRITICAL bug):

    • Реализовать FIXME // Implement client queue depth limit
    • При превышении — отложить read из сокета или возвращать -EBUSY
  2. Graceful shutdown:

    • В handle_sigint поставить stopping=true, дождаться shutdown(), потом exit
    • Минимум 30 секунд stop_timeout в systemd unit
  3. Lazy peer connections:

    • Не full mesh — connect только когда есть I/O
    • LRU eviction для idle peer connections
  4. Заменить std::map на robin_hood:

    • pgs, pg_locks, unstable_writes, dirty_osds
    • epoll_handlers
    • inode_stats

A.3. Monitor слой

  1. Шардирование etcd-схемы:

    • Per-pool ключи /pg/config/<pool_id> вместо одного /pg/config
    • Per-pool stats вместо одного /stats
    • Per-pool history
  2. Chunked etcd transactions:

    • Все transactions ≤100 ops
    • Особенно update_total_stats, save_new_pgs_txn, report_pg_states
  3. Concurrency control для lp_solve:

    • Semaphore max 2-4 параллельных
    • Timeout 60-300 секунд + fallback
    • Worker threads для random_custom_combinations
  4. Инкрементальный recheck:

    • Per-pool tree_hash вместо общего
    • Recheck только изменившихся пулов

A.4. etcd слой

  1. Адаптивные таймауты:

    • etcd_mon_ttl от 5 до 30 сек для крупных кластеров
    • Lease renew каждые TTL/3, не TTL
    • Exponential backoff + jitter в reconnect
  2. Пагинированный snapshot:

    • load_cluster_state через range с limit
    • load_pgs через cursor по mod_revision
  3. HTTP/gRPC оптимизации:

    • READ_BUFFER 64-256 КБ
    • Connection pool вместо одного keepalive_client
    • response.append() вместо +=

A.5. NFS / CSI / Deployment

  1. NFS multi-thread:

    • Sharded proxy с L3 LB
    • Бинарное хранение inode metadata
    • Per-thread shared inodes для small writes
    • Atomic multi-key transactions
  2. CSI persistent etcd connection:

    • go.etcd.io/etcd/client/v3
    • Идемпотентный DeleteVolume
    • Реальный Probe() с health check
    • ListVolumes/NodeGetVolumeStats реализовать
    • Поднять NBD/UBLK лимиты
  3. Mass-deploy:

    • Параллельный prepare
    • Batched alloc-osd
    • flock на shared SSD при partition operations
    • Atomic purge (clear superblock ДО rm-osd)

A.6. Жёсткие лимиты конфигурации

# Safe configuration for hyperscaler scale
etcd:
  --max-txn-ops: 10000          # default 128 катастрофически мало
  --max-request-bytes: 10485760 # 10MB, default 1.5MB
  --quota-backend-bytes: 8589934592  # 8GB

monitor:
  etcd_mon_ttl: 30
  etcd_mon_timeout: 30000
  mon_change_timeout: 5000
  mon_retry_change_timeout: 1000  # exponential backoff!

per-pool:
  max_osd_combinations:
    pg_size ≤ 3: 5000
    pg_size ≤ 8: 2000
    pg_size > 8: 500
  pg_count: max(256, min(8192, total_osds * 100 / pg_size))
  pg_size (EC): k+m ≤ 6 рекомендовано, > 10 запрещено

cluster:
  max_osd_per_failure_domain: 50
  max_hierarchy_depth: 4
  max_total_osds_per_monitor: 2000 (без рефакторинга)

osd:
  recovery_queue_depth: 4-8 (default 1 слишком мало)
  scrub_queue_depth: 8-16
  client_queue_depth: ОБЯЗАТЕЛЬНО реализовать

systemd:
  LimitNOFILE: 1048576
  TimeoutStopSec: 60 (для graceful drain)

rdma:
  rdma_max_recv: 4 (вместо 16)
  rdma_traffic_class: настроить под PFC priority
  use_srq: true (когда реализуется)

B. Прогноз готовности

Compunent Status Required Work ETA
Core blockstore ✅ Стабилен для среднего масштаба Slab allocator, lazy load 2-3 месяца
OSD core ⚠️ Требует backpressure, robin_hood client_queue_depth, graceful shutdown 1-2 месяца
Messenger TCP ⚠️ Bottleneck на 5k+ connections epoll robin_hood, SO_KEEPALIVE, dynamic buffers 1 месяц
Messenger RDMA ❌ Не масштабируется SRQ, DC, recovery on error, PFC config 3-6 месяцев
Monitor ❌ Узкие места при 500+ OSD Шардирование etcd-схемы, инкрементальность 3-4 месяца
etcd_state_client ❌ Не масштабируется Proxy слой, пагинация, нативный gRPC 2-3 месяца
cluster_client ❌ Не подходит для VM-density Per-host proxy daemon 4-6 месяцев
NFS proxy ❌ Single-thread, single shared_inode Полная переработка 6+ месяцев
CSI ❌ fork+exec на каждый вызов Native Go etcd client 2-3 месяца
Deployment ❌ Sequential, не параллелится Batched API, parallel disk ops 2-3 месяца

C. Минимальный safe path к production hyperscaler

Фаза 1 (3 месяца) — устранение блокеров:

  1. SRQ для RDMA
  2. Backpressure в OSD (client_queue_depth)
  3. Graceful shutdown
  4. Шардирование etcd-схемы (/pg/config/)
  5. Chunked transactions в monitor
  6. Native Go etcd client в CSI
  7. flock на disk_tool

Фаза 2 (3 месяца) — масштабирование:

  1. Per-host vitastor-proxy daemon
  2. etcd grpc-proxy в deployment guide
  3. DC QP или жёсткий лимит RDMA peers
  4. Rate-limiting в client
  5. Batched alloc-osd
  6. Lazy peer connections

Фаза 3 (6 месяцев) — переписывание узких компонентов:

  1. NFS proxy multi-threaded
  2. Monitor с инкрементальным recheck
  3. Lazy-load inode_config в клиенте
  4. CSI с persistent connections и идемпотентностью

Без этих изменений 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.

# Аудит масштабируемости 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"): > **"Vitastor OSD однопоточные и всегда такими останутся."** Если вам нужно выделить больше 1 ядра на 1 диск — создайте несколько OSD на разделах этого диска. → **Следствие для нас:** 24 NVMe сервер = 96+ OSD per server. Это многократно усугубляет все проблемы full-mesh (Часть 3.2) и количества соединений (Часть 5). Также по архитектурной гарантии "и всегда такими останутся" — multi-threaded OSD ждать не стоит. > **"Распределение PG не основано на консистентных хешах. Вместо этого все маппинги PG хранятся прямо в etcd... Перераспределение PG по OSD выполняется через математическую оптимизацию... В таком подходе есть и потенциальный недостаток — есть предположение, что в очень большом кластере он может сломаться — однако вплоть до нескольких сотен OSD подход точно работает нормально."** → **Прямое признание автора**: Vitastor рассчитан "вплоть до нескольких сотен OSD". IaaS с 5000+ OSD **выходит за официально поддерживаемые пределы**. Все находки части 2 (LP optimizer) — это закономерное следствие подхода, рассчитанного на меньший масштаб. > **"Метаданные всегда размещаются в памяти, благодаря чему никогда не тратится лишнее время на чтение метаданных с диска. Объём метаданных линейно зависит от ёмкости диска и размера блока хранилища (block_size, по умолчанию 128 КБ). С 128 КБ блоком потребление памяти составляет примерно 512 МБ на 1 ТБ данных."** → **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` (раздел "Клиентские компоненты"): > **"Клиентская библиотека — инкапсулирует логику на стороне клиента. Соединяется с etcd и со всеми OSD, от etcd получает состояние кластера, команды чтения и записи отправляет на все OSD напрямую. В силу архитектуры все отдельные блоки данных (по умолчанию по 128 КБ) располагается на разных OSD, но клиент устроен так, что всегда точно знает, к какому OSD обращаться, и подключается к нему напрямую."** → **By-design**: full-mesh клиент → ВСЕ OSD. Наши находки Части 7 (cluster_client per VM = независимый клиент со всеми соединениями) — это **архитектурный фундамент**, не bug. Per-host proxy daemon, который мы рекомендуем — это **сменa архитектурной парадигмы**, не "фикс". Из `docs/config/network.ru.md` (раздел `rdma_max_recv`): > **"Один Vitastor-клиент с RDMA использует `rdma_max_recv * rdma_max_msg * ЧИСЛО_OSD` байт памяти, по умолчанию — примерно 2 МБ * число OSD."** → **Официально документированный расход**: 2 МБ × N_OSD на одного клиента. Для 5000 OSD = **10 ГБ pinned RAM на одного клиента**. Это полностью совпадает с нашей Частью 4.2 (находка #R2) — но это **документированная by-design характеристика**, не баг. Из `docs/intro/features.ru.md` (раздел "Планы развития"): > Планы развития включают: > - **Многопоточный клиент** > - **Более быстрое переключение при отказах** > - **Оптимизация слоя управления** > - iSCSI и NVMeoF прокси > - SSD-кэширование (tiered storage) → Автор сам признаёт, что эти направления — TODO. Наш аудит подтверждает: эти направления **критичны** для IaaS. Из `docs/intro/architecture.ru.md` (раздел "Принципы разработки"): > **"Я люблю простые решения. Поэтому Vitastor простой и быстрый и всегда будет таковым. Я не против иногда изобрести велосипед, например, собственный простенький HTTP-клиент для работы с etcd, вместо того, чтобы взять тяжёлую готовую библиотеку. Общепринятые практики написания C++ кода с RAII, наследованием, умными указателями и так далее меня также не волнуют, поэтому если вы хотели увидеть здесь красивый идиоматичный C++ код, вы, вероятно, пришли не по адресу."** → Это объясняет 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" для наших находок | Находка | Категория | Комментарий | |---|---|---| | Full-mesh O(N²) соединений (S9, 3.2) | **BY-DESIGN** | Архитектурная цитата: "клиент устроен так, что всегда точно знает, к какому OSD обращаться, и подключается к нему напрямую". Решается только per-host proxy. | | Однопоточный OSD (3.3, 5, 8) | **BY-DESIGN** | "Vitastor OSD однопоточные и всегда такими останутся". Решается через "несколько OSD на разделах" — что усугубляет деплоймент и количество соединений. | | Метаданные в RAM (11.1, 11.6) | **BY-DESIGN** | "Метаданные всегда размещаются в памяти". Решается только sharding на много мелких OSD. | | Множественные cluster_client per VM (7.1) | **BY-DESIGN** | Документированно. Решается только per-host vitastor-proxy. | | LP optimizer для PG distribution (2) | **BY-DESIGN (с признанием ограничения)** | Автор признаёт "вплоть до нескольких сотен OSD". Для тысяч — необходимо переходить на consistent hashing (CRUSH-like). | | 2 МБ × N_OSD RAM на клиента (RDMA) (4.2) | **BY-DESIGN (документировано)** | SRQ необходимо реализовать. | | NFS proxy single-thread (9.1) | **BY-DESIGN (упомянуто как TODO)** | "Многопоточный клиент" в планах развития. | | CSI fork+exec на каждый вызов (10.1) | **BUG/FIXABLE** | Технический долг, не by-design. | | `client_queue_depth` не применяется (3.1) | **BUG** | В коде FIXME комментарий: "Implement client queue depth limit". | | `force_stop(1)` при изменении pg_count (3.3) | **BUG** | Должен ждать drain. | | SIGTERM = exit() без drain (3.4) | **BUG/FIXABLE** | `shutdown()` метод существует, но не используется в SIGTERM handler. | | `update_total_stats` гигантская TXN (1.1) | **BUG/FIXABLE** | Нужна пагинация. | | `stop_all_pgs` compare на каждый OSD (1.2) | **BUG/FIXABLE** | Можно epoch-based. | | `target_history` без GC (8.1) | **BUG** | Должен GC независимо от degraded флага. | | Auto-tune cutoff bug (8.3) | **BUG** | `return` вместо `cap`. | | RDMA `exit(1)` на любую ошибку (4.4) | **BUG/FIXABLE** | Должен `stop_client()`. | | Nbds_max=128 hardcoded (10.2) | **BUG/FIXABLE** | Поднять до 1024+. | | Hostname как failure domain (12.7) | **BUG/CONFIG-FIXABLE** | Опция `node_placement` есть, но default плохой. | **Соотношение по нашим топ-30 находкам:** - **BY-DESIGN (требует архитектурного решения)**: ~7-8 находок. Их нельзя исправить мелким PR, нужно переходить на per-host proxy daemon, consistent hashing для PG, multi-thread architecture. - **BUG/FIXABLE (можно фиксить)**: ~22-23 находки. Большинство — за вечер-неделю работы каждая. ### Что это значит для вашей задачи 1. **Если идёте на IaaS с RDMA RoCEv2 на Vitastor — вы выходите за официально поддерживаемые пределы по архитектуре**, и должны быть готовы к **серьёзной собственной разработке** или к финансированию разработки Vitastor. 2. **Без архитектурных изменений (proxy daemon, multi-thread, шардирование mon)** Vitastor подходит максимум для **500-1000 OSD кластеров**. 3. **С мелкими fixes (закрытие 23 bug-находок) и тщательным тюнингом конфигурации** можно дойти до **1500-2500 OSD**, но всё ещё с **низкой VM-density** (10-50 VM/host без proxy). 4. **Для полноценного hyperscaler (5000+ OSD, 100+ VM/host, RDMA)** необходимо: - Per-host vitastor-proxy daemon (новый компонент) - etcd grpc-proxy слой в deployment - Реализация SRQ в RDMA транспорте - Возможно — переход на consistent hashing для PG distribution - Реализация multi-threaded NFS proxy - Backplane для управления — отдельный orchestrator вместо текущего CSI плагина 5. **Альтернатива**: использовать Vitastor только как **подкомпонент** (например, для одного rack/AZ из 200-500 OSD), а на уровне облака над ним делать собственный шардирующий слой. В этой модели каждый Vitastor-кластер обслуживает 100-500 хостов, и облако имеет 50-100 таких независимых кластеров. ### Положительные особенности архитектуры Не всё плохо. Дизайн Vitastor имеет сильные стороны, релевантные для облака: - **PG эфемерны** — не хранятся на дисках. Это позволяет быстрый reshuffle PG без disk I/O. - **Метаданные в памяти** — мгновенное чтение, нет latency overhead. - **immediate_commit=all** — для серверных SSD с конденсаторами оптимальный режим (без накладных fsync). - **Atomic write support** — Write Amplification ≈ 1 на NVMe с поддержкой атомарной записи. - **Лёгкий монитор** — JSON в etcd, не критичный компонент (как MGR в Ceph). - **Простой код** — относительно лёгко аудитировать и патчить, нет over-engineering. - **Локальные чтения** — `local_reads` для multi-AZ оптимизации. - **Поддержка multiple pool schemes** — replication + EC в одном кластере. Эти преимущества делают Vitastor очень привлекательным для **on-prem private cloud** с десятками-сотнями серверов, и нишево привлекательным как компонент **IaaS, разделённого на изолированные shard-кластеры**. --- ## TL;DR — оценка готовности ### Краткий вердикт **Vitastor в текущем виде НЕ ГОТОВ к IaaS-уровню (5000+ OSD, 50000+ клиентов, RoCEv2).** Архитектурные решения, принятые для среднего масштаба (десятки-сотни OSD, единичные тысячи клиентов), создают непреодолимые узкие места: 1. **Full mesh** TCP/RDMA соединений между всеми OSD — O(N²) ресурсов сети. 2. **Один cluster_client_t на каждый процесс-клиент** — каждая VM держит свои копии метаданных кластера + полный набор соединений ко всем OSD. 3. **Гигантские etcd-транзакции** — отдельные ключи `/pg/config`, `/stats`, `last_clean_pgs` монолитны; любое изменение → перезапись 10-50 МБ. 4. **Один global mon процесс** с однопоточной сериализованной обработкой; lp_solve через child_process на каждый recheck с параллельным запуском N процессов. 5. **RDMA реализован как RC QP per peer без SRQ/DC** → 2.1 МБ pinned RAM на каждое соединение, нет масштабирования > 1000 пиров на HCA. 6. **NFS proxy / CSI / disk_tool однопоточные и блокирующие** — массовый deploy и операции не параллелятся. ### Безопасные пределы текущей реализации (без рефакторинга) | Параметр | Безопасный потолок | Жёсткий предел до коллапса | |---|---|---| | **OSD в одном кластере** | 500-1000 | ~2000 (с тюнингом etcd, ulimit) | | **PG на OSD** | 100-200 | 500 (потом OOM на target_history) | | **VM/клиентов на хост (без proxy)** | 10-20 (RDMA), 50-80 (TCP) | 100 (FD/RAM exhaustion) | | **Параллельных соединений на OSD** | 5000-10000 | 50000 (нужен переписанный messenger) | | **PG count на пул** | ≤ 256 | 8192 (потом max_txn_ops error) | | **Пулов в кластере** | ≤ 8 | 16-32 (потом lp_solve OOM/timeout) | | **EC pg_size** | k+m ≤ 6 (например, 4+2) | 8+3 (затем LP-задача не сходится) | | **Inode в одном NFS-FS** | 1-10M | 100M (B-tree depth, latency unusable) | | **PV на K8s узел** | ~50 | 64 (UBLK) / 128 (NBD), затем hard fail | | **Размер OSD (raw)** | 16 ТБ | 64 ТБ (потом RAM > 100 ГБ) | | **Объектов на OSD** | 5-10M | 16M (потом OOM/long init) | ### Топ-30 критических находок для IaaS | # | Подсистема | Файл:строка | Severity | Эффект на scale | |---|------------|-------------|----------|------------------| | **S1** | RDMA | `msgr_rdma.cpp:662-680` | CRITICAL | Eager recv-буфер 2.1 МБ × N соединений = 21 ГБ pinned RAM на популярном OSD при 10K пиров | | **S2** | RDMA | весь msgr_rdma | CRITICAL | Нет SRQ — RAM квадратично по числу соединений | | **S3** | RDMA | `msgr_rdma.cpp:405-461` | CRITICAL | RC QP per peer, нет DC — HCA TLB miss при >16K QP | | **S4** | RDMA | `msgr_rdma.cpp:566, 656, 670` | CRITICAL | `exit(1)` на любой RDMA error → кластер коллапсирует от одного OOM на pinned RAM | | **S5** | RDMA | весь | HIGH | Нет PFC/ECN/SL — RoCEv2 в congestion collapse без lossless network | | **S6** | OSD cluster | `osd.h:120` + `osd.cpp:240` | CRITICAL | `client_queue_depth` НЕ применяется → OOM при load | | **S7** | OSD cluster | `osd_cluster.cpp:777` | CRITICAL | `force_stop(1)` при изменении pg_count если есть активные PG → суицид OSD | | **S8** | OSD cluster | `osd_main.cpp:12-21` | CRITICAL | SIGTERM = моментальный `exit()` без drain → тысячи timeout у клиентов при rolling restart | | **S9** | OSD cluster | `osd_cluster.cpp:992` | CRITICAL | Full-mesh O(N²) соединений, lazy connect отсутствует | | **S10** | Monitor | `mon.js:744-803` | CRITICAL | `update_total_stats` шлёт 100K+ inode-stats в одной etcd-транзакции каждую секунду | | **S11** | Monitor | `mon.js:466-486` | CRITICAL | `stop_all_pgs` — compare на каждый OSD → fails при стандартном `max-txn-ops=128` | | **S12** | Monitor | `mon.js:398-432` | CRITICAL | `load_cluster_state` грузит всё одним range → монитор не стартует при > 1.5 МБ state | | **S13** | Monitor | `mon.js:518-519` | CRITICAL | `Promise.all` запускает N lp_solve процессов одновременно — 16 ГБ RAM при 16 пулах | | **S14** | LP optimizer | `dsl_pgs.js:244-292` | CRITICAL | `random_custom_combinations` 8-24 мин на 5000 OSD × 16 пулов × EC 10+4 | | **S15** | LP optimizer | `dsl_pgs.js:294-340` | CRITICAL | `filter_tree_by_rules` splice в расширяющемся массиве — часы CPU | | **S16** | LP optimizer | `pg_gen.js:121-148` | CRITICAL | `save_new_pgs_txn` — 2 op per PG, гарантированный отказ etcd при 16 пулов × 8192 PG | | **S17** | etcd_state_client | `etcd_state_client.cpp:635-740` | CRITICAL | `load_pgs` тащит 7 range-запросов ~35 МБ JSON × 55K клиентов = 2.5 ТБ при reconnect storm | | **S18** | etcd_state_client | `etcd_state_client.cpp:480-491` | CRITICAL | Reconnect без jitter — thundering herd при etcd failover | | **S19** | etcd_state_client | `etcd_state_client.cpp:512-520` | CRITICAL | Все 55K клиентов watch'ат `/pg/` — etcd watchserver bottleneck при ~50 PG events/sec | | **S20** | cluster_client | весь vitastor_c.cpp | CRITICAL | Каждый процесс — независимый cluster_client → невозможен hyperscaler VM-density | | **S21** | cluster_client | весь | CRITICAL | Нет rate-limiting/throttling → один tenant DoS-ит весь кластер | | **S22** | cluster_client | etcd_state_client.cpp:670 | CRITICAL | Полный load всех inode_config при старте каждого клиента — etcd query storm | | **S23** | Peering | `osd_peering.cpp:700-712` | CRITICAL | `target_history` не GC при degraded → OOM + etcd падает | | **S24** | Peering | `osd_peering_pg.cpp:407-449` | CRITICAL | Peering 10M объектов = 1.2 ГБ RAM + 75с CPU на 1 PG | | **S25** | Peering | `osd_flush.cpp:420-423` | CRITICAL | Auto-tune cutoff bug: при перегрузке recovery НЕ throttle'ится | | **S26** | NFS | `nfs_proxy.cpp:256-322` | CRITICAL | Single-thread event loop = 1 ядро на все NFS клиенты | | **S27** | NFS | `nfs_kv_write.cpp:56-145` | CRITICAL | Single global `cur_shared_inode` — точка сериализации для small-file writes | | **S28** | CSI | `controllerserver.go:115-123` | CRITICAL | fork+exec `vitastor-cli` на каждый CSI вызов + watch всех inodes в etcd | | **S29** | CSI | `Dockerfile:31` | CRITICAL | NBD захардкожен `nbds_max=128` на узел | | **S30** | Blockstore | `blockstore_impl.cpp:36-40` + heap | CRITICAL | `inmemory_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: - 100K × ~500 байт ≈ **50 МБ JSON** в одной TXN - Превышает `--max-request-bytes` etcd (по умолчанию 1.5 МБ; даже vitastor-скрипт ставит 100 МБ) - При `max-txn-ops=128` (default etcd!) транзакция ВСЕГДА фейлит - JSON-сериализация 50 МБ блокирует Node.js event loop на сотни мс → lease истекает **Что сделать:** Разбить на батчи ≤100 ops. Писать только изменившиеся inode (хранить prev_inode_stats). Адаптивный `mon_stats_timeout` при N>1000. ## 1.2. CRITICAL — `stop_all_pgs()` создаёт compare на КАЖДЫЙ OSD **Файл:** `mon/mon.js:466-486` ```js for (const osd_num of this.all_osds()) { const key = b64(this.config.etcd_prefix+'/osd/state/'+osd_num); checks.push({ key, target: 'MOD', result: 'LESS', mod_revision: ''+this.etcd_watch_revision }); } ``` При 5000 OSD + изменении pg_count пула → 5002 compare-операции в одной TXN. С `max-txn-ops=128` (стандартный etcd) **никогда не сработает** — pg_count невозможно изменить, кластер не масштабируется. **Что сделать:** Использовать одну compare на общий `osd_revision` epoch-ключ, а не проверку каждого OSD. ## 1.3. CRITICAL — `load_cluster_state()` грузит всё дерево одним range-запросом **Файл:** `mon/mon.js:398-432` ```js const res = await this.etcd.etcd_call('/kv/txn', { success: [ { requestRange: { key: b64(this.config.etcd_prefix+'/'), range_end: b64(this.config.etcd_prefix+'0') } }, ] }, this.config.etcd_start_timeout, -1); ``` При 5000 OSD × (state+stats+inodestats+space) + 50000 PG (state+history+stats) + 100K inodes — dump **10-50 МБ**. - Превышает `--max-request-bytes` (1.5 МБ default) - При failover monitor не стартует - JSON.parse 50 МБ = 2-5 секунд блокировки event loop - `etcd_start_timeout=5s` недостаточно **Что сделать:** Пагинировать с `limit=1000`, либо bootstrap через `watch start_revision=0`. ## 1.4. CRITICAL — `recheck_pgs()` пересчитывает все пулы при ЛЮБОМ изменении OSD tree **Файл:** `mon/mon.js:506-549` ```js const optimize_results = (await Promise.all(Object.keys(this.state.config.pools) .map(pool_id => generate_pool_pgs(this.state, this.config, pool_id, osd_tree, levels)))).filter(r => r); ``` `Promise.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. Recheck только изменившихся пулов (incremental hash per pool). 2. Лимитировать concurrency lp_solve через семафор (2-4 параллельных). 3. Debounce при burst-добавлении OSD. ## 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-109` `export_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` Алгоритм: - Строки 250-265: первый цикл по `first` (все top-level OSD), для каждого вызывает `filter_tree_by_rules` `pg_size-1` раз + `select_murmur3` (сам O(N)) - Строки 267-282: ещё `count = max_osd_combinations = 10000` итераций по `rules.length` (= pg_size), каждая снова `filter_tree_by_rules` + `select_murmur3` `select_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 concat - ~30-90 секунд per pool на одном ядре - 16 пулов через `Promise.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` ```js cur.splice(i+1, 0, ...item.children); // strung 324, в середине итерации ``` Сценарий 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-328` LP-задача `optimize_initial`: - Переменных = `max_osd_combinations + |first|` = до 15000 - Constraints: 1 objective + |osd_count| неравенств + |all_pgs| границ + sec → 20000 строк - objective string содержит все 15000 переменных = **сотни КБ** - `optimize_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, может **никогда не вернуть** результат. **Что сделать:** 1. Таймаут на lp_solve (60-300 сек) + fallback. 2. Лимит `max_osd_combinations` по формуле, чувствительной к `pg_size`. 3. array.push + .join('') или streaming pipe в stdin. 4. **Запретить EC k+m > 6 при pg_count > 256** без явного override. ## 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:602` Deep 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` ```js for (let i = 1; i < pg.size; i++) // у массива нет .size! ``` Цикл **никогда не выполняется** → для 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-67` While-loop пересборки `per_parent` на каждой итерации. 5000 OSD × depth=4 = 123M ops + GC pressure × 16 пулов = 2 млрд ops, 5-15 минут. ## 2.11. HIGH — `scale_pg_count` shared-ref + push в `generate_pool_pgs` **Файл:** `mon/pg_utils.js:98-112`, `pg_gen.js:222-223` ```js prev_pgs[i] = prev_pgs[i % prev_pgs.length]; // shared ref // затем pg.push(0) мутирует разделяемый массив ``` При `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` ```js combinator: use_rules ? new RuleCombinator(folded_tree, rules, pool_cfg.max_osd_combinations) // ordered=undefined : new SimpleCombinator(...), // ordered=undefined ordered: pool_cfg.scheme != 'replicated', // только для самого optimizer ``` Для 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` ```cpp // FIXME: Implement client queue depth limit int client_queue_depth = 128; ``` При 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`) ```cpp for (auto pg_osd: all_peers) { if (pg_osd != this->osd_num && msgr.osd_peers.find(pg_osd) == msgr.osd_peers.end()) msgr.connect_peer(pg_osd, st_cli.peer_states[pg_osd]); } ``` 5000 OSD, 100 PG per OSD, pg_size=3 → каждый OSD держит ~5000 peer connections. На одном OSD: - **5000 TCP sockets × 65 KiB** (receive_buffer_size) = **325 МБ только in_buf** - 5000 file descriptors (стандартный ulimit 1024 — OSD не стартует) - 5000 × `osd_client_t` (~300 байт + sent_ops + outbox) = 1.5+ МБ - При RDMA: 5000 × 2.1 МБ recv-буфер = **10.5 ГБ pinned** **Что сделать:** Lazy connection (только когда есть I/O). Минимум — поднять ulimit в systemd до 1М. ## 3.3. CRITICAL — `apply_pg_count()` self-kill при изменении pg_count **Файл:** `src/osd/osd_cluster.cpp:757-778` ```cpp if (still_active_primary > 0 || still_active_secondary > 0) { printf("... This is not allowed. Exiting\n"); force_stop(1); return; } ``` Все 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-677` ```cpp static void handle_sigint(int sig) { if (osd && !force_stopping) { force_stopping = true; osd->force_stop(0); // -> exit() через etcd_call callback return; } exit(0); } ``` `shutdown()` (с проверкой 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-1144` 5000 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-411` 500 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-118` ```cpp for (auto & p: pgs) { for (osd_num_t pg_osd: pg.all_peers) { if (pg_osd == peer_osd) { repeer = true; break; } } } ``` 500 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-174` ```cpp std::map<pool_pg_num_t, osd_pg_lock_t> pg_locks; std::map<pool_pg_num_t, pg_t> pgs; ``` `pgs.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-622` ```cpp tfd->set_timer(1000*peer_connect_interval, false, [this, peer_osd](int) { try_connect_peer(peer_osd); }); ``` Network partition: 1000 OSD теряют связь к 500 OSD. Через 5 сек ВСЕ 1000 одновременно reconnect → 500000 SYN packets. После accept — каждое connection делает `OSD_OP_SHOW_CONFIG` handshake. **Что сделать:** Exponential backoff + jitter (`peer_connect_interval * (1 + random()/2)`). ## 3.10. HIGH — При неудачном lock — fixed 100ms retry до бесконечности **Файл:** `src/osd/osd_peering.cpp:500-511` Split-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 walk `apply_pg_config()`. Wasted CPU на 5000 OSD кластере. ## 3.12. HIGH — `start_pg_peering` блокирует PG как INCOMPLETE при ANY disconnected `all_peers` **Файл:** `src/osd/osd_peering.cpp:202-221` `all_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-708` 1000 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-171` 1M 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-261` ```cpp keepalive_timer_id = tfd->set_timer(1000, true, [this](int) { auto cl_it = clients.begin(); while (cl_it != clients.end()) { if (!cl->osd_num && !cl->in_osd_num || ...) continue; ... } }); ``` 5000 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-448` `recovery_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 пиров): - QP context: ~16-64 КБ + WQE-память (8 send + 16 recv × ~256 байт) ≈ **80 КБ на QP × 10000 = ~800 МБ** - Mellanox ConnectX-6 теоретически 262K QP, но при **>16-32K QP** начинаются проблемы с TLB-кэшем UAR и mem registration cache HCA → **5-10x latency**. - Для ConnectX-5 практический потолок ~10-20K QP per device; ConnectX-6 ~50K. **Что сделать:** 1. **DC (Dynamic Connected)** на ConnectX-5+: один DCI на исходящий поток, DCT на приёмный. 2. Альтернативно: SRQ + XRC. 3. Минимально: ограничить `rdma_max_connections` с fallback на TCP. ## 4.2. CRITICAL #R2 — Eager-аллокация recv-буферов: `max_msg * max_recv` на КАЖДОЕ соединение **Файл:** `msgr_rdma.cpp:662-680` (`init_recv_rdma`) ```cpp rc->recv_buf.buf = (uint8_t*)malloc_or_die(rc->max_msg * rc->max_recv); rc->recv_buf.mr = ibv_reg_mr(rc->ctx->pd, rc->recv_buf.buf, rc->max_msg * rc->max_recv, IBV_ACCESS_LOCAL_WRITE); ``` - Per connection: 16 × 132096 ≈ **2.1 МБ pinned RAM, registered в HCA** - 10000 соединений (популярный OSD): **~21 ГБ pinned RAM на один OSD только под recv** - 5000 OSD кластер с full-mesh + клиентами: **~100 ТБ pinned RAM глобально**. Нереалистично. Эта память **pinned + registered** — нельзя свопировать, занимает HCA TLB entries. **Что сделать:** 1. **SRQ (Shared Receive Queue)** — один пул recv-буферов на все QP одного device. **Стандартная практика для RDMA без неё RDMA не масштабируется.** 2. Уменьшить `rdma_max_recv` дефолт (16 буферов × 129 КБ — пережиток "1 клиент к 1 OSD"). 3. Документировать предел. ## 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 limit `ulimit -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` ```cpp .min_rnr_timer = 1, // ~16us .timeout = 14, // ~134ms .retry_cnt = 7, .rnr_retry = 7, ``` При обнаружении `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.** **Что сделать:** 1. Сделать timeout/retry конфигурируемыми. 2. При `IBV_WC_RNR_RETRY_EXC_ERR` — reset QP (RESET → INIT → RTR → RTS). 3. Fallback на TCP при многократных RDMA сбоях с одним пиром. ## 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 при сотнях клиентов. **Что сделать:** 1. Конфиги `rdma_traffic_class`, `rdma_service_level`, `rdma_tos`. 2. Передавать в `ah_attr.sl` и `rdma_set_option(RDMA_OPTION_ID_TOS,...)`. 3. Документировать требование PFC priority N + ECN на коммутаторах. ## 4.7. HIGH #R5 — MTU не валидируется, нет path-MTU обмена **Файл:** `msgr_rdma.cpp:267, 468` MTU берётся из `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-buffer `ibv_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` ```cpp .is_global = (uint8_t)(dest->gid.global.subnet_prefix || dest->gid.global.interface_id ? 1 : 0) ``` Для 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-796` `max_cqe = 4096`, ресайз через `ibv_resize_cq` — **blocking call**, миллисекунды на больших значениях. Под burst-нагрузкой подключения блокирует event-loop OSD на 5-50 мс. **Что сделать:** - Заранее sizing CQ под expected max connections. - Несколько CQ per context, привязанных к разным iothread. - Сейчас RDMA event handler сидит в main thread — не использует `iothread_count > 0`. ## 4.13. MEDIUM #R7 — ARP/ND storm при cold start **Файлы:** `msgr_rdma.cpp:149-179, msgr_rdmacm.cpp:384, 419, 452` 10000 пиров одновременно делают `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-319` `create_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-301` `match_port_gid` выбирает один best GID per port. Если несколько подсетей — `choose_rdma_context` (messenger.cpp:806) выбирает один контекст по `peer_addr` matching. 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:442` `addr.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): 1. **SRQ** — без него RAM растёт квадратично, scale невозможен. 2. **DC QP** (Mellanox CX-5+) либо жёсткий лимит RDMA peers с fallback на TCP. 3. **Configurable PFC/ECN/SL/ToS** — без них production RoCEv2 в congestion collapse. 4. **Recovery вместо `exit(1)`** на RDMA errors — главная причина потенциального flap storm. 5. **Recovery state QP** при RNR/RETRY_EXC вместо drop клиента. 6. **MTU handshake** между пирами. 7. **Pre-registration больших MR** + per-iothread CQ. ### Желательно: 8. NUMA-awareness. 9. Inline data для small ops. 10. Multipath на несколько HCA. 11. Zero-copy через MR cache (registry per user buffer). 12. Rate-limit на connect burst. ### Что уже работает разумно: - Credit-based flow control в NFS RDMA proxy. - Auto-detect RoCEv2 vs RoCEv1. - Auto-discovery GID по `osd_network_masks`. - Application-level keepalive через PING. - Fallback на TCP при неудаче RDMA-CM. --- # Часть 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, 790` ```c cl->in_buf = malloc_or_die(receive_buffer_size); // 64 КБ ``` 50000 клиентов × 64 КБ = **3.2 ГБ RAM только на recv-staging**, **независимо от активности**. Idle VM/клиент держит соединение → занимает 64 КБ всё время. **Что сделать:** - Lazy in_buf, освобождать после idle timeout. - Adaptive `receive_buffer_size` (start small). - Использовать `recvmsg` напрямую в OP-buffer без staging. - 4-16 КБ достаточно для header-only трафика. ## 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:17` RB-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-153` 64 события — слишком маленький 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-802` ```c if (peer_fd == -1 && errno != EAGAIN) { throw std::runtime_error(std::string("accept: ") + strerror(errno)); } ``` OSD достигает 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` ```c if (!cl->osd_num && !cl->in_osd_num || ...) { // Do not run keepalive on regular clients continue; } ``` И НЕТ `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:117` OSD рестартует → все 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 `outbox` per 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-164` Failover cascade: 100 OSD умирает → 5000 клиентов одновременно `stop_client` → каждый O(N) scan → **25M операций** + cascading failures. **Что сделать:** Маркировать как stopped (skip-flag в deque). ## 5.12. MEDIUM — `timerfd_manager` O(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 без переписывания | Bottleneck | Лимит | |---|---| | RAM (in_buf 64KB) | ~50k @ 16GB OSD; ~150k @ 32GB | | io_uring SQE | ~512-5000 concurrent active | | FD ulimit | Default 1024-4096 (с `LimitNOFILE=1048576` → ~1M) | | Connect-storm | Practical ceiling ~500 reconnect/sec | | O(N²) data structures | Stable до ~10-20k clients | **Реалистичный потолок: 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` В одной транзакции: 1. `/config/pools` (100 pools × 500 байт = 50 КБ) 2. `/config/pgs` (legacy) 3. `/pg/config` (50000 PG × 200 байт = **~10 МБ**) 4. `/config/inode/` (10000 × 300 байт = **~3 МБ**) 5. `/pg/history/` (50000 × 200 байт = **~10 МБ**) 6. `/pg/state/` (50000 × 150 байт = **~7.5 МБ**) 7. `/osd/state/` (5000 × 500 байт = **~2.5 МБ**) Итого: **~35 МБ JSON в одном HTTP-ответе на КАЖДЫЙ клиент**. С base64 + 30%. **~45 МБ × 55000 клиентов = 2.5 ТБ** трафика на reconnect storm. etcd default `--max-request-bytes=1.5 МБ` — **транзакция просто не пройдёт без специального тюнинга**. **Что сделать:** 1. Пагинировать с `limit:` (cursor по `mod_revision`). 2. **Не грузить `/pg/history/` на клиентах** — FIXME уже стоит, но история всё равно запрашивается (line 678-681). 3. Не грузить `/pg/state/` целиком на клиентах. 4. Дельта-снапшот через snapshot+watch-replay с локального кэша. ## 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` → fixed `etcd_quick_timeout` (1000мс), **без jitter** SYN 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-520` Healthy кластер с 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. **Что сделать:** 1. **Развернуть etcd grpc-proxy** на каждом гипервизоре — 1000 VM на хост = 1 watch к etcd. Снижение нагрузки на 3 порядка. 2. Не грузить полное состояние на клиенте. 3. Использовать узкие префиксы. ## 6.4. HIGH — Watch payload включает `/pg/history/` — на клиентах не нужен **Файл:** `etcd_state_client.cpp:521` (FIXME), 1088-1142 FIXME отмечено: `// 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-152` `keepalive_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-570` 55000 × 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:139` `json11::Json` shared_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-451` `json11::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_etcd` round-robin прилипает к одному узлу **Файл:** `etcd_state_client.cpp:294-322` `selected_etcd_address` сбрасывается только при ошибках. 55000 клиентов прилипают к одному etcd-узлу. etcd HTTP gateway single-process, max ~10k connections. **Дисбаланс нагрузки.** ## 6.11. MEDIUM — `load_pgs` retry 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 на хосте:** - 200 cluster_client_t = 200 × (250МБ pool_config + 300МБ inodes + RDMA buffers + connections) - При RDMA каждый QEMU инициализирует свой `rdma_context` с MR — **очень дорого по pinned memory и HW resources** (NIC лимит на QP/MR) - 200 × 5000 OSD = до **1M соединений на хост** — превышает любые ulimit, kernel state, conntrack **Эффект:** - FD exhaustion - TCP port exhaustion (ephemeral ports 30K) - RDMA QP exhaustion (NIC обычно 64K-256K) - io_uring instances exhaustion (`/proc/sys/kernel/io_uring`) - Memory ballooning (дублирование metadata) - VM startup time: каждая делает `load_pgs()` с полным range-scan **Что сделать:** **ОБЯЗАТЕЛЬНО ввести per-host `vitastor-proxy` daemon**, к которому VM подключаются через unix socket/shared memory ring. Один процесс — одно соединение на OSD. ## 7.2. CRITICAL — Полная загрузка ВСЕХ inode_config **Файл:** `etcd_state_client.cpp:670-675, 1283-1293` `load_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-224` `osd_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_overflow` unbounded **Файл:** `cluster_client.h:9-13`, `cluster_client.cpp:802-813, 784-787` Defaults: `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-298` `repeat_ops_for(peer_osd)` сканирует **всю** `dirty_buffers` map (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, 583` VM с 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, 444` `pg->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-357` `buffers` (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-1093` `OSD_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-228` Destructor не дожидается inflight ops. Callbacks теряются. fio_cluster.cpp:347-363 — не делает flush перед destroy → потеря данных в fio с writeback. ## Ответы **Сколько VM на гипервизоре без proxy:** - Типичная конфигурация (1000 OSD, без RDMA): **50-80 VM/host** - IaaS (5000 OSD, RDMA): **10-20 VM/host** **Нужен ли per-host vitastor-proxy:** **ДА, БЕЗУСЛОВНО.** Без него IaaS с RDMA RoCEv2 физически нереализуем. **Какие защиты отсутствуют:** 1. Rate-limiting (IOPS/BW) 2. op_queue cap 3. writeback_overflow cap (OOM возможен) 4. offline_ops cap 5. Cancellation/timeout client-side 6. Защита от misbehaving listing (миллионы объектов в памяти) 7. Per-inode/per-tenant fairness 8. Проверка reasonable parameters (1TB read) 9. Защита от reconnect storm 10. CAS не делает internal retry --- # Часть 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_MISPLACED` `target_history` **никогда не очищается** (условие GC `pg.state == PG_ACTIVE` без флагов). Каждый `pg.epoch++` (`osd_primary_write.cpp:194`) при degraded состоянии добавляет запись в `target_history`. **Сценарий:** - Кластер с долго-живущим degraded состоянием (длинный rebalance) — каждый write на новой epoch добавляет запись - После 10000 writes: `target_history.size() = 10000`, ~400 КБ на 1 PG - 50000 PG → до **20 ГБ RAM**. И эта история улетает в etcd при `report_pg_state` → etcd получит JSON value 400 КБ × 50000 = невозможно **Эффект:** OOM на OSD; etcd value overflow; кластер мёртв. **Что сделать:** - Ограничить `target_history.size()` (FIFO 1024) - Чистить history когда `pg.state` включает `PG_ACTIVE` независимо от degraded/misplaced - Дедуплицировать через consistent identifier ## 8.2. CRITICAL — `calc_object_states` — std::sort всего списка + 24-40 байт × N **Файл:** `osd_peering_pg.cpp:407-449` - PG с 10M объектов (replication x3) → 30M записей в `st.list` - `obj_ver_role` ≈ 40 байт (с alignment) → 30M × 40 = **1.2 ГБ на одну PG** - Для 100 PG в peering одновременно — **120 ГБ** - `std::sort` 30M элементов ≈ 30M × 25 × 100ns = **75 секунд CPU на 1 PG** - При EC 5+3, 1 OSD с 100 PG × 10M = 1 млрд объектов: невыполнимо **Что сделать:** Стриминг + 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` ```cpp if (sleep_us > recovery_tune_sleep_cutoff_us) { return; // НЕ обновляет recovery_target_sleep_us! } ``` При перегрузке (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-234` PG с 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_ops` O(N²) при больших flush **Файл:** `osd_flush.cpp:9-65, 67-157` `FLUSH_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-74` 50000 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-40` Restart 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-377` PG в `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-326` `submit_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-89` Non-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 (~10k объектов): **30-120 секунд** - Большие PG (~1M объектов): **5-30 минут** - Огромные (~10M объектов): **OOM** до завершения **Безопасный максимум 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-977` `try_evict` пропускает блоки с `updating > 0`. При большой write-нагрузке все блоки на горячем пути updating → cache **превышает** `cache_max_blocks`. Default `memory_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_cookies` map растёт без ограничения **Файл:** `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_cache` flush каждую секунду на ВСЕ inodes **Файл:** `nfs_kv.cpp:412-446` (`touch_inodes`, каждые 1000мс) Для RDMA-клиентов `read_hack_cache` (fattr3 hack). `touch_inodes()` делает `read_hack_cache.clear()` раз в секунду. После clear — все RDMA READ снова делают KV `get`. **Периодические "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-305` RENAME каталога — 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-1018` `volume_removed` map → O(unique_shared_inodes); `kvfs->extends` → O(active extending inodes); `xdr_pool`/`free_buffers` per-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-678` Linux 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 на `id` key — все 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-71` `ls -la` в каталоге 100K файлов = 100K KV gets через 8 parallel = 12500 sequential rounds. На RDMA 10µs — 125ms. На TCP 1ms — **12 секунд**. ## Ответы **NFS клиентов на proxy без переписывания: 100-500 активных.** **Максимум inode в KV-FS:** - Архитектурный: 2^56 - Практический: **10-100M** (выше — операции unusable) **Готов ли 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`: - etcd выдаёт snapshot из ~1000 inode-конфигов (100-200 КБ) - Watch establishment latency 50-200 мс - При 1000 одновременных PV → **1000 параллельных watch'ей** на одну etcd → etcd либо лопнет, либо CSI calls таймаутят - 30-60 сек на запуск vitastor-cli → `--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` ``` (echo options nbd nbds_max=128 > /etc/modprobe.d/nbd.conf) ``` Жёсткий лимит = **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` ```go func (is *IdentityServer) Probe(...) { return &csi.ProbeResponse{}, nil } ``` Нет проверки etcd connectivity, vitastor-client, дисков. **Зомби-CSI не будет рестартован.** ## 10.5. HIGH — `volumeLocks` — глобальный mutex с broadcast cond, thundering herd **Файл:** `csi/src/nodeserver.go:42-44, 115-133` 200 pod'ов на узле, kubelet делает параллельно 200 NodeStageVolume → 200 горутин. На каждом unlock — **все 200 пробуждаются**, конкурируют за `mu.Lock`. ## 10.6. HIGH — VolumeId = JSON, раздутый и хрупкий **Файл:** `csi/src/controllerserver.go:183, 187, 242, 246, 513, 610` ```go volumeIdJson, _ := json.Marshal(ctxVars) VolumeId: string(volumeIdJson) ``` JSON `{"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-437` ```go csi.ControllerServiceCapability_RPC_LIST_VOLUMES, // объявлено, но не реализовано csi.ControllerServiceCapability_RPC_LIST_SNAPSHOTS, ``` CSI 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-1037` K8s метрика `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 узел:** - NBD: **128** (hardcoded) - UBLK: **~64** (kernel default) - VDUSE: **~16-32** 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 ГБ` метаданных НА ДИСКЕ - В RAM: `block_info.resize(meta_block_count)` = **4.6M × ~80 байт = ~370 МБ только block_info** - + buffer_area (inmemory_journal): + journal_len **Итого 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-1529` `malloc_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_lsn` `std::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_lsn` `resize` массива → миллионы записей → **сотни МБ**. ## 11.4. CRITICAL — Cold start blocks 20-60 сек без heartbeat **Файл:** `blockstore_init.cpp` Для 16M объектов: 1. Read 18 ГБ metadata от NVMe (3 ГБ/с) = **~6 сек I/O** 2. Parse + CRC + malloc + insert: 16M × ~500 ns = **~8 сек CPU** 3. mark_used_blocks (16M × 300ns) = **~5 сек CPU** 4. recheck_full_gc (при 10% garbage): **5-10 сек** 5. finish_recheck (sort compact_queue): 2-5 сек **Итог: 30-60 секунд.** Vitastor monitor может пометить такой OSD как failed → ребалансировка. **Что сделать:** Early heartbeat в monitor; параллелизовать парсинг; lazy-load. ## 11.5. CRITICAL — `iterate_with_stable` walks ВСЮ 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-189` `robin_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-128` `heap_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_alloc` bitmap 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_stats` `std::map` **Файл:** `blockstore_heap.h:196` std::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-2250` Listing 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_queue` `std::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:159` V1 формат: - `dirty_db` `std::map` — ~96 байт/entry, неограниченный рост при больших журналах - `used_sectors` — **80 МБ на 1 ГБ journal** в worst case - `clean_bitmaps` `malloc(block_count × 2 × 32)` = **1 ГБ для 16 ТБ + 1MB block, 8 ГБ для 128K block** **V1 не рекомендовать для больших OSD.** ## Ответы **RAM на 16 ТБ NVMe в типичной конфигурации:** | Компонент | Размер | |---|---| | heap_list_item_t × 16M (big + csum, ~1.2 КБ/entry) | ~20 ГБ | | block_info (4.6M × 64 + vectors) | ~500 МБ | | block_index (PG → inode → stripe) | ~500 МБ | | inmemory_journal | 10 ГБ | | inode_space_stats | ~70-700 МБ | | Прочее + glibc fragmentation 10% | ~3-5 ГБ | | **ИТОГО** | **~32-35 ГБ RAM на 1 OSD** | При **data_block_size=128KB**: ~150 ГБ RAM — **невозможно**. **Максимальный размер OSD:** - 4K block: **8 ТБ** (hard limit) - 128K block: **256 ТБ** - 1M block: **2 ПБ** (теория); практически ~64 ТБ при 100 ГБ 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-828` 24 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 events - `systemctl enable --now vitastor-osd@N` — блокирующий **~5-15 минут на сервер с 96 OSD.** ## 12.3. CRITICAL — `alloc-osd` O(N²) etcd traffic при mass alloc **Файл:** `src/cmd/cli_alloc_osd.cpp:30-98` 1000 OSD создаются параллельно: 1. Все N процессов одновременно пытаются взять id=1 → 999 fail 2. 999 процессов читают весь список (range request → большой ответ при 10K OSD), все определяют id=2 → 998 fail 3. Goto resume_1 **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 минут** - Удаление partition: 96 sfdisk + 96 partprobe последовательно **Десятки минут на 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:3` `After=network-online.target local-fs.target time-sync.target` — все 96 ждут одно и то же → запустятся "залпом": - 96 OSD × LimitNOFILE=1048576 = ~100M FD; ulimit может не успеть подняться - etcd получает шторм - При recovery — etcd-storm ## 12.7. HIGH — hostname как failure domain **Файл:** `src/osd/osd_cluster.cpp:171`, `mon/osd_tree.js:31` OSD регистрируется с `host = gethostname()`. Дублирование hostname (default `localhost.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-277` CAS только на `config/pools`, НЕ на `node_placement`. Между read snapshot и put pool — мог быть изменён `node_placement` → pool создаётся с поломанным размещением. ## 12.9. HIGH — `rm-osd` retry 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:57` 96 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 на сервере:** - Софт лимитов нет - Практический: 48-96 OSD при тщательной настройке `inmemory_metadata=0` - Без настройки: ~24 OSD стабильно **Rolling 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. Сетевой слой 1. **Per-host vitastor-proxy daemon** (КРИТИЧНО): - Один cluster_client_t на хост → одно соединение к каждому OSD - Shared pool_config / inode_config между всеми VM на хосте - Shared RDMA MR (memory regions) - Multiplexing через unix socket / shared memory ring (типа vhost-user-blk) - Per-VM/per-tenant rate-limiting - Без этого — фундаментально невозможен hyperscaler на RDMA 2. **etcd grpc-proxy на каждом гипервизоре** (КРИТИЧНО): - Coalesces watches: 1000 VM на хосте = 1 watch к etcd - Кэширует snapshot для load_pgs без хождения в etcd - 100 хостов × 1 proxy vs 100000 connections к etcd — снижение на 3 порядка 3. **RDMA RoCEv2 переписать**: - SRQ (Shared Receive Queue) — обязательно - DC QP (Mellanox CX-5+) либо жёсткий лимит RDMA peers с fallback на TCP - Configurable PFC/ECN/SL/ToS - Recovery вместо `exit(1)` на RDMA errors - QP reset вместо drop клиента при RNR/RETRY_EXC - MTU handshake между пирами - Pre-registration больших MR ### A.2. OSD слой 4. **Backpressure на client_queue_depth** (CRITICAL bug): - Реализовать FIXME `// Implement client queue depth limit` - При превышении — отложить read из сокета или возвращать `-EBUSY` 5. **Graceful shutdown**: - В `handle_sigint` поставить `stopping=true`, дождаться `shutdown()`, потом exit - Минимум 30 секунд stop_timeout в systemd unit 6. **Lazy peer connections**: - Не full mesh — connect только когда есть I/O - LRU eviction для idle peer connections 7. **Заменить std::map на robin_hood**: - `pgs`, `pg_locks`, `unstable_writes`, `dirty_osds` - epoll_handlers - inode_stats ### A.3. Monitor слой 8. **Шардирование etcd-схемы**: - Per-pool ключи `/pg/config/<pool_id>` вместо одного `/pg/config` - Per-pool stats вместо одного `/stats` - Per-pool history 9. **Chunked etcd transactions**: - Все transactions ≤100 ops - Особенно `update_total_stats`, `save_new_pgs_txn`, `report_pg_states` 10. **Concurrency control для lp_solve**: - Semaphore max 2-4 параллельных - Timeout 60-300 секунд + fallback - Worker threads для random_custom_combinations 11. **Инкрементальный recheck**: - Per-pool tree_hash вместо общего - Recheck только изменившихся пулов ### A.4. etcd слой 12. **Адаптивные таймауты**: - `etcd_mon_ttl` от 5 до 30 сек для крупных кластеров - Lease renew каждые TTL/3, не TTL - Exponential backoff + jitter в reconnect 13. **Пагинированный snapshot**: - `load_cluster_state` через range с limit - `load_pgs` через cursor по mod_revision 14. **HTTP/gRPC оптимизации**: - READ_BUFFER 64-256 КБ - Connection pool вместо одного keepalive_client - response.append() вместо += ### A.5. NFS / CSI / Deployment 15. **NFS multi-thread**: - Sharded proxy с L3 LB - Бинарное хранение inode metadata - Per-thread shared inodes для small writes - Atomic multi-key transactions 16. **CSI persistent etcd connection**: - go.etcd.io/etcd/client/v3 - Идемпотентный DeleteVolume - Реальный Probe() с health check - ListVolumes/NodeGetVolumeStats реализовать - Поднять NBD/UBLK лимиты 17. **Mass-deploy**: - Параллельный prepare - Batched alloc-osd - flock на shared SSD при partition operations - Atomic purge (clear superblock ДО rm-osd) ### A.6. Жёсткие лимиты конфигурации ```yaml # Safe configuration for hyperscaler scale etcd: --max-txn-ops: 10000 # default 128 катастрофически мало --max-request-bytes: 10485760 # 10MB, default 1.5MB --quota-backend-bytes: 8589934592 # 8GB monitor: etcd_mon_ttl: 30 etcd_mon_timeout: 30000 mon_change_timeout: 5000 mon_retry_change_timeout: 1000 # exponential backoff! per-pool: max_osd_combinations: pg_size ≤ 3: 5000 pg_size ≤ 8: 2000 pg_size > 8: 500 pg_count: max(256, min(8192, total_osds * 100 / pg_size)) pg_size (EC): k+m ≤ 6 рекомендовано, > 10 запрещено cluster: max_osd_per_failure_domain: 50 max_hierarchy_depth: 4 max_total_osds_per_monitor: 2000 (без рефакторинга) osd: recovery_queue_depth: 4-8 (default 1 слишком мало) scrub_queue_depth: 8-16 client_queue_depth: ОБЯЗАТЕЛЬНО реализовать systemd: LimitNOFILE: 1048576 TimeoutStopSec: 60 (для graceful drain) rdma: rdma_max_recv: 4 (вместо 16) rdma_traffic_class: настроить под PFC priority use_srq: true (когда реализуется) ``` ## B. Прогноз готовности | Compunent | Status | Required Work | ETA | |---|---|---|---| | Core blockstore | ✅ Стабилен для среднего масштаба | Slab allocator, lazy load | 2-3 месяца | | OSD core | ⚠️ Требует backpressure, robin_hood | client_queue_depth, graceful shutdown | 1-2 месяца | | Messenger TCP | ⚠️ Bottleneck на 5k+ connections | epoll robin_hood, SO_KEEPALIVE, dynamic buffers | 1 месяц | | Messenger RDMA | ❌ Не масштабируется | SRQ, DC, recovery on error, PFC config | 3-6 месяцев | | Monitor | ❌ Узкие места при 500+ OSD | Шардирование etcd-схемы, инкрементальность | 3-4 месяца | | etcd_state_client | ❌ Не масштабируется | Proxy слой, пагинация, нативный gRPC | 2-3 месяца | | cluster_client | ❌ Не подходит для VM-density | Per-host proxy daemon | 4-6 месяцев | | NFS proxy | ❌ Single-thread, single shared_inode | Полная переработка | 6+ месяцев | | CSI | ❌ fork+exec на каждый вызов | Native Go etcd client | 2-3 месяца | | Deployment | ❌ Sequential, не параллелится | Batched API, parallel disk ops | 2-3 месяца | ## C. Минимальный safe path к production hyperscaler **Фаза 1 (3 месяца) — устранение блокеров:** 1. SRQ для RDMA 2. Backpressure в OSD (client_queue_depth) 3. Graceful shutdown 4. Шардирование etcd-схемы (/pg/config/<pool>) 5. Chunked transactions в monitor 6. Native Go etcd client в CSI 7. flock на disk_tool **Фаза 2 (3 месяца) — масштабирование:** 1. Per-host vitastor-proxy daemon 2. etcd grpc-proxy в deployment guide 3. DC QP или жёсткий лимит RDMA peers 4. Rate-limiting в client 5. Batched alloc-osd 6. Lazy peer connections **Фаза 3 (6 месяцев) — переписывание узких компонентов:** 1. NFS proxy multi-threaded 2. Monitor с инкрементальным recheck 3. Lazy-load inode_config в клиенте 4. CSI с persistent connections и идемпотентностью **Без этих изменений 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`.
Author

Переделаю с помощью Opus 4.8 + ultracode

Переделаю с помощью Opus 4.8 + ultracode
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vitalif/vitastor#70
No description provided.