Zpět na domů

Go a YDB Serverless místo Spring Boot

Vývojář migroval aplikaci ze Spring Boot a PostgreSQL na Go a YDB Serverless v Yandex Cloud. Popsány výzvy manuálního SQL, transakcí, DI a optimalizace RU pro bezplatný tier.

Go + YDB: vejdeme se do bezplatného tieru Yandex Cloud
Advertisement 728x90

Přechod z Spring Boot na Go a YDB Serverless: zkušenosti s migrací trenérku rychlého psaní

Vývojář s praxí v Java a Spring Boot migroval trenér rychlého psaní TypeStep do Yandex Cloud Serverless. Odstoupení od placených virtuálních strojů a ekvivalentů ECS vedlo k volbě Go pro backend a YDB místo PostgreSQL. Výsledek: využití bezplatného tieru při chladném startu kontejnerů za milisekundy a 1 milion Request Units měsíčně.

Go se kompiluje do statického binárního souboru bez JVM a JIT-prohřívání. Kontejner se spustí okamžitě – kritické pro Serverless Containers, kde se pro každý požadavek může vytvořit nový instanci. Spring Boot s GraalVM vyžaduje dodatečnou konfiguraci, což se nevešlo do omezeného rozpočtu.

YDB Serverless nahradil PostgreSQL kvůli nákladům. 1 milion RU/měsíc je zdarma, ale výpočet jednotek zůstal částečně nejasný bez hlubokého prozkoumání dokumentace.

Google AdInline article slot

Architektura bez nadbytečných vrstev

Frontend na Next.js s statickou generací je uložen ve Storage objektů. Backend je Go v Dockeru na Serverless Containers. Databáze je YDB Serverless. Požadavky jdou přímo z prohlížeče: statické soubory z úložiště, API do kontejnerů. Odstranění API Gateway minimalizuje latenci i náklady.

// Příklad enum pro sloupce 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
}

Po přechodu z JPA na ruční návrh schémat a YQL dotazy se cítí jako regrese. Každé pole vyžaduje DECLARE, typované parametry a ruční skenování.

Repository a YQL dotazy

Repository používá YQL s transakcemi. Příklad vytvoření záznamu:

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

        // ... scan result into &result
        return res.Err()
    })
    return result, err
}

Přidání sloupce vyžaduje úpravy ve všech operacích: INSERT, UPDATE, SELECT. Na rozdíl od JPA, kde entita se aktualizuje automaticky.

  • Výhody ručního SQL: Plná kontrola nad dotazy, žádné problémy N+1.
  • Nevýhody: Verbóznost, ruční mapování typů a enumerací.
  • Srovnání s JPA: JPA je jednodušší pro jednoduché schémata, ale skrývá detaily provádění.

Transakce v distribuované YDB

YDB nepodporuje foreign keys, ale nabízí transakce. Bez @Transactional je nutné ruční řízení.

TransactionManager ověřuje kontext na přítomnost tx:

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

To emuluje propagation Spring: nová transakce nebo reužití existující. Eliminuje duplikaci metod (Create vs CreateTx).

Služby a repository injektují manager. Více operací v jedné transakci funguje bez problémů.

DI a ruční sbírání závislostí

Bez Spring IoC jsou závislosti sbírány v main.go podle pořadí: DB → repositories → services → handlers. Žádná reflexe, proxy ani runtime cykly.

Výhody:

  • Výslovný graf závislostí.
  • Žádná magie a neočekávané chyby.
  • Lehký refaktoring.

Nevýhody: ruční boilerplate pro každý komponent.

Zpracování chyb s kontextem

Chyby v Go jsou hodnoty bez stektreysov. Řešení: unikátní prefixy v 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)
    }
    // ...
}

Ve logech: ProcessItem: failed to find item abc123: ItemRepository.FindById: query failed: connection refused. Řetězec ukazuje přesné místo selhání.

Optimalizace RU v YDB

Původní schéma kopírovalo PostgreSQL: statistika podle písmen v samostatných řádcích, INSERT/UPDATE v cyklu přes klávesy. To spalovalo RU.

Optimalizace:

  • Agregace dat na klientovi před vložením.
  • Batčování operací.
  • Přezkum indexů a particií.

Po úpravách se vejde do bezplatného limitu.

Co je důležité

  • Go zajišťuje chladné starty <1s pro Serverless Containers proti sekundám u Spring Boot.
  • YDB vyžaduje ruční YQL a DECLARE parametry; zjednodušte schémata pro úsporu RU.
  • TransactionManager emuluje @Transactional bez duplikace kódu.
  • Ruční DI je přehlednější než Spring, ale vyžaduje disciplínu v main.go.
  • Chyby obalujte s prefixy pro lepší čitelnost logů.

— Editorial Team

Advertisement 728x90

Číst dál