กลับไปหน้าบทความ
#Node.js#Database#Connection Pool#Performance

แก้ API ช้าใน Node.js ด้วยการวัด Connection Pool และ Query แยกกัน

แยกเวลารอ connection ออกจากเวลารัน SQL ควบคุม concurrency, batch และ transaction เพื่อไม่ให้ Promise.all หรือ pool ที่ใหญ่เกินไปกดฐานข้อมูลจนช้า

9 กรกฎาคม 2569อ่านประมาณ 3 นาที

แชร์บทความ

แก้ API ช้าใน Node.js ด้วยการวัด Connection Pool และ Query แยกกัน

ภาพรวม

เวลา API ช้า หลายทีมรีบเปิดไฟล์ SQL ก่อนเลย แต่ใน Node.js ปัญหา DB ช้าไม่ได้อยู่ที่ query อย่างเดียวครับ หลัง Express/NestJS อาจ p95 พุ่งและ timeout เป็นระยะ 🚦

สาเหตุที่เจอบ่อยคือ connection pool ถูกใช้ผิด pool คือชุด connection ที่ Node.js เปิดค้างไว้ให้ยืมไปคุยกับฐานข้อมูล ข้อดีคือไม่ต้องสร้าง connection ใหม่ทุก request ข้อเสียคือถ้า request พร้อมกันเยอะเกิน ทุกอย่างจะไปต่อคิวหน้า pool แทน

ตัวอย่างง่าย ๆ pool size = 10 endpoint หนึ่งยิง query 3 ครั้งต่อ request ถ้ามี 20 request เข้ามาพร้อมกัน มันอาจพยายามใช้ connection 60 รอบในช่วงสั้น ๆ ส่วนเกินจะรอคิว เงียบ ๆ อยู่ในแอป

นี่คือเหตุผลที่ “เพิ่ม pool size” ไม่ใช่คำตอบเสมอไป ถ้าเพิ่มจาก 10 เป็น 100 Node.js อาจดูโล่งขึ้นชั่วคราว แต่ PostgreSQL/MySQL อาจรับ connection เยอะเกิน lock รอหนักขึ้น และ DB ทั้งก้อนช้าลงแทน 😵

workflow ที่ดีกว่าคือเริ่มจากวัดก่อน แยกเวลาเป็น 3 ส่วน

  1. เวลารอ connection จาก pool
  2. เวลาที่ query รันจริงใน DB
  3. เวลาที่ endpoint ใช้หลังได้ข้อมูลกลับมา ถ้าวัดรวมเป็น “DB time” อย่างเดียว เราจะไม่รู้ว่าช้าที่ SQL หรือช้าที่ queue หน้า pool

อีก pitfall ใหญ่คือ Promise.all หลายคนเขียนแบบนี้เพราะอยากให้เร็ว แต่ถ้าทุกตัวคือ query DB Promise.all คือการบอกแอปว่า “ยิงให้หมดเดี๋ยวนี้” ถ้ามี 100 requests พร้อมกัน จาก parallel กลายเป็น DB storm ได้ทันที ⚡

ทางเลือกคือรวม query เมื่อทำได้ ใช้ JOIN หรือ IN query แทน query ทีละ item เช่นโหลด profile ของ userIds 100 คน อย่าวน await ทีละคน และอย่า Promise.all 100 queries ให้ยิง SELECT ... WHERE id IN (...) รอบเดียวก่อน

แต่ batching ก็ต้องมีเพดาน batch ใหญ่เกินไปอาจกิน memory, lock นาน, result ใหญ่เกิน ตั้ง batch size เช่น 100, 500, 1000 ตามชนิดงาน แล้ววัด p95/p99 จริง สำหรับงานเขียนจำนวนมาก ลองใช้ bulk insert หรือ transaction แบบแบ่งก้อน แทน transaction ยาว ๆ หนึ่งก้อนมหึมา

transaction เป็นอีกจุดที่ทำให้ pool ตันแบบไม่รู้ตัว เมื่อเริ่ม transaction แล้ว connection จะถูกยึดไว้จน commit/rollback ถ้าข้างในมี await เรียก API ภายนอก หรือทำ business logic นาน ๆ connection นั้นจะนั่งรอไปด้วย ผลคือ request อื่นเข้า DB ไม่ได้ แม้ DB ยังว่างอยู่

กฎง่าย ๆ คือ transaction ควรสั้นและชัด เตรียมข้อมูลให้พร้อมก่อนเข้า transaction เข้าไปทำเฉพาะ read/write ที่ต้อง atomic แล้วออกมาเร็วที่สุด อย่าใส่ HTTP call, file upload, sleep, หรือ loop ใหญ่ไว้ข้างใน

เรื่อง timeout ต้องแยกให้ถูกด้วย query timeout ฝั่ง client ช่วยไม่ให้ request ค้างตลอดไป แต่ถ้า cancel ไม่ถึง DB query อาจยังรันอยู่ข้างหลัง กิน CPU/lock ต่อไป PostgreSQL มี statement_timeout สำหรับจำกัดเวลารัน statement ที่ DB ฝั่ง Node.js ควรตั้ง timeout และส่ง cancellation ให้ถูกเมื่อ request ถูกยกเลิก

policy ที่ใช้ได้จริง API อ่านทั่วไป timeout 2-5 วินาที admin report แยก queue หรือ worker อย่าให้ report หนัก ๆ แย่ง pool ของ API สำคัญ

observability ควรดูเป็นราย endpoint ไม่ใช่ดู average ทั้งระบบ เก็บ metric เช่น pool wait, active connection, query duration, timeout count, cancellation count แล้วดู p95/p99 แยกตาม route 📊

ถ้าเริ่ม debug production ให้ไล่แบบนี้ ดูว่า pool saturated ไหม ดู endpoint ที่ใช้ connection นานผิดปกติ ดู slow queries ใน DB ดู transaction ที่เปิดค้าง ดูจำนวน concurrent queries ต่อ request แล้วค่อยตัดสินใจว่าจะปรับ SQL, ลด concurrency, หรือเพิ่ม index

สรุปคือ performance ของ Node.js + SQL เป็นทั้งระบบ SQL ดี แต่ pool ตัน ก็ช้า pool ใหญ่ แต่ Promise.all ถล่ม DB ก็ล่ม มี timeout แต่ไม่ cancel query ก็ยังเผา DB transaction ที่ถือ connection นาน อาจทำให้ทุก endpoint รอคิว

เริ่มจากทำให้มองเห็นก่อน วัด queue หน้า pool วัด query จริง ตั้ง timeout ให้ครบ จำกัด concurrency batch อย่างมีขนาด ✅

แนวทางนำไปใช้

  • นำแนวคิดจากโพสต์นี้ไปทดลองกับงานจริงของคุณ
  • แยกเป็นขั้นตอนเล็ก ๆ แล้วทำทีละส่วน
  • บันทึกผลลัพธ์และสิ่งที่เรียนรู้เพื่อต่อยอด

สรุป

แยกเวลารอ connection ออกจากเวลารัน SQL ควบคุม concurrency, batch และ transaction เพื่อไม่ให้ Promise.all หรือ pool ที่ใหญ่เกินไปกดฐานข้อมูลจนช้า