ซ้อน reverse proxy หลายชั้น: สถาปัตยกรรมสำหรับการใช้งานจริง
การวาง reverse proxy สี่ตัวไว้หน้าแอปพลิเคชันเดียวอาจดูเหมือนระบบที่ควรลดความซับซ้อน คู่มือโครงสร้างพื้นฐานเว็บสำหรับการใช้งานจริงส่วนใหญ่เลือกเพียงชั้นเดียว เช่น Nginx, Caddy หรือ Traefik และมองว่าการเพิ่มชั้นอื่นเป็นการออกแบบเกินความจำเป็น แต่แต่ละเครื่องมือแก้ปัญหาคนละด้าน เมื่อเข้าใจการแบ่งหน้าที่แล้ว ความซ้ำซ้อนที่เห็นก็กลายเป็นสถาปัตยกรรมที่สอดคล้องกัน
นี่คือสถาปัตยกรรมที่เราใช้กับระบบของลูกค้าหลายราย: Cloudflare อยู่ที่ขอบเครือข่าย Traefik เป็นจุดรับคำขอที่รับรู้สถานะคอนเทนเนอร์ Varnish ทำแคช HTTP ส่วน Nginx ให้บริการแอปพลิเคชัน โดยมีตัวแอปพลิเคชันอยู่ปลายทาง ลำดับการรับคำขอเป็นดังนี้:
Cloudflare → Traefik → Varnish → Nginx → Application
ต่อไปนี้อธิบายหน้าที่ของแต่ละชั้น ขอบเขตความรับผิดชอบ และผลต่อประสิทธิภาพ รวมถึงภาระที่สถาปัตยกรรมนี้เพิ่มขึ้นเมื่อเทียบกับระบบที่เรียบง่ายกว่า
หน้าที่จริงของแต่ละชั้น
Cloudflare ทำงานที่ขอบเครือข่ายอินเทอร์เน็ตสาธารณะ รับมือกับทราฟฟิก DDoS ใช้กฎ WAF ยุติการเชื่อมต่อ TLS ที่ PoP ซึ่งกระจายอยู่ทั่วโลก และแคชไฟล์คงที่ใกล้ผู้ใช้ สิ่งที่ทำได้ไม่ดีคือการเลือกเส้นทางไปยังคอนเทนเนอร์อย่างชาญฉลาด หรือแคชการตอบกลับของแอปพลิเคชันที่ต้องตรวจสอบคุกกี้ หน้าที่เหล่านี้จึงเป็นของเครื่องมืออื่น
Traefik เป็นจุดรับคำขอภายในโครงสร้างพื้นฐานที่รับรู้สถานะคอนเทนเนอร์ เมื่อใช้ ชุดระบบ Docker สำหรับผู้เช่าหลายราย Traefik จะอ่าน label ของคอนเทนเนอร์และเลือกเส้นทางตามนั้น จึงไม่ต้องดูแลไฟล์กำหนดค่าด้วยมือที่อาจล้าสมัยเมื่อคอนเทนเนอร์รีสตาร์ทหรือเพิ่มจำนวน นอกจากนี้ยังจัดการใบรับรอง TLS จาก Let's Encrypt สำหรับโดเมนภายในหรือสภาพแวดล้อมทดสอบก่อนใช้งานจริง ลบหรือเขียนส่วนหัวใหม่ และใช้ชุด middleware เช่น รายการ IP ที่อนุญาตหรือ basic auth Traefik ไม่ทำแคช และบีบอัดข้อมูลได้ไม่ดีเมื่อรับภาระงานสูง หน้าที่หลักคือการกำหนดเส้นทางและ middleware ซึ่งทำได้อย่างมีประสิทธิภาพ
Varnish เป็นตัวเร่งการให้บริการ HTTP อยู่ระหว่างจุดรับคำขอกับเซิร์ฟเวอร์แอปพลิเคชันเพื่อรองรับทราฟฟิกการอ่านโดยเฉพาะ อินสแตนซ์ที่ปรับแต่งดีสามารถให้บริการคำขอหลายหมื่นครั้งต่อวินาทีจากหน่วยความจำ และส่งหน้า HTML ทั้งหน้าได้ในเวลาต่ำกว่าหนึ่งมิลลิวินาที VCL (Varnish Configuration Language) ช่วยควบคุมอย่างละเอียดว่าจะเก็บอะไรในแคช นานเท่าไร และภายใต้เงื่อนไขใด ละเอียดกว่ากฎแคชของ Cloudflare มาก จึงสำคัญต่อแอปพลิเคชันที่ต้องยืนยันตัวตนซึ่งอาจแคชการตอบกลับบางส่วนแยกตามผู้ใช้ได้ด้วยการกำหนดการคำนวณแฮชเอง (ตัวอย่างด้านล่างไม่ได้ทำเช่นนี้ แต่ส่งต่อทราฟฟิกของผู้ใช้ที่เข้าสู่ระบบโดยไม่ใช้แคช) หรือหน้าที่ต้องกำหนด TTL ต่างกันตามพารามิเตอร์ในคำขอ
Nginx ในสถาปัตยกรรมนี้ทำหน้าที่ตามที่ออกแบบมาแต่เดิม คือให้บริการไฟล์และส่งคำขอไปยังกระบวนการแอปพลิเคชันปลายทาง CloudPanel ใช้ Nginx เป็นเว็บเซิร์ฟเวอร์ที่ระบบดูแลให้ แต่ละ vhost จึงมีค่ากำหนดที่สร้างอัตโนมัติสำหรับ PHP-FPM การให้บริการไฟล์คงที่ และการบีบอัด gzip CloudPanel เป็นผู้ดูแลค่ากำหนดนี้ จึงไม่ควรแก้ไขโดยตรง Nginx ในที่นี้รับคำขอ ส่งต่อไปยังแอปพลิเคชัน และส่งการตอบกลับคืน โดยไม่ได้ทำหน้าที่ตัดสินใจเลือกเส้นทาง
กำหนดค่า Traefik ด้วย label ของคอนเทนเนอร์
การกำหนดเส้นทางของ Traefik ในชุดระบบนี้ใช้ label ของ Docker Compose ทั้งหมด เมื่อวาง Varnish ไว้หลัง Traefik ต้องให้ Traefik ส่งคำขอไปยังคอนเทนเนอร์ Varnish แทนการส่งตรงไปยัง Nginx จุดนี้มักทำให้สับสน เพราะตัวอย่าง Traefik ทั่วไปใช้รูปแบบส่งตรงไปยังแอปพลิเคชัน และเอกสารอธิบายการแทรก Varnish ในเส้นทางนี้ยังมีไม่มาก
services:
varnish:
image: varnish:7.5
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`www.example.com`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls=true"
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
- "traefik.http.services.myapp.loadbalancer.server.port=80"
- "traefik.http.middlewares.myapp-proto.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.middlewares.myapp-proto.headers.customrequestheaders.X-Forwarded-Port=443"
- "traefik.http.routers.myapp.middlewares=myapp-proto"
networks:
- traefik-public
- internal
ส่วนหัว X-Forwarded-Proto สำคัญมาก หากไม่มี แอปพลิเคชันจะเห็นทุกคำขอเป็น HTTP เพราะ Varnish สื่อสารกับ Nginx ผ่าน HTTP ที่ไม่เข้ารหัสบนเครือข่าย Docker ภายใน ส่งผลให้แอปพลิเคชันที่บังคับใช้ HTTPS เปลี่ยนเส้นทางวนซ้ำ Traefik ใส่ส่วนหัวนี้ก่อนคำขอเข้าสู่เครือข่ายภายใน Varnish ส่งผ่านโดยไม่แก้ไข และ Nginx ส่งต่อไปยังแอปพลิเคชัน ส่วนหัว X-Forwarded-Port ป้องกันปัญหาแบบเดียวกันในแอปพลิเคชันที่ตรวจสอบพอร์ตด้วย
อีกจุดที่ต้องพิจารณาคือ passHostHeader ของ Traefik ซึ่งมีค่าเริ่มต้นเป็น true ทำให้ Varnish ได้รับส่วนหัว Host เดิม VCL ของ Varnish ใช้ค่านี้กำหนดคีย์แคช และ Nginx ใช้เลือก vhost หากเปลี่ยนค่านี้ ทั้ง Varnish และ Nginx อาจได้รับโฮสต์ผิดและทำงานล้มเหลวโดยไม่มีสัญญาณชัดเจน ทำให้หาสาเหตุยาก
Varnish VCL: ตรรกะการทำแคช
VCL ด้านล่างเป็นจุดเริ่มต้นสำหรับแอปพลิเคชัน WordPress หรือ PHP หลัง CloudPanel ยังไม่ใช่นโยบายที่พร้อมใช้ในระบบจริง ครอบคลุมการข้ามแคชสำหรับผู้ใช้ที่เข้าสู่ระบบและคำขอที่ทำซ้ำแล้วอาจเกิดผลซ้ำซ้อน การเพิ่มระยะเวลาแคชไฟล์คงที่ การปรับพารามิเตอร์ติดตามให้เป็นรูปแบบเดียวกัน และช่วงผ่อนผันที่ให้บริการหน้าเว็บเก่าเล็กน้อยระหว่างที่ Varnish อัปเดตหน้าเหล่านั้น ก่อนใช้งานจริง ให้ข้ามแคชสำหรับหน้าตะกร้าสินค้า ขั้นตอนชำระเงิน และบัญชีของ WooCommerce รวมถึงคุกกี้ wp_woocommerce_session_ ด้วย เนื่องจาก vcl_backend_response นี้ลงท้ายด้วย return(deliver) ตรรกะในตัวของ Varnish ที่กำหนดให้การตอบกลับซึ่งมี Set-Cookie หรือ Cache-Control: private/no-store แคชไม่ได้จึงไม่ทำงาน ต้องเพิ่มการตรวจสอบที่เทียบเท่ากัน
vcl 4.1;
backend default {
.host = "nginx"; # service name in docker-compose
.port = "80";
.connect_timeout = 5s;
.first_byte_timeout = 30s;
.between_bytes_timeout = 10s;
}
sub vcl_recv {
# Pass admin and authenticated requests
if (req.url ~ "^/wp-(admin|login|cron|json)" ||
req.http.Cookie ~ "wordpress_logged_in_" ||
req.http.Cookie ~ "woocommerce_items_in_cart") {
return(pass);
}
# Only cache GET and HEAD
if (req.method != "GET" && req.method != "HEAD") {
return(pass);
}
# Normalise tracking parameters out of the cache key
if (req.url ~ "[?&](utm_source|utm_medium|utm_campaign|fbclid|gclid)=") {
set req.url = regsuball(req.url,
"&(utm_source|utm_medium|utm_campaign|fbclid|gclid)=[^&]+", "");
set req.url = regsuball(req.url,
"\?(utm_source|utm_medium|utm_campaign|fbclid|gclid)=[^&]+&", "?");
set req.url = regsub(req.url,
"\?(utm_source|utm_medium|utm_campaign|fbclid|gclid)=[^&]+$", "");
}
# Remove cookies for static assets — allows caching
if (req.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|woff2?|svg|webp)(\?.*)?$") {
unset req.http.Cookie;
}
return(hash);
}
sub vcl_backend_response {
# Cache static assets for 7 days
if (bereq.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|woff2?|svg|webp)(\?.*)?$") {
set beresp.ttl = 7d;
set beresp.grace = 24h;
unset beresp.http.Set-Cookie;
return(deliver);
}
# Cache HTML pages for 5 minutes with a 60-second grace period
if (beresp.status == 200 && beresp.http.Content-Type ~ "text/html") {
set beresp.ttl = 300s;
set beresp.grace = 60s;
}
# Never cache error responses
if (beresp.status >= 500) {
set beresp.ttl = 0s;
}
return(deliver);
}
sub vcl_deliver {
# Expose cache status for debugging
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
set resp.http.X-Cache-Hits = obj.hits;
} else {
set resp.http.X-Cache = "MISS";
}
# Remove server identity headers
unset resp.http.Via;
unset resp.http.X-Varnish;
return(deliver);
}
การปรับพารามิเตอร์ติดตามใน vcl_recv เป็นรูปแบบเดียวกันมักถูกมองข้ามและมีต้นทุนสูงหากละเลย หากไม่ได้กำหนดการเขียน URL ใหม่ที่ Cloudflare ค่า UTM และโทเคน fbclid ยังมาถึง Varnish ใน URL เดิม หากไม่ปรับรูปแบบ ทุกการคลิกโฆษณา Facebook จะสร้างรายการแคชใหม่ ทั้งที่แอปพลิเคชันมองว่าเป็นหน้าเดียวกัน ระหว่างแคมเปญ แคชจึงแตกเป็นรายการที่ยังไม่เคยถูกเรียกใช้หลายพันรายการ จนเสียประโยชน์จากอัตราการพบข้อมูลในแคช
กลไกอีกอย่างที่ควรเข้าใจคือ grace ใน vcl_backend_response เมื่อ TTL ของรายการแคชหมดอายุ ปกติคำขอถัดไปต้องรอ Nginx ส่งการตอบกลับใหม่ก่อนที่ Varnish จะส่งข้อมูลได้ เมื่อทราฟฟิกพุ่งสูง เช่น หลังส่งจดหมายข่าวหรือมีการกล่าวถึงบนโซเชียลมีเดีย คำขอหลายรายการจะเข้ามาพร้อมกันเพื่อเรียกแคชที่หมดอายุ หากไม่มีช่วงผ่อนผัน Varnish ก็ยังรวมคำขอสำหรับรายการที่แคชได้ให้เหลือการดึงข้อมูลจาก Nginx เพียงครั้งเดียว แต่คำขออื่นต้องรอให้การดึงข้อมูลนั้นเสร็จ เมื่อกำหนดช่วงผ่อนผัน 60 วินาที Varnish จะส่งข้อมูลแคชเก่าให้คำขอเหล่านั้นทันที ระหว่างที่ดึงข้อมูลใหม่เบื้องหลังเพียงครั้งเดียวเพื่ออัปเดตแคช Nginx จึงรับคำขอเพียงหนึ่งรายการแทนที่จะเป็นหลายร้อยรายการเมื่อแคชหมดอายุ และผู้เข้าชมไม่ต้องรอ
ผลต่อประสิทธิภาพ
ผลด้านประสิทธิภาพของชุดระบบนี้มีทั้งข้อดีและภาระส่วนเพิ่มที่วัดได้
ส่วนที่ทำงานได้ดี: หน้าที่เก็บแคชได้และให้บริการจากหน่วยความจำของ Varnish จะตอบกลับในเวลาต่ำกว่า 2ms ที่ชั้น Traefik หน้าแรก WordPress ที่ PHP ใช้เวลาสร้าง 400ms จะให้บริการคำขอถัดไปใน 1-2ms จนกว่า TTL จะหมด อัตราการรองรับคำขอเพิ่มขึ้นถึงระดับสิบเท่า ไม่ใช่เพียงเล็กน้อย การให้บริการไฟล์คงที่ผ่าน Nginx เร็วอยู่แล้ว และ Cloudflare ยังแคชไฟล์เหล่านั้นที่ขอบเครือข่ายด้วย Nginx จึงแทบไม่ต้องให้บริการไฟล์เหล่านี้ในสภาพแวดล้อมใช้งานจริง
ส่วนที่เพิ่มภาระ: เมื่อไม่พบข้อมูลในแคช คำขอต้องผ่านสี่จุดแทนที่จะเป็นจุดเดียว คำขอ PHP ที่แคชไม่ได้ เช่น เซสชันของผู้ใช้ที่เข้าสู่ระบบ การส่ง POST หรือหน้าตะกร้าของ WooCommerce จะผ่าน Traefik, Varnish และ Nginx ก่อนถึง PHP-FPM การผ่านเครือข่าย Docker แต่ละจุดบนโฮสต์เดียวกันเพิ่มเวลาหน่วงประมาณ 0.1-0.3ms ซึ่งยอมรับได้สำหรับการตอบกลับ PHP ที่ใช้เวลา 50ms แต่ไม่เหมาะกับปลายทาง API ที่ควรตอบในเวลาต่ำกว่า 5ms กรณีหลังควรใช้กฎเราเตอร์ Traefik แยกเพื่อส่งเส้นทาง API ตรงไปยังคอนเทนเนอร์แอปพลิเคชันโดยข้าม Varnish
ขนาดหน่วยความจำสำคัญกว่าพารามิเตอร์ส่วนใหญ่: Varnish ใช้ RAM เก็บรายการแคช โดยอิมเมจ Docker อย่างเป็นทางการจัดสรร 100MB เป็นค่าเริ่มต้น เว็บไซต์ที่มี URL จำนวนมากจะใช้พื้นที่นี้จนเต็มอย่างรวดเร็ว อัตราการไล่ข้อมูลออกจากแคชเพิ่มขึ้น และอัตราการพบข้อมูลในแคชยังต่ำไม่ว่าจะกำหนด TTL เท่าใด การเพิ่มประสิทธิภาพขั้นแรกหลังนำระบบขึ้นใช้งานจึงควรเพิ่มหน่วยความจำด้วยตัวเลือกเริ่มต้น -s malloc,2G หรือตัวแปรสภาพแวดล้อม VARNISH_SIZE=2G ใน Docker แล้วติดตามผลด้วย varnishstat -1 | grep hit
การยุติการเชื่อมต่อ SSL: TLS จากอินเทอร์เน็ตสาธารณะยุติที่ Cloudflare เมื่อใช้โหมด SSL “Full (strict)” ของ Cloudflare จากนั้น Cloudflare จะเชื่อมต่อกับ Traefik ผ่าน HTTPS โดยใช้ใบรับรอง Let’s Encrypt ของเราเตอร์ที่แสดงไว้ด้านบน หลัง Traefik ช่วง Traefik-to-Varnish และ Varnish-to-Nginx ใช้ HTTP ที่ไม่เข้ารหัสบนเครือข่าย bridge ภายใน Docker โดยตั้งใจ บนโฮสต์เดียวที่คุณควบคุมทุกคอนเทนเนอร์ การเพิ่ม TLS สำหรับการสื่อสารภายในระหว่างคอนเทนเนอร์เพิ่มภาระ CPU แต่ให้ประโยชน์ด้านความปลอดภัยจำกัด เพราะทราฟฟิกของ bridge ไม่ออกนอกโฮสต์ และไม่ช่วยป้องกันกรณีที่คอนเทนเนอร์หรือโฮสต์ถูกเจาะ หากแบบจำลองภัยคุกคามของคุณกำหนดให้เข้ารหัสการสื่อสารระหว่างคอนเทนเนอร์ ต้องพิจารณาสถาปัตยกรรมอีกแบบหนึ่ง
กรณีที่เหมาะกับสถาปัตยกรรมนี้
นี่ไม่ใช่คำแนะนำสำหรับทุกระบบ หากมีแอปพลิเคชันในคอนเทนเนอร์เพียงตัวเดียว ไม่ต้องทำแคช และไม่มีข้อจำกัดจาก CloudPanel การให้ Traefik ส่งคำขอตรงไปยังแอปพลิเคชันย่อมเรียบง่ายและเหมาะสมกว่า สี่ชั้นนี้คุ้มกับความซับซ้อนในกรณีเฉพาะ เช่น ระบบที่ CloudPanel ดูแลและจำเป็นต้องใช้ Nginx เว็บไซต์บน CMS ที่แคชทั้งหน้าช่วยเพิ่มอัตราการรองรับคำขอถึงระดับสิบเท่า หรือระบบที่มีหลายบริการซึ่งใช้ Traefik กำหนดเส้นทางให้คอนเทนเนอร์สิบกว่าตัวอยู่แล้ว การเพิ่ม Varnish ให้บริการหนึ่งจึงเป็นภาระส่วนเพิ่มแทนการสร้างระบบใหม่
การแยกหน้าที่ช่วยให้ดูแลความซับซ้อนนี้ได้ในระยะยาว ค่ากำหนด Traefik อยู่ใน label ของ Docker Compose ตรรกะ Varnish อยู่ในไฟล์ VCL ที่ควบคุมเวอร์ชันร่วมกับชุดระบบ Compose ส่วน CloudPanel ดูแลค่ากำหนด Nginx แต่ละระบบไม่ก้าวล่วงหน้าที่ของอีกระบบ เมื่อเกิดพฤติกรรมผิดคาด เส้นทางวินิจฉัยจึงชัดเจน: ส่วนหัว X-Cache บอกว่า Varnish เป็นผู้ส่งการตอบกลับหรือไม่ บันทึกการเข้าถึงของ Traefik แสดง เราเตอร์ บริการ backend และรหัสสถานะที่ระบบปลายทางส่งกลับมาและที่ส่งไปยังไคลเอนต์ ส่วนบันทึกข้อผิดพลาดของ Nginx แสดงสิ่งที่ PHP-FPM รายงาน แต่ละชั้นมีสัญญาณที่สังเกตได้ของตัวเอง ความล้มเหลวจึงไม่ทบกันข้ามชั้นโดยไม่มีใครเห็น
สิ่งที่ดูซ้ำซ้อนจึงเป็นขอบเขตที่ชัดเจนระหว่างการป้องกันที่ขอบเครือข่าย การกำหนดเส้นทาง การทำแคช และการให้บริการแอปพลิเคชัน ทั้งสี่ส่วนขยายขนาดและเกิดความล้มเหลวแยกกันได้ ขอบเขตนี้คุ้มกับค่ากำหนดที่ต้องดูแลเพิ่มขึ้น
บทความที่เกี่ยวข้อง
- นำแอปพลิเคชัน React ขึ้นใช้งานจริง: คู่มือตั้งค่า Docker กับ Traefik reverse proxy ฉบับสมบูรณ์
- คู่มือแก้ปัญหา n8n ที่ติดตั้งและดูแลเองฉบับสมบูรณ์ปี 2025: ขนาดข้อมูลการรันและปัญหา webhook กับ Traefik
- สร้างชุดระบบพัฒนา Docker สำหรับผู้เช่าหลายราย: คู่มือตั้งค่าระบบลูกค้าที่ขยายขนาดได้
- เพิ่มประสิทธิภาพ CloudPanel: ปรับเซิร์ฟเวอร์ Hetzner Cloud ให้ส่งหน้าเว็บไซต์ได้รวดเร็ว