Laravel Vapor یک راهکار میزبانی بدون سرور (Serverless) است که بهطور اختصاصی برای اپلیکیشنهای PHP مبتنی بر Laravel طراحی شده و بر بستر Amazon Web Service قرار دارد. معماری Serverless میتواند راهکار بسیار مناسبی برای APIهای HTTP/REST باشد؛ بهخصوص برای سرویسهایی که نمیخواهند وقتی هیچکس درخواستی ارسال نمیکند، منابع سرور را هدر بدهند. اما وقتی کاربران بارها و بارها همان سوالها را میپرسند، رویکرد Serverless همچنان در حال هدر دادن منابع است؛ چون Lambda مدام دوباره گرم میشود، در میان انبوهی از فراخوانیهای PHP و SQL دستوپا میزند و بدون هیچ دلیل منطقی، همان پاسخها را بارها تولید و ارسال میکند. کش شبکهای HTTP راهحل این مشکل است، و در عمل فقط کافی است آن را فعال کنید.
راهکارهای زیادی برای کش شبکهای HTTP وجود دارد؛ هم راهکارهای self-hosted مثل Varnish و Squid و هم سرویسهای SaaS مثل Fastly و Cloudflare Cache. اما در اینجا نیازی به استفاده از هیچکدام از اینها نداریم، چون Laravel Vapor از AWS API Gateway استفاده میکند و مثل اغلب API Gatewayها، راهکارهای کش شبکهای داخلی خودش را دارد.
با وجود اینکه Laravel Vapor تقریباً همه چیز را فوقالعاده ساده کرده، اما نه در رابط کاربری وب و نه در فایل تنظیمات مبتنی بر YAML، گزینهای برای فعال کردن کش شبکهای HTTP وجود ندارد. بنابراین باید آستینها را بالا بزنیم و خودمان این کار را انجام دهیم.
این زحمت ارزشش را دارد، چون هم هزینهها را کاهش میدهد و هم به کاهش اثر کربنی نرمافزار شما کمک میکند؛ بنابراین فعال نکردنش تقریباً بیادبانه است.
کش HTTP چیست؟
کش HTTP به کلاینتهای API مثل مرورگرها، اپلیکیشنهای موبایل یا سایر سیستمهای بکاند میگوید که آیا لازم است بارها و بارها همان داده را درخواست کنند، یا میتوانند از دادهای که از قبل دارند استفاده کنند. این کار از طریق هدرهای HTTP روی پاسخها انجام میشود که به کلاینت اعلام میکنند تا چه مدت میتواند آن پاسخ را «نگه دارد» یا چگونه بررسی کند که آیا هنوز معتبر است یا نه.
وقتی سرورهای کش HTTP (reverse proxyها) وارد ماجرا میشوند، سطح کار بالاتر میرود؛ به این معنا که حتی اگر کلاینتها زحمت کش کردن را به خودشان ندهند، سرور اپلیکیشن API در صورتی که یک پاسخ کششده قابل استفاده باشد، بیدلیل تحت فشار قرار نمیگیرد. راههای بیشماری برای پیادهسازی این موضوع وجود دارد، اما با گسترش شبکههای توزیع محتوا (CDNها)، حالا سادهتر از همیشه میتوان یک پراکسی کش بین کلاینت و سرور قرار داد و پاسخها را هر جا که ممکن است برای استفاده مجدد ذخیره کرد.

این موضوع کاملاً با ابزارهای کش سمت سرور مثل Redis یا Memcached متفاوت است؛ ابزارهایی که همیشه مستلزم برقراری اتصال HTTP به وبسرور هستند، اما بعد به اپلیکیشن کمک میکنند که از اجرای دوباره کارهایی مثل کوئری زدن به دیتابیس یا عملیاتهای سنگین دیگر جلوگیری کند. در این حالت همچنان ترافیک وب به سمت سرورهای اپلیکیشن وجود دارد، اما این سرورها میتوانند درخواستهای تکراری را کمی سریعتر پاسخ دهند. کش HTTP اما کمک میکند اصلاً درخواستی ارسال نشود (کش سمت کلاینت) یا قبل از رسیدن به وبسرور متوقف شود (کش شبکهای).
در یک محیط Serverless، داشتن یک CDN که در سطح شبکه کش انجام دهد واقعاً مفید است، چون در معماری Serverless هزینه بهازای هر درخواست محاسبه میشود. استفاده از کش HTTP یعنی کاهش ترافیک، کاهش تعداد دفعاتی که Lambda باید بالا بیاید، و کاهش تعداد پاسخهایی که سرور باید تولید کند؛ که همه اینها در نهایت هزینه پهنای باند را هم کاهش میدهد.
مطالب بیشتری درباره اینکه کش HTTP دقیقاً چگونه برای APIها کار میکند و چطور باید APIها را طوری طراحی کرد که کشپذیرتر باشند وجود دارد؛ برای یادگیری بیشتر میتوانید راهنمای اخیر ما با عنوان API Design Basics: Caching را مطالعه کنید. اما این راهنما تمرکز خود را روی فعالسازی کش HTTP برای اپلیکیشنهای Laravel که روی Laravel Vapor میزبانی میشوند گذاشته است.
کش شبکهای با AWS Gateway و CloudFront
AWS یک CDN به نام CloudFront دارد که بهخوبی با AWS API Gateway کار میکند. هر دوی این سرویسها از قبل توسط Vapor راهاندازی شدهاند، پس فقط کافی است آنها را به هم متصل کنیم. استفاده از CloudFront ممکن است در نگاه اول کمی عجیب به نظر برسد، چون CDNهایی مثل CloudFront معمولاً بیشتر با کش کردن تصاویر، CSS و JS برای اپلیکیشنهای فرانتاند شناخته میشوند. اما واقعیت این است که هیچ تفاوتی بین کش کردن JS، CSS و تصاویر با کش کردن یک API مبتنی بر REST/HTTP وجود ندارد.
در یک API مبتنی بر REST، همه چیز «منبع» (resource) محسوب میشود؛ درست مثل تصاویر و سایر فایلها. هر کدام از این منابع ممکن است در طول زمان تغییر کنند و در نتیجه نسخههای مختلفی از آنها برای مدتی در گردش باشند، و این سرور است که قوانین مربوط به میزان قابل قبول بودن کهنگی (staleness) را بر اساس ویژگیهای هر منبع مشخص میکند.
متأسفانه تعداد زیادی از توسعهدهندگان API از استفاده از کش HTTP میترسند، و این ترس از ناشناختهها باعث هدر رفتن پول و منابع طبیعی بدون هیچ دلیل منطقی میشود. روشهای زیادی برای اعتبارسنجی (گرفتن داده جدید فقط در صورت تغییر) و ابطال کش (پاکسازی CDN در شرایط خاص) وجود دارد، بنابراین هیچ دلیلی برای ترس وجود ندارد. ادامه بدهید.
وقتی کش در سطح شبکه فعال شود، میتوانیم منطق کش را با استفاده از middleware کش در فریمورک Laravel اضافه کنیم. این middleware بهصورت خودکار درخواستهای GET و HEAD را با استفاده از هدرهای استاندارد Cache-Control و ETag کش میکند، بدون اینکه لازم باشد خیلی به آن فکر کنیم.
مرحله ۱: ارتقا به AWS API Gateway نسخه ۲
اولین و بهترین کار این است که به آخرین نسخه AWS API Gateway ارتقا دهید. AWS API Gateway v2 هم ارزانتر است و هم سریعتر، بنابراین حتی اگر وسط کار حوصلهتان سر رفت و ادامه این راهنما را فراموش کردید، باز هم ارتقا دادن ایده خوبی است.
فایل vapor.yml را ویرایش کنید و خط gateway-version: 2 را اضافه کنید.
اگر Vapor بهصورت خودکار DNS را مدیریت میکند، این کار به سادگی deploy کردن تغییرات است و همه چیز خودش انجام میشود. یک API جدید ایجاد میشود و اپلیکیشن Vapor شما بهزودی از آن استفاده خواهد کرد.
کسانی که DNS را بهصورت دستی مدیریت میکنند، باید این تغییر را deploy کنند تا جزئیات CNAME جدید در رابط کاربری Laravel Vapor نمایش داده شود؛ اما بهتر است تا مرحله بعدی صبر کنید، چون در غیر این صورت مجبور میشوید دوباره تنظیمات را تغییر دهید.
مرحله ۲: قرار دادن CloudFront در جلوی API Gateway
به بخش CloudFront در پنل AWS بروید و یک distribution جدید بسازید که به AWS API Gateway مربوط به اپلیکیشن شما اشاره کند.
نام DNS alias را برابر با دامنه یا زیردامنهای قرار دهید که قصد استفاده از آن را دارید. در مورد من، این مقدار api.protect.earth است، چون وبسایت اصلی protect.earth فعلاً با SquareSpace اجرا میشود.
در بخش Cache key and origin requests گزینه پیشنهادی (recommended) را انتخاب کنید. با این کار یک منوی کشویی نمایش داده میشود که در آن میتوانید Cache Policy را روی مقدار CachingOptimized قرار دهید.
گزینه Origin Cache Headers را فعال کنید تا تنظیمات Cache-Control که از API ارسال میشوند رعایت شوند (کمی جلوتر بیشتر درباره این موضوع صحبت میکنیم).
پس از ساخته شدن این distribution، مدتی زمان صرف راهاندازی آن میشود و به محض آماده شدن، CloudFront یک زیردامنه شبیه به این نمایش میدهد:
از این مقدار برای بهروزرسانی DNS زیردامنه API خود مثل api.protect.earth استفاده کنید، یا اگر خلاقتر هستید، آن را به یک زیردایرکتوری مثل protect.earth/api پروکسی کنید. هر روشی که انتخاب میکنید، هدف این است که تمام ترافیک API از CloudFront عبور کند تا هر هدر کشی که API ارسال میکند، در تمام edge locationهای انتخابشده توسط CDN رعایت شود.
مرحله ۳: اضافه کردن هدرهای کش به API
چندین روش برای اضافه کردن هدرهای کش وجود دارد.
یکی از روشها این است که هدرها را مستقیماً در پاسخ هر کنترلر قرار دهید، مثلاً به این شکل:
این روش برای سادهترین سناریوها جواب میدهد، اما بخشی از قابلیتهای خودکار را از دست میدهید؛ قابلیتهایی که وقتی شروع به استفاده از ETag یا Last-Modified میکنیم بسیار کاربردی خواهند بود. به همین دلیل توصیه میکنم از middleware داخلی Laravel یعنی
Illuminate\Http\Middleware\SetCacheHeaders استفاده کنید.
مطمئن شوید alias مربوط به cache.headers در فایل app/Http/Kernel.php فعال شده است.
سپس میتوان این middleware را به کل گروه api اعمال کرد، یا آن را بهصورت جداگانه به هر route اضافه نمود، چیزی شبیه به این:
برای دادههایی که احتمال تغییرشان بسیار کم است، میتوان زمان انقضا را واقعاً بالا برد. برای مثال، پروژه Protect Earth دارای پروژههای کاشت درخت در مکانهای مختلف (sites) است و بعید است نام یک site بهطور مکرر تغییر کند. درختها فقط یکبار کاشته میشوند و شاید هر زمستان چند تای دیگر اضافه شود یا اصلاً نیازی به اضافه کردن نباشد، و هیچوقت قرار نیست یک زمین را به جای دیگری منتقل کنیم، بنابراین مختصات جغرافیایی هم تغییر نمیکند. واقعاً لازم نیست وانمود کنیم این دادهها real-time هستند. شروع با یک کش یکروزه میتواند قدم اول معقولی باشد و بهمرور زمان میتوان آن را افزایش داد.
برای دادههایی که ممکن است بیشتر تغییر کنند، میتوانید درباره اعتبارسنجی با استفاده از ETag بیشتر یاد بگیرید تا کشپذیری دادههای قابل تغییر افزایش پیدا کند.
من هیچ مستندات مستقیمی درباره نحوه کار این middleware کش HTTP در Laravel پیدا نکردم، اما از آنجایی که این middleware در واقع یک wrapper روی Symfony HTTP Cache است، میتوان اطلاعات مورد نیاز را از آنجا استخراج کرد. گزینهها به شکل زیر هستند:
تمام این اصطلاحات و کلیدواژهها معنای خود را از RFC 9111: Caching میگیرند، بنابراین میتوان آنها را در ترکیبهای مختلف و متناسب با شرایط خاص استفاده کرد.
ذخیره نکردن پاسخ در هیچ کشی
اگر اطلاعات حساس هستند یا ممکن است شامل دادههای شناسایی شخصی (PII) باشند و نمیخواهید در هیچ پراکسی کش، کش مرورگر یا هیچ جای دیگری ذخیره شوند، کافی است از no_store استفاده کنید.
کش به مدت پنج دقیقه فقط برای این کاربر
گاهی داده لازم نیست عمومی باشد و میتواند به یک کاربر خاص محدود شود؛ کاربری که در هدر Authorization تعریف شده است. این یکی از دلایل خوب استفاده از این روش در APIهاست، بهجای ابداع روشهای عجیب مثل My-Special-API-Key.
این داده هرگز تغییر نخواهد کرد
برخی اطلاعات واقعاً هیچوقت تغییر نمیکنند. مثلاً یک سند که از قبل نسخهبندی شده است، مثل:
اگر این فایل ویرایش شود، نسخه جدیدی ساخته خواهد شد. در این حالت میتوانید یک max-age بسیار بزرگ تنظیم کنید و گزینه immutable را اضافه کنید تا به کلاینت بگویید: «حتی زحمت revalidate کردن را هم به خودت نده، این داده تغییر نخواهد کرد.»
این داده ممکن است تغییر کند
گاهی یک پاسخ نسبتاً حجیم دارید و میدانید کلاینتها مرتب آن را برای بررسی تغییرات poll میکنند. در Protect Earth، این مورد شامل لیست سفارشهای سازمانهاست که هر روز بررسی میشود تا ببینند آیا درخت جدیدی کاشته شده یا نه. من از آنها میخواهم این کار را نکنند، چون گاهی ماهها هیچ درختی کاشته نمیشود (فصل کاشت در بریتانیا از اکتبر تا آوریل است)، اما با این حال این polling انجام میشود و پاسخها هم بزرگ هستند.
میتوانیم از هدرهای کش زیر استفاده کنیم تا یک کش هفتگی تنظیم شود (چون یک هفته زمان مناسبی است) و سپس ETag را اضافه کنیم تا کلاینتها مجبور به revalidate شوند.
middleware داخلی Laravel در هر درخواست یک ETag تولید میکند، و وقتی این ETag از طریق هدر If-None-Match توسط یک HTTP client آگاه به کش ارسال شود، CloudFront دقیقاً میداند باید چه کار کند. اگر ورودیای در کش وجود داشته باشد که با مقدار ETag مطابقت دارد، پاسخ ۳۰۴ بدون body ارسال میشود.
این یک cache hit است، اما CDN میداند که اپلیکیشن کلاینت از قبل پاسخی دارد که میتواند دوباره از آن استفاده کند، بهجای اینکه همان JSON عظیم دوباره ارسال شود و منابع هدر بروند. سرور API هیچ کاری انجام نداده، CDN هم کار زیادی نکرده، و کلاینت هم راضی است.
مرحله ۴: اضافه کردن چند تست
معمولاً ایده خوبی است که چند تست اضافه کنید تا مطمئن شوید هدرهای کش دقیقاً همانطور که انتظار دارید در پاسخها قرار میگیرند؛ چون راههای زیادی برای اعمال این هدرها وجود دارد و ممکن است کسی ناخواسته تنظیمات شما را برهم بزند و هیچکس متوجه نشود تا وقتی که هزینههای سرور در پایان ماه ناگهان جهش کند.
اگر این تستها برایتان کمی ناآشنا به نظر میرسند، راهنمای ما درباره contract testing با Laravel و OpenAPI را ببینید؛ سپس میتوانید این تست را دقیقاً کنار همان تستها اضافه کنید.
مرحله ۵: تحریک API برای بررسی اینکه آیا کار میکند یا نه
اولین کار این است که مطمئن شویم API هنوز در دسترس است.
عالی است؛ ۰.۴۹ ثانیه، و هدر X-Cache نشان میدهد که این یک Miss بوده است.
Miss یعنی هیچ چیزی در کش وجود نداشته که بتواند این درخواست را پاسخ دهد، که کاملاً طبیعی است چون همین چند لحظه پیش کش را فعال کردهایم.
اگر دوباره امتحان کنیم، باید سرعت بهطور محسوسی بیشتر شود.
این بار Cache Hit داریم؛ یعنی چیزی در کش پیدا شده که شرایط لازم را داشته است. چون پاسخ دادن از کش خیلی سریعتر از اذیت کردن وبسرور مبدا است، زمان پاسخ از ۰.۴۹ ثانیه به ۰.۲۹ ثانیه کاهش پیدا کرده است.
یک بهبود قابلتوجه، مخصوصاً با توجه به اینکه من این تست را از وسط کوههای آلپ انجام میدهم، جایی که بهمنها فعال هستند و کل روستا روی برق اضطراری کار میکند.
حالا که این بخش درست کار میکند، وقت آن است که یاد بگیریم چطور فراتر از یک تست سریع، عملکرد API را پایش کنیم.
مرحله ۶: مانیتور کردن Hit Rate در AWS
تمام اپلیکیشنهای کلاینتی که حالا از API استفاده میکنند، از طریق کش شبکه عبور میکنند؛ بنابراین میتوانید وضعیت آنها را از طریق رابط کاربری AWS مانیتور کنید.
به distribution مربوط به CloudFront برای API بروید و روی Cache statistics کلیک کنید. در آنجا، تفکیکی از hit و missها در طول زمان خواهید دید.
تعداد درخواستهایی که hit یا miss میشوند به عنوان hit rate شناخته میشود، و هر hit یعنی یک کار کمتر که API مجبور بوده انجام دهد.

اندازه پاسخها در حالت hit یا miss هم جالب است، چون میتوانید ببینید چه مقدار داده دیگر بهصورت کند توسط API تولید نمیشود و در عوض بهسرعت از کش CloudFront سرو میشود.

قرار گرفتن CloudFront جلوی ترافیک API چند مزیت مفید دیگر هم دارد، از جمله اینکه میتوانیم ببینیم ترافیک ما دقیقاً از کجا میآید.

اینکه ۵۰٪ ترافیک مربوط به باتها و crawlerها باشد، رقم بالایی است. حالا که کاربران واقعی ما بهینه شدهاند و اطلاعات را بسیار سریعتر دریافت میکنند، قدم بعدی برای جلوگیری از فشار بیدلیل به سرورها این خواهد بود که جلوی crawlerها و باتها را بگیریم؛ اما این خودش یک مقاله جداگانه میطلبد.
بردار و اجرا کن
اینکه فقط با کمی Cache-Control وارد دنیای کش API شوید، شروع محکمی است. یاد گرفتن بیشتر درباره ETagها و کل جریان validation به شما کمک میکند تا درِ کش کردن اسنادی را باز کنید که ممکن است در طول زمان تغییر کنند.
خیلیها دستکم میگیرند که چه بخش بزرگی از API آنها واقعاً قابل کش شدن است، اما ارزشش را دارد که روی آن عمیق شوید؛ چون کاهش درخواستهای تکراری باعث میشود:
-
بار روی سرور کاهش پیدا کند (و هزینههای هاستینگ کمتر شود).
-
ترافیک شبکه کم شود (و هزینه پهنای باند پایین بیاید).
-
مصرف انرژی کاهش یابد (و اثرات زیستمحیطی کمتر شود).
تصور کنید میلیونها کاربر دیگر درخواستهای غیرضروری برای دادههای بدون تغییر ارسال نکنند. طراحی APIها بهصورت cache-friendly از همان ابتدا، نهتنها به محیط زیست کمک میکند، بلکه منجر به APIهایی سریعتر، کارآمدتر و کاربرپسندتر میشود. یک برد-برد واقعی: عملکرد بهتر برای کاربران، هزینه عملیاتی کمتر برای ارائهدهندگان، و اثر مثبت برای سیاره زمین.
همچنین، حالا که هدرهای کش را اضافه کردهاید، یک مزیت دیگر هم در کاهش ترافیک دارید:
مرورگرها و اپلیکیشنهای HTTP که کش را پشتیبانی میکنند، حتی زحمت ارسال درخواست HTTP برای منبعی که از قبل در کش محلی خود دارند را هم به خود نمیدهند.
