Snabba isolerade Docker-miljöer för AI-agenters RL-rollouts med gvx

Flaskhalsen i säkerhet och infrastruktur för RL-rollouts med kodagenter
Träning och utvärdering av mjukvaruutvecklingsagenter över benchmarks och ramverk för förstärkningsinlärning (reinforcement learning, RL), såsom SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl och OpenHands, kräver exekvering av tusentals samtidiga rollouts av ej betrodd, LLM-genererad kod. I verkliga uppgifter inom mjukvaruutveckling redigerar en agent inte bara textfiler: den kör docker build, startar databaser och mikrotjänster via docker compose, exekverar integrationstestsviter och inspekterar containerloggar.
Vid rollout-skala (128 till 1 024 samtidiga episoder per kluster) träffar befintlig containerinfrastruktur fyra systemiska flaskhalsar för säkerhet och prestanda:
- Låskonkurrens i värdens
dockerdoch tillståndsläckage: När hundratals samtidiga RL-arbetare riktas mot en enda/var/run/docker.sockpå värden får varje agent root-motsvarande åtkomst till värdens daemon, extraktionen av bildlager serialiseras, adressutrymmet i subnätetdocker0tar slut, felenAnother app is currently holding the xtables lockutlöses i värdensiptablesoch föräldralösa containrar läcker mellan episoderna. - Risk för containerrymning via privilegierad DinD: Att köra standardcontainrar med
docker:dindkräver--privileged(CAP_SYS_ADMIN), vilket exponerar råa blockenheter på värden, cgroup release agents och kärnmoduler för ej betrodda RL-rollouts. - Begränsningar för hårdvarubaserad virtualisering (
/dev/kvm): MicroVM-runtimes (Firecracker,QEMU,Kata Containers) isolerar inre kärnor på ett rent sätt, men de kräver/dev/kvm. Standardpods i Kubernetes utan nästlad virtualisering, bare-metal-molninstanser där KVM är inaktiverat och befintligagVisor(runsc)-sandlådor kan inte starta KVM-microVMs. - Långsam latens vid episodåterställning (
env.reset()): Att starta en nydocker:dind-miljö och hämta eller packa upp basbilder för varje RL-episod tar 10 till 60 sekunder, vilket lämnar GPU-träningsarbetare overksamma medan de väntar på miljöåterställningar.
gvx (rungv CLI och z.gfire.dev/gvx/pkg/gvx Go SDK) tillhandahåller en säker rollout-runtime med försvar på flera nivåer som löser alla fyra flaskhalsarna. Varje rollout-miljö körs som en enda oprivilegierad, självlåst process i användarutrymmet (isolerad av Googles gVisor-kärna Sentry, pkg/hostlockdown och ett minnesbaserat netstack) vars hela tillstånd i /var/lib/docker finns inuti en enda kopiera-vid-skrivning-fil .ext4 (ext4lite).
Nollprivilegiesäkerhet för ej betrodda agent-rollouts: Varje
gvx-episod låser ned sin egen värdprocess i operativsystemet innan gästkod körs (PR_SET_NO_NEW_PRIVS+ BPFseccomppå Linux, Seatbeltdeny defaultpå macOS, Restricted Token + Job Object på Windows). Även om en RL-agent kördocker run --privilegedeller destruktivarm -rf /-kommandon inom sin episod, förblir dessa operationer strikt begränsade till episodens kärna i användarutrymmet (Sentry) och dess tillfälliga volymfil.ext4.
Rollout-arkitektur: Parallella oprivilegierade instanser + reflink-ögonblicksbilder
I gvx är varje rollout-arbetare en vanlig oprivilegierad OS-process med sin egen gVisor-kärna Sentry, sitt eget netstack i användarutrymmet (10.0.2.15/24) och sin egen blockvolym ext4lite (ep-N.ext4). Samtidigt delar arbetarna en skrivskyddad, innehållsadresserbar OCI-bildcache i formatet erofs över värdens sidcache.

Varför volymer i form av enskilda ext4lite-filer förändrar ekonomin för RL-återställning
Eftersom ext4lite-filsystemet i gvx lagrar en hel JBD2-journalförd ext4-volym (inklusive Dockers lagerkataloger för overlayfs, utökade attribut trusted.overlay.*, whiteout-enhetsnoder och bildmetadata) i en enda vanlig värdfil, blir hanteringen av episodens livscykel en enda filoperation:
- 100 millisekunders
env.reset()viacp --reflink=always:- Du bygger en gång en fil
golden-docker.ext4som innehåller förhämtade basbilder (python:3.11,postgres:16,redis:7eller uppgiftsspecifika basbilder förSWE-bench). - I början av varje RL-episod skapar
cp --reflink=always golden-docker.ext4 ep-0042.ext4(på XFS, Btrfs, ZFS eller macOS APFS) en oberoende klon med kopiera-vid-skrivning på under 100 millisekunder, utan att kopiera underliggande diskblock. - Endast de 4 KiB-block som ändras under just den episoden tar upp ytterligare diskutrymme.
- Du bygger en gång en fil
- Nedtagning utan kvarlämnade spår (
env.close()):- Att avsluta processen
rungv(sb.Close()i Go) och ta bortep-0042.ext4frigör 100 % av episodens processer, nätverkssockets, monteringar och lagring på ungefär 10 millisekunder. Ingenting monteras någonsin i värdkärnans monteringstabell eller registreras i värdens cgroups.
- Att avsluta processen
Steg-för-steg-arbetsflöde: Gyllene ögonblicksbild .ext4 och snabb episodåterställning
Du kan installera rungv på varje rollout-arbetarnod antingen genom att ladda ner den SLSA-attesterade förkompilerade binären från GitHub 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) eller genom att köra CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest.
Steg 1: Skapa den gyllene volymen docker:dind en gång
1mkdir -p /srv/rl-snapshots /srv/rl-episodes
2
3# 1. Starta en startinstans av docker:dind med stöd från golden-docker.ext4
4rungv run \
5 -p unix:/srv/rl-snapshots/seed.sock:2375 \
6 --mount type=volume,src=/srv/rl-snapshots/golden-docker.ext4,dst=/var/lib/docker \
7 -e DOCKER_TLS_CERTDIR= \
8 docker:dind --tls=false &
9SEED_PID=$!
10
11# 2. Förfyll basbilder och byggcache inuti golden-docker.ext4
12export DOCKER_HOST="unix:///srv/rl-snapshots/seed.sock"
13until docker info >/dev/null 2>&1; do sleep 0.5; done
14
15docker pull python:3.11-slim
16docker pull redis:7-alpine
17docker pull postgres:16-alpine
18
19# 3. Stoppa startinstansen på ett kontrollerat sätt så att ext4lite spolar och kompakterar avbildningen
20kill -INT $SEED_PID
21wait $SEED_PID
Steg 2: Starta parallella RL-episoder på 100 millisekunder
Varje parallell rollout-arbetare klonar golden-docker.ext4 via reflink och startar en isolerad docker:dind-instans med alla bilder redan närvarande i /var/lib/docker:
1EPISODE_ID="ep-0042"
2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
3
4# Omedelbar klon med kopiera-vid-skrivning (~100 ms på XFS/Btrfs/APFS)
5cp --reflink=always \
6 /srv/rl-snapshots/golden-docker.ext4 \
7 "/srv/rl-episodes/${EPISODE_ID}.ext4"
8
9# Starta isolerad oprivilegierad episod med 4 GiB RAM och 2 CPU:er
10rungv run \
11 -m 4g --cpus 2 \
12 -p "unix:${SOCK}:2375" \
13 --mount "type=volume,src=/srv/rl-episodes/${EPISODE_ID}.ext4,dst=/var/lib/docker" \
14 -e DOCKER_TLS_CERTDIR= \
15 docker:dind --tls=false &
16EP_PID=$!
17
18export DOCKER_HOST="unix://${SOCK}"
19until docker info >/dev/null 2>&1; do sleep 0.2; done
20
21# Inuti rollout-arbetaren finns bilderna omedelbart tillgängliga utan nätverkshämtning
22docker images
23
24# Nedtagning av episoden efter beräkning av belöningen (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
Programmatisk rollout-pool med Go SDK:t z.gfire.dev/gvx/pkg/gvx
Om din RL-miljöserver eller utvärderingsharness är skriven i Go (eller anropar en Go-daemon för rollouts över gRPC) kan du hantera parallella containerepisoder direkt via z.gfire.dev/gvx/pkg/gvx:
1package main
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "log"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "time"
12
13 "z.gfire.dev/gvx/pkg/gvx"
14)
15
16func runEpisode(ctx context.Context, goldenExt4, scratchDir string, epID int) error {
17 epExt4 := filepath.Join(scratchDir, fmt.Sprintf("ep-%04d.ext4", epID))
18 defer os.Remove(epExt4)
19
20 // 1. Reflink-klona den gyllene ext4lite-volymen
21 cpCmd := exec.CommandContext(ctx, "cp", "--reflink=always", goldenExt4, epExt4)
22 if err := cpCmd.Run(); err != nil {
23 return fmt.Errorf("clone golden volume: %w", err)
24 }
25
26 // 2. Tillämpa en tidsgräns per episod (väggklocka)
27 epCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
28 defer cancel()
29
30 var stdout, stderr bytes.Buffer
31 sb, err := gvx.New(epCtx, gvx.Config{
32 Platform: gvx.PlatformAuto,
33 NetworkMode: gvx.NetModeNAT,
34 NetRaw: true,
35 AllowPacketSocketWrite: true,
36 AllowSUID: true,
37 OCIImage: "docker:dind",
38 Entrypoint: []string{"sh", "-c"},
39 EntrypointSet: true,
40 Cmd: []string{
41 "dockerd-entrypoint.sh --tls=false >/var/log/dockerd.log 2>&1 & " +
42 "until docker info >/dev/null 2>&1; do sleep 0.2; done && " +
43 "docker run --rm python:3.11-slim python -c 'print(6 * 7)'",
44 },
45 Envv: []string{"DOCKER_TLS_CERTDIR="},
46 Mounts: []gvx.Mount{
47 {
48 Kind: gvx.MountVolume,
49 ImagePath: epExt4,
50 GuestPath: "/var/lib/docker",
51 },
52 },
53 Resources: gvx.Resources{
54 MemoryBytes: 4 << 30, // 4 GiB tak
55 CPUMillis: 2000, // 2.0 CPU:er
56 },
57 Stdout: &stdout,
58 Stderr: &stderr,
59 })
60 if err != nil {
61 return fmt.Errorf("init episode %d: %w", epID, err)
62 }
63 defer sb.Close()
64
65 exitCode, err := sb.Run(epCtx)
66 if err != nil {
67 return fmt.Errorf("run episode %d: %w", epID, err)
68 }
69
70 log.Printf("episode %d finished: exit=%d stdout=%q", epID, exitCode, stdout.String())
71 return nil
72}
73
74func main() {
75 ctx := context.Background()
76 if err := runEpisode(ctx, "/srv/rl-snapshots/golden-docker.ext4", "/srv/rl-episodes", 1); err != nil {
77 log.Fatal(err)
78 }
79}
Jämförelse av runtimes för sandlådor för RL-agenter
| Mätvärde / krav | Delad värd-dockerd | Privilegierad docker:dind | Firecracker / KVM MicroVM | Standard-gVisor (runsc) | gvx (rungv / pkg/gvx) |
|---|---|---|---|---|---|
Kräver värdens root eller --privileged | Ja (docker.sock = root) | Ja (--privileged) | Kräver /dev/kvm + TAP-konfiguration | Ja (eller userns + mount) | Nej (en enda oprivilegierad process) |
Fungerar i oprivilegierade K8s-pods / befintlig runsc | Nej | Nej | Nej (ingen /dev/kvm) | Misslyckas (EBUSY vid nästlad seccomp) | Ja (automatisk reservlösning med futex-loop) |
Stöder inre docker build och docker compose | Delar värddaemon | Ja | Ja | Misslyckas utan DinD-justering på värden | Ja (overlayfs på ext4lite) |
| Nätverksisolering per episod | Delat docker0 / xtables-lås | Separat namnrymd | Separat VM-nätverkskort | Separat netns | Isolerat netstack i processen |
Latens för återställning av varm episod (env.reset()) | 5 s till 30 s (beskärning av container/volym) | 10 s till 60 s (kall daemon) | ~150 ms (VM-ögonblicksbild) | 5 s till 20 s | ~100 ms (cp --reflink=always .ext4) |
| Plattformsoberoende lokal utveckling (Windows / macOS / Linux) | Kräver Linux-VM | Kräver Linux-VM | Endast Linux KVM | Endast Linux | Native på Windows, macOS och Linux |
Fler artiklar i denna serie
- gvx: Köra Linux-containrar, Docker och Kubernetes i en enda oprivilegierad process: Fullständig arkitekturgenomgång av
gvxpå Windows (whpx,systrap), macOS (hvf) och Linux (systrap). - Köra Docker, docker build och docker compose utan root eller användarnamnrymder: Snabb tregegsuppstart och felsökningsguide när
sudo,unshare(CLONE_NEWUSER),mount(2)och/dev/fuseär blockerade. - Köra Docker-in-Docker inuti gVisor och nästlade sandlådor med gvx: Fördjupning i varför
docker.iooch nästladrunscmisslyckas inutigVisor(runsc) och hurgvxfaller tillbaka på futex-loopen_RUN_SYSCALL_LOOPvidEBUSY. - Snabba isolerade Docker-miljöer för RL-rollouts med AI-agenter med gvx (detta inlägg)