رفتار کش CDN: چه چیزی کش می‌شود و چطور باطل کنیم

راهنمای دقیق رفتار کش CDN: کدام پاسخ‌ها پیش‌فرض کش می‌شوند، اثر هدرهای Cache-Control و Vary، و روش باطل‌سازی گزینشی بدون پاک‌کردن کل کش.

۷ دقیقه به‌روزرسانی ۱۹ آبان ۱۴۰۵

سایت را روی CDN گذاشته‌اید، فایل CSS را عوض کرده‌اید، اما مرورگر کاربر هنوز نسخهٔ قبلی را نشان می‌دهد. یا بدتر: صفحهٔ محصول بعد از تغییر قیمت، برای بعضی کاربران عدد قدیمی را برمی‌گرداند و برای بعضی عدد جدید. مشکل معمولاً در CDN نیست؛ در این است که نمی‌دانید لبهٔ شبکه (edge) دقیقاً چه چیزی را با چه کلیدی ذخیره کرده. این متن همان چیزی است که باید بدانید تا رفتار کش CDN را پیش‌بینی کنید، نه اینکه با حدس و آزمایش سراغش بروید.

پیش‌فرض CDN چه چیزی را کش می‌کند

پاسخ کوتاه: کمتر از آنچه فکر می‌کنید. اکثر CDNها فقط پاسخ‌های GET و HEAD را کش می‌کنند و هر پاسخ با کد وضعیت 200، 203، 204، 206، 300، 301، 404، 410 را به‌عنوان «کش‌شدنی» می‌شناسند. پاسخ‌های POST، PUT، DELETE هرگز کش نمی‌شوند، حتی اگر هدر Cache-Control بگذارید. این را خیلی‌ها نمی‌دانند و ساعت‌ها دنبال باگ می‌گردند.

نکتهٔ دوم که بیشتر از اولی دردسر می‌سازد: اگر پاسخ هیچ هدر کشی نداشته باشد، CDN با یک سیاست پیش‌فرض تصمیم می‌گیرد. این سیاست بین ارائه‌دهنده‌ها فرق می‌کند. بعضی‌ها پاسخ بدون هدر را اصلاً کش نمی‌کنند؛ بعضی‌ها با یک TTL پیش‌فرض کوتاه کش می‌کنند. اگر روی رفتار پیش‌فرض حساب باز کنید، روزی که ارائه‌دهنده سیاستش را عوض کند، سایت شما بی‌خبر تغییر رفتار می‌دهد.

کلید کش فقط URL نیست

کلید پیش‌فرض تقریباً همیشه شامل متد و مسیر و رشتهٔ کوئری است. یعنی /product?id=12 و /product?id=13 دو ورودی جدا هستند، اما /product?id=12&utm_source=x هم یک ورودی سوم می‌شود. اینجاست که حجم کش شما بی‌دلیل چند برابر می‌شود و hit ratio می‌افتد. راه‌حل استاندارد، نادیده‌گرفتن پارامترهای بازاریابی در کلید کش است؛ تقریباً همهٔ CDNها برای این کار یک لیست query string قابل تنظیم دارند.

هدر Vary کلید را پیچیده‌تر می‌کند. اگر پاسخ شما Vary: Accept-Encoding دارد، نسخهٔ gzip و brotli جدا ذخیره می‌شوند. اگر Vary: User-Agent بگذارید، عملاً کش را غیرفعال کرده‌اید، چون هر مرورگر کلید متفاوتی می‌سازد و hit ratio به نزدیک صفر می‌رسد. من در عمل فقط دو مقدار را روی Vary می‌پسندم: Accept-Encoding و در موارد خاص Accept-Language. بقیه را حذف کنید.

هدرها دقیقاً چطور تصمیم می‌گیرند

ترتیب اولویت را حفظ کنید، چون CDNها همین ترتیب را پیاده می‌کنند:

  1. Cache-Control: no-store بر همه چیز مقدم است. پاسخ نه در لبه و نه در مرورگر ذخیره نمی‌شود.
  2. Cache-Control: private یعنی فقط مرورگر می‌تواند کش کند، لبه نه. برای صفحاتی که به کوکی کاربر وابسته‌اند.
  3. Cache-Control: public, max-age=31536000, immutable برای فایل‌های نسخه‌دار مثل app.4f2c1a.js.
  4. s-maxage فقط برای کش اشتراکی (لبه) است و بر max-age اولویت دارد.
  5. Expires فقط وقتی استفاده می‌شود که Cache-Control نباشد. اگر هر دو باشند، Cache-Control برنده است.

یک خط واقعی که برای فایل‌های استاتیک نسخه‌دار می‌نویسم:

Cache-Control: public, max-age=31536000, s-maxage=31536000, immutable

و برای HTML که ممکن است هر لحظه عوض شود:

Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=300, stale-if-error=86400

عدد s-maxage=60 یعنی لبه حداکثر یک دقیقه نسخهٔ قدیمی را سرو می‌کند. stale-while-revalidate=300 یعنی تا پنج دقیقهٔ بعد از انقضا، پاسخ قدیمی را فوری بده و در پس‌زمینه تازه کن. stale-if-error=86400 یعنی اگر origin یک روز کامل خطا داد، لبه همان نسخهٔ قدیمی را سرو کند. این دو دایرکتیو در زمان قطعی origin تفاوت بین «سایت بالا» و «صفحهٔ ۵۰۲» است.

این‌جا اشتباه می‌کنند

رایج‌ترین اشتباهی که دیده‌ام: گذاشتن Cache-Control: no-cache با این تصور که یعنی «کش نکن». این هدر یعنی «کش کن، اما قبل از هر بار استفاده از origin بپرس». نتیجه‌اش این است که لبه برای هر درخواست یک رفت‌وبرگشت به origin می‌زند، TTFB بالا می‌رود و بار origin چند برابر می‌شود، در حالی که تیم فکر می‌کند کش را کامل خاموش کرده. اگر واقعاً نمی‌خواهید کش شود، no-store بنویسید. تفاوت این دو کلمه، تفاوت بین یک CDN بی‌فایده و یک CDN کارآمد است.

اشتباه دوم: Set-Cookie روی پاسخ‌های استاتیک. بعضی CDNها به‌محض دیدن Set-Cookie در پاسخ، آن را غیرقابل‌کش علامت می‌زنند. اگر روی فایل‌های JS و CSS کوکی ست می‌کنید، دارید کش را با دست خودتان می‌کشید.

باطل‌سازی گزینشی، نه پاک‌کردن کل کش

وقتی محتوایی عوض می‌شود، دو راه دارید. راه اول purge کامل است: همه چیز از لبه پاک می‌شود و موج درخواست‌ها یک‌باره به origin می‌خورد. اگر سایت شما ترافیک بالایی دارد، این کار origin را زمین می‌زند. راه دوم باطل‌سازی گزینشی است: فقط همان مسیر یا همان الگو را پاک کنید.

اکثر CDNها سه نوع purge دارند: بر اساس URL دقیق، بر اساس prefix یا wildcard، و بر اساس tag یا surrogate key. نوع سوم دقیق‌ترین است. اگر در پاسخ هدر Cache-Tag: product-12, category-shoes بگذارید، بعداً می‌توانید فقط product-12 را باطل کنید، بدون اینکه به بقیهٔ دسته‌بندی دست بزنید. این تنها روشی است که در فروشگاه‌های بزرگ واقعاً جواب می‌دهد.

روشدقتهزینهکجا مناسب است
Purge کاملصفرموج سنگین به originتغییر layout سراسری، به‌ندرت
Purge بر اساس URLبالاسبکچند صفحهٔ مشخص
Purge بر اساس prefixمتوسطمتوسطکل یک مسیر مثل /blog/*
Purge بر اساس tagبالانیاز به تغییر کدفروشگاه، محتوای وابسته به داده

یک محدودیت واقعی که کمتر گفته می‌شود: باطل‌سازی فوری نیست. بین صفر تا چند ثانیه طول می‌کشد تا دستور به همهٔ لبه‌ها برسد، و در شبکه‌های بزرگ جغرافیایی این عدد می‌تواند بیشتر شود. اگر منطق کسب‌وکار شما به «بلافاصله» وابسته است، به کش لبه تکیه نکنید؛ آن مسیر را از کش خارج کنید.

الگوی نسخه‌گذاری که باطل‌سازی را کم می‌کند

بهترین باطل‌سازی، باطل‌سازی‌ای است که لازم نباشد. نام فایل را به محتوایش گره بزنید: main.a91f3c.css به‌جای main.css. حالا هر تغییر یک URL جدید می‌سازد و فایل قدیمی خودش با TTL یک‌ساله منقضی می‌شود. HTML را کوتاه کش کنید، دارایی‌ها را طولانی. این ترکیب هم hit ratio بالا می‌دهد و هم باطل‌سازی را به یک عملیات نادر تبدیل می‌کند.

اگر زیرساخت شما روی سرور ابری سرورنت یا زیرساخت ابری (IaaS) است، این الگو را می‌توانید با یک لود بالانسر با health check و SSL جلوی origin ترکیب کنید تا موج purge کامل هم origin را زمین نزند. برای سرو کردن دارایی‌های نسخه‌دار هم فضای ذخیره‌سازی آبجکت گزینهٔ منطقی‌تری از دیسک وب‌سرور است، چون هدرها را یک‌بار ست می‌کنید و دیگر دست نمی‌زنید.

چطور رفتار واقعی کش را اندازه بگیریم

به هدر پاسخ نگاه کنید، نه به حرف مستندات. این دستور را بزنید:

curl -sSI -H 'Accept-Encoding: gzip' https://example.com/main.css | grep -iE 'cache-control|age|x-cache|cf-cache|vary'

هدر Age عدد طلایی است. اگر Age: 0 باشد، پاسخ تازه از origin آمده. اگر Age: 47 باشد، این پاسخ ۴۷ ثانیه در لبه نشسته و شما hit گرفته‌اید. دو بار پشت سر هم درخواست بزنید؛ بار دوم باید Age بزرگ‌تر بدهد. اگر نداد، کش کار نمی‌کند و باید دنبال Set-Cookie یا Vary اضافی بگردید.

برای سنجش hit ratio واقعی، به داشبورد CDN تکیه نکنید؛ لاگ origin را بشمارید. اگر تعداد درخواست‌های رسیده به origin بعد از فعال‌کردن کش تغییری نکرد، احتمالاً همهٔ ترافیک از کش عبور می‌کند بدون اینکه ذخیره شود. این وضعیت را با مانیتورینگ ابری و هشدار روی نرخ درخواست origin قابل تشخیص است؛ یک هشدار ساده روی جهش ناگهانی این عدد، خیلی از قطعی‌ها را قبل از وقوع می‌گیرد.

اگر بعد از تغییر تنظیمات کش، پیکربندی سرور را هم عوض می‌کنید، قبل از هر تغییر پرخطر یک اسنپ‌شات از فضای ابری بگیرید. برگشتن از یک اسنپ‌شات چند دقیقه است؛ بازسازی دستی پیکربندی چند ساعت.

پرسش‌های پرتکرار

تفاوت no-cache و no-store در رفتار کش CDN چیست؟

no-cache اجازهٔ ذخیره می‌دهد اما هر بار قبل از استفاده باید از origin اعتبارسنجی شود، پس ترافیک origin کم نمی‌شود. no-store اصلاً ذخیره نمی‌کند و هیچ نسخه‌ای در لبه یا مرورگر نمی‌ماند. برای دادهٔ حساس مثل صفحهٔ پرداخت، no-store درست است.

چرا بعد از purge، بعضی کاربران هنوز نسخهٔ قدیمی را می‌بینند؟

چون purge فقط کش لبه را پاک می‌کند، نه کش مرورگر کاربر. اگر فایل با max-age=31536000 در مرورگر ذخیره شده باشد، تا انقضا همان نسخه را می‌بیند. راه‌حل دائمی، تغییر نام فایل بر اساس هش محتواست، نه purge مکرر.

آیا CDN پاسخ‌های 404 را هم کش می‌کند؟

بله، در بسیاری از پیاده‌سازی‌ها پاسخ 404 کش‌شدنی است. اگر صفحه‌ای را موقتاً حذف کرده‌اید و بعد برگردانده‌اید، ممکن است لبه همچنان 404 بدهد. برای جلوگیری، روی پاسخ‌های 404 یک Cache-Control کوتاه مثل max-age=60 بگذارید.

هدر Vary چطور hit ratio را خراب می‌کند؟

هر مقداری که در Vary بیاید بخشی از کلید کش می‌شود. Vary: User-Agent عملاً برای هر نسخهٔ مرورگر یک ورودی جدا می‌سازد و hit ratio را به نزدیک صفر می‌رساند. تا جای ممکن فقط Accept-Encoding را نگه دارید.

قدم بعدی ساده است: همین حالا با curl -sSI هدرهای یک فایل استاتیک و یک صفحهٔ HTML را بگیرید و Age و Cache-Control را کنار هم بگذارید. اگر عدد Age در درخواست دوم بزرگ نشد، مشکل کش شما همان‌جاست، نه در تنظیمات CDN.

آیا این مطلب برایتان مفید بود؟