การจัดการ Backpressure ใน Spring WebFlux

Sharing is caring!

ภาพหน้าปก: อัตราการไหลของข้อมูลถูกควบคุมด้วย Backpressure

เวลาอ่านโดยประมาณ 20 นาที — บทความนี้จะพาคุณเจาะลึกแนวคิด Reactive Streams Backpressure และวิธีนำไปใช้งานอย่างเป็นระบบใน Spring WebFlux (บน Project Reactor) เพื่อป้องกัน Out-Of-Memory, ปรับสมดุล workload และยกระดับ throughput ของแอป Reactive API ที่ใช้ Netty หรือ Undertow อยู่เบื้องหลัง


สารบัญ

  1. Backpressure คืออะไร & ทำไมต้องสนใจ?
  2. สัญญา Reactive Streams 4 ข้อ
  3. Operators รับมือ Backpressure ยอดนิยม
  4. ตัวอย่างโค้ด ครบวงจร
  5. เทคนิค Tuning ใน Spring Boot 3.5+
  6. ข้อผิดพลาดที่พบบ่อย
  7. Checklist สำหรับ Production

1. Backpressure คืออะไร & ทำไมต้องสนใจ?

ในระบบ Reactive Publisher กับ Subscriber จะสื่อสารกันด้วยสัญญาณ request(n) ผู้บริโภค (Consumer) จะบอกผู้ผลิต (Producer) ว่าต้องการข้อมูลทีละกี่ชิ้น หากผู้ผลิตส่งเร็วกว่าที่ร้องขอ เราเรียกว่า “overflow”. อาการที่พบบ่อย:

  • คิว buffer โตไม่จำกัด → RAM พุ่งจน OOM
  • Thread ถูกบล็อกรอ I/O เพราะ Storm ของ onNext()
  • Latency กระโดดสูงใน P99 เพราะ GC ยิงถี่
ภาพที่ 1 : แนวทางไหลของ data และสัญญาณ request(n)

การเปิด Backpressure จึงเหมือนใส่วาล์วควบคุมแรงดัน ช่วยรักษาอัตราการไหลให้สมดุล

2. สัญญา Reactive Streams 4 ข้อ

  1. Responsive – ต้องพร้อมตอบสนองตลอด
  2. Resilient – ต้องทนต่อความผิดพลาด
  3. Elastic – ขยายตามภาระงาน
  4. Message-Driven – ใช้ non-blocking message เป็นตัวควบคุม

ข้อสุดท้ายคือหัวใจ เพราะ Publisher จะส่ง onNext ได้ก็ต่อเมื่อ Subscriber ขอ

3. Operators รับมือ Backpressure ยอดนิยม

ภาพที่ 2 : ตารางเปรียบเทียบ กลยุทธ์หลัก ๆ
Operatorหลักการข้อดีข้อควรระวัง
limitRate(int n)ผ่อนส่งข้อมูลทีละก้อนควบคุมแรงดันได้แม่น, API อ่านง่ายเพิ่ม latency เล็กน้อยหาก n ต่ำไป
buffer(int size)กักข้อมูลในหน่วยความจำไม่มี data lossเสี่ยง OOM ถ้า size ไม่จำกัด
onBackpressureDrop()ทิ้ง element เกิน demandรักษา latency, ใช้ RAM น้อยข้อมูลหาย ไม่เหมาะกับ critical data

4. ตัวอย่างโค้ด ครบวงจร

4.1 Controller แบบ Endpoint-Streaming

// src/main/java/com/example/controller/EventController.java
@RestController
@RequestMapping("/v1/events")
public class EventController {

  private final EventService eventService;

  public EventController(EventService eventService) {
      this.eventService = eventService;
  }

  @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
  public Flux<Event> streamEvents() {
      return eventService.events()
              .limitRate(128)                // ปรับแรงดัน
              .onBackpressureBuffer(256)     // กักไม่เกิน 256
              .publishOn(Schedulers.boundedElastic());
  }
}

4.2 Service จำลอง Producer เร็วกว่า Consumer

@Service
public class EventService {

  public Flux<Event> events() {
      return Flux.interval(Duration.ofMillis(5)) // ยิงถี่มาก
                 .map(this::toEvent);
  }

  private Event toEvent(Long seq) {
      return new Event(seq, Instant.now());
  }
}

4.3 ทดสอบ Backpressure ใน JUnit 5

@Test
void backpressure_should_not_overflow() {
    StepVerifier.create(eventService.events()
                     .limitRate(10)
                     .take(100))
          .expectNextCount(100)
          .verifyComplete();
}

5. เทคนิค Tuning ใน Spring Boot 3.5+

  • ปรับ reactor.netty.io-worker-count เท่าจำนวน CPU logical core
  • ตั้ง spring.codec.max-in-memory-size จำกัด buffer แปลง JSON ใหญ่ ๆ
  • ใช้ @Scheduler กำหนด thread pool เฉพาะสำหรับ blocking I/O
  • เปิด BlockHound ตรวจเจอ blocking call ขณะ ทำ load test
ภาพที่ 3 : กราฟ throughput หลังเปิด limitRate(128) เส้นแดง คือ consumer

6. ข้อผิดพลาดที่พบบ่อย

⚠ ลืม Backpressure ที่ DB Layer
เช่น ดึง Flux ใหญ่จาก R2DBC แล้ว collectList() ใน Service → กลายเป็น blocking

  • ทำ parallel() แต่ไม่ .runOn() → ยังคงทำงานบน main event-loop
  • ละเลย error signal → ต้อง onErrorResume() ก่อน retry
  • เชน operator ที่ขยายข้อมูล (flatMap) โดยไม่จำกัด concurrency

7. Checklist ก่อนขึ้น Production

  1. เขียน StepVerifier กับทุก Flux/Mono ที่ Critical
  2. ตั้งค่า limitRate ใน ทุก จุด ที่ Producer > Consumer
  3. กำหนดขนาด buffer ให้เหมาะกับ JVM Heap
  4. ใช้ SLA (P99) ตรวจ latency เสมอใน Grafana/Prometheus
  5. จำลอง Load Spike จริง ด้วย k6 หรือ Vegeta

สรุป

Backpressure ไม่ใช่แค่ตัวเลือก แต่คือหัวใจของสถาปัตยกรรม Reactive ทุกระดับ ตั้งแต่ function เล็ก ๆ จนถึง microservice ทั้งระบบ หากเข้าใจจังหวะ request-response ของ Reactive Streams และนำ operators มาใช้ถูกต้อง คุณจะได้แอปที่ เร็ว เสถียร และ รองรับ โหลดสูง อย่างมั่นใจ

Leave a Reply

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องข้อมูลจำเป็นถูกทำเครื่องหมาย *