แก้ปัญหา RabbitMQ ประมวลผลในคิวช้า และรอคิวนาน
Some checks failed
Build & Deploy Checkin Service / build (push) Failing after 40s
Some checks failed
Build & Deploy Checkin Service / build (push) Failing after 40s
This commit is contained in:
parent
ae417e4777
commit
5f678b2898
3 changed files with 139 additions and 24 deletions
83
BMA.EHR.CheckInConsumer/CHANGELOG-checkin-speedup.md
Normal file
83
BMA.EHR.CheckInConsumer/CHANGELOG-checkin-speedup.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# สรุปการปรับปรุงระบบลงเวลา (CheckInConsumer)
|
||||
|
||||
วันที่แก้ไข: 23 มิถุนายน 2026
|
||||
|
||||
---
|
||||
|
||||
## ปัญหาเดิม
|
||||
|
||||
ตอนที่พนักงานลงเวลาพร้อมกันจำนวนมาก (ประมาณ 2,000 รายการ) ระบบประมวลผลทีละรายการ ทำให้ต้องรอคิวนานถึง **22 นาที** กว่าจะประมวลผลเสร็จทั้งหมด
|
||||
|
||||
เปรียบเทียบเหมือน **โต๊ะบัญชี 1 คน รับคิวทีละคน** ทั้งที่มีคนรอ 2,000 คน → คิวยาวมาก
|
||||
|
||||
---
|
||||
|
||||
## วิธีที่แก้ (เข้าใจง่าย ๆ)
|
||||
|
||||
### 1. เพิ่มคนช่วยประมวลผลพร้อมกัน (Concurrency)
|
||||
- **ก่อน:** ประมวลผลทีละรายการ (เหมือนมีโต๊ะบัญชี 1 โต๊ะ)
|
||||
- **หลัง:** ประมวลผลพร้อมกันได้สูงสุด **5 รายการ** (เหมือนเปิดโต๊ะบัญชี 5 โต๊ะ)
|
||||
|
||||
> ผลที่ได้: เวลารอคิวลดลงจาก **22 นาที → ประมาณ 4–5 นาที**
|
||||
|
||||
### 2. จัดคิวล่วงหน้าให้ RabbitMQ (Prefetch)
|
||||
- **ก่อน:** ระบบดึงข้อมูลมาทีละชิ้น ทำให้เสียเวลารอส่งต่อ
|
||||
- **หลัง:** ระบบดึงข้อมูลมาเป็นชุด ๆ ละ 20 ชิ้นไว้เตรียมพร้อม → ลดเวลารอระหว่างรายการ
|
||||
|
||||
### 3. ลดเวลารอเมื่อ API มีปัญหา (Timeout)
|
||||
- **ก่อน:** ถ้า API ค้าง ระบบจะรอนานถึง **5 นาที** ต่อรายการ
|
||||
- **หลัง:** ลดเหลือ **1 นาที** → รายการที่มีปัญหาจะถูกปฏิเสธเร็วขึ้น ไม่ทำให้คิวค้าง
|
||||
|
||||
### 4. ปรับปรุงการเชื่อมต่อ HTTP
|
||||
- เปลี่ยนระบบเชื่อมต่อให้รองรับการส่งคำขอหลายรายการพร้อมกันโดยไม่สะดุด
|
||||
|
||||
---
|
||||
|
||||
## ตัวเลขเปรียบเทียบ
|
||||
|
||||
| รายการ | ก่อนแก้ | หลังแก้ |
|
||||
|---|---|---|
|
||||
| จำนวนรายการที่ประมวลผลพร้อมกัน | 1 | 5 |
|
||||
| เวลารอคิวสูงสุด (2,000 รายการ) | ~22 นาที | ~4–5 นาที |
|
||||
| เวลารอเมื่อ API มีปัญหา | 5 นาที | 1 นาที |
|
||||
|
||||
---
|
||||
|
||||
## ไฟล์ที่แก้ไข
|
||||
|
||||
1. **`Program.cs`** — โค้ดหลักของตัวประมวลผลคิว
|
||||
2. **`appsettings.json`** — ไฟล์ตั้งค่าระบบ
|
||||
|
||||
---
|
||||
|
||||
## วิธีปรับความเร็วเพิ่มเติม (ไม่ต้องเขียนโค้ดใหม่)
|
||||
|
||||
ถ้าหลังทดสอบแล้วเห็นว่าระบบรับได้ และอยากให้เร็วขึ้นอีก ให้แก้ไขไฟล์ `appsettings.json` แล้ว restart โปรแกรมได้เลย:
|
||||
|
||||
```json
|
||||
{
|
||||
"MaxConcurrency": 10, ← เพิ่มจาก 5 เป็น 10 (ประมวลผลพร้อมกัน 10 รายการ)
|
||||
"PrefetchCount": 50, ← ควรตั้งเป็น ประมาณ MaxConcurrency × 2 ขึ้นไป
|
||||
"HttpTimeoutSeconds": 60 ← เวลารอ API วินาที
|
||||
}
|
||||
```
|
||||
|
||||
**ค่าที่ใช้และผลที่คาดการณ์:**
|
||||
- `MaxConcurrency = 5` → ใช้เวลา ~4–5 นาที (ค่าเริ่มต้นปลอดภัย)
|
||||
- `MaxConcurrency = 10` → ใช้เวลา ~2–3 นาที
|
||||
- `MaxConcurrency = 20` → ใช้เวลา ~1–2 นาที (ต้องตรวจสอบว่าระบบหลังบ้านรับไหวก่อน)
|
||||
|
||||
---
|
||||
|
||||
## ข้อควรระวัง / คำแนะนำ
|
||||
|
||||
1. **ควรทดสอบในระบบทดสอบก่อน** โดยดูว่า
|
||||
- ไม่มี error ในระบบหลัก (API)
|
||||
- ฐานข้อมูลไม่ช้าผิดปกติ
|
||||
- ไม่พบปัญหาลงเวลาซ้ำซ้อน
|
||||
|
||||
2. ถ้าพบปัญหา เช่น
|
||||
- มี error ใน API → **ลด** `MaxConcurrency` เหลือ 2 หรือ 3
|
||||
- ลงเวลาซ้ำ → แจ้งทีมเทคนิคเพื่อแก้ฝั่ง API เพิ่มเติม
|
||||
|
||||
3. **ค่า `MaxConcurrency = 5` เป็นค่าปลอดภัย** เพราะระบบ API ด้านหลังยังมีข้อจำกัดอยู่บางส่วน หากต้องการเพิ่มให้สูงกว่านี้ (เช่น 20–50) ควรปรึกษาทีมเทคนิคเพื่อปรับปรุงฝั่ง API ก่อน
|
||||
|
|
@ -18,6 +18,13 @@ var user = configuration["Rabbit:User"] ?? "";
|
|||
var pass = configuration["Rabbit:Password"] ?? "";
|
||||
var queue = configuration["Rabbit:Queue"] ?? "basic-queue";
|
||||
|
||||
// Concurrency & prefetch (configurable via appsettings.json)
|
||||
var maxConcurrency = int.TryParse(configuration["MaxConcurrency"], out var c) && c > 0 ? c : 5;
|
||||
var prefetchCount = ushort.TryParse(configuration["PrefetchCount"], out var p) && p > 0 ? p : (ushort)20;
|
||||
var httpTimeoutSec = int.TryParse(configuration["HttpTimeoutSeconds"], out var t) && t > 0 ? t : 60;
|
||||
|
||||
WriteToConsole($"Config -> MaxConcurrency: {maxConcurrency}, PrefetchCount: {prefetchCount}, HttpTimeout: {httpTimeoutSec}s");
|
||||
|
||||
// create connection
|
||||
var factory = new ConnectionFactory()
|
||||
{
|
||||
|
|
@ -32,39 +39,61 @@ using var channel = connection.CreateModel();
|
|||
|
||||
channel.QueueDeclare(queue: queue, durable: true, exclusive: false, autoDelete: false, arguments: null);
|
||||
|
||||
// Create a SINGLE static HttpClient instance to prevent socket exhaustion
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(300); // 5 นาที
|
||||
// Prefetch: RabbitMQ จะส่ง message หลายตัวมาที่ consumer พร้อมกัน (ลด network round-trip)
|
||||
channel.BasicQos(prefetchSize: 0, prefetchCount: prefetchCount, global: false);
|
||||
|
||||
// HttpClient แบบ SocketsHttpHandler พร้อม connection pooling รองรับ concurrent requests
|
||||
var socketsHandler = new SocketsHttpHandler
|
||||
{
|
||||
MaxConnectionsPerServer = maxConcurrency * 2,
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
|
||||
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
using var httpClient = new HttpClient(socketsHandler);
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(httpTimeoutSec);
|
||||
|
||||
// SemaphoreSlim คุมจำนวน message ที่ประมวลผลพร้อมกัน (เนื่องจาก API มีข้อจำกัดเรื่อง concurrency)
|
||||
using var semaphore = new SemaphoreSlim(maxConcurrency, maxConcurrency);
|
||||
|
||||
var consumer = new AsyncEventingBasicConsumer(channel);
|
||||
|
||||
consumer.Received += async (model, ea) =>
|
||||
consumer.Received += (model, ea) =>
|
||||
{
|
||||
try
|
||||
// รอ semaphore ก่อนเริ่มประมวลผล
|
||||
semaphore.WaitAsync().ContinueWith(async _ =>
|
||||
{
|
||||
var body = ea.Body.ToArray();
|
||||
var message = Encoding.UTF8.GetString(body);
|
||||
|
||||
WriteToConsole($"Received message: {message}");
|
||||
|
||||
var success = await CallRestApi(message, httpClient, configuration);
|
||||
|
||||
if (success)
|
||||
try
|
||||
{
|
||||
channel.BasicAck(ea.DeliveryTag, multiple: false);
|
||||
WriteToConsole("Message processed successfully");
|
||||
var body = ea.Body.ToArray();
|
||||
var message = Encoding.UTF8.GetString(body);
|
||||
|
||||
WriteToConsole($"Received message: {message}");
|
||||
|
||||
var success = await CallRestApi(message, httpClient, configuration);
|
||||
|
||||
if (success)
|
||||
{
|
||||
channel.BasicAck(ea.DeliveryTag, multiple: false);
|
||||
WriteToConsole("Message processed successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false);
|
||||
WriteToConsole("Message processing failed - message rejected");
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteToConsole($"Error processing message: {ex.Message}");
|
||||
channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false);
|
||||
WriteToConsole("Message processing failed - message rejected");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteToConsole($"Error processing message: {ex.Message}");
|
||||
channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}, TaskScheduler.Default).ConfigureAwait(false);
|
||||
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
channel.BasicConsume(queue: queue, autoAck: false, consumer: consumer);
|
||||
|
|
|
|||
|
|
@ -5,5 +5,8 @@
|
|||
"Password": "12345678",
|
||||
"Queue": "hrms-checkin-queue-dev"
|
||||
},
|
||||
"API": "https://localhost:7283/api/v1"
|
||||
"API": "https://localhost:7283/api/v1",
|
||||
"MaxConcurrency": 5,
|
||||
"PrefetchCount": 20,
|
||||
"HttpTimeoutSeconds": 60
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue