Zurück zur Startseite

Go und YDB Serverless statt Spring Boot

Entwickler hat Anwendung von Spring Boot und PostgreSQL zu Go und YDB Serverless in Yandex Cloud migriert. Beschreibt Herausforderungen bei manuellem SQL, Transaktionen, DI und RU-Optimierung für Free Tiers.

Go + YDB: Einpassen in den Free Tier von Yandex Cloud
Advertisement 728x90

Von Spring Boot nach Go und YDB Serverless: Die Reise eines Tipptrainers

Ein Entwickler mit Erfahrung in Java und Spring Boot hat den Tipptrainer TypeStep in die Serverless-Architektur von Yandex Cloud migriert. Die Entscheidung, kostenpflichtige VMs und ECS-ähnliche Dienste abzuschaffen, führte zur Wahl von Go für die Backend-Logik und YDB anstelle von PostgreSQL. Ergebnis: Kaltstarts von Containern in Millisekunden, ohne die kostenlose Stufe zu verlassen – mit einer monatlichen Kapazität von 1 Million Request Units.

Go kompiliert zu einem statischen Binärfile ohne JVM oder JIT-Warm-up. Container starten sofort – entscheidend für Serverless Containers, bei denen jeder Anfrage ein neuer Instanz entstehen kann. Spring Boot mit GraalVM erfordert zusätzliche Konfiguration, was im engen Budget nicht machbar war.

YDB Serverless ersetzte PostgreSQL aus Kostengründen. 1 Million RU/Monat sind kostenlos, doch die Einheitenberechnung blieb teilweise unklar, ohne tief in die Dokumentation einzusteigen.

Google AdInline article slot

Architektur ohne unnötige Schichten

Die Frontend-Seite läuft auf Next.js mit statischer Generierung und wird in Object Storage gespeichert. Das Backend ist Go in Docker auf Serverless Containers. Die Datenbank ist YDB Serverless. Anfragen fließen direkt vom Browser: statische Assets aus dem Speicher, API-Aufrufe an Container. Kein API Gateway senkt Latenz und Kosten.

// Beispiel-Enum für YDB-Spalten

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
}

Nach dem Wechsel von JPA fühlen sich manuelle Schema-Designs und YQL-Abfragen wie eine Rückschritt an. Jedes Feld erfordert DECLARE, typisierte Parameter und manuelles Scannen.

Repositories und YQL-Abfragen

Repositories nutzen YQL mit Transaktionen. Beispiel zum Erstellen eines Datensatzes:

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()

        // ... Ergebnis in &result scannen
        return res.Err()
    })
    return result, err
}

Das Hinzufügen einer Spalte erfordert Aktualisierungen in allen Operationen: INSERT, UPDATE, SELECT. Im Gegensatz zu JPA, wo Entitäten automatisch aktualisiert werden.

  • Vorteile manueller SQL: Vollständige Kontrolle über Abfragen, keine N+1-Probleme.
  • Nachteile: Aufwendig, manuelle Typzuordnung, Enums.
  • Vergleich mit JPA: JPA ist einfacher für grundlegende Schemata, verbirgt aber Ausführungsdetails.

Transaktionen in verteilter YDB

YDB unterstützt keine Fremdschlüssel, bietet aber Transaktionen. Ohne @Transactional muss die Verwaltung manuell erfolgen.

TransactionManager prüft den Kontext auf bestehende Transaktion:

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

Dies emuliert Spring’s Propagation: entweder neue Transaktion starten oder bestehende wiederverwenden. Vermeidet Code-Duplikation (Create vs CreateTx).

Services und Repositories injizieren den Manager. Mehrere Operationen in einer Transaktion laufen nahtlos.

Manuelle Dependency Injection und Build

Ohne Spring IoC werden Abhängigkeiten in main.go in der richtigen Reihenfolge zusammengebaut: DB → Repositories → Services → Handler. Keine Reflexion, Proxy-Objekte oder Laufzeit-Schleifen.

Vorteile:

  • Expliziter Abhängigkeitsgraph.
  • Kein Magie oder unerwartete Fehler.
  • Einfache Refaktorisierung.

Nachteile: Manueller Boilerplate für jedes Komponente.

Fehlerbehandlung mit Context

Go-Fehler sind Werte ohne Stacktraces. Lösung: Eindeutige Präfixe in 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)
    }
    // ...
}

In Logs: ProcessItem: failed to find item abc123: ItemRepository.FindById: query failed: connection refused. Die Kette zeigt genau den Fehlerort an.

Optimierung von YDB Request Units

Ursprüngliches Schema spiegelte PostgreSQL wider: Buchstabenauswertungen in separaten Zeilen, INSERT/UPDATE-Schleifen pro Schlüssel. Das verbrauchte schnell RU.

Optimierungen:

  • Aggregation der Daten clientseitig vor Einfügen.
  • Batch-Operationen.
  • Neubewertung von Indexen und Partitionierung.

Nach den Änderungen bleiben wir innerhalb der kostenlosen Stufe.

Wichtige Erkenntnisse

  • Go erreicht Kaltstarts unter 1 Sekunde für Serverless Containers – gegenüber mehreren Sekunden bei Spring Boot.
  • YDB erfordert manuelle YQL und DECLARE-Parameter; vereinfachte Schemata sparen RU.
  • TransactionManager emuliert @Transactional ohne Code-Duplikation.
  • Manuelle DI ist klarer als Spring, erfordert aber Disziplin in main.go.
  • Fehler mit Präfixen umschließen für lesbare Logs.

— Editorial Team

Advertisement 728x90

Weiterlesen