Ghost, bir içerik yönetim sistemi. Modern ve oldukça hızlı. Benim için en büyük cazibesi ise Markdown ile makale yazabilme kolaylığı.

Uzun zamandır bir arayışın içindeydim. Wordpress artık beni bunaltmaya başladı çünkü bir çok temel eksiklikleri mevcut. Bunları tamamlamak için pluginler deryasında boğulmaktan kaçamıyorsunuz. Qmail patch çılgınlığı gibi...

Basit, hızlı, hafif ve kolay kullanılabilir bir şeye ihtiyacım vardı. Grav ilk durağım oldu ve işin doğrusu hoşuma da gitti. Üzerinde epeyce zaman harcadım ve hatta bir süre sitemi yayınladım. Neticede kendisi ile de yollarımızı ayırdık malesef. Çok detaya girmek istemiyorum.

Ghost hali hazırda zaten oldukça hızlı çalışan bir CMS. Fakat Express'i olduğu gibi yayınlamak fikrine pek sıcak bakmıyorum. Bu nedenle önüne bir NGINX yerleştirmeye karar verdim.

Bu sayede NGINX ile bir çok numara yapabilirim. Proxy Cache de bunlardan yalnızca bir tanesi.

Proxy Cache ile tüm çıktıyı cacheleyeceğim ve böylelikle arkadaki Ghost servisini bir nevi maskelemiş olacağım. Ayrıca Lua ve Ghost'un Webhooklarını kullanarak, sitede değişikiller oldukça, cachein tazelenmesini de sağlayacağım.

Tüm yaptıklarımı adım adım anlatacağı ancak bir parça NGINX biliyor olmak işi kolaylaştıracaktır.

Altyapı Senaryosu

Elimde bir Ghost olacak ve https://www.gokhangeyik.net ile erişebileceğim. Ancak önünde bir NGINX olacak ve ilk olarak trafiği karşılayacak. İsteğe bağlı olarak, Ghost backend ne cevap verirse, ziyaretçiye iletirken aynı zamanda da bir sonraki istekler için 2 saat boyunca cache yapacak.

Cache purge için son derece basit bir Lua script kullanacağım. Çünkü sitenin içeriği güncellendiğinde tüm cache içeriğinin temizlenmesi büyük bir problem değil. Ancak daha kompleks purge isteklerini elbette kendiniz de yazabilirsiniz. Tabi bu minvalde kendi webhooklarınızı düzenlemeniz gerekir.

Kuruluma Başlayalım

Tüm bunlar için Docker kullanacağım. Ancak bu bare metal kurulum yapanlar için de bir fark yaratmaz. Neticede odaklandığımız konu NGINX ve Ghost olacak.

Ghost Kurulumu

docker run --name ghost -p 2368:2368 ghost:3-alpine

Ghost ilk kez çalıştığı için, gerek duyduğu bir takım işlemler gerçekleştirecek ve neticede https://www.gokhangeyik.neterişilebilir hale gelecek.

Bunu curlile test edebiliriz.

➜ curl -I https://www.gokhangeyik.net
HTTP/1.1 200 OK
X-Powered-By: Express
Cache-Control: public, max-age=0
Content-Type: text/html; charset=utf-8
Content-Length: 29518
ETag: W/"734e-9ZdP5ZYyAQqraAJyQref33AByfQ"
Vary: Accept-Encoding
Date: Wed, 06 Jan 2021 21:16:43 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Ghost çalışıyor ve isteklere yanıt veriyor. Artık devam edebiliriz.

NGINX Kurulumu

Bunun için de Docker kullanacağım. Basit tutmak için, contaier temelini Alpine tercih ediyorum.

docker run --name nginx --rm -it -p 80:80 alpine:latest sh

Artık bir Alpine containerım var ve gerekli paketleri kurabilirim. İhtiyacım olan paketler:

  • nginx
  • nginx-mod-http-lua
  • lua-resty-core
apk update
apk add --no-cache nginx nginx-mod-http-lua lua-resty-core

Amacıma ulaşmaya hemen hemen hazırım ancak burada kısa bir bilgi vermem gerekiyor. Herşeyi cache etmek akıllıca olmayacaktır. Çünkü Ghost'un bazı lokasyonları tam tersine cache edilmemeli. Çünkü bu lokasyonlar, giriş yapmış kullanıcılara ait araçları içeriyor. Burada proxy_cache_bypass kullanabilirdim ve cookie bunun için harika bir belirleyici olabilirdi, ancak ben biraz daha net olmak istiyorum ve bu nedenle location konteksti ile bu özel alanları ayrı tutacağım.

proxy_cache_path /var/cache/nginx/ levels=1:2 keys_zone=ghost_cache:10m max_size=512m inactive=2h;
proxy_cache_key "$request_method$host$request_uri";
proxy_cache_methods GET HEAD;

Yukarıdaki direktifler http kontekstinin içinde olmalı. Şimdi tek tek ne yaptığımı anlatacağım.

proxy_cache_path, yapacağım işlem için oldukça kritik. Cache edilmiş isteklere ait dosyaların, nerede, ne şekilde ve ne kadar süreyle tutulmasını istediğimi belirtebilmemi sağlar. Ayrıca bu dosyaların byte cinsinden kaplayabileceği en fazla alanı da limitleyebilirim.

Örneğimden gidecek olursam, cache istekleri /var/cache/nginx dizininde depolanacak.

level=1:2 ise bu cache çıktılarının hiyerarşik olarak düzenini belirtiyor. NGINX, cache çıktılarını MD5 ile isimlendirir. Yani günün sonunda aşağıdaki gibi dosya isimlerine sahip olacağım.

bash-5.1# ls -Al
total 32
drwx------    3 nginx    nginx         4096 Jan 10 08:40 3
drwx------    3 nginx    nginx         4096 Jan 10 08:41 4
drwx------    3 nginx    nginx         4096 Jan 10 08:40 6
drwx------    4 nginx    nginx         4096 Jan 10 08:41 8
drwx------    3 nginx    nginx         4096 Jan 10 08:40 9
drwx------    3 nginx    nginx         4096 Jan 10 08:40 d
drwx------    3 nginx    nginx         4096 Jan 10 08:41 e
drwx------    3 nginx    nginx         4096 Jan 10 08:40 f
/var/cache/nginx

Örnek olarak 8 dizininin içeriğine bakalım.

bash-5.1# cd /var/cache/nginx/8/
bash-5.1# find .
.
./ca
./ca/31b4504a91cc38c996e2dff211e59ca8
./2e
./2e/b999904f61cd8ade87149edef72dd2e8

Yani NGINX, oluşturduğu bu dosya isimlerinden sonran 1. karakteri ilk seviye dizin olarak oluşturuyor. Bundan sonra gelen 2 karakteri de -ki bizim çıktımızda bu ca ve 2e- bir alt dizin olarak oluşturup, cache çıktısını burada saklıyor.

keys_zone bir çeşit index olarak düşünülebilir. Cache dosyalarının anahtarları burada tutuluyor. NGINX dökümanına göre 1 Mbyte boyutundan bir zone 8.000 anahtar saklayabiliyor. Aslında 10 Mbyte dahi benim için büyük bir alan.

max_size ise adı üzerinde, cache dosyalarının belirtilen dizin içerisinde kaplayabileceği maksimum disk alanı.

inactive ise bir pasif cache anahtarının maksimum süresini belirtiyor. Eğer cache edilmiş bir dosyaya, bu süre zarfında hiç erişilmemiş ise, cache durumu ne olursa olsun NGINX silecektir.

proxy_cache_key, keys_zone içerisinde tutulacak anahtarları isimlendirmek için bir şablon. Bu yönerge bir çok amaç için kullanılabilir. Eğer responsive bir web  uygulaması kullanmıyorsanız, mobil ve masa üstü istemcileri için farklı cacheler üretebilirsiniz.

Ve son olarak proxy_cache_methods ise istediğim HTTP istek metodlarını cache için seçmeme yardımcı oluyor.

Devamında artık cache yapmak istediğim ve cache dışında tutmak istediğim lokasyonları belirleyip yayınlamaya başlayabilirim.

location / {
            proxy_hide_header x-powered-by;
            proxy_hide_header Etag;
            proxy_redirect off;

            proxy_pass http://ghost:2368;

            proxy_cache ghost_cache;
            proxy_cache_valid 200 60m;
            proxy_cache_valid 404 3m;
            proxy_cache_revalidate off;
            proxy_ignore_headers X-Accel-Expires Expires Cache-Control;
            add_header X-NGINX-Cache $upstream_cache_status;
        }

Tek tek parametreleri tanıtmak ve anlatmak istemiyorum o nedenlen buradan sonrasını biraz daha hızlı devam edeceğim. location / Hemen her yeri kapsayan bir cache tanımı oldu. HTTP 200 60 dakika boyunca, HTTP 404 ise 3 dakika boyunca cache olacak. Bir takım HTTP başlıklarını gizlemeyi tercih ettim.

Buraya gelen tüm istekler, eğer keys_zone içerisinde değilse proxy_pass marifetiyle ghost ismine sahip containera gidecek. Son olarak da bir belirleyici ekledim ki cache işe yarıyor mu görebileyim.

Önemli iki alan var ki onları cache etmemeliyim. Bunlardan biri Ghost'un yönetim ara yüzü, diğeri ise gönderileri ön izleme yapmama yarayan alan. Aşağıdaki kontekst işime yarayacaktır.

location ~* ^(/ghost/|/p/) {
            proxy_hide_header x-powered-by;
            proxy_hide_header Etag;
            proxy_redirect off;

            proxy_pass http://ghost:2368;
            break;
        }

Şimdi bunları bir araya getirip ilk testlere başlayayım. Testlerimde curl ve httpstat kullanacağım ve konfigürasyonum şu şekilde görünecek.

proxy_cache_path /var/cache/nginx/ levels=1:2 keys_zone=ghost_cache:75m max_size=512m inactive=2h;
proxy_cache_key "$request_method$host$request_uri";
proxy_cache_methods GET HEAD;

server {
    listen 80;

    location ~* ^(/ghost/|/p/) {
        proxy_hide_header x-powered-by;
        proxy_hide_header Etag;
        proxy_redirect off;

        proxy_pass http://ghost:2368;
        break;
    }

    location / {
        proxy_hide_header x-powered-by;
        proxy_hide_header Etag;
        proxy_redirect off;

        proxy_pass http://ghost:2368;

        proxy_cache ghost_cache;
        proxy_cache_valid 200 60m;
        proxy_cache_valid 404 3m;
        proxy_cache_revalidate off;
        proxy_ignore_headers X-Accel-Expires Expires Cache-Control;
        add_header X-NGINX-Cache $upstream_cache_status;
    }
}

İlk olarak Ghost'u direk erişim ile test edeceğim. Söylediğim gibi aslında Ghost oldukça hızlı çalışan bir içerik yönetim sistemi.

➜ httpstat https://www.gokhangeyik.net
Connected to ::1:2368 from ::1:56947

HTTP/1.1 200 OK
X-Powered-By: Express
Cache-Control: public, max-age=0
Content-Type: text/html; charset=utf-8
Content-Length: 29518
ETag: W/"734e-7qGhBLEw4KHXjVrEObfphnPgXMc"
Vary: Accept-Encoding
Date: Sun, 10 Jan 2021 10:14:37 GMT
Connection: keep-alive
Keep-Alive: timeout=5

  DNS Lookup   TCP Connection   Server Processing   Content Transfer
[     1ms    |       0ms      |       57ms        |        1ms       ]
             |                |                   |                  |
    namelookup:1ms            |                   |                  |
                        connect:1ms               |                  |
                                      starttransfer:58ms             |
                                                                 total:59ms

Test blogunun ana sayfası 58ms içerisinde erişilir hale geldi. Bu oldukça iyi. Tabi httpstat maalesef javascript çalıştırmıyor. Bu süre reel değil. Yalnızca çıkarım yapmama yardım edecek.

Şimdi aynı isteği NGINX proxy üzerinden yapacağım. İlk isteğim cache edilmemiş olacağından 58ms'den daha uzun olma ihtimali var.

➜ httpstat http://localhost
Connected to ::1:80 from ::1:57112

HTTP/1.1 200 OK
Server: nginx
Date: Sun, 10 Jan 2021 10:19:06 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 29532
Connection: keep-alive
Cache-Control: public, max-age=0
Vary: Accept-Encoding
X-NGINX-Cache: MISS

  DNS Lookup   TCP Connection   Server Processing   Content Transfer
[     2ms    |       0ms      |       68ms        |        0ms       ]
             |                |                   |                  |
    namelookup:2ms            |                   |                  |
                        connect:2ms               |                  |
                                      starttransfer:70ms             |
                                                                 total:70ms

Beklediğim gibi oldu ve gayet makul. İlk istek 70ms kadar sürdü ve X-NGINX-Cache: MISS headerı henüz cache edilmediğini gösteriyor. Bundan sonraki isteğim direk olarak NGINX cache olacak ve arada ciddi bir hız farkı olmasını bekliyorum.

➜ httpstat http://localhost
Connected to ::1:80 from ::1:57190

HTTP/1.1 200 OK
Server: nginx
Date: Sun, 10 Jan 2021 10:21:21 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 29532
Connection: keep-alive
Cache-Control: public, max-age=0
Vary: Accept-Encoding
X-NGINX-Cache: HIT

  DNS Lookup   TCP Connection   Server Processing   Content Transfer
[     1ms    |       0ms      |        3ms        |        0ms       ]
             |                |                   |                  |
    namelookup:1ms            |                   |                  |
                        connect:1ms               |                  |
                                      starttransfer:4ms              |
                                                                 total:4ms

Toplam süre 4ms. Mükemmel bir sonuç. X-NGINX-Cache: HIT Artık cevabın Ghost'tan değil NGINX'ten geldiğini biliyorum.Öte yandan artık Ghost'u dert etmek zorunda değilim. Cache olduğu müddetçe, yüksek bir trafik ile karşılaşmayacak.

Şimdi sıra bu cache içeriğini temizlemekte. İştiyorum ki sitemde bir değişiklik yaptığımda, bu cache tamamen silinsin ve ben de değişiklikleri görebileyim. Bunun için iki şeye ihtiyacım var.

  1. NGINX purge lokasyonu
  2. Ghost içerisinde bunu tetikleyebilecek bir mekanizma.

Bunların hepsine sahibim. NGINX konfigürasyonumu aşağıdaki şekilde güncelliyorum.

proxy_cache_path /var/cache/nginx/ levels=1:2 keys_zone=ghost_cache:75m max_size=512m inactive=2h;
proxy_cache_key "$request_method$host$request_uri";
proxy_cache_methods GET HEAD;

server {
    listen 80;

    location ~* ^(/ghost/|/p/) {
        proxy_hide_header x-powered-by;
        proxy_hide_header Etag;
        proxy_redirect off;

        proxy_pass http://ghost:2368;
        break;
    }

    location / {
        proxy_hide_header x-powered-by;
        proxy_hide_header Etag;
        proxy_redirect off;

        proxy_pass http://ghost:2368;

        proxy_cache ghost_cache;
        proxy_cache_valid 200 60m;
        proxy_cache_valid 404 3m;
        proxy_cache_revalidate off;
        proxy_ignore_headers X-Accel-Expires Expires Cache-Control;
        add_header X-NGINX-Cache $upstream_cache_status;
    }
location /purgethecache {
        content_by_lua_block {
            os.execute("/bin/rm -rf /var/cache/nginx/*")
        }
    }
}

purgethecache isminde bir lokasyon ekledim ve oldukça basit bir lua direktifi çalıştırıyorum. Buraya gelen her istek ile birlikte /var/cache/nginx dizininin içeriği temizlenecek ve böylece cache yok edilmiş olacak.

Her Ghost blog bir yönetim arayüzüne sahip. Bu arayüzde Integrations bölümü mevcut. Bu bölümden bir Custom Integration ekleyeceğim.

Ghost Custom Integration Screen
Ghost Custom Integrations

Şimdi de bu entegrasyona bir Webhook ekleyeceğim. Yani Ghost, sitede herhangi bir değişiklik olunca, gidip purgethecache lokasyonunu ziyaret edecek. NGINX de tanımlandığı gibi cache içeriğini silecek.

Ghost Webhook
Ghost Webhook

Hedefime ulaştım. Artık calışan bir cache mekanizmam ve purge mekanizmam mevcut. Bu konfigurasyon elbette geliştiriebilir ve ben elimden geldiğince sadeleştirerek anlatmak istedim.