Thread Pool Size Calculator: คำนวณขนาด Executor ด้วยสูตร N = Cores × (1 + W/C)
Thread Pool Size Calculator ช่วยหาขนาด thread pool ที่เหมาะสมจากจำนวน CPU core และอัตราส่วนเวลารอต่อเวลาคำนวณของงาน พร้อม preset สำหรับงาน IO-bound และ CPU-bound และ executor config ที่คัดลอกไปใช้ได้ทันที
Table of Contents
ทุกคนที่เคยตั้งค่า executor ในระบบ backend ย่อมเคยเจอคำถามเดียวกัน นั่นคือ pool นี้ควรมี thread กี่ตัวกันแน่? ถ้าตั้งน้อยไป CPU จะว่างเฉยขณะที่ request ต่อคิวยาวขึ้นเรื่อย ๆ แต่ถ้าตั้งมากไป ระบบจะเปลือง memory กับ thread stack และเสียเวลาไปกับ context switching มากกว่าทำงานจริง Thread Pool Size Calculator เปลี่ยนการเดาให้กลายเป็นตัวเลขที่คุณอธิบายเหตุผลได้ โดยคำนวณขนาด pool จากจำนวน CPU core และอัตราส่วนเวลารอ (wait) ต่อเวลาคำนวณ (compute) ของงานด้วยสูตรคลาสสิก N = cores × (1 + W/C)
เครื่องมือนี้ทำงานในเบราว์เซอร์ทั้งหมด ไม่ต้องสมัครสมาชิกและไม่มีข้อมูลออกจากเครื่องของคุณ เพียงกรอกจำนวน core และสัดส่วนเวลาที่งานทั่วไปใช้ระหว่างรอกับคำนวณ ก็จะได้ขนาด pool ที่แนะนำสำหรับทั้ง workload แบบ IO-bound และ CPU-bound พร้อม executor config ที่คัดลอกไปวางในโปรเจกต์ได้ทันที
บทความนี้จะพาไล่วิธีใช้เครื่องมือทีละขั้นตอน อธิบายสัญชาตญาณเบื้องหลังสูตรอย่างเข้าใจง่าย และพาไปดูสถานการณ์จริงที่ตัวเลขจากเครื่องมือนี้ช่วยประหยัดเวลาได้มากที่สุด
ทำไมต้องใช้ Thread Pool Size Calculator?
- เลิกเดาจำนวน thread. ตัวเลขวิเศษที่คัดลอกมาจากบล็อกมักไม่ตรงกับฮาร์ดแวร์หรือ workload ของคุณ เครื่องมือนี้คำนวณจากจำนวน core และลักษณะงานจริงของคุณเสมอ
- อ้างอิงสูตรที่พิสูจน์ตัวเองมาแล้ว. N = cores × (1 + W/C) ถูกใช้ปรับจูนระบบ production มาหลายสิบปี มันสมดุลสองหน้าที่ของ pool คือทำให้ core ยุ่งตลอดและชดเชยเวลาที่รอ
- มี preset สำหรับ IO-bound และ CPU-bound. ถ้ายังไม่รู้อัตราส่วนที่แท้จริง คลิกเดียวก็ได้จุดเริ่มต้นที่สมเหตุสมผลสำหรับงานทั้งสองแบบ
- คัดลอก executor config ไปใช้ได้ทันที. เครื่องมือสร้าง config snippet ที่พร้อมใช้ ทำให้ตัวเลขกลายเป็นโค้ดที่รันได้ในไม่กี่วินาที
- ทำงานในเบราว์เซอร์ล้วน ๆ. ไม่ต้องติดตั้ง ไม่ต้องลงทะเบียน เปิดหน้าเว็บ ได้ตัวเลข ปิดแท็บจบ
- ช่วยจับข้อผิดพลาดคลาสสิกตั้งแต่ต้นทาง. ทั้ง pool ใหญ่เกินและคิวไม่จำกัดตรวจพบยากจนต้องเจอตอนรับ load จริง การมีค่าเริ่มต้นที่มีเหตุผลช่วยย่นเวลา tuning ทุกรอบ
ฟีเจอร์หลัก
เครื่องมือนี้ตั้งใจให้เรียบง่าย แต่ทุกช่องกรอกสอดคล้องกับการตัดสินใจไซส์จริง:
| ฟีเจอร์ | สิ่งที่ทำ |
|---|---|
| ช่องกรอกจำนวน CPU core | ใช้จำนวน core ของเซิร์ฟเวอร์ VM หรือ container เป็นฐานในการคำนวณ |
| ช่องกรอกอัตราส่วน wait/compute | เก็บสัดส่วนเวลาที่งานใช้รอ (W) บน IO กับเวลาที่ใช้คำนวณ (C) บน CPU |
| ผลลัพธ์จากสูตร | คำนวณ N = cores × (1 + W/C) ทันทีเพื่อให้ขนาด pool ที่แนะนำ |
| preset IO-bound | สำหรับงานที่ส่วนใหญ่ใช้เวลารอ ให้ pool ขนาดใหญ่ |
| preset CPU-bound | สำหรับงานหนักด้านการคำนวณ ให้ pool ใกล้เคียงจำนวน core |
| executor config snippet | สร้าง config สำหรับ pool ที่คัดลอกไปวางได้ทันที |
| ทำงานบนเบราว์เซอร์ | ทุกการคำนวณเกิดขึ้นฝั่ง client รวดเร็วและเป็นส่วนตัว |
รายละเอียดสามข้อนี้ทำให้เครื่องมือใช้งานจริงได้ดีเป็นพิเศษ:
- ช่องอัตราส่วนรับค่าทศนิยมได้ งานที่รอ 80 ms และคำนวณ 20 ms ก็แค่กรอก W/C = 4
- ผลลัพธ์อัปเดตสด ๆ เมื่อสลับ preset หรือแก้ค่า เปรียบเทียบหลาย scenario ได้ในหน้าเดียว
- snippet ที่ได้มาพร้อม thread factory ที่ตั้งชื่อ thread ให้ ซึ่งช่วยเรื่องการ debug มากกว่าที่หลายคนคิด
วิธีใช้งาน Thread Pool Size Calculator
- กรอกจำนวน CPU core. ใช้จำนวนจริงที่ process ของคุณได้รับ ถ้ารันใน container ให้ดูที่ CPU limit ไม่ใช่จำนวน core ทั้งเครื่องแม่ข่าย
- วัดหรือประมาณสัดส่วนเวลารอกับเวลาคำนวณ. หาค่า W และ C จาก profiler, APM trace หรือ timing log แม้ค่าคร่าว ๆ ก็ยังดีกว่าการเดามืด
- เลือก preset: IO-bound หรือ CPU-bound. ใช้ IO-bound เมื่องานส่วนใหญ่รอ network, disk หรือ database ใช้ CPU-bound เมื่องานส่วนใหญ่คำนวณ หรือจะกรอกค่า W/C เองก็ได้
- อ่านค่าขนาด pool ที่แนะนำ N. ถือว่าเป็นจุดเริ่มต้น ไม่ใช่กฎเหล็ก ปัดค่าให้สมเหตุสมผลและจดสมมติฐานที่ใช้ไว้
- คัดลอก executor config. นำ snippet ไปวางในโค้ด รัน load test แล้วปรับจากข้อมูลที่วัดได้ ไม่ใช่จากคำบอกต่อกันมา
สูตรเล็ก ๆ ที่ปรับจูนเซิร์ฟเวอร์มาแล้วนับพันเครื่อง
ทำไมสูตรง่าย ๆ อย่าง N = cores × (1 + W/C) ถึงใช้ได้จริง? เริ่มจากความล้มเหลวที่มันป้องกัน ถ้า thread น้อยเกิน CPU จะว่างเฉย เพราะ worker แต่ละตัว block รอ socket หรือ database cursor ทำให้ core ทำงานแค่ยี่สิบเปอร์เซ็นต์และ latency พุ่งขึ้นทั้งที่เครื่องดูเหมือนกำลังง่วงนอน แต่ถ้า thread มากเกินปัญหาจะกลับด้าน kernel เริ่มเสียเวลาในทุกวินาทีไปกับการบันทึกและกู้คืน context ของ thread CPU cache ถูกเขี่ยทิ้งด้วยการสลับงานตลอดเวลา และ stack ของทุก thread ก็กิน memory ราวหนึ่งเมกะไบต์ต่อตัวตามค่า default ของ Java แปลว่า pool 500 thread อาจกิน RAM ครึ่งกิกะไบต์ก่อนจะได้ทำงานที่มีค่าสักนิด
สูตรนี้เดินเข็มได้พอดี thread หนึ่งตัวใช้เวลาเพียงส่วนน้อยไปกับการคำนวณ ส่วนที่เหลือคือการรอ ถ้าเราต้องการให้ทุก core ยุ่งตลอดเวลา เราต้องมี thread มากพอที่ช่วงที่คำนวณของบาง thread ทับกับช่วงที่อีก thread กำลังรอ N = cores × (1 + W/C) คือจำนวนพอดีนั้น ทุก core ได้ thread ที่คำนวณต่อเนื่องหนึ่งตัว บวกกับ thread เพิ่มอีก W/C ตัวเพื่อชดเชยพวกที่กำลังรอ
สอง preset ก็เป็นแค่สุดขั้วของแนวคิดเดียวกัน งาน IO-bound ที่รอ 90 ms และคำนวณ 10 ms มี W/C = 9 เครื่อง 8 core จึงได้ราว 80 thread เพราะการรอครองงาน และ thread ที่เพิ่มขึ้นคือประกันราคาถูกกับ core ที่เสี่ยงว่าง ส่วนงาน CPU-bound ที่ W ใกล้ศูนย์จะยุบสูตรเหลือ N ≈ cores เพราะ thread ที่เกินจำนวน core ทำได้แค่สลับกันใช้เครื่อง เพิ่มค่า switching แต่ไม่เพิ่ม throughput นี่คือความต่างของ io bound threads กับ cpu bound threads สูตรเดียวกัน คนละขั้ว
จงรู้จุดที่สูตรใช้ไม่ได้ สูตรนี้สมมติว่า memory ไม่ใช่ข้อจำกัด แต่ถ้า stack ละหนึ่งเมกะไบต์ การได้ 400 thread มาแปลว่าใช้ RAM จริง ๆ ใน container ที่จำกัด memory แน่นอาจต้องลดขนาด pool หรือลดขนาด stack สูตรยังสมมติว่า thread ของคุณทำงานได้จริง ถ้า API ปลายทางจำกัดการเรียกพร้อมกันไว้แค่ 30 สาย การตั้ง pool ไว้ 200 ก็แค่สร้างคิวยาวขึ้นใน process ของตัวเอง lock และ shared cache ก็จำกัดความขนานที่แท้จริงได้เช่นกัน และที่สำคัญสูตรจะดีแค่ไหนขึ้นกับความซื่อสัตย์ของการวัด W และ C ต้องมาจาก trace หรือ timing log ของงานจริง อย่างดีดูทั้ง p50 และ p95 ไม่ใช่จากความรู้สึกว่าฟีเจอร์นี้ช้าในเดโม วัดทั้งสองค่า คำนวณใหม่ แล้วถือว่าคำตอบเป็นสมมติฐานที่แข็งแรงซึ่ง load test จะยืนยันหรือแก้ไข
กรณีการใช้งานจริง
กำหนดขนาด Executor ของ API Server
เซอร์วิสของคุณรันบนเครื่อง 8 core และ handler แต่ละตัวเรียก API ปลายทาง ใช้เวลารอ 80 ms กับงานจริง 20 ms แปลว่า W/C = 4 เครื่องมือให้คำตอบ N = 8 × (1 + 4) = 40 thread นำไปใส่ fixed pool พร้อม bounded queue รัน load test แล้วคุณจะมีฐานที่อธิบายเหตุผลได้ แทนตัวเลขวิเศษ 200 แบบไม่มีที่มา
ปรับจูน Worker ของ Batch Job
งานกลางคืนของคุณแปลงและบีบอัดไฟล์มีเดียที่ผู้ใช้อัปโหลด profiling พบว่าใช้เวลา IO เพียง 5 ms ต่อการคำนวณล้วน ๆ 95 ms ดังนั้น W/C ≈ 0.05 และ preset CPU-bound ตอบ 8 มาเลย ซึ่งแทบไม่เกินจำนวน core ที่มี ให้ queue ทำหน้าที่เป็นบัฟเฟอร์ระหว่างการรับไฟล์กับการประมวลผล
ตรวจสอบความสมเหตุสมผลของ Database Connection Pool
สมมติว่าสูตรบอกว่า request pool ของคุณควรมี 60 thread แต่ database connection pool จำกัดอยู่ที่ 20 connection ความไม่ลงรอยนี้แปลว่าตอน peak จะมี thread อีก 40 ตัวต่อคิวรอ connection ทางแก้คือขยาย pool หรือย่อ executor หรือลดจำนวน query ต่อ request ประเด็นคือคุณมองเห็นความขัดแย้งนี้ก่อน production จะเห็น
ปรับจูนด้วย Load Test
ใช้ขนาดที่คำนวณได้เป็นรอบแรกของ load test ค่อย ๆ เพิ่ม traffic ตามดู throughput, p99 latency, CPU และอัตรา context switch แล้วขยับขนาด pool ทีละราวสิบเปอร์เซ็นต์ สูตรพาคุณเข้าใกล้คำตอบในหนึ่งรอบ แทนที่จะลองผิดลองถูกสิบรอบ
แนวปฏิบัติที่ดี
- ตรวจสอบด้วย load test เสมอ. สูตรเป็นสมมติฐานตั้งต้น มีแต่ throughput และ latency ที่วัดจาก traffic จริงเท่านั้นที่ยืนยันได้
- ตั้งชื่อ thread ให้ชัดเจน. thread factory ที่ใส่ prefix บอกหน้าที่ เปลี่ยน thread dump ลึกลับให้กลายเป็นงานวินิจฉัยห้าวินาที
- กำหนดขอบเขต queue ให้สมเหตุสมผล. คิวแบบไม่จำกัดปิดบัง overload จนกว่า memory จะตาย ส่วนคิวแบบจำกัดพร้อมนโยบาย rejection ที่ชัดเจนจะ fail เร็วและเห็นได้ชัด
- กลับมาทบทวนเมื่อลักษณะงานเปลี่ยน. dependency ปลายทางใหม่หรือ query ที่หนักขึ้นทำให้ W/C เปลี่ยน ให้คำนวณใหม่เมื่อ workload เปลี่ยน ไม่ใช่ปีละครั้ง
- เฝ้าดู memory ควบคู่กับ CPU. ทุก thread มี stack ติดตัวมา ใน container ที่ memory จำกัด ให้เช็กว่าขนาดที่คำนวณได้ยังอยู่ในลิมิต
- แยก pool ตามหน้าที่. การรวมงานเร็วกับงาน batch ช้าไว้ใน pool เดียวทำให้งานช้ากลืนงานเร็ว ควรแยก pool และไซส์แยกกัน
คราวนี้เมื่อใครถามว่า "ควรใส่ thread กี่ตัว" คุณจะตอบด้วยการคำนวณ แทนคำเล่าต่อกันมา เปิด Thread Pool Size Calculator กรอกจำนวน core และอัตราส่วนเวลารอต่อเวลาคำนวณ ดึง executor config ไป แล้วพิสูจน์ด้วย load test ใช้เวลาแค่สองนาทีในเบราว์เซอร์ และเปลี่ยนข้อถกเถียงเก่าแก่ที่สุดข้อหนึ่งของโลก concurrency ให้กลายเป็นตัวเลขที่จบได้
เครื่องมือที่เกี่ยวข้องที่คุณอาจสนใจ:
ขอให้สนุกกับการปรับจูน!
คำถามที่พบบ่อย
ถ: สูตร N = cores × (1 + W/C) แปลว่าอะไรกันแน่? ตอบ: มันนับจำนวน thread ที่ต้องมีเพื่อให้ทุก core ทำงานต่อเนื่องในเมื่อ thread แต่ละตัวใช้เวลาส่วนหนึ่งไปกับการรอ W คือเวลาเฉลี่ยที่งานรอ IO และ C คือเวลาที่ใช้คำนวณ อัตราส่วน W/C บอกว่า thread ที่คำนวณอยู่หนึ่งตัวต้องมี thread ที่รออยู่คอยชดเชยกี่ตัว
ถ: จะรู้ได้อย่างไรว่า workload ของผมเป็น IO-bound หรือ CPU-bound? ตอบ: profile งานตัวแทนสักหนึ่งงานแล้วเทียบเวลารอกับเวลาคำนวณ การเรียก database, HTTP request และการอ่านไฟล์ชี้ไปทาง IO-bound ส่วนการ parse, การ hash และการคำนวณเชิงคณิตชี้ไปทาง CPU-bound ถ้ายังวัดไม่ได้ ให้ใช้ preset เป็นค่าประมาณแรกไปก่อน
ถ: ควรตั้งขนาด pool เท่ากับค่า N ที่คำนวณได้เป๊ะ ๆ เลยไหม? ตอบ: ถือว่าเป็นจุดเริ่มต้นที่แข็งแรง ปัดค่าให้สมเหตุสมผล เคารพขีดจำกัดด้าน memory และเพดาน concurrency ของระบบปลายทาง แล้ว load test และปรับทีละน้อยตามพฤติกรรมของ latency และ CPU
ถ: สูตรนี้ใช้ได้กับแค่ Java executor เท่านั้นไหม? ตอบ: ไม่ใช่ คณิตศาสตร์เบื้องหลังเป็นกลางภาษา ใช้ได้กับ thread pool ใน .NET, Python และ Go และช่วยให้ความคิดกับการไซส์ database connection ด้วย เครื่องมือเพียงสร้าง executor config snippet ให้ เพราะ Java ทำให้การคัดลอกไปใช้ง่ายเป็นพิเศษ