Retour à l'accueil

Go et YDB Serverless au lieu de Spring Boot

Développeur a migré l'application de Spring Boot et PostgreSQL vers Go et YDB Serverless dans Yandex Cloud. A décrit les défis du SQL manuel, des transactions, du DI et de l'optimisation RU pour les niveaux gratuits.

Go + YDB : s'intégrer dans le niveau gratuit de Yandex Cloud
Advertisement 728x90

Migration de Spring Boot vers Go et YDB Serverless : le parcours d’un entraîneur de frappe

Un développeur expérimenté en Java et Spring Boot a migré l'entraîneur de frappe TypeStep vers le cloud serverless d’Yandex. En abandonnant les machines virtuelles payantes et les services similaires à ECS, il a opté pour Go au niveau du backend et YDB plutôt que PostgreSQL. Résultat : démarrages de conteneurs froids en quelques millisecondes, tout en restant dans le forfait gratuit avec 1 million d'unités de requête par mois.

Go se compile en binaire statique sans JVM ni chauffage JIT. Les conteneurs démarrent instantanément — essentiel pour les conteneurs serverless où chaque requête peut déclencher une instance nouvelle. Spring Boot avec GraalVM nécessite une configuration supplémentaire, peu compatible avec un budget serré.

YDB Serverless a remplacé PostgreSQL en raison du coût. 1 million d'unités de requête par mois est gratuit, mais le calcul des unités restait partiellement flou sans une lecture approfondie de la documentation.

Google AdInline article slot

Architecture sans couches inutiles

Le frontend tourne sur Next.js avec génération statique, stocké dans un stockage d'objets. Le backend est écrit en Go sous Docker, exécuté sur des conteneurs serverless. La base de données est YDB Serverless. Les requêtes circulent directement depuis le navigateur : les ressources statiques proviennent du stockage, les appels API vont aux conteneurs. Pas de passerelle API → réduction de la latence et des coûts.

// Exemple d'énumération pour les colonnes YDB

type StageType string

const (
    Lowercase   StageType = "lowercase"
    Uppercase   StageType = "uppercase"
    Punctuation StageType = "punctuation"
    Numbers     StageType = "numbers"
)

func (s *StageType) Scan(value interface{}) error {
    return ScanStringEnum(s, value, "StageType")
}

func (s *StageType) Value() (driver.Value, error) {
    return string(*s), nil
}

Après avoir quitté JPA, concevoir manuellement les schémas et écrire des requêtes YQL semble être un pas en arrière. Chaque champ nécessite un DECLARE, des paramètres typés et un balayage manuel.

Repositories et requêtes YQL

Les repositories utilisent YQL avec transactions. Exemple de création d’un enregistrement :

Google AdInline article slot
func (r *ItemRepository) CreateItem(ctx context.Context, item Item) (*Item, error) {
    var result *Item
    err := r.txManager.DoTx(ctx, func(ctx context.Context, tx table.TransactionActor) error {
        res, err := tx.Execute(ctx, `
            DECLARE $id AS Utf8;
            DECLARE $name AS Utf8;
            DECLARE $status AS Utf8;
            DECLARE $created_at AS Datetime;

            INSERT INTO items (id, name, status, created_at)
            VALUES ($id, $name, $status, $created_at)
            RETURNING id, name, status, created_at;
        `,
            table.NewQueryParameters(
                table.ValueParam("$id", types.UTF8Value(item.Id)),
                table.ValueParam("$name", types.UTF8Value(item.Name)),
                table.ValueParam("$status", types.UTF8Value(string(item.Status))),
                table.ValueParam("$created_at", types.DatetimeValueFromTime(item.CreatedAt)),
            ),
        )
        if err != nil {
            return fmt.Errorf("failed to execute CreateItem: %w", err)
        }
        defer res.Close()

        // ... balayer le résultat dans &result
        return res.Err()
    })
    return result, err
}

Ajouter une colonne nécessite des mises à jour dans toutes les opérations : INSERT, UPDATE, SELECT. Contrairement à JPA, où les entités s’actualisent automatiquement.

  • Avantages du SQL manuel : contrôle total sur les requêtes, pas de problèmes N+1.
  • Inconvénients : verbeux, mappage de type manuel, gestion des énumérations.
  • Comparaison avec JPA : JPA est plus simple pour les schémas basiques, mais cache les détails d’exécution.

Transactions dans YDB distribué

YDB ne prend pas en charge les clés étrangères, mais propose des transactions. Sans annotation @Transactional, la gestion doit être manuelle.

TransactionManager vérifie si une transaction existe déjà dans le contexte :

Google AdInline article slot
func (t *TransactionManager) DoTx(ctx context.Context, fn func(ctx context.Context, tx table.TransactionActor) error) error {
    if tx, ok := TxFromContext(ctx); ok {
        return fn(ctx, *tx)
    }
    return t.ydb.NativeDriver.Table().DoTx(ctx, func(ctx context.Context, tx table.TransactionActor) error {
        ctx = context.WithValue(ctx, txKey{}, &tx)
        return fn(ctx, tx)
    })
}

Cela simule la propagation de Spring : soit démarrer une nouvelle transaction, soit réutiliser une existante. Élimine la duplication de code (Create vs CreateTx).

Les services et repositories injectent le manager. Plusieurs opérations dans une même transaction fonctionnent sans heurt.

Injection de dépendances manuelle et compilation

Sans IoC Spring, les dépendances sont assemblées dans main.go dans l’ordre : DB → repositories → services → handlers. Pas de réflexion, de proxy ou de boucle à runtime.

Avantages :

  • Graphique de dépendances explicite.
  • Pas de magie ni d’erreurs imprévues.
  • Refactoring facile.

Inconvénients : boilerplate manuel pour chaque composant.

Gestion des erreurs avec contexte

En Go, les erreurs sont des valeurs sans traces de pile. Solution : préfixes uniques dans fmt.Errorf.

func (s *ItemService) ProcessItem(ctx context.Context, id string) error {
    item, err := s.repo.FindById(ctx, id)
    if err != nil {
        return fmt.Errorf("ProcessItem: failed to find item %s: %w", id, err)
    }
    // ...
}

Dans les logs : ProcessItem: failed to find item abc123: ItemRepository.FindById: query failed: connection refused. La chaîne pointe précisément vers le point de défaillance.

Optimisation des unités de requête YDB

Le schéma initial reproduisait PostgreSQL : statistiques de lettres dans des lignes séparées, boucles INSERT/UPDATE par clé. Cela consommait rapidement les unités de requête.

Optimisations :

  • Agrégation côté client avant insertion.
  • Opérations groupées.
  • Réévaluation des index et partitionnement.

Après ces corrections, nous restons dans le forfait gratuit.

Points clés

  • Go offre des démarrages froids sous 1 seconde pour les conteneurs serverless, contre plusieurs secondes avec Spring Boot.
  • YDB impose des YQL manuels et des paramètres DECLARE ; simplifiez les schémas pour économiser les unités de requête.
  • TransactionManager simule @Transactional sans duplication de code.
  • L’injection manuelle de dépendances est plus claire que Spring, mais exige une discipline dans main.go.
  • Encapsulez les erreurs avec des préfixes pour des logs lisibles.

— Editorial Team

Advertisement 728x90

Lire ensuite