← กลับไปหน้าบทความ
ขั้นสูง (MarTech)

ทำไม Background Job ที่เคยรันบน Cron ถึงเริ่มพังตอนโหลดสูง

ทีมบรรณาธิการ linli12 ส.ค. 04:52อัปเดต 12 ส.ค. 04:52อ่าน 3 นาที
ทำไม Background Job ที่เคยรันบน Cron ถึงเริ่มพังตอนโหลดสูง
บทความนี้จัดทำเพื่อความรู้ ชื่อธุรกิจ ตัวเลข หรือบทสนทนาที่ไม่มีแหล่งอ้างอิงกำกับเป็นกรณีตัวอย่างเพื่ออธิบายแนวคิด ดูรายละเอียดที่ นโยบายกองบรรณาธิการ

สรุปสั้น ๆ

Cloudflare Queues เป็นบริการคิวข้อความที่ผูกกับ Workers ใช้โมเดล Producer ส่งข้อความเข้าคิวและ Consumer ดึงออกไปประมวลผลเป็น batch พร้อมกลไก retry และ dead letter queue ในตัว เหมาะกับงานที่ Cron Trigger เดิมเริ่มรับมือไม่ไหวเมื่อปริมาณ event สูงขึ้นหรือมีงานที่ล้มเหลวบางส่วนที่ต้องจัดการแยก

หลายทีมเริ่มต้นทำ background job ด้วย Cron Trigger ของ Workers เพราะตั้งค่าง่าย แค่กำหนดตารางเวลาแล้วให้ฟังก์ชันรันตามรอบ วิธีนี้ใช้ได้ดีตอนงานยังมีปริมาณน้อยและไม่ซับซ้อน เช่นดึงข้อมูลอัปเดตทุกชั่วโมง หรือส่งอีเมลสรุปทุกเช้า

ปัญหาเริ่มปรากฏเมื่อปริมาณงานต่อรอบเพิ่มขึ้นมาก เช่นจากดึงข้อมูล 100 รายการต่อรอบ กลายเป็นหลักหมื่นรายการ ฟังก์ชันที่เคยรันจบภายในไม่กี่วินาทีเริ่มรันไม่ทันภายในเวลาที่กำหนด บางงานล้มเหลวกลางทางแล้วไม่มีกลไก retry ที่ดี บางงานประมวลผลซ้ำเพราะ Cron รอบถัดไปเริ่มทำงานก่อนรอบเก่าจะจบ

Cloudflare Queues ถูกออกแบบมาแก้ปัญหานี้โดยเฉพาะ ด้วยการแยกขั้นตอน 'รับงานเข้ามา' ออกจาก 'ประมวลผลงาน' อย่างชัดเจน พร้อมกลไก retry และการจัดการงานที่ล้มเหลวซ้ำ ๆ ในตัว บทความนี้จะพาไปดูว่าโมเดล Producer/Consumer ทำงานยังไง และทำไมมันแก้ปัญหาที่ Cron Trigger เดิมทำไม่ได้

Cloudflare Queues คืออะไร และต่างจาก Cron Trigger ยังไง

Cloudflare Queues คือบริการคิวข้อความ (message queue) ที่ผูกกับ Workers ผ่าน binding เช่นเดียวกับบริการอื่นในกลุ่ม Cloudflare Workers Platform หลักการทำงานคือมี Worker ตัวหนึ่งทำหน้าที่ส่งข้อความเข้าคิว เรียกว่า Producer และมี Worker อีกตัวทำหน้าที่ดึงข้อความออกจากคิวมาประมวลผล เรียกว่า Consumer

ความต่างพื้นฐานจาก Cron Trigger คือ Cron ทำงานตามตารางเวลา ไม่สนใจว่ามีงานค้างอยู่เท่าไร ถึงเวลาก็รันตามรอบ ในขณะที่ Queues ทำงานตามปริมาณข้อความที่เข้ามาจริง ถ้ามีข้อความเข้าคิวมาก ระบบจะประมวลผล Consumer หลาย instance พร้อมกันเพื่อระบายงานออกให้เร็วขึ้น ถ้าไม่มีข้อความเข้ามา Consumer ก็ไม่ทำงานเลย ไม่เสียทรัพยากรไปกับการเช็คทุกรอบเวลาแบบ Cron

อีกความต่างสำคัญคือ Queues แยกความรับผิดชอบระหว่างจุดที่ 'เหตุการณ์เกิดขึ้น' กับจุดที่ 'ประมวลผลเหตุการณ์' ออกจากกันอย่างชัดเจน ทำให้ Producer ทำงานเสร็จเร็วโดยแค่ส่งข้อความเข้าคิว ไม่ต้องรอให้การประมวลผลจริงเสร็จก่อน ต่างจาก Cron ที่มักรวมทั้งการรับงานและประมวลผลไว้ในฟังก์ชันเดียวกัน

โมเดล Producer และ Consumer ทำงานร่วมกันยังไง

ฝั่ง Producer คือ Worker ที่มีหน้าที่ส่งข้อความเข้าคิวเมื่อมีเหตุการณ์เกิดขึ้น เช่นเมื่อผู้ใช้อัปโหลดไฟล์เสร็จ Producer จะส่งข้อความที่บอกว่า 'มีไฟล์ใหม่ต้องประมวลผล' เข้าคิว โดยไม่ต้องรอให้การประมวลผลไฟล์นั้นเสร็จจริง ทำให้ผู้ใช้ได้รับ response กลับเร็วแม้งานเบื้องหลังจะยังไม่เสร็จ

ฝั่ง Consumer คือ Worker อีกตัวที่ถูกกำหนดค่าให้ดึงข้อความออกจากคิวมาประมวลผลเป็น batch ทีมสามารถกำหนดขนาด batch และเวลารอสูงสุดก่อนประมวลผลได้ เช่นรอให้มีข้อความครบ 10 ชิ้นหรือรอไม่เกิน 5 วินาทีแล้วประมวลผลเท่าที่มี การตั้งค่านี้ช่วยให้สมดุลระหว่าง latency กับประสิทธิภาพการประมวลผลแบบกลุ่ม

จุดที่ทำให้โมเดลนี้ทนทานกว่า Cron คือ ถ้า Consumer ล้มเหลวระหว่างประมวลผล ข้อความนั้นจะไม่หายไปเฉย ๆ แต่จะถูกนำกลับเข้าคิวเพื่อลองใหม่ตามกลไก retry ที่ตั้งไว้ ต่างจาก Cron ที่ถ้าฟังก์ชันล้มเหลวกลางทาง งานที่ยังไม่เสร็จมักหายไปโดยไม่มีร่องรอยเว้นแต่ทีมจะเขียน logic เช็คสถานะเองทั้งหมด

ทำไม Cron Job เดิมถึงพังเมื่อโหลดสูงขึ้น

อาการแรกที่พบบ่อยคือ Cron Job มีเวลาทำงานจำกัดต่อรอบ เมื่อปริมาณงานเพิ่มขึ้นจนรันไม่ทันภายในเวลาที่กำหนด งานบางส่วนจะถูกตัดทิ้งกลางทางโดยไม่มีการแจ้งเตือนที่ชัดเจน ทีมมักรู้ปัญหานี้ช้าเพราะไม่มีกลไกติดตามว่างานไหนทำสำเร็จหรือไม่สำเร็จบ้างในแต่ละรอบ

อาการที่สองคือ Cron รอบใหม่เริ่มทำงานก่อนรอบเก่าจะประมวลผลเสร็จ ถ้าไม่มีกลไก lock ป้องกัน จะเกิดการประมวลผลข้อมูลชุดเดียวกันซ้ำสองครั้ง ซึ่งอันตรายมากสำหรับงานที่มีผลข้างเคียง เช่นการส่งอีเมลหรือตัดยอดเงิน เพราะผู้ใช้อาจได้รับอีเมลซ้ำหรือถูกตัดยอดซ้ำโดยไม่ตั้งใจ

อาการที่สามคือเมื่อปริมาณงานผันผวนสูงในบางช่วงเวลา เช่นมีคนอัปโหลดไฟล์พร้อมกันจำนวนมากในช่วงเวลาสั้น ๆ Cron ที่รันตามตารางเวลาคงที่ไม่สามารถปรับตัวตามปริมาณงานจริงได้ ทำให้ช่วงที่งานเยอะ Cron รับมือไม่ทัน ในขณะที่ช่วงที่งานน้อย Cron ก็ยังรันตามรอบเดิมทั้งที่ไม่มีอะไรต้องทำ เสียทรัพยากรโดยเปล่าประโยชน์

Batching และ Retry จัดการงานที่ล้มเหลวยังไง

ตารางด้านล่างสรุปพารามิเตอร์หลักที่ทีมต้องตั้งค่าเมื่อใช้ Queues เป็นกรอบเข้าใจเบื้องต้น ค่าที่เหมาะสมจริงขึ้นกับลักษณะงานของแต่ละระบบและควรตรวจสอบจากเอกสารล่าสุดก่อนตั้งค่า production

พารามิเตอร์หน้าที่ผลถ้าตั้งค่าไม่เหมาะสม
max batch sizeจำนวนข้อความสูงสุดที่ Consumer ประมวลผลในครั้งเดียวตั้งใหญ่เกินไปอาจทำให้ Consumer ทำงานนานเกินเวลาที่กำหนด
max batch timeoutเวลารอสูงสุดก่อนประมวลผลแม้ batch ยังไม่เต็มตั้งนานเกินไปเพิ่ม latency ของงานที่มาไม่ถี่
retry countจำนวนครั้งที่ลองประมวลผลซ้ำเมื่อล้มเหลวตั้งน้อยเกินไปอาจทิ้งงานที่ควรสำเร็จได้ถ้าลองอีกครั้ง
dead letter queueปลายทางของข้อความที่ล้มเหลวซ้ำเกินจำนวนที่กำหนดไม่ตั้งไว้อาจทำให้งานที่ล้มเหลวซ้ำ ๆ หายไปเงียบ ๆ

Dead Letter Queue กันงานหายตอนล้มเหลวซ้ำ ๆ

บางข้อความในคิวอาจล้มเหลวซ้ำแล้วซ้ำอีกไม่ว่าจะ retry กี่ครั้งก็ตาม เช่นข้อความที่มีข้อมูลผิดรูปแบบตั้งแต่ต้น หรืออ้างอิงถึงทรัพยากรที่ถูกลบไปแล้ว ถ้าปล่อยให้ retry ไปเรื่อย ๆ โดยไม่มีเพดาน ข้อความเหล่านี้จะกินทรัพยากรของระบบและอาจบล็อกการประมวลผลข้อความอื่นที่ปกติดีอยู่

Dead Letter Queue คือคิวสำรองที่รับข้อความซึ่งล้มเหลวเกินจำนวนครั้งที่กำหนดไว้ แทนที่จะปล่อยให้หายไปเฉย ๆ ทีมสามารถตั้งระบบแยกต่างหากมาตรวจสอบ dead letter queue เป็นระยะ เพื่อดูว่ามีข้อความอะไรที่ค้างอยู่ แล้วตัดสินใจว่าจะแก้ไขและส่งกลับเข้าคิวหลัก หรือทิ้งไปพร้อมบันทึกเหตุผล

ตัวอย่างสมมติที่ใช้แนวทางนี้คือระบบประมวลผลรูปภาพที่ผู้ใช้อัปโหลด ถ้าไฟล์บางไฟล์เสียหายจนประมวลผลไม่ได้ไม่ว่าจะลองกี่ครั้ง ระบบจะส่งข้อความนั้นเข้า dead letter queue แล้วแจ้งเตือนทีมแทนที่จะปล่อยให้ retry ไม่รู้จบจนกินทรัพยากรของ Consumer ที่ควรเอาไปประมวลผลไฟล์อื่นที่ปกติดีอยู่

งานแบบไหนที่ควรย้ายจาก Cron ไปใช้ Queues

งานที่เหมาะย้ายมากที่สุดคืองานที่เกิดจาก event ของผู้ใช้โดยตรง เช่นการส่งอีเมลยืนยัน การประมวลผลไฟล์ที่อัปโหลด การส่ง webhook ไปยังระบบภายนอก หรือการอัปเดตข้อมูลที่ไม่จำเป็นต้องเสร็จทันทีใน request เดียวกับที่ผู้ใช้กด งานประเภทนี้ได้ประโยชน์ทั้งจาก latency ที่ลดลงฝั่งผู้ใช้และความทนทานต่อความล้มเหลวที่ Queues มีให้

งานที่ยังเหมาะกับ Cron อยู่คืองานที่ต้องรันตามตารางเวลาจริง ๆ ไม่ได้ขึ้นกับ event ใด เช่นการสรุปรายงานประจำวัน การล้างข้อมูลเก่าทุกสัปดาห์ หรือการตรวจสุขภาพระบบเป็นระยะ งานเหล่านี้ไม่มี 'เหตุการณ์ต้นทาง' ที่จะส่งเข้าคิวได้ จึงยังเหมาะกับการรันตามตารางเวลาแบบ Cron มากกว่า

ทีมที่ต้องการ state ร่วมกันระหว่างการประมวลผลหลายขั้นตอน เช่นต้องรู้ว่าผู้ใช้คนไหนกำลังทำ workflow ค้างอยู่ขั้นตอนไหน ควรพิจารณาผสม Queues เข้ากับ Durable Objects สำหรับ coordination แบบเรียลไทม์ เพราะ Queues เก่งเรื่องรับและกระจายงาน แต่ไม่เหมาะกับการเก็บ state ที่ต้องอ่านเขียนบ่อยระหว่างขั้นตอน

วางโครงสร้าง Event Processing Pipeline ด้วย Queues

ตัวอย่างสมมติของการวางโครงสร้าง pipeline ประมวลผลไฟล์ที่ผู้ใช้อัปโหลด แสดงลำดับขั้นตอนตั้งแต่รับ event จนถึงจัดการความล้มเหลว

  1. สร้างคิวหลักสำหรับรับ event 'ไฟล์ใหม่ถูกอัปโหลด' แล้วให้ Worker ที่รับ request การอัปโหลดทำหน้าที่เป็น Producer ส่งข้อความเข้าคิวทันทีหลังบันทึกไฟล์ลง R2 เสร็จ
  2. สร้าง Consumer แยกต่างหากที่ดึงข้อความจากคิวมาประมวลผลไฟล์ เช่นสร้าง thumbnail หรือสแกนไวรัส โดยตั้ง batch size และ timeout ให้เหมาะกับเวลาที่การประมวลผลแต่ละไฟล์ใช้จริง
  3. ตั้งค่า retry count ให้เหมาะสมกับลักษณะความล้มเหลวที่อาจเกิดขึ้น เช่นถ้าความล้มเหลวส่วนใหญ่เกิดจากปัญหาชั่วคราวอย่าง network timeout ควรตั้ง retry มากกว่าความล้มเหลวที่เกิดจากข้อมูลผิดรูปแบบตั้งแต่ต้น
  4. ตั้ง dead letter queue แยกไว้รับข้อความที่ล้มเหลวเกินจำนวนครั้งที่กำหนด แล้วสร้างระบบแจ้งเตือนหรือ dashboard แยกสำหรับตรวจสอบข้อความที่ค้างอยู่ในนั้นเป็นระยะ
  5. ทดสอบ flow เต็มรูปแบบด้วยการจำลองความล้มเหลวในหลายจุด เช่นให้ Consumer throw error กลางทาง เพื่อยืนยันว่าข้อความถูก retry ตามที่ตั้งไว้จริง และเข้า dead letter queue เมื่อเกินจำนวนครั้งที่กำหนดจริง ก่อนขึ้น production

ทำแบบนี้แล้วพัง เพราะลืมออกแบบให้ Consumer Idempotent

ปัญหาที่ร้ายแรงที่สุดที่ทีมมักเจอคือลืมว่าข้อความหนึ่งชิ้นอาจถูกประมวลผลมากกว่าหนึ่งครั้งได้ ไม่ว่าจะเป็นเพราะ retry หลัง timeout หรือความล้มเหลวบางส่วนที่ทำให้ระบบไม่แน่ใจว่าประมวลผลสำเร็จหรือไม่ ถ้า Consumer ไม่ได้ออกแบบให้ idempotent คือรันซ้ำกี่ครั้งก็ได้ผลลัพธ์เหมือนเดิม การประมวลผลซ้ำอาจทำให้เกิดผลข้างเคียงซ้ำ เช่นส่งอีเมลซ้ำหรือตัดเงินซ้ำ

วิธีป้องกันที่ใช้ได้จริงคือให้ทุกข้อความมี id ที่ไม่ซ้ำกัน แล้วให้ Consumer ตรวจสอบก่อนว่า id นี้เคยประมวลผลสำเร็จไปแล้วหรือยังก่อนเริ่มทำงานจริง ถ้าเคยสำเร็จแล้วให้ข้ามไปเลยแทนที่จะทำซ้ำ วิธีนี้ต้องมีที่เก็บสถานะว่า id ไหนประมวลผลสำเร็จแล้วบ้าง ซึ่งอาจเป็น D1 หรือ Durable Object ก็ได้ขึ้นกับปริมาณและความถี่ในการตรวจสอบ

อีกอาการที่พบคือทีมไม่ได้ตั้ง alert สำหรับ dead letter queue ที่เริ่มมีข้อความสะสม ทำให้ปัญหาที่ควรรู้ตั้งแต่วันแรกกลับถูกค้นพบหลังจากผ่านไปหลายสัปดาห์ตอนที่ผู้ใช้เริ่มร้องเรียนว่าฟีเจอร์บางอย่างไม่ทำงาน ควรตั้งระบบแจ้งเตือนอัตโนมัติเมื่อ dead letter queue มีข้อความเกินจำนวนที่ยอมรับได้ ไม่ใช่รอให้มีคนไปเปิดดูเองเป็นระยะ

สรุป

Cron Trigger ยังเป็นเครื่องมือที่ดีสำหรับงานที่รันตามตารางเวลาจริง แต่ไม่เหมาะกับงานที่เกิดจาก event ปริมาณผันผวนของผู้ใช้ Cloudflare Queues เข้ามาเติมเต็มช่องว่างนี้ด้วยโมเดล Producer/Consumer ที่แยกการรับงานออกจากการประมวลผล พร้อมกลไก retry และ dead letter queue ที่ช่วยให้งานไม่หายไปเงียบ ๆ เมื่อเกิดความล้มเหลว

สิ่งสำคัญที่สุดที่ต้องทำก่อนย้ายงานจาก Cron มา Queues คือออกแบบให้ Consumer เป็น idempotent เสมอ เพราะกลไก retry ของ Queues หมายความว่าข้อความหนึ่งชิ้นอาจถูกประมวลผลมากกว่าหนึ่งครั้งได้ ถ้าไม่ป้องกันไว้ตั้งแต่ต้น ปัญหาข้อมูลซ้ำอาจร้ายแรงกว่าปัญหางานหายที่ Cron เดิมเคยมีเสียอีก

  • Queues แยกการรับ event ออกจากการประมวลผล ต่างจาก Cron ที่รวมสองอย่างไว้ด้วยกัน
  • โมเดล Producer/Consumer ทำงานตามปริมาณข้อความจริง ไม่ใช่ตามตารางเวลาคงที่
  • retry และ dead letter queue ช่วยป้องกันงานหายเงียบ ๆ เมื่อ Consumer ล้มเหลว
  • ควรออกแบบ Consumer ให้ idempotent เสมอ เพราะข้อความอาจถูกประมวลผลซ้ำจาก retry
  • งานที่รันตามตารางเวลาจริงไม่มี event ต้นทาง ยังเหมาะกับ Cron Trigger มากกว่า Queues

คำถามที่พบบ่อย

Queues ต่างจาก Durable Objects ตรงไหน ควรใช้ตัวไหนเมื่อไร

Queues เหมาะกับงานที่รับ event เข้ามาแล้วกระจายไปประมวลผลแบบ batch โดยไม่ต้องมี state ร่วมกันระหว่างข้อความ ส่วน Durable Objects เหมาะกับงานที่ต้องการ coordination หรือ state ที่ต้องอ่านเขียนบ่อยระหว่างหลาย event บางระบบใช้ทั้งสองตัวร่วมกันได้ตามลักษณะงานแต่ละส่วน

ถ้า Consumer ล้มเหลวกลางทาง ข้อความจะหายไหม

ไม่หายในทันที ข้อความจะถูกนำกลับเข้าคิวเพื่อลองประมวลผลใหม่ตามจำนวน retry ที่ตั้งไว้ ถ้าล้มเหลวซ้ำจนเกินจำนวนที่กำหนด ข้อความจะถูกส่งไปยัง dead letter queue แทนที่จะหายไปเงียบ ๆ

ตั้ง batch size ใหญ่ไว้เลยดีไหมเพื่อประมวลผลได้เยอะในครั้งเดียว

ไม่ควรตั้งใหญ่เกินความจำเป็น เพราะถ้า batch ใหญ่เกินไปจนการประมวลผลใช้เวลานานเกินเวลาที่ Consumer มี อาจทำให้ทั้ง batch ล้มเหลวพร้อมกันแทนที่จะสำเร็จบางส่วน ควรทดสอบหาค่าที่เหมาะกับเวลาประมวลผลจริงของงานแต่ละประเภท

จำเป็นต้องทำให้ Consumer idempotent เสมอไหม

จำเป็นอย่างยิ่งสำหรับงานที่มีผลข้างเคียง เช่นส่งอีเมลหรือตัดยอดเงิน เพราะข้อความอาจถูกประมวลผลซ้ำได้จากกลไก retry การออกแบบให้รันซ้ำแล้วผลลัพธ์เหมือนเดิมช่วยป้องกันความเสียหายจากการประมวลผลซ้ำโดยไม่ตั้งใจ

Queues แทน Cron Trigger ได้ทั้งหมดไหม

ไม่ทั้งหมด งานที่ต้องรันตามตารางเวลาจริงโดยไม่มี event ต้นทาง เช่นสรุปรายงานประจำวันหรือล้างข้อมูลเก่าตามรอบ ยังเหมาะกับ Cron Trigger มากกว่า Queues เหมาะกับงานที่เกิดจาก event จริงที่ต้องประมวลผลนอก request หลัก

ตรวจสอบสุขภาพของคิวอย่างไรว่ากำลังมีปัญหาหรือไม่

ควรตั้งระบบติดตามจำนวนข้อความที่ค้างอยู่ในคิวหลักและใน dead letter queue เป็นระยะ ถ้าจำนวนข้อความค้างเพิ่มขึ้นต่อเนื่องหรือ dead letter queue เริ่มมีข้อความสะสม เป็นสัญญาณว่า Consumer อาจประมวลผลไม่ทันหรือมีข้อความที่ล้มเหลวซ้ำอยู่ ควรตั้ง alert อัตโนมัติแทนการเช็คด้วยมือ

วัด Conversion LINE ทุกแบรนด์ รวมศูนย์ที่เดียว

องค์กรที่มีหลายแบรนด์ หลายสาขา หลาย LINE OA — linli รวม Tracking และส่ง Conversion กลับทุกแพลตฟอร์ม พร้อม Custom Limit เริ่มฟรี 14 วัน

ติดต่อทีม Sales

บทความที่เกี่ยวข้อง

R2 เก็บไฟล์แบบไม่มีค่า Egress แต่ยังมีเงื่อนไขที่ต้องรู้ก่อนย้ายจาก S3

R2 เก็บไฟล์แบบไม่มีค่า Egress แต่ยังมีเงื่อนไขที่ต้องรู้ก่อนย้ายจาก S3

จุดขายหลักของ R2 คือไม่คิดค่า Egress เวลาดึงไฟล์ออก แต่ก่อนย้ายจาก S3 ทั้งระบบ ทีมต้องเข้าใจความต่างเรื่อง consistency, feature ที่ยังไม่รองรับ และวิธีคำนวณความคุ้มค่าให้ครบก่อน
เขียน query เดิมทุกอย่าง แต่ทำไม Workers ต่อ Postgres ถึงช้าเป็นวินาที

เขียน query เดิมทุกอย่าง แต่ทำไม Workers ต่อ Postgres ถึงช้าเป็นวินาที

Worker ที่รันใกล้ผู้ใช้แต่ยังต้องเปิดการเชื่อมต่อใหม่ไปหา Postgres ที่อยู่ไกลทุกครั้ง ทำให้ query เดิมที่เคยเร็วบน server กลายเป็นช้าเป็นวินาที Hyperdrive แก้ปัญหานี้ด้วยการ pool connection และแคชเส้นทางให้แทน
ทีมขนาดไหนควรแยก Agent เฉพาะทาง และทีมไหนยังไม่จำเป็นต้องลงทุน

ทีมขนาดไหนควรแยก Agent เฉพาะทาง และทีมไหนยังไม่จำเป็นต้องลงทุน

หลายทีมได้ยินเรื่อง Custom Agent สำหรับ Frontend, Backend หรือ Security แล้วรีบสร้างตามทันที ทั้งที่ยังไม่แน่ใจว่าทีมตัวเองจำเป็นแค่ไหน บทความนี้อธิบายว่า GitHub Copilot Custom Agents คืออะไร สร้างยังไง และเงื่อนไขแบบไหนที่บอกว่าถึงเวลาต้องแยกจริง ๆ หรือยังไม่ต้องรีบ