มหาวิทยาลัยมหามกุฏราชวิทยาลัย · ระบบสาธิต (Demo)
ข้ามไปยังเนื้อหา

รายงานผลการทดสอบประสิทธิภาพ

งวดที่ 2 · รายงานอัตโนมัติ

ดาวน์โหลด .md
← กลับไปศูนย์เอกสารสร้างอัตโนมัติ · สร้างเมื่อ 3 ตุลาคม 2569 เวลา 09:53ข้อ TOR: 4.7.6, 12.2, ก.1.4.1, ก.1.4.2

สร้างอัตโนมัติเมื่อ 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.jsv24.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
LLMTyphoon 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 บนเครื่องเดียวกัน) นอกจากนี้ระหว่างทดสอบมีงานพัฒนา/ทดสอบอื่นใช้เครื่องและเซิร์ฟเวอร์เดียวกันอยู่

วิธีทดสอบ

  1. เข้าสู่ระบบด้วยผู้ใช้ seed (GET /api/csrf → POST /api/auth/login) แล้วใช้ session cookie + CSRF token เดียวกันกับทุก virtual user (VU)
  2. Warm-up: เรียก /api/health, /th และถามแชต 1 คำถาม (ไม่นับผล latency) เพื่อให้เซิร์ฟเวอร์ compile route และโหลดดัชนีค้นคืนเข้าหน่วยความจำ
  3. HTTP: ที่แต่ละระดับ (10, 50, 100 VU พร้อมกัน) × 3 รอบ — แต่ละ VU ส่ง 3 คำขอต่อเนื่องกันต่อรอบ ไปที่ GET /api/health (ผ่าน middleware + ตรวจ session + query ฐานข้อมูล) และ GET /th (หน้าเว็บ SSR) วัดเวลาจนอ่าน body ครบ; error = สถานะไม่ใช่ 200 หรือ timeout
  4. แชต: ที่แต่ละระดับ (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 = เวลาตั้งแต่ส่งคำขอจนได้ event delta แรก และเวลารวมจนจบ stream; error = สถานะไม่ใช่ 200, มี event error, หรือไม่มี event done (timeout 120 วินาที)
  5. 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)คำขอp50p95maxp95 รายรอบError rate429คำขอ/วินาที
109069 ms85 ms86 ms86 ms / 76 ms / 74 ms0.0%0140.3
50450154 ms701 ms711 ms262 ms / 708 ms / 224 ms0.0%0206.0
100900343 ms1.27 s1.33 s381 ms / 1.03 s / 1.29 s0.0%0188.2

GET /th (หน้าแรก, SSR)

Concurrent (VU)คำขอp50p95maxp95 รายรอบError rate429คำขอ/วินาที
1090464 ms1.53 s1.53 s645 ms / 1.53 s / 466 ms0.0%017.0
504502.59 s3.32 s3.33 s2.69 s / 3.14 s / 3.33 s0.0%019.3
1009007.09 s12.04 s12.07 s7.12 s / 7.89 s / 12.05 s0.0%013.3

ทุกคำขอตอบสถานะ 200

latency คำนวณจากคำขอที่สำเร็จ (สถานะ 200) เท่านั้น

ผลการทดสอบแชต (POST /api/chat/qa, SSE, LLM จริง)

Concurrent (VU)คำขอTTFT p50TTFT p95TTFT maxเวลารวม p50เวลารวม p95Error rateถูกปฏิเสธโดย guardrailTTFT ≤ 15 วิ (p95)Error ≤ 5%
5151.37 s1.89 s1.89 s6.50 s10.36 s0.0%0ผ่านผ่าน
10301.62 s2.66 s2.73 s7.21 s10.37 s0.0%0ผ่านผ่าน
20602.58 s11.15 s13.32 s15.28 s21.79 s0.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 p50FCP p95FCP maxเกณฑ์ ≤ 5 วิ
/th5128 ms132 ms132 msผ่าน
/th/chat5128 ms1.78 s1.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 ms1,000 ครั้งแบบสุ่ม
ค้นหาด้วย emailHash — ไม่พบp50 2.9 µs · p95 84.2 µs1,000 ครั้ง (อีเมลที่ไม่มีในระบบ)
COUNT(*) ทั้งตาราง2.41 msมัธยฐานจาก 3 ครั้ง
รายการแบ่งหน้า ORDER BY createdAt DESC LIMIT 50 OFFSET 0132 msมัธยฐานจาก 3 ครั้ง
รายการแบ่งหน้า ORDER BY createdAt DESC LIMIT 50 OFFSET 1,00095 msมัธยฐานจาก 3 ครั้ง
รายการแบ่งหน้า ORDER BY createdAt DESC LIMIT 50 OFFSET 500,0002.80 sมัธยฐานจาก 3 ครั้ง
รายการแบ่งหน้า ORDER BY createdAt DESC LIMIT 50 OFFSET 999,9503.38 sมัธยฐานจาก 3 ครั้ง
นับตามตัวกรอง role=GENERAL_USER, status=active21 ms (969,000 แถว)ใช้ดัชนี (role, status)
นับตามตัวกรอง role=GENERAL_USER, status=suspended0.28 ms (20,000 แถว)ใช้ดัชนี (role, status)
นับตามตัวกรอง role=ADMIN, status=active18.0 µs (1,000 แถว)ใช้ดัชนี (role, status)
กรอง role=GENERAL_USER, status=suspended LIMIT 5031.3 µsใช้ดัชนี (role, status)
อ่านทั้งตาราง SELECT * … ORDER BY createdAt DESC2.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)