← บล็อก

ทำไมเราเลิกรันเซิร์ฟเวอร์ WebSocket เอง

· Hibira

เซิร์ฟเวอร์เรียลไทม์ที่รันเองจะติดขัดเมื่อข้อความพุ่งขึ้น และคุณต้องตามแพตช์ช่องโหว่อย่าง PHP2Shell เอง สิ่งที่เราเจอ สิ่งที่การรันเองต้องมี และสิ่งที่บริการแบบโฮสต์รับภาระแทน

ก่อนจะสร้าง Hibira เราเคยดูแลเซิร์ฟเวอร์ WebSocket ที่สร้างด้วย PHP และดีพลอยรวมกับแอปพลิเคชันแบบโมโนลิธเองในโปรเจกต์ที่ผ่านมา ส่วนใหญ่มันทำงานได้ดี แต่เมื่อปริมาณข้อความพุ่งขึ้น มันก็ตามไม่ทัน เซิร์ฟเวอร์ยุ่งอยู่กับข้อความที่มีอยู่แล้ว คำขอใหม่ต้องต่อคิวอยู่ข้างหลัง และเวลาแฝงก็สูงขึ้นเรื่อย ๆ กว่าข้อความจะถึงมือผู้ใช้ เนื้อหาก็ล้าสมัยไปแล้ว ราคาเปลี่ยนไปแล้ว คะแนนอัปเดตไปแล้ว และสถานะ "กำลังพิมพ์…" โผล่ขึ้นมาหลังจากอีกฝ่ายส่งข้อความไปแล้ว

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

การติดขัดเกิดขึ้นได้อย่างไร

งานของเซิร์ฟเวอร์เรียลไทม์ดูเรียบง่าย รับอีเวนต์มาแล้วส่งต่อให้ทุกการเชื่อมต่อที่สมัครรับไว้ ปัญหาคืองานนี้ถูกคูณให้ใหญ่ขึ้น และทันทีที่ส่วนใดส่วนหนึ่งช้าลง ทุกอย่างที่อยู่ข้างหลังก็ต้องรอ

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

เข้ามาเร็วกว่าที่ประมวลผลได้ คิวก็มีแต่จะยาวขึ้น

นี่คือกฎพื้นฐานของคิว ตราบใดที่ข้อความเข้ามาเร็วกว่าที่เซิร์ฟเวอร์ประมวลผลได้ คิวก็จะยาวขึ้นเรื่อย ๆ และข้อความใหม่ทุกข้อความต้องรอให้ข้อความก่อนหน้าทั้งหมดประมวลผลเสร็จ

ลองคิดเป็นตัวเลข สมมติว่าเซิร์ฟเวอร์ประมวลผลได้วินาทีละ 5,000 ข้อความ และช่วงพีกมีข้อความเข้ามาวินาทีละ 6,000 ข้อความเป็นเวลา 10 วินาที 10 วินาทีนี้จะทิ้งข้อความที่ยังไม่ได้ประมวลผลไว้ 10,000 ข้อความ ต่อให้พีกจบและไม่มีข้อความใหม่เข้ามาอีกเลย เซิร์ฟเวอร์ก็ยังต้องใช้อีก 2 วินาทีเพื่อเคลียร์ ข้อความสุดท้ายในคิวจึงถูกส่งออกช้าไป 2 วินาที ถ้าหลังพีกยังมีทราฟฟิกปกติเข้ามาต่อ ก็จะเคลียร์ได้ช้ากว่านั้นอีก

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

อีเวนต์เดียว คูณด้วยผู้สมัครรับทุกคน

เผยแพร่ครั้งเดียวไม่ได้แปลว่าส่งครั้งเดียว ในแชนเนลที่มีผู้สมัครรับ 2,000 คน ทุกอีเวนต์ที่เผยแพร่ เซิร์ฟเวอร์ต้องเขียนออก 2,000 ครั้ง งานจริงจึงเท่ากับ "จำนวนครั้งที่เผยแพร่ต่อวินาที × จำนวนผู้สมัครรับ" กรณีแบบหนึ่งต่อหลายอย่างห้องแชตไลฟ์ คะแนนการแข่งขัน หรือแดชบอร์ดปฏิบัติการ คือกรณีที่มักเกินขีดจำกัดอย่างฉับพลันในจังหวะที่ผู้ชมเพิ่มขึ้น

การเชื่อมต่อที่ช้าเพียงหนึ่งเดียวรั้งทุกคนไว้

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

การรอคอยลุกลาม

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

โมโนลิธทำให้ปัญหาใหญ่ขึ้น

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

อีกเหตุผลหนึ่ง: การอัปเดตความปลอดภัยของสภาพแวดล้อมการทำงาน

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

ตัวอย่างล่าสุดคือช่องโหว่ร้ายแรงสองรายการที่เรียกกันว่า "PHP2Shell" ซึ่งโปรเจกต์ PHP แก้ไขเมื่อวันที่ 21 ตุลาคม 2026

  • CVE-2026-109618: หน่วยความจำเสียหายระหว่างจัดการกรณีหน่วยความจำไม่พอ (OOM)
  • CVE-2026-109619: use-after-free ที่เกิดจากสถานะที่ค้างอยู่ระหว่างคำขอ

ทั้งสองได้คะแนน CVSS v4.0 ที่ 9.2 และอาจทำให้ผู้โจมตีที่ไม่ได้ยืนยันตัวตนรันโค้ดจากระยะไกลได้ สิ่งที่ได้รับผลกระทบคือ PHP ที่ให้บริการคำขอเว็บ เช่น PHP-FPM, Apache mod_php และ php-cgi เวอร์ชันที่แก้ไขแล้วคือ 8.2.35, 8.3.36, 8.4.27 และ 8.5.12 ส่วน 8.1 และเก่ากว่าไม่ได้รับการอัปเดตอย่างเป็นทางการตามรอบแล้ว

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

ถ้ารันเอง ต้องเพิ่มสิ่งเหล่านี้

ทุกปัญหามีวิธีแก้ แต่ทุกวิธีเป็นงานต่อเนื่อง ไม่ใช่การตั้งค่าครั้งเดียวแล้วจบ

  • ดีพลอยแยกจากแอปพลิเคชัน แยกเซิร์ฟเวอร์เรียลไทม์ออกจากโมโนลิธ ให้ขยายและดีพลอยได้อย่างอิสระ ช่วงพีกจะได้ไม่ลากฟีเจอร์อื่นล่มไปด้วย
  • คิวที่มีขีดจำกัดสำหรับแต่ละการเชื่อมต่อ การเขียนต้องไม่รอกันเอง แต่ละการเชื่อมต่อมีคิวของตัวเอง เมื่อคิวเต็มแปลว่าไคลเอนต์นั้นตามไม่ทัน ควรตัดการเชื่อมต่อแทนที่จะปล่อยให้มันทำให้คนอื่นช้าลง
  • แยกการรับออกจากการส่ง API สำหรับเผยแพร่ไม่ควรรอจนผู้สมัครรับทุกคนได้รับข้อความก่อนจึงตอบกลับ ไม่อย่างนั้นเมื่อบรอดแคสต์ช้า แบ็กเอนด์ของคุณก็ช้าตาม
  • ขยายแนวนอน เซิร์ฟเวอร์เครื่องเดียวมีขีดจำกัดเสมอ การเพิ่มเครื่องหมายความว่าทุกเครื่องต้องแชร์แชนเนลและสถานะออนไลน์ร่วมกัน และมีโหลดบาลานเซอร์อยู่ด้านหน้า
  • ทดสอบความจุโดยอิงช่วงพีก การทดสอบโหลดที่ทราฟฟิกเฉลี่ยจะบอกแค่ว่า "ไม่มีปัญหา" สิ่งที่ต้องทดสอบคือหลายเท่าของพีก และสถานการณ์ที่ไคลเอนต์จำนวนมากเชื่อมต่อใหม่พร้อมกัน
  • เฝ้าดูคิวและเวลาแฝง ไม่ใช่แค่ CPU ขณะที่ CPU ยังดูว่าง คิวอาจกำลังยาวขึ้นอยู่ คุณต้องเห็นว่าข้อความรอนานแค่ไหน มีการเชื่อมต่อกี่รายการที่ถูกตัดเพราะช้าเกินไป และได้รับการแจ้งเตือนเมื่อสถานการณ์แย่ลง
  • มีคนเข้าเวร ช่วงพีกมักเป็นช่วงที่สำคัญที่สุดของคุณ การเปิดขายบัตร นาทีสุดท้ายของการแข่งขัน การเริ่มโปรโมชัน เมื่อเซิร์ฟเวอร์มีปัญหาต้องมีคนจัดการ
  • ติดตามการอัปเดตความปลอดภัย เมื่อช่องโหว่ในระบบปฏิบัติการ รันไทม์ของภาษา หรือไลบรารีถูกเปิดเผย ต้องประเมินผลกระทบ อัปเกรด และดีพลอยใหม่

เขียนทีละรายการตามลำดับ

อีเวนต์ใหม่
เผยแพร่
เซิร์ฟเวอร์
เขียนให้ B ก่อน
การเชื่อมต่อ Bเครือข่ายช้า
รอ B
การเชื่อมต่ออื่นทั้งหมดล่าช้าตามกัน

คิวของแต่ละการเชื่อมต่อ

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

บริการแบบโฮสต์รับภาระอะไรแทนคุณ

เมื่อใช้บริการเรียลไทม์แบบโฮสต์ งานข้างต้นผู้ให้บริการจะรับผิดชอบแทน ยกตัวอย่าง Hibira

  • ไคลเอนต์ที่ช้าไม่ทำให้คนอื่นช้าตาม แต่ละการเชื่อมต่อมีคิวที่มีขีดจำกัดของตัวเอง และการเขียนไม่ต้องรอกัน ไคลเอนต์ที่คิวเต็มหรือการเขียนหมดเวลาจะถูกปิดด้วยรหัสข้อผิดพลาด 4100 ของโปรโตคอล Pusher และ pusher-js จะเชื่อมต่อใหม่เองเมื่อได้รับรหัสนี้
  • คุณเห็นว่าเกิดอะไรขึ้น การตัดการเชื่อมต่อเหล่านี้จะแสดงบนการ์ดข้อผิดพลาดของแอปว่า "ตัดการเชื่อมต่อไคลเอนต์ที่ช้า" คุณจึงไม่ต้องไปขุดล็อกของเซิร์ฟเวอร์เอง
  • เราวางแผนความจุให้ เซิร์ฟเวอร์ทำงานเป็นคลัสเตอร์ที่แชร์แชนเนลและสถานะออนไลน์ร่วมกัน และขยายแนวนอนได้ ขีดจำกัดของแต่ละแพ็กเกจเป็นขีดจำกัดตายตัวที่ชัดเจน จึงไม่มีเรื่องน่าตกใจในบิล
  • ใช้โปรโตคอล Pusher pusher-js, Laravel Echo และไลบรารีฝั่งเซิร์ฟเวอร์ที่คุณใช้อยู่ใช้ต่อได้ทันที การเปลี่ยนบริการคือการเปลี่ยนโฮสต์และคีย์เท่านั้น
  • เราดูแลการแพตช์ การอัปเดตความปลอดภัยของเซิร์ฟเวอร์ WebSocket และสภาพแวดล้อมที่อยู่ข้างใต้เป็นหน้าที่ของเรา คุณยังต้องอัปเดตแอปของคุณเอง แต่มีสิ่งที่ต้องเฝ้าดูน้อยลงหนึ่งอย่าง

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

เมื่อไหร่ที่การรันเองยังสมเหตุสมผล

บริการแบบโฮสต์ไม่ใช่คำตอบเดียว ในกรณีต่อไปนี้ การรันเองอาจเหมาะกว่า

  • ทราฟฟิกน้อยและคงที่ ไม่มีพีกที่ชัดเจน
  • เนื้อหาของอีเวนต์ออกนอกโครงสร้างพื้นฐานของคุณไม่ได้
  • คุณมีทีมปฏิบัติการ ระบบเฝ้าดู และเวรเข้าเวรอยู่แล้ว บริการที่รันตลอดเวลาเพิ่มอีกหนึ่งตัวจึงไม่เป็นภาระ

ถ้าคุณใช้ Laravel บทความ รัน Reverb เองหรือใช้บริการแบบโฮสต์ เปรียบเทียบไว้ละเอียดกว่านี้ ทั้งสองแบบใช้โปรโตคอล Pusher ถ้าเปลี่ยนใจภายหลังก็แค่เปลี่ยนการตั้งค่า

ข้อสรุปของเรา

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

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

แพ็กเกจฟรีของ Hibira ไม่ต้องใช้บัตรเครดิต และเพียงพอสำหรับการพัฒนาและแอปขนาดเล็ก ถ้าคุณใช้ Pusher อยู่แล้ว ทำตามขั้นตอนใน ย้ายจาก Pusher Channels เพื่อย้ายมาโดยไม่ต้องแก้โค้ด