مقیاس گذاری Next.js با کنترل کننده کش Redis

فرض کنید دهها نمونه Next.js در حال تولید دارید که در خوشه Kubernetes شما اجرا میشوند. اکثر صفحات شما از بازسازی استاتیک افزایشی (ISR) استفاده می کنند که به صفحات اجازه می دهد در اولین بازدید کاربر تولید و در ذخیره سازی فایل ذخیره شوند. درخواستهای بعدی به همان صفحه بلافاصله از نسخه ذخیرهشده، دور زدن بازسازی، حداقل تا زمانی که دوره تأیید مجدد تنظیم شده منقضی شود، ارائه میشوند. خوب به نظر می رسد، درست است؟
با این تفاوت که خیلی خوب مقیاس نمی شود.
مسئله
داده ها تولید می شوند اما هرگز پاک نمی شوند. علاوه بر این، هر نمونه از NextJS از داده های مشابه، تکراری و ایزوله استفاده می کند. در اینجا در Odrabiamy.pl، متوجه شدیم که همه نمونههای k8s ما هر کدام تا 30 گیگابایت فضای ذخیرهسازی را اشغال میکنند. این حجم عظیمی از داده برای یک گره است، اما اگر 20 گره داشته باشیم چه؟ این 600 گیگابایت داده است که می تواند به راحتی به اشتراک گذاشته شود.
راه حل های امکان پذیر
ما سعی کردیم راه حلی برای این مشکل پیدا کنیم و اینها گزینه های ما بودند:
-
از یک حجم دائمی Kubernetes استفاده کنید و داخل آن را به اشتراک بگذارید
.next
فهرست راهنما، اما معایب خود را دارد:- هر پاد دسترسی خواندن/نوشتن دارد که میتواند مشکلات بزرگی در شرایط مسابقه بین پادها ایجاد کند. برای اطمینان از اینکه همه چیز پایدار است، باید کنترلر کش خود را بنویسیم.
- مکانیزمی برای کپی کردن مورد نیاز است
.next
دایرکتوری به یک حجم مشترک در حین استقرار و پس از اینکه دیگر نیازی به آن نیست، آن را حذف کنید.
- از Redis و پیکربندی Next.js موجود برای ذخیره تمام صفحات تولید شده استفاده کنید – که از نظر زمان لازم برای اجرا و پیچیدگی راه حل برای ما عالی بود.
Next.js و Redis
به طور پیش فرض، Next.js از یک کنترل کننده کش مبتنی بر فایل استفاده می کند. با این حال، Vercel یک گزینه پیکربندی جدید برای سفارشی کردن آن منتشر کرده است. برای انجام این کار، ما باید یک کنترل کننده کش سفارشی را در خود بارگذاری کنیم next.config.js
:
cacheHandler:
process.env.NODE_ENV === 'production'
? require.resolve('./cache-handler.cjs')
: undefined,
ما فقط آن را در محیط تولید بارگذاری می کنیم، زیرا در حالت توسعه لازم نیست. اکنون زمان اجرای آن است cache-handler.cjs
فایل. (توجه: بسته به پیکربندی npm خود، ممکن است لازم باشد این را با استفاده از ماژولهای ES بنویسید.)
ما از بسته @neshca/cache-handler استفاده خواهیم کرد، که کتابخانه ای است که با کنترلرهای از پیش نوشته شده ارائه می شود. طرح این است که:
- Redis را به عنوان کنترل کننده اصلی حافظه پنهان تنظیم کنید
- به عنوان یک پشتیبان، از کش LRU استفاده کنید (حداقل استفاده شده در حافظه نهان)
پیاده سازی اساسی به شرح زیر خواهد بود:
// cache-handler.cjs
const createClient = require('redis').createClient;
const CacheHandler = require('@neshca/cache-handler').CacheHandler;
const createLruCache = require('@neshca/cache-handler/local-lru').default;
const createRedisCache = require('@neshca/cache-handler/redis-strings').default;
CacheHandler.onCreation(async () => {
const localCache = createLruCache({
maxItemsNumber: 10000,
maxItemSizeBytes: 1024 * 1024 * 250, // Limit to 250 MB
});
let redisCache;
if (!process.env.REDIS_URL) {
console.warn('REDIS_URL env is not set, using local cache only.');
} else {
try {
const client = createClient({
url: process.env.REDIS_URL,
});
client.on('error', (error) => {
console.error('Redis error', error);
});
await client.connect();
redisCache = createRedisCache({
client,
keyPrefix: `next-shared-cache-${process.env.NEXT_PUBLIC_BUILD_NUMBER}:`,
// timeout for the Redis client operations like `get` and `set`
// after this timeout, the operation will be considered failed and the `localCache` will be used
timeoutMs: 5000,
});
} catch (error) {
console.log(
'Failed to initialize Redis cache, using local cache only.',
error,
);
}
}
return {
handlers: [redisCache, localCache],
ttl: {
// This value is also used as revalidation time for every ISR site
defaultStaleAge: process.env.NEXT_PUBLIC_CACHE_IN_SECONDS,
// This makes sure, that resources without set revalidation time aren't stored infinitely in Redis
estimateExpireAge: (staleAge) => staleAge,
},
};
});
module.exports = CacheHandler;
اما در اینجا یک هشدار جالب وجود دارد. اگر Redis در هنگام شروع سرور در دسترس نباشد، چه؟ خط await client.connect();
شکست می خورد و صفحه با تاخیر بارگیری می شود. اما به همین دلیل، Next.js سعی می کند هر بار که شخصی از هر صفحه ای بازدید می کند، یک CacheHandler جدید را مقداردهی اولیه کند.
به همین دلیل است که تصمیم گرفتیم در چنین مواردی فقط از LRU استفاده کنیم. با این حال، راه حل این مشکل بی اهمیت نیست، همانطور که createClient
خطا نمی کند؛ آن را فقط در پاسخ به تماس عمل می کند. بنابراین یک راه حل مورد نیاز است:
...
let isReady = false;
const client = createClient({
url: process.env.REDIS_URL,
socket: {
reconnectStrategy: () => (isReady ? 5000 : false),
},
client.on('error', (error) => {
console.error('Redis error', error);
});
client.on('ready', () => {
isReady = true;
});
await client.connect();
...
این تضمین می کند که در صورت عدم موفقیت اتصال اولیه، Next.js سعی در اتصال مجدد نخواهد داشت. در موارد دیگر، اتصال مجدد مورد نظر است و مانند یک افسون عمل می کند.
عملکرد و ثبات
تست های عملکرد ما نشان داد که استفاده از CPU حدود 2٪ افزایش یافته است، اما زمان پاسخگویی ثابت باقی مانده است.
در Odrabiamy، هدف ما نه تنها داشتن یک راهحل کارآمد، بلکه داشتن لایههای زیرساخت مستقل است، به طوری که هرگونه خرابی بر عملکرد کل برنامه تأثیر نمیگذارد. این جایی است که حافظه نهان کمترین استفاده اخیر (LRU) به عنوان یک مکانیسم بازگشتی حیاتی وارد بازی می شود. در طول آزمایشهای عملکردی، چندین بار Redis را بهصورت دستی خاتمه دادیم که نتیجه آن شد توقف صفر. انتقال بین Redis و حافظه نهان LRU به قدری یکپارچه بود که حتی در نمودارهای عملکرد ما قابل توجه نبود.
نتیجه
در مورد چندین نمونه Next.js که روی یک خوشه Kubernetes اجرا میشوند، بهتر است حافظه پنهان مبتنی بر سیستم فایل پیشفرض را با یک Redis جایگزین کنید. این می تواند منابع ذخیره سازی شما را بدون هیچ گونه خطر و کاهش عملکرد آزاد کند. تنظیم این پیکربندی بسیار آسان است و قبلاً در محیط تولید ما آزمایش شده بود.