DEV Community

Cover image for Avoiding the Top 10 NGINX Configuration Mistakes - Part 1/2
terngr
terngr

Posted on

Avoiding the Top 10 NGINX Configuration Mistakes - Part 1/2

อ้างอิงจากบทความ NGINX: Avoiding the Top 10 NGINX Configuration Mistakes เขียนโดย Timo Stark of F5 & Sergey Budnevich of F5

TLDR

บทความต้นฉบับเขียนโดยทีม Engineer ของ NGINX รวบรวม Common Mistakes ที่พบตอนแก้เคสให้ลูกค้า เพื่อ Configure NGINX ให้ทำงานถูกต้องและมีประสิทธิภาพ บทความนี้เหมาะกับผู้มีพื้นฐานคุ้นเคยกับ NGINX ครับ

  1. Not enough file descriptors per worker
  2. The error_log off directive
  3. Not enabling keepalive connections to upstream servers
  4. Forgetting how directive inheritance works
  5. The proxy_buffering off directive
  6. Improper use of the if directive
  7. Excessive health checks
  8. Unsecured access to metrics
  9. Using ip_hash when all traffic comes from the same /24 CIDR block
  10. Not taking advantage of upstream groups

RTFA(Read the Full Article)

Mistake 1: Not Enough File Descriptors per Worker

การทำงานของ NGINX จะมี Master Process และ Worker processes เราสามารถกำหนดจำนวน Worker processes ได้ แต่โดย default จำนวน Worker processes จะเท่ากับจำนวน CPU Cores(หากดูใน nginx.conf จะพบ "worker_process auto;")
worker_process auto;

ในแต่ละ Worker process จะรับ connections ได้โดยค่า default=512 connections แต่เมื่อเราติดตั้ง NGINX เราจะได้ NGINX configuration file(nginx.conf) มาด้วย ซึ่งภายในมีการระบุ directive "worker_connections 1024;" ฉะนั้นเมื่อเราติดตั้ง NGINX โดยยังไม่ปรับแต่งอะไรเลย แต่ละ worker ก็จะรับ connections ได้ 1024 connections ตาม nginx.conf
worker_connection 1024;

ทีนี้แต่ละ connection จะต้องใช้ FDs(File descriptors) ด้วยครับ โดยเมื่อใช้งาน NGINX เป็น web server จะต้องใช้ 1FD สำหรับ client connection, และ 1FD สำหรับไฟล์ที่ web server จะ serve ให้กับ client ถ้ามีหลายไฟล์ ก็ใช้หลาย FDs ซึ่ง web pages ในปัจจุบันมักจะมีหลายไฟล์เป็นปกติ จึงต้องใช้ FDs มากตามไปด้วย

ถ้าใช้งาน NGINX เป็น proxy จะใช้ 1FD สำหรับ client connection, 1 FD สำหรับ upstream connection, และมักจะต้องการอีก 1FD สำหรับเก็บ temporary server response

ถ้าใช้งาน NGINX เป็น caching server จะแบ่งเป็น กรณี response ด้วย cache file ที่เก็บไว้ จะนับ FDs แบบเดียวกับการใช้งานเป็น web server, กรณีไม่มี cache file/cache expired จะนับ FDs แบบเดียวกับการใช้งานแบบ proxy

FDs ยังถูกใช้สำหรับแต่ละ log file และใช้ตอนที่ NGINX worker process คุยกับ NGINX master process แต่ก็เป็นจำนวนน้อยมากเมื่อเทียบกับ FDs ที่ใช้กับ Connections และ files

ทั้งนี้ค่า FDs limit per process หรือ maximum file descriptors ของแต่ละ worker process จะถูก limit โดย Operating System, โดยค่า FDs ที่เหมาะสมควรเป็นสองเท่าของ worker_connection(ดูจำนวน FDs ที่ต้องใช้ตาม Use cases ได้จากย่อหน้าก่อน) หรือก็คือ 1024*2=2048 แต่ค่า FDs limit โดย default เท่ากับ 1024

เราสามารถปรับค่า FDs limit per process ได้โดย

  • ulimit command กรณี start NGINX จาก shell
  • ใช้ init script หรือ systemd service manifest กรณี start NGINX จาก service
  • แก้ไขไฟล์ /etc/security/limits.conf

วิธีที่ง่ายและแนะนำคือระบุ "worker_rlimit_nofile" directive ใน nginx.conf จะกำหนด maximum file descriptors per worker process ได้ทันที
worker_rlimit_nofile 2048;

สุดท้าย ยังมี system-wide limit ของ จำนวน FDs ด้วย หรือก็คือจำนวน FDs ของทุกๆ processes รวมกัน โดยตรวจสอบได้จากคำสั่ง "sysctl fs.file-max" ค่านี้มักจะตั้งมาใหญ่เพียงพออยู่แล้ว แต่ควรตรวจสอบให้แน่ใจว่าตั้งค่า system-wide limit ของจำนวน FDs ไว้สูงกว่าจำนวน Maximum FDs ของทุก NGINX Worker process รวมกัน(worker_rlimit_nofile*worker_processes) ไม่เช่นนั้น ถ้า NGINX มีการใช้ FDs จนเต็ม(เช่น จำนวนผู้ใช้งานสูง, โดน DoS attack) ก็จะ ใช้ FDs เกิน system-wide limit ทำให้สร้าง FDs ใหม่เพิ่มไม่ได้ ผลคือทำให้ Login เข้าเครื่องเพื่อแก้ไขปัญหาไม่ได้ไปด้วย
sysctl fs.file-max
fs.file-max = 9223372036854775807

Mistake 2: The error_log off directive

error_log off;
การ Configure "error_log off;" ไม่ใช่การปิด Error log แต่เป็นการบอกให้เขียน Error log ลงไฟล์ที่ชื่อ off ใน default location(ส่วนใหญ่ default location คือ /etc/nginx)

ในสถานการณ์ที่ Storage มีจำกัดมากๆ ที่การเขียน Error log จะทำให้ Disk เต็ม เราสามารถ Disable Error log ได้โดยระบุ Directive "error_log /dev/null emerg;" ใน nginx.conf
error_log /dev/null emerg;

Directive นี้จะมีผลต่อเมื่อ NGINX ได้อ่านและ validate configuration ใหม่แล้วเท่านั้นครับ

อีกวิธีในการเปลี่ยนที่เก็บ Error log คือเพิ่ม -e ตอนรัน nginx command

Mistake 3: Not enabling keepalive connections to upstream servers

โดย Default เมื่อมี request เข้ามา NGINX จะเปิด connection ใหม่ไปยัง upstream(backend) วิธีการนี้ปลอดภัย แต่ไม่มีประสิทธิภาพ เพราะการเปิด connection แต่ละครั้งจะต้องมีการทำ Handshake ส่ง packages ไปกลับ 3 packages, และเมื่อปิด connection ต้องส่งไปกลับอีก 3-4 packages

การเปิด connection ใหม่สำหรับทุกๆ request โดยเฉพาะช่วง high traffic เป็นการสิ้นเปลือง system resource และอาจเจอสถานการณ์ที่ไม่สามารถเปิด Connection แต่ละ connection จะมีค่าเฉพาะของตัวเองคือ

  1. Source Address(คงที่)
  2. Source Port(เปลี่ยนไปตาม connection ใหม่ที่เปิดขึ้นมา)
  3. Destination Address(คงที่)
  4. Destination Port(คงที่)

จะเห็นได้ว่าข้อ1, 3, 4 เป็นค่าเดิมเสมอ แต่ Source Port นั้น ทุกครั้งที่เปิด connection ใหม่ จะต้องใช้ source port ใหม่ และเมื่อปิด connection Linux socket จะอยู่ในสถานะ TIME-WAIT 2 นาที จนกว่า port นั้นจะพร้อมใช้งานอีกครั้ง

ถ้า source ports ถูกใช้งานจนหมด ก็จะไม่สามารถเปิด connection ใหม่ได้

ปัญหานี้แก้ได้โดยเปิดใช้ keepalive connections ระหว่าง NGINX และ Upstream servers เมื่อแต่ละ request ทำงานเสร็จ แทนที่จะปิด connection ก็เปิดไว้ใช้งานกับ request ถัดไป เป็นการป้องกันปัญหา source ports หมด และยังช่วยเพิ่ม Performance

การเปิดใช้งาน keepalive ทำได้โดยระบุจำนวน keepalive connection ให้กับแต่ละ upstream{} ที่ต้องการ โดยจำนวนที่เหมาะสมคือสองเท่า ของจำนวน servers ใน upstream นั้นๆ โดยจำนวนนี้มากพอที่จะทำ keepalive ให้กับทุกๆ servers ใน upstream และเล็กเพียงพอให้ upstream สามารถ process connection ใหม่ได้ด้วย

ข้อควรระวัง keepalive เป็นการกำหนดจำนวน keepalive connection เท่านั้น ไม่ใช่การจำกัดจำนวน connection per worker ฉะนั้นจึงไม่จำเป็นต้องตั้งไว้สูง

ข้อควรระวังถัดไป ถ้าใน upstream นั้นมีการใช้ load balance algorithm ได้แก่ hash, ip_hash, least_conn, least_time, random จะต้องวาง keepalive ไว้หลัง load balance algorithm เสมอ อันนี้เป็นหนึ่งในข้อยกเว้นเรื่องลำดับ(อีกตัวอย่างหนึ่งก็คือการทำ ACL ที่ลำดับก่อนหลังมีความสำคัญ) ซึ่งส่วนใหญ่ NGINX จะไม่มีลำดับก่อนหลังในการเขียน configuration

upstream backend {
least_time
keepalive 8
server backend1.example.com;
server backend2.example.com;
server backend3.example.com;
server backend4.example.com;
}

โดย default NGINX จะใช้ HTTP/1.0 และเพิ่ม header ชื่อ Connection: close ไปใน connection กับ upstream โดยอัตโนมัติ ทำให้เมื่อ request ทำงานเสร็จ ก็จะปิด connection ทันทีแม้จะมีการระบุ keepalive ไว้ที่ upstream แล้วก็ตาม

ปัญหานี้แก้ได้โดยเพิ่ม directive ให้ใช้ http/1.1 และ บังคับ header Connection: ""

location / {
proxy_http_version 1.1;
proxy_set_header "Connection" "";
...
}

Mistake 4: Forgetting how directive inheritance works

ก่อนอื่นต้องอธิบายเรื่อง inheritance ก่อนครับ ว่าเป็นการ inherit แบบ downwards หรือ outside-in, โดย directive ที่อยู่ชั้นบนสุด จะ inherit ไปยัง directive ที่อยู่ชั้นถัดๆมาทั้งหมด ตัวอย่างเช่น directive ในชั้น http{} จะ inherit ไปยัง server{} ทั้งหมดใต้ http นั้น, และยัง inherit ไป location{} ที่อยู่ใต้แต่ละ server{} ด้วย

กรณีถ้า parent context(ชั้นบน) และ child context(ชั้นล่าง) ระบุ directive ตัวเดียวกัน

child context จะ overrides ค่า directives จาก parent context ไม่ใช่เพิ่ม directives ต่อเข้าไป

ข้อผิดพลาดนี้ มักจะเกิดกับ Array directives เช่น ถ้าที่ http{} เราใส่ 2 headers
add_header X-HTTP-LEVEL-HEADER 1;
add_header X-ANOTHER-HTTP-LEVEL-HEADER 1;

จากนั้นที่ server{} เราใส่ 1 header
add_header X-HTTP-LEVEL-HEADER 1;

ผลที่ได้ คือ directive ที่ server{} จะ overrides directive ของ http{} ทำให้เหลือ header เพียงตัวเดียวคือ X-HTTP-LEVEL-HEADER: 1

มาดูตัวอย่างกันครับ จาก NGINX Configuration

Image description

เรียกไปที่พอร์ต 8080 ไม่มี add_header ทั้งระดับ server{} และ location{} จึงใช้ค่า add_header ที่ inherit มาจาก http{} จำนวน 2 headers
Image description

เรียกไปที่พอร์ต 8081 มี add_header ทั้งระดับ http{} และ server{} แต่ไม่มีที่ location{} ฉะนั้น add_header ของ server{} overrides http{} ทำให้ add_header ทั้งสองอันของ http{} ถูกแทนที่ด้วย add_header ตัวเดียวของ server{}

Image description

เรียกไปที่พอร์ต 8081 ที่ location /test มี add_header ทั้งระดับ http{}, server{} และ location{} ฉะนั้น add_header ของ location{} overrides http{} และ server{} ทำให้ add_header ทั้งของ http{} และ server{} ทั้งหมด ถูกแทนที่ด้วย add_header ของ location{}
Image description

ถ้าเราต้องการให้ location{} แสดง add_header ทั้งหมดจาก http{} และ server{} จะต้องระบุ add_header จาก http{} และ server{} ซ้ำอีกครั้งที่ location{} ดังตัวอย่าง เรียกไปที่พอร์ต 8081 ที่ location /correct
Image description

Mistake 5: The proxy_buffering off directive

proxy_buffering จะเก็บ responses ที่ได้รับจาก server ไว้ที่ internal buffer จนครบก่อน แล้วจึงส่ง response ไปยัง client โดย buffer นี้จะถูกเก็บไว้จนกว่า client จะได้รับ response ครบทั้งหมด

ข้อดีของ proxy_buffering คือถ้า client มี slow connection NGINX สามารถรับ response ทั้งหมดจาก server ได้ในคราวเดียว เมื่อ server ส่ง response เสร็จ ก็สามารถ serve request อื่นต่อได้ทันที, โดยที่ NGINX ยังคงทยอยส่ง response ให้ Client จนกว่า Client จะได้รับ Response ทั้งหมด

หากปิดการใช้งาน proxy_buffering NGINX จะทำการ Buffer เฉพาะ response ส่วนแรกตามขนาด memory page เท่านั้น แล้วส่ง response ไปยัง client ทันที โดยเป็นการส่งแบบ synchronously ทำให้ server ไม่สามารถส่ง response ทั้งหมดให้กับ NGINX ได้ทันที แต่ server ต้องรอในสถานะ idle จนกว่า client จะได้รับ response ส่วนนี้เสร็จแล้ว NGINX จึงจะพร้อมรับ response ส่วนถัดไปได้

การปิด proxy_buffering ทำให้ไม่สามารถใช้งาน rate limiting และ caching ได้ด้วย

ประโยชน์ที่ได้จากการปิด proxy_buffering จะเป็นเรื่อง latency ที่เพิ่มมาเพียงเล็กน้อยและไม่คุ้มค่า แต่ก็อาจมี Use case ที่จะเป็นต้องเปิด proxy_buffering เช่นการทำ long polling

NGINX เปิดใช้งาน proxy_buffering โดย default Mistake นี้เลี่ยงได้โดยไม่ระบุ proxy_buffering off; เพิ่มครับ

ตัวอย่างการทำ long polling เช่น Client ต้องการทราบข้อมูลแบบ Real time ว่า data ฝั่ง server มีการเปลี่ยนแปลงหรือไม่(ยกตัวอย่าง chat box ต้องการทราบว่ามีการส่งข้อความจากผู้ใช้คนอื่นๆมาเพิ่มไหม) โดยฝั่ง Client จะทำ long polling รอไว้ และฝั่ง server จะส่ง response ต่อเมื่อ data มีการเปลี่ยนแปลงเท่านั้น(ตัวอย่าง chat box คือมีข้อความใหม่), แต่ถ้า server ส่ง response ทันทีไม่ว่า data นั้นเปลี่ยนแปลงหรือไม่(ตัวอย่าง chat box ก็คือมีการ response ไม่ว่าจะมีข้อความใหม่หรือไม่ก็ตาม) ฝั่ง client ก็จะต้อง request ไปใหม่เพื่อขอ data ล่าสุด ทำให้เปลือง Resources มาก

Photo by pixabay

Top comments (0)