في شبكة توصيل المحتوى CDN للمبتدئين شرحنا كيف تعمل ترويسة Cache-Control، ومفتاح ال Cache، وبنينا Cache صغيراً بـ Nginx أمام سيرفر بطيء، ورأينا صفحة خاصة تتسرب من مستخدم إلى آخر. وفي التخزين المؤقت Caching شرحنا ال Cache داخل التطبيق نفسه مع Valkey. وبقي سؤال يسأله كل من يريد أن يضع طبقة Cache أمام موقعه على سيرفره: هل يكفيني proxy_cache في Nginx الذي أستخدمه أصلاً، أم أحتاج Varnish؟ وما الذي يحدث عندما يضيف التطبيق ترويسة Set-Cookie، أو عندما أريد مسح صفحة واحدة، أو عندما يتوقف السيرفر الأصلي؟ وهذا ما سوف نجيب عنه هنا بتجربة واحدة نضع فيها الاثنين أمام السيرفر الأصلي نفسه ونقيس.
وسوف نناقش في هذا المقال ما يلي:
- ما الفرق بين Varnish Cache وVinyl Cache اليوم، وأي إصدار ورخصة تستخدم، ونسخة Nginx المفتوحة وما ينقصها.
- كيف يقرر كل منهما أن يحفظ الرد أو يمرره، ولماذا يكفي سطر Set-Cookie واحد ليفسد كل شيء.
- تجربة بأرقام حقيقية: نسبة HIT وزمن الاستجابة لكل منهما أمام سيرفر يأخذ 150 مللي ثانية لكل صفحة.
- فترة السماح Grace وstale-while-revalidate، وتقديم الصفحات عندما يتوقف السيرفر الأصلي.
- المسح Purge والحظر Ban، وثغرة ACL تفتح المسح للجميع.
- تركيب الصفحة من أجزاء بـ ESI، ثم عدة نقاط حافة PoPs مع GeoDNS وطبقة Shield.
ما الفرق بين Varnish Cache وVinyl Cache اليوم؟
إذا بحثت عن Varnish اليوم سوف تجد اسمين ومشروعين، وفهم الفرق مهم قبل أن تختار ما تثبته وما تقرأ توثيقه:
- Vinyl Cache: هو مشروع المجتمع الذي كان اسمه Varnish Cache منذ 2006، ومطوره الأساسي Poul-Henning Kamp. وفي 15 سبتمبر 2025، مع إعلان Varnish Cache 8.0، كتب Poul-Henning Kamp أن المشروع سوف يغير اسمه «على مضض» لتجنب الخلط بينه وبين شركة Varnish Software التي تستخدم الاسم تجارياً، وصفحة المشروع On Vinyl Cache and Varnish Cache تجعل إصدار Vinyl Cache 9.0 في 16 مارس 2026 نقطة الفصل بين المشروعين. وآخر إصدار له هو Vinyl Cache 9.1.0 بتاريخ 16 سبتمبر 2026، والرخصة BSD-2-Clause بحسب ملف الرخصة. ولاحظ أن الأسماء في هذا الإصدار تغيرت من varnish إلى vinyl في البرامج والمجلدات، فالأوامر والمسارات في الأدلة القديمة لن تعمل كما هي.
- Varnish Cache من Varnish Software: الشركة تستمر بالاسم القديم، وموقعها varnish.org يصفه بأنه توزيعة Downstream مبنية على Vinyl Cache مع ميزات وأدوات إضافية، ومستودعه varnish/varnish برخصة BSD-2-Clause أيضاً بحسب ملف الرخصة. وبحسب صفحة الإصدارات فآخر إصدار 9.1.0 بتاريخ 23 سبتمبر 2026 وينتهي دعمه في 30 سبتمبر 2027، وما زالت السلسلة الطويلة 6.0 مدعومة وآخرها 6.0.18. وDocker Image الرسمية varnish تتبع هذه التوزيعة، وهي التي استخدمناها في التجربة بالإصدار 9.1.0.
- Nginx: النسخة المفتوحة برخصة BSD-2-Clause بحسب ملف الرخصة، وصفحة التنزيل تذكر اليوم 1.31.6 كإصدار Mainline و1.30.5 كإصدار Stable، واستخدمنا 1.31.6. وأهم ما ينقصها في هذا الموضوع، بحسب توثيق proxy، أن التعليمة
proxy_cache_purgeمتاحة فقط ضمن الاشتراك التجاري، وكذلك فحوص الصحة النشطة في وحدة health_check.
والخلاصة العملية من هذا القسم أن الأوامر وملفات VCL في هذا المقال تعمل على Varnish Cache 9.1 كما هي، وأن معظمها يعمل على Vinyl Cache بعد تغيير أسماء البرامج، فاقرأ ملاحظات الترقية للإصدار الذي تختاره قبل أن تنقل الإعداد.
كيف يقرر كل منهما أن يحفظ الرد أو يمرره؟
الحل البسيط هو أن تقول لل Cache «احفظ كل شيء دقيقة»، وهذا يعمل في الدقائق الأولى ثم يعرض صفحة حساب مستخدم لمستخدم آخر، كما رأينا في تجربة الصفحة الخاصة. لذلك يأتي كل منهما بقواعد افتراضية حذرة، والصورة التالية تبين مسار الطلب:

وفي Varnish تكتب هذه القواعد بلغة VCL، وكل ما لا تكتبه بنفسك تكمله VCL المدمجة Built-in VCL، وتستطيع أن تقرأها كاملة بالأمر varnishd -x builtin. وهذه هي القواعد التي تهمنا كما طبعها الإصدار 9.1.0:
sub vcl_req_authorization {
if (req.http.Authorization) {
# Not cacheable by default.
return (pass);
}
}
sub vcl_req_cookie {
if (req.http.Cookie) {
# Risky to cache by default.
return (pass);
}
}
sub vcl_beresp_cookie {
if (beresp.http.Set-Cookie) {
call vcl_beresp_hitmiss;
}
}
sub vcl_beresp_hitmiss {
set beresp.ttl = param.uncacheable_ttl;
set beresp.uncacheable = true;
return (deliver);
}في الإعداد أعلاه لاحظ التالي:
- أي طلب فيه Cookie أو Authorization يمرر إلى السيرفر الأصلي دون بحث في ال Cache، وهذا يحمي الصفحات الخاصة، لكنه يعني أيضاً أن موقعاً يضع Cookie تحليلات عند كل زائر لن يستفيد من ال Cache أبداً حتى تحذف هذه ال Cookies من الطلبات العامة بنفسك.
- أي رد فيه Set-Cookie يصبح Hit-for-Miss، أي أن Varnish يحفظ مؤقتاً أن هذا الرابط لا يحفظ، لمدة
uncacheable_ttlوقيمتها 120 ثانية، فيرسل الطلبات التالية مباشرة إلى السيرفر الأصلي دون أن ينتظر بعضها بعضاً. - الرد الذي لا يحمل Cache-Control ولا Expires يحفظ مدة
default_ttl، وقيمتها 120 ثانية، ولذلك الصفحة الخاصة التي لا ترسل Cache-Control قد تحفظ إذا جاء طلبها بلا Cookie.
أما Nginx فلا توجد فيه لغة، وإنما تعليمات: المفتاح الافتراضي في proxy_cache_key قريب من $scheme$proxy_host$request_uri، والرد الذي فيه Set-Cookie لا يحفظ بحسب التوثيق، والرد الذي لا يحمل Cache-Control ولا Expires لا يحفظ إلا إذا كتبت proxy_cache_valid، وتجميع الطلبات المتزامنة على المفتاح نفسه معطل افتراضياً حتى تفعل proxy_cache_lock، بينما هو مفعل دائماً في Varnish.
تجربة عملية: Varnish وNginx أمام السيرفر الأصلي نفسه
الآن سوف نقوم ببناء سيرفر أصلي بطيء بلغة Python يأخذ 150 مللي ثانية لكل صفحة، وأمامه Varnish 9.1.0 وNginx 1.31.6 جنباً إلى جنب، وContainer كلاينت نرسل منه الحمل، وكلها على شبكة Docker خاصة، والمنافذ المنشورة على 127.0.0.1 فقط.
ملفات التجربة
السيرفر الأصلي origin.py، وفيه صفحات عامة، وصفحة أخبار بصلاحية قصيرة، وصفحة حساب لا ترسل Cache-Control، وصفحة رئيسية فيها ESI، وعداد لكل طلب يصل إليه:
"""Slow origin for the cache lab: every page costs 150 ms, and /stats counts origin hits per path."""
import os, time, threading
from collections import Counter
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
HITS = Counter()
LOCK = threading.Lock()
SET_COOKIE_EVERYWHERE = os.environ.get("SET_COOKIE_EVERYWHERE") == "1"
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, *a):
pass
def send(self, code, body, headers):
data = body.encode()
self.send_response(code)
for k, v in headers.items():
self.send_header(k, v)
if SET_COOKIE_EVERYWHERE:
# what an analytics or session plugin often does on every page
self.send_header("Set-Cookie", f"visitor={time.time_ns()}; Path=/")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_GET(self):
path = self.path.split("?")[0]
if path == "/stats":
with LOCK:
body = "\n".join(f"{k} {v}" for k, v in sorted(HITS.items()))
body += f"\nTOTAL {sum(HITS.values())}\n"
return self.send(200, body, {"Cache-Control": "no-store", "Content-Type": "text/plain"})
if path == "/reset":
with LOCK:
HITS.clear()
return self.send(200, "ok\n", {"Cache-Control": "no-store"})
with LOCK:
HITS[path] += 1
time.sleep(0.15) # a slow page: database queries, templates...
now = time.strftime("%H:%M:%S")
if path.startswith("/article/"):
return self.send(200, f"<h1>{path}</h1><p>built at {now}</p>\n",
{"Cache-Control": "public, max-age=30", "Content-Type": "text/html"})
if path == "/news":
return self.send(200, f"news built at {now}\n",
{"Cache-Control": "public, max-age=5, stale-while-revalidate=60",
"Content-Type": "text/plain"})
if path == "/account":
user = "guest"
for part in self.headers.get("Cookie", "").split(";"):
if part.strip().startswith("session="):
user = part.strip()[8:]
# no Cache-Control header at all: a common mistake on personal pages
return self.send(200, f"account page of {user}, balance 1200 SAR\n",
{"Content-Type": "text/plain"})
if path == "/home":
return self.send(200,
f"<p>home built at {now}</p>\n<esi:include src=\"/fragment/cart\"/>\n",
{"Cache-Control": "public, max-age=60", "Content-Type": "text/html"})
if path == "/fragment/cart":
return self.send(200, f"<p>cart fragment built at {now}</p>\n",
{"Cache-Control": "no-store", "Content-Type": "text/html"})
return self.send(404, "not found\n", {"Cache-Control": "no-store"})
ThreadingHTTPServer(("0.0.0.0", 8000), Handler).serve_forever()ثم إعداد Varnish في الملف default.vcl:
vcl 4.1;
backend origin {
.host = "lab-stor2-origin";
.port = "8000";
.probe = { .url = "/stats"; .interval = 2s; .timeout = 1s; .window = 3; .threshold = 2; }
}
acl purgers {
"localhost";
"10.250.43.50"; # the admin box only, not the whole subnet
}
sub vcl_recv {
if (req.method == "PURGE") {
if (client.ip !~ purgers) { return (synth(405, "Not allowed")); }
return (purge);
}
if (req.method == "BAN") {
if (client.ip !~ purgers) { return (synth(405, "Not allowed")); }
ban("obj.http.x-url ~ " + req.http.x-ban-regex);
return (synth(200, "Ban added"));
}
# public pages do not depend on cookies, so drop them before the lookup
if (req.url ~ "^/(article|news|home)") {
unset req.http.Cookie;
}
# everything else (e.g. /account) falls through to the built-in VCL,
# which passes any request that carries a Cookie or Authorization header
}
sub vcl_backend_response {
set beresp.http.x-url = bereq.url; # used by BAN expressions
if (bereq.url ~ "^/(article|news|home)") {
unset beresp.http.Set-Cookie; # see the break-it step
}
if (bereq.url == "/home") {
set beresp.do_esi = true;
}
set beresp.grace = 1h; # serve stale for up to an hour if needed
}
sub vcl_deliver {
unset resp.http.x-url;
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
} else {
set resp.http.X-Cache = "MISS";
}
}في الإعداد أعلاه لاحظ التالي:
- ال
.probeيفحص السيرفر الأصلي كل ثانيتين، فيعرف Varnish أنه متوقف قبل أن يصل إليه طلب زائر. - نحذف ال Cookie من الطلبات العامة فقط، وكل ما عداها يذهب إلى VCL المدمجة التي تمرر أي طلب فيه Cookie، فصفحة الحساب محمية دون أن نكتب لها قاعدة.
- نحذف Set-Cookie من ردود الصفحات العامة، وهذا قرار يجب أن تتأكد منه مع التطبيق، لأنه يعني أن هذه الصفحات لا تستطيع أن تبدأ جلسة.
- ال ACL المسمى
purgersيسمح بالمسح من Container واحد فقط هو الكلاينت، وسوف نرى بعد قليل لماذا لم نكتب الشبكة كلها. - الترويسة
X-Cacheنضيفها بأنفسنا لنرى HIT أو MISS، وهي ليست من Varnish.
ثم إعداد Nginx في الملف nginx.conf، وفيه موقعان: الأول على المنفذ 80 بإعداد حذر، والثاني على 8081 بالإعداد الذي ينسخه الناس من الإنترنت، سوف نستخدمه في خطوة الكسر:
events {}
http {
proxy_cache_path /var/cache/nginx/lab levels=1:2 keys_zone=lab:10m max_size=200m inactive=1h use_temp_path=off;
log_format cache '$request_uri $upstream_cache_status $request_time';
access_log /var/log/nginx/cache.log cache;
upstream origin { server lab-stor2-origin:8000; }
server {
listen 80;
location / {
proxy_pass http://origin;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_cache lab;
proxy_cache_key $scheme$host$request_uri;
proxy_cache_lock on; # one request fills a missing key
proxy_cache_background_update on; # refresh stale entries in the background
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_connect_timeout 2s;
# a refresh lever for one key (open-source nginx has no purge; see the article)
proxy_cache_bypass $http_x_refresh;
add_header X-Cache $upstream_cache_status always;
}
}
# break-it: the copy-pasted "cache everything for 10 minutes" block
server {
listen 8081;
location / {
proxy_pass http://origin;
proxy_cache lab;
proxy_cache_key $scheme$host$request_uri; # the cookie is not part of the key
proxy_cache_valid 200 10m; # applies to pages without Cache-Control
add_header X-Cache $upstream_cache_status always;
}
}
}ثم compose.yaml:
# Lab only: a slow origin, Varnish and Nginx in front of it, and a client to send traffic.
name: lab-stor2-cache
networks:
net:
name: lab-stor2-cnet
ipam: { config: [ { subnet: 10.250.43.0/24 } ] }
services:
origin:
image: python:3.13.16-slim
container_name: lab-stor2-origin
command: ["python", "-u", "/lab/origin.py"]
environment: { SET_COOKIE_EVERYWHERE: "${SET_COOKIE_EVERYWHERE:-0}" }
volumes: ["./:/lab:ro"]
networks: [net]
varnish:
image: varnish:9.1.0
container_name: lab-stor2-varnish
environment: { VARNISH_SIZE: 256M }
volumes: ["./default.vcl:/etc/varnish/default.vcl:ro", "./:/lab:ro"]
ports: ["127.0.0.1:18080:80"]
networks: [net]
depends_on: [origin]
nginx:
image: nginx:1.31.6-alpine
container_name: lab-stor2-nginx
volumes: ["./nginx.conf:/etc/nginx/nginx.conf:ro"]
ports: ["127.0.0.1:18081:80"]
networks: [net]
depends_on: [origin]
client:
image: python:3.13.16-slim
container_name: lab-stor2-client
command: ["sleep", "infinity"]
volumes: ["./:/lab:ro"]
networks: { net: { ipv4_address: 10.250.43.50 } }وأداتان صغيرتان للقياس: probe.py يرسل طلباً واحداً ويطبع الحالة والزمن، وload.py يرسل 2000 طلب بثمانية Threads على 50 مقالاً، بعضها مشهور جداً وأغلبها نادر كما في المواقع الحقيقية، ثم يطبع الزمن وعدد الطلبات التي وصلت إلى السيرفر الأصلي:
"""Send N requests over 50 article URLs (a few popular, many rare) with 8 threads.
Prints latency percentiles, the X-Cache counts and how many requests reached the origin."""
import sys, time, random, threading, http.client, urllib.request
host, port, n = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
random.seed(42)
weights = [1 / (i + 1) for i in range(50)] # Zipf-like popularity
urls = random.choices([f"/article/{i}" for i in range(50)], weights, k=n)
lat, status = [], {}
lock = threading.Lock()
def worker(chunk):
c = http.client.HTTPConnection(host, port, timeout=10)
for u in chunk:
t = time.perf_counter()
c.request("GET", u)
r = c.getresponse()
r.read()
dt = (time.perf_counter() - t) * 1000
x = r.getheader("X-Cache", "none")
with lock:
lat.append(dt)
status[x] = status.get(x, 0) + 1
urllib.request.urlopen("http://lab-stor2-origin:8000/reset").read()
t0 = time.perf_counter()
threads = [threading.Thread(target=worker, args=(urls[i::8],)) for i in range(8)]
for t in threads:
t.start()
for t in threads:
t.join()
wall = time.perf_counter() - t0
lat.sort()
def q(p):
return lat[int(p * (len(lat) - 1))]
stats = urllib.request.urlopen("http://lab-stor2-origin:8000/stats").read().decode()
origin = stats.split("TOTAL ")[1].strip()
print(f"{host}: {n} requests in {wall:.1f}s, p50 {q(.5):.1f} ms, p95 {q(.95):.1f} ms, "
f"p99 {q(.99):.1f} ms, X-Cache {dict(sorted(status.items()))}, origin hits {origin}")"""One request: prints time, cache, status, X-Cache, Age, latency and body.
Usage: probe.py <host> <port> <path> [METHOD] [Header:Value ...]"""
import sys, time, http.client
host, port, path = sys.argv[1], int(sys.argv[2]), sys.argv[3]
method = sys.argv[4] if len(sys.argv) > 4 else "GET"
headers = dict(h.split(":", 1) for h in sys.argv[5:])
c = http.client.HTTPConnection(host, port, timeout=15)
t = time.perf_counter()
c.request(method, path, headers=headers)
r = c.getresponse()
body = r.read().decode().strip().replace("\n", " | ")
ms = (time.perf_counter() - t) * 1000
name = host.replace("lab-stor2-", "")
print(f"{time.strftime('%H:%M:%S')} {name:8} {method} {path} -> {r.status} "
f"X-Cache={r.getheader('X-Cache')} Age={r.getheader('Age')} {ms:.1f} ms {body}")docker compose up -d
docker exec lab-stor2-varnish varnishd -V
docker exec lab-stor2-nginx nginx -vvarnishd (varnish-9.1.0 revision ffd36e9db9a1ec2bf6637deda180a87d40a13d0f)
nginx version: nginx/1.31.6نسبة HIT وزمن الاستجابة
نرسل الحمل نفسه ثلاث مرات: إلى السيرفر الأصلي مباشرة، ثم إلى Varnish، ثم إلى Nginx، وكل منهما يبدأ فارغاً، ثم مرة ثانية والذاكرة ممتلئة:
docker exec lab-stor2-client bash -c 'cd /lab
python3 load.py lab-stor2-origin 8000 2000
python3 load.py lab-stor2-varnish 80 2000
python3 load.py lab-stor2-nginx 80 2000
python3 load.py lab-stor2-varnish 80 2000
python3 load.py lab-stor2-nginx 80 2000'
docker exec lab-stor2-varnish varnishstat -1 -f MAIN.cache_hit -f MAIN.cache_miss -f MAIN.cache_hitmisslab-stor2-origin: 2000 requests in 51.0s, p50 200.1 ms, p95 212.3 ms, p99 226.9 ms, X-Cache {'none': 2000}, origin hits 2000
lab-stor2-varnish: 2000 requests in 2.5s, p50 2.4 ms, p95 18.6 ms, p99 200.0 ms, X-Cache {'HIT': 1950, 'MISS': 50}, origin hits 50
lab-stor2-nginx: 2000 requests in 3.1s, p50 1.0 ms, p95 16.7 ms, p99 244.9 ms, X-Cache {'HIT': 1950, 'MISS': 50}, origin hits 50
lab-stor2-varnish: 2000 requests in 1.7s, p50 3.4 ms, p95 26.8 ms, p99 47.4 ms, X-Cache {'HIT': 2000}, origin hits 0
lab-stor2-nginx: 2000 requests in 1.1s, p50 3.1 ms, p95 10.8 ms, p99 23.6 ms, X-Cache {'HIT': 2000}, origin hits 0
MAIN.cache_hit 3950 52.67 Cache hits
MAIN.cache_hitmiss 0 0.00 Cache hits for miss.
MAIN.cache_miss 50 0.67 Cache misses
| الهدف | الزمن الكلي | p50 | p95 | نسبة HIT | طلبات وصلت إلى السيرفر الأصلي |
|---|---|---|---|---|---|
| السيرفر الأصلي مباشرة | 51.0 ثانية | 200 ms | 212 ms | لا يوجد | 2000 |
| Varnish فارغ | 2.5 ثانية | 2.4 ms | 18.6 ms | 97.5% | 50 |
| Nginx فارغ | 3.1 ثانية | 1.0 ms | 16.7 ms | 97.5% | 50 |
| Varnish ممتلئ | 1.7 ثانية | 3.4 ms | 26.8 ms | 100% | 0 |
| Nginx ممتلئ | 1.1 ثانية | 3.1 ms | 10.8 ms | 100% | 0 |
في النتائج أعلاه لاحظ التالي:
- الفرق الحقيقي ليس بين Varnish وNginx، وإنما بين وجود Cache وعدم وجوده: من 51 ثانية و2000 طلب على السيرفر الأصلي إلى ثانيتين أو ثلاث و50 طلباً فقط، أي طلب واحد لكل مقال.
- 50 MISS بالضبط مع أن الطلبات متزامنة بثمانية Threads، والسبب تجميع الطلبات Request Coalescing في Varnish و
proxy_cache_lock onفي Nginx، فالطلبات المتزامنة على مقال لم يحفظ بعد انتظرت جلباً واحداً بدل أن تذهب كلها إلى السيرفر الأصلي، وهذه هي مشكلة الاندفاع Cache Stampede التي شرحناها في التخزين المؤقت Caching. - الفروق بين Varnish وNginx هنا بضعة مللي ثانية في اتجاهات مختلفة بين التشغيلين، وهي ضمن تذبذب جهاز واحد عليه Containers أخرى، فلا تختر بينهما على أساس السرعة في هذا الحجم، والأرقام كلها للتوضيح.
نكسره: Set-Cookie في كل صفحة
الآن نشغل السيرفر الأصلي بالمتغير SET_COOKIE_EVERYWHERE=1، فيضيف ترويسة Set-Cookie لكل رد، كما تفعل إضافات التحليلات أو الجلسات في كثير من التطبيقات، ونحمل في Varnish ملف VCL لا يحتوي إلا تعريف السيرفر الأصلي، أي أن القرار كله ل VCL المدمجة:
vcl 4.1;
# Only a backend: every decision comes from the built-in VCL.
backend origin {
.host = "lab-stor2-origin";
.port = "8000";
}SET_COOKIE_EVERYWHERE=1 docker compose up -d
docker exec lab-stor2-varnish sh -c 'varnishadm vcl.load minimal /lab/minimal.vcl && varnishadm vcl.use minimal'
docker exec lab-stor2-client bash -c 'cd /lab; python3 load.py lab-stor2-varnish 80 2000; python3 load.py lab-stor2-nginx 80 2000'
docker exec lab-stor2-varnish varnishstat -1 -f MAIN.cache_hit -f MAIN.cache_miss -f MAIN.cache_hitmissSet-Cookie: visitor=1791836743727339471; Path=/
lab-stor2-varnish: 2000 requests in 50.9s, p50 200.0 ms, p95 210.2 ms, p99 220.1 ms, X-Cache {'none': 2000}, origin hits 2000
lab-stor2-nginx: 2000 requests in 114.0s, p50 200.1 ms, p95 1651.6 ms, p99 3656.7 ms, X-Cache {'MISS': 2000}, origin hits 2000
MAIN.cache_hit 0 0.00 Cache hits
MAIN.cache_hitmiss 1950 9.90 Cache hits for miss.
MAIN.cache_miss 2001 10.16 Cache missesفي المخرج أعلاه لاحظ التالي:
- نسبة HIT صفر في الاثنين، وكل الطلبات ال 2000 وصلت إلى السيرفر الأصلي، أي أن ال Cache أصبح Proxy عادياً، وسطر واحد في التطبيق ألغى كل ما بنيته.
- في Varnish ظهر العداد
cache_hitmissبقيمة 1950، أي أن Varnish تذكر أن هذه الروابط لا تحفظ، فأرسل الطلبات مباشرة دون انتظار، فبقي الزمن مثل السيرفر الأصلي بالضبط. - في Nginx كان الأسوأ: p95 بلغ 1.65 ثانية وp99 بلغ 3.66 ثانية، والزمن الكلي 114 ثانية بدل 51، والسبب
proxy_cache_lockنفسه الذي أنقذنا قبل قليل، فالتوثيق يقول إن الطلبات الأخرى على المفتاح نفسه تنتظر حتى يظهر الرد في ال Cache أو يتحرر القفل حتى مهلةproxy_cache_lock_timeout، والرد هنا لا يحفظ أبداً، فأصبحت الطلبات تقف في طابور خلف بعضها.
والإصلاح في Varnish هو ما كتبناه في default.vcl، أي حذف Set-Cookie من ردود الصفحات العامة، وبعد إعادة تفعيله:
docker exec lab-stor2-varnish varnishadm vcl.use boot
docker exec lab-stor2-client bash -c 'cd /lab; python3 load.py lab-stor2-varnish 80 2000'VCL 'boot' now active
lab-stor2-varnish: 2000 requests in 2.7s, p50 2.4 ms, p95 25.3 ms, p99 204.1 ms, X-Cache {'HIT': 1937, 'MISS': 63}, origin hits 63ولاحظ أن ال MISS أصبحت 63 بدل 50، والسبب أن كائنات Hit-for-Miss التي حفظت قبل قليل ما زالت صالحة 120 ثانية لبعض الروابط. والإصلاح الأصح دائماً أن يتوقف التطبيق عن إرسال Set-Cookie للزائر الذي لا يحتاج جلسة، لأن حذفها في ال Cache قد يكسر ميزة في التطبيق لا تعرفها، وفي Nginx لا يوجد إلا هذا الإصلاح، أو proxy_ignore_headers Set-Cookie مع proxy_hide_header Set-Cookie للمواقع العامة بالكامل وبحذر شديد.
نكسره: صفحة خاصة في ال Cache
شرحنا هذا الخطأ بالتفصيل في تجربة CDN، لذلك نعيده هنا فقط لنقارن سلوك الاثنين بالإعداد الافتراضي. صفحة /account لا ترسل Cache-Control، ونطلبها من أليس ثم من بوب، كل منهما بجلسته:
--- nginx main block (port 80): no proxy_cache_valid, page has no Cache-Control
alice -> MISS account page of alice, balance 1200 SAR
bob -> MISS account page of bob, balance 1200 SAR
--- nginx copy-pasted block (port 8081): proxy_cache_valid 200 10m
alice -> MISS account page of alice, balance 1200 SAR
bob -> HIT account page of alice, balance 1200 SAR
--- Varnish default.vcl
alice -> MISS account page of alice, balance 1200 SAR
bob -> MISS account page of bob, balance 1200 SARNginx بإعداده الحذر لم يحفظ الصفحة لأنها بلا Cache-Control، لكن سطراً واحداً proxy_cache_valid 200 10m جعل بوب يرى رصيد أليس، أما Varnish فمرر الطلبين لأن فيهما Cookie. ولاحظنا شيئاً آخر في محاولة أولى: الموقعان في nginx.conf يستخدمان منطقة ال Cache نفسها والمفتاح نفسه، فبعد أن حفظ الموقع 8081 صفحة أليس صار الموقع 80 الحذر يقدمها هو أيضاً كـ HIT، أي أن خطأ في موقع واحد يتسرب إلى كل المواقع التي تشترك في المنطقة. لذلك اجعل لكل موقع منطقة Cache خاصة به، وأرسل من التطبيق Cache-Control: private, no-store لكل صفحة خاصة، فهذا هو الحل الذي يعمل مع أي Cache.
فترة السماح Grace وstale-while-revalidate
عندما تنتهي صلاحية صفحة مشهورة يحدث أمران سيئان: الزائر الذي جاء في تلك اللحظة ينتظر السيرفر الأصلي كاملاً، وإذا كان السيرفر الأصلي متوقفاً يرى خطأ رغم أن نسخة قبل ثانية كانت صالحة. والحل في الاثنين أن تسمح بتقديم النسخة القديمة لفترة محددة بعد انتهاء صلاحيتها، والصورة التالية تبين ذلك:

وفي Varnish اسمها grace، وقيمتها بحسب مرجع متغيرات VCL تأتي من ترويسة stale-while-revalidate في Cache-Control إذا وجدت، وإلا من الإعداد default_grace وقيمته 10 ثوان، وفي default.vcl رفعناها إلى ساعة. وفي Nginx تفعلها بـ proxy_cache_use_stale updating مع proxy_cache_background_update on، والتوثيق يذكر أن Nginx يحترم أيضاً stale-while-revalidate وstale-if-error. وصفحة /news في التجربة ترسل max-age=5, stale-while-revalidate=60، فنطلبها ثم ننتظر سبع ثوان ونطلبها مرتين:
P="docker exec lab-stor2-client python3 /lab/probe.py"
for h in lab-stor2-varnish lab-stor2-nginx; do $P $h 80 /news; done; sleep 7
for h in lab-stor2-varnish lab-stor2-nginx; do $P $h 80 /news; done; sleep 1
for h in lab-stor2-varnish lab-stor2-nginx; do $P $h 80 /news; done20:30:49 varnish GET /news -> 200 X-Cache=MISS Age=0 161.1 ms news built at 20:30:49
20:30:51 nginx GET /news -> 200 X-Cache=MISS Age=None 165.8 ms news built at 20:30:51
20:30:58 varnish GET /news -> 200 X-Cache=HIT Age=8 4.0 ms news built at 20:30:49
20:30:59 nginx GET /news -> 200 X-Cache=STALE Age=None 4.7 ms news built at 20:30:51
20:31:01 varnish GET /news -> 200 X-Cache=HIT Age=2 39.6 ms news built at 20:30:58
20:31:01 nginx GET /news -> 200 X-Cache=HIT Age=None 3.2 ms news built at 20:30:59في المخرج أعلاه لاحظ التالي:
- بعد سبع ثوان انتهت صلاحية الخمس ثوان، ومع ذلك جاء الرد في 4 مللي ثانية من الاثنين بالنسخة القديمة، فالزائر لم ينتظر السيرفر الأصلي.
- Nginx يقول ذلك صراحة بالحالة
STALE، أما Varnish فيقول HIT معAge=8، أي أكبر من max-age، وهذه هي العلامة التي تبحث عنها في Varnish. - في الطلب التالي جاءت النسخة الجديدة المبنية في 20:30:58 و20:30:59، أي أن كلاً منهما جلبها في الخلفية أثناء تقديم القديمة.
والآن الحالة الأهم: نترك صفحة /article/7 تنتهي صلاحيتها (30 ثانية)، ثم نوقف السيرفر الأصلي، ونطلبها مع رابط لم يحفظ أبداً:
sleep 35; docker stop lab-stor2-origin; sleep 7
for h in lab-stor2-varnish lab-stor2-nginx; do $P $h 80 /article/7; $P $h 80 /never-cached; doneorigin stopped at 20:33:23
20:33:32 varnish GET /article/7 -> 200 X-Cache=HIT Age=56 6.2 ms <h1>/article/7</h1><p>built at 20:32:35</p>
20:33:33 varnish GET /never-cached -> 503 X-Cache=MISS Age=0 10.8 ms ... <title>503 Backend fetch failed</title> ...
20:33:34 nginx GET /article/7 -> 200 X-Cache=STALE Age=None 20.5 ms <h1>/article/7</h1><p>built at 20:32:35</p>
20:33:39 nginx GET /never-cached -> 504 X-Cache=MISS Age=None 2008.6 ms ... <title>504 Gateway Time-out</title> ...الاثنان قدما الصفحة القديمة والسيرفر الأصلي متوقف، والزائر لم يعرف بالعطل. لكن انظر إلى الرابط الذي لم يحفظ: Varnish رد بخطأ 503 في 11 مللي ثانية، لأن ال probe أخبره مسبقاً أن السيرفر الأصلي مريض Sick كما ظهر في varnishadm backend.list، بينما انتظر Nginx مهلة الاتصال ثانيتين كاملتين في كل طلب ثم رد بـ 504، والسبب أن النسخة المفتوحة من Nginx لا تفحص السيرفر الأصلي إلا عندما يصلها طلب. وفي موقع عليه ألف طلب في الثانية، ثانيتان لكل طلب يعني آلاف الاتصالات المعلقة في Nginx خلال ثوان.
المسح Purge والحظر Ban
عندما تعدل مقالاً لا تريد أن ينتظر القراء حتى تنتهي صلاحيته، وقد ذكرنا في CDN للمبتدئين متى تحتاج المسح ومتى يكفيك تغيير اسم الملف. وفي Varnish طريقتان بحسب توثيق المسح:
- Purge: يحذف كائن رابط واحد فوراً، ويناسب «عدلت هذا المقال».
- Ban: تعبير يطابق كائنات كثيرة، مثل «كل الروابط التي تبدأ بـ /article/1»، ولا يحذفها فوراً، وإنما يضيف التعبير إلى قائمة الحظر Ban List، فيفحص كل كائن عند طلبه التالي، وتمر عليها في الخلفية خدمة اسمها Ban Lurker. ولكي تعمل في الخلفية يجب أن يكون التعبير على خصائص الكائن
obj.*فقط، ولذلك نسخنا الرابط إلى ترويسةx-urlعند الحفظ ثم حذفناها قبل التسليم.
V=lab-stor2-varnish
$P $V 80 /article/3; $P $V 80 /article/3 PURGE; $P $V 80 /article/3
for u in /article/10 /article/11 /article/12; do $P $V 80 $u >/dev/null; done
$P $V 80 / BAN "x-ban-regex:^/article/1[0-9]$"
docker exec $V varnishadm ban.list
$P $V 80 /article/10; $P $V 80 /article/320:34:05 varnish GET /article/3 -> 200 X-Cache=HIT Age=1 14.8 ms <h1>/article/3</h1><p>built at 20:34:03</p>
20:34:08 varnish PURGE /article/3 -> 200 X-Cache=None Age=None 9.0 ms ... <title>200 Purged</title> ...
20:34:09 varnish GET /article/3 -> 200 X-Cache=MISS Age=0 157.8 ms <h1>/article/3</h1><p>built at 20:34:09</p>
20:34:16 varnish BAN / -> 200 X-Cache=None Age=None 7.1 ms ... <title>200 Ban added</title> ...
Present bans:
1791837256.452598 0 - obj.http.x-url ~ ^/article/1[0-9]$
1791836717.770480 64 C
20:34:18 varnish GET /article/10 -> 200 X-Cache=MISS Age=0 155.4 ms <h1>/article/10</h1><p>built at 20:34:18</p>
20:34:18 varnish GET /article/3 -> 200 X-Cache=HIT Age=9 6.9 ms <h1>/article/3</h1><p>built at 20:34:09</p>بعد ال PURGE أصبح /article/3 MISS ثم بني من جديد، وبعد ال BAN أصبح /article/10 MISS، بينما بقي /article/3 HIT لأنه لا يطابق التعبير. وأما Nginx فلا يوجد فيه مسح في النسخة المفتوحة كما ذكرنا، وأقرب شيء هو أن تجبره على تحديث مفتاح واحد بترويسة سرية مع proxy_cache_bypass:
20:34:19 nginx GET /article/3 -> 200 X-Cache=MISS Age=None 160.4 ms ...built at 20:34:19...
20:34:20 nginx PURGE /article/3 -> 501 X-Cache=None Age=None 5.9 ms ...Unsupported method ('PURGE')...
20:34:21 nginx GET /article/3 -> 200 X-Cache=BYPASS Age=None 160.7 ms ...built at 20:34:21...
20:34:24 nginx GET /article/3 -> 200 X-Cache=HIT Age=None 6.7 ms ...built at 20:34:21...طلب PURGE مر عبر Nginx إلى السيرفر الأصلي الذي رد بـ 501، أي أن Nginx لا يعرف هذا الطلب أصلاً، ثم الطلب بالترويسة X-Refresh: 1 تجاوز ال Cache وحدث النسخة المحفوظة، فجاء الطلب التالي HIT بالنسخة الجديدة. وهذه الطريقة لا تمسح مجموعة روابط، ولا تحذف شيئاً، ويجب أن تحجب الترويسة عن الزوار وإلا استطاع أي زائر أن يتجاوز ال Cache في كل طلب.
نكسره: ACL تسمح بالمسح للجميع
في النسخة الأولى من default.vcl كتبنا في ال ACL الشبكة كلها "10.250.43.0"/24، على أساس أن كل ما في الشبكة الخاصة موثوق، ثم أرسلنا PURGE من جهاز التجربة نفسه عبر المنفذ المنشور 127.0.0.1:18080، أي من خارج الشبكة الخاصة:
curl -s -X PURGE -o /dev/null -w "host curl PURGE -> %{http_code}\n" http://127.0.0.1:18080/article/3
docker exec lab-stor2-varnish varnishlog -g request -q 'ReqMethod eq "PURGE"' -i ReqStart -i ReqMethodhost curl PURGE -> 200
* << Request >> 770
- ReqStart 10.250.43.1 44444 http http
- ReqMethod PURGEنجح المسح، لأن Varnish رأى الطلب قادماً من 10.250.43.1، وهو عنوان البوابة Gateway الذي يترجم إليه Docker كل اتصال يأتي عبر منفذ منشور، وهذا العنوان داخل الشبكة التي سمحنا لها. والحالة نفسها تحدث في الإنتاج عندما يكون أمام Varnish موزع حمل أو Reverse Proxy، فيرى Varnish كل الطلبات من عنوان واحد موثوق، وأي زائر يستطيع أن يمسح موقعك كله ويرسل كل الطلبات إلى السيرفر الأصلي. وقد شرحنا مشكلة العنوان الحقيقي للزائر خلف ال Proxy في تطبيقك خلف Reverse Proxy. والإصلاح أن تسمح بعنوان محدد لا يمر عبره الزوار، كما في default.vcl النهائي، أو أن تمسح من varnishadm على السيرفر نفسه. وبعد التعديل رفض Varnish الطلب نفسه من جهاز التجربة بالرمز 405، وقبله من Container الكلاينت الذي عنوانه 10.250.43.50:
--- after narrowing the ACL and restarting Varnish
host curl PURGE -> 405
20:36:06 varnish PURGE /article/3 -> 200 X-Cache=None ...ولاحظ أننا أعدنا تشغيل Varnish بعد تعديل الملف، والسبب أن الملف مربوط ك Bind Mount لملف واحد، وأداة sed تنشئ ملفاً جديداً بدل تعديل القديم، فلم يلاحظ ال Container التعديل حتى أعدنا تشغيله، وفي الإنتاج تحمل الإعداد الجديد بـ varnishadm vcl.load ثم vcl.use دون إعادة تشغيل ودون أن تفقد ال Cache.
تركيب الصفحة من أجزاء بـ ESI
الصفحة الرئيسية في أغلب المواقع عامة إلا جزءاً صغيراً، مثل عدد العناصر في السلة، وهذا الجزء الصغير يمنع حفظ الصفحة كلها. وESI (Edge Side Includes) يحل ذلك: الصفحة تحفظ دقيقة كاملة، وفيها وسم <esi:include src="/fragment/cart"/> يجلبه Varnish ويضعه مكانه عند كل طلب بسياسة الحفظ الخاصة به. وتوثيق ESI يذكر أن Varnish يطبق جزءاً صغيراً من المواصفة فقط: esi:include وesi:remove و<!--esi ...-->، ولا يطبق الاستبدال حسب المتغيرات وال Cookies.
20:35:41 varnish GET /home -> 200 X-Cache=MISS Age=0 356.8 ms <p>home built at 20:35:41</p> | <p>cart fragment built at 20:35:41</p>
20:35:46 varnish GET /home -> 200 X-Cache=HIT Age=4 155.2 ms <p>home built at 20:35:41</p> | <p>cart fragment built at 20:35:46</p>
20:35:47 nginx GET /home -> 200 X-Cache=MISS Age=None 157.5 ms <p>home built at 20:35:47</p> | <esi:include src="/fragment/cart"/>في الطلب الثاني جاءت الصفحة من ال Cache (مبنية في 20:35:41) والجزء جديد (20:35:46)، لكن الزمن 155 مللي ثانية لأن الجزء نفسه يجلب من السيرفر الأصلي البطيء في كل طلب، فلا تجعل ESI ذريعة لترك أجزاء بطيئة، واجعل الجزء سريعاً أو احفظه بمفتاح لكل مستخدم. وأما Nginx فسلم الوسم كما هو للمتصفح لأنه لا يعرف ESI، وفيه بدلاً منه وحدة SSI بصيغة مختلفة.
عدة نقاط حافة PoPs مع GeoDNS
كل ما سبق Cache واحد أمام سيرفر واحد، وهو يخفف الحمل ولا يقرب الموقع من الزائر البعيد. وقد شرحنا في CDN صغيرة من عدة سيرفرات كيف تجيب GeoDNS كل زائر بعنوان أقرب سيرفر، وما الذي يتضاعف عليك من شهادات وتحديثات، فلن نكرره هنا، وإنما نضيف ما يخص Varnish وNginx عندما يصبح لديك أكثر من نقطة، والصورة التالية تبين الفكرة:

- طبقة Shield: Cache إضافي قرب السيرفر الأصلي تطلب منه كل النقاط بدل أن تطلب من السيرفر الأصلي، فإذا انتهت صلاحية صفحة في ثلاث مدن وصل إلى السيرفر الأصلي طلب واحد بدل ثلاثة، وتجميع الطلبات الذي رأيناه في التجربة يعمل في ال Shield على مستوى المدن كلها.
- ترتيب المسح: امسح ال Shield أولاً ثم النقاط، وإلا قد تطلب نقطة الصفحة بعد مسحها مباشرة فتأخذ من ال Shield النسخة القديمة نفسها وتحفظها من جديد. ومع Nginx المفتوح لا يوجد مسح أصلاً، فكل ما تستطيعه هو صلاحيات قصيرة وتغيير أسماء الملفات.
- إعداد واحد في كل مكان: ملف VCL أو nginx.conf نفسه بالإصدار نفسه على كل النقاط من مستودع Git، فإذا اختلف مفتاح ال Cache أو قاعدة Set-Cookie بين نقطة وأخرى رأى الزوار في مدينة ما لا يراه غيرهم.
- الصحة: GeoDNS يجب أن يتوقف عن إرسال الزوار إلى نقطة متوقفة، وهذا يحتاج فحصاً من الخارج، لأن فحص Varnish لسيرفره الأصلي لا يخبرك أن Varnish نفسه متوقف.
- ال Grace أهم هنا: نقطة بعيدة عن السيرفر الأصلي تتأثر بأي انقطاع في الطريق إليه، وفترة سماح طويلة تجعل الانقطاعات القصيرة غير مرئية للزوار.
Varnish أم Nginx؟ المقارنة في جدول
| المعيار | Varnish Cache 9.1 | Nginx 1.31 المفتوح |
|---|---|---|
| الإعداد | لغة VCL، تتحكم في كل مرحلة | تعليمات proxy_cache_* |
| تجميع الطلبات المتزامنة | دائماً | بعد تفعيل proxy_cache_lock |
| رد فيه Set-Cookie | Hit-for-Miss، والطلبات تذهب مباشرة | لا يحفظ، ومع القفل تتراكم الطلبات |
| طلب فيه Cookie | يمرر افتراضياً | لا علاقة لل Cookie إلا إذا أضفتها إلى المفتاح |
| تقديم القديم | grace مع probe نشط | proxy_cache_use_stale |
| رابط غير محفوظ والسيرفر الأصلي متوقف | 503 في 11 ms في التجربة | 504 بعد ثانيتين في التجربة |
| المسح | Purge وBan | غير موجود، فقط في الاشتراك التجاري |
| ESI | جزء من المواصفة | لا يوجد، وفيه SSI |
| TLS | لم نختبره، وفي التصميم المقترح هنا ينهي TLS أمامه Nginx أو HAProxy | مدمج |
| الرخصة | BSD-2-Clause | BSD-2-Clause |
| نسبة HIT في التجربة | 97.5% ثم 100% | 97.5% ثم 100% |
ونقاط القوة في Varnish هي التحكم الكامل بالقواعد، والمسح والحظر، وفحص السيرفر الأصلي، وESI، ونقاط ضعفه أنه طبقة إضافية تديرها ولغة تتعلمها، وأن تغيير الاسم والانقسام إلى مشروعين يحتاج منك متابعة أي توثيق تقرأ. ونقاط القوة في Nginx أنه موجود أصلاً أمام أغلب المواقع، وينهي TLS، وإعداده سطور قليلة، ونقاط ضعفه غياب المسح والفحص النشط في النسخة المفتوحة، وأن القواعد المعقدة تصبح صعبة القراءة. وإذا كان Nginx هو ال Reverse Proxy عندك، فقد شرحنا أدواته في Reverse Proxy أم Load Balancer أم API Gateway؟.
أيهما تختار؟
- موقع أو مدونة أو متجر صغير، والتعديلات قليلة: proxy_cache في Nginx الذي لديك، مع
proxy_cache_lockوproxy_cache_use_staleوproxy_cache_background_updateكما في الملف أعلاه، وصلاحيات قصيرة للصفحات بدل المسح. - موقع محتوى أو أخبار يعدل كثيراً ويحتاج مسح صفحات أو أقسام فوراً، أو صفحات فيها أجزاء شخصية: Varnish خلف Nginx أو HAProxy الذي ينهي TLS، وقد شرحنا HAProxy في تثبيت HAProxy.
- زوار في مدن بعيدة عن سيرفرك: ابدأ بخدمة CDN تجارية كما في خدمات Cloudflare وبدائلها، وابن نقاطك الخاصة فقط إذا كان لديك سبب واضح مثل بقاء البيانات داخل البلد.
- الملفات الكبيرة من تخزين الكائنات: Cache أمام S3 بالطريقة نفسها، مع حفظ الملفات التي في أسمائها بصمة مدة طويلة، كما شرحنا في S3 من داخل تطبيقك.
التنظيف
docker compose down -v
docker rmi varnish:9.1.0أخطاء شائعة
- تطبيق يرسل Set-Cookie لكل زائر، فتصبح نسبة HIT صفراً دون أي خطأ ظاهر، وفي Nginx مع القفل يصبح الموقع أبطأ مما كان بدون Cache، فراقب نسبة HIT دائماً.
proxy_cache_validأوdefault_ttlلصفحات لا ترسل Cache-Control، فتحفظ صفحات خاصة، والحل Cache-Control صريح من التطبيق لكل صفحة.- حذف كل ال Cookies في
vcl_recvلكل الروابط لرفع نسبة HIT، فتتوقف لوحة الإدارة وتسجيل الدخول، وتسرب صفحات خاصة. - ACL للمسح بشبكة كاملة خلف Proxy أو Docker، فيستطيع أي زائر أن يمسح ال Cache.
- منطقة Cache واحدة في Nginx لعدة مواقع بالمفتاح نفسه، فيتسرب خطأ موقع إلى الآخر.
- عدم تفعيل grace أو proxy_cache_use_stale، فيظهر الخطأ للزوار عند أول عطل في السيرفر الأصلي رغم وجود نسخ صالحة قبل ثوان.
- قراءة أدلة Varnish القديمة على Vinyl Cache دون الانتباه لتغير أسماء البرامج والمجلدات.
الخلاصة
- Vinyl Cache هو مشروع المجتمع الذي كان اسمه Varnish Cache، وVarnish Cache اليوم توزيعة Varnish Software المبنية عليه، والاثنان BSD-2-Clause، واستخدمنا Varnish Cache 9.1.0 وNginx 1.31.6.
- Cache واحد أمام سيرفر يأخذ 150 مللي ثانية خفض 2000 طلب إلى 50 طلباً على السيرفر الأصلي، وp50 من 200 إلى قرابة 2 مللي ثانية، والفرق بين Varnish وNginx في السرعة لا يذكر في هذا الحجم.
- سطر Set-Cookie واحد جعل نسبة HIT صفراً في الاثنين، وفي Nginx مع proxy_cache_lock ارتفع p95 إلى 1.65 ثانية.
- VCL المدمجة في Varnish تمرر الطلبات التي فيها Cookie، فلم تتسرب صفحة الحساب، بينما سطر proxy_cache_valid في Nginx أراها لمستخدم آخر.
- الاثنان قدما الصفحات القديمة عندما توقف السيرفر الأصلي، وVarnish أسرع في رفض ما لا يملكه بفضل ال probe.
- المسح والحظر وESI في Varnish فقط من الاثنين، وACL المسح يجب أن يكون لعنوان محدد وليس للشبكة كلها.
- عدة نقاط حافة تحتاج Shield وترتيباً للمسح وإعداداً واحداً في كل مكان، ولأغلب المواقع تكفي خدمة CDN تجارية أمام Cache واحد.
دليلكيف تعمل شبكة توصيل المحتوى CDN وما الذي تحفظهلماذا يبطؤ موقعك على الزائر البعيد، وكيف تقرب شبكة CDN ملفاتك منه، وكيف تمنعها من حفظ الصفحات الخاصة.
دليلالتخزين المؤقت Caching بالتفصيل: الاستراتيجيات والإبطالكيف تختار نمط التخزين المؤقت المناسب، ومتى تبطل القيمة، وكيف تمنع انهيار قاعدة البيانات عندما ينتهي مفتاح مشهور.
دليلخدمات Cloudflare بكلمات بسيطة وبدائلها مفتوحة المصدرما الذي تقدمه كل خدمة من Cloudflare، وما المتاح منها مجاناً، وما البديل مفتوح المصدر الذي تشغله على سيرفرك.
دليلReverse Proxy أم Load Balancer أم API Gateway؟بماذا تقرر كل طبقة أمام تطبيقك، ومتى تضيف موزع الحمل أو بوابة ال API، مع تجربة تقيس ما تضيفه كل طبقة.سجل التحديثات
- أكتوبر 2026: كتابة المقال، والتجربة على Varnish Cache 9.1.0 من Docker Image الرسمية varnish:9.1.0 وNginx 1.31.6 (nginx:1.31.6-alpine) أمام سيرفر Python 3.13.16، فوق Docker Engine 29.8.0. والمعلومات عن الإصدارات والأسماء والرخص من صفحات Vinyl Cache وvarnish.org وnginx.org كما قرأناها في 12 أكتوبر 2026.