Acest articol face parte dintr-o serie de 3 articole menite să ajute magazinele online să treacă cu bine peste Black Friday.

Black Friday este deja un eveniment de amploare în România. Nu o spunem doar noi cei din industrie ci se vede în toată presa. Dacă cei din marketing și vânzări au motive de bucurie, oamenii din departementele tehnice sunt cu siguranță stresați de eveniment. Noi am fost.

Anul acesta serverele Avanticart au mers aproape perfect 1, fără downtime și cu timpi de răspuns foarte buni (în general sub 500 milisecunde). NU am inventat nimic nou. Doar am citit mult, am experimentat și am pus cap la cap diverse tehnologii.

Așa cum pe noi ne-a ajutat ce au scris alții, vrem să dăm înapoi ceva. Credem că doar împărțind ceea ce am învățat putem ajunge cu toții mai departe. Prin acest articol, vrem să ajutăm dezvoltatorii și comunitatea de eCommerce din România să treacă cu bine peste Vinerile Negre ce vor urma.

Bine, lăsați poveștile și spuneți ce ați făcut

Pe scurt, au fost 3 lucruri care au dus la bunul mers al lucrurilor:

  1. Varnish - un cache puternic
  2. CDN pentru imagini și css/javascript
  3. împărțirea request-urilor pe mai multe servere

Dacă ești un proprietar de magazin la început de drum, te rugăm nu-ți stresa programatorul cu toate cele de mai sus. Nu toate 3 sunt accesibile oricărui magazin. Probabil e de ajuns punctul 2.

Ok, să le luăm pe fiecare în parte. Începem cu Varnish pentru că e probabil cel care a „salvat” situația la Avanticart. Pentru CDN și folosirea mai multor servere vom reveni cu articole în curând.

Varnish

Spre deosebire de un blog sau site „normal”, e mult mai greu să faci cache la un magazin online. De ce? Două motive principale:

  • pentru că nu poți să faci cache la coșul de cumpărături. Ne referim la bucata asta din pagină: coș cumpărături
  • pentru că stocurile trebuie să funcționeze în timp real

Probabil că platforma ta are deja un modul de cache inclus implicit (cache-ul se poate face pe mai multe nivele) însă îți garantăm că nimic nu bate Varnish. Așa că te sfătuim: caută un plugin de varnish pentru platforma ta. Chiar dacă costă (plugin-ul în sine + implementarea) o să vezi că merită. Merită nu doar de Black Friday. De ce n-ai vrea să ai timpi de răspuns de 100ms tot timpul anului?

De ce e Varnish așa de tare?

Varnish funcționează ca un server web, fiind primul punct de interacțiune cu vizitatorul. Practic Varnish „stă” înaintea lui Apache/Nginx sau ce server web folosești și face cache la tot ce prinde. Astfel, foarte puține cereri mai ajung să consume resurse (conexiuni la baza de date, etc).

După ce instalezi Varnish, el rulează implicit pe portul 6081. Trebuie să-l modifici pe portul 80, altfel pierzi toată distracția. Pentru asta, trebuie modificat fișierul /etc/default/varnish. Noi folosim Debian, pentru CentOS sau alte distribuții, modul de configurare s-au putea să difere.

În fișierul de mai sus, găsești o linie de genul:

DAEMON_OPTS="-a :6081 \
  -T localhost:6082 \
  -f /etc/varnish/default.vcl \
  -S /etc/varnish/secret \
  -s malloc,256m"

Aici trebuie să schimbi din -a: 6081 în -a: 80. Deasemenea, ar fi util să setezi și un TTL folosind parametrul -t. Prin urmare linia respectivă devine ceva de genul:

DAEMON_OPTS="-a :80 -t 3600 \
  -T localhost:6082 \
  -f /etc/varnish/default.vcl \
  -S /etc/varnish/secret \
  -s malloc,256m"

Bun, acum că am schimbat portul din Varnish, va trebui să schimbam portul și în serverul web. Noi folosim Apache, deci modificăm fișierul /etc/apache2/ports.conf. Schimbăm linia Listen 80 în Listen 8080. Teoretic asta e suficient, dar depinde cum ai configurat Virtual Hosts. Dăm un restart la Apache și apoi la Vanish.

Până aici a fost simplu, dar încă nu e gata. Varnish rulează dar foarte probabil că nu face cache la nimic. Cum știi dacă face sau nu cache? Dai refresh la o pagina de 2-3 ori. Apoi te uiți în Firebug/Developer tools.

Varnish request age header

Cel mai probabil header-ul Age îți arată zero. Semn că nu se face cache.

Probleme cu dieta?

Ceva important de știut: Varnish are o problemă cu prăjiturile. Nu va face cache la o pagină dacă aceasta trimite cookie-uri. Deasemenea, nu va face cache dacă serverul trimite un header Cache-Control cu valoarea no-cache. Să vedem cum rezolvăm problemele astea.

Avanticart e scris în PHP, ca majoritatea soluțiilor de eCommerce open-source (sau proprietare) disponibile în piață. Dacă magazinul tău e scris în alt limbaj, suntem siguri că vei reuși să găsești soluții similare.

Implicit, PHP-ul trimite un header Cache-Control în momentul în care pornești sesiunea. În Firebug/Developer tools poți vedea ceva de genul:

Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

Pentru a scăpa de el, e de ajuns să apelezi session_cache_limiter(''); înainte de session_start();. Ai grijă cum adaugi codul ăsta. Dacă folosești o platformă open-source, o să ai probleme la viitorul upgrade dacă modifici codul sursă direct. Poate scrii un plugin sau găsești vreo setare.

Acum că problema asta e rezolvată, hai să vedem cum rezolvăm cu cookie-urile. Detalii interesante am găsit pe pagina celor de la Fastly. Practic, se fac o serie de șmecherii pentru a șterge cookie-urile și a le pune înapoi sub forma unui header. Trebuie modificat fișierul /etc/varnish/default.vcl să arate ceva de genul:

backend default {
    .host = "127.0.0.2";
    .port = "8080";
}

sub vcl_recv {
  if (req.url ~ "^/.*\.php.*" || req.url ~ "/one-page-checkout/") {
	   return (pass);
  }else {

    if (req.http.Cookie ~ "APP_SESSID_.*=") {
      # The request does have a tracking cookie so store it temporarily
      set req.http.Tmp-Set-Cookie = req.http.Cookie;
      unset req.http.Cookie;
    } else {
      # The request doesn't have a tracking cookie so force a miss
      set req.hash_always_miss = true;
    }
  }

}

sub vcl_fetch {
  set beresp.do_esi = true;

  if (req.url ~ "^/.*\.php.*" || req.url ~ "/one-page-checkout/") {
    return (deliver);
  }else{
    # The response has a Set-Cookie ...
    if (beresp.http.Set-Cookie) {
      # ... so store it temporarily
      set req.http.Tmp-Set-Cookie = beresp.http.Set-Cookie;
      # ... and then unset it
      unset beresp.http.Set-Cookie;
    }
  }

}

sub vcl_deliver {
  # Send the Cookie header again if we have it
  if (req.http.Tmp-Set-Cookie) {
    set resp.http.Set-Cookie = req.http.Tmp-Set-Cookie;
  }

}

Comentariile din cod credem că sunt suficiente, dar hai să remarcăm faptul că se caută un cookie de sesiune cu numele de forma APP_SESSID_(ceva). Aici va trebui să modifici cu numele cookie-ului din platforma ta.

Un alt lucru important de remarcat este faptul că primul request făcut de un vizitator va ajunge întotdeauna în magazin, fără să treacă prin cache. Acest lucru se face pentru a primi cookie-ul de sesiune.

Față de varianta propusă de Fastly, noi am adăugat în plus partea asta:

if (req.url ~ "^/.*\.php.*" || req.url ~ "/one-page-checkout/" || req.url ~ "/contul-meu/") {
   return (pass);
}

Adică nu vrem să se facă cache dacă vizitatorul este în pagina de checkout, în pagina de cont sau dacă request-ul este către fișiere .php (de obicei request-urile Ajax, pentru că majoritatea URL-urile sunt SEO-friendly, deci nu se termină în .php). Cu siguranță ai și tu pagini care nu trebuie să ajungă în cache. Trebuie să le identifici și să modifici fișierul în mod adecvat.

După ce dai un restart la Varnish și faci 2-3 request-uri ar trebui să vezi header-ul Age diferit de zero, în Firebug/Developer tools.

Dacă ai ajuns aici și ai scăpat de problemele cu dieta, felicitări, Varnish-ul tău se poate îngrășa (îi crește burta - pardon - cache-ul). Dacă header-ul Age îți arată tot zero, înseamnă că ceva nu merge bine. Trebuie să rezolvi problema înainte să continui.

Cine a mâncat prăjiturile?

Bun, acum se face cache-ul, dar totuși cookie-urile nu ajung înapoi în aplicația magazinului. Pentru asta, trebuie să generăm noi variabila $_COOKIE din header-ul setat de către Varnish.

Așa că am creat funcția asta:

function setCookiesFromVarnishHeader(){
  $header = $_SERVER['HTTP_TMP_SET_COOKIE'];
  if (empty($header)){
    return;
  }

  $header = explode(';', $header);

  foreach ($header as $h){
    $cookie = explode('=', $h);
    $cookieName = trim($cookie[0]);
    $cookieValue = trim($cookie[1]);
    $_COOKIE[$cookieName] = $cookieValue;
  }

}

Din nou, atenție unde scrii/apelezi funcția asta pentru a nu avea probleme la upgrade-ul viitor.

Coșul, cum e cu coșul?

Super. Avem cache, avem cookies. Mai rămâne o singură problemă: se face cache la toată pagina, inclusiv la partea de cod care îți arată câte produse ai în coșul de cumpărături.

coș cumpărături

Pentru a testa, poți să deschizi o nouă fereastra de browser în mod privat/incognito. La al doilea refresh (primul nu ajunge niciodată în cache, mai ții minte?) vei vedea coșul din primul browser.

Ne salveză ESI.

Varnish ne oferă un tag special pe care-l putem introduce în pagină. Deci acolo unde avem coșul de cumpărături, putem introduce un cod de genul:

<esi:include src="/esi_shopping_cart.php" />

Când Varnish va vedea codul acesta în pagină, va face un request în background către fișierul /esi_shopping_cart.php, va lua rezultatul lui și va recompune pagina, totul fără ca vizitatorul să știe.

Nu punem aici codul din /esi_shopping_cart.php deoarece implementarea diferă de la platformă la platformă, dar poate să fie un simplu echo pentru niște variabile din sesiune.

Pentru ca ESI să funcționeze, e necesar să remarci că în sub vcl_fetch am pus linia set beresp.do_esi = true;.

Gata?

Ok, totul merge perfect, deci e timpul să scoți berea de la rece, nu? Nu te grăbi, până aici a fost partea ușoară.

Cache-ul ăsta trebuie și sters. Metoda simplă e să ștergi tot cache-ul când se face orice modificare în magazin. Lucrul ăsta n-ar fi o problemă într-o zi obișnuită, dar aici vorbim de Black Friday. Dacă te apuci să ștergi tot cache-ul la fiecare comanda pentru a re-împrospăta stocul… o să ai o problemă. Ideal ar fi să ștergi cache-ul doar pentru produsul al cărui stoc s-a schimbat și eventual pagina de categorie pentru produsul respectiv.

Pentru a șterge cache-ul, trebuie să faci niște request-uri speciale (PURGE/BAN în loc de GET/POST) către Varnish. Request-urile astea se pot face în mai multe feluri iar mai multe detalii găsești în documentație. Noi preferăm să folosim BAN.

La Avanticart, fiindcă avem mai multe magazine pe același server, trebuie să știm cărui magazin vrem să-i ștergem cache-ul. Prin urmare ne folosim de niște headere adiționale (X-Ban-Host și X-Ban-Url). E nevoie să modificăm /etc/varnish/default.vcl pentru a adăuga suport pentru ștergere + setare headere.

Fiindcă aceste request-uri pot veni de oriunde, ar fi bine să nu lăsăm pe oricine să șteargă cache-ul. Adăugăm linia asta la începutul fișierului:

acl purge {
        "localhost";
        "192.168.1.0"/24;
}

Apoi modificăm funcția vcl_recv astfel:

sub vcl_recv {
  if (req.request == "BAN") {
        if ( !client.ip ~ purge) {
              error 405 "Not allowed.";
        }
        ban("obj.http.x-url ~ " + req.http.x-ban-url +
                    " && obj.http.x-host ~ " + req.http.x-ban-host);
        error 200 "Banned";
  }

  [... restul codului scris anterior ...]

Modificăm funcția vcl_fetch pentru a adaugă header-ele speciale:

sub vcl_fetch {
  set beresp.do_esi = true;
  set beresp.http.x-url = req.url;
  set beresp.http.x-host = req.http.host;

  [... restul codului scris anterior ...]

Iar acum dăm un restart la Vanish pentru a beneficia de modificările făcute.

Aici ai și funcția de PHP care se ocupă de ștergere:

function clearVarnishCache($host, $url){
  $c = curl_init('http://127.0.0.1/');
  curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($c, CURLOPT_CUSTOMREQUEST, 'BAN');
  curl_setopt($c, CURLOPT_HTTPHEADER, array('X-Ban-Url: ' . $url . '$', 'X-Ban-Host: ' . $host));
  $exec = curl_exec($c);
}

Desigur, când și unde apelezi această funcție e foarte important și poate avea impact asupra performanței. Pentru $url se pot da expresii regulare. Astfel, pentru a șterge tot cache-ul putem să apelam clearVarnishCache($host, '.*');

Gata!

Acum chiar că e gata. Dacă crezi că am ratat ceva, lasă-ne un comentariu. Iar dacă vrei să primești articolele viitoare legate de CDN și servere multiple, abonează-te.


  1. un singur client a fost afectat de o setare greșită care a dus la un comportament ciudat al coșului de cumpărături (Emilian, ne cerem scuze din nou).