tva
← Insights

การซ้อน Reverse Proxy: สถาปัตยกรรมสำหรับโปรดักชัน

การรัน reverse proxy สี่ตัวหน้า application เดียว ดูผิวเผินแล้วเหมือนระบบที่ต้องการการทำให้เรียบง่ายขึ้น บทช่วยสอนส่วนใหญ่ที่ครอบคลุมโครงสร้างพื้นฐานเว็บสำหรับโปรดักชันมักจะจบที่ชั้นเดียว — Nginx, Caddy หรือ Traefik — และถือว่าสิ่งที่มากกว่านั้นคือการ over-engineering แต่ในความเป็นจริง แต่ละเครื่องมือแก้ปัญหาที่แตกต่างกัน และเมื่อคุณเข้าใจการแบ่งงานแล้ว ความซ้ำซ้อนที่เห็นก็กลายเป็นสิ่งที่สมเหตุสมผล

นี่คือสถาปัตยกรรมที่เรารันสำหรับ client deployment หลายราย: Cloudflare ที่ edge, Traefik เป็น container-aware entry point, Varnish สำหรับ HTTP caching, Nginx เป็น application host และตัว application เองที่ปลายสาย ขั้นตอนการรับ request คือ:

Cloudflare → Traefik → Varnish → Nginx → Application

ต่อไปนี้จะอธิบายสิ่งที่แต่ละชั้นมีส่วนร่วม ขอบเขตที่ตั้งอยู่ และผลกระทบต่อประสิทธิภาพ รวมถึงจุดที่ stack นี้สร้าง overhead ที่การตั้งค่าที่เรียบง่ายกว่าจะไม่มี

สิ่งที่แต่ละชั้นทำจริง ๆ

Cloudflare ทำงานที่ edge ของอินเทอร์เน็ตสาธารณะ มันดูดซับ traffic DDoS, ใช้กฎ WAF, ยุติ TLS ที่ PoP ที่กระจายทั่วโลก และ cache static assets ใกล้กับผู้ใช้ปลายทาง สิ่งที่มันทำได้ไม่ดีคือการตัดสินใจ routing อัจฉริยะเกี่ยวกับ container ของคุณ หรือ cache response ของ application ที่ต้องการการตรวจสอบ cookie นั่นเป็นงานของคนอื่น

Traefik คือ container-aware entry point ภายในโครงสร้างพื้นฐานของคุณ เมื่อคุณรัน multi-tenant Docker stack คุณค่าของ Traefik คือมันอ่าน container labels และ route ตามนั้น — ไม่มีไฟล์ configuration ที่ต้องดูแลด้วยตนเองซึ่งอาจไม่ตรงกันเมื่อ container รีสตาร์หรือ scale มันจัดการ TLS certificates จาก Let's Encrypt สำหรับ domain ภายในหรือ staging, แทรกหรือเขียน headers ใหม่ และบังคับใช้ middleware chain เช่น IP allowlisting หรือ basic auth Traefik ไม่ cache ไม่บีบอัดได้ดีภายใต้ load งานของมันคือ routing และ middleware และมันทำงานนั้นได้อย่างมีประสิทธิภาพ

Varnish คือ HTTP accelerator มันอยู่ระหว่าง entry point และ application host โดยเฉพาะเพื่อดูดซับ read traffic อินสแตนซ์ Varnish ที่ tuned ดีสามารถให้บริการหลายหมื่น request ต่อวินาทีจาก memory โดย return หน้า HTML เต็มในเวลาน้อยกว่าหนึ่งมิลลิวินาที VCL (Varnish Configuration Language) ให้คุณควบคุมอย่างละเอียดว่าอะไรจะถูก cache นานแค่ไหน และภายใต้เงื่อนไขใด — ละเอียดกว่ากฎ cache ของ Cloudflare มาก สิ่งนี้มีความสำคัญสำหรับ application ที่มีการ authenticate ที่ response บางส่วน cache ได้ต่อ user หรือสำหรับหน้าที่ควรมี TTL ที่แตกต่างกันขึ้นอยู่กับ query parameters

Nginx ในชุดนี้ทำหน้าที่ที่ถูกออกแบบมาตั้งแต่ต้น: ให้บริการไฟล์และ proxy ไปยัง upstream application process CloudPanel ใช้ Nginx เป็น web server ที่จัดการ ซึ่งหมายความว่าแต่ละ vhost มี configuration ที่สร้างอัตโนมัติจัดการ PHP-FPM, การให้บริการ static asset และการบีบอัด gzip Configuration นั้นดูแลโดย CloudPanel และไม่ควรแก้ไขโดยตรง Nginx ในที่นี้ไม่ใช่ชั้นตัดสินใจ — มันรับ request, proxy ไปยัง application และ return response

การกำหนดค่า Traefik: Container Labels

การ routing ของ Traefik สำหรับ stack นี้กำหนดค่าผ่าน Docker Compose labels ทั้งหมด ข้อมูลเชิงลึกสำคัญเมื่อวาง Varnish ไว้หลัง Traefik คือ Traefik ควร forward ไปยัง Varnish container ไม่ใช่ไปยัง Nginx โดยตรง นี่คือจุดที่คนส่วนใหญ่สับสน: ตัวอย่าง Traefik มาตรฐาน แสดง pattern ตรงไปยัง app และการนำ Varnish เข้าไปใน flow นั้นไม่ได้รับการบันทึกไว้ดีนัก

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

Header X-Forwarded-Proto มีความสำคัญมาก หากไม่มี application ของคุณจะเห็น request แต่ละตัวเป็น HTTP — เพราะการสื่อสาร Varnish-to-Nginx ทำงานบน plain HTTP บน Docker internal network — ซึ่งทำให้เกิด redirect loop ใน application ใด ๆ ที่บังคับใช้ HTTPS Traefik ประทับ header ก่อนที่ request จะเข้าสู่ internal network, Varnish ส่งผ่านโดยไม่เปลี่ยนแปลง และ Nginx forward ไปยัง application Header X-Forwarded-Port ป้องกัน redirect loop ประเภทเดียวกันใน application ที่ตรวจสอบ port ด้วย

ข้อพิจารณาที่สอง: การตั้งค่า passHostHeader ของ Traefik ค่าเริ่มต้นเป็น true ซึ่งหมายความว่า Varnish ได้รับ header Host ดั้งเดิม นี่คือสิ่งที่คุณต้องการ — VCL ของ Varnish สามารถใช้มันสำหรับ cache keying และ Nginx ใช้มันสำหรับการเลือก vhost ถ้าคุณ override สิ่งนี้ ทั้ง Varnish และ Nginx จะได้รับ host ที่ผิดและล้มเหลวอย่างเงียบ ๆ ในลักษณะที่ยากต่อการตรวจสอบ

Varnish VCL: Cache Logic

VCL ด้านล่างเป็นจุดเริ่มต้นที่ใช้งานได้สำหรับ WordPress หรือ PHP application หลัง CloudPanel มันครอบคลุมกรณีทั่วไป: bypass สำหรับผู้ใช้ที่ login และ request ที่ไม่ใช่ idempotent, การ cache ขยายสำหรับ static assets, การ normalize tracking parameters และช่วง grace เพื่อป้องกัน cache stampede

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);
}

การ normalize tracking parameters ใน vcl_recv มักถูกมองข้ามและมีค่าใช้จ่ายหาก neglect Cloudflare กรอง parameter บางส่วนที่ edge แต่ค่า UTM และ token fbclid มาถึง Varnish พร้อม URL ดั้งเดิม หากไม่ normalize การคลิก Facebook ad แต่ละครั้งจะสร้าง cache entry ที่ไม่ซ้ำกันสำหรับสิ่งที่จาก perspective ของ application คือหน้าเดียวกัน ในช่วงแคมเปญ สิ่งนี้จะแตก Varnish cache เป็น entry เย็นนับพันตัวและกำจัดประโยชน์ของ hit ratio ทั้งหมด

การตั้งค่า grace ใน vcl_backend_response เป็นกลไกอีกตัวที่ควรทำความเข้าใจอย่างละเอียด เมื่อ TTL ของ object ที่ถูก cache หมดอายุ request ถัดไปปกติจะรอให้ Nginx return response ใหม่ก่อนที่ Varnish จะ deliver อะไร ภายใต้ traffic spike — การส่ง newsletter, การกล่าวถึงในโซเชียลมีเดีย — request หลายตัวมาถึงพร้อมกันสำหรับ entry ที่หมดอายุ หากไม่มี grace แต่ละ request จะผ่านไปยัง Nginx พร้อมกัน ด้วย grace period 60 วินาที Varnish ให้บริการ object ที่ stale กับ request ทั้งหมดยกเว้นหนึ่งในขณะที่ background fetch เดียว revalidate cache ผลลัพธ์คือ Nginx จัดการ request หนึ่งตัวแทนที่จะเป็นหลายร้อยตัวในขณะที่หมดอายุ

ผลกระทบต่อประสิทธิภาพ

เรื่องของประสิทธิภาพสำหรับ stack นี้ไม่สม่ำเสมอ มีจุดที่มันเก่งและมีจุดที่มันเพิ่ม overhead ที่วัดได้

จุดที่ทำงานได้ดี: หน้าที่ cache ได้ใด ๆ ที่ให้บริการจาก memory ของ Varnish จะ return ในเวลาน้อยกว่า 2ms ที่ระดับ Traefik หน้าแรก WordPress ที่ใช้เวลา 400ms ในการสร้างจาก PHP จะให้บริการใน 1-2ms สำหรับ request ถัดไปทุกตัวจนกว่า TTL จะหมด การปรับปรุง throughput ไม่ใช่เพิ่มขึ้นเล็กน้อย — มันคือระดับ order of magnitude การให้บริการ static asset ผ่าน Nginx นั้นเร็วไม่ว่ากรณีใด และ Cloudflare cache asset เหล่านั้นที่ edge ไม่ว่าอย่างไรก็ตาม ดังนั้น Nginx แทบไม่ต้องจัดการกับมันในโปรดักชัน

จุดที่เพิ่ม overhead: cache miss ตอนนี้ต้องผ่าน hop สี่ตัวแทนที่จะเป็นหนึ่ง request PHP ที่ไม่สามารถ cache ได้ — session ผู้ใช้ที่ login, การส่ง POST, หน้า cart ของ WooCommerce — ผ่าน Traefik, Varnish และ Nginx ก่อนถึง PHP-FPM แต่ละ Docker network hop บนโฮสต์เดียวกันเพิ่ม latency ประมาณ 0.1-0.3ms สำหรับ PHP response 50ms นี้ยอมรับได้ สำหรับ API endpoint ที่ควร respond ในเวลาน้อยกว่า 5ms นี้ไม่ใช่ ในกรณีนั้น วิธีที่ถูกต้องคือ route path ของ API ตรงไปยัง application container ผ่าน Traefik router rule แยกต่างหาก โดย bypass Varnish ทั้งหมด

การกำหนดขนาด memory มีความสำคัญมากกว่า parameter อื่นส่วนใหญ่: Varnish จัดสรร object store จาก RAM configuration ค่าเริ่มต้นใช้ 256MB สำหรับ site ที่มี URL ที่ไม่ซ้ำกันจำนวนมาก จะเต็มอย่างรวดเร็ว อัตรา eviction จะสูงขึ้น และ cache hit ratio ยังคงต่ำไม่ว่า TTL จะตั้งเป็นอะไร lever ประสิทธิภาพแรกที่ต้องดึงหลังการ deploy คือการเพิ่ม allocation — flag -s malloc,2G ตอนเริ่มต้น หรือ environment variable VARNISH_SIZE=2G ใน Docker — และตรวจสอบผลด้วย varnishstat -1 | grep hit

การยุติ SSL: TLS ยุติที่ Cloudflare ในการตั้งค่าที่ front ด้วย Cloudflare, ส่วน Traefik-to-Varnish และ Varnish-to-Nginx ทำงานบน plain HTTP บน Docker internal bridge network นี่เป็นสิ่งที่ตั้งใจ การเพิ่ม TLS ให้การสื่อสาร inter-container บนโฮสต์เดียวกันนำ CPU overhead โดยไม่มีการปรับปรุงความปลอดภัยที่มีความหมาย — threat model สำหรับ Docker bridge traffic นั้นแตกต่างอย่างมากจาก traffic อินเทอร์เน็ตสาธารณะ

จุดที่ Stack นี้เหมาะสม

นี่ไม่ใช่คำแนะนำสำหรับทุกคน สำหรับ application แบบ containerized เดียวที่ไม่มีความต้องการ caching และไม่มีข้อจำกัด CloudPanel, Traefik routing ตรงไปยัง app นั้นเรียบง่ายและถูกต้อง stack สี่ชั้นนี้ได้รับความซับซ้อนในสถานการณ์เฉพาะ: สภาพแวดล้อมที่จัดการโดย CloudPanel ที่ Nginx ไม่สามารถต่อรองได้, site ที่ขับเคลื่อนด้วย CMS ที่การ cache แบบ full-page ให้การปรับปรุง throughput ระดับ order of magnitude หรือ multi-service deployment ที่ Traefik เป็น routing layer สำหรับ container สิบกว่าตัวอยู่แล้ว และการเพิ่ม Varnish สำหรับหนึ่งในนั้นเป็นต้นทุนที่เพิ่มขึ้น ไม่ใช่ระบบใหม่

การแยกหน้าที่คือสิ่งที่ทำให้ความซับซ้อนจัดการได้ตลอดเวลา การกำหนดค่า Traefik อยู่ใน Docker Compose labels Logic ของ Varnish อยู่ในไฟล์ VCL ที่ควบคุมด้วย version ร่วมกับ compose stack การกำหนดค่า Nginx เป็นของ CloudPanel ไม่มีระบบใดเข้าไปใน domain ของอีกระบบ เมื่อบางอย่างทำงานผิดปกติ เส้นทางการวินิจฉัยชัดเจน: header X-Cache บอกว่า Varnish ให้บริการ response หรือไม่; access log ของ Traefik บันทึก middleware ที่รันและ upstream ได้รับอะไร; error log ของ Nginx บันทึกสิ่งที่ PHP-FPM รายงาน แต่ละชั้นสร้าง signal ที่สังเกตได้ของตัวเอง และความล้มเหลวไม่รวมกันอย่างเงียบ ๆ ข้ามชั้น

สิ่งที่ดูเหมือนซ้ำซ้อนคือขอบเขตที่ชัดเจนระหว่าง routing, caching, การให้บริการ application และ compute — สี่ concerns ที่ scale อิสระและล้มเหลวอิสระ ขอบเขตนั้นคุ้มค่ากับ configuration surface ที่เพิ่มขึ้น

บทความที่เกี่ยวข้อง

บทความที่เกี่ยวข้อง