สวัสดีครับ หลายคนอาจจะเคยเจอปัญหาเวลา Deploy Gunicorn (Python) บน Cloud Run แล้ว Instance มัน Scale-out (Spawn เครื่องใหม่) เร็วเกินไป ทั้งๆ ที่เครื่องเก่ายังรับโหลดได้อีก หรือเจอปัญหา RAM เต็ม, CPU พุ่ง โดยไม่รู้สาเหตุ
ทีมเราได้ใช้เวลาทดลองและจูนค่าต่างๆ (โดยเฉพาะจากโปรเจกต์ UpPass) จนตกผลึกเป็น “สูตร” และแนวทางที่ค่อนข้างลงตัว เลยอยากมาแชร์ให้ทุกคน
🎯 “สูตร” การตั้งค่า Gunicorn + Cloud Run
นี่คือหลักการ 3 ข้อที่เรายึดเป็นแนวทางในการจูนครับ
- Gunicorn Workers: เริ่มต้นที่
Workers = จำนวน vCPU(เช่น 2vCPU = 2 Workers) - Gunicorn Threads: นี่คือ “ตัวแปร” ที่เราใช้จูน โดยมีเป้าหมายคือ
CPU Utilization ≈ 40% - 50% - Cloud Run Concurrency: ต้องตั้งค่าให้เท่ากับสูตร
Workers * Threadsเสมอ
🚨 กฎทองคำ (The Golden Rule): “ห้ามให้ CPU Utilization เฉลี่ยเกิน 60%”
เพราะอะไร? ค่า Default ของ Cloud Run Autcaler จะพยายามรักษา CPU เฉลี่ยไว้ที่ 60% ทันทีที่ CPU ของคุณแตะ 60% ระบบจะตัดสินว่า “Instance นี้งานหนัก” และจะเริ่ม Scale-out (สร้างเครื่องใหม่) ทันที… แม้ว่าเครื่องนั้นจะยังรับไหวก็ตาม
นี่คือสาเหตุหลักที่ทำให้ “เครื่อง Spawn ใหม่โดยไม่จำเป็น” ครับ
🧠 ทบทวน Definitions (Workers vs Threads)
gunicorn workers
- จำนวน Process ที่ Spawn ขึ้นมาเพื่อรับ Load
- มาก = เปลือง Memory (เพราะทุก Worker คือการ “คัดลอก” แอปของเราทั้งตัวขึ้น RAM ใหม่)
gunicorn threads
- คือ จำนวน Thread (ผู้ช่วย) ต่อ 1 Worker เพื่อเพิ่ม Concurrency
- มาก = เปลือง CPU (ไม่ใช่เพราะ Thread เอง แต่เพราะ “Context Switching” หรือ “ต้นทุนการสลับงาน” ที่ CPU ต้องทำเพื่อดูแลหลาย Threads)
💡 Q&A จากปัญหาจริงที่เราเจอ (UpPass Project)
นี่คือ Case Study และวิธีแก้ปัญหาที่เราเจอมาครับ
Q: ปรับ Workers 5 ตัว (เครื่อง 2vCPU) ทำให้ใช้ Memory พุ่งไปถึง 3GB แต่ไม่สามารถรับโหลดได้ดี?
A: เราตั้ง Worker เยอะเกินไป ทำให้ Memory ชนเพดาน
วิธีแก้: ให้ลด Workers เหลือ 2 ตัว (เท่า vCPU) Memory จะลดลงเหลือราวๆ 600MB ทันที (เพราะโหลดแอปขึ้น RAM แค่ 2 ชุด)
Q: ปรับ Workers เหลือ 2 ตัว แล้ว Threads เป็น 5 (รวม 10 Concurrent) ใช้ Memory น้อยแล้ว แต่รับโหลดต่อเครื่องยังไม่เต็มเลย เครื่องก็ Spawn ใหม่แล้ว?
A: นี่คืออาการของ “CPU แตะ 60%”
วิธีแก้: การตั้ง Threads = 5 ทำให้ “ต้นทุนการสลับงาน” (Context Switching) สูงเกินไป จน CPU พุ่งแตะ 60% ระบบเลยรีบ Spawn เครื่องใหม่ ให้ลองลด Threads ลง (เช่น 4, 3, 2) แล้ว Load Test ใหม่ จนกว่าจะเจอจุดที่ CPU อยู่ที่ 40-50% และอย่าลืมปรับ Cloud Run Concurrency ให้เท่ากันด้วย (เช่น w=2, t=3 -> ตั้ง Concurrency เป็น 6)
Q: ถ้างานส่วนใหญ่เป็นงาน I/O (รอ DB, รอ API) ให้ทำยังไง?
A: งาน I/O เหมาะกับการใช้ Thread มาก เพราะ Thread ส่วนใหญ่จะ “นอนรอ” (ไม่ใช้ CPU)
วิธีแก้: ให้เพิ่มจำนวน Threads เข้าไป (เช่น w=2, t=10) และ เพิ่ม Cloud Run Concurrency (เป็น 20) เพื่อให้รับมือการ “รอ” พร้อมกันได้เยอะขึ้น (ตราบใดที่ CPU ยังไม่เกิน 60%)
Q: ถ้าเป็นแอปที่ใช้ Memory มากๆ (เช่น โหลด Model ML, Cache) ควรเซทยังไง?
A: ต้องประหยัด RAM ให้มากที่สุด
วิธีแก้: ให้ใช้ Worker น้อยๆ (อาจจะแค่ 1 ตัว) แล้วไปเน้น เพิ่มจำนวน Threads แทน เพราะ Threads ทั้งหมดจะ “แชร์ Memory” ก้อนเดียวกันจาก Worker แม่
Q: ตั้งเครื่องใหญ่ๆ (4vCPU) หรือเครื่องเล็กๆ (1vCPU) ดีกว่ากัน?
A: ขึ้นอยู่กับความต้องการ
- เครื่องใหญ่ (vCPU เยอะ): เหมาะกับงานที่ใช้ Computing Power สูง (ประมวลผลหนัก) ให้ตั้ง Worker = vCPU และลด Thread ลง
- เครื่องเล็ก (vCPU น้อย): เหมาะกับ Request ที่เล็กๆ สั้นๆ หรือ I/O หนักๆ (ให้เน้นเพิ่ม Thread เอา)
🔑 Q: แล้วใครจะไปตรัสรู้ว่าต้องเซทอะไรเท่าไหร่?
A: ไม่มีใครตรัสรู้ครับ… เราต้อง “Load Test” เท่านั้น
วิธีทำ: ให้ตั้ง jMeter (หรือ k6, locust) ขึ้นมายิงโหลดเข้าไปเรื่อยๆ แล้วคอยดู Dashboard CPU/RAM จากนั้นก็ “พยายามปรับหา Balance” ที่เราต้องการ (Worker, Thread, Concurrency) ตามแนวทางข้างบน
⚙️ Bonus: เลือก Platform แบบไหนดี?
Instance Base หรือ Request Base ดี?
- Instance Base (Gen2): ถ้าหากเป็น Server ที่มีงานใช้งานตลอด (Always-on) จะถูกกว่าประมาณ 20%
- Request Base (Gen1/Gen2): เหมาะกับเครื่องโหลดน้อยๆ นานๆมาที หรือใช้ Uptime Probe ยิงเพื่อ Warm (Scale-from-zero)
Cloud Run Gen1 หรือ Gen2 ดี?
- Gen1: ถ้างานเล็กๆ เน้นประหยัด เกิดการ Spawn จาก 0 -> 1 บ่อย (Cold Start ไวกว่า)
- Gen2: ถ้าเป็นแอปพลิเคชันใหญ่, ต้องการ CPU/Memory/I-O ที่ดีและเสถียรกว่า
จบแล้ว หวังว่าจะเป็นประโยชน์กับทุกคนนะครับ!