Digitalagentur für Business Software aus Kiel

Symfony 2 mit Varnish 4 // Teil 1 - Einrichten

Veröffentlicht von Felix Baltruschat am 11.07.2016

Felix Baltruschat

Wer viel Traffic auf seiner Webseite hat und vernünftige Response-Zeiten auf den Requests haben will, kommt nicht um eine erweiterte Cachelösung drumherum. Varnish ist dafür perfekt geeignet und agiert als Caching-Proxy vor dem eigentlichen Webserver.

Beim Aufruf einer Seite wird geguckt, ob die Seite im Cache vorhanden ist und wird bei einem Hit direkt vom Varnish ausgeliefert. Die Webseiten bleiben so lange im Cache liegen bis das mitgelieferte Verfallsdatum abgelaufen ist oder der Varnish neugestartet wurde. Wenn in der Zwischenzeit sich Daten auf der Seite verändert haben, werden diese erst nach dem Verfallsdatum oder nach einer Invalidierung der Seite im Cache angezeigt.

Für das invalidieren (Purge) der Cache-Seiten eignet sich das FriendsOfSymfony HttpCacheBundle, da es Varnish unterstützt und die Invalidierung sich mit Tags und Routen umsetzen lässt.
Wir wollen in diesem Beispiel eine einfache Lösung mit Cache-Tags zeigen.

Als erstes installieren wir Varnish auf dem Server; in unserem Beispiel auf einem Mac mit OS X

brew install varnish

Danach müssen wir die Varnish-Config an das Symfony-System anpassen. Die Config liegt Standardmässig hier: /usr/local/etc/varnish/default.vcl

vcl 4.0;

import std;
import directors;

# Default backend definition. Set this to point to your content server.
backend apache_server {
  .host = "127.0.0.1";
  .port = "8080";
}

acl purge_ip {
    "localhost";
    "127.0.0.1";
}

sub vcl_init{
    new ws = directors.random();
    ws.add_backend(apache_server, 1.0);
}


sub vcl_recv {

    set req.backend_hint = ws.backend();

    # Check correct header
    if (req.method != "GET" &&
        req.method != "HEAD" &&
        req.method != "PUT" &&
        req.method != "POST" &&
        req.method != "TRACE" &&
        req.method != "OPTIONS" &&
        req.method != "PURGE" &&
        req.method != "BAN" &&
        req.method != "DELETE") {
        /* Non-RFC2616 or CONNECT which is weird. */
        return (pipe);
    }

    if (req.restarts == 0) {
      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;
      }
    }

    if (req.http.Cookie) {
        # Some generic cookie manipulation, useful for all templates that follow
        # Remove the "has_js" cooki
        set req.http.Cookie = ";" + req.http.Cookie;
        set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
        set req.http.Cookie = regsuball(req.http.Cookie, ";(PHPSESSID)=", "; \1=");
        set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");

        # Remove any Google Analytics based cookies
        set req.http.Cookie = regsuball(req.http.Cookie, "__utm.=[^;]+(; )?", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "_ga=[^;]+(; )?", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "_gat=[^;]+(; )?", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "utmctr=[^;]+(; )?", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "utmcmd.=[^;]+(; )?", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "utmccn.=[^;]+(; )?", "");

        # Remove DoubleClick offensive cookies
        set req.http.Cookie = regsuball(req.http.Cookie, "__gads=[^;]+(; )?", "");


        if (req.http.Cookie == "") {
            // If there are no more cookies, remove the header to get page cached.
            unset req.http.Cookie;
        }
    }


    # Allow purging
    if (req.method == "PURGE") {
         if (!client.ip ~ purge_ip) {
             #return(synth(405, "Not Found"));
             return(synth(403, "Not allowed"));
         }
         return (purge);
    }

    # Allow banning
    if (req.method == "BAN") {
        if (!client.ip ~ purge_ip) {
            return (synth(405, "Not allowed"));
        }

        if (req.http.X-Cache-Tags) {
            ban("obj.http.X-Host ~ " + req.http.X-Host
                + " && obj.http.X-Url ~ " + req.http.X-Url
                + " && obj.http.content-type ~ " + req.http.X-Content-Type
                + " && obj.http.X-Cache-Tags ~ " + req.http.X-Cache-Tags
            );
        } else {
            ban("obj.http.X-Host ~ " + req.http.X-Host
                + " && obj.http.X-Url ~ " + req.http.X-Url
                + " && obj.http.content-type ~ " + req.http.X-Content-Type
            );
        }

        return (synth(200, "Banned"));
    }

    # Don't cache POST request
    if (req.method ~ "POST") {
        return (pass);
    }

    # Don't cache SonataAdmin admin area
    if (req.url ~ "(^/app.php|^/app_dev.php|^)/admin" || req.url ~ "(^/app.php|^/app_dev.php|^)/(([a-z]{2})/admin)") {
        return (pass);
    }

    # Don't cache FOS UserBundle security areas
    if (req.url ~ "(^/app.php|^/app_dev.php|^)/(([a-z]{2}/|)(login|logout|login_check).*)") {
        return (pass);
    }

    # Only cache GET or HEAD requests.
    if (req.method != "GET" && req.method != "HEAD") {
        return (pipe);
    }

    return (hash);
}


sub vcl_hash {
    hash_data(req.url);
    return (lookup);
}

sub vcl_backend_response {
    set beresp.http.X-Url = bereq.url;
    set beresp.http.X-Host = bereq.http.host;
    return (deliver);
}

sub vcl_deliver {
    # Add dev response, if X-Cache-Debug header exists
    if (!resp.http.X-Cache-Debug) {
        unset resp.http.X-Url;
        unset resp.http.X-Host;
        unset resp.http.X-Cache-Tags;
    }else{
      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";
      }
    }

}

sub vcl_synth {
    set resp.http.Content-Type = "text/html; charset=utf-8";
    set resp.http.Retry-After = "5";
    synthetic ("Error");
    return (deliver);
}
/usr/local/etc/varnish/default.vcl

Jetzt noch schnell Varnish starten.

$ sudo /usr/local/sbin/varnishd -f /usr/local/etc/varnish/default.vcl -a 127.0.0.1:80

Wir haben den Varnish auf den Port 80 gelegt und der Backend-Response soll vom Port 8080 kommen. Dafür muss noch der Apache eingerichtet werden.

<VirtualHost *:80>

  # ändern in
  
<VirtualHost *:8080>

Symfony mit dem FOS HttpCacheBundle erweitert werden.

$ php composer require friendsofsymfony/http-cache-bundle

Nachdem Composer die Bundle-Dateien installiert hat, muss das Bundle noch im AppKernel registriert werden.

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            ...
            new FOS\HttpCacheBundle\FOSHttpCacheBundle(),
        );
    }
}
app/AppKernel.php

Das HttpCacheBundle braucht noch ein paar zusätzliche Configurations-Parameter, damit es mit dem Varnish kommuniziert und wir Tags setzten können.

fos_http_cache:
    tags:
        enabled: true
    proxy_client:
        default: varnish
        varnish:
            servers: 127.0.0.1:80
            base_url: yourwebsite.com
app/config.yml

Wir haben Varnish jetzt installiert, den Apache für einen Reverse-Proxy konfiguriert und Symfony mit dem CacheBundle erweitert. Im zweiten Teil dieser Serie zeigen wir eine einfache Purge-Logik.

© 2020, Braune Digital GmbH, Niemannsweg 69, 24105 Kiel, +49 (0) 431 55 68 63 59