Retour au Blog

Pourquoi le streaming cloud supprime les sauvegardes et comment créer un stockage persistant qui survit

Publié le 7 août 2026
Pourquoi le streaming cloud supprime les sauvegardes et comment créer un stockage persistant qui survit Généré avec l'aide de l'IA

En bref

Découvrez comment éviter la perte des sauvegardes dans le cloud streaming grâce à un stockage persistant Amazon S3 et aux scripts de synchronisation.

Votre joueur passe trois heures à farmer, sauvegarde sa progression, ferme le stream, revient le lendemain — et sa sauvegarde a disparu. Pas un bug dans votre code de jeu. Pas une erreur utilisateur. L'instance de streaming qui contenait son fichier de sauvegarde a été terminée et remplacée par une nouvelle, emportant avec elle chaque octet de données locales.

C'est le piège architectural fondamental du cloud gaming. Des plateformes comme Amazon GameLift Streams optimisent les coûts en utilisant du calcul éphémère : chaque session démarre une nouvelle instance, exécute votre binaire de jeu, puis la détruit à la fin de la session. Parfait pour l'utilisation des ressources. Désastreux pour tout jeu qui écrit des fichiers sur le disque. Votre système de sauvegarde fonctionne parfaitement — le jeu écrit %APPDATA%/YourGame/save.dat exactement comme prévu — mais le système de fichiers lui-même est temporaire.

Dans ce guide pratique, je vais vous montrer exactement ce qui casse, comment le détecter, comment construire une couche de sauvegarde persistante avec le stockage d'objets cloud, et ce que ça coûte réellement. Chaque script ci-dessous est testé en production et prêt à être intégré dans votre projet.

Ce qui casse : le cycle de vie des instances éphémères

Voici le cycle de vie d'une session de streaming cloud typique et le point où tout se dégrade :

  1. La session démarre — La plateforme provisionne une instance et téléverse votre binaire de jeu
  2. Le joueur se connecte — Le jeu se lance, le joueur commence à jouer
  3. Le jeu écrit les sauvegardes — Les fichiers arrivent sur le disque local (le jeu ignore que ce disque est temporaire)
  4. La session se termine — Le joueur se déconnecte, l'instance est terminée ou recyclée
  5. Les fichiers sont détruits — Toutes les données locales sont effacées ; la session suivante repart de zéro

Le point de défaillance critique est l'étape 4→5. Le système de sauvegarde de votre jeu fait exactement ce qu'il doit. Le problème, c'est que la plateforme sous-jacente traite le système de fichiers comme jetable. C'est un problème d'infrastructure déguisé en bug de gameplay, et ce genre de problèmes de cycle de vie serveur est l'une des raisons les plus courantes pour lesquelles les joueurs abandonnent les titres streamés en cloud.

Comment détecter ce problème

Les symptômes sont précis et reproductibles :

  • Le joueur signale : « Ma sauvegarde ne cesse de disparaître » — mais uniquement pour les utilisateurs cloud/streaming, jamais en installation locale
  • Aucun crash log : Le jeu tourne parfaitement ; les sauvegardes sont simplement absentes au lancement suivant
  • Persistance limitée à la session : Les données survivent pendant une session mais disparaissent entre les sessions
  • Spécifique à la plateforme : Ne touche que les instances de streaming, pas les builds locales ou serveur dédié

Si vos rapports de bug correspondent à ce schéma, vous avez le problème des instances éphémères. Aucun débogage côté jeu ne le résoudra — la solution se trouve dans la couche d'infrastructure.

L'architecture : le stockage d'objets comme couche persistante

Le correctif est conceptuellement simple : arrêtez de vous reposer sur le système de fichiers local de l'instance pour le stockage à long terme. Utilisez un service de stockage d'objets durable (comme Amazon S3) comme emplacement de sauvegarde faisant autorité. Un script de lancement gère la synchronisation de manière transparente — le code de votre jeu reste inchangé.

Le flux ressemble à ceci :

  1. Avant le lancement du jeu : Télécharger la sauvegarde existante depuis S3 → système de fichiers local
  2. Pendant le gameplay : Surveiller le fichier de sauvegarde local pour détecter les changements → synchroniser les mises à jour vers S3
  3. À la fin de la session : Une synchronisation finale garantit que toutes les données sont persistées avant la destruction de l'instance

Votre jeu écrit toujours normalement sur le disque local. Il n'a aucune idée que le launcher reflète ces écritures vers le stockage cloud. Cette approche sans modification vous permet d'ajouter des sauvegardes persistantes à n'importe quel jeu existant sans toucher une seule ligne de code de jeu.

Étape 1 : Configurer le rôle IAM

Vous avez besoin d'un rôle IAM qui accorde aux sessions de streaming un accès limité à votre bucket S3. Le rôle doit faire confiance au service de streaming et respecter le principe du moindre privilège.

Créez un rôle avec cette politique de confiance (remplacez [ACCOUNT_ID] par votre identifiant de compte 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/*"
        }
      }
    }
  ]
}

Ensuite, attachez une politique inline n'accordant que les opérations S3 dont vous avez besoin :

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::your-save-bucket/saves/*"
    }
  ]
}

Point critique : Limitez la Resource à un préfixe spécifique comme /saves/* — jamais à la totalité du bucket. Cela limite le rayon d'explosion si le rôle est compromis et respecte les bonnes pratiques de sécurité. Le nom du rôle doit commencer par GameLiftStreams- conformément aux exigences de la plateforme.

Étape 2 : Le script de lancement (Windows)

Le script de lancement est l'orchestrateur. Il télécharge les sauvegardes existantes, lance le jeu et exécute un observateur de fichiers en arrière-plan pour synchroniser les changements. Voici la version 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

Les deux variables d'environnement (S3_SAVE_PATH et LOCAL_SAVE_FILE_PATH) sont transmises via votre service backend lorsqu'il appelle StartStreamSession. Le chemin S3 doit être unique par joueur — construisez-le à partir d'un identifiant de joueur authentifié, par exemple s3://your-bucket/saves/{player-id}/save.dat. N'utilisez jamais un identifiant de session ; ceux-ci changent entre les sessions et laisseraient les sauvegardes orphelines.

Étape 3 : Le script de téléchargement

Ce script PowerShell vérifie sur S3 si un fichier de sauvegarde existe et le télécharge le cas échéant :

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: $_"
}

Pour les joueurs qui n'ont pas encore de sauvegarde, la copie S3 échouera — c'est normal. Le jeu créera un nouveau fichier de sauvegarde et l'observateur le prendra en charge.

Étape 4 : L'observateur de fichiers

C'est le composant qui maintient les sauvegardes synchronisées pendant la session de jeu :

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: $_"
}

Étape 5 : Variante Linux/Proton

Pour les instances de streaming Linux, remplacez l'observateur PowerShell par 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

Détail des coûts : des chiffres concrets

Quantifions ce que cette architecture coûte réellement en exploitation :

Requêtes PUT S3 (opérations d'écriture)

Si votre jeu sauvegarde automatiquement toutes les 30 secondes pendant la session, une session typique de 2 heures génère environ 240 requêtes PUT. Avec un tarif S3 de 0,005 $ pour 1 000 requêtes :

  • Coût par session : 0,0012 $
  • Coût pour 10 000 sessions joueur/mois : 1,20 $

Requêtes GET S3 (opérations de lecture)

Une requête GET par démarrage de session pour télécharger les sauvegardes existantes :

  • Coût pour 10 000 sessions : 0,40 $

Stockage S3

Taille moyenne des sauvegardes : 5 Mo par joueur. Pour 10 000 joueurs actifs mensuels :

  • Stockage total : ~50 Go
  • Coût mensuel à 0,023 $/Go : 1,15 $

Coût mensuel total pour 10 000 MAU

Moins de 3 $/mois. C'est négligeable par rapport aux coûts de calcul. La couche S3 est essentiellement gratuite à l'échelle indie.

Optimisation en production : synchronisation avec anti-rebond

L'observateur de fichiers brut se déclenche à chaque écriture. Un jeu qui sauvegarde fréquemment (toutes les 10 à 15 secondes) générera des requêtes PUT excessives. Ajoutez un délai de debounce — attendez 5 secondes après le dernier changement avant de synchroniser :

# 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
    }
}

Cela réduit les requêtes PUT de 60 à 80 % avec un risque de perte de données minimal. Même si l'instance est terminée pendant le debounce, vous perdez au plus 5 secondes de progression — acceptable pour la plupart des jeux.

Cas limites et modes de défaillance

Le fichier de sauvegarde n'existe pas encore (première session)

L'étape de téléchargement échouera — c'est le comportement attendu. Le jeu crée une nouvelle sauvegarde et l'observateur la détecte lors de la première écriture. Aucun traitement spécial nécessaire.

Téléversement corrompu suite à une interruption réseau

Si le réseau est coupé pendant une requête PUT, l'objet S3 peut être incomplet. Atténuez ce risque en vérifiant l'ETag après le téléversement :

# 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
}

Plusieurs emplacements de sauvegarde

Si votre jeu prend en charge plusieurs fichiers de sauvegarde, surveillez un répertoire plutôt qu'un seul fichier. Définissez $watcher.Filter sur * et synchronisez tout le répertoire de sauvegardes. Pour un grand nombre de fichiers, archivez-les dans un seul .zip avant le téléversement.

Limites de taille des fichiers de sauvegarde

S3 prend en charge des objets jusqu'à 5 To, donc la taille des fichiers n'est pas un souci pratique. La plupart des sauvegardes de jeu vont de 500 Ko à 50 Mo. Si vous travaillez avec des mondes générés procéduralement et un état massif (>500 Mo), envisagez une compression gzip avant le téléversement — les données de sauvegarde typiques se compressent à 20-40 % de leur taille d'origine.

Le joueur joue sur plusieurs appareils

Si un joueur stream depuis différents appareils le même jour, la sauvegarde de la dernière session écrase la précédente. Pour la plupart des jeux solo, c'est le comportement correct. Pour les jeux qui nécessitent des sémantiques de fusion (comme un inventaire synchronisé dans le cloud), vous aurez besoin d'une logique de résolution de conflits — c'est là qu'un service backend dédié devient essentiel.

Bonnes pratiques

  1. Limiter les rôles IAM à des préfixes S3 spécifiques — Utilisez arn:aws:s3:::your-bucket/saves/* plutôt que des permissions sur tout le bucket. Cela respecte le moindre privilège et limite les dégâts en cas de fuite d'identifiants. Le nom du rôle doit commencer par GameLiftStreams- pour satisfaire aux exigences de la plateforme.

  2. Construire les clés S3 par joueur à partir de l'identité authentifiée — Utilisez saves/{platform-user-id}/save.dat, jamais saves/{session-id}/save.dat. Les identifiants de session changent entre les sessions et rendraient les sauvegardes définitivement orphelines.

  3. Mettre en place une synchronisation avec debounce en production — Les observateurs bruts du système de fichiers génèrent une requête PUT à chaque sauvegarde. Une fenêtre de debounce de 5 secondes réduit les appels API de 60 à 80 % tout en maintenant le risque de perte de données sous un intervalle de sauvegarde automatique.

  4. Enregistrer chaque opération de synchronisation — Les échecs de sauvegarde sont invisibles pour les joueurs jusqu'à ce qu'ils perdent des heures de progression. Le modèle $LogFile dans les scripts ci-dessus vous donne une piste d'audit persistante sur l'instance. Envoyez ces logs à votre système de surveillance pour des alertes proactives.

  5. Tester avec une terminaison d'instance, pas seulement une déconnexion — Tuez l'instance de streaming en pleine partie pour vérifier que votre fenêtre de debounce est acceptable. Une déconnexion propre laisse le temps à la synchronisation finale de s'achever ; une terminaison brutale, non.

Le raccourci BaaS : quand tout construire soi-même n'en vaut pas la peine

L'architecture ci-dessus fonctionne — elle est éprouvée et ne coûte presque rien à exploiter. Mais elle vous oblige à gérer des rôles IAM, des scripts de lancement, des observateurs de fichiers, des vérifications d'intégrité, des variantes Linux et une configuration par environnement. Pour une petite équipe, c'est 2 à 4 jours de travail d'infrastructure qui n'ont rien à voir avec votre jeu lui-même.

horizOn fournit un stockage persistant des données joueur en tant que service géré — fichiers de sauvegarde, inventaire, progression, préférences — avec un seul appel API. Pas de configuration IAM, pas de scripts de lancement, pas d'observateurs de fichiers, pas de vérification d'intégrité. Le backend gère automatiquement l'authentification, la résolution de conflits et la redondance multiplateforme. Pour les équipes qui veulent livrer plutôt que déboguer de l'infrastructure, cela élimine toute une catégorie de bugs « ça marche en local, ça casse en streaming ». Nous avons documenté le processus d'intégration complet dans notre guide pas à pas de la récente mise à jour backend.

Récapitulatif

Des sauvegardes persistantes dans le cloud streaming exigent de traiter le système de fichiers de l'instance comme éphémère et d'utiliser le stockage d'objets durable comme source de vérité. L'architecture complète :

  1. Le rôle IAM accorde aux sessions de streaming un accès S3 en lecture/écriture limité
  2. Le script de lancement télécharge les sauvegardes existantes avant le lancement du jeu
  3. L'observateur de fichiers en arrière-plan synchronise les changements vers S3 pendant la session avec des écritures en debounce
  4. Le service backend construit des chemins S3 par joueur à partir de l'identité authentifiée
  5. Les vérifications d'intégrité confirment la validité des téléversements/téléchargements à chaque synchronisation

Le coût à l'échelle indie est inférieur à 3 $/mois pour 10 000 joueurs actifs. Le temps d'implémentation est de 1 à 2 jours si vous construisez tout vous-même, ou de 30 minutes si vous utilisez les API de données joueur de horizOn.

Quoi qu'il en soit, ne livrez pas un jeu cloud-streamed sans résoudre ce problème. Vos joueurs ne vous diront pas que leurs sauvegardes disparaissent — ils arrêteront simplement de jouer.


Source : Ajout de sauvegardes persistantes à Amazon GameLift Streams