Mooncake — это KVCacheкэш ключей и значений: промежуточные тензоры внимания, которые модель переиспользует между запросами. Обслуживание LLM упирается в то, что этот кэш огромен: 40 ГБ данных эквивалентны размеру KVCache, сгенерированного 128k токенов в модели LLaMA3-70B. Передавать и хранить такие объёмы между узлами — отдельная инженерная задача, и Mooncake решает её, разделив роли между мастер-узлом, клиентом, движком передачи и локальным кэшем. Разберём, кто за что отвечает и как именно проходит запрос.
Дезагрегация prefill/decode и KVCache-центричность
Mooncake построен как KVCache-центричная дезагрегированная архитектура: кластеры prefillфаза обработки промпта, когда модель считает представления всех входных токенов и decodeфаза генерации ответа токен за токеном разделены, а простаивающие ресурсы CPU, DRAM и SSD GPU-кластера используются для распределённого пула KV-кэша.
При дезагрегированном обслуживании Mooncake позволяет vLLM разделять prefill и decode нагрузки по разным узлам, а блоки KV-кэша передаются от prefill-воркеров к decode-воркерам с помощью высокопроизводительного движка передачи. Это даёт возможность масштабировать ресурсы prefill и decode независимо при низких накладных расходах на кросс-узловую передачу KV.
Mooncake Store расширяет vLLM от изолированных по-инстансовых KV-кэшей до общего кластерного пула: несколько инстансов vLLM могут сохранять, извлекать и повторно использовать блоки KV-кэша на основе хэш-префиксного кэширования, снижая избыточные prefill-вычисления.
Передача данных идёт через Transfer Engine, который обеспечивает zero-copy и multi-NIC передачу по VRAM/DRAM/NVMe SSD. Мастер-узел централизованно управляет отображениями объектов на буферы VRAM/DRAM/NVM и управляет узлами пула буферов. Transfer Engine достигает до 87 ГБ/с и 190 ГБ/с в сетях 4×200 Гбит/с и 8×400 Гбит/с RoCE соответственно.
Роли компонентов при обработке Get/Put
Мастер-узел централизованно хранит отображения объектов в буферы VRAM/DRAM/NVM и управляет узлами пула буферов, вызывая API Transfer Engine. По запросу клиента он возвращает список реплик: клиент запрашивает список реплик для объекта, а мастер находит метаданные и формирует ответ, в котором перечислены реплики, указан срок аренды и контрольная сумма объекта; на основе этого ответа клиент выбирает реплику для чтения и получает read-leaseаренда на чтение: гарантия, что реплика не будет удалена, пока клиент её читает. Если объекта нет — возвращается OBJECT_NOT_FOUND, если готовых реплик нет — REPLICA_IS_NOT_READY.
Клиент (Store) инициирует операции. Он инициализирует Transfer Engine и локальный hot cache, выполняет Query/BatchQuery, выбирает первую полную реплику и требует, чтобы она была memory-репликой, а затем группирует операции по transport_endpoint_.
Transfer Engine отвечает за фактическую передачу данных. При отправке батча он по стратегии выбирает либо локальный memcpy, либо передачу через движок; для NOF-реплик используется отдельный путь через SPDK, для остальных — чтение из файла. Batch-передача поддерживает только memory-реплики.
Локальный hot cache — кэш на стороне клиента. При инициализации он создаётся с заданными total_size_bytes и block_size_bytes, его память регистрируется в Transfer Engine, а для обслуживания создаётся обработчик с двумя рабочими потоками. При Get, если реплика memory и hot cache активен, клиент перенаправляет чтение в кэш, а после завершения передачи (или при ошибке) освобождает ключ.
При обработке Get клиент после успешной передачи проверяет контрольную сумму. При Put отправка использует локальный memcpy, если операция — запись и все реплики допускают локальное копирование.
Метаданные и маршрутизация объектов
Шард метаданных и отвязка от группы
Индекс шардагоризонтальная секция хранилища метаданных, за которую отвечает отдельный поток или узел метаданных объекта вычисляется по хэшу пары (tenant, key), а не по группе. Функция GetGroupIdForKey, наоборот, шард не вычисляет: она лишь извлекает group_id из config.group_ids по позиции key_index, проверяя, что размер списка совпадает с key_count и что индекс в пределах. При отсутствии group_ids возвращается пустая строка, при несоответствии размеров — ошибка INVALID_PARAMS.
Маршрутизация отвязана от группы, потому что group_id — это только аннотация: объект всегда маршрутизируется по хэшу (tenant, key), и group_id на маршрутизацию не влияет. Это подтверждается и при восстановлении: объекты, восстановленные на шардах по hash(group_id), перемещаются на шард, вычисленный как getShardIndex(tenant_id, user_key), поскольку остальная часть версии ищет каждый объект по hash(tenant, key). Групповая принадлежность при этом неизменяема, пока объект существует: попытка изменить её через config.group_ids приводит к ошибке group_membership_is_immutable.
Реплики и проверки читаемости
Реплики объекта перебираются, для каждой проверяется статус и валидность дескриптора. Реплика отбрасывается, если она не завершена, имеет невалидный mem-handle или nof-handle, если дескриптор не удалось выдать, либо если её endpoint (memory, nof или local_disk) содержится в списке недействительных endpoint'ов. IsReplicaReadable просто вызывает эту проверку, а HasReadableReplica проверяет, есть ли в метаданных хотя бы одна читаемая реплика. Сбор дескрипторов всех читаемых реплик пропускает нечитаемые.
На клиенте выбор первой полной реплики идёт по списку: берётся первая реплика со статусом COMPLETE, иначе возвращается INVALID_REPLICA.
Soft pin и eviction
Механизм soft pinмягкая защита объекта от вытеснения на ограниченное время управляется через запрос с действием. При PRESERVE или DISABLE параметр TTL должен отсутствовать, иначе возвращается INVALID_PARAMS, а сам запрос разрешается с нулевым TTL. При ENABLE TTL берётся из параметра или значения по умолчанию, и если он превышает максимум — возвращается INVALID_PARAMS.
Проверка активности soft pin вычисляет оценку, применяет её и возвращает признак активности. Применение оценки при наличии дедлайна делает вставку в индекс дедлайнов, при снятии — удаление по совпадению, и в обоих случаях корректирует метрику. Очистка истёкших забирает из индекса записи с истёкшим дедлайном, сортирует их по шарду, для каждой находит метаданные и проверяет совпадение дедлайна, а при успехе уменьшает счётчик.
На eviction soft pin влияет так: в цикле эвикции объект пропускается, если он hard pinned, дедлайн эвикции ещё не наступил или реплики нельзя эвиктить. Иначе он попадает в список на удаление, если soft pin не активен или его дедлайн эвикции не позже целевого времени. То есть активный soft pin защищает объект, но только пока его дедлайн не истёк и не наступил soft_target_timeout.
Жизненный цикл записи: Put / Upsert / Batch
PutStart → PutEnd → PutRevoke на мастере
PutStart сначала проверяет параметры. Нулевые replica_num, nof_replica_num и dfs_replica_num, пустой ключ или нулевая длина слайса дают INVALID_PARAMS. Отдельно отклоняются: dfs_replica_num > 1 или dfs_replica_num > 0 при replica_num == 0; dfs при не-default tenant; dfs без инициализированного аллокатора (как DFS_SERVICE_UNAVAILABLE); prefer_alloc_in_same_node вместе с nof_replica_num > 0; nof_replica_num > 0 без USE_NOF. Затем запрос разрешает soft-pin, обновляет host_id клиента и берёт group_id.
Далее AllocateAndInsertMetadata проверяет отсутствие ключа в метаданных (иначе OBJECT_ALREADY_EXISTS), начисляет pending-квоту, вызывает AllocateReplicas и при ошибке освобождает квоту. AllocateReplicas выбирает предпочтительные сегменты, делает снимок аллокатора и вызывает стратегию аллокации для memory-реплик; при неудаче возвращает NO_AVAILABLE_HANDLE, инкрементит счётчик неудач и при достатке сегментов взводит флаг необходимости эвикции памяти. Аналогично для NOF-реплик.
InsertMetadata проверяет отсутствие ключа (иначе освобождает DFS-реплики и возвращает OBJECT_ALREADY_EXISTS), собирает список реплик и вставляет запись; при неудаче вставки освобождает DFS-реплики, при multi-tenant принимает pending-начисление и при ошибке откатывает запись. Затем запускает soft-pin, при наличии дедлайна делает вставку в индекс, при непустом group_id регистрирует групповой lease и, наконец, добавляет ключ в processing_keys.
PutEnd проверяет существование объекта (иначе OBJECT_NOT_FOUND), совпадение client_id (иначе ILLEGAL_CLIENT), и если запись не в процессе, но все целевые реплики завершены — возвращает успех как no-op, иначе INVALID_WRITE. Затем для DFS-реплик при наличии bucket-аллокатора коммитит (при неудаче FILE_WRITE_FAIL), помечает целевые реплики завершёнными, при необходимости коммитит pending soft-pin и записывает контрольную сумму.
Состояние processing_keys удаляется при обработке удаления реплик: если все реплики завершены, ключ стирается. При финализации истёкших processing-реплик после durable-записи, если расчёт квоты прошёл и запись всё ещё в процессе, она убирается из processing.
Клиентский Put/BatchPut
Клиент выполняет запись через последовательность стадий над вектором операций. Сначала создаются операции и считаются контрольные суммы, затем StartBatchPut для нерешённых операций собирает ключи и длины слайсов и вызывает пакетный старт; при несовпадении размера ответа все операции получают RPC_FAIL. После этого идут стадии подготовки буферов, отправки передач, ожидания, отправки DFS-записей, финализации и сбора результатов.
Отправка передач пропускает уже решённые операции и операции без реплик (для них ставится внутренняя ошибка), а для остальных по каждой реплике отправляет запрос на запись; для NoF-реплик требуется непрерывный диапазон, иначе записывается INVALID_PARAMS. Ожидание передач по каждой записи фиксирует успех или неудачу. Запись в DFS выполняется синхронно: проверяются размеры, при отсутствии бэкенда возвращается DFS_SERVICE_UNAVAILABLE, выполняется стейджинг с копированием с устройства, собирается запрос, и по результатам пакетной записи фиксируется успех или неудача.
Финализация по каждой нерешённой операции принимает решение, которое требует ожидаемого распределения реплик и успеха всех распределённых передач, иначе возвращает тип отзыва ALL с ошибкой передачи или отсутствием доступного хэндла. Успех по байтам считается суммированием размеров слайсов только для успешных операций.
Abandoned-write
abandoned-writeситуация, когда DFS-реплика выделена, но запись в неё не выполняется, потому что сначала провалилась передача в не-DFS реплику. В этом случае клиент пропускает запись в DFS и учитывает её счётчиком пропущенных записей, логируя причину.
Для восстановления «висящей» локальной disk-реплики (запись в локальный файл, чей файл исчез) служит отдельный механизм: он получает список реплик, проверяет, что все завершённые реплики — это LOCAL_DISK, принадлежащие этому клиенту, и что проба файла вернула «файл точно отсутствует». Только тогда вызывается эвикция disk-реплики, чтобы мастер удалил метаданные о пропавшей реплике. Пакетный вариант делает то же для набора ключей: одним пакетным запросом проверяет кандидатов, для прошедших проверку собирает ключи и вызывает пакетную эвикцию, а затем повторяет пакетный старт записи для освобождённых ключей.
Disk-реплика обрабатывается раньше отзыва/завершения, потому что иначе финализация могла бы пройти до того, как локальный файл записан: сначала запись в локальный файл для disk-реплики, и только затем решение о финализации и вызовы завершения или отзыва.
Чтение и hot cache
Выбор реплики для чтения
Клиент выбирает реплику через фильтр, который строит результат только с выбранной репликой, чтобы обычные Get/BatchGet не могли выбрать другой тип реплики. Селектор сначала возвращает первую завершённую memory-реплику, чей endpoint входит в локальные, иначе запоминает первую memory-реплику. Селектор сессии сначала пробует завершённую memory-реплику, затем завершённую dfs-реплику, затем завершённую local_disk или disk.
Стейдж-буфер для сессии при нулевом размере не выделяется. Иначе при предпочтении pinned и файловом хранилище делается попытка выделить pinned-буфер, при неудаче — обычное выделение.
Cache hit и miss
При попадании в hot cache проверяется, что реплика memory и кэш есть, берётся блок по ключу; если блока нет — это промах. При несовпадении размера логируется ошибка и возвращается промах. Иначе подменяются endpoint и адрес буфера на адрес блока. При успешной передаче после попадания ключ освобождается, а при промахе проверяется, стоит ли продвигать ключ в кэш, и запускается асинхронная обработка слайсов.
Ranged read и сессии
Session-based чтение начинается со старта сессии: для каждого ключа выполняется пакетный запрос, проверяется размер ответа, и для каждого успешного результата выбирается реплика. Если аренда истекла — LEASE_EXPIRED, если реплика не найдена — INVALID_REPLICA; иначе результат фильтруется и сохраняется в сессии.
Подготовка ranged-запросов проверяет, что размеры буферов, размеров и смещений совпадают, и формирует запрос. Классификация раскладывает запросы по типу выбранной реплики: memory, dfs, local_disk, disk; при неизвестном типе пишется INVALID_REPLICA.
Чтение memory-диапазонов собирает слайсы и смещения и вызывает пакетное чтение диапазонов, которое требует memory-реплику и объединяет все фрагменты в одну scatter-отправку. Чтение dfs-диапазонов строит стейджинг-арену с выравниванием 64 и вызывает пакетное чтение. Чтение local_disk-диапазонов группирует запросы по endpoint: для каждого endpoint и ключа хранит размер восстановления и список запросов, затем вызывает восстановление объектов и пакетное чтение offload-диапазонов с удалёнными базами из аренды.
Истечение лизы сессии
Истечение проверяется по абсолютному дедлайну: если текущее время уже наступило, под мьютексом сессии записывается код LEASE_EXPIRED и сессия удаляется по ключу объекта. Эта проверка вызывается дважды — до и после scatter-чтения, чтобы не отдавать данные по уже истёкшей лизе. При memory-чтениях аналогичная проверка делается после завершения.
Для файловых (DFS/дисковых) запросов пометка проходит по всем запросам и тем, у которых ключ совпадает с заданным, записывает переданный код ошибки — так помечаются все запросы по объекту, например при переполнении буфера или отсутствии доступного хэндла.
Перед отправкой планирование может занять большую часть короткой лизы, поэтому при разрешённом обновлении и непустых арендах вызывается обновление лиз, которое через пакетный запрос обновляет время истечения.
Transfer Engine: транспорт и регистрация памяти
Выбор транспорта и TENT-режим
Transfer Engine выбирает реализацию в конструкторе: если включена переменная окружения MC_USE_TENT или MC_USE_TEV1, включается TENTальтернативная реализация движка передачи, выбираемая переменной окружения, иначе создаётся классическая реализация. TENT отличается тем, что установка транспорта в нём — совместимая заглушка: аргументы игнорируются, один раз логируется сообщение, а сам вызов не выполняет никакой работы; получение P2P-, RDMA- и NCCL-транспортов не даёт транспорта; проверка «только TCP» всегда сообщает, что режим не ограничен TCP, потому что TENT сам отклоняет TCP loopback при выключенном MC_STORE_MEMCPY. При инициализации в TENT-режиме строится конфиг, при протоколе tcp принудительно включается TCP, затем создаётся движок и проверяется его доступность. В классическом режиме инициализация просто делегируется реализации.
Регистрация памяти и частичный успех
Регистрация памяти сначала проверяет длину (нулевая запрещена), затем резервирует регион с проверкой пересечений; при неудаче возвращается ERR_ADDRESS_OVERLAPPED. Далее по очереди регистрируется на каждом транспорте, накапливая попытки; при ошибке любого транспорта выполняется откат уже зарегистрированных в обратном порядке, затем освобождается резерв и возвращается код ошибки. При полном успехе регион переносится из резервируемых в зарегистрированные.
Снятие регистрации работает best-effort: вызывается на всех транспортах, запоминается первая ошибка, и только если ошибок нет — регион удаляется из зарегистрированных.
Пакетная регистрация сортирует буферы по адресу, проверяет нулевые длины и пересечения соседних буферов, затем резервирует все регионы; при ошибке транспорта откатывает уже зарегистрированные и освобождает резерв. Многопротокольная регистрация сначала проверяет пересечения и нулевые длины для всех буферов, затем для каждого протокола получает транспорт и регистрирует буферы, накапливая успешные записи; при ошибке транспорта или его отсутствии откатывает все записи. При полном успехе все регионы вставляются под блокировкой.
Сегменты
Открытие сегмента принимает имя, отбрасывает ведущие символы /, получает идентификатор сегмента и возвращает его как дескриптор; при пустом имени или пустом после обрезки возвращается ERR_INVALID_ARGUMENT. Закрытие сегмента в классической реализации просто возвращает 0, то есть ничего не делает. Удаление локального сегмента также обрезает ведущие /, проверяет непустоту и делегирует удаление.
Проверка пересечения берёт разделяемую блокировку и проверяет пересечение с зарегистрированными и резервируемыми регионами. Проверка статуса сегмента в классической реализации возвращает успех (или делегирует проверку статуса при использовании barex). Синхронизация кэша сегмента просто делегирует реализацию.
Асинхронные задачи и пулы воркеров
Пулы воркеров TransferTask
Пул SPDK/NoF создаёт заданное число потоков, каждому даёт отдельную очередь, мьютекс и условную переменную. Отправка задачи привязывает сегмент к воркеру: новый сегмент получает индекс по кругу, иначе берётся уже сохранённый, и задача кладётся в очередь этого воркера. Поток воркера ждёт задачу, остановку, наличие незавершённых операций ввода-вывода или буферизованных задач, затем переносит задачи из очереди в очередь качества обслуживания по сегменту и разбивает их на подзадачи, беря их из пула подзадач, который пополняется блоками фиксированного размера.
Пул memcpy запускает ровно один поток, потому что копирование ограничено пропускной способностью памяти. Отправка задачи кладёт её в общую очередь, а поток воркера выполняет операции: при отсутствии устройств для указателей делает обычное копирование, иначе выбирает направление (устройство-хост, хост-устройство, устройство-устройство) и вызывает копирование через ускоритель.
Пул чтения файлов запускает число потоков из конфигурации, отправка кладёт задачу в общую очередь, а поток вызывает загрузку объекта и выставляет результат.
Выбор стратегии
Стратегия выбирается так: если локальное копирование применимо, возвращается LOCAL_MEMCPY, иначе TRANSFER_ENGINE. Локальное копирование требует, чтобы оно было включено и endpoint совпадал с локальным хостом или локальным endpoint. Совпадение endpoint проверяется полностью, потому что два процесса на одном хосте делят IP, но имеют разные виртуальные адресные пространства.
TransferFuture и отмены
TransferFuture — обёртка над состоянием операции: проверка готовности возвращает признак завершения, ожидание при отсутствии готовности дожидается завершения и возвращает результат, а получение просто вызывает ожидание. Отправка выбирает путь по типу реплики: для memory-реплики при чтении вызывается чтение памяти, иначе выбирается стратегия и вызывается либо локальное копирование, либо передача через движок; для nof-реплики при включённом NoF требуется ненулевой указатель и размер, иначе — чтение файла.
Пакетная отправка принимает только memory-реплики, проверяет совпадение размеров и параметров и решает, использовать ли локальное копирование (запись и применимость для всех endpoint'ов).
Внутренняя реализация для TENT в ожидании крутит опрос до завершения; по истечении таймаута (по умолчанию 60 секунд) запрашивает прерывание, затем ждёт льготный период (по умолчанию 1 секунда) и при необходимости выполняет принудительную очистку. Ожидание с таймаутом при истечении запрашивает прерывание и возвращает агрегированный статус. Запоминание сохраняет первую не-OK ошибку, завершение помечает индекс, вызывает запоминание и колбэк, повторное закрытие сегментов до 3 раз пытается закрыть оставшиеся, провал ожидающих завершает все индексы, запрос прерывания запоминает статус, выставляет флаги и отменяет незавершённые задачи, а принудительная очистка завершает ожидающие, освобождает батч и финиширует.
Фоновые задачи мастера
Фоновые задачи выполняются отдельными потоками, каждый в цикле спит заданное число миллисекунд, а затем выполняет работу.
Поток очистки задач ждёт заданный интервал, после чего под блокировкой удаляет истёкшие и завершённые задачи, а также очищает истёкшие soft pin и истёкшее состояние динамической репликации. Поток эвикции спит; при превышении верхнего порога или взведённом флаге необходимости эвикции вызывает пакетное вытеснение, иначе раз в заданный таймаут отбрасывает истёкшие processing-реплики и освобождает истёкшие отброшенные; отдельно раз в интервал проверки вытесняет тенантов сверх порога, а раз в интервал проверки DFS-аллокатора запускает DFS-эвикцию.
Поток мониторинга клиентов спит и для каждого клиента оценивает и отстраняет его по TTL активности и подозрительности, переводя клиента в состояние подозреваемого или офлайн. Поток диспетчеризации задач спит и обрабатывает задачи слива.
Поток heartbeat хранилища в клиенте использует интервалы успешного и неуспешного пинга по 1000 мс, а после 3 неудачных пингов переподключается через HA-координатор.
Отказоустойчивость и HA
HA-режим на клиенте
HA-режим включается при подключении к мастеру: если строка адреса содержит HA-бэкенд, создаётся координатор лидеракомпонент, который хранит текущее представление о том, какой узел является лидером, и оповещает клиента о его смене, затем ожидается готовое представление лидера с начальным таймаутом; при его получении выполняется переключение лидера, сохраняется координатор и запускается поток мониторинга. Вход в HA-режим лишь переключает клиент мастера на быструю HA-политику соединения.
Переключение лидера под мьютексом игнорирует устаревшие или совпадающие представления, иначе подключается к адресу лидера и при успехе сохраняет представление и признак успешного пинга. Поток мониторинга циклически ждёт изменения представления с таймаутом 30000 мс и при ошибке спит 1000 мс; при получении нового представления вызывает переключение.
Порог переключения по пингам: 3 неудачи, интервалы успешного и неуспешного пинга по 1000 мс. После превышения порога при наличии координатора читается текущее представление и вызывается переключение, иначе выполняется переподключение к прямому адресу мастера.
Восстановление из снапшота
Восстановление состояния из снапшота идёт в три фазы: сначала находятся кандидаты, затем для каждого кандидата в цикле сбрасывается состояние, скачиваются полезные нагрузки, декодируется снапшот и применяется состояние; при успехе возвращается управление, при неудаче переходит к следующему кандидату, а после перебора всех снова сбрасывает состояние и стартует с чистого.
Сброс состояния очищает сериализаторы, менеджер локального SSD и клиентские структуры, сбрасывает метрики памяти и клиентов. Применение состояния пересобирает записи о живости клиентов: собирает идентификаторы из сегментов, менеджера локального SSD и локально-дисковых реплик, создаёт записи, привязывает их и перепривязывает буферы к владеющим сегментам; если у memory-реплики нет регистрации сегмента, восстановление завершается ошибкой десериализации.
Восстановление из standby-снапшота и из продвижения по журналу операций делегируют в общее восстановление, которое при включённом DFS завершается ошибкой недоступности сервиса DFS, проверяет параметры и метаданные весов, а затем под блокировками клиента и снапшота выполняет восстановление.
Валидация legacy-записей standby отбрасывает записи с недействительным tenant, дубликатом объекта, уже существующим объектом, неизвестным endpoint'ом, недействительным дескриптором памяти; перекрывающиеся диапазоны помечаются как отброшенные реплики; объекты без надёжных реплик получают признак отсутствия надёжной реплики и попадают в список на удаление при починке. Установка legacy-объектов завершается ошибкой недопустимых параметров, если все объекты отклонены и число уже существующих меньше общего, и ошибкой «объект уже существует» при коллизии ключей; иначе вставляет метаданные, регистрирует группу и возвращает число восстановленных.
Graceful unmount сегментов
Graceful unmount запускается клиентом: если льготный период равен нулю, сразу выполняется немедленное размонтирование, иначе клиент запрашивает у мастера graceful-размонтирование, переносит сегмент в список размонтируемых, сохраняет колбэк очистки и запускает таймер.
Таймер ставит в пул потоков задачу, которая ждёт задержку, равную льготному периоду плюс 10 секунд; если ожидание не прервано флагом остановки, вызывается обработчик с 3 попытками. Обработчик опрашивает мастер о статусе сегмента: сегмент считается удалённым, если вернулась ошибка «сегмент не найден» или статус «не определён»; тогда клиент снимает регистрацию памяти, удаляет запись и вызывает колбэк очистки. Если сегмент ещё не удалён и попытки остались, задача перепланируется с ожиданием 10 секунд и уменьшением счётчика попыток; при нуле попыток пишется предупреждение о таймауте.
Мастер при graceful-размонтировании сначала проверяет владение сегментом, отклоняя запрос, если сегмент не принадлежит клиенту, затем подготавливает сегмент к размонтированию и планирует задачу на время истечения. Подтверждение удаления клиент получает не от мастера напрямую, а опросом статуса сегмента.
Stale handles и quarantined shms
stale handleреплика с недействительным дескриптором памяти или устаревшим владельцем локального диска, при этом завершённая. План очистки проходит по всем репликам, отбирает такие в список на удаление, а остальные завершённые складывает в оставшиеся; флаг «сделает недействительным» выставляется, если после очистки не остаётся валидных реплик.
Очистка идёт в два прохода по шардам: сначала под разделяемой блокировкой собираются ключи с устаревшими репликами или невалидными метаданными, затем под эксклюзивной блокировкой они обрабатываются батчами. Если включены HA и журнал операций, вместо немедленного удаления запись резервирует слот в журнале, помечает реплики как удалённые и откладывает фактическое удаление до durable-финализации.
Очистка удаляет реплики, отменяет задачи продвижения, освобождает квоту памяти и локального диска и синхронизирует KV-состояние; если после неё валидных реплик не остаётся, объект считается полностью очищенным.
Обработка quarantined shms: пока снятие регистрации в TE ожидает, оно повторяется, и только после успеха снимается флаг; затем при необходимости снимается регистрация SPDK и делается munmap. Снятие регистрации в TE делается до munmap, потому что TE может всё ещё держать регион: если сделать munmap при активной регистрации, последующий mmap, переиспользующий этот виртуальный адрес, столкнётся с коллизией адреса или DMA в немаппированную память.
Eviction, offload и promotion
Eviction и tenant quota
Эвикция запускается в фоновом потоке: он раз в цикл
Где смотреть в коде
- transfer_task.cpp: submit_batch
- transfer_engine_impl.cpp: closeSegment
- real_client.cpp: getInstance
- master_service.cpp: AllocateAndInsertMetadata
- master_service.cpp: CancelQueuedOffloadTask
- real_client.cpp: FilterQueryResult
- real_client.cpp: get_config_size
- master_service.cpp: DynamicReplicationEnabled