Mengapa Cloud Streaming Menghapus File Save dan Cara Membangun Penyimpanan Persisten yang Bertahan
Ringkasnya
Pelajari cara membangun penyimpanan save persisten untuk cloud streaming dengan Amazon S3, lengkap dengan skrip produksi dan analisis biaya.
Player Anda bermain selama tiga jam, menyimpan progresnya, menutup streaming, kembali keesokan harinya — dan save-nya hilang. Bukan bug di kode game Anda. Bukan kesalahan pengguna. Instance streaming yang menyimpan file save-nya telah dihentikan dan diganti dengan yang baru, membawa serta setiap byte data lokal.
Ini adalah jebakan arsitektural fundamental dari cloud game streaming. Platform seperti Amazon GameLift Streams mengoptimalkan biaya dengan menggunakan komputasi ephemeral: setiap sesi membuat instance baru, menjalankan binary game Anda, dan menghancurkannya saat sesi berakhir. Bagus untuk utilisasi resource. Buruk untuk game apa pun yang menulis file ke disk. Sistem save Anda bekerja dengan sempurna — game menulis %APPDATA%/YourGame/save.dat persis seperti yang dirancang — tetapi filesystem-nya sendiri bersifat sementara.
Dalam runbook ini, saya akan membahas apa yang rusak, cara mendeteksinya, cara membangun lapisan save persisten menggunakan object storage cloud, dan berapa biaya sebenarnya untuk menjalankannya. Setiap skrip di bawah ini sudah teruji produksi dan siap dipasang ke proyek Anda.
Apa yang Rusak: Siklus Hidup Instance Ephemeral
Berikut siklus hidup sesi cloud streaming yang umum dan di mana kegagalannya terjadi:
- Sesi dimulai — Platform menyediakan instance, mengunggah binary game Anda
- Player terhubung — Game diluncurkan, player mulai bermain
- Game menulis save — File save tersimpan di disk lokal (game tidak tahu disk ini bersifat sementara)
- Sesi berakhir — Player terputus, instance dihentikan atau didaur ulang
- File dihancurkan — Semua file lokal dihapus; sesi berikutnya dimulai dari awal
Titik kegagalan kritis adalah langkah 4→5. Sistem save game Anda melakukan persis apa yang seharusnya. Masalahnya adalah platform di bawahnya memperlakukan filesystem sebagai sesuatu yang sekali pakai. Ini adalah masalah infrastruktur yang menyamar sebagai bug gameplay, dan masalah siklus hidup server seperti ini adalah salah satu alasan paling umum mengapa player meninggalkan game streaming cloud.
Cara Mendeteksi Masalah Ini
Gejalanya spesifik dan berulang:
- Laporan player: "Save saya terus hilang" — tetapi hanya untuk pengguna cloud/streaming, tidak pernah untuk instalasi lokal
- Tidak ada crash log: Game berjalan tanpa masalah; save-nya saja yang tidak ada di peluncuran berikutnya
- Persistensi terbatas sesi: Data bertahan dalam satu sesi tetapi hilang di antara sesi
- Spesifik platform: Hanya memengaruhi instance streaming, bukan build lokal atau dedicated server
Jika laporan bug Anda cocok dengan pola ini, Anda menghadapi masalah instance ephemeral. Tidak ada jumlah debugging sisi game yang akan memperbaikinya — solusinya ada di lapisan infrastruktur.
Arsitektur: Object Storage sebagai Lapisan Persisten
Perbaikannya secara konseptual sederhana: berhenti mengandalkan filesystem lokal instance untuk penyimpanan jangka panjang. Gunakan layanan object storage yang tahan lama (seperti Amazon S3) sebagai lokasi save yang otoritatif. Skrip launcher menangani sinkronisasi secara transparan — kode game Anda tetap tidak berubah.
Alurnya seperti ini:
- Sebelum peluncuran game: Unduh save yang ada dari S3 → filesystem lokal
- Selama gameplay: Pantau file save lokal untuk perubahan → sinkronkan pembaruan ke S3
- Saat sesi berakhir: Sinkronisasi akhir memastikan semua data tersimpan sebelum instance dihentikan
Game Anda tetap menulis ke disk lokal secara normal. Game tidak tahu bahwa launcher mencerminkan tulisan tersebut ke cloud storage. Pendekatan tanpa modifikasi ini berarti Anda dapat memasang save persisten ke game apa pun yang ada tanpa menyentuh satu baris kode game.
Langkah 1: Konfigurasi IAM Role
Anda memerlukan IAM role yang memberikan akses terbatas ke bucket S3 Anda untuk sesi streaming. Role tersebut harus mempercayai layanan streaming dan mengikuti prinsip hak akses paling rendah.
Buat role dengan trust policy ini (ganti [ACCOUNT_ID] dengan ID akun AWS Anda):
{
"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/*"
}
}
}
]
}
Kemudian lampirkan inline policy yang hanya memberikan operasi S3 yang Anda perlukan:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::your-save-bucket/saves/*"
}
]
}
Penting: Batasi Resource ke prefix tertentu seperti /saves/* — jangan pernah ke seluruh bucket. Ini membatasi radius ledakan jika role disusupi dan mengikuti praktik keamanan terbaik. Nama role harus dimulai dengan GameLiftStreams- sesuai persyaratan platform.
Langkah 2: Skrip Launcher (Windows)
Skrip launcher adalah orkestrator. Ia mengunduh save yang ada, meluncurkan game, dan menjalankan file watcher latar belakang untuk menyinkronkan perubahan. Berikut versi batch 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
Kedua environment variable (S3_SAVE_PATH dan LOCAL_SAVE_FILE_PATH) diteruskan melalui layanan backend Anda saat memanggil StartStreamSession. Path S3 harus unik per player — buat dari ID player yang terautentikasi seperti s3://your-bucket/saves/{player-id}/save.dat. Jangan pernah menggunakan session ID; itu berubah di antara sesi dan akan membuat data save menjadi yatim.
Langkah 3: Skrip Unduhan
Skrip PowerShell ini memeriksa S3 untuk file save yang ada dan mengunduhnya jika ditemukan:
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: $_"
}
Untuk player pertama kali yang belum memiliki save, operasi salin S3 akan gagal — itu yang diharapkan. Game akan membuat file save baru, dan watcher akan mengambilnya.
Langkah 4: File Watcher
Ini adalah komponen yang menjaga sinkronisasi save selama gameplay:
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: $_"
}
Langkah 5: Varian Linux/Proton
Untuk instance streaming berbasis Linux, ganti watcher PowerShell dengan 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
Rincian Biaya: Angka Nyata
Mari kita kuantifikasi berapa biaya sebenarnya untuk menjalankan arsitektur ini:
Permintaan PUT S3 (Operasi Tulis)
Jika game Anda auto-save setiap 30 detik selama gameplay, sesi 2 jam yang umum menghasilkan sekitar 240 permintaan PUT. Dengan harga S3 $0,005 per 1.000 permintaan:
- Biaya per sesi: $0,0012
- Biaya per 10.000 sesi player/bulan: $1,20
Permintaan GET S3 (Operasi Baca)
Satu permintaan GET per awal sesi untuk mengunduh save yang ada:
- Biaya per 10.000 sesi: $0,40
Penyimpanan S3
Ukuran save rata-rata: 5 MB per player. Untuk 10.000 player aktif bulanan:
- Total penyimpanan: ~50 GB
- Biaya bulanan pada $0,023/GB: $1,15
Total Biaya Bulanan untuk 10.000 MAU
Di bawah $3/bulan. Ini dapat diabaikan dibandingkan biaya komputasi. Lapisan S3 pada dasarnya gratis di skala indie.
Optimasi Produksi: Sinkronisasi dengan Debounce
File watcher mentah aktif pada setiap penulisan. Game yang auto-save sering (setiap 10-15 detik) akan menghasilkan permintaan PUT yang berlebihan. Tambahkan penundaan debounce — tunggu 5 detik setelah perubahan terakhir sebelum sinkronisasi:
# 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
}
}
Ini mengurangi permintaan PUT sebesar 60-80% dengan risiko kehilangan data yang minimal. Bahkan jika instance dihentikan di tengah debounce, Anda kehilangan paling banyak 5 detik progres — dapat diterima untuk sebagian besar game.
Kasus Tepi dan Mode Kegagalan
File Save Belum Ada (Player Pertama Kali)
Langkah unduhan akan gagal — itu perilaku yang benar. Game membuat save baru, dan file watcher mengambilnya pada penulisan pertama. Tidak perlu penanganan khusus.
Unggahan Rusak karena Gangguan Jaringan
Jika jaringan terputus selama permintaan PUT, objek S3 bisa tidak lengkap. Mitigasi dengan memverifikasi ETag setelah unggahan:
# 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
}
Banyak Slot Save
Jika game Anda mendukung banyak file save, pantau direktori alih-alih satu file. Setel $watcher.Filter ke * dan sinkronkan seluruh direktori save. Untuk jumlah file yang banyak, arsipkan ke satu .zip sebelum mengunggah.
Batas Ukuran File Save
S3 mendukung objek hingga 5 TB, jadi ukuran file bukan masalah praktis. Sebagian besar save game berkisar dari 500 KB hingga 50 MB. Jika Anda berurusan dengan dunia yang dihasilkan secara prosedural dengan state besar (>500 MB), pertimbangkan kompresi dengan gzip sebelum unggah — data save umumnya terkompresi menjadi 20-40% dari ukuran aslinya.
Player Bermain di Banyak Perangkat
Jika player melakukan streaming dari perangkat berbeda di hari yang sama, save dari sesi terakhir akan menimpa yang sebelumnya. Untuk sebagian besar game single-player, ini adalah perilaku yang benar. Untuk game yang membutuhkan semantik penggabungan (seperti inventory yang disinkronkan cloud), Anda memerlukan logika resolusi konflik — di sinilah layanan backend khusus menjadi penting.
Praktik Terbaik
Batasi IAM role ke prefix S3 tertentu — Gunakan
arn:aws:s3:::your-bucket/saves/*alih-alih izin seluruh bucket. Ini mengikuti prinsip hak akses paling rendah dan membatasi kerusakan jika kredensial bocor. Nama role harus dimulai denganGameLiftStreams-untuk memenuhi persyaratan platform.Buat kunci S3 per player dari identitas terautentikasi — Gunakan
saves/{platform-user-id}/save.dat, jangan pernahsaves/{session-id}/save.dat. Session ID berubah di antara sesi dan akan membuat data save yatim secara permanen.Terapkan sinkronisasi debounce di produksi — File watcher mentah menghasilkan permintaan PUT pada setiap penyimpanan. Jendela debounce 5 detik memotong panggilan API sebesar 60-80% sambil menjaga risiko kehilangan data di bawah satu interval auto-save.
Catat setiap operasi sinkronisasi — Kegagalan penyimpanan tidak terlihat oleh player sampai mereka kehilangan berjam-jam progres. Pola
$LogFiledalam skrip di atas memberi Anda jejak audit persisten di instance. Kirim log ini ke sistem monitoring Anda untuk alerting proaktif.Uji dengan penghentian instance, bukan hanya pemutusan koneksi — Matikan instance streaming di tengah permainan untuk memverifikasi bahwa jendela sinkronisasi debounce Anda dapat diterima. Pemutusan yang mulus memberi waktu sinkronisasi akhir untuk selesai; penghentian mendadak tidak.
Jalan Pintas BaaS: Kapan Membangun Ini Sendiri Tidak Sebanding
Arsitektur di atas berfungsi — sudah teruji di lapangan dan hampir tidak memakan biaya untuk dijalankan. Tetapi ini mengharuskan Anda mengelola IAM role, skrip launcher, file watcher, pemeriksaan integritas, varian Linux, dan konfigurasi per lingkungan. Untuk tim kecil, itu 2-4 hari kerja infrastruktur yang tidak ada hubungannya dengan game Anda yang sebenarnya.
horizOn menyediakan penyimpanan data player persisten sebagai layanan terkelola — file save, inventory, progres, preferensi — dengan satu panggilan API. Tanpa konfigurasi IAM, tanpa skrip launcher, tanpa file watcher, tanpa verifikasi integritas. Backend menangani autentikasi, resolusi konflik, dan redundansi lintas platform secara otomatis. Untuk tim yang ingin merilis game alih-alih men-debug infrastruktur, ini menghilangkan seluruh kategori bug "berfungsi lokal, rusak di streaming". Kami mendokumentasikan proses integrasi lengkap dalam panduan pembaruan backend terbaru kami.
Ringkasan
Save game persisten di cloud streaming memerlukan perlakuan terhadap filesystem instance sebagai sesuatu yang ephemeral dan penggunaan object storage yang tahan lama sebagai sumber kebenaran. Arsitektur lengkapnya:
- IAM role memberikan akses baca/tulis S3 terbatas untuk sesi streaming
- Skrip launcher mengunduh save yang ada sebelum peluncuran game
- File watcher latar belakang menyinkronkan perubahan ke S3 selama gameplay dengan penulisan debounce
- Layanan backend membuat path S3 per player dari identitas player yang terautentikasi
- Pemeriksaan integritas memverifikasi kebenaran unggah/unduh pada setiap sinkronisasi
Biaya di skala indie di bawah $3/bulan untuk 10.000 player aktif. Waktu implementasi 1-2 hari jika Anda membangunnya sendiri, atau 30 menit jika Anda menggunakan horizOn API data player.
Bagaimanapun, jangan merilis game cloud streaming tanpa menyelesaikan masalah ini. Player Anda tidak akan memberi tahu Anda bahwa save mereka hilang — mereka hanya akan berhenti bermain.
Sumber: Menambahkan save game persisten ke Amazon GameLift Streams