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.net
erişilebilir hale gelecek.
Bunu curl
ile 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.
Ö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.
- NGINX purge lokasyonu
- 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.
Ş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.
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.