Java Virtual Threads vs Reactive: Kedy Zahodiť WebFlux za Project Loom
Chcel som, aby virtual threads boli strieborny naboj; boli len lepsim nozom. “Prepísali sme na WebFlux kvôli škálovateľnosti.” Teraz máte 10x zložitejší kód a developerov, ktorí ho nevedia debugovať.
Java 21 Virtual Threads sľubujú rovnakú škálovateľnosť s bežným blocking kódom. Je WebFlux konečne obsolete?
Testované na: Java 21, Spring Boot 3.2, WebFlux 6.1, PostgreSQL 16, 10k concurrent connections
Problém Škálovateľnosti
Platform Threads (Tradičné)
// Každý request = jeden OS thread
// OS thread = ~1MB stack + kernel overhead
// 10,000 concurrent requestov = 10GB RAM len pre stacky
@GetMapping("/blocking")
public String blocking() {
// Thread blokovaný čakajúc na DB
return repository.findById(1).getName(); // Thread pool: max 200 threads
}
// Výsledok: max 200 concurrent requestov, zvyšok vo fronte
Reactive (WebFlux)
// Non-blocking, event-driven
// Málo threadov zvláda veľa requestov
@GetMapping("/reactive")
public Mono<String> reactive() {
return repository.findById(1)
.map(Entity::getName); // Non-blocking
}
// Výsledok: 10,000+ concurrent requestov možných
// Cena: Zložitý kód, ťažké debugovanie
Virtual Threads (Java 21)
// Ľahké thready, spravované JVM
// Blocking kód, non-blocking vykonávanie
@GetMapping("/virtual")
public String virtual() {
// Vyzerá blocking, ale JVM odmontuje thread počas čakania
return repository.findById(1).getName(); // Jednoduché!
}
// Výsledok: 10,000+ concurrent requestov s blocking kódom
Ako Virtual Threads Fungujú
Platform Thread vs Virtual Thread
Platform Thread:
┌─────────────────────────────────┐
│ OS Kernel Thread │ ~1MB stack
│ ├── Tvoj kód │
│ └── Blokovaný na I/O (čaká) │ Plytvanie!
└─────────────────────────────────┘
Virtual Thread:
┌─────────────────────────────────┐
│ Carrier Thread (platform) │ Zdieľaný pool
│ ├── VT1: vykonáva kód │
│ ├── VT2: vykonáva kód │
│ └── ... │
└─────────────────────────────────┘
↓ Keď VT blokuje na I/O
┌─────────────────────────────────┐
│ VT odmontovaný z carrier │
│ Carrier beží iný VT │ Žiadne plytvanie!
│ VT namontovaný späť po I/O │
└─────────────────────────────────┘
Povolenie Virtual Threads
// Spring Boot 3.2+
// application.yml
spring:
threads:
virtual:
enabled: true
// Alebo programaticky
@Bean
public TomcatProtocolHandlerCustomizer<?> virtualThreads() {
return handler -> handler.setExecutor(
Executors.newVirtualThreadPerTaskExecutor()
);
}
Benchmark Setup
Testovacia Aplikácia
@RestController
public class BenchmarkController {
@Autowired
private JdbcTemplate jdbc;
// Simuluje typické API: DB query + nejaké spracovanie
@GetMapping("/api")
public Response handle() {
// 50ms DB query
var data = jdbc.queryForObject(
"SELECT pg_sleep(0.05), current_timestamp",
(rs, i) -> rs.getTimestamp(2)
);
// 10ms CPU práca
doSomeProcessing();
return new Response(data);
}
}
Load Test
// k6_benchmark.js
import http from 'k6/http';
export const options = {
scenarios: {
concurrent_users: {
executor: 'constant-vus',
vus: 10000, // 10k concurrent
duration: '5m',
},
},
};
export default function () {
http.get('http://localhost:8080/api');
}
Výsledky: 10k Concurrent Connections
Platform Threads (Tomcat default: 200 threads)
Konfigurácia:
server.tomcat.threads.max: 200
Výsledky:
Throughput: 180 req/s
Latency p50: 850ms
Latency p99: 4,200ms
Errors: 12% (connection timeout)
Memory: 1.2GB
Problém: 200 threads, 10k requestov = 9,800 vo fronte
WebFlux + R2DBC
Konfigurácia:
WebFlux s Netty
R2DBC PostgreSQL driver
Výsledky:
Throughput: 8,500 req/s
Latency p50: 58ms
Latency p99: 180ms
Errors: 0.1%
Memory: 850MB
Zložitosť: ~3x viac kódu, Mono/Flux všade
Virtual Threads + JDBC
Konfigurácia:
spring.threads.virtual.enabled: true
Štandardný JDBC (blocking driver)
Výsledky:
Throughput: 7,200 req/s
Latency p50: 62ms
Latency p99: 195ms
Errors: 0.1%
Memory: 920MB
Zložitosť: Rovnaká ako tradičný blocking kód!
Súhrnná Tabuľka
| Metrika | Platform Threads | WebFlux | Virtual Threads |
|---|---|---|---|
| Throughput | 180 req/s | 8,500 req/s | 7,200 req/s |
| Latency p99 | 4,200ms | 180ms | 195ms |
| Zložitosť kódu | Jednoduchá | Zložitá | Jednoduchá |
| Debugovanie | Ľahké | Ťažké | Ľahké |
| Memory | 1.2GB | 850MB | 920MB |
Kedy Čo Použiť
Virtual Threads Vyhrávajú
// I/O-bound workloady s jednoduchou logikou
// Tradičné blocking knižnice (JDBC, RestTemplate)
@Service
public class OrderService {
public Order createOrder(OrderRequest request) {
// Všetky blocking volania, ale VT ich robí efektívne
var inventory = inventoryClient.check(request); // HTTP call
var payment = paymentClient.charge(request); // HTTP call
var order = orderRepository.save(request); // DB call
emailService.send(order); // SMTP
return order;
}
}
WebFlux Vyhráva
// Streaming dát
// Backpressure požiadavky
// Už máte reactive ekosystém
@GetMapping(value = "/stream", produces = TEXT_EVENT_STREAM_VALUE)
public Flux<Event> stream() {
return eventRepository
.findAllAsStream()
.delayElements(Duration.ofMillis(100))
.onBackpressureBuffer(1000);
}
// Server-Sent Events, WebSocket s backpressure
// Virtual Threads toto nevedia elegantne
Platform Threads Vyhrávajú
// CPU-bound workloady
// Nízka konkurencia (< 200 concurrent requestov)
// Keď jednoduchosť je najdôležitejšia
@GetMapping("/compute")
public Result heavyComputation() {
// CPU-intensive, nie I/O
// Virtual threads tu nepomôžu
return compute.intensive().operation();
}
Virtual Thread Gotchas
1. Pinning: synchronized Bloky
// ZLE: VT sa pripne na carrier thread
public synchronized void pinnedMethod() {
database.query(); // Carrier thread blokovaný!
}
// DOBRE: Použi ReentrantLock
private final Lock lock = new ReentrantLock();
public void unpinnedMethod() {
lock.lock();
try {
database.query(); // VT sa môže odmontovať
} finally {
lock.unlock();
}
}
2. Ťažké Používanie Thread-Local
// Thread-locals fungujú ale môžu sa akumulovať
// Každý VT dostane vlastnú kópiu
// Zváž ScopedValue (Java 21 preview) pre request context
ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
ScopedValue.runWhere(CURRENT_USER, user, () -> {
// user dostupný v tomto scope
processRequest();
});
3. Pool-based Knižnice
// Niektoré knižnice poolujú thready interne
// Nemusia benefitovať z virtual threads
// Skontroluj kompatibilitu knižníc:
// - HikariCP: Funguje dobre
// - Apache HttpClient 5: Dobrá VT podpora
// - OkHttp: Funguje ale pinning v niektorých prípadoch
// - Lettuce (Redis): Reactive, nepotrebuje VT
Migračná Cesta
Z Platform Threads
# Krok 1: Povoľ virtual threads
spring:
threads:
virtual:
enabled: true
# Krok 2: Zväčši connection pool
spring:
datasource:
hikari:
maximum-pool-size: 100 # Potrebných viac connections
# Krok 3: Testuj pod záťažou
# Krok 4: Monitoruj pinning
Z WebFlux
// Postupná migrácia je možná
// Ponechaj reactive pre streaming endpointy
// Konvertuj jednoduché endpointy:
// Pred (WebFlux)
@GetMapping("/user/{id}")
public Mono<User> getUser(@PathVariable Long id) {
return userRepository.findById(id);
}
// Po (Virtual Threads + JDBC)
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id).orElseThrow();
}
Monitoring Virtual Threads
JFR Events
# Nahrávaj virtual thread eventy
java -XX:StartFlightRecording=name=vt,settings=profile \
-jar app.jar
# Analyzuj pinning eventy
jfr print --events jdk.VirtualThreadPinned recording.jfr
Prometheus Metriky
// Micrometer + VT metriky
@Bean
MeterBinder virtualThreadMetrics() {
return registry -> {
Gauge.builder("jvm.threads.virtual.count", () ->
Thread.getAllStackTraces().keySet().stream()
.filter(Thread::isVirtual)
.count()
).register(registry);
};
}
Checklist
## Migrácia na Virtual Threads
### Predpoklady
- [ ] Java 21+
- [ ] Spring Boot 3.2+
- [ ] Skontroluj kompatibilitu knižníc (JDBC drivers, HTTP clients)
### Konfigurácia
- [ ] Povoľ spring.threads.virtual.enabled
- [ ] Zväčši connection pool
- [ ] Prezri synchronized bloky (konvertuj na Lock)
### Testovanie
- [ ] Load test s cieľovou konkurenciou
- [ ] Monitoruj pinning (JFR eventy)
- [ ] Porovnaj latency/throughput s baseline
### Ponechaj Reactive Pre
- [ ] Streaming endpointy (SSE, WebSocket)
- [ ] Backpressure požiadavky
- [ ] Už reactive závislosti (R2DBC, WebClient)
Záver
Virtual Threads v Java 21 menia hru:
- Rovnaká škálovateľnosť ako WebFlux s blocking kódom
- Jednoduchšie debugovanie - stack traces fungujú normálne
- Ľahká migrácia - jedna konfiguračná zmena
- WebFlux stále vyhráva pre streaming a backpressure
Pre väčšinu CRUD API: Virtual Threads > WebFlux.
Súvisiace články
- Connection Pool Sizing s Little’s Law - Pool sizing
- K8s CPU Throttling Pitva - Resource tuning
Súvisiace články
JVM Native Memory v Kubernetes: Prečo Pod Dostane OOMKilled s 50% Heap
Heap je 50% plný ale pod dostane OOMKilled. Ukážem ako sledovať native memory (Metaspace, threads, NIO) a zabrániť container memory problémom.
Keď Prepared Statements Spravia PostgreSQL 10× Pomalším: Generic Plan Trap
Rovnaký query, rovnaké parametre, ale prod je pomalý a staging funguje. Ukážem ako reprodukovať generic plan problém s pgBouncer, Java/Go a ako ho fixnúť.
Circuit Breaker vs Rate Limiter vs Bulkhead: Kedy Ktorý Pattern Použiť
Tri resilience patterns, ktoré sa často zamieňajú. Ukážem presne kedy ktorý bráni cascade failures a kedy to zhoršuje so skutočnými metrikami.
Kubernetes CPU Throttling Pitva: Prečo p99 Latencia Exploduje pri 40% CPU Usage
CPU vyzerá OK, ale tail latencia je katastrofálna. Ukážem ako korelovať CFS throttling s latency spikes a prečo odstránenie CPU limitov môže paradoxne pomôcť.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.