Dlaczego cloud streaming usuwa pliki zapisu i jak zbudować trwały storage, który przetrwa
W skrócie
Dowiedz się, dlaczego cloud streaming usuwa pliki zapisu i jak zbudować trwały storage, który przetrwa dzięki S3 — z gotowymi skryptami i kosztami.
Twój gracz grinduje przez trzy godziny, zapisuje postępy, zamyka strumień, wraca następnego dnia — a jego zapis zniknął. To nie bug w kodzie gry. To nie błąd użytkownika. Instancja strumieniowania, która przechowywała plik zapisu, została zakończona i zastąpiona nową, zabierając ze sobą każdy bajt lokalnych danych.
To jest fundamentalna pułapka architektoniczna cloud gamingu. Platformy takie jak Amazon GameLift Streams optymalizują koszty, używając efemerycznych zasobów obliczeniowych: każda sesja uruchamia nową instancję, odpala binarkę gry i usuwa ją po zakończeniu sesji. Świetnie dla wykorzystania zasobów. Fatalnie dla każdej gry, która zapisuje pliki na dysku. Twój system zapisu działa idealnie — gra zapisuje %APPDATA%/YourGame/save.dat dokładnie tak, jak zaprojektowano — ale sam system plików jest tymczasowy.
W tym runbooku pokażę dokładnie, co się psuje, jak to wykryć, jak zbudować trwałą warstwę zapisu przy użyciu cloud object storage i ile to naprawdę kosztuje. Każdy skrypt poniżej jest przetestowany w produkcji i gotowy do wdrożenia w twoim projekcie.
Co się psuje: cykl życia efemerycznej instancji
Oto cykl życia typowej sesji cloud streamingu i miejsce, w którym następuje awaria:
- Sesja startuje — Platforma provisionuje instancję, wgrywa binarkę gry
- Gracz się łączy — Gra się uruchamia, gracz zaczyna grać
- Gra zapisuje sejwy — Pliki zapisu trafiają na lokalny dysk (gra nie ma pojęcia, że ten dysk jest tymczasowy)
- Sesja się kończy — Gracz się rozłącza, instancja jest kończona lub recyklingowana
- Pliki zniszczone — Wszystkie lokalne pliki są kasowane; następna sesja startuje od zera
Krytyczny punkt awarii to krok 4→5. System zapisu twojej gry robi dokładnie to, co powinien. Problem polega na tym, że platforma pod spodem traktuje system plików jako jednorazowy. To problem infrastrukturalny udający buga rozgrywki, a problemy z cyklem życia serwerów to jeden z najczęstszych powodów, dla których gracze porzucają tytuły w cloud streamingu.
Jak wykryć ten problem
Objawy są konkretne i powtarzalne:
- Zgłoszenia graczy: „Mój zapis ciągle znika" — ale tylko u użytkowników cloud/streaming, nigdy przy instalacjach lokalnych
- Brak logów crashy: Gra działa bez zarzutu; zapisów po prostu nie ma przy następnym uruchomieniu
- Trwałość ograniczona do sesji: Dane przetrwają w obrębie jednej sesji, ale znikają między sesjami
- Specyficzne dla platformy: Dotyczy tylko instancji streamingowych, nie buildów lokalnych ani dedicated server
Jeśli twoje zgłoszenia bugów pasują do tego wzorca, masz problem efemerycznej instancji. Żadna ilość debugowania po stronie gry tego nie naprawi — rozwiązanie leży w warstwie infrastruktury.
Architektura: object storage jako trwała warstwa
Naprawa jest koncepcyjnie prosta: przestań polegać na lokalnym systemie plików instancji w przypadku długoterminowego przechowywania. Użyj trwałego serwisu object storage (takiego jak Amazon S3) jako autorytatywnego miejsca zapisu. Skrypt launcher obsługuje synchronizację w sposób przezroczysty — kod twojej gry pozostaje bez zmian.
Przepływ wygląda tak:
- Przed uruchomieniem gry: Pobierz istniejący zapis z S3 → lokalny system plików
- Podczas rozgrywki: Monitoruj lokalny plik zapisu pod kątem zmian → synchronizuj aktualizacje do S3
- Na koniec sesji: Finalna synchronizacja zapewnia utrwalenie wszystkich danych przed zniszczeniem instancji
Twoja gra nadal zapisuje normalnie na lokalny dysk. Nie ma pojęcia, że launcher lustruje te zapisy do chmury. To podejście zero-modification oznacza, że możesz dodać trwałe zapisy do dowolnej istniejącej gry bez dotykania ani jednej linijki kodu gry.
Krok 1: Skonfiguruj rolę IAM
Potrzebujesz roli IAM, która daje sesjom streamingowym ograniczony dostęp do twojego bucketa S3. Rola musi ufać serwisowi streamingowemu i przestrzegać zasad least-privilege.
Utwórz rolę z tą polityką zaufania (zastąp [ACCOUNT_ID] swoim ID konta AWS):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "gameliftstreams.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "[ACCOUNT_ID]"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:gameliftstreams:*:[ACCOUNT_ID]:streamsession/*"
}
}
}
]
}
Następnie dołącz politykę inline przyznającą tylko operacje S3, których potrzebujesz:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::your-save-bucket/saves/*"
}
]
}
Krytyczne: Ogranicz Resource do konkretnego prefiksu, np. /saves/* — nigdy do całego bucketa. To ogranicza promień rażenia w przypadku kompromitacji roli i jest zgodne z najlepszymi praktykami bezpieczeństwa. Nazwa roli musi zaczynać się od GameLiftStreams- zgodnie z wymaganiami platformy.
Krok 2: Skrypt launcher (Windows)
Skrypt launcher jest orkiestratorem. Pobiera istniejące zapisy, uruchamia grę i odpala w tle obserwatora plików, który synchronizuje zmiany. Oto wersja batch dla Windows:
@echo off
setlocal
set SCRIPT_DIR=%~dp0
if not defined LOCAL_SAVE_FILE_PATH (
echo WARNING: LOCAL_SAVE_FILE_PATH not set, skipping save sync
goto :start_app
)
if not defined S3_SAVE_PATH (
echo WARNING: S3_SAVE_PATH not set, skipping save sync
goto :start_app
)
set LOG_FILE=%SCRIPT_DIR%save_sync.log
set REGION_ARG=
if defined AWS_REGION set REGION_ARG=-Region "%AWS_REGION%"
REM Download existing save from S3
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%download-save.ps1" ^
-S3Path "%S3_SAVE_PATH%" -LocalPath "%LOCAL_SAVE_FILE_PATH%" ^
-LogFile "%LOG_FILE%" %REGION_ARG%
REM Start background file watcher
start "" /b powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass ^
-File "%SCRIPT_DIR%watch-save.ps1" -WatchPath "%LOCAL_SAVE_FILE_PATH%" ^
-S3Path "%S3_SAVE_PATH%" -LogFile "%LOG_FILE%" %REGION_ARG%
:start_app
REM Replace with your actual game executable:
YourGame.exe -f
Dwie zmienne środowiskowe (S3_SAVE_PATH i LOCAL_SAVE_FILE_PATH) są przekazywane przez twój backend, gdy wywołuje StartStreamSession. Ścieżka S3 musi być unikalna dla gracza — zbuduj ją z uwierzytelnionego ID gracza, np. s3://your-bucket/saves/{player-id}/save.dat. Nigdy nie używaj ID sesji; te zmieniają się między sesjami i osierociłyby dane zapisu.
Krok 3: Skrypt pobierania
Ten skrypt PowerShell sprawdza S3 pod kątem istniejącego pliku zapisu i pobiera go, jeśli istnieje:
param(
[Parameter(Mandatory=$true)][string]$S3Path,
[Parameter(Mandatory=$true)][string]$LocalPath,
[Parameter(Mandatory=$true)][string]$LogFile,
[Parameter(Mandatory=$false)][string]$Region
)
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LogFile -Value "$timestamp - Checking for save at: $S3Path"
$regionArgs = @()
if (-not [string]::IsNullOrWhiteSpace($Region)) {
$regionArgs = @('--region', $Region)
}
# Ensure local directory exists
$localDir = Split-Path $LocalPath -Parent
if (-not (Test-Path $localDir)) {
New-Item -ItemType Directory -Path $localDir -Force | Out-Null
}
try {
$result = & aws s3 cp $S3Path $LocalPath @regionArgs 2>&1
if ($LASTEXITCODE -eq 0) {
Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Downloaded save from S3"
} else {
Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - No existing save found (first session?)"
}
} catch {
Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Download error: $_"
}
Dla graczy grających pierwszy raz, bez istniejącego zapisu, kopiowanie z S3 zakończy się błędem — to oczekiwane. Gra utworzy nowy plik zapisu, a obserwator go wychwyci.
Krok 4: Obserwator plików
To komponent, który utrzymuje synchronizację zapisów podczas rozgrywki:
param(
[Parameter(Mandatory=$true)][string]$WatchPath,
[Parameter(Mandatory=$true)][string]$S3Path,
[Parameter(Mandatory=$true)][string]$LogFile,
[Parameter(Mandatory=$false)][string]$Region
)
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LogFile -Value "$timestamp - Watching for changes: $WatchPath"
$regionArgs = @()
if (-not [string]::IsNullOrWhiteSpace($Region)) {
$regionArgs = @('--region', $Region)
}
$folder = Split-Path $WatchPath -Parent
$fileName = Split-Path $WatchPath -Leaf
if (-not (Test-Path $folder)) {
New-Item -ItemType Directory -Path $folder -Force | Out-Null
}
try {
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $folder
$watcher.Filter = $fileName
$watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite -bor `
[System.IO.NotifyFilters]::Size
$watcher.EnableRaisingEvents = $true
while ($true) {
$change = $watcher.WaitForChanged(
[System.IO.WatcherChangeTypes]::All, 1000
)
if (-not $change.TimedOut) {
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LogFile -Value "$ts - Change detected: $($change.ChangeType)"
try {
$s3Result = & aws s3 cp $WatchPath $S3Path @regionArgs 2>&1
if ($LASTEXITCODE -eq 0) {
Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Synced to S3"
} else {
Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Sync failed: $s3Result"
}
} catch {
Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Sync error: $_"
}
}
}
} catch {
Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Watcher crashed: $_"
}
Krok 5: Wariant Linux/Proton
Dla instancji streamingowych opartych na Linuksie zastąp obserwatora PowerShell narzędziem inotifywait:
#!/bin/bash
# watch-save.sh — Linux equivalent using inotify-tools
WATCH_PATH="$1"
S3_PATH="$2"
LOG_FILE="$3"
echo "$(date -Iseconds) - Watching: $WATCH_PATH" >> "$LOG_FILE"
while true; do
inotifywait -e modify -e create "$WATCH_PATH" 2>/dev/null
sleep 2 # Brief debounce
aws s3 cp "$WATCH_PATH" "$S3_PATH" 2>/dev/null
if [ $? -eq 0 ]; then
echo "$(date -Iseconds) - Synced to S3" >> "$LOG_FILE"
else
echo "$(date -Iseconds) - Sync failed" >> "$LOG_FILE"
fi
done
Analiza kosztów: konkretne liczby
Policzmy, ile naprawdę kosztuje utrzymanie tej architektury:
Żądania PUT S3 (operacje zapisu)
Jeśli twoja gra robi autosave co 30 sekund podczas rozgrywki, typowa 2-godzinna sesja generuje około 240 żądań PUT. Przy cenniku S3 wynoszącym 0,005 USD za 1 000 żądań:
- Koszt na sesję: 0,0012 USD
- Koszt na 10 000 sesji graczy miesięcznie: 1,20 USD
Żądania GET S3 (operacje odczytu)
Jedno żądanie GET na start sesji w celu pobrania istniejących zapisów:
- Koszt na 10 000 sesji: 0,40 USD
Pamięć S3
Średni plik zapisu: 5 MB na gracza. Dla 10 000 aktywnych graczy miesięcznie:
- Łączna pamięć: ~50 GB
- Koszt miesięczny przy 0,023 USD/GB: 1,15 USD
Łączny koszt miesięczny dla 10 000 MAU
Poniżej 3 USD miesięcznie. To wartość pomijalna w porównaniu z kosztami obliczeniowymi. Warstwa S3 jest praktycznie darmowa w skali indie.
Optymalizacja produkcyjna: synchronizacja z debounce
Surowy obserwator plików odpala się przy każdym zapisie. Gra, która robi autosave często (co 10-15 sekund), będzie generować nadmierną liczbę żądań PUT. Dodaj opóźnienie debounce — poczekaj 5 sekund po ostatniej zmianie przed synchronizacją:
# Debounce logic — add to the change handler in watch-save.ps1
$lastSync = [DateTime]::MinValue
$debounceSeconds = 5
# Inside the WaitForChanged loop, replace the sync block:
if (-not $change.TimedOut) {
$now = Get-Date
if (($now - $lastSync).TotalSeconds -ge $debounceSeconds) {
& aws s3 cp $WatchPath $S3Path @regionArgs 2>&1
$lastSync = $now
}
}
To redukuje liczbę żądań PUT o 60-80% przy minimalnym ryzyku utraty danych. Nawet jeśli instancja zostanie zakończona w trakcie debounce, tracisz maksymalnie 5 sekund postępów — akceptowalne dla większości gier.
Przypadki brzegowe i tryby awarii
Plik zapisu jeszcze nie istnieje (gracze pierwszorazowi)
Krok pobierania zakończy się błędem — to poprawne zachowanie. Gra tworzy nowy zapis, a obserwator plików wychwytuje go przy pierwszym zapisie. Nie jest potrzebna żadna specjalna obsługa.
Uszkodzony upload w wyniku przerwania sieci
Jeśli sieć zrywa się podczas żądania PUT, obiekt S3 może być niekompletny. Można to ograniczyć, weryfikując ETag po uploadzie:
# After syncing, verify upload integrity
$localHash = (Get-FileHash -Path $WatchPath -Algorithm MD5).Hash.ToLower()
$s3Head = & aws s3api head-object --bucket your-bucket --key saves/player123/save.dat 2>&1
$s3ETag = ($s3Head | ConvertFrom-Json).ETag.Trim('"')
if ($localHash -ne $s3ETag) {
Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - INTEGRITY MISMATCH - re-uploading"
& aws s3 cp $WatchPath $S3Path @regionArgs 2>&1
}
Wiele slotów zapisu
Jeśli twoja gra obsługuje wiele plików zapisu, obserwuj katalog zamiast pojedynczego pliku. Ustaw $watcher.Filter na * i synchronizuj cały katalog zapisów. Przy dużej liczbie plików spakuj je do jednego .zip przed wysłaniem.
Limity rozmiaru pliku zapisu
S3 obsługuje obiekty do 5 TB, więc rozmiar pliku nie jest praktycznym problemem. Większość zapisów gier ma od 500 KB do 50 MB. Jeśli masz do czynienia z proceduralnie generowanymi światami z ogromnym stanem (>500 MB), rozważ kompresję gzip przed wysłaniem — typowe dane zapisu kompresują się do 20-40% pierwotnego rozmiaru.
Gracz gra na wielu urządzeniach
Jeśli gracz streamuje z różnych urządzeń tego samego dnia, zapis z ostatniej sesji nadpisuje poprzedni. Dla większości gier single-player to poprawne zachowanie. W grach wymagających semantyki scalania (jak synchronizowany w chmurze ekwipunek) potrzebna będzie logika rozwiązywania konfliktów — i tu dedykowany backend staje się niezbędny.
Najlepsze praktyki
Ogranicz role IAM do konkretnych prefiksów S3 — Użyj
arn:aws:s3:::your-bucket/saves/*zamiast uprawnień obejmujących cały bucket. To zgodne z zasadą least-privilege i ogranicza szkody w przypadku wycieku poświadczeń. Nazwa roli musi zaczynać się odGameLiftStreams-, aby spełnić wymagania platformy.Buduj klucze S3 per gracz na podstawie uwierzytelnionej tożsamości — Użyj
saves/{platform-user-id}/save.dat, nigdysaves/{session-id}/save.dat. ID sesji zmieniają się między sesjami i trwale osierociłyby dane zapisu.Wdróż synchronizację z debounce w produkcji — Surowe obserwatory systemu plików generują żądanie PUT przy każdym zapisie. Okno debounce wynoszące 5 sekund ogranicza wywołania API o 60-80%, utrzymując ryzyko utraty danych poniżej jednego interwału autosave.
Loguj każdą operację synchronizacji — Błędy zapisu są niewidoczne dla graczy, dopóki nie stracą godzin postępów. Wzorzec
$LogFilew skryptach powyżej daje trwały ślad audytowy na instancji. Wysyłaj te logi do systemu monitorowania, aby otrzymywać proaktywne alerty.Testuj przez zakończenie instancji, nie tylko rozłączenie — Zabij instancję streamingową w trakcie gry, aby zweryfikować, czy okno synchronizacji z debounce jest akceptowalne. Łagodne rozłączenie daje finalnej synchronizacji czas na zakończenie; nagłe zakończenie już nie.
Skrót przez BaaS: kiedy budowanie tego samemu nie ma sensu
Opisana architektura działa — jest sprawdzona w boju i kosztuje prawie nic w utrzymaniu. Wymaga jednak zarządzania rolami IAM, skryptami launcher, obserwatorami plików, kontrolami integralności, wariantami Linuksa i konfiguracją per środowisko. Dla małego zespołu to 2-4 dni pracy nad infrastrukturą, która nie ma nic wspólnego z samą grą.
horizOn zapewnia trwałe przechowywanie danych gracza jako usługę zarządzaną — pliki zapisu, ekwipunek, postępy, preferencje — za pomocą jednego wywołania API. Bez konfiguracji IAM, bez skryptów launcher, bez obserwatorów plików, bez weryfikacji integralności. Backend automatycznie obsługuje uwierzytelnianie, rozwiązywanie konfliktów i redundancję międzyplatformową. Dla zespołów, które chcą wydawać grę zamiast debugować infrastrukturę, eliminuje całą kategorię bugów „działa lokalnie, psuje się w streamingu". Pełny proces integracji opisaliśmy w naszym omówieniu aktualizacji backendu.
Podsumowanie
Trwałe zapisy w cloud streamingu wymagają traktowania systemu plików instancji jako efemerycznego i używania trwałego object storage jako źródła prawdy. Kompletna architektura:
- Rola IAM daje sesjom streamingowym ograniczony dostęp do odczytu/zapisu S3
- Skrypt launcher pobiera istniejące zapisy przed uruchomieniem gry
- Obserwator plików w tle synchronizuje zmiany do S3 podczas rozgrywki z zapisami z debounce
- Backend buduje ścieżki S3 per gracz na podstawie uwierzytelnionej tożsamości
- Kontrole integralności weryfikują poprawność uploadu/downloadu przy każdej synchronizacji
Koszt w skali indie to poniżej 3 USD miesięcznie dla 10 000 aktywnych graczy. Czas wdrożenia to 1-2 dni, jeśli budujesz to sam, albo 30 minut, jeśli użyjesz API danych graczy horizOn.
Tak czy inaczej, nie wydawaj gry w cloud streamingu bez rozwiązania tego problemu. Twoi gracze nie powiedzą ci, że ich zapisy znikają — po prostu przestaną grać.
Źródło: Dodawanie trwałych zapisów gier do Amazon GameLift Streams