محمد قتيبة شيخانيأبني. أستكشف. أشارك

17من 20

أساسيات الإنترنت والشبكات

التخزين المؤقت (Caching): الطبقات التي تجعل الويب سريعًا

غلاف المقال: رسمٌ بخطوطٍ رقيقة على أرضٍ كريميّة. إلى اليمين علبةٌ بالأحمر الترابيّ مكتوبٌ فيها «نسخةٌ قريبة» وتحتها 9 من 10، يصلها سهمٌ أحمر قصير. وإلى اليسار علبةٌ مكتوبٌ فيها «الأصل» وتحتها 1 من 10، يصلها من العلبة الأولى سهمٌ طويلٌ رقيق مكتوبٌ فوقه أنّ واحدًا يقطع المسافة كلّها. وفي أسفل الغلاف عنوان المقال وسطرٌ تحته، واسم الكاتب وعلامته.

في المقال السابق عن «نقاط تبادل الإنترنت» رأيتَ أن شركات المحتوى تضع نسخًا من محتواها قريبةً من المستخدمين لتصل أسرع. هذه فكرةٌ واحدةٌ من تطبيقات مبدأٍ هو على الأرجح أهمّ تقنيّة أداءٍ في الحوسبة كلّها: «التخزين المؤقت» (Caching). فما هو، ولماذا يجعل الويب سريعًا إلى هذا الحدّ؟

هذا المقال يفتح التخزين المؤقت من جذره: ما هو ولماذا، ثمّ الطبقات التي يعمل عليها من جهازك إلى الخادم البعيد، ثمّ المفاهيم الصعبة التي تحكمه (الطزاجة، والتحقّق، والإبطال، والإخلاء)، ثمّ طبقتان في متصفّحك قلّ من يذكرهما، وأخيرًا واقعه العمليّ لمن يبني المواقع.

ما التخزين المؤقت، ولماذا

التخزين المؤقت مبدأٌ بسيطٌ عبقريّ: احتفظ بنسخةٍ من الأشياء التي تستعملها كثيرًا في مكانٍ قريبٍ منك، لتبلغها بسرعةٍ في المرّة التالية بدل أن تعيد جلبها من بعيد.[3] تخيّل أنك تقرأ فصلًا من كتابٍ في المكتبة المركزية كلّ يوم: من العبث أن تقطع المسافة الطويلة في كل مرّة، فتحتفظ بنسخةٍ على طاولتك. هذا هو التخزين المؤقت.

وبدونه، يعيد متصفّحك تنزيل كل شعارٍ وملفّ تنسيقٍ وبرنامجٍ في كل زيارةٍ ولو لم يتغيّر، فيُهدَر وقتك ونطاقك، ويُثقَل الخادم بلا داعٍ. أمّا فوائده فأربع: يقلّل زمن الاستجابة (النسخة الأقرب أسرع)، ويخفّف الحمل عن الخادم الأصليّ (طلباتٌ أقلّ تصله)، ويوفّر النطاق والكلفة، ويتيح للموقع أن يخدم أعدادًا أكبر دون أن ينهار. والفرق في زمن الوصول هائل: قراءة ملفٍّ من قرص جهازك تتمّ في أجزاءٍ ضئيلةٍ من الثانية، بينما جلبه من خادمٍ في قارّةٍ أخرى قد يكلّف عشرات أو مئات أضعاف ذلك. حين تتكرّر هذه الوفورات في كل أصلٍ وكل طلب، تصنع الفرقَ بين موقعٍ يبدو فوريًّا وآخر يبدو متثاقلًا. لهذا يكاد لا يخلو نظامٌ سريعٌ من التخزين المؤقت في مكانٍ ما.

وهو مبدأٌ يتجاوز الويب إلى كل طبقات الحوسبة: فلمعالج حاسوبك ذواكر مؤقتةٌ صغيرةٌ سريعة (تُسمّى L1 وL2) يحتفظ فيها بما يحتاجه قريبًا من نواته، ولنظام التشغيل ذاكرةٌ يخزّن فيها ملفّات القرص المستعملة. ولماذا ينجح هذا أصلًا؟ لظاهرةٍ تُسمّى «محلّية المرجع»: ما استُعمل قريبًا يُرجَّح أن يُستعمل ثانيةً قريبًا (محلّيةٌ زمنيّة)، وما جاوره يُرجَّح أن يُطلب معه (محلّيةٌ مكانيّة). فالتخزين المؤقت رهانٌ على أن الماضي القريب دليلٌ على المستقبل القريب، ورهانٌ يربح في الغالب.

صورةٌ مكبّرةٌ لسطح رقاقة معالجٍ عارية: نصفها الأيسر مساحاتٌ زرقاء واسعةٌ منتظمة مقسّمةٌ إلى مستطيلاتٍ متشابهة، ونصفها الأيمن نسيجٌ ذهبيٌّ دقيقٌ غير منتظم، ويحيط بالرقاقة إطارٌ من وصلاتٍ صغيرة.
سطح معالج «بنتيوم 3» (نواة Tualatin) عاريًا. المساحات الزرقاء المنتظمة إلى اليسار هي ذاكرته المؤقتة من المستوى الثاني، والنسيج الذهبيّ إلى اليمين هو المنطق الذي يحسب. قِسِ المساحة بعينك: في هذا المعالج تأخذ الذاكرة المؤقتة من الأرض قدرَ ما يأخذه الحساب.الصورة: Martijn Boer؛ ملكيّة عامّة، عبر Wikimedia Commons.

طبقات التخزين المؤقت: من جهازك إلى الأصل

ليس التخزين المؤقت مكانًا واحدًا، بل سلسلة طبقاتٍ يمرّ بها الطلب من جهازك إلى الخادم الأصليّ. والقاعدة الجامعة: إذا وُجدت النسخة في أيّ طبقةٍ قريبة، توقّف الطلب عندها ولم يصل إلى ما خلفها الأبطأ. ولنرها من الأقرب إلى الأبعد:

  • ذاكرة المتصفّح: الأقرب إليك، على قرص جهازك. تحميل ملفٍّ منها أسرع بآلاف المرّات من جلبه عبر الإنترنت.
  • ذاكرة DNS: نسخةٌ محفوظةٌ من ترجمة الاسم إلى عنوان (مرّت بك في مقال DNS) تُجنّبك استعلامًا كاملًا.
  • الوسطاء في الطريق: ذاكراتٌ في شبكة مزوّدك أو مؤسّستك. وهذه الطبقة كادت تزول، ولها تفصيلٌ بعد قليل.
  • حافّة شبكة التوصيل (CDN): نسخٌ في خوادمَ قريبةٍ منك جغرافيًّا، وهي موضوع المقال التالي.
  • الوكيل العكسيّ عند الخادم: ذاكرةٌ أمام التطبيق (مثل Varnish أو Nginx) تخزّن صفحاتٍ كاملةً جاهزة.
  • ذاكرة الكائنات والكود: داخل الخادم، تُخزَّن نتائج الحسابات والاستعلامات (في Redis أو Memcached) والكود المترجَم.

فإذا طلبت صفحةً وُجدت في ذاكرة متصفّحك، انتهى الأمر فورًا دون أن يلمس الإنترنت. وإن لم تكن، فقد تجدها حافّة CDN قريبة. وإن لم تكن، فقد يخدمها الوكيل العكسيّ جاهزةً دون أن يُتعِب التطبيق. وكلّما عمُقت الطبقة التي وصلها الطلب، بطؤ وكلّف. هدف التصميم الجيّد أن يُجاب أكثر الطلبات في الطبقات القريبة.

ولنرَ ذلك في زيارتين. في الزيارة الأولى لموقعٍ، لا توجد نسخةٌ في أيّ طبقة، فيقطع الطلب الطريق كلّه إلى الأصل، الذي يبني الصفحة ويرسلها، وفي طريق العودة تحتفظ الطبقات بنسخٍ منها. في الزيارة الثانية، يجد متصفّحك ملفّات التنسيق والصور في ذاكرته المحلّية فلا يطلبها أصلًا، وما يحتاج جلبه قد يجده في حافّةٍ قريبة. فما بدا بطيئًا أوّل مرّةٍ صار فوريًّا، والفضل كلّه لنسخٍ تراكمت في الطبقات.

شكل 1 أين يتوقّف الطلب في الزيارة الأولى وفي الثانية الطلب نفسه مرسومٌ مرّتين على الطبقات الأربع ذاتها: المتصفّح، ثمّ حافّة الشبكة، ثمّ الوكيل العكسيّ، ثمّ الأصل. في الزيارة الأولى لا توجد نسخةٌ في أيّ طبقة، فيقطع الطلب الوصلات كلّها حتى يبلغ الأصل. وفي الزيارة الثانية، وهي المشدَّدة، تكون النسخة عند المتصفّح، فينتهي الطلب عند الطبقة الأولى ولا يخرج إلى الشبكة، وتبقى الطبقات الثلاث الباقية قائمةً لم يصلها شيء. والطبقات هي هي في الحالتين، والفرق في طول ما قُطِع منها. الزيارة الأولى: لا نسخة في أيّ طبقة المتصفّح حافّة الشبكة وكيلٌ عكسيّ الأصل فقطع الوصلات كلّها حتى بلغ الأصل الزيارة الثانية: النسخة عند المتصفّح المتصفّح حافّة الشبكة وكيلٌ عكسيّ الأصل فانتهى عند الطبقة الأولى ولم يخرج إلى الشبكة أين يتوقّف الطلب في الزيارة الأولى وفي الثانية عمودان للطبقات الأربع نفسها: المتصفّح، ثمّ حافّة الشبكة، ثمّ الوكيل العكسيّ، ثمّ الأصل. العمود الأيمن هو الزيارة الأولى، ووصلاته موصولةٌ من أعلى إلى أسفل حتى الأصل. والعمود الأيسر هو الزيارة الثانية، وهو المشدَّد، وفيه وصلةٌ واحدةٌ إلى الطبقة الأولى ثمّ لا شيء، فتبقى الطبقات الثلاث الباقية قائمةً لم يصلها الطلب. الزيارة الأولى الزيارة الثانية المتصفّح المتصفّح حافّة الشبكة حافّة الشبكة وكيلٌ عكسيّ وكيلٌ عكسيّ الأصل الأصل قطعها كلّها توقّف عند الأولى
الزيارة الأولى تقطع الطبقات كلّها إلى الأصل، والثانية تتوقّف عند أوّل طبقةٍ تجد فيها نسختها. والطلب واحدٌ في الحالتين؛ ما يختلف هو الموضع الذي ينتهي عنده.

وكيف تمتلئ هذه الطبقات؟ الأصل (Origin) هو مصدر الحقيقة الوحيد، وكلّ نسخةٍ مشتقّةٌ منه. وأكثر الذواكر «تسحب» النسخة عند أوّل طلبٍ لها (فيدفع أوّل زائرٍ ثمن البناء، ويستفيد من بعده)، وبعضها «يُدفَع» إليها المحتوى مسبقًا قبل وصول الزوّار. ويلجأ بعض المواقع قبل أحداثٍ كبيرةٍ إلى «تسخين الذاكرة»: ملؤها بالمحتوى المتوقّع طلبه سلفًا، حتى لا يصطدم أوّل المستخدمين بذاكرةٍ فارغة.

طبقةٌ زالت، وطبقةٌ حلّت مكانها

يتحدّث كثيرٌ من الشروح عن «وكيلٍ وسيط» في شبكة المزوّد يخزّن المحتوى للجميع. وقد زال هذا عمليًّا. فمع تشفير الاتّصال لم يبقَ للوسيط أن يرى ما يمرّ به، فصار يمرّر الاستجابة تمريرًا لا يخزّنها؛ وتقول توثيقات موزيلا ذلك صريحًا: الوسطاء في الطريق لم يبقَ لهم في كثيرٍ من الحالات إلّا أن ينقلوا الاستجابة، ولا يعملون عمل الذاكرة.[1] والتقسيم الأدقّ اليوم يقوم على المِلكيّة لا على المسافة: ذاكرةٌ خاصّةٌ بمستخدمٍ واحدٍ هي متصفّحه، وذاكرةٌ مُدارة ينشرها صاحب الموقع بنفسه ليخفّف عن أصله (وكيلٌ عكسيّ، أو حافّة CDN، أو عامل خدمةٍ سيأتي ذكره).[1] الأولى لا تملكها ولا تطهّرها، والثانية تملكها وتضبطها وتُسقِط ما فيها متى شئت. وهذا الفرق، لا المسافة، هو ما يحدّد أيّ طبقةٍ تستطيع أن تعتمد عليها.

ونصيحةٌ قديمةٌ بطلت هنا أيضًا. كان يُقال: حمّل مكتبةً شائعةً من شبكة توصيلٍ عامّةٍ يستعملها الجميع، فلعلّ زائرك يحملها في ذاكرته من موقعٍ آخر فتصله بلا تنزيل. هذا لم يعد يعمل. فمنذ الإصدار 86 من كروم صارت ذاكرة المتصفّح مقسَّمة: للنسخة مفتاحٌ يضمّ الموقع الأعلى الذي تزوره، فلا يرى موقعٌ نسخةَ موقعٍ آخر ولو كان الملفّ نفسه وبالعنوان نفسه.[6] والسبب أمنيّ: بغير التقسيم يستطيع موقعٌ أن يستدلّ على المواقع التي زرتَها بقياس سرعة تحميل ملفٍّ منها. وقد قِيس الثمن ونُشر: ارتفعت نسبة الإخفاق نحو 3.6%، وارتفع ما يُجلَب من الشبكة نحو 4%، ولم يتأثّر «أوّل رسمٍ للمحتوى» إلّا بنحو 0.3%.[6] ثمنٌ صغيرٌ في مقابل ثقبٍ في الخصوصيّة أُغلق.

الطزاجة والتحقّق: كيف يعرف المتصفّح متى يستعمل النسخة

لكن كيف تعرف الطبقةُ أن نسختها ما زالت صالحة؟ بآليّتين رأيتَ طرفهما في مقال HTTP، ويجدر تعميقهما هنا لأنهما لبّ التخزين المؤقت. الأولى «الطزاجة»: يرفق الخادم مع الملفّ ترويسة Cache-Control تقول مثلًا «هذا صالحٌ لأسبوع» (max-age). فما دام ضمن المدّة، يستعمله المتصفّح مباشرةً دون أن يرسل أيّ طلبٍ أصلًا. وتضبط هذه الترويسة أيضًا من يحقّ له التخزين: عامٌّ يصلح للوسطاء (public)، أو خاصٌّ بالمتصفّح وحده (private)، أو ممنوعٌ تخزينه أصلًا (no-store) للبيانات الحسّاسة.

والثانية «التحقّق»: حين تنتهي المدّة، لا يُعيد المتصفّح التنزيل أعمى، بل يسأل الخادم: «هل تغيّر الملفّ؟».[1] يحمل سؤاله بصمة النسخة التي عنده (ETag، أو تاريخ آخر تعديل). فإن لم يتغيّر شيءٌ، يردّ الخادم بـ304 Not Modified دون أن يرسل الملفّ ثانيةً، فيُعاد استعمال النسخة المحلّية وقد وُفِّر النقل كلّه. وحين يُقال إنّ طلبًا «أصاب» الذاكرة (Cache Hit) فقد وُجدت نسخته فخُدِم فورًا، وإن «أخطأها» (Cache Miss) فقد لزم بناؤه أو جلبه. ونسبة الإصابة العالية هي مقياس نجاح أيّ نظام تخزينٍ مؤقت.[2] فحين يُقال إن ذاكرةً تحقّق «نسبة إصابةٍ» تسعين في المئة، فمعناه أن تسعةً من كل عشرة طلباتٍ خُدِمت دون أن تُتعِب الأصل، وهذا فرقٌ هائلٌ في السرعة والحمل والكلفة معًا.

شكل 2 ثلاثة مصائر لطلبِ ملفٍّ في ذاكرة المتصفّح ثلاث حالاتٍ لطلبٍ واحد. في الأولى تكون النسخة ما زالت في مدّتها، فيَخدُم المتصفّح نفسه منها ولا يظهر الخادم في الصورة أصلًا. وفي الثانية، وهي المشدَّدة، تكون المدّة انتهت والملفّ لم يتغيّر، فيسأل المتصفّح الخادم ومعه بصمة نسخته، فيعود الجواب بلا مِلفّ. وفي الثالثة تكون المدّة انتهت والملفّ تغيّر، فيعود الجواب ومعه المِلفّ كاملًا. فالسؤال واحدٌ في الحالتين الأخيرتين، والمختلف هو حجم ما يعود. ملفٌّ في ذاكرة متصفّحك، وثلاثة مصائر لطلبه المتصفّح النسخة في مدّتها: يَخدُم نفسه منها انتهت المدّة: سؤالٌ ومعه بصمة النسخة المتصفّح الخادم لم يتغيّر، فيعود بلا مِلفّ: 304 انتهت المدّة، والملفّ تغيّر المتصفّح الخادم فيعود ومعه المِلفّ كاملًا: 200 ثلاثة مصائر لطلبِ ملفٍّ في ذاكرة المتصفّح ثلاث حالاتٍ لطلبٍ واحد، كلٌّ منها في شريط. في الأولى تكون النسخة في مدّتها فيَخدُم المتصفّح نفسه منها، ولا خادم في الشريط أصلًا. وفي الثانية، وهي المشدَّدة، انتهت المدّة والملفّ لم يتغيّر، فيهبط سؤالٌ إلى الخادم ومعه بصمة النسخة، ويصعد جوابٌ بلا مِلفّ. وفي الثالثة انتهت المدّة وتغيّر الملفّ، فيصعد الجواب ومعه المِلفّ كاملًا. ثلاثة مصائر لطلبٍ واحد المتصفّح النسخة في مدّتها: يَخدُم نفسه المتصفّح سؤالٌ ومعه بصمة النسخة جوابٌ بلا مِلفّ: 304 الخادم المتصفّح السؤال نفسه، والملفّ تغيّر جوابٌ ومعه المِلفّ: 200 الخادم
ثلاثة مصائر لطلبٍ واحد بحسب حال النسخة: ما دامت طازجةً فلا طلب على الشبكة أصلًا، وإن انتهت مدّتها ولم يتغيّر الملفّ عاد الخادم بترويساتٍ بلا مِلفّ، وإن تغيّر عاد به كاملًا. المشدَّد هو الطريق الذي لا يكلّف شيئًا.

وهنا اسمان يُخلَط بينهما دائمًا. no-cache لا يمنع التخزين، بل يمنع استعمال النسخة قبل التحقّق منها؛ أمّا no-store فيمنع التخزين أصلًا.[1] فإن أردت محتوًى يبقى محفوظًا ويُسأل عنه في كلّ مرّة، فالأوّل هو ما تريد. وإن أردت ألّا يُكتب على قرص أحدٍ أبدًا، فالثاني. والخلط بينهما يكلّف: مواقع كثيرة تكتب no-store وهي تقصد no-cache، فتخسر التخزين كلّه بلا حاجةٍ إليه.

وماذا لو لم يرسل الخادم ترويسةً أصلًا؟ لا يعني ذلك ألّا يُخزَّن الملفّ. فالبروتوكول مصمَّمٌ ليخزّن ما استطاع، فإن غابت الترويسة قدّر المتصفّح للنسخة عمرًا تخمينًا من تاريخ آخر تعديل، والمواصفة توصي بنحو عُشر تلك المدّة: فملفٌّ لم يُعدَّل منذ سنةٍ يُعَدّ طازجًا نحو خمسة أسابيع.[1] وهذا وحده يفسّر شكوى شائعة: «لم أضبط شيئًا، فلماذا يعرض المتصفّح القديم؟». لم يخترع المتصفّح المدّة، بل خمّنها لأنك لم تقلها.

فكم ينبغي أن تخزّن إذن؟ القاعدة العمليّة أن تطابق العمر مع معدّل التغيّر. الأصول الثابتة التي تحمل بصمةً في اسمها تُخزَّن سنةً كاملةً بلا قلق، لأنها إن تغيّرت تغيّر اسمها. وصفحات HTML تُخزَّن دقائق أو ثوانٍ، فهي تتغيّر وتحتاج ظهور التعديل سريعًا. والبيانات الحسّاسة لا تُخزَّن أصلًا. فلا توجد مدّةٌ واحدةٌ صحيحة، بل لكلّ نوعٍ مدّته التي توازن بين الطزاجة والسرعة.

ولكلّ طبقةٍ ساعتها. فليس مطلوبًا أن يكون عمر النسخة واحدًا في متصفّح الزائر وعلى حافّة الشبكة: ما تريده قصيرًا عند المستخدم قد تريده طويلًا على الحافّة، لأنك تملك الحافّة وتستطيع إسقاط ما فيها في الحال، ولا تملك متصفّحه. ولهذا ترويستان إلى جانب max-age: s-maxage تخصّ الذواكر المشتركة وتتقدّم عليها، وCDN-Cache-Control قنّنها معيارٌ من IETF سنة 2022 ليخاطب شبكات التوصيل وحدها.[8] فاستجابةٌ تحمل max-age=60, s-maxage=120 مع CDN-Cache-Control: max-age=600 تقول ثلاثة أشياء في وقتٍ واحد: دقيقةٌ في متصفّح الزائر، ودقيقتان في أيّ ذاكرةٍ مشتركة، وعشر دقائق في شبكة التوصيل.[8] ثلاث ساعاتٍ لاستجابةٍ واحدة، وهذا هو الضبط الذي يجعل الطبقات تتعاون بدل أن تتنازع.

وهنا جوهر التخزين المؤقت كلّه: إنه مقايضةٌ واعية، تبيع فيها قليلًا من «الطزاجة» مقابل كثيرٍ من السرعة. فالنسخة المخزّنة قد تكون أقدم من الأصل بثوانٍ أو ساعات، وأنت تقبل هذا الفارق الضئيل لقاء مكسبٍ كبيرٍ في الأداء والكلفة. والفنّ كلّه في تحديد كم من القِدَم تحتمله كلّ بيانة: ثوانٍ لسعرٍ متغيّر، وعامٌ لشعارٍ ثابت. من أتقن هذه الموازنة أتقن التخزين المؤقت.

أصعب مشكلة: إبطال الذاكرة المؤقتة

تدور نكتةٌ شهيرة بين المبرمجين: «في علوم الحاسوب مسألتان صعبتان: إبطال الذاكرة المؤقتة، وتسمية الأشياء». والإبطال صعبٌ فعلًا: فإن خزّنت نسخةً مدّةً طويلةً ثمّ تغيّر الأصل، رأى الناس محتوًى قديمًا؛ وإن خزّنتها مدّةً قصيرةً، ضاعت فائدة التخزين. كيف توازن؟ بعدّة أدوات.

أبسطها انتهاء المدّة (TTL): تُحدَّد للنسخة عمرٌ تُسقَط بعده. ثمّ «التطهير» الصريح (Purge): عند تعديل صفحةٍ أو نشرها، تأمر الذاكرة بإسقاط نسختها القديمة فورًا. وأذكى الحيل «تغيير الاسم عند التغيير» (Cache-Busting): تُسمّى الملفّات الثابتة باسمٍ يحمل بصمةً مثل style.a1b2c3.css، فإذا تغيّر محتواها تغيّر اسمها، فتراها الذاكرة ملفًّا جديدًا وتجلبه، بينما يُخزَّن القديم سنةً كاملةً بأمان. لهذا تطبع أدوات البناء بصماتٍ على أسماء الملفّات. وتكمِّل هذه الحيلةَ ترويسةٌ صغيرة: immutable. فالملفّ الذي يحمل بصمةً في اسمه لن يتغيّر محتواه أبدًا، فلا معنى لأن يسأل المتصفّح عنه حين يعيد المستخدم تحميل الصفحة؛ وتقول Cache-Control: max-age=31536000, immutable إنه صالحٌ سنةً كاملةً ولا يحتاج تحقّقًا.[1]

وهناك حلٌّ أنيقٌ للتأخير: «اخدم القديم وحدّث في الخلفية» (stale-while-revalidate): حين تنتهي صلاحية النسخة، يُخدَم الطلب الأوّل بالنسخة القديمة فورًا (فلا ينتظر المستخدم)، بينما تُجلَب نسخةٌ جديدةٌ في الخلفية للطلبات التالية.[4] وأخيرًا تبقى أداة المستخدم: «التحديث القسريّ» الذي يتجاوز كل الذواكر ويطلب نسخةً طازجةً من الأصل. ويزداد الإبطال صعوبةً مع تعدّد الطبقات: فقد تطهّر نسخة الخادم وتبقى نسخةٌ قديمةٌ في حافّة CDN أو في متصفّح الزائر، فيرى كلّ طرفٍ حالةً مختلفة. ضبط الإبطال عبر الطبقات كلّها معًا هو ما يفصل نظامًا متّسقًا من فوضى نسخٍ متضاربة.

وللإبطال خطرٌ خفيّ اسمه «اندفاع الذاكرة» (Cache Stampede): حين تنتهي صلاحية عنصرٍ مطلوبٍ بشدّة، قد تصل آلاف الطلبات في اللحظة نفسها فلا تجده، فتنقضّ كلّها على الأصل دفعةً واحدةً لتعيد بناءه، فتُغرقه. ويُعالَج بأن يُسمَح لطلبٍ واحدٍ فقط بإعادة البناء بينما ينتظر الباقون أو يُخدَمون بالقديم، أو بتوزيع أعمار النسخ قليلًا حتى لا تنتهي صلاحيّتها معًا. فحتى التخزين المؤقت، إن أُسيء ضبطه، قد يصير سبب العطل لا حلّه.

حين تمتلئ الذاكرة: سياسات الإخلاء

الذاكرة المؤقتة محدودةٌ بطبيعتها، فماذا حين تمتلئ؟ تُخلي بعض ما فيها وفق سياسةٍ ما. أشهرها «إخراج الأقدم استعمالًا» (LRU): يُحذف ما لم يُستعمل منذ أطول مدّة، رهانًا على أن ما لم يُطلب حديثًا أقلّ احتمالًا أن يُطلب قريبًا. وهناك «الأقلّ تكرارًا» (LFU) الذي يحتفظ بالأكثر طلبًا. هذه السياسات تضمن أن تبقى الذاكرة المحدودة مليئةً بالأنفع، لا بأوّل ما دخلها.

تخزين البيانات: استراتيجيات الكتابة

إلى الآن دار الحديث عن تخزين ما يُقرأ. لكنّ ذواكر البيانات داخل الخادم (مثل Redis التي تخزّن نتائج استعلاماتٍ ثقيلة) تواجه سؤالًا إضافيًّا: ماذا حين تتغيّر البيانات؟ هنا استراتيجيات الكتابة. في «الكتابة المارّة» (Write-Through) تُكتَب البيانة في الذاكرة وقاعدة البيانات معًا، فتبقيان متّسقتين على حساب بطءٍ في الكتابة. وفي «الكتابة المؤجّلة» (Write-Back) تُكتَب في الذاكرة أوّلًا وتُؤجَّل إلى القاعدة، فأسرع لكن أخطر إن تعطّلت الذاكرة قبل الحفظ. الاختيار مفاضلةٌ بين الاتّساق والسرعة، وهي مفاضلةٌ تتكرّر في كل قرار تخزينٍ مؤقت.[5]

ما الذي يُخزَّن وما لا يُخزَّن

ليس كلّ شيءٍ قابلًا للتخزين بسهولة. فالمحتوى الثابت (الصور وملفّات التنسيق والبرامج) سهلٌ: يُخزَّن مدّةً طويلةً مع بصمةٍ على اسمه. أمّا المحتوى الديناميكيّ والشخصيّ فمعضلة: صفحةٌ تعرض «مرحبًا يا فلان» أو سلّة تسوّقٍ أو سعرًا متغيّرًا لا يصحّ أن تُخزَّن وتُقدَّم لغيرك. هذا توتّرٌ أصيل بين السرعة والتخصيص.

وتُعالَج بحيلٍ متدرّجة: تخزين الأجزاء العامّة وترك الخاصّة تُبنى لحظيًّا، أو «التمييز حسب الكوكي» فلا تُشارَك نسخة المستسجِل مع الزائر، أو «التخزين الدقيق جدًّا» لثوانٍ معدودةٍ يكفي لامتصاص موجات الزوّار دون أن يبدو المحتوى قديمًا. فحتى ما يبدو غير قابلٍ للتخزين، كثيرًا ما يُخزَّن جزءٌ منه بذكاء.

وهنا تكمن أيضًا خطورةٌ أمنيّة: لو خُزِّنت صفحةٌ شخصيّةٌ ثمّ قُدِّمت لمستخدمٍ آخر، تسرّبت بياناتٌ خاصّة. لهذا تُوسَم البيانات الحسّاسة بـno-store، وتُستعمل ترويسة Vary لتقول للذاكرة: «اختلِف في التخزين حسب هذا العامل» (كاللغة أو الكوكي)، فلا تُخلَط نسخةٌ بأخرى. فضبط ما يُخزَّن وكيف ليس مسألة أداءٍ فقط، بل مسألة خصوصيّةٍ أيضًا.

وتُخزَّن أحيانًا حتى النتائج السالبة: فلو طُلب شيءٌ غير موجود، خُزِّن «غير موجود» مدّةً قصيرةً («تخزينٌ سالب») حتى لا يُرهَق الأصل بطلباتٍ متكرّرةٍ عن شيءٍ لن يجده. فكلّ ما يتكرّر، حتى الفشل، يستحقّ أن يُخزَّن جوابه.

الطبقة التي تكتب منطقها بنفسك: عامل الخدمة

كلّ ما مرّ حتى الآن ذواكرُ تطيع ترويسات، وأنت تخاطبها من بعيد وتأمل أن تفهم. لكن في المتصفّح طبقةً واحدةً تكتب أنت منطقها بنفسك: «عامل الخدمة» (Service Worker)، وهو برنامجٌ صغيرٌ يعمل بجانب الصفحة ويعترض طلباتها قبل أن تخرج إلى الشبكة، فيقرّر بالكود: أخدم من الذاكرة، أم أذهب إلى الشبكة، أم أجرّب الشبكة فإن تعذّرت رجعت إلى الذاكرة؟ وله مخزنه الخاصّ (Cache API) لا يحكمه max-age ولا ETag، بل يحكمه ما كتبتَه أنت.

وهذا ما يجعل تطبيق ويبٍ يفتح بلا اتّصالٍ أصلًا. فلا شيء في ترويسات HTTP يستطيع أن يقول «إن انقطعت الشبكة فاعرض هذه الصفحة»، أمّا عامل الخدمة فيقولها في أسطرٍ قليلة. ولذلك تضع موزيلا عامل الخدمة في خانةٍ واحدة مع الوكيل العكسيّ وشبكة التوصيل: ذواكرُ «مُدارة» يَنشرها صاحب الموقع عن قصد، لا ذواكرُ يصادفها الطلب في طريقه.[1]

وثمنه أنّ الخطأ فيه يدوم. فعامل خدمةٍ يخزّن نسخةً غلطًا ويصرّ عليها قد يُعطِّل الموقع على زائره حتى يعود فيسحبه عاملٌ جديد، ولا تنفع فيه ترويسةٌ تصلحها على الخادم. لذلك لا يُنشر عامل خدمةٍ إلّا وله طريق تحديثٍ مضمون. أقوى الطبقات أخطرها.

ذاكرةٌ لا تحفظ الاستجابات بل الصفحة كلّها

وفي متصفّحك ذاكرةٌ تختلف عن كلّ ما سبق، وقلّ من يذكرها: «ذاكرة الرجوع والتقدّم» (bfcache). فذاكرة HTTP تحفظ استجاباتٍ لطلباتٍ مضت، أمّا هذه فتحفظ الصفحة نفسها لحظةَ تركتها: بنيتها، وحالتها، وكومة الجافاسكربت بما فيها.[7] فحين تضغط «رجوع» لا تُبنى الصفحة من جديد، بل تُستأنف كما كانت، والصفحة في موضع تمريرها والنماذج ممتلئة.

وليست حالةً هامشيّة: تقول قياسات كروم إنّ نحو واحدٍ من كلّ عشرة تنقّلاتٍ على الحاسوب، وواحدٍ من كلّ خمسةٍ على الهاتف، رجوعٌ أو تقدّم.[7] لكنّ الصفحة تُستثنى من هذه الذاكرة لأسبابٍ يصنعها المطوّر نفسه، وأشهرها مستمعٌ لحادثة unload، ولذلك النصيحة قاطعة: لا تستعملها، واستعمل pagehide مكانها.[7]

ولها مفارقةٌ تستحقّ الانتباه: ترويسة no-store التي تكتبها لتحمي صفحةً حسّاسةً كانت تُخرج صفحتك من هذه الذاكرة أيضًا، فتخسر سرعة الرجوع كلّها، وكروم يعمل على تغيير ذلك بما يحفظ الخصوصيّة.[7] فترويسةٌ واحدة قد تُصلح طبقةً وتُعطِّل أخرى، وهذا هو سبب وجود هذا المقال: الطبقات لا تُضبط واحدةً واحدة.

الواقع العمليّ لمن يبني المواقع

لمن يطوّر المواقع، التخزين المؤقت ليس نظريّةً بل أدواتٌ يوميّة تتراكب: «ذاكرة صفحاتٍ» تخزّن صفحات HTML كاملةً جاهزة، و«ذاكرة كائنات» (Redis) تخزّن نتائج الاستعلامات المتكرّرة فلا تُرهَق قاعدة البيانات، و«ذاكرة كودٍ مترجَم» تتجنّب إعادة ترجمة البرنامج في كل طلب، وشبكة CDN تخدم الأصول الثابتة من حافّةٍ قريبة. اجتماعها يحوّل موقعًا بطيئًا إلى سريع.

وفي ووردبريس تحديدًا، يتجلّى هذا في أدواتٍ معروفة: «ذاكرة صفحاتٍ» تحفظ ناتج كل صفحةٍ كملفّ HTML جاهزٍ فلا يُعاد تشغيل PHP وقاعدة البيانات في كل زيارة، و«ذاكرة كائناتٍ ثابتة» تربط ووردبريس بـRedis فتبقى نتائج الاستعلامات بين الطلبات بدل أن تُحسَب من جديد، إضافةً إلى ذاكرة الكود في PHP. والموقع الذي يجمعها يخدم آلاف الزوّار من خادمٍ متواضع، بينما يختنق موقعٌ بلا تخزينٍ مؤقت تحت حملٍ يسير.

ويقترن التخزين المؤقت عادةً بـتقنيّاتٍ تخفّف الحجم نفسه: ضغط الملفّات قبل إرسالها، وتصغير ملفّات التنسيق والبرامج بحذف ما لا يلزم. فالأسرع أن تُرسل قليلًا من القريب: تخزينٌ مؤقتٌ يقرّب، وضغطٌ يخفّف، فيجتمع الأمران على السرعة.

ولها وجهٌ مزعجٌ يعرفه كل مطوّر: «عدّلتُ شيئًا فلا أراه يتغيّر». غالبًا لأن نسخةً قديمةً محفوظةٌ في طبقةٍ ما (المتصفّح، أو الوكيل، أو CDN). فأوّل ما يُسأل عند سلوكٍ غريبٍ بعد تعديل: «أين الذاكرة التي تخدم القديم؟». معرفة الطبقات تحوّل هذا اللغز إلى تشخيصٍ مباشر: طهّر الطبقة الصحيحة، أو غيّر اسم الملفّ، فيظهر الجديد. وفي متصفّحك مفتاحٌ يتجاوز الذواكر كلّها ويجلب كلّ شيءٍ من الأصل: «التحديث القسريّ» (Ctrl+Shift+R على ويندوز ولينكس، وCmd+Shift+R على ماك). ومن هنا جاءت النصيحة التي يسمعها كلّ مستخدم: «امسح ذاكرة متصفّحك». وهي نصيحةٌ صائبةٌ للمستخدم لأنه لا يملك غيرها، وكسولةٌ للمطوّر لأنه يملك أن يُصلح الترويسة أو الاسم فلا يحتاجها أحد.

وللتخزين المؤقت وجهٌ اقتصاديٌّ صريح: كلّ طلبٍ يُخدَم من ذاكرةٍ قريبةٍ هو طلبٌ لم يدفع الموقع ثمن نقله من الأصل ولا حِمله على خوادمه. لمواقع كبيرةٍ تخدم ملايين الطلبات، يترجَم هذا إلى توفيرٍ ضخمٍ في فواتير النطاق والخوادم، وإلى قدرةٍ على تحمّل موجات الزوّار المفاجئة دون انهيار. فالتخزين المؤقت ليس تحسينًا تقنيًّا فقط، بل قرارٌ يوفّر المال ويصون الاستقرار.

جرّبه بنفسك: اقرأ ذاكرة متصفّحك

كلّ ما قرأتَه هنا مرئيٌّ في متصفّحك في دقيقتين. افتح موقعًا تزوره كثيرًا، ثمّ افتح أدوات المطوّر (F12 أو Ctrl+Shift+I) وانتقل إلى لسان الشبكة، وأعِد تحميل الصفحة. وانظر عمود الحجم: ما كُتب أمامه أنه من ذاكرة القرص أو ذاكرة الجهاز لم يُنقل عبر الشبكة أصلًا، وحجمه المنقول صفر. تلك هي الطبقة الأولى تعمل أمام عينيك.

ثمّ أربع خطواتٍ تكشف الباقي. أوّلًا: علّم على خيار تعطيل الذاكرة وأعِد التحميل، وقارن مجموع ما نُقل قبل وبعد؛ الفرق هو ما وفّره التخزين المؤقت عليك. ثانيًا: ابحث في عمود الحالة عن 304، فتلك نسخةٌ سأل المتصفّح عنها فأُخبِر أنها ما زالت صالحة، فعاد الجواب بلا مِلفّ. ثالثًا: اضغط على ملفّ تنسيقٍ واقرأ ترويسات جوابه، وابحث عن Cache-Control وETag؛ ستجد للملفّات الثابتة عمرًا طويلًا وبصمةً في الاسم، وللصفحة نفسها عمرًا قصيرًا أو منعًا من الاستعمال قبل التحقّق. رابعًا: إن كان الطرفيّة أقرب إليك، فـcurl -I https://example.com يعطيك الترويسات وحدها دون تحميل الصفحة.

وآخر خطوةٍ هي الأمتع: اضغط «رجوع» بعد أن تنتقل إلى صفحةٍ أخرى، وراقب هل تُبنى الصفحة من جديد أم تظهر في لحظةٍ كما تركتها. إن ظهرت كما تركتها فقد رأيت ذاكرة الرجوع والتقدّم تعمل، وستلاحظ أنّ لسان الشبكة لا يسجّل لها طلبًا واحدًا.

من الذاكرة القريبة إلى الحافّة العالمية

هذا هو التخزين المؤقت: نسخةٌ قريبةٌ تختصر مسافةً بعيدة، تعمل في طبقاتٍ من متصفّحك إلى قلب الخادم، تحكمها موازنةٌ دقيقةٌ بين الطزاجة والسرعة، وإبطالٌ هو أصعب ما فيها، وإخلاءٌ يحفظ الأنفع حين تمتلئ. به يكتسب الويب «ذاكرةً» تجعله سريعًا وقابلًا للتوسّع. وهو حاضرٌ في كل مكانٍ دون أن تراه: في معالجك، وفي نظامك، وفي متصفّحك، وعلى حافّة الشبكة، وفي قلب الخادم. وكلّما شعرتَ أن موقعًا «فتح فورًا»، فغالبًا أن نسخةً قريبةً وفّرت عليك مسافةً طويلةً ما كنت لتشعر بها لو قُطِعت.

وأقوى طبقةٍ منه حين يكون جمهورك موزّعًا على الكوكب هي التخزين عند «الحافّة»: نسخٌ من موقعك في خوادمَ قريبةٍ من كل مستخدمٍ أينما كان. هذه هي «شبكة توصيل المحتوى»، وهي موضوع المقال التالي: «شبكة توصيل المحتوى (CDN)».

المصادر

  1. MDN: HTTP Caching. الطزاجة والتحقّق وترويسات التخزين.
  2. IETF: RFC 9111: HTTP Caching. المواصفة الرسمية للتخزين المؤقت في HTTP.
  3. Cloudflare: ما التخزين المؤقت؟ المبدأ والطبقات والفائدة.
  4. IETF: RFC 5861: stale-while-revalidate. خدمة القديم مع التحديث في الخلفية.
  5. Redis: إصابة الذاكرة وإخفاقها وتخزين الكائنات. ذاكرة الكائنات داخل الخادم.
  6. Chrome for Developers: تقسيم ذاكرة المتصفّح لأجل الأمن والخصوصيّة. التقسيم منذ كروم 86، وسببه الأمنيّ، وكلفته المقيسة في الإخفاق والنقل وزمن الرسم.
  7. web.dev: ذاكرة الرجوع والتقدّم. فرقها عن ذاكرة HTTP، ونسبة تنقّلات الرجوع، وحادثة unload، وأثر no-store.
  8. IETF: RFC 9213: Targeted HTTP Cache Control. ترويسة CDN-Cache-Control وأمثلة الأعمار المختلفة لكلّ طبقة.

اترك تعليقاً

الحقول المميّزة بـ مطلوبة