İçeriğe geç

WebFlux ve Event Loop — Bloklayan Tek Satır Neyi Durdurur?

İleri 9 dk Sık karşılaşılır

Önce şunu oku: Virtual Threads vs Platform Threads , @Async ve Thread Havuzu — Max Neden Hiç Dolmuyor?

30 saniyede özet

WebFlux birkaç işçiyle binlerce isteği taşıyabilir, ama yalnızca hiçbir işçi bir yerde beklemediği sürece. Tek bir bekleyen veritabanı çağrısı herkesi durdurur. Bekleyen kodun çoksa virtual thread daha basit bir yoldur.

Dört garsonlu bir restoran kırk masaya yetişebilir; yeter ki hiçbir garson mutfak kapısında beklemesin.

  1. Bayt: Servisi WebFlux'a taşıdık. Birkaç thread, binlerce istek!

  2. Sen: Yük testi nasıl geçti?

  3. Bayt: Tuhaf: işlemci neredeyse boştu, ama her şey durmuştu.

  4. Bayt: Dört garsonlu restoranda garsonlar mutfak kapısında beklemeye başlarsa ne olur?

Servis WebFlux’a taşındı ve ilk yük testinde tuhaf bir şey oldu: işlemci neredeyse boşken uygulama tıkandı.

İki farklı garson

Spring MVC her isteğe bir thread verir. Thread, aşağı akış cevabını beklerken de o isteğe bağlı kalır.

WebFlux’ta az sayıda event loopAz sayıda thread'in, beklemeden olaydan olaya geçerek çok sayıda bağlantıyı taşıdığı model. WebFlux'ta Reactor Netty çekirdek başına bir loop kullanır.Sözlükte gör → thread’i vardır, genellikle çekirdek sayısı kadar. Beklemezler: isteği bir non-blocking I/OCevap beklenirken thread'i tutmayan G/Ç. Thread başka işe geçer, cevap gelince bir geri çağrı ya da sinyal işi sürdürür. WebClient ve R2DBC böyledir; JDBC değildir.Sözlükte gör → çağrısına bırakır ve cevap gelince devam ederler.

Kafam karıştı, daha basit anlat

MVC’de her masaya bir garson düşer, mutfak beklenirken garson masada durur. WebFlux’ta birkaç garson vardır, siparişi mutfağa verip hemen başka masaya geçerler.

Hızlı kontrolBaşlangıç

Spring MVC ile WebFlux arasındaki temel fark hangisi?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Aşağıdaki kod çalıştırılınca ne olur? Mono<Order> order = orderClient.fetch(id); log.info("hazır");

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Bir garson beklerse

WebFlux, 4 event loop. Her istek JPA ile bir sorgu yapıyor ve 12 istek aynı anda geldi. /actuator/health ne yapar? Cevabı göster

Bekler. JDBC bloklayan bir çağrıdır: event loop thread’i sorgu bitene kadar durur. Dört sorgu dört loop’u tutar; health probe da sırasını bekler.

Az garson çok masaya yetişir, ta ki biri kapıda beklemeye başlayana kadar.
Adım adım oku
  1. Dört event loop thread'i, dört garson gibi, çok sayıda isteğe hızla hizmet eder; kimse beklemez.
  2. Her istek bloklayan bir JPA sorgusu yapınca garsonların dördü de mutfak kapısında beklemeye başlar.
  3. Artık hiçbir masa, sağlık kontrolü bile, cevap alamaz; Kubernetes pod'u ölü sanar.
  4. Bloklayan iş boundedElastic gibi ayrı bir ekibe verilince garsonlar masalara döner.

MVC’de bloklamak bir thread’i tutar. WebFlux’ta ise yüzlerce isteği taşıyan bir loop’u durdurur. Bu yüzden bloklayan kod WebFlux’ta MVC’dekinden daha pahalıdır.

Kaçınılmaz bir bloklayan çağrı Mono.fromCallable(...) ile sarılır ve subscribeOn(Schedulers.boundedElastic()) ile ayrı, sınırlı bir havuza gönderilir.

Kafam karıştı, daha basit anlat

WebFlux garsonlarından biri mutfağın önünde beklemeye başlarsa, onun baktığı bütün masalar da bekler. Bu yüzden orada bekleyen kod çok daha pahalıdır.

Hızlı kontrolOrta

WebFlux controller'ında JPA repository'si ile veritabanı sorgusu yapıldı. Yük altında /actuator/health bile timeout alıyor. Neden?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

WebFlux uygulamasında, bloklamayan bir alternatifi olmayan eski bir SOAP istemcisini çağırman gerekiyor. Doğru yol hangisi?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin gör

Event loop'u kim durdurdu?

Tohum 470156

Event loop thread'leri

Henüz istek yok.

Biten istek: 0/12

Oynat ya da adımla.

Hız
Adım 0

Şu an ne oldu?

WebFlux (4 event loop)

Her istek bir kez aşağı akış servisini bekliyor. Beklerken thread'i kim tutuyor?

Görevler0/3

  • Health probe'u event loop'ta takılı bırakaçık

    İpucu

    WebFlux'ta JDBC çağır.

  • Bloklayan kodu WebFlux'ta tek turda bitiraçık

    İpucu

    İşi event loop'tan başka bir havuza gönder.

  • Bloklayan kodu reaktif yazmadan tek turda bitiraçık

    İpucu

    MVC'de kalıp thread türünü değiştir.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

  1. Varsayılanla oynat. WebFlux + JDBC: üç tur, health probe takıldı.
  2. “subscribeOn(boundedElastic)“i aç. Tek tur, loop’lar serbest.
  3. Offload’u kapat, çağrıyı “Bloklamayan” yap. Tek tur, 4 thread 12 isteği taşıdı.
  4. Yığını “MVC (8 platform thread)” yap, çağrıyı “Bloklayan” yap. Havuz doldu.
  5. “MVC + virtual thread” seç. Aynı bloklayan kod, tek tur.
Hızlı kontrolOrta

Bu WebFlux uç noktası ilk istekte 'block() are blocking, which is not supported in thread reactor-http-nio-2' hatası veriyor. Hatalı satır hangisi?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Hatalı satıra dokun, sonra kontrol et.

OrderController.java
Java 21UTF-8LF

Her servisi, en uygun yığına göre ayır.

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

Spring MVC (+ virtual thread)

Bloklayan kütüphaneler, klasik CRUD.

    WebFlux

    Uçtan uca bloklamayan, akış ya da çok sayıda uzun bağlantı.

      Tuzaklar

      Tembel zincir. Mono biri subscribe olana kadar çalışmaz. Controller zinciri döndürmeli; ortada .subscribe() çağırmak hataları kaybettirir.

      Mono ile sarılmış JPA. İmza reaktif görünür, altındaki JDBC yine bloklar. Uçtan uca reaktif için R2DBC gerekir.

      ThreadLocal bağlam. MDC ve SecurityContextHolder thread’e bağlıdır; reaktif zincir thread değiştirir. Reactor Context ve context-propagation kullanılır.

      Yanlış sebeple geçiş. Thread havuzu doluyorsa önce virtual thread’leri dene. WebFlux’un asıl kazancı gateway’ler, uzun bağlantılar ve backpressure isteyen akışlardır.

      Kafam karıştı, daha basit anlat

      Mono bir tariftir, yemeğin kendisi değil. Biri “pişir” diyene kadar hiçbir şey olmaz.

      Hızlı kontrolİleri

      WebFlux'a geçtikten sonra log'larda traceId ve kullanıcı id'si rastgele eksik ya da yanlış görünüyor. Neden?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Aşağıdaki örnek bir bankanın mobil uygulamasının açılış ekranından; WebFlux uç noktası uçtan uca bloklamadan kuruluyor. Kaçınılmaz tek bloklayan çağrı ayrı havuza gönderiliyor, test de event loop’ta bloklamayı yakalıyor.

      Derinleş · Mobil bankacılık hesap özeti: uçtan uca reaktif 6 dosya · ~110 satır · ilk okumada atlayabilirsin
      Proje dosyaları

      src/main/java/com/bank/mobile/ AccountSummaryController.java Controller Mono döndürür; üç kaynak zip ile aynı anda çağrılır. Hiçbir yerde block() yok.

      src/main/java/com/bank/mobile/AccountSummaryController.java
      @RestController
      @RequestMapping("/accounts")
      class AccountSummaryController {
      private final AccountRepository accounts;
      private final CustomerClient customers;
      private final LegacyCardAdapter cards;
      AccountSummaryController(AccountRepository accounts, CustomerClient customers, LegacyCardAdapter cards) {
      this.accounts = accounts;
      this.customers = customers;
      this.cards = cards;
      }
      // The app's home screen: balance, greeting and card limit. Opened by every
      // customer every morning, so thousands of these wait on I/O at the same time.
      @GetMapping("/{id}/summary")
      Mono<AccountSummary> summary(@PathVariable long id) {
      return accounts.findById(id)
      .switchIfEmpty(Mono.error(new AccountNotFoundException(id)))
      // The three lookups run concurrently; no thread waits for any of them.
      .flatMap(account -> Mono.zip(
      Mono.just(account),
      customers.fetch(account.customerId()),
      cards.availableLimit(account.customerId())))
      .map(tuple -> AccountSummary.of(tuple.getT1(), tuple.getT2(), tuple.getT3()));
      }
      }

      src/main/java/com/bank/mobile/ CustomerClient.java WebClient: bloklamayan HTTP; zaman aşımı zincirin içinde.

      src/main/java/com/bank/mobile/CustomerClient.java
      @Component
      class CustomerClient {
      private final WebClient http;
      CustomerClient(WebClient.Builder builder) {
      this.http = builder.baseUrl("http://customer-service").build();
      }
      Mono<Customer> fetch(long customerId) {
      return http.get().uri("/customers/{id}", customerId)
      .retrieve()
      .bodyToMono(Customer.class)
      .timeout(Duration.ofSeconds(2)); // a timeout is part of the chain, not a thread setting
      }
      }

      src/main/java/com/bank/mobile/ LegacyCardAdapter.java Eski kart sisteminin SOAP istemcisi bloklayan tek parça: fromCallable ile tembel, boundedElastic üzerinde.

      src/main/java/com/bank/mobile/LegacyCardAdapter.java
      @Component
      class LegacyCardAdapter {
      private final CardSystemSoapClient soap; // the card platform only speaks SOAP: blocking
      LegacyCardAdapter(CardSystemSoapClient soap) {
      this.soap = soap;
      }
      Mono<CardLimit> availableLimit(long customerId) {
      // fromCallable: nothing runs until subscription (Mono.just would call SOAP right here).
      // subscribeOn(boundedElastic): the blocking call runs off the event loop.
      return Mono.fromCallable(() -> soap.availableLimit(customerId))
      .subscribeOn(Schedulers.boundedElastic())
      .timeout(Duration.ofSeconds(3))
      // The balance still shows if the card system is slow; the app says "limit unavailable".
      .onErrorReturn(CardLimit.UNAVAILABLE);
      }
      }

      src/main/java/com/bank/mobile/ AccountRepository.java R2DBC repository: veritabanı da bloklamıyor.

      src/main/java/com/bank/mobile/AccountRepository.java
      interface AccountRepository extends ReactiveCrudRepository<AccountRow, Long> {
      // R2DBC: the driver itself is non-blocking. A JPA repository wrapped in Mono would not be.
      Flux<AccountRow> findByCustomerIdOrderByOpenedAtAsc(long customerId);
      }
      @Table("accounts")
      record AccountRow(@Id Long id, long customerId, String iban, BigDecimal balance, Instant openedAt) {
      }

      src/main/resources/ application.yml R2DBC bağlantısı ve MDC/trace bağlamının reaktif zincirde taşınması.

      src/main/resources/application.yml
      spring:
      r2dbc:
      url: r2dbc:postgresql://db:5432/mobile
      username: mobile_api
      password: ${DB_PASSWORD}
      pool:
      max-size: 20
      reactor:
      context-propagation: auto # MDC and tracing follow the chain across threads

      src/test/java/com/bank/mobile/ AccountSummaryBlockingTest.java BlockHound, event loop'ta çalışan bloklayan bir çağrıyı testte hataya çevirir.

      src/test/java/com/bank/mobile/AccountSummaryBlockingTest.java
      @WebFluxTest(AccountSummaryController.class)
      class AccountSummaryBlockingTest {
      @BeforeAll
      static void installBlockHound() {
      // Any blocking call on a non-blocking thread now throws.
      // On JDK 13+ the test JVM needs -XX:+AllowRedefinitionToAddDeleteMethods.
      BlockHound.install();
      }
      @Autowired WebTestClient client;
      @MockitoBean AccountRepository accounts;
      @MockitoBean CustomerClient customers;
      @MockitoBean LegacyCardAdapter cards;
      @Test
      void servesTheSummaryWithoutBlockingTheEventLoop() {
      given(accounts.findById(7L)).willReturn(Mono.just(
      new AccountRow(7L, 3L, "TR330006100519786457841326", new BigDecimal("1250.40"), Instant.EPOCH)));
      given(customers.fetch(3L)).willReturn(Mono.just(new Customer(3L, "Ada")));
      given(cards.availableLimit(3L)).willReturn(Mono.just(new CardLimit(new BigDecimal("8000.00"))));
      client.get().uri("/accounts/7/summary").exchange()
      .expectStatus().isOk()
      .expectBody()
      .jsonPath("$.customerName").isEqualTo("Ada")
      .jsonPath("$.balance").isEqualTo(1250.40);
      }
      }

      Kendini sına

      Şimşek turu1/5

      WebFlux'ta birkaç event loop thread'i çok sayıda isteği taşıyabilir.

      Soru 1/2İleri

      JPA kullanan, bloklayan bir MVC servisi yüksek eşzamanlılıkta thread havuzunu dolduruyor. WebFlux'a geçmek yerine ilk denenecek şey ne olmalı?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Aklında kalacak üç şey

      1. 1 WebFlux'ta birkaç event loop thread'i bütün istekleri taşır. Birini bekletmek, o thread'deki her isteği durdurur.
      2. 2 Bekleyen bir çağrı kaçınılmazsa subscribeOn(Schedulers.boundedElastic()) ile ayrı bir havuza gönderilir.
      3. 3 Bekleyen kütüphanelerle çalışan bir servis için virtual thread, her şeyi reaktif yeniden yazmaktan ucuzdur. WebFlux'un asıl kazancı baştan sona beklemeyen akışlardadır.
      Sonraki kapı Listede yalnızca isim ve fiyat göstereceksen bütün nesneyi yüklemek neden pahalı? JPA Projection ve DTO — Üç Alan İçin Ne Kadar Veri? · 8 dk

      5 kart sonraki derste seni bekliyor

      0/5 kart bu dersten toplandı