แก้ปัญหา 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 pass = configuration["Rabbit:Password"] ?? "";
|
||||||
var queue = configuration["Rabbit:Queue"] ?? "basic-queue";
|
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
|
// create connection
|
||||||
var factory = new ConnectionFactory()
|
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);
|
channel.QueueDeclare(queue: queue, durable: true, exclusive: false, autoDelete: false, arguments: null);
|
||||||
|
|
||||||
// Create a SINGLE static HttpClient instance to prevent socket exhaustion
|
// Prefetch: RabbitMQ จะส่ง message หลายตัวมาที่ consumer พร้อมกัน (ลด network round-trip)
|
||||||
using var httpClient = new HttpClient();
|
channel.BasicQos(prefetchSize: 0, prefetchCount: prefetchCount, global: false);
|
||||||
httpClient.Timeout = TimeSpan.FromSeconds(300); // 5 นาที
|
|
||||||
|
// 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);
|
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();
|
try
|
||||||
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);
|
var body = ea.Body.ToArray();
|
||||||
WriteToConsole("Message processed successfully");
|
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);
|
channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false);
|
||||||
WriteToConsole("Message processing failed - message rejected");
|
|
||||||
}
|
}
|
||||||
}
|
finally
|
||||||
catch (Exception ex)
|
{
|
||||||
{
|
semaphore.Release();
|
||||||
WriteToConsole($"Error processing message: {ex.Message}");
|
}
|
||||||
channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false);
|
}, TaskScheduler.Default).ConfigureAwait(false);
|
||||||
}
|
|
||||||
|
return Task.CompletedTask;
|
||||||
};
|
};
|
||||||
|
|
||||||
channel.BasicConsume(queue: queue, autoAck: false, consumer: consumer);
|
channel.BasicConsume(queue: queue, autoAck: false, consumer: consumer);
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,8 @@
|
||||||
"Password": "12345678",
|
"Password": "12345678",
|
||||||
"Queue": "hrms-checkin-queue-dev"
|
"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