Warm-up Quick Fire: Retention Check (from 5.2 Dynamic Template Injection)
สถานการณ์จำลอง (Snowball Scenario): ในบทที่ 5.2 เราป้องกัน Metaspace Leak สำเร็จด้วย In-Memory AST Mutation ร่วมกับ Schema Fingerprint Caching แต่หากผู้บริหารระดับสูงกดร้องขอรายงานบัญชีแยกประเภทแบบ Ad-hoc ย้อนหลัง 10 ปี (มีข้อมูลกว่า 2,000,000 แถว ใช้เวลาประมวลผลประมาณ 6-8 นาที) แล้วระบบ REST API ของคุณยังประมวลผลคำขอนี้แบบ Synchronous Blocking HTTP Request ตรงๆ ใน Web Thread (เช่น Spring Boot Tomcat Worker Thread) จะก่อให้เกิดความล้มเหลวร้ายแรงต่อสถาปัตยกรรม Microservices อย่างไร?
5.3 Contextual Anchoring: The Synchronous Thread Starvation Crisis
Why Long-Running Document Generation Destroys Synchronous Web Runtimes & How Event-Driven Queues Save Clusters
Web Container มีขีดจำกัด Worker Threads (เช่น max-threads=200) เมื่อมีคำขอรายงานขนาดใหญ่เข้ามาพร้อมกัน 50 คำขอ แต่ละคำขอกินเวลา 5 นาที เธรดทั้งหมดจะถูกจองค้างไว้ ทำให้ API ทั่วไป (เช่น Login, Check Balance) ไม่สามารถรับการเชื่อมต่อใหม่ได้ ส่งผลให้ Health Check ล้มเหลวและ Kubernetes ทยอย Kill Pod ทิ้งทีละโหนด
เมื่อเกิด Network Timeout ไคลเอนต์จะ Retry คำขอซ้ำ หรือเมื่อ Worker ใน Consumer Group ค้างนานจนเกิน max.poll.interval.ms ใน Kafka คลัสเตอร์จะทริกเกอร์ Partition Rebalance และส่งงานเดียวกันไปให้ Worker ตัวอื่นทำซ้ำ สิ้นเปลืองทั้ง CPU, RAM, และ Disk ซ้อนทับกันหลายเท่าตัว
สถาปัตยกรรมระดับ Mission-Critical ตัดขาด Web Layer ออกจาก Report Engine 100%: Web API รับคำขอแล้วส่ง Job Ticket ลงในคิว (Kafka/RabbitMQ) พร้อมคืนรหัส 202 Accepted ทันที จากนั้น Isolated Worker Pods จะดึงงานไป Fill ร่วมกับ JRSwapFileVirtualizer และอัปโหลดไฟล์ตรงเข้า AWS S3 / MinIO พร้อมแจ้งเตือนผ่าน WebSocket หรือ Webhook
🏛️ Dynamic Core Pillars (4 เสาหลักสถาปัตยกรรมการประมวลผลรายงานแบบกระจายศูนย์)
Event-Driven Asynchronous Decoupling
การแยกหน้าที่ระหว่าง Web Ingestion Pods (เน้นรับคำขอรวดเร็ว) และ Dedicated Report Worker Pods (เน้นใช้ CPU/Heap สูง) ผ่าน Distributed Message Broker (Kafka/RabbitMQ) ทำให้โหลดการสร้างเอกสารไม่รบกวน Core Business APIs
Idempotency & Distributed Lease Locks
การใช้ Distributed Lock (Redis Redisson / PostgreSQL Advisory Lock) ควบคู่กับ Idempotency-Key ประจำงาน เพื่อรับประกันว่ารายงานเดียวกันจะไม่ถูกเรนเดอร์ซ้ำสองครั้ง แม้จะเกิด Message Retry หรือ Consumer Rebalance
Ephemeral Storage Offload (S3/MinIO)
การสตรีมไฟล์ผลลัพธ์ (PDF/XLSX) จาก Exporter ไปยัง Cloud Object Storage ทันทีโดยไม่ต้องบันทึกค้างไว้บน Persistent Volume ของ Container และส่งมอบให้ผู้ใช้ผ่าน Pre-signed URL ที่มีอายุจำกัด
Reactive State & Progress Telemetry
การรายงานความคืบหน้าของสถานะการ Fill (เช่น QUEUED → PROCESSING (Page 45,000) → COMPLETED) สู่ Frontend ผ่าน Server-Sent Events (SSE) หรือ Webhook Callbacks แบบ Real-time
Engine Internals: Distributed Job State Machine & Worker Lifecycle
เมื่อระบบรายงานระดับองค์กรถูกแปลงเป็น Asynchronous Pipeline การทำงานภายในของระบบจะดำเนินตามวัฏจักร 5 ขั้นตอนหลัก:
- Job Dispatch & Fast Ack: Web Ingestion Pod รับคำขอจากผู้ใช้ บันทึก Metadata เบื้องต้นลงในฐานข้อมูลด้วยสถานะ
PENDINGจากนั้น Publish ข้อความ JSON (บรรจุ Job ID, Template Name, และ Parameters) ลงใน Kafka Topic แล้วตอบกลับ HTTP202 Acceptedทันทีในเวลา < 20ms - Distributed Lease Acquisition: Worker Pod ที่ว่างดึงข้อความจากคิว และทำการแย่งสิทธิ์ผ่าน Distributed Lock (Redis Redisson
tryLock(jobId, leaseTime=15m)) เพื่อรับประกันความปลอดภัยจากการทำงานซ้ำ (Idempotency Guard) - Off-Heap Isolated Execution: Worker ทำการโหลด Compiled
JasperReportจาก Local Cache หรือ S3, ทำการ InitializeJRSwapFileVirtualizerเพื่อควบคุม Heap และดึงข้อมูลทีละ Batch จาก Database หรือ Data Lake เข้าสู่JasperFillManager.fillReport() - Multipart Streaming to S3: เมื่อ Fill สำเร็จ Worker จะสั่ง
JRPdfExporterหรือJRXlsxExporterให้สตรีมไบต์เอกสารผ่าน S3 Multipart Upload Pipe สู่ Bucket โดยตรงโดยไม่ผ่าน RAM - Lifecycle Disposal & Callback: คืนบล็อกดิสก์ด้วย
virtualizer.cleanup(), ปล่อย Redis Lock, อัปเดตสถานะเป็นCOMPLETEDพร้อมบันทึก S3 Object Key และส่ง Event แจ้งเตือนไคลเอนต์ผ่าน SSE/WebSocket
| Production Asynchronous Hazard | Underlying Root Cause | Runtime Symptom / Exception | Architectural Defense |
|---|---|---|---|
| Kafka Consumer Rebalance Loop | การ Fill รายงานใช้เวลานานเกินกว่าค่า max.poll.interval.ms (เช่น เกิน 5 นาที) ทำให้ Kafka คิดว่า Worker ตาย |
CommitFailedException: Consumer was revoked due to heartbeat failure | ปรับเพิ่ม max.poll.interval.ms ให้เหมาะสม และแยกเธรด Poll ข้อความออกจากเธรด Fill ผ่าน ThreadPoolExecutor |
| Poison Pill Message Loop | คำขอบางรายการมี Parameters ไม่ถูกต้องทำให้เกิด JRValidationException วนซ้ำในคิว |
Deadlock Storm: Worker พยายามรันงานเดิมซ้ำๆ จน CPU พุ่ง 100% ทั้งคลัสเตอร์ | ตั้งค่า Dead Letter Queue (DLQ) กำหนด Retry สูงสุด 3 ครั้ง และส่งข้อความที่ล้มเหลวไปเก็บใน DLQ ทันที |
| Duplicate Report Spilling (At-Least-Once) | Message Broker ส่งข้อความซ้ำหลังจาก Consumer ล้มเหลวกลางคัน | Duplicate Billing / Duplicate S3 Objects: ผู้ใช้ได้รับเอกสารสองฉบับซ้อนกัน | ใช้ Idempotency Key (SHA-256) เช็กสถานะใน Redis/Database ก่อนเริ่มดำเนินการ |
| Worker Node Out-of-Disk Leak | เมื่อเกิด OutOfMemory หรือ Unhandled Exception แล้วไม่มีการเรียก virtualizer.cleanup() ในบล็อก finally |
IOException: No space left on device (/tmp/jasper_swap บวมข้ามสัปดาห์) | ครอบโค้ดด้วย try-finally ทุกครั้ง และใช้ Kubernetes EmptyDir Volume ที่กำหนด Size Limit พร้อม CronJob คอยตรวจสอบ |
Strict 7.0.x Architecture Implementation
โค้ดสถาปัตยกรรมด้านล่างแสดงการทำงานของ AsynchronousReportWorkerService ซึ่งทำหน้าที่ Consume งานจาก Kafka, ควบคุม Idempotency ด้วย Redisson Distributed Lock, จัดการ Off-Heap Paging ด้วย JRSwapFileVirtualizer, และสตรีมไฟล์ตรงเข้าสู่ AWS S3/MinIO Bucket:
package com.enterprise.report.worker;
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.fill.JRSwapFileVirtualizer;
import net.sf.jasperreports.engine.util.JRSwapFile;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
import net.sf.jasperreports.pdf.JRPdfExporter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import java.io.*;
import java.nio.file.Files;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
public class AsynchronousReportWorkerService {
private final RedissonClient redissonClient;
private final S3Client s3Client;
private final JRSwapFile sharedSwapFile;
public record ReportTaskMessage(String jobId, String reportName, Map params, String s3Bucket, String s3Key) {}
public AsynchronousReportWorkerService(RedissonClient redissonClient, S3Client s3Client) {
this.redissonClient = redissonClient;
this.s3Client = s3Client;
// Shared Paged Swap File บน NVMe storage บล็อกละ 4KB เติบโตครั้งละ 1024 บล็อก
this.sharedSwapFile = new JRSwapFile("/tmp/jasper_swap", 4096, 1024);
}
@KafkaListener(topics = "report.generation.tasks", groupId = "report-worker-cluster", concurrency = "4")
public void processReportGenerationTask(ReportTaskMessage task) throws Exception {
// 1. Idempotency Lock: ป้องกันการประมวลผลซ้ำเมื่อเกิด Rebalance หรือ Retry
RLock lock = redissonClient.getLock("lock:report:job:" + task.jobId());
boolean acquired = lock.tryLock(5, 600, TimeUnit.SECONDS);
if (!acquired) {
// มี Worker ตัวอื่นกำลังประมวลผลงานนี้อยู่แล้ว ข้ามทันที
return;
}
// 2. ควบคุม Heap ด้วย Virtualizer: ค้างใน RAM ไม่เกิน 100 หน้า ส่วนเกินลงดิสก์
JRSwapFileVirtualizer virtualizer = new JRSwapFileVirtualizer(100, sharedSwapFile, false);
File tempExportFile = File.createTempFile("async_report_" + task.jobId(), ".pdf");
try {
task.params().put(JRParameter.REPORT_VIRTUALIZER, virtualizer);
// 3. โหลด Pre-compiled Template จาก Classpath
InputStream templateStream = getClass().getResourceAsStream("/reports/" + task.reportName() + ".jasper");
JasperReport jasperReport = (JasperReport) net.sf.jasperreports.engine.util.JRLoader.loadObject(templateStream);
// 4. Fill phase: ปลอดภัยจาก OOM แม้รายงานมีขนาดนับล้านแถว
JRDataSource dataSource = fetchDataSourceForTask(task);
JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, task.params(), dataSource);
// 5. Export phase ไปป์ลง Temporary File ก่อนส่งขึ้น Cloud
try (OutputStream fileOut = new BufferedOutputStream(new FileOutputStream(tempExportFile))) {
JRPdfExporter exporter = new JRPdfExporter();
exporter.setExporterInput(new SimpleExporterInput(jasperPrint));
exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(fileOut));
exporter.exportReport();
}
// 6. อัปโหลดผลลัพธ์ตรงเข้าสู่ S3 / MinIO
s3Client.putObject(
PutObjectRequest.builder().bucket(task.s3Bucket()).key(task.s3Key()).contentType("application/pdf").build(),
RequestBody.fromFile(tempExportFile)
);
// 7. แจ้งเตือนผลลัพธ์ความสำเร็จไปยัง Notification Dispatcher
notifyJobCompletion(task.jobId(), task.s3Key());
} finally {
// สำคัญที่สุด: คืนบล็อกดิสก์ใน Swap File และลบไฟล์ชั่วคราวเสมอ
virtualizer.cleanup();
Files.deleteIfExists(tempExportFile.toPath());
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
private JRDataSource fetchDataSourceForTask(ReportTaskMessage task) {
// เชื่อมต่อไปยัง Data Lake / Read-Replica DB
return new JREmptyDataSource(500000);
}
private void notifyJobCompletion(String jobId, String s3Key) {
// ส่งข้อความแจ้งเตือนผ่าน SSE หรือ WebSocket
}
}
Enterprise Omnibus Settlement Ledger (100% Unified Element Model 7.0.x)
<?xml version="1.0" encoding="UTF-8"?>
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd"
name="AsyncOmnibusSettlementLedger"
pageWidth="595" pageHeight="842"
columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20"
uuid="e8f1c2d3-b4a5-6789-0123-abcdef456789">
<parameter name="TASK_ID" class="java.lang.String"/>
<parameter name="EXECUTION_CLUSTER" class="java.lang.String"/>
<field name="transactionRef" class="java.lang.String"/>
<field name="clearingMember" class="java.lang.String"/>
<field name="settlementAmount" class="java.math.BigDecimal"/>
<field name="executionStatus" class="java.lang.String"/>
<title height="55">
<element kind="staticText" x="0" y="5" width="555" height="25">
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="Sarabun" size="16" isBold="true"/>
</textElement>
<text><![CDATA[รายงานสรุปการชำระราคาแบบกระจายศูนย์ (Omnibus Settlement Run)]]></text>
</element>
<element kind="textField" x="0" y="30" width="555" height="20">
<textElement textAlignment="Center">
<font fontName="Sarabun" size="10"/>
</textElement>
<expression><![CDATA["Job Ticket: " + $P{TASK_ID} + " | Worker Pod Cluster: " + $P{EXECUTION_CLUSTER}]]></expression>
</element>
</title>
<columnHeader height="20">
<element kind="staticText" x="0" y="0" width="130" height="20">
<textElement><font fontName="Sarabun" isBold="true"/></textElement>
<text><![CDATA[TRANSACTION REF]]></text>
</element>
<element kind="staticText" x="130" y="0" width="190" height="20">
<textElement><font fontName="Sarabun" isBold="true"/></textElement>
<text><![CDATA[CLEARING MEMBER]]></text>
</element>
<element kind="staticText" x="320" y="0" width="135" height="20">
<textElement textAlignment="Right"><font fontName="Sarabun" isBold="true"/></textElement>
<text><![CDATA[SETTLED VALUE (USD)]]></text>
</element>
<element kind="staticText" x="455" y="0" width="100" height="20">
<textElement textAlignment="Center"><font fontName="Sarabun" isBold="true"/></textElement>
<text><![CDATA[STATUS]]></text>
</element>
</columnHeader>
<detail>
<band height="20">
<element kind="textField" x="0" y="0" width="130" height="20">
<expression><![CDATA[$F{transactionRef}]]></expression>
</element>
<element kind="textField" x="130" y="0" width="190" height="20">
<expression><![CDATA[$F{clearingMember}]]></expression>
</element>
<element kind="textField" x="320" y="0" width="135" height="20" pattern="#,##0.00">
<textElement textAlignment="Right"/>
<expression><![CDATA[$F{settlementAmount}]]></expression>
</element>
<element kind="textField" x="455" y="0" width="100" height="20">
<textElement textAlignment="Center"/>
<expression><![CDATA[$F{executionStatus}]]></expression>
</element>
</band>
</detail>
<pageFooter height="20">
<element kind="textField" x="0" y="0" width="555" height="20">
<textElement textAlignment="Center"><font size="8"/></textElement>
<expression><![CDATA["หน้า " + $V{PAGE_NUMBER} + " (จัดเก็บลง AWS S3 สำเร็จ)"]]></expression>
</element>
</pageFooter>
</jasperReport>
Interactive Architecture Workspace (Split-View Studio)
Lab 5.3.1: Asynchronous Kafka Worker & Task Isolation
Target: Distributed Decouplingโจทย์สถาปัตยกรรม: พัฒนา Asynchronous Consumer Service ที่ประมวลผลรายงานขนาดใหญ่โดยไม่บล็อก Web API และไม่ทำให้เกิด Duplicate Execution:
- ต้องใช้ Distributed Lock (Redisson
tryLock) เพื่อรับประกัน Idempotent execution 100% - ต้องฉีด
JRSwapFileVirtualizerเข้าในREPORT_VIRTUALIZERก่อนเรียกfillReport() - ต้องมีกลไก
virtualizer.cleanup()และการปลดล็อกในบล็อกfinallyเสมอ - โครงสร้างของ Template ต้องเป็น Unified Element Model (
<element kind="...">) 100%
| TRANSACTION REF | CLEARING MEMBER | SETTLED VALUE (USD) | STATUS |
|---|---|---|---|
| TX-OMNI-9901 | Barclays Capital Services | $24,500,000.00 | SETTLED |
| TX-OMNI-9902 | BNP Paribas Global Securities | $18,200,400.00 | SETTLED |
ทดสอบจำลองข้อผิดพลาดที่มักเกิดบน Production ในระบบสร้างรายงานแบบกระจายศูนย์:
Production Image Specification Prompts (Visual Acceptance Standard)
"2D Vector architectural flowchart, modern dark slate background #0b0f19. JasperReports 7.0.3+ Asynchronous Decoupled Worker Pipeline: Client browser submitting report generation request to Spring Boot REST Gateway, Gateway publishing Job Ticket to Kafka cluster, returning HTTP 202 Accepted. Decoupled Kubernetes Report Worker Pod acquiring Redisson distributed lock, executing JRVerticalFiller with JRSwapFileVirtualizer, streaming output PDF directly to AWS S3/MinIO bucket via multipart upload, dispatching Server-Sent Events (SSE) notification to frontend, neon cyan (#06b6d4) and emerald (#10b981) glowing data streams, crisp typography, 8k resolution."
"Realistic high-resolution commercial PDF A4 portrait financial statement representing an omnibus clearing report generated asynchronously. Pristine white paper background, crisp corporate header 'CENTRAL CLEARING HOUSE - ASYNC OMNIBUS BATCH RUN', execution metadata 'Job Ticket: ASYNC-2026-X991 • Cluster Pod: Worker-04', clean tabular grid with columns TRANSACTION REF, CLEARING MEMBER, SETTLED VALUE (USD), and STATUS. Exact right-aligned currency figures like '$24,500,000.00', elegant typography, subtle green verified badges, perfectly straight layout, zero blueprint outlines, zero technical bounding boxes, enterprise banking production standard."
"Commercial financial statement detail view focusing on the asynchronous execution telemetry. Bottom footer displaying 'Cloud S3 Offloaded Artifact • SHA-256 Storage Hash: 8fa4c90b • Processed Pages: 50,000 • Execution Time: 42.1s • Presigned URL Active'. Crisp monochrome DataMatrix barcode for transaction authenticity verification at bottom right, razor-sharp typography, flawless contrast, authentic bond paper texture, photorealistic document lighting."
15 Architectural Mastery Quizzes
เจาะลึก Kafka/RabbitMQ Worker Pools, Idempotency Locks, S3 Multipart Offloading, และ Zero-Leak Lifecycle