
ภาพหน้าปก: Scheduler & Thread Context Landscape
การใช้ Scheduler และ Thread Context ใน WebFlux
เวลาอ่าน ≈ 20 นาที — บทความนี้ลงลึกทุกแง่มุมของการจัดการ Thread-Switching, Scheduler และ Context Propagation ในโลก Spring WebFlux / Project Reactor. ครอบคลุม best-practice, pitfalls และตัวอย่างโค้ดพร้อมใช้ รวมทั้งภาพประกอบเข้าใจง่าย
สารบัญ
- ทำไม Scheduler สำคัญ?
- Event-loop พื้นฐานของ WebFlux
- Scheduler ชนิดต่าง ๆ
- subscribeOn vs publishOn
- Thread Context & Reactor Context
- MDC, SecurityContext, Tracing
- Performance Tuning Checklist
- การทดสอบ Scheduler & Context
- Demo ครบวงจร
1. ทำไม Scheduler สำคัญ?
Reactor ดำเนินงานทุกอย่างแบบ non-blocking โดยอาศัย Scheduler เพื่อขับเคลื่อน Subscriber
. หากเลือก Scheduler ไม่เหมาะสมจะเกิดปัญหา “Starvation” หรือ “Blocking the event-loop” นำไปสู่ Latency พุ่งสูงและ Throughput ลดฮวบ

2. Event-loop พื้นฐานของ WebFlux
- Spring Boot 3.5 ใช้
reactor.netty.http.server.HttpServer
บน Netty - Netty สร้าง
NioEventLoopGroup
(ค่าเริ่มต้น = CPU logical cores × 2) - ถ้าไม่ระบุ Scheduler Reactor จะใช้ “event-loop” นี้ทันที
// ไม่กำหนด Scheduler → ดำเนินงานใน Netty event-loop @GetMapping("/quick") public Mono<String> quick() { return Mono.just("OK") .doOnNext(v -> log.info("thread=" + Thread.currentThread())); }
3. Scheduler ชนิดต่าง ๆ
Scheduler | เหมาะกับ | ข้อดี | ข้อควรระวัง |
---|---|---|---|
parallel() | คำนวณ CPU-bound | ใช้ ForkJoinPool เล็ก = #CPU | ห้าม block |
boundedElastic() | Blocking I/O (JDBC, S3, SOAP) | ขยาย pool สูงสุด (ค่า def = CPU × 10) | เสี่ยง OOM ถ้า block นานเกินควร |
single() | ดำเนินงานลำดับเดียว | Debug ง่ายมาก | คอขวดถ้างานเยอะ |
immediate() | Internal Fusion | ไม่ switch thread | ใช้เฉพาะ optimization ภายใน |
4. subscribeOn vs publishOn
subscribeOn
กำหนด Scheduler ต้นทาง ของ Pipeline; ส่วน publishOn
เปลี่ยน Scheduler นับจากตำแหน่งเรียกเป็นต้นไป
Mono<String> result = Mono.fromCallable(this::ioBlocking) .subscribeOn(Schedulers.boundedElastic()) // เริ่มบน elastic .map(this::cpuHeavy) .publishOn(Schedulers.parallel()) // เปลี่ยนไป parallel .block();
5. Thread Context & Reactor Context

Reactor มี Context
เป็น Map-like Immutable data structure ผูกกับ Signal แทนที่จะอาศัย ThreadLocal
. ดังนั้น Context จะเคลื่อนย้ายพร้อม Signal ไปทุก Thread
@GetMapping("/context") public Mono<String> ctx() { return Mono.deferContextual(ctx -> { String reqId = ctx.get("REQ_ID"); return Mono.just("reqId=" + reqId); }) .contextWrite(Context.of("REQ_ID", UUID.randomUUID().toString())); }
6. MDC, SecurityContext, Tracing

SLF4J MDC ใช้ ThreadLocal
จึงไม่ปลอดภัยใน Reactive. แนวทาง:
- ใช้ไลบรารี
reactor-tools
หรือlogstash-logback-encoder
+ Hooks - สำหรับ Spring Security ใช้
ReactiveSecurityContextHolder
- โอเพนเทเลเมตรี (OTel) ใช้ Context โดยตรง → ไม่ต้อง patch
7. Performance Tuning Checklist
- อย่า block event-loop; ส่งไป
boundedElastic
- จำกัด pool ด้วย
SYSTEM_PROPERTY reactor.schedulers.defaultBoundedElasticSize
- ใช้
parallel()
เฉพาะ CPU heavy - เปิด
BlockHound.install()
ใน Dev เพื่อจับ block call - ใช้
Hooks.onOperatorDebug()
เฉพาะตอน Debug (ช้า)
8. การทดสอบ Scheduler & Context
@Test void context_should_flow_between_threads() { StepVerifier.withVirtualTime(() -> Mono.deferContextual(ctx -> Mono.just(ctx.get("K"))) .subscribeOn(Schedulers.boundedElastic()) .contextWrite(Context.of("K", "V"))) .expectNext("V") .verifyComplete(); }
ข้อควรจำ: ใช้ VirtualTimeScheduler
สำหรับ delayElements
หรือ time-based operator
9. Demo ครบวงจร: File Upload → Virus Scan
// 1) รับไฟล์ → 2) เขียน S3 (I/O) → 3) เรียก VirusScan (CPU) → 4) ตอบผล @PostMapping("/upload") public Mono<ResponseDto> upload(FilePart part) { return part.transferTo(tmpDir.resolve(part.filename())) .publishOn(Schedulers.boundedElastic()) // I/O write .flatMap(this::uploadToS3) .publishOn(Schedulers.parallel()) // CPU heavy scan .flatMap(this::virusScan) .map(result -> new ResponseDto(result)) .contextWrite(Context.of("file", part.filename())); }
Pipeline นี้แสดงการสลับ Scheduler สองครั้งพร้อมส่ง Context (ชื่อไฟล์) ติดไปด้วย
สรุป
การเข้าใจ Scheduler และ Thread Context คือหัวใจของการสร้าง WebFlux ที่ เร็ว, ปลอดภัย, และ ดูแลง่าย. เลือก Scheduler ให้ตรงงาน, ส่ง Context ด้วย Reactor Context แทน ThreadLocal
, และตรวจสอบด้วย BlockHound คือ 3 กุญแจสำคัญสู่ Production-grade Reactive Service.