หากคุณเป็น System Analyst หรือ Developer ที่ต้องจัดการเว็บเซิร์ฟเวอร์ Nginx และชอบใช้เทคนิคความปลอดภัยประเภท Security through Obscurity เช่นการสุ่มชื่อ Subdomain ยาว ๆ เพื่อซ่อนแอปพลิเคชันหลังบ้าน (เช่น phpMyAdmin หรือคิวรี Extended Events ของฐานข้อมูล) วันดีคืนดีคุณอาจจะเจอ Error สยองโลกตอนสั่ง nginx -t ว่า:
[emerg] could not build server_names_hash, you should increase server_names_hash_bucket_size: 64
nginx: configuration file /etc/nginx/nginx.conf test failed
หากลองคำนวณดู โดเมนสุ่มแบบ Hex สายยาวของคุณมีความยาว 61 ตัวอักษร ในขณะที่ค่าเริ่มต้นของระบบบอกว่ามีพื้นที่ 64 ไบต์ ในทางคณิตศาสตร์ $61 < 64$ มันควรจะใส่ได้พอดีและไม่เกิด Error ใช่ไหมครับ? แต่ทำไม Nginx ถึงสั่งระบบพังทันที? บทความนี้มีคำตอบเชิงลึกในระดับโครงสร้างข้อมูลมาเล่าให้ฟังครับ
Default ของ Nginx (ค่าเริ่มต้นมาจากไหน?)
ก่อนอื่นต้องเข้าใจก่อนว่า Nginx เป็นเว็บเซิร์ฟเวอร์ที่ขึ้นชื่อเรื่อง ความเร็วและการประหยัดหน่วยความจำ เวลาที่มี Request วิ่งเข้ามา Nginx จะต้องหาให้เจอเร็วที่สุดว่า Request นี้ต้องการคุยกับ server_name ตัวไหนในคอนฟิก
เพื่อความรวดเร็ว Nginx จะไม่ใช้วิธีไล่อ่านคอนฟิกทีละบรรทัดแบบเรียงลำดับ (Linear Search) แต่จะนำชื่อโดเมนทั้งหมดในระบบมาสร้างเป็น ตารางแฮช (Hash Table) เก็บไว้ในหน่วยความจำ CPU Cache
- ค่า Default ทั่วไป: โดยปกติจะขึ้นอยู่กับสถาปัตยกรรมของ CPU และขนาดของ Cache line (มักเป็น 32, 64 หรือ 128 ไบต์) แต่บนระบบปฏิบัติการลินุกซ์ส่วนใหญ่ เช่น Ubuntu/Debian ค่าเริ่มต้นในไฟล์
nginx.confมักจะถูกล็อกไว้ที่64ไบต์
เจาะลึก 3 กลไกภายในของ Nginx (ทำไมยาว 64 พอดีถึงใส่ไม่ได้?)
สาเหตุที่ชื่อโดเมนยาว 64 ไบต์ ไม่สามารถลงกล่องหน่วยความจำขนาด 64 ไบต์ได้ เกิดจากกลไกการจัดการหน่วยความจำแฝงในภาษา C และสถาปัตยกรรมของ Nginx ดังนี้
🛠️ กลไกที่ 1: พื้นที่สำหรับอักขระปิดท้าย (Null Terminator)
Nginx ถูกพัฒนาขึ้นด้วยภาษา C ซึ่งโครงสร้างของข้อมูลประเภทข้อความสตริง (String) ทุกชุด จะต้องปิดท้ายด้วยอักขระพิเศษ \0 (Null Terminator) เสมอ เพื่อให้ระบบคอมไพเลอร์รู้ว่าข้อความสิ้นสุดตรงไหน
- นั่นหมายความว่า โดเมนยาว 61 ตัวอักษร จะต้องใช้พื้นที่จัดเก็บจริงในหน่วยความจำอย่างน้อย $61 + 1 = 62$ ไบต์
🛠️ กลไกที่ 2: ค่าธรรมเนียมแฝงในล็อกเกอร์ (Hash Bucket Overhead)
ในตารางแฮชของ Nginx ข้อมูลที่อยู่ในกล่อง (Bucket) ไม่ได้มีแค่ตัวอักษรของชื่อโดเมนโดด ๆ แต่ภายในแต่ละกล่องจะต้องแบ่งพื้นที่ (Overhead) ไปเก็บโครงสร้างข้อมูลสำคัญเพื่อใช้ในระบบควบคุมด้วย ได้แก่
- Pointer: ตัวชี้ตำแหน่งหน่วยความจำที่จะกระโดดไปยัง Core Server Configuration Block (ใช้พื้นที่ 8 ไบต์ เต็ม ๆ บนระบบ 64-bit)
- Hash Node Header: โครงสร้างส่วนหัวเพื่อบอกสถานะของ Node นั้น ๆ (ใช้อีกประมาณ 4 – 8 ไบต์)
เมื่อหักค่าธรรมเนียมแฝงของระบบออกไปประมาณ 12 – 16 ไบต์ ทำให้กล่องขนาด 64 ไบต์ เหลือพื้นที่ให้วางชื่อโดเมนจริง ๆ ไม่ถึง 50 ไบต์ ด้วยซ้ำครับ โดเมนยาว 61 ไบต์จึงชนเพดานทันที
🛠️ กลไกที่ 3: การจัดการข้อมูลชนกัน (Hash Collision Mitigation)
ตารางแฮชของ Nginx ใช้กลไกการกระจายข้อมูล ถ้าเกิดกรณีที่มีชื่อโดเมนอื่นในเซิร์ฟเวอร์มีค่าแฮชคำนวณออกมาแล้วตกอยู่กล่องเดียวกัน (Collision) Nginx จะใช้วิธีต่อท่อนข้อมูลเชื่อมกันในกล่องนั้นเลย Nginx จึงตั้งเงื่อนไขความปลอดภัยไว้ว่า หากความยาวข้อมูลรวมค่าแฮชแฝง มีขนาดเข้าใกล้หรือเกือบเต็มขนาดกล่อง ระบบจะสั่งบล็อกและฟ้อง Error ทันที เพื่อป้องกันไม่ให้เกิดอาการหน่วยความจำล้น (Buffer Overflow) ในระดับ CPU Cache
📊 ตารางเปรียบเทียบ: แต่ละขนาด Bucket รองรับโดเมนได้ยาวเท่าไหร่?
จากสูตรคำนวณพื้นที่จัดเก็บที่แท้จริงของ Nginx
PHP
$$\text{Max Domain Length} \approx \text{Bucket Size} - \text{System Overhead (ประมาณ 16-24 bytes)}$$