สร้างอัตโนมัติเมื่อ 2026-10-03T02:53:27Z
รายงานผลการทดสอบประสิทธิภาพ
อ้างอิง TOR: ข้อ 4.7.6 (Performance Test ≥ 500 concurrent), ข้อ 12.2 (FCP ≤ 5 วินาที / TTFT ≤ 15 วินาที / Error ≤ 5% ที่ p95), ข้อ ก.1.4.1 (รองรับ concurrent ≥ 500), ข้อ ก.1.4.2 (รองรับ 1,000,000 บัญชี)
รายงานนี้สร้างโดย scripts/perf/load-test.ts (npm run report:perf) จากการยิงคำขอจริงไปยังเซิร์ฟเวอร์ที่รันอยู่ และ scripts/perf/seed-perf-users.ts (npm run seed:perf-users) สำหรับหัวข้อจำนวนบัญชีผู้ใช้ ตัวเลขทั้งหมดเป็นค่าที่วัดได้จริงในสภาพแวดล้อมสาธิต (เครื่องเดียว) — ยังไม่ใช่ผลทดสอบที่ 500 concurrent บนสภาพแวดล้อม production ซึ่งต้องทดสอบซ้ำก่อนใช้งานจริง
สรุปผลเทียบเกณฑ์ TOR 12.2
| ตัวชี้วัด | เกณฑ์ | ค่าที่วัดได้ (แย่ที่สุดในทุกระดับที่ทดสอบ) | ระดับโหลดที่ทดสอบ | ผล |
|---|---|---|---|---|
| FCP (p95) | ≤ 5 วินาที | 1.78 s | ผู้ใช้ 1 ราย (browser จริง) หน้า /th, /th/chat | ผ่าน |
| TTFT (p95) | ≤ 15 วินาที | 11.15 s | แชต 5/10/20 concurrent | ผ่าน |
| Error rate — แชต (SSE) | ≤ 5% | 0.0% | สูงสุด 20 concurrent | ผ่าน |
Error rate — HTTP (/api/health, /th) | ≤ 5% | 0.0% | สูงสุด 100 concurrent | ผ่าน |
ข้อสังเกตสำคัญ: ผลข้างต้นเป็นผลที่ระดับโหลดของ sandbox (HTTP สูงสุด 100 concurrent, แชตสูงสุด 20 concurrent) เกณฑ์ TOR 4.7.6 / ก.1.4.1 กำหนด 500 concurrent ซึ่ง ยังไม่ได้ทดสอบ ใน demo นี้ — ดูหัวข้อ "การประมาณผลที่ 500 concurrent"
สภาพแวดล้อมที่ทดสอบ
| รายการ | ค่า |
|---|---|
| เครื่องทดสอบ (client และ server อยู่เครื่องเดียวกัน) | Apple M5 Pro (15 cores), RAM 24 GB |
| ระบบปฏิบัติการ | Darwin 25.4.0 (arm64) |
| Node.js | v24.19.0 |
| เซิร์ฟเวอร์ | http://localhost:3210 — Next.js dev server (next dev) |
| เวอร์ชันแอป / ดัชนีค้นคืน | 1.0.0-demo / 4,392 chunks จาก 329 เอกสาร (BM25 in-memory) |
| ฐานข้อมูล | SQLite (ไฟล์เดียว, WAL) ผ่าน Prisma — production ใช้ PostgreSQL |
| LLM | Typhoon API (typhoon-v2.5-30b-a3b-instruct) — LLM จริง เรียกบริการภายนอกผ่านอินเทอร์เน็ต |
| ระยะเวลาทดสอบรวม | 280 วินาที |
เซิร์ฟเวอร์ที่ทดสอบเป็น Next.js dev server (
next dev) ซึ่งไม่ได้ minify/optimize, compile หน้าแบบ on-demand และมี overhead ของ HMR — latency และ FCP จึงสูงกว่า production build (next build && next start) อย่างมีนัยสำคัญ ตัวเลขในรายงานนี้จึงเป็นค่าฝั่งอนุรักษ์ (แย่กว่าที่จะได้จาก production build บนเครื่องเดียวกัน) นอกจากนี้ระหว่างทดสอบมีงานพัฒนา/ทดสอบอื่นใช้เครื่องและเซิร์ฟเวอร์เดียวกันอยู่
วิธีทดสอบ
- เข้าสู่ระบบด้วยผู้ใช้ seed (GET
/api/csrf→ POST/api/auth/login) แล้วใช้ session cookie + CSRF token เดียวกันกับทุก virtual user (VU) - Warm-up: เรียก
/api/health,/thและถามแชต 1 คำถาม (ไม่นับผล latency) เพื่อให้เซิร์ฟเวอร์ compile route และโหลดดัชนีค้นคืนเข้าหน่วยความจำ - HTTP: ที่แต่ละระดับ (10, 50, 100 VU พร้อมกัน) × 3 รอบ — แต่ละ VU ส่ง 3 คำขอต่อเนื่องกันต่อรอบ ไปที่
GET /api/health(ผ่าน middleware + ตรวจ session + query ฐานข้อมูล) และGET /th(หน้าเว็บ SSR) วัดเวลาจนอ่าน body ครบ; error = สถานะไม่ใช่ 200 หรือ timeout - แชต: ที่แต่ละระดับ (5, 10, 20 VU พร้อมกัน) × 3 รอบ — แต่ละ VU ส่ง
POST /api/chat/qa(SSE) 1 คำถามต่อรอบ หมุนเวียนจากคำถามจริง 10 ข้อ ผ่าน pipeline เต็ม (guardrail → query rewrite → hybrid search → rerank → LLM stream → output guardrail → บันทึก) วัด TTFT = เวลาตั้งแต่ส่งคำขอจนได้ eventdeltaแรก และเวลารวมจนจบ stream; error = สถานะไม่ใช่ 200, มี eventerror, หรือไม่มี eventdone(timeout 120 วินาที) - FCP: เปิดหน้า
/thและ/th/chat(เข้าสู่ระบบแล้ว) ด้วย Chromium ผ่าน Playwright หน้าละ 5 ครั้ง (context ใหม่ทุกครั้ง = ไม่มี cache) อ่านค่าfirst-contentful-paintจากperformance.getEntriesByType('paint')
เรื่อง rate limit: middleware จำกัด 60 คำขอ/นาที/IP สำหรับ /api/* และ 10 คำขอ/นาที/IP สำหรับ login หากยิงจาก IP เดียว ผลจะถูกครอบด้วย 429 ทั้งหมดและไม่สะท้อนความสามารถของระบบ สคริปต์จึงส่ง header x-forwarded-for ที่ไม่ซ้ำกันต่อ VU (ช่วง 10.77.x.y) เพื่อจำลองผู้ใช้จากหลาย IP — rate limiter ยังเปิดทำงานตามปกติตลอดการทดสอบ (จำนวน 429 ที่พบแสดงในตาราง)
ผลการทดสอบ HTTP
GET /api/health
| Concurrent (VU) | คำขอ | p50 | p95 | max | p95 รายรอบ | Error rate | 429 | คำขอ/วินาที |
|---|---|---|---|---|---|---|---|---|
| 10 | 90 | 69 ms | 85 ms | 86 ms | 86 ms / 76 ms / 74 ms | 0.0% | 0 | 140.3 |
| 50 | 450 | 154 ms | 701 ms | 711 ms | 262 ms / 708 ms / 224 ms | 0.0% | 0 | 206.0 |
| 100 | 900 | 343 ms | 1.27 s | 1.33 s | 381 ms / 1.03 s / 1.29 s | 0.0% | 0 | 188.2 |
GET /th (หน้าแรก, SSR)
| Concurrent (VU) | คำขอ | p50 | p95 | max | p95 รายรอบ | Error rate | 429 | คำขอ/วินาที |
|---|---|---|---|---|---|---|---|---|
| 10 | 90 | 464 ms | 1.53 s | 1.53 s | 645 ms / 1.53 s / 466 ms | 0.0% | 0 | 17.0 |
| 50 | 450 | 2.59 s | 3.32 s | 3.33 s | 2.69 s / 3.14 s / 3.33 s | 0.0% | 0 | 19.3 |
| 100 | 900 | 7.09 s | 12.04 s | 12.07 s | 7.12 s / 7.89 s / 12.05 s | 0.0% | 0 | 13.3 |
ทุกคำขอตอบสถานะ 200
latency คำนวณจากคำขอที่สำเร็จ (สถานะ 200) เท่านั้น
ผลการทดสอบแชต (POST /api/chat/qa, SSE, LLM จริง)
| Concurrent (VU) | คำขอ | TTFT p50 | TTFT p95 | TTFT max | เวลารวม p50 | เวลารวม p95 | Error rate | ถูกปฏิเสธโดย guardrail | TTFT ≤ 15 วิ (p95) | Error ≤ 5% |
|---|---|---|---|---|---|---|---|---|---|---|
| 5 | 15 | 1.37 s | 1.89 s | 1.89 s | 6.50 s | 10.36 s | 0.0% | 0 | ผ่าน | ผ่าน |
| 10 | 30 | 1.62 s | 2.66 s | 2.73 s | 7.21 s | 10.37 s | 0.0% | 0 | ผ่าน | ผ่าน |
| 20 | 60 | 2.58 s | 11.15 s | 13.32 s | 15.28 s | 21.79 s | 0.0% | 0 | ผ่าน | ผ่าน |
ไม่มีคำขอแชตที่ผิดพลาด
TTFT และเวลารวมคำนวณจากคำขอที่สำเร็จ; คำขอที่ guardrail ปฏิเสธ (ไม่มี event delta) นับเป็นคำขอสำเร็จแต่ไม่นำมาคิด TTFT
โทเคนที่ใช้จริง: การทดสอบแชตเรียก LLM จริง จึงใช้โทเคนจริงของโครงการ — รวม 560,863 โทเคน จาก 106 คำขอ (input 514,236, thinking 0, output 46,627; รวม warm-up 4,044 โทเคนแล้ว) นับจาก usage ใน event done ของทุกคำขอ และถูกบันทึกในตาราง TokenUsage / นับรวมในเพดานโทเคน 24 ชม. ของระบบตามปกติ
First Contentful Paint (FCP)
Browser: Chromium ที่มากับ Playwright 153.0.8010.12 (headless, viewport 1280×800, ไม่มี cache)
| หน้า | จำนวนครั้ง | FCP p50 | FCP p95 | FCP max | เกณฑ์ ≤ 5 วิ |
|---|---|---|---|---|---|
/th | 5 | 128 ms | 132 ms | 132 ms | ผ่าน |
/th/chat | 5 | 128 ms | 1.78 s | 1.78 s | ผ่าน |
FCP วัดขณะไม่มีโหลดอื่นจากสคริปต์นี้ (ผู้ใช้ 1 ราย) — ยังไม่ได้วัด FCP ขณะมีโหลด 500 concurrent
การประมาณผลที่ 500 concurrent (TOR 4.7.6, ก.1.4.1)
สิ่งที่วัดจริง: /api/health ที่ 100 VU: p95 1.27 s, error 0.0%, 188 คำขอ/วินาที; /th ที่ 100 VU: p95 12.04 s, error 0.0%; แชตที่ 20 VU: TTFT p95 11.15 s, error 0.0%
สิ่งที่ไม่ได้วัด: โหลด 500 concurrent ทั้ง HTTP และแชต — demo นี้ ไม่อ้างว่าผ่านเกณฑ์ 500 concurrent การประมาณด้านล่างเป็นการวิเคราะห์คอขวด ไม่ใช่ผลทดสอบ
คอขวดของสถาปัตยกรรม demo ที่ทำให้ไม่สามารถขยายผลเชิงเส้นไปถึง 500 ได้:
| คอขวดใน demo | ผลเมื่อโหลดเพิ่ม | สิ่งที่ใช้ใน production (Solution Architecture) |
|---|---|---|
| Node.js process เดียว (event loop เดียว) รับทั้งหน้าเว็บ, API, SSE และงานค้นคืน | งานที่ใช้ CPU (BM25, rerank, SSR) บล็อกคำขออื่นทั้งหมด latency เพิ่มตามคิว | แยก 8 services บน Kubernetes พร้อม HPA ขยาย pod ตามโหลด |
| ดัชนี BM25 + pseudo-embedding อยู่ในหน่วยความจำของ process เดียว | ค้นคืนเป็นงาน synchronous บน event loop และขยายแนวนอนไม่ได้ | Elasticsearch (hybrid BM25 + vector) และบริการ embedding/rerank แยก |
| SQLite single-writer (WAL) | ทุกคำถามเขียนหลายตาราง (ข้อความ, โทเคน, audit, request log) — การเขียนเข้าคิวทีละรายการ เสี่ยง SQLITE_BUSY เมื่อ concurrent สูง | PostgreSQL + connection pool |
| Rate limit และยอดจองโทเคนเก็บใน memory ของ process | ใช้ร่วมกันหลาย instance ไม่ได้ | Redis |
| โควตา/rate limit ของ LLM API ภายนอก (ใช้ร่วมกับผู้ใช้รายอื่นของผู้ให้บริการ) — 1 คำถามเรียก LLM 3 ครั้ง (guard classifier, query rewrite, generate) | ที่ 500 concurrent = ~1,500 คำขอ LLM พร้อมกัน เกินโควตา API สาธารณะ → 429/timeout และ TTFT สูงขึ้น | LLM endpoint ที่จัดสรรเฉพาะ (dedicated/self-hosted) ตามขนาดที่ออกแบบ + คิว/แบ็กออฟ |
| Next.js dev server | ช้ากว่า production build หลายเท่า | production build หลัง CDN/Ingress |
ข้อสรุปอย่างตรงไปตรงมา: ส่วน HTTP/หน้าเว็บไม่พบความผิดพลาดเกินเกณฑ์ที่ระดับสูงสุดที่ทดสอบ (100 VU) แต่ latency เพิ่มตามจำนวน VU เพราะเป็น process เดียว ส่วนเส้นทางแชตถูกจำกัดด้วย LLM API และ process เดียวเป็นหลัก จึง ไม่สามารถยืนยันจาก demo นี้ว่าระบบผ่านเกณฑ์ที่ 500 concurrent การยืนยัน TOR 4.7.6 / ก.1.4.1 ต้องทำ load test ที่ 500 concurrent บนสภาพแวดล้อม production (หรือ staging ที่เทียบเท่า) ด้วยสคริปต์เดียวกันนี้ (PERF_LEVELS=100,250,500 PERF_CHAT_LEVELS=100,250,500 PERF_CHAT_MAX_REQUESTS=...) ก่อนส่งมอบงวดที่เกี่ยวข้อง
ผลการทดสอบรองรับบัญชีผู้ใช้จำนวนมาก (ก.1.4.2)
ทดสอบเมื่อ 2026-10-03T02:54:59Z ด้วย
scripts/perf/seed-perf-users.ts --count=1000000(npm run seed:perf-users -- --count=1000000)
สร้างบัญชีผู้ใช้ 1,000,000 บัญชี ลงฐานข้อมูลทดสอบแยก prisma/perf.db (schema เดียวกับระบบจริงจาก prisma migrate deploy; ไม่แตะฐานข้อมูลของ demo) แล้ววัดเวลา query ที่เกี่ยวกับจำนวนผู้ใช้โดยตรง
| รายการ | ผลที่วัดได้ | หมายเหตุ |
|---|---|---|
| จำนวนบัญชีที่สร้าง | 1,000,000 (ขอ 1,000,000) | ครบ |
| เวลา insert รวม | 65.73 s (15,215 แถว/วินาที) | batch ละ 5,000 แถว/transaction ผ่าน node:sqlite (Node v24.19.0) |
| ขนาดไฟล์ฐานข้อมูล | 580.7 MB | ≈ 609 bytes/บัญชี รวมดัชนี 4 ตัวของตาราง User |
| ค้นหาด้วย emailHash (เส้นทางล็อกอิน) — พบ | p50 80.8 µs · p95 0.17 ms · max 1.26 ms | 1,000 ครั้งแบบสุ่ม |
| ค้นหาด้วย emailHash — ไม่พบ | p50 2.9 µs · p95 84.2 µs | 1,000 ครั้ง (อีเมลที่ไม่มีในระบบ) |
COUNT(*) ทั้งตาราง | 2.41 ms | มัธยฐานจาก 3 ครั้ง |
รายการแบ่งหน้า ORDER BY createdAt DESC LIMIT 50 OFFSET 0 | 132 ms | มัธยฐานจาก 3 ครั้ง |
รายการแบ่งหน้า ORDER BY createdAt DESC LIMIT 50 OFFSET 1,000 | 95 ms | มัธยฐานจาก 3 ครั้ง |
รายการแบ่งหน้า ORDER BY createdAt DESC LIMIT 50 OFFSET 500,000 | 2.80 s | มัธยฐานจาก 3 ครั้ง |
รายการแบ่งหน้า ORDER BY createdAt DESC LIMIT 50 OFFSET 999,950 | 3.38 s | มัธยฐานจาก 3 ครั้ง |
| นับตามตัวกรอง role=GENERAL_USER, status=active | 21 ms (969,000 แถว) | ใช้ดัชนี (role, status) |
| นับตามตัวกรอง role=GENERAL_USER, status=suspended | 0.28 ms (20,000 แถว) | ใช้ดัชนี (role, status) |
| นับตามตัวกรอง role=ADMIN, status=active | 18.0 µs (1,000 แถว) | ใช้ดัชนี (role, status) |
| กรอง role=GENERAL_USER, status=suspended LIMIT 50 | 31.3 µs | ใช้ดัชนี (role, status) |
อ่านทั้งตาราง SELECT * … ORDER BY createdAt DESC | 2.11 s | วิธีที่หน้า Admin › Users ของ demo ใช้อยู่ (ดูข้อจำกัด) |
Query plan (SQLite EXPLAIN QUERY PLAN):
- ล็อกอิน:
SEARCH User USING INDEX User_emailHash_key (emailHash=?) - รายการแบ่งหน้า:
SCAN User; USE TEMP B-TREE FOR ORDER BY
การตีความ:
- เส้นทางล็อกอินค้นด้วยดัชนี unique ของ
emailHash(B-tree) เวลาค้นจึงแทบไม่ขึ้นกับจำนวนบัญชี — ที่ 1,000,000 บัญชี p95 = 0.17 ms ต่อครั้ง เวลาล็อกอินจริงถูกกำหนดโดยการตรวจรหัสผ่านด้วย bcrypt ซึ่งใช้เวลามากกว่าการค้นฐานข้อมูลหลายเท่าและไม่ขึ้นกับจำนวนบัญชี - การนับ/กรองตาม role + status ใช้ดัชนี
(role, status) - รายการแบ่งหน้าเรียงตาม
createdAtไม่มีดัชนีรองรับ จึงสแกนทั้งตารางและช้าลงตามจำนวนบัญชีและ OFFSET — production ต้องเพิ่มดัชนีcreatedAtและใช้ keyset pagination
ข้อจำกัดที่ต้องระบุอย่างตรงไปตรงมา:
- ข้อมูลผู้ใช้เป็นข้อมูลจำลอง:
emailHash= SHA-256 ของอีเมลที่สร้างขึ้น (เหมือนระบบจริง) แต่emailEnc/displayNameEnc/passwordHashเป็นค่าจำลองที่ยาวเท่าของจริง ไม่ได้เข้ารหัส AES-256-GCM หรือ bcrypt จริง — ใช้วัดขนาดข้อมูลและความเร็ว query ได้ แต่ไม่ได้วัดต้นทุนการถอดรหัส - วัดที่ชั้นฐานข้อมูล (SQLite ไฟล์เดียว, ผู้ใช้ 1 connection, ไม่มีโหลดอื่น) ไม่ได้ทดสอบผ่าน HTTP API และไม่ได้ทดสอบการล็อกอินพร้อมกันจำนวนมากบนฐานข้อมูลขนาดนี้
- หน้า Admin › Users ของ demo (
listUsersในsrc/application/identity/admin-user-usecases.ts) โหลดผู้ใช้ ทั้งตาราง แล้วถอดรหัสและค้นหา/แบ่งหน้าในหน่วยความจำ เพราะอีเมล/ชื่อถูกเข้ารหัส — เฉพาะการอ่านทั้งตารางที่ 1,000,000 บัญชีใช้ 2.11 s (ยังไม่รวมการถอดรหัสทีละแถวและหน่วยความจำ) วิธีนี้ ไม่รองรับ 1,000,000 บัญชี ต้องเปลี่ยนเป็นการแบ่งหน้าที่ฐานข้อมูล + blind index สำหรับค้นหาก่อนใช้งานจริง - production ใช้ PostgreSQL (ไม่ใช่ SQLite) จึงต้องทดสอบซ้ำบนฐานข้อมูลจริงด้วยข้อมูลขนาดเดียวกัน ผลในหัวข้อนี้ยืนยันได้เพียงว่า schema และดัชนีของเส้นทางล็อกอินรองรับข้อมูลระดับนี้
ไฟล์ prisma/perf.db ถูกเก็บไว้ให้ตรวจสอบซ้ำได้ (อยู่ใน .gitignore; ลบได้ด้วย rm prisma/perf.db*)
วิธีรันซ้ำ
npm run report:perf # load test + TTFT + FCP (ต้องมีเซิร์ฟเวอร์รันอยู่; แชตใช้โทเคนจริง)
PERF_SKIP_CHAT=1 npm run report:perf # เฉพาะ HTTP + FCP ไม่ใช้โทเคน
PERF_LEVELS=10,50,100 PERF_CHAT_LEVELS=5,10,20 PERF_ROUNDS=3 BASE_URL=http://localhost:3210 npm run report:perf
npm run seed:perf-users -- --count=1000000 # ทดสอบจำนวนบัญชีผู้ใช้ (เขียนหัวข้อ ก.1.4.2)