Ačkoliv vím, že přepisování software je něco, co byste nikdy neměli dělat, tak jsme se i přesto do jednoho přepisu pustili. Velmi důležité byly nároky na výkon a propustnost. Zkušenost Netflixu s virtuálními vlákny na Java 21 nás varovaly, že tuhle cestu zatím ještě zkoušet nechceme. Situace se od té doby změnila, viz JEP 491: Synchronize Virtual Threads without Pinning Tehdy jsme učinili pokus s reaktivním programováním a WebFlux ve Spring Boot. Udělal jsem si pár poznámek, které bych nerad ztratil někde v archivu, kde už se mi víc jak rok válí (tak snad to ještě nestačilo tolik zastarat).

Úvod

Spring MVC používá thread-per-request model, kde každý požadavek je zpracováván jedním vláknem od začátku do konce.

Oproti tomu WebFlux je založen na reaktivním, neblokujícím modelu zpracování požadavků. Používá malý počet vláken (event-loop model). Je-li celý řetězec zpracování neblokující, tak se tato vlákna neblokují čekáním na I/O, ale jsou uvolněna pro zpracování dalších požadavků. Standardně staví na projektu Reactor a jako výchozí runtime používá Netty.

Databáze

Dřív se člověk mohl vymlouvat, že reaktivní programování stejně nemá cenu, když neexistují reaktivní databázové ovladače. To už dávno neplatí, máme R2DBC a i odpovídající Spring Data, ale přišlo mi, že jsou pořád trochu nedotažené. Například jsem nebyl schopný dosáhnout Load aggregates with a single select, tedy načíst celý agregát (i se všemi vazbami) jedním SQL SELECTem s JOINy, což by v JDBC normálně mělo být možné.

Pooling

Při prvním spuštění výkonnostních testů jsme dostávali žalostné výsledky. Když celý příběh zkrátím, tak z metrik vyplynulo, že se dlouho čeká na vykonání databázového dotazu, respektive už na samotné získaní spojení z poolu. To bylo obzvláště divné, jelikož pool měl alokováno víc spojení, ale nikdy nebyla využitá všechna. Tedy měli jsme alokováno 50 spojení, ale nikdy jich to nepoužilo všech dostupných 50 paralelně.

Hlavní příčinou bylo, že colocation je ve výchozím nastavení zapnuté. V JavaDoc se píše:

true means that EventLoopGroup created for clients will reuse current local event loop if already working inside one

Přeloženo do lidštiny: To způsobí, že se znovu použije aktuální event loop a vede na pozorované nerovnoměrné rozložení zátěže.

Není to úplně špatně vymyšlené, protože to šetří přepínání kontextu a zlepšuje práci s cache, ale pro vysokou paralelní zátěž je to nevhodné.

Samozřejmě se to netýká jen poolu databázových spojení ale i HTTP. Vyřešila to následující Java konfigurace.

@Bean
public ConnectionFactoryOptionsBuilderCustomizer loopResourcesCustomizer() {
    logger.info("Configuring Netty Loop Resources for PostgreSQL R2DBC connection pool not using colocated event loop threads.");
    return builder -> builder.option(
            PostgresqlConnectionFactoryProvider.LOOP_RESOURCES,
            LoopResources.create("pg-pool", LoopResources.DEFAULT_IO_SELECT_COUNT, LoopResources.DEFAULT_IO_WORKER_COUNT, true, false)
    );
}

@Bean
public NettyServerCustomizer nettyServerCustomizer() {
    logger.info("Configuring Netty Loop Resources for HTTP connection pool not using colocated event loop threads.");
    return httpServer -> httpServer
            .runOn(LoopResources.create("reactor-http", LoopResources.DEFAULT_IO_SELECT_COUNT, LoopResources.DEFAULT_IO_WORKER_COUNT, true, false));
}

Výstup z monitoringu ilustruje moment, kdy aplikace konečně začne využívat plnou kapacitu poolu. Růžová (acquired) je stabilně na maximu (kolem 50), to je to, čeho jsme potřebovali dosáhnout. Pool je plně vytížený a aplikace dokonce žádá o více spojení, než má k dispozici, což ukazuje tyrkysová (pending), která vystřeluje až ke 200. Aplikace má tedy velkou frontu čekajících požadavků na databázová spojení, ale to už stačí jen navýšit kapacitu poolu (samozřejmě s ohledem na to, co unese infrastruktura).

Závěr

Řešení s reaktivním programování zafungovalo a prošlo výkonnostními testy. Konstrukty mi přišly neintuitivní (reaktivní programování je pro běžného Java programátora změna paradigmatu) a knihovny stále nepodporovaly scénáře, které bych očekával, že už budou dávno hotové. Pokud nepotřebujete backpressure, zkusil bych dnes dát virtuálním vláknům šanci.

Související