使用 Varnish 為WordPress網站加速

由於Wordpress通常用來建構以內容為主的網站,當網站流量變高時,如何加速自然是個重要的課題。加速的方法不少,跟Wordpress的執行環境息息相關。本文說明如何在VPS或者dedicate環境安裝Varnish來加速。

Varnish 是個網站加速軟體,透過將常用的檔案內容快取,減低網頁服務器、PHP以及資料庫存取的次數,以達到加速的目的。除了作為網站加速外,Varnish本身提供基本的覆載均衡(load balancing)的功能。

 前提假設

作業系統:Ubuntu 10.04 64-bit
Varnish: 2.1.0-2ubuntu0.1

目標

  • 使用Varnish來達到網站加速以及高可用性的目的。
  • 當網站伺服器不再回應,Varnish就不應該再將用戶請求導到那個故障的伺服器。
  • 對於登入的會員不使用快取的頁面,因為可能需要顯示個性化的內容。

安裝Varnish

使用Ubuntu的套件庫,Varnish的安裝相當容易,執行下列指令即可:

# apt-get -y install varnish

設定Varnish

編輯 /etc/default/varnish, 假設我們希望客製化的Varnish設定檔位置為/srv/www/webhost/varnish/wordpress.vcl。

# Configuration file for varnish
#
# /etc/init.d/varnish expects the variables $DAEMON_OPTS, $NFILES and $MEMLOCK
# to be set from this shell script fragment.
#

# Maximum number of open files (for ulimit -n)
NFILES=8192

# Maximum locked memory size (for ulimit -l)
# Used for locking the shared memory log in memory.  If you increase log size,
# you need to increase this number as well
MEMLOCK=82000

# Default varnish instance name is the local nodename.  Can be overridden with
# the -n switch, to have more instances on a single server.
INSTANCE=$(uname -n)

#
# See varnishd(1) for more information.
#
# # Main configuration file. You probably want to change it :)
VARNISH_VCL_CONF=/srv/www/webhost/etc/varnish/wordpress.vcl
#
# # Default address and port to bind to
# # Blank address means all IPv4 and IPv6 interfaces, otherwise specify
# # a host name, an IPv4 dotted quad, or an IPv6 address in brackets.
VARNISH_LISTEN_ADDRESS=
VARNISH_LISTEN_PORT=80
#
# # Telnet admin interface listen address and port
VARNISH_ADMIN_LISTEN_ADDRESS=127.0.0.1
VARNISH_ADMIN_LISTEN_PORT=6082
#
# # The minimum number of worker threads to start
VARNISH_MIN_THREADS=4
#
# # The Maximum number of worker threads to start
VARNISH_MAX_THREADS=1000
#
# # Idle timeout for worker threads
VARNISH_THREAD_TIMEOUT=120
#
# # Cache file location
VARNISH_STORAGE_FILE=/var/lib/varnish/$INSTANCE/varnish_storage.bin
#
# # Cache file size: in bytes, optionally using k / M / G / T suffix,
# # or in percentage of available disk space using the % suffix.
 VARNISH_STORAGE_SIZE=100M
#
# # File containing administration secret
 VARNISH_SECRET_FILE=/etc/varnish/secret
#
# # Backend storage specification
 VARNISH_STORAGE="file,${VARNISH_STORAGE_FILE},${VARNISH_STORAGE_SIZE}"
#
# # Default TTL used when the backend does not specify one
 VARNISH_TTL=120
#
# # DAEMON_OPTS is used by the init script.  If you add or remove options, make
# # sure you update this section, too.
 DAEMON_OPTS="-a ${VARNISH_LISTEN_ADDRESS}:${VARNISH_LISTEN_PORT} \
              -f ${VARNISH_VCL_CONF} \
              -T ${VARNISH_ADMIN_LISTEN_ADDRESS}:${VARNISH_ADMIN_LISTEN_PORT} \
              -t ${VARNISH_TTL} \
              -w ${VARNISH_MIN_THREADS},${VARNISH_MAX_THREADS},${VARNISH_THREAD_TIMEOUT} \
              -S ${VARNISH_SECRET_FILE} \
              -s ${VARNISH_STORAGE}"

## Alternative 4, Do It Yourself
#
# DAEMON_OPTS=""

編輯Varnish設定檔(wordpress.vcl)

編輯/srv/www/webhost/varnish/wordpress.vcl檔案,下面為其參考內容。

// Defining our backends
backend node1 {
  .host = "node1.local";
  .port = "8080";
  .probe = {
                .url = "/status.html";
                .interval = 5s;
                .timeout = 1 s;
                .window = 5;
                .threshold = 3;
  }
  .connect_timeout = 600s;
  .first_byte_timeout = 600s;
  .between_bytes_timeout = 600s;
}

backend node2 {
  .host = "node2.local";
  .port = "8080";
  .probe = {
                .url = "/status.html";
                .interval = 5s;
                .timeout = 1 s;
                .window = 5;
                .threshold = 3;
  }
  .connect_timeout = 600s;
  .first_byte_timeout = 600s;
  .between_bytes_timeout = 600s;
}

// Defining our cluster including end points for purge
director cluster round-robin {
  {.backend = node1;}
  {.backend = node2;}
}

acl purge {
        "localhost";
        "node1";
        "node2";
}

sub vcl_recv {

        ## determine real client IP
        if (req.http.cf-connecting-ip) {
            set req.http.X-Forwarded-For = req.http.cf-connecting-ip;
        } else if (req.http.x-forwarded-for) {
           set req.http.X-Forwarded-For = req.http.X-Forwarded-For  ", "  client.ip;
        } else {
           set req.http.X-Forwarded-For = client.ip;
        }

        ## set the backend to the cluster
        set req.backend = cluster;

        ## check for a mobile device
        call device_detection;

        ## enable grace period.
        set req.grace = 30s;

        if (req.request == "PURGE") {
                if (!client.ip ~ purge) {
                        error 405 "Not allowed.";
                }
                purge("req.url == " req.url " && req.http.host == " req.http.host);
                error 200 "Purged.";
        }

        ## normalize Accept-Encoding header.
        if (req.http.Accept-Encoding) {
            if (req.url ~ "\.(jpg|png|gif|gz|tgz|bz2|tbz|mp3|ogg)$") {
                # No point in compressing these
                remove req.http.Accept-Encoding;
            } elsif (req.http.Accept-Encoding ~ "gzip") {
                set req.http.Accept-Encoding = "gzip";
            } elsif (req.http.Accept-Encoding ~ "deflate") {
                set req.http.Accept-Encoding = "deflate";
            } else {
                # unkown algorithm
                remove req.http.Accept-Encoding;
            }
        }

        ## remove cookies for static resources.
        if (req.url ~ "^/[^?]+\.(jpeg|jpg|png|gif|ico|js|css|txt|gz|zip|lzma|bz2|tgz|tbz|html|htm)(\?.*|)$") {
                unset req.http.cookie;
                set req.url = regsub(req.url, "\?.*$", "");
                return(lookup);
        }

        ## rename wordpress_test_cookie to something else to simplfy cookie removal afterwards.
        if (req.http.Cookie && req.http.Cookie ~ "wordpress_") {
                set req.http.Cookie = regsuball(req.http.Cookie, "wordpress_test_cookie=", "; wpjunk=");
        }

        ### do not cache these files:
        ##never cache the admin pages, or the server-status page
        if (req.request == "GET" && (req.url ~ "(wp-admin|bb-admin|server-status)"))
        {
                return(pipe);
        }

        ##dont cache ajax requests
        if(req.http.X-Requested-With == "XMLHttpRequest" || req.url ~ "nocache" || req.url ~ "(control.php|wp-comments-post.php|wp-login.php|bb-login.php|bb-reset-password.php|register.php)")
        {
                return (pass);
        }

        ### don't cache authenticated sessions
        if (req.http.Cookie && req.http.Cookie ~ "(wordpress_|PHPSESSID)") {
                return(pass);
        }

        ## Cache any dynamic content
        if (req.url !~ "wp-(login|admin|signup)" && req.url !~ "preview" || req.url ~ "admin-ajax.php"){
                ## "Request is not for login, admin, preview, sign up or admin-ajax so don't cache it";
                if (req.http.Cookie !~ "wordpress_logged_in "){
                        //log "User is not logged in";
                        if (req.http.Cookie !~ "wp-postpass"){
                            //log "Post is not password protected";
                            unset req.http.cookie;
                            return(lookup);
                        }
                }
        }

}

sub vcl_hit {
        if (req.request == "PURGE") {
                set obj.ttl = 0s;
                error 200 "Purged.";
        }
}

sub vcl_miss {
        if (req.request == "PURGE") {
                error 404 "Not in cache.";
        }
        if (!(req.url ~ "wp-(login|admin)")) {
                unset req.http.cookie;
        }
        if (req.url ~ "^/[^?]+.(jpeg|jpg|png|gif|ico|js|css|txt|gz|zip|lzma|bz2|tgz|tbz|html|htm)(\?.|)$") {
                unset req.http.cookie;
                set req.url = regsub(req.url, "\?.*$", "");
        }
        if (req.url ~ "^/$") {
                unset req.http.cookie;
        }
}

sub vcl_fetch {
        // When fetching images we can set a long caching marker that we can access later
        if (req.request == "GET" && req.url ~ "\.(jpg|jpeg|gif|ico|css|js|png)$") {
           set beresp.http.magicmarker = "1";
        }
        // Don't cache mobile requests
        if (req.http.X-Device == "mobile"){
            set beresp.ttl = 0s;
            //log "Not caching mobile requests";
        }
        // Don't cache error pages
        if (beresp.status == 404 || beresp.status == 503 || beresp.status >= 500){
            set beresp.ttl = 0s;
        }

        ## enable grace period.
        set req.grace = 30s;

        // Some debug code for why objects are/aren't cachable
        // Varnish determined the object was not cacheable
        if (!beresp.cacheable) {
            set beresp.http.X-Cacheable = "NO:Not Cacheable";

        // You don't wish to cache content for logged in users
        } elsif (req.http.Cookie ~ "(UserID|_session)") {
            set beresp.http.X-Cacheable = "NO:Got Session";
            //log "It appears a session is in process so we have returned pass";
            return(pass);

        // You are respecting the Cache-Control=private header from the backend
        } elsif (beresp.http.Cache-Control ~ "private") {
            set beresp.http.X-Cacheable = "NO:Cache-Control=private";
            //log "It appears this is private so we have returned pass";
            return(pass);

        // You are extending the lifetime of the object artificially
        } elsif (beresp.ttl < 1s) {
            set beresp.ttl   = 5s;
            set beresp.grace = 5s;
            set beresp.http.X-Cacheable = "YES:FORCED";
        // Varnish determined the object was cacheable
        } else {
            set beresp.http.X-Cacheable = "YES";
        }
}

sub vcl_hash {
    # Each cached page has to be identified by a key that unlocks it.
    # Add the browser cookie only if a WordPress cookie found.
    if ( req.http.Cookie ~"(wp-postpass|wordpress_logged_in|comment_author_)" ) {
        set req.hash += req.http.Cookie;
    }
}

## Deliver
sub vcl_deliver {
        ## set header for indicating hit or miss.
        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";
        }

        ## We'll be hiding some headers added by Varnish. We want to make sure people are not seeing we're using Varnish.
        ## Since we're not caching (yet), why bother telling people we use it?
        remove resp.http.X-Varnish;
        remove resp.http.Via;
        remove resp.http.Age;
        remove resp.http.X-Pingback;

        ## We'd like to hide the X-Powered-By headers. Nobody has to know we can run PHP and have version xyz of it.
        remove resp.http.X-Powered-By;

        ## fake the server signature
        set resp.http.Server = "i-me.tw/1.0";

        if (resp.http.magicmarker) {
                //log "Magicmarker set so setting our own client side caching";
                unset resp.http.magicmarker;
                set resp.http.Cache-Control = "max-age=648000";
                set resp.http.Expires = "Thu, 01 May 2014 00:10:22 GMT";
                set resp.http.Last-Modified = "Mon, 25 Apr 2011 01:00:00 GMT";
                set resp.http.Age = "647";
        }
}

sub device_detection {
  // Default to thinking it's a PC
  set req.http.X-Device = "pc";

  // Add all possible agent strings - These are the most popular agent strings
  //log "Checking for mobile device";
  if (req.http.User-Agent ~ "iP(hone|od)" || req.http.User-Agent ~ "Android" || req.http.User-Agent ~ "Symbian" || req.http.User-Agent ~ "^BlackBerry" || req.http.User-Agent ~ "^SonyEricsson"
    || req.http.User-Agent ~ "^Nokia" || req.http.User-Agent ~ "^SAMSUNG" || req.http.User-Agent ~ "^LG" || req.http.User-Agent ~ "webOS") {
    //log "Mobile device detected";
    //log req.http.cookie;
    //log "Following req.url";
    //log req.url;
    if (req.url !~ "wptouch_view=normal"){
      //log "wptouch_switch_toggle is not set";
      set req.http.X-Device = "mobile";
    }
    else{
      //log "this should not be redirecting to mobile";
      //log "wp touch view is set to normal so we shouldn't be setting a device type other thna PC";
      set req.http.X-Device = "pc";
      error 750 req.http.host;
    }
  }

  // These are some more obscure agent strings
  if (req.http.User-Agent ~ "^PalmSource"){
    set req.http.X-Device = "mobile";
  }
}

結論

筆者不保證本文提供的Varnish設定正確或已經是最佳化,讀者要根據自己需要自行調整。

參考來源

, ,

尚未有迴響。

發表迴響