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.
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:
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:
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
Zatím žádné komentáře.