ป้ายกำกับ: System Analyst

nginx: ทำไมโดเมนยาว ๆ ถึงทำเซิร์ฟเวอร์พัง?nginx: ทำไมโดเมนยาว ๆ ถึงทำเซิร์ฟเวอร์พัง?

หากคุณเป็น 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

$$\text{Max Domain Length} \approx \text{Bucket Size} - \text{System Overhead (ประมาณ 16-24 bytes)}$$

นี่คือระยะความยาวของชื่อโดเมนที่ปลอดภัยที่สุด (รวมซับโดเมนและนามสกุลท้ายแล้ว) ในแต่ละขนาดของ server_names_hash_bucket_size บนสถาปัตยกรรมเซิร์ฟเวอร์ 64-bit

Bucket Sizeความยาวชื่อโดเมนที่รองรับได้จริง (โดยไม่ Error)เหมาะสำหรับ
32 ไบต์ไม่เกิน 8 – 16 ตัวอักษรโดเมนสั้นมากระดับจดทะเบียนพิเศษ (มักมี Error ตั้งแต่สตาร์ท)
64 ไบต์ (Default)ไม่เกิน 40 – 48 ตัวอักษรโดเมนมาตรฐานทั่วไป เช่น [www.plusmagi.com](https://www.plusmagi.com)
128 ไบต์ไม่เกิน 104 – 112 ตัวอักษรโดเมนสุ่มสายยาวความปลอดภัยสูง (Hex/สุ่มสตริง)
256 ไบต์ไม่เกิน 232 – 240 ตัวอักษรระบบที่มี Subdomain ซ้อนกันหลายชั้น หรือใช้งานแบบ Dynamic

แนวทางการแก้ไขและผลกระทบต่อประสิทธิภาพ

การแก้ไขทำได้ง่ายมาก เพียงแค่ไปขยายขนาด “กล่องล็อกเกอร์” ในคอนฟิกหลักของ Nginx ให้กว้างขึ้น โดยขยับจากค่าเริ่มต้น 64 ไปเป็น 128

เปิดไฟล์คอนฟิกหลัก

sudo nano /etc/nginx/nginx.conf

แทรกหรือเปิดใช้งานคำสั่งนี้เข้าไปภายใต้บล็อก http { ... }

http {
    # Increase bucket size to support long/randomized subdomains
    server_names_hash_bucket_size 128; 
    
    # (Optional) Increase max size if managing thousands of subdomains
    # server_names_hash_max_size 1024;
}

🏎️ การขยายค่านี้ส่งผลต่อประสิทธิภาพ (Performance) ไหม?

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


อ่านเพิ่มเติม