Skip to content

[NBS] Support linked disk copy across shards within a zone - #7494

Draft
agalibin wants to merge 12 commits into
mainfrom
users/agalibin/nbs-cross-shard-disk-copy
Draft

agalibin wants to merge 12 commits into
mainfrom
users/agalibin/nbs-cross-shard-disk-copy

Conversation

@agalibin

@agalibin agalibin commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Extend the existing NBS linked-volume copy mechanism to copy disks between storage shards within one availability zone and one YDB cluster. Reuse background copying, mirrored writes, automatic leadership transfer and deletion of the old source volume.

Current state and scope

The existing workflow does not consistently carry shard addresses through schema resolution, request routing and copy operations. This change makes those addresses explicit without introducing a separate copy engine.

  • Support replicated network SSD/HDD disks, including an optional media kind change.
  • Preserve source creation parameters through the existing destination creation workflow.
  • Support both mounted and detached disks.
  • Exclude Disk Manager integration, cross-zone transfer and cluster-wide disk discovery.

Solution overview

  • Add Headers.ShardId and explicit source/destination shard fields to link requests.
  • Resolve shard identifiers through the configured ShardDirectories allowlist.
  • Resolve schema operations and cache volume connections independently for each shard.
  • Route copied data, mirrored writes and cleanup requests to the correct physical volume.
  • Keep detached source partitions running while an active copy needs them.
  • Reject cancellation once leadership transfer starts, including requests received after source deletion.
  • Extend the administrative client with source/destination shard options and document the internal workflow.

Compatibility and rollout

Requests without shard identifiers continue to use the receiving node's local shard. Cross-shard links require explicit identifiers for both endpoints; unknown identifiers are rejected.

Participating nodes must have the same shard mapping. The caller creates the destination with the required parameters and routes subsequent requests to the destination shard after transfer. This change does not update Disk Manager or Compute placement metadata.

File summaries
File or group Summary
cloud/blockstore/public/api/protos/headers.proto, volume.proto Add explicit shard addressing to requests.
cloud/blockstore/config/storage.proto, libs/storage/core/config.{h,cpp} Add and validate the shard-to-directory mapping.
cloud/blockstore/libs/storage/volume_proxy/volume_proxy.cpp Separate routing and cached connections by shard.
cloud/blockstore/libs/storage/volume/actors/volume_as_partition_actor.{h,cpp} Route copy and mirrored I/O to the destination shard.
cloud/blockstore/libs/storage/volume/volume_actor_{leader,follower}.cpp Enforce the cancellation boundary on the owning tablets.
doc/blockstore/cross-shard-copy.md Document configuration, copy control, cleanup and routing after transfer.
Other files Propagate shard addresses through schema and service operations, retain detached source partitions, extend CLI options and add regression coverage.

Verification

Successfully built nbsd and blockstore-client. All 1,142 tests across nine suites passed.

Coverage includes SSD/HDD copy combinations, concurrent writes and zeroes, detached disks, source/destination tablet restarts, reverse copying, cancellation and old-source cleanup. Existing linked-copy regression suites also pass.

Live-cluster validation has not been performed.

Add backward-compatible shard identifiers to link requests and internal request headers. Resolve allowlisted shard directories with validation and cover configuration and request compatibility with unit tests.
Route create, describe and destroy operations through allowlisted shard directories, including scheme-cache and backup reads. Test isolated SSD/HDD lifecycles across two YDB subdomains and reject unknown shards.
Resolve explicit shard headers and isolate cached volume connections by canonical shard directory and disk ID. Cover same-name SSD/HDD volumes, local aliases, keep-alives and unknown-shard rejection.
Propagate source and destination shard addresses through link control, mirrored writes and source cleanup. Add shard-aware NBS volume operations and CLI options, with SSD/HDD control-path and compatibility tests.
Keep source partitions running while a follower needs data and release copy-only ownership on cancellation. Cover detached and online SSD/HDD copy matrices, concurrent writes and zeroes, source cleanup, session recovery and local shard aliases.
Enforce cancellation checks on source and destination tablets. Add restart, reverse-copy and detached-cancellation coverage, shard-aware legacy fixtures and internal API documentation. Split long service scenarios into independent test chunks.

Validation: nbsd and blockstore-client built; 1142 tests passed across nine suites.
@agalibin agalibin added large-tests Launch large tests for PR blockstore Add this label to run only cloud/blockstore build and tests on PR labels Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Note

This is an automated comment that will be appended during run.

Note

All workloads for linux-x86_64-relwithdebinfo have completed.

Tip

Planned checks for linux-x86_64-relwithdebinfo.

  • 🚫 blockstore (cancelled or timed out before reporting completion)

@agalibin agalibin left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review Summary

Review mode: deep

Verdict: ❌ 3 PR-related critical issue(s) found

Critical issues

  • 🔴 Critical | High: Удаление remote DR-диска освобождает устройства локального диска — cloud/blockstore/libs/storage/service/service_actor_destroy.cpp:194
  • 🔴 Critical | High: Проверка отмены не атомарна с удалением связи — cloud/blockstore/libs/storage/volume/volume_actor_leader.cpp:215
  • 🔴 Critical | High: Часть запросов с ShardId выполняется на локальном диске — cloud/blockstore/libs/storage/service/service_actor_alter.cpp:238

Direct issues

  • 🟡 Major | High: После отмены copy-only source остаётся READY с остановленными partitions — cloud/blockstore/libs/storage/volume/volume_actor_startstop.cpp:496
  • 🟡 Major | High: Эквивалентные shard aliases не находят существующую связь — cloud/blockstore/libs/storage/service/service_actor_create_volume_link.cpp:69

Unrelated issues

  • 🔴 Critical | High: Запоздалый progress update восстанавливает отменённую связь — cloud/blockstore/libs/storage/volume/volume_actor_leader.cpp:273
  • 🟡 Major | High: Перезапуск destination не возобновляет завершение cleanup — cloud/blockstore/libs/storage/volume/volume_actor_loadstate.cpp:190

Positive observations

  • VolumeProxy последовательно использует канонический directory в ключах соединений, изолируя одноимённые диски разных shards.
  • Добавлено существенное покрытие SSD/HDD-комбинаций, конкурентных записей и zeroes, перезапусков tablets и обратного переноса.

Issue details

Unrelated issues

🔴 Запоздалый progress update восстанавливает отменённую связь

Severity: Critical | Confidence: High

Механизм уже существует в base, volume_actor_leader.cpp:273–303.

Если старый migration actor отправил progress/DataReady update, а отмена от другого sender успела удалить связь первой, обработчик принимает поздний update: monotonicity проверяется только для существующего follower, а для отсутствующего достаточно непустого UUID. Транзакция снова записывает отменённую связь.

PoisonPill не отзывает отправленные сообщения. Кроме того, migration actor в zombie-state всё ещё передаёт state responses владельцу, который может продолжить leadership propagation. Между успешной отменой и поздним DataReady source уже мог принять новые записи без mirroring.

Риск: Возобновление отменённого переноса, рассогласование данных и потеря записей при последующем transfer/cleanup.


🟡 Перезапуск destination не возобновляет завершение cleanup

Severity: Major | Confidence: High

В base, volume_actor_loadstate.cpp:190–196 отсутствует восстановление cleanup для destination с сохранённым состоянием Leader, если нет pending requests, mounts и необходимости запуска partitions для GC. Такой target допустим и в прежнем same-shard workflow.

Удаление планируется через DestroyOutdatedLeaderIfNeeded, но после reload этот метод не вызывается независимо от запуска partitions. При tablet reboot scheduled delete может сохраниться: его recipient — storage service. Однако response адресован прежнему target actor, поэтому новый actor не фиксирует переход Leader → Principal. При process/node restart до deadline теряется и scheduled event.

Риск: Статус остаётся LINK_STATUS_LEADERSHIP_TRANSFERRED; исходный диск может остаться неудалённым, а завершение операции требует внешнего I/O или mount.


auto request = std::make_unique<TEvService::TEvStatVolumeRequest>();
request->Record.SetDiskId(DiskId);
request->Record.MutableHeaders()->SetShardId(ShardId);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Удаление remote DR-диска освобождает устройства локального диска

Severity: Critical | Confidence: High

В новом маршруте DestroyVolume StatVolume, GracefulShutdown и schema deletion учитывают ShardId, а MarkDiskForCleanup и DeallocateDisk продолжают обращаться к локальному DiskRegistryProxy.

Если shards A и B используют разные DiskRegistry и содержат одноимённые DR-диски, удаление диска B через endpoint A помечает allocation диска A, удаляет schema диска B и освобождает устройства A. Реальная деаллокация удаляет allocation и передаёт устройства на очистку. При отсутствии локального совпадения allocation B остаётся утекшим.

Ограничение remote CreateVolume этого не предотвращает: существующие DR-диски можно создать через собственные endpoints. CreateVolumeLink также не проверяет media kinds обоих endpoints, поэтому ошибочный маршрут достижим через автоматический cleanup после копирования.

Риск: Потеря данных другого, локального диска.

Предлагаемое решение: До любых обращений к DiskRegistry отклонять нелокальное удаление DR-дисков; отдельно исключить локальную деаллокацию при remote Sync/not-found. Для заявленного SSD/HDD scope проверять media kinds обоих endpoints до сохранения cross-shard link.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Нелокальное удаление DiskRegistry-дисков теперь отклоняется до обращений к registry и изменения схемы. Для remote Sync/not-found исключена локальная деаллокация. При создании межшардовой связи оба endpoint проверяются на replicated SSD/HDD до сохранения связи.

.FollowerShardId = msg->Record.GetFollowerShardId()};

auto follower = State->FindFollower(link);
if (msg->Record.GetRequireCancellable() && follower &&

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Проверка отмены не атомарна с удалением связи

Severity: Critical | Confidence: High

Новые проверки RequireCancellable выполняются в обработчике до ExecuteTx. Аналогично устроен destination-side guard. Сами ExecuteRemoveFollower и ExecuteRemoveLeader безусловно удаляют запись.

Это существенное окно: YDB executor действительно может отложить Execute. Уже поставленный в очередь переход Preparing → DataReady может исполниться после проверки отмены, но перед удалением.

Дополнительно source запускает destruction propagation ещё до commit удаления. Этот запрос не несёт RequireCancellable и может удалить уже принявшую лидерство связь на destination. Возможна последовательность, где destination успел запланировать удаление source, а отмена затем вернула source в Principal и разрешила новые немирроренные записи.

Риск: Нарушение границы передачи лидерства и удаление source вместе с записями, принятыми после успешной отмены.

Предлагаемое решение: Проверять актуальное состояние непосредственно в удаляющей транзакции, передавать результат отказа в Complete и запускать propagation только после успешно принятого удаления. Destination-side удаление должно сохранять ту же границу отмены.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Проверку границы отмены перенёс внутрь ExecuteRemoveFollower и ExecuteRemoveLeader: она использует актуальное состояние в удаляющей транзакции. Отказ передаётся через Complete без удаления и дальнейших действий. Распространение отмены запускается только после успешной фиксации удаления и также несёт RequireCancellable.

{
StartPartitionsForGc(ctx);
} else {
PartitionsStartedReason = EPartitionsStartedReason::NOT_STARTED;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 После отмены copy-only source остаётся READY с остановленными partitions

Severity: Major | Confidence: High

Сценарий: replicated source после reboot работает в STARTED_FOR_COPY, partitions готовы, StartPartitionsNeeded=false; затем link отменяют в состоянии Preparing.

В новой ветке RestartPartition сначала вызывается StopPartitions, затем reason становится NOT_STARTED. При этом partition state и ссылки на actors не сбрасываются. Поздний STOPPED игнорируется, поскольку Bootstrapper уже очищен.

В результате Ready() продолжает возвращать true. WaitReady и I/O не запускают partitions заново, а forwarding использует прежние actor IDs. Remote mount может успешно добавить клиента без перезапуска tablet, поэтому сам remount проблему не исправляет.

Риск: После успешной отмены здоровый исходный диск перестаёт обслуживать I/O до внешнего перезапуска tablet.

Предлагаемое решение: Сбрасывать состояние остановленных partitions, как это уже делает GC-путь:

} else {
    State->Reset();
    PartitionsStartedReason = EPartitionsStartedReason::NOT_STARTED;
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Добавил State->Reset() при переходе copy-only тома в NOT_STARTED после остановки partitions. Состояние READY и старые ссылки больше не сохраняются; последующий WaitReady/I/O запускает partitions заново.

RequestInfo->CallContext);
request->Record.SetDiskId(LeaderDiskId);
request->Record.SetFollowerDiskId(FollowerDiskId);
request->Record.SetLeaderShardId(LeaderShardId);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Эквивалентные shard aliases не находят существующую связь

Severity: Major | Confidence: High

Конфигурация и VolumeProxy считают пустой local selector и aliases одного directory эквивалентными адресами. Но создание link передаёт raw shard strings, а поиск связи без UUID сравнивает эти строки буквально.

Например, local link создан без shard IDs, а отмена отправлена с разрешённым local для обеих сторон. Запрос приходит в правильные tablets, однако обе стороны не находят сохранённый tuple и отвечают S_ALREADY. Связь остаётся активной. GetLinkStatus с эквивалентным selector аналогично возвращает NOT_FOUND.

Риск: Клиент получает успешную отмену, хотя копирование продолжает работать и может перенести лидерство и удалить source.

Предлагаемое решение: Согласовать нормализацию адресов и идентичность link. При сопоставлении учитывать эквивалентность aliases и ранее сохранённых пустых local IDs либо использовать устойчивый UUID операции.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сопоставление связей теперь использует эквивалентность канонических directories из конфигурации, а не буквальное равенство shard aliases. Это применяется и к сохранённым пустым local selectors, и к ожидающим операциям создания. При наличии UUID сохраняется строгая идентичность поколения операции.

// Destination storage shard for internal cross-shard requests.
// Must be present in ShardDirectories. Empty selects the local shard.
// CellId identifies the caller and must not be used for routing.
string ShardId = 17;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[OUTER CONTEXT]

🔴 Часть запросов с ShardId выполняется на локальном диске

Severity: Critical | Confidence: High

Уже в целевой ветви (main, base de06bca2): Resize/Alter описывают диск без shard selector, после чего изменяют найденный локальный path. AssignVolume также создаёт ModifyVolumeRequest без shard. Сервисные sessions кешируются только по DiskId; mount lookup не передаёт shard, а прямое forwarding к известному клиенту обходит VolumeProxy.

Добавлено или изменено в PR: Headers.ShardId объявлен destination selector, а документация разрешает использовать его для последующих запросов после переноса. Однако перечисленные пути не обновлены.

При одинаковом DiskId в двух shards запрос ResizeVolume, AlterVolume или AssignVolume с target header изменяет локальный диск. Для mounted requests выбор cached actor также игнорирует selector. После удаления source remount через исходный endpoint с target header продолжает искать локальный source и не восстанавливает session.

Риск: Изменение другого диска и невозможность заявленного восстановления сессии через ShardId.

Предлагаемое решение: Передавать selector через schema operations, включить канонический shard в идентичность service/session и проверять его перед прямым forwarding. Пока конкретный путь не поддержан, запрос с нелокальным selector должен явно отклоняться.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ограничил нелокальный ShardId в StorageService явно поддержанными CreateVolume, DescribeVolume, DestroyVolume и StatVolume. Остальные запросы отклоняются до обработчика и поиска локальной session; неизвестные selectors также отклоняются. Remote StatVolume направляется через VolumeProxy без использования кешированной локальной session. В инструкции уточнил, что mount и session-bound I/O после переноса идут через endpoint целевого шарда.

Fence cancellation and stale progress updates in volume transactions. Preserve alias-equivalent link identity and restore copy-only readiness and idle target cleanup. Reject unsupported remote service and DiskRegistry operations before side effects.

Add 11 regression scenarios and clarify supported shard routing. Validation: nbsd and blockstore-client built; 1153 tests passed across nine suites.
@agalibin

agalibin commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

По двум замечаниям из сводки ревью:

  • Поздний progress update: обновляющая транзакция больше не создаёт отсутствующую связь, если UUID не принадлежит активной операции создания. Отмена удаляет ожидающую операцию; поздний update не может восстановить её или изменить новую связь с другим UUID. Migration actor не запускает передачу лидерства по отклонённому state response.
  • Cleanup после перезапуска destination: восстановление удаления старого leader вызывается при загрузке сохранённого состояния тома, независимо от mount, I/O и запуска partitions для GC.

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Note

This is an automated comment that will be appended during run.

Note

All workloads for linux-x86_64-relwithdebinfo have completed.

Tip

Planned checks for linux-x86_64-relwithdebinfo.

  • 🚫 blockstore (cancelled or timed out before reporting completion)

Bind cleanup to the expected source incarnation and atomic schema preconditions. Keep one actor-owned cleanup obligation, persist cancelled UUID fences, preserve pending creation identity and diagnostic unlink flags. Revalidate recovered link admission and reject foreign DiskRegistry descriptions.

Add 10 regression scenarios and a deterministic executor-queue cancellation test. Build nbsd and blockstore-client; all 1163 tests pass across nine suites. Leave unrelated legacy issues unchanged.
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Note

This is an automated comment that will be appended during run.

Note

All workloads for linux-x86_64-relwithdebinfo have completed.

Tip

Planned checks for linux-x86_64-relwithdebinfo.

  • 🚫 blockstore (cancelled or timed out before reporting completion)

Persist cancellation delivery until the destination acknowledgment is committed.
Restore pending cancellations after restart without restarting partitions on retries.
Release stale cleanup reservations across cancellation and completion races.
Fence destination CREATE by tablet incarnation and recover legacy Created links.
Reject conditional DR deletion before mutations while preserving local DR copy cleanup.

Add nine regression tests and update the cross-shard copy documentation.
Validation: 1172 tests passed in nine suites; nbsd and blockstore-client built.
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Note

This is an automated comment that will be appended during run.

Note

All workloads for linux-x86_64-relwithdebinfo have completed.

Tip

Planned checks for linux-x86_64-relwithdebinfo.

🟢 linux-x86_64-relwithdebinfo target: cloud/blockstore/ (test time: 12327s): all tests PASSED for commit a27e119.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
8233 8206 0 0 0 27 0

Skip legacy DiskRegistry deallocation when conditional Sync deletion receives a not-found Stat result.
Release copy-only partitions after a committed recovery error.
Promote partitions to normal use instead of stopping active remote clients.
Add seven regression tests and document the corrected lifecycle boundaries.

Validation: all 1179 tests passed in nine suites; nbsd and blockstore-client built.
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Note

This is an automated comment that will be appended during run.

Note

All workloads for linux-x86_64-relwithdebinfo have completed.

Tip

Planned checks for linux-x86_64-relwithdebinfo.

🟢 linux-x86_64-relwithdebinfo target: cloud/blockstore/ (test time: 11921s): all tests PASSED for commit 0a071b9.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
8240 8213 0 0 0 27 0

Replace per-UUID cancellation history with a durable destination generation
watermark while preserving destination reuse.

Persist generations before CREATE, fence delayed requests and durable retries,
compact legacy fences safely, and keep the watermark across reboot and resize.
Preserve active legacy links during compaction and reject cancelled legacy
recovery without assigning a fresh generation.

Add ten regression tests, adapt private-status request mocks, and make the
conditional cleanup retry test deterministic without weakening its checks.

Validation: 1189 tests passed in nine suites; nbsd and blockstore-client built.
Overlay and base-disk behavior is unchanged.
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Note

This is an automated comment that will be appended during run.

Note

All workloads for linux-x86_64-relwithdebinfo have completed.

Tip

Planned checks for linux-x86_64-relwithdebinfo.

  • 🚫 blockstore (cancelled or timed out before reporting completion)

@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Note

This is an automated comment that will be appended during run.

Note

All workloads for linux-x86_64-relwithdebinfo have completed.

Tip

Planned checks for linux-x86_64-relwithdebinfo.

🔴 linux-x86_64-relwithdebinfo target: cloud/blockstore/ (test time: 11784s): some tests FAILED for commit 3ca0399.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
8240 8208 0 2 0 30 0

🟢 linux-x86_64-relwithdebinfo target: cloud/blockstore/ (test time: 112s): all tests PASSED for commit 3ca0399.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
8 8 0 0 0 0 0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blockstore Add this label to run only cloud/blockstore build and tests on PR large-tests Launch large tests for PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant