Skip to content
nullchefo
← Обратно към дневника
2 мин четене

Виртуални нишки в Spring Boot, две години по-късно

Бележки от Project Loom в продукция: къде виртуалните нишки наистина опростяват един Spring сървис и кои са двата капана, които още хапят.

#java#spring#backend Налична и на: English

Когато виртуалните нишки станаха стабилни в JDK 21, обещанието беше просто: пишеш блокиращ код, получаваш почти reactive пропускливост. След като ги карам в продукционни Spring Boot сървиси от доста време, обещанието в общи линии се изпълнява — с уговорки, които си заслужава да бъдат записани.

Включването е лесната част

В Spring Boot 3.2+ това е едно-единствено property:

spring:
  threads:
    virtual:
      enabled: true

С този флаг Tomcat обработва всяка заявка върху виртуална нишка, @Async методите и task executor-ите я следват, и класическият модел нишка-на-заявка изведнъж се мащабира като event loop. Без Mono, без Flux, без “оцветени” функции.

Къде реално помогна

Нашите сървиси са типично интеграционно лепило: приемаш заявка, викаш два-три downstream API-я, удряш MongoDB, агрегираш, отговаряш. Под натоварване платформените нишки бяха тясното място — 200 нишки, паркирани на I/O, са 200 стека пропиляна памет.

С виртуални нишки същият блокиращ код изглежда така и се мащабира добре:

@GetMapping("/items/{id}/summary")
ItemSummary summary(@PathVariable String id) {
    var details = detailsClient.fetch(id);     // блокира, евтино
    var reviews = reviewsClient.fetch(id);     // блокира, евтино
    return ItemSummary.merge(details, reviews);
}

Менталният модел, който екипът ти вече има — една заявка, една нишка, четими stack trace-ове — остава непокътнат. Това е истинската печалба: изтрихме един WebFlux сървис и го заменихме с код, който и junior колеги могат да дебъгват.

Двата капана

Pinning. Виртуална нишка, която блокира вътре в synchronized блок, заковава carrier нишката си и тихо изяжда мащабируемостта. По-старите connection pool-ове и драйвери бяха пълни с това. Пускайте с включен детектор, докато не се доверите на зависимостите си:

java -Djdk.tracePinnedThreads=full -jar app.jar

Неограничена конкурентност. Виртуалните нишки правят създаването евтино, което прави лесно да насочиш пожарникарски маркуч към downstream сървис, който все още много си има лимити. Решението е същото, каквото винаги е било — експлицитен backpressure:

var semaphore = new Semaphore(50);

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    for (var id : itemIds) {
        scope.fork(() -> {
            semaphore.acquire();
            try {
                return enrich(id);
            } finally {
                semaphore.release();
            }
        });
    }
    scope.join().throwIfFailed();
}

Виртуалните нишки премахват цената на блокирането, не цената на нещото, на което блокираш.

Бих ли ги направил default?

За нови Spring сървиси: да. Започни със spring.threads.virtual.enabled=true, дръж кода блокиращ и скучен, и посягай към reactive само когато ти трябва streaming семантика, а не пропускливост. Скучният код, който се мащабира, е най-добрият вид код.

Stefan Kehayov

Full-stack инженер · Доспат · България