---
id: 517bf76b20da9b1bac643f4bac1bfd6b
author: Lemon Mint
title: Быстрые изолированные среды Docker для развертывания траекторий обучения с подкреплением (RL) AI-агентов с помощью gvx
description: Как безопасно запускать сотни параллельных, строго изолированных непривилегированных окружений Docker и Kubernetes для SWE-bench и обучения с подкреплением на траекториях (rollouts) коding-агентов со сбросом томов ext4lite по схеме copy-on-write за 100 миллисекунд.
language: ru
date: 2026-11-06T12:02:55.95126412Z
path: /blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04
go_package: z.gfire.dev/gvx
go_repourl: https://github.com/gfire-sigs/gvx.git
category: ai
---

## Узкое место безопасности и инфраструктуры в RL-развертываниях кодирующих агентов

Обучение и оценка агентов для разработки программного обеспечения на таких бенчмарках и фреймворках обучения с подкреплением (reinforcement learning, RL), как **SWE-bench**, **SWE-ReX**, **R2E-Gym**, **SWE-Gym**, **Verl** и **OpenHands**, требует выполнения тысяч параллельных развертываний (rollouts) недоверенного кода, сгенерированного LLM. В реальных задачах разработки программного обеспечения агент не просто редактирует текстовые файлы: он выполняет `docker build`, поднимает базы данных и микросервисы через `docker compose`, запускает наборы интеграционных тестов и анализирует логи контейнеров.

При масштабе развертываний (от 128 до 1,024 параллельных эпизодов на кластер) существующая контейнерная инфраструктура сталкивается с четырьмя системными проблемами безопасности и производительности:

1. **Блокировки и утечка состояния у хостового `dockerd`**: Направление сотен параллельных RL-воркеров на один хостовый `/var/run/docker.sock` предоставляет каждому агенту доступ к демону хоста с правами, эквивалентными root. Это сериализует извлечение слоев образов, исчерпывает подсети `docker0`, вызывает ошибки `Another app is currently holding the xtables lock` в хостовом `iptables` и приводит к утечке осиротевших контейнеров между эпизодами.
2. **Риск выхода из контейнера при привилегированном DinD**: Запуск стандартных контейнеров `docker:dind` требует `--privileged` (`CAP_SYS_ADMIN`), что открывает недоверенным RL-развертываниям прямой доступ к блочным устройствам хоста, release agent в cgroup и модулям ядра.
3. **Ограничения аппаратной виртуализации (`/dev/kvm`)**: MicroVM-рантаймы (`Firecracker`, `QEMU`, `Kata Containers`) надежно изолируют внутреннее ядро, но требуют `/dev/kvm`. Стандартные Kubernetes-поды без вложенной виртуализации, bare-metal облачные инстансы, где KVM отключен, и существующие песочницы `gVisor` (`runsc`) не могут запускать KVM microVM.
4. **Высокая задержка сброса эпизода (`env.reset()`)**: Запуск нового окружения `docker:dind` и загрузка или распаковка базовых образов для каждого RL-эпизода занимает от 10 до 60 секунд, в течение которых GPU-воркеры обучения простаивают в ожидании сброса окружения.

**[gvx](https://github.com/gfire-sigs/gvx)** (CLI `rungv` и Go SDK `z.gfire.dev/gvx/pkg/gvx`) предоставляет **безопасный рантайм развертываний с многоуровневой защитой (defense-in-depth)**, который устраняет все четыре узких места. Каждое окружение развертывания работает как **единственный непривилегированный самоблокирующийся процесс пользовательского пространства** (изолированный ядром gVisor `Sentry` от Google, `pkg/hostlockdown` и встроенным в память `netstack`), а все его состояние `/var/lib/docker` находится внутри **одного файла `.ext4` с копированием при записи (`ext4lite`)**.

> **Безопасность без привилегий для недоверенных развертываний агентов**: Каждый эпизод `gvx` блокирует собственный процесс хостовой ОС до запуска гостевого кода (`PR_SET_NO_NEW_PRIVS` и BPF `seccomp` в Linux, Seatbelt `deny default` в macOS, Restricted Token и Job Object в Windows). Даже если RL-агент выполнит `docker run --privileged` или деструктивную команду `rm -rf /` внутри своего эпизода, эти операции остаются строго ограниченными пользовательским ядром `Sentry` этого эпизода и его временным файлом тома `.ext4`.

---

## Архитектура развертывания: параллельные непривилегированные экземпляры и снимки reflink

В `gvx` каждый воркер развертывания является обычным непривилегированным процессом ОС со своим ядром gVisor `Sentry`, собственным `netstack` пользовательского пространства (`10.0.2.15/24`) и собственным блочным томом `ext4lite` (`ep-N.ext4`), при этом все воркеры совместно используют доступный только для чтения адресуемый по содержимому кэш образов OCI `erofs` в страничном кэше хоста.

![Архитектура параллельных RL-развертываний агентов с gvx](/assets/images/gvx/gvx-rl-rollout-snapshot.png)

### Почему тома `ext4lite` в одном файле меняют экономику сброса в RL

Поскольку файловая система `ext4lite` хранит целый том `ext4` с журналом JBD2 (включая каталоги слоев `overlayfs` Docker, расширенные атрибуты `trusted.overlay.*`, узлы устройств whiteout и метаданные образов) внутри одного обычного файла хоста, управление жизненным циклом эпизода сводится к одной файловой операции:

1. **Сброс `env.reset()` за 100 миллисекунд через `cp --reflink=always`**:
   - Однократно создайте файл `golden-docker.ext4`, содержащий предварительно загруженные базовые образы (`python:3.11`, `postgres:16`, `redis:7` или специфичные для задачи базовые образы `SWE-bench`).
   - В начале каждого RL-эпизода выполните `cp --reflink=always golden-docker.ext4 ep-0042.ext4` (на XFS, Btrfs, ZFS или macOS APFS). Это создает независимый клон с копированием при записи менее чем за 100 миллисекунд без копирования базовых блоков диска.
   - Дополнительное место на диске расходуют только блоки по 4 КиБ, изменённые в ходе данного эпизода.
2. **Очистка без остатков (`env.close()`)**:
   - Завершение процесса `rungv` (`sb.Close()` в Go) и удаление `ep-0042.ext4` освобождает 100% процессов, сетевых сокетов, точек монтирования и хранилища эпизода примерно за 10 миллисекунд. Ничего никогда не монтируется в таблицу монтирования ядра хоста и не регистрируется в cgroup хоста.

---

## Пошаговый процесс: золотой снимок `.ext4` и быстрый сброс эпизода

Установить `rungv` на каждом узле воркеров можно двумя способами: загрузив бинарный файл с атестацией SLSA из [GitHub Releases](https://github.com/gfire-sigs/gvx/releases) (`gh attestation verify rungv-v0.2.0-linux-amd64.tar.gz --repo gfire-sigs/gvx --signer-workflow gfire-sigs/gvx/.github/workflows/release.yml --deny-self-hosted-runners`) или выполнив `CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest`.

### Шаг 1: Однократное создание золотого тома `docker:dind`

```sh
mkdir -p /srv/rl-snapshots /srv/rl-episodes

# 1. Запустить исходный экземпляр docker:dind, использующий golden-docker.ext4
rungv run \
  -p unix:/srv/rl-snapshots/seed.sock:2375 \
  --mount type=volume,src=/srv/rl-snapshots/golden-docker.ext4,dst=/var/lib/docker \
  -e DOCKER_TLS_CERTDIR= \
  docker:dind --tls=false &
SEED_PID=$!

# 2. Предварительно загрузить базовые образы и кэш сборки внутрь golden-docker.ext4
export DOCKER_HOST="unix:///srv/rl-snapshots/seed.sock"
until docker info >/dev/null 2>&1; do sleep 0.5; done

docker pull python:3.11-slim
docker pull redis:7-alpine
docker pull postgres:16-alpine

# 3. Корректно остановить исходный экземпляр, чтобы ext4lite сбросил данные и уплотнил образ
kill -INT $SEED_PID
wait $SEED_PID
```

### Шаг 2: Запуск параллельных RL-эпизодов за 100 миллисекунд

Каждый параллельный воркер развертывания клонирует `golden-docker.ext4` через reflink и запускает изолированный экземпляр `docker:dind`, в котором все образы уже находятся в `/var/lib/docker`:

```sh
EPISODE_ID="ep-0042"
SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"

# Мгновенный клон с копированием при записи (~100 мс на XFS/Btrfs/APFS)
cp --reflink=always \
  /srv/rl-snapshots/golden-docker.ext4 \
  "/srv/rl-episodes/${EPISODE_ID}.ext4"

# Запустить изолированный непривилегированный эпизод с 4 ГиБ ОЗУ и 2 ЦП
rungv run \
  -m 4g --cpus 2 \
  -p "unix:${SOCK}:2375" \
  --mount "type=volume,src=/srv/rl-episodes/${EPISODE_ID}.ext4,dst=/var/lib/docker" \
  -e DOCKER_TLS_CERTDIR= \
  docker:dind --tls=false &
EP_PID=$!

export DOCKER_HOST="unix://${SOCK}"
until docker info >/dev/null 2>&1; do sleep 0.2; done

# Внутри воркера развертывания образы доступны сразу, без загрузки из сети
docker images

# Завершение эпизода после расчета награды (~10 мс)
kill -KILL $EP_PID
rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
```

---

## Программный пул развертываний с использованием Go SDK `z.gfire.dev/gvx/pkg/gvx`

Если ваш сервер RL-окружений или harness оценки написан на Go (или обращается к демону развертываний на Go через gRPC), вы можете управлять параллельными контейнерными эпизодами напрямую через `z.gfire.dev/gvx/pkg/gvx`:

```go
package main

import (
	"bytes"
	"context"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	"z.gfire.dev/gvx/pkg/gvx"
)

func runEpisode(ctx context.Context, goldenExt4, scratchDir string, epID int) error {
	epExt4 := filepath.Join(scratchDir, fmt.Sprintf("ep-%04d.ext4", epID))
	defer os.Remove(epExt4)

	// 1. Клонировать золотой том ext4lite через reflink
	cpCmd := exec.CommandContext(ctx, "cp", "--reflink=always", goldenExt4, epExt4)
	if err := cpCmd.Run(); err != nil {
		return fmt.Errorf("clone golden volume: %w", err)
	}

	// 2. Задать ограничение wall-clock времени для эпизода
	epCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
	defer cancel()

	var stdout, stderr bytes.Buffer
	sb, err := gvx.New(epCtx, gvx.Config{
		Platform:               gvx.PlatformAuto,
		NetworkMode:            gvx.NetModeNAT,
		NetRaw:                 true,
		AllowPacketSocketWrite: true,
		AllowSUID:              true,
		OCIImage:               "docker:dind",
		Entrypoint:             []string{"sh", "-c"},
		EntrypointSet:          true,
		Cmd: []string{
			"dockerd-entrypoint.sh --tls=false >/var/log/dockerd.log 2>&1 & " +
				"until docker info >/dev/null 2>&1; do sleep 0.2; done && " +
				"docker run --rm python:3.11-slim python -c 'print(6 * 7)'",
		},
		Envv: []string{"DOCKER_TLS_CERTDIR="},
		Mounts: []gvx.Mount{
			{
				Kind:      gvx.MountVolume,
				ImagePath: epExt4,
				GuestPath: "/var/lib/docker",
			},
		},
		Resources: gvx.Resources{
			MemoryBytes: 4 << 30, // ограничение 4 ГиБ
			CPUMillis:   2000,    // 2,0 ЦП
		},
		Stdout: &stdout,
		Stderr: &stderr,
	})
	if err != nil {
		return fmt.Errorf("init episode %d: %w", epID, err)
	}
	defer sb.Close()

	exitCode, err := sb.Run(epCtx)
	if err != nil {
		return fmt.Errorf("run episode %d: %w", epID, err)
	}

	log.Printf("episode %d finished: exit=%d stdout=%q", epID, exitCode, stdout.String())
	return nil
}

func main() {
	ctx := context.Background()
	if err := runEpisode(ctx, "/srv/rl-snapshots/golden-docker.ext4", "/srv/rl-episodes", 1); err != nil {
		log.Fatal(err)
	}
}
```

---

## Сравнение рантаймов песочниц для RL-агентов

| Метрика / требование                                      | Общий хостовый `dockerd`          | Привилегированный `docker:dind` | Firecracker / KVM MicroVM      | Стандартный `gVisor` (`runsc`)       | `gvx` (`rungv` / `pkg/gvx`)                  |
| :-------------------------------------------------------- | :-------------------------------- | :------------------------------ | :----------------------------- | :----------------------------------- | :------------------------------------------- |
| **Требуется `root` хоста или `--privileged`**             | Да (`docker.sock` = root)         | Да (`--privileged`)             | Нужны `/dev/kvm` и настройка `TAP` | Да (или `userns` + `mount`)          | **Нет (единственный непривилегированный процесс)** |
| **Работает внутри непривилегированных K8s Pod / существующего `runsc`** | Нет                  | Нет                             | Нет (нет `/dev/kvm`)           | Ошибка (`EBUSY` при вложенном seccomp) | **Да (автоматический откат на цикл futex)**   |
| **Поддерживает внутренние `docker build` и `docker compose`** | Использует общий демон хоста      | Да                              | Да                             | Ошибка без настройки host DinD       | **Да (`overlayfs` на `ext4lite`)**           |
| **Сетевая изоляция для каждого эпизода**                  | Общие `docker0` / блокировка `xtables` | Отдельное пространство имен | Отдельный сетевой интерфейс VM | Отдельный netns                      | **Изолированный `netstack` внутри процесса** |
| **Задержка сброса теплого эпизода (`env.reset()`)**       | От 5 с до 30 с (очистка контейнеров/томов) | От 10 с до 60 с (холодный демон) | ~150 мс (снимок VM)      | От 5 с до 20 с                       | **~100 мс (`cp --reflink=always` для `.ext4`)** |
| **Локальная разработка на разных платформах (Windows / macOS / Linux)** | Требуется VM с Linux    | Требуется VM с Linux            | Только Linux KVM               | Только Linux                         | **Нативно на Windows, macOS и Linux**        |

---

## Дополнительные материалы в этой серии

1. **[gvx: запуск контейнеров Linux, Docker и Kubernetes в одном непривилегированном процессе](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Полный архитектурный обзор `gvx` для Windows (`whpx`, `systrap`), macOS (`hvf`) и Linux (`systrap`).
2. **[Запуск Docker, docker build и docker compose без root и пользовательских пространств имен](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Краткое руководство из трех шагов по начальной настройке и устранению неполадок, когда заблокированы `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` и `/dev/fuse`.
3. **[Запуск Docker-in-Docker внутри gVisor и вложенных песочниц с gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)**: Подробный разбор причин, по которым `docker.io` и вложенный `runsc` не работают внутри `gVisor` (`runsc`), и того, как `gvx` переключается на цикл futex `_RUN_SYSCALL_LOOP` при `EBUSY`.
4. **[Быстрые изолированные Docker-окружения для RL-развертываний AI-агентов с gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)** (этот пост)
