بناء CDN Server خاص بكم: جلب مرة واحدة، وتسليم ألف مرة

نُشر في مدة القراءة: 8 دقيقة

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

كل ملف يحمّله زائر من موقعكم، يجلبه خادم الأصل عندكم من نظام الملفات، ويضعه في جواب، ويرسله. ومع عشرة زوّار في اليوم لا يلاحظ ذلك أحد. أما مع عشرة آلاف زائر في وقت واحد، فهذا بالضبط هو السبب في بطء الموقع، رغم أن التطبيق نفسه لا يفعل شيئاً.

ويجلس CDN Server أمام ذلك. فهو يجلب كل ملف مرة واحدة من الأصل، ويسلّمه بعد ذلك بنفسه، بقدر ما يُطلب. وبذلك لا يرى الأصل سوى جزء صغير من الطلبات، ويحصل الزوّار على ملفاتهم من خادم لا يفعل شيئاً آخر غير التسليم. يبيّن هذا المقال ما يجنيه ذلك فعلاً، وكيف تُعِدّون خادماً كهذا باستخدام nginx، وما المهم في الترافيك والموقع.

ما الذي يفعله CDN Server حقاً

يُحكى سير العمل في ثلاث جمل:

  • يطلب زائر ملفاً من CDN Server.
  • إن كان الملف موجوداً هناك، خرج فوراً. وهذه إصابة.
  • وإن لم يكن موجوداً، جلبه CDN Server من الأصل، وخزّنه، وسلّمه. وهذا إخفاق، ويحدث لكل ملف مرة واحدة فقط في كل مدة صلاحية.

والمبدأ هو نفسه مبدأ Stream Server: جلب مرة واحدة، وتسليم متكرّر. والفرق الوحيد أن Stream Server يوزّع تدفّق بيانات جارياً، وأن CDN Server يوزّع ملفات جاهزة.

الحساب: ما مقدار التخفيف الذي تجلبه الذاكرة المؤقتة

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

نسبة الإصابةالطلبات على الأصلالتخفيف
50 %500,000معامل 2
80 %200,000معامل 5
95 %50,000معامل 20
99 %10,000معامل 100

وبالنسبة للملفات الثابتة فإن 95 بالمئة وأكثر هي الحالة الطبيعية، لأن الصور والسكربتات والخطوط تتغيّر نادراً. والقفزة من 95 إلى 99 بالمئة أقيم ممّا توحي به الأرقام: فهي تُنزل الحمل المتبقّي إلى خُمسه مرة أخرى.

وفي الوقت نفسه ينتقل الترافيك. فالأصل لا يكاد ينقل شيئاً، وCDN Server ينقل أكثر بكثير. فعند 500,000 استدعاء للصور في اليوم بحجم ⁦400 KB⁩ لكل صورة، يكون ذلك ⁦200 GB⁩ يومياً، أي نحو ⁦6 TB⁩ في الشهر، للصور وحدها. ولهذا فإن الرقم المهم في عقدة CDN ليس المعالج، بل الوصلة والحجم.

متى يستحق CDN Server خاص الأمر

يحاسب مزوّدو CDN التجاريون على كل جيجابايت منقول. وهذا مريح ورخيص عند الحجم القليل. لكن النسبة تنقلب بدءاً من كمية معيّنة:

الحالةCDN Server خاصالمحاسبة على الجيجابايت
بضعة GB في الشهرلا يكاد يستحقأرخص
عدة TB في الشهرسعر شهري ثابتينمو خطّياً
تنزيلات كبيرة وأرشيفات فيديومتقدّم بوضوحيصبح مكلفاً سريعاً
تحكّم كامل في القواعد والسجلاتكاملمحدود

ويُضاف إلى ذلك أمر يصعب تقديره بالأرقام: فمع عقدة خاصة تقرّرون أنتم ما يُخزَّن مؤقتاً، وكم يبقى، ومتى يختفي. ولستم مقيّدين بقواعد مزوّد غريب، وتستطيعون كذلك تسليم أشياء لا تدخل في مخططات الآخرين.

البناء باستخدام nginx كذاكرة مؤقتة

التثبيت

apt update
apt install -y nginx

لا يحتاج الأمر أكثر من ذلك، فالذاكرة المؤقتة مبنية في nginx. أنشئوا المجلد الذي ستوضع فيه الملفات:

mkdir -p /var/cache/cdn
chown www-data:www-data /var/cache/cdn

الضبط

تُوصَف الذاكرة المؤقتة مرة واحدة في كتلة http، ثم تُستخدَم في المواضع المفردة:

proxy_cache_path /var/cache/cdn
                 levels=1:2
                 keys_zone=cdn:100m
                 max_size=200g
                 inactive=30d
                 use_temp_path=off;

proxy_cache_key "$scheme$request_method$host$request_uri";

وتعني القيم على التفصيل: keys_zone=cdn:100m يكفي لنحو 800,000 مدخل، وmax_size يحدّ المساحة على القرص، وinactive=30d يرمي الملفات التي لم يطلبها أحد خلال 30 يوماً، وuse_temp_path=off يوفّر نقلاً لا داعي له بين المجلدات.

وتحت ذلك الخادم نفسه:

server {
    listen 443 ssl;
    http2 on;
    server_name cdn.example.com;

    ssl_certificate     /etc/letsencrypt/live/cdn.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cdn.example.com/privkey.pem;

    location / {
        proxy_pass https://origin.example.com;
        proxy_set_header Host origin.example.com;
        proxy_http_version 1.1;

        proxy_cache cdn;
        proxy_cache_valid 200 301 302 24h;
        proxy_cache_valid 404 1m;

        # تجميع عدة إخفاقات متزامنة في طلب واحد
        proxy_cache_lock on;
        proxy_cache_lock_timeout 10s;

        # مواصلة التسليم عند حدوث مشاكل في الأصل
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;

        # كوكيز الجلسة لا يجوز أن تُفسد الذاكرة المؤقتة
        proxy_ignore_headers Set-Cookie;
        proxy_hide_header Set-Cookie;

        add_header X-Cache-Status $upstream_cache_status always;
    }
}

وثلاثة أسطر منها تستحق انتباهاً خاصاً. فـ proxy_cache_lock يضمن ألا يطلب مئة زائر الأصل في الوقت نفسه عند ملف جديد، بل واحد فقط وينتظر الباقون. وproxy_cache_use_stale يُبقي التسليم قائماً أثناء إعادة تشغيل الأصل أو بطء جوابه. وإخفاء Set-Cookie يمنع أن يُفسد جوابٌ واحد يحمل كوكي جلسة الذاكرة المؤقتة كلها.

التحقّق من أنه يعمل

nginx -t && systemctl reload nginx
curl -sI https://cdn.example.com/bild.jpg | grep -i x-cache-status
curl -sI https://cdn.example.com/bild.jpg | grep -i x-cache-status

في الاستدعاء الأول تظهر هناك MISS، وفي الثاني HIT. وإذا ظهرت MISS في المرة الثانية أيضاً، فالسبب يكون دائماً تقريباً أحد المتهمين الثلاثة المعتادين: Set-Cookie في الجواب، أو Cache-Control: no-store من الأصل، أو غياب بيان الطول.

ما يُخزَّن مؤقتاً وما لا يُخزَّن

الحلّ النظيف هو أن يقول الأصل ذلك بنفسه. فهو يعرف محتوياته، والذاكرة المؤقتة لا تعرفها. ولذلك يضبط الترويسات المناسبة:

المحتوىالترويسة على الأصل
ملف يحمل بصمة تحقّق في اسمهCache-Control: public, max-age=31536000, immutable
صورة أو تنزيل من دون ترقيم إصدارCache-Control: public, max-age=86400
HTML لصفحة عامةCache-Control: public, max-age=300
منطقة بعد تسجيل الدخول، سلة الشراءCache-Control: private, no-store

وإن أردتم أن تقرّروا في الذاكرة المؤقتة نفسها، فذلك يجري عبر مواضع خاصة لكل نوع ملف:

location ~* \.(jpg|jpeg|png|gif|webp|avif|svg|ico|css|js|woff2|mp4|zip)$ {
    proxy_pass https://origin.example.com;
    proxy_cache cdn;
    proxy_cache_valid 200 7d;
    add_header Cache-Control "public, max-age=604800";
    add_header X-Cache-Status $upstream_cache_status always;
}

التخلّص من المحتويات بشكل موجّه

أبسط طريق هو ألا تحتاجوا إلى الحذف أصلاً: فمن يمنح كل صورة مُعدَّلة اسم ملف جديد، مثل logo.a1b2c3.png، لا تواجهه هذه المشكلة. والملف القديم يسقط من الذاكرة المؤقتة من تلقاء نفسه في وقت ما.

وإن لم يكن ذلك ممكناً، فتساعد في nginx الوحدة الحرة ngx_cache_purge. وبلا وحدة إضافية يمكن حساب المسار من مفتاح الذاكرة المؤقتة، لأن nginx يستخدم مجموع MD5 الخاص به كاسم للملف:

KEY="httpsGETcdn.example.com/bild.jpg"
H=$(printf '%s' "$KEY" | md5sum | cut -d' ' -f1)
rm -f "/var/cache/cdn/${H: -1}/${H: -3:2}/$H"

وللاستخدام اليومي يكفي غالباً سكربت صغير ينفّذ هذه الأسطر على قائمة من المسارات.

الملفات الكبيرة والفيديو

عند الملفات التي تتجاوز بضع مئات من الميجابايت تحتاجون إلى التقسيم إلى كتل، وإلا حمّلت الذاكرة المؤقتة الملف كله من جديد عند كل قفزة في الفيديو:

location /video/ {
    proxy_pass https://origin.example.com;
    proxy_cache cdn;
    proxy_cache_valid 200 206 7d;

    slice 1m;
    proxy_set_header Range $slice_range;
    proxy_cache_key "$uri$slice_range";
}

وبذلك يجلب nginx الكتل المطلوبة فعلاً وحدها. فإذا قفز مشاهد إلى منتصف فيلم، مرّت بضعة ميجابايت عبر الخط بدلاً من عدة جيجابايت.

TLS وHTTP/2 وHTTP/3

تسلّم عقدة CDN كثيراً من الملفات الصغيرة إلى كثير من الاتصالات، وهناك بالضبط تثمر البروتوكولات الأحدث. وHTTP/2 ينتهي أمره بـ http2 on;. أما HTTP/3 فيجني شيئاً على اتصالات الهاتف المحمول خصوصاً، لأن تغيير الشبكة لا يقطع الاتصال:

listen 443 quic reuseport;
listen 443 ssl;
http2 on;
add_header Alt-Svc 'h3=":443"; ma=86400';

ولا تنسوا أثناء ذلك فتح UDP 443 في جدار الحماية. فهذا أكثر سبب يجعل HTTP/3 مضبوطاً ومع ذلك لا يستخدمه أحد.

عدة مواقع

عقدة واحدة تجلب أصلاً الجزء الأكبر من المكسب، أي تخفيف الحمل عن الأصل. أما المكسب الثاني، وهو قصر الطريق إلى الزائر، فلا يأتي إلا مع عدة مواقع.

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

وتوفّر KernelHost مواقع في ألمانيا، وعلى الساحل الغربي للولايات المتحدة، وعلى الساحل الشرقي للولايات المتحدة، وكذلك في سنغافورة وكوريا الجنوبية. وتجدون نظرة عامة مع قيم زمن الوصول في مواقع الخوادم. وكقاعدة عامة: عقدة واحدة لكل قارة يجلس فيها جزء يُعتدّ به من جمهوركم.

القياس بدلاً من التخمين

اكتبوا حالة الذاكرة المؤقتة في السجل، فتستطيعون عدّ نسبة الإصابة في أي وقت:

log_format cdn '$remote_addr $status $body_bytes_sent '
               '$upstream_cache_status "$request"';
access_log /var/log/nginx/cdn.log cdn;

ويمكن تحليل ذلك بسطر واحد:

awk '{print $4}' /var/log/nginx/cdn.log | sort | uniq -c | sort -rn

وإذا كانت حصة HIT في الملفات الثابتة أقل من 90 بالمئة، فالبحث عن السبب يستحق الأمر دائماً تقريباً: فالسبب غالباً كوكيز في الأجوبة، أو مدد صلاحية قصيرة جداً، أو طلبات بمعاملات متغيّرة في آخر العنوان تبدو للذاكرة المؤقتة كل مرة كملف جديد.

أي خادم يناسب

عقدة CDN آلة تسليم. ولذلك يبدو ترتيب الخصائص المهمة مختلفاً عمّا هو في خادم تطبيقات:

  1. الترافيك والوصلة. هنا ينشأ الحمل كله. فعقدة واحدة تنقل في التشغيل العادي عدة تيرابايت في الشهر بانتظام.
  2. القرص. NVMe متقدّم بوضوح، لأن الذاكرة المؤقتة تتكوّن من كثير من الوصولات العشوائية الصغيرة. ويتحدّد الحجم بمقدار ما هو ساخن من محتواكم، لا بحجم المحتوى كله.
  3. الذاكرة. يستخدم نظام التشغيل الذاكرة الحرة كذاكرة مؤقتة ثانية أمام القرص. وهنا تعني الذاكرة الأكبر سرعة أكبر مباشرة.
  4. المعالج. الأقل أهمية، فهو لا يُستخدَم إلا لـ TLS، والمعالجات الحديثة تنجز ذلك على الهامش.

ولهذه الحالة بالضبط صُنعت الخوادم الجذرية بحركة مرور غير محدودة عندنا: ⁦2 × 1 Gbit/s⁩ حقيقية لكل خادم بلا خفض للسرعة، وحجم غير محدود بسعر شهري ثابت، وأقراص NVMe من فئة مراكز البيانات في RAID، وحماية من الإغراق بسعة ⁦3.2 Tbps⁩ من المصنع. ويستغرق التجهيز أقل من 30 ثانية، مع وصول Root كامل، وIPv4، وشبكة IPv6 بحجم ⁦/64⁩. ولأن المحاسبة تجري على أساس الدفع المسبق مع إمكان الإلغاء شهرياً، يمكنكم البدء بعقدة واحدة وإضافة موقع ثانٍ في أي وقت، إذا توزّع جمهوركم.

الأسئلة الشائعة

ما الذي يفعله CDN Server بالضبط؟
يجلس بين زوّاركم وخادم الأصل عندكم. فالطلب الأول على ملف يمرّره إلى الأصل، ويخزّن الجواب أثناء ذلك. وكل الطلبات التالية على الملف نفسه يجيب عنها هو بنفسه، من دون أن يُزعج الأصل.
ما مقدار التخفيف فعلياً؟
هذا يعتمد على نسبة الإصابة. فعند 95 بالمئة لا يرى الأصل عندكم سوى طلب واحد من كل عشرين، أي أن الحمل يهبط إلى عشرينه. وعند 80 بالمئة يبقى الحمل خُمساً. وبالنسبة للملفات الثابتة مثل الصور والسكربتات والخطوط والتنزيلات، فإن 95 بالمئة وأكثر أمر طبيعي.
هل أحتاج لذلك إلى مزوّد تجاري؟
ليس بالضرورة. فـ CDN Server خاص يستحق الأمر خصوصاً عند الملفات الكبيرة والحجم الكثير، لأن المزوّدين التجاريين يحاسبون على كل جيجابايت منقول. أما في خادم بحركة مرور غير محدودة، فالتيرابايت المئة يكلّف ما يكلّفه الأول، أي لا شيء إضافياً.
ما الملفات التي ينبغي تخزينها مؤقتاً وما التي لا ينبغي؟
كل ما يبدو واحداً لجميع الزوّار: الصور والفيديو وCSS وJavaScript والخطوط وPDF وملفات التثبيت. وما لا ينبغي تخزينه مؤقتاً هو الصفحات المخصّصة وسلال الشراء ومناطق تسجيل الدخول وكل ما يحمل كوكي جلسة. والأفضل أن يتخذ القرار خادم الأصل عبر ترويسة Cache-Control.
كم ينبغي أن تحتفظ الذاكرة المؤقتة بالملفات؟
للملفات التي تحمل رقم إصدار أو بصمة تحقّق في اسمها، سنة كاملة بلا قلق، فهي لا تتغيّر أبداً. وللصور والتنزيلات من دون ترقيم إصدار، المعتاد ساعات إلى أيام. وللـ HTML، إن خزّنتموه مؤقتاً أصلاً، دقائق. والمهم ليس الرقم الدقيق، بل أن يكون لديكم طريق لحذف ملفات معيّنة بشكل موجّه.
كيف أحذف ملفاً من الذاكرة المؤقتة؟
أبسط طريق هو اسم الملف: فمن يمنح كل تغيير اسماً جديداً لا يحتاج إلى حذف أبداً. وإن لم يكن ذلك ممكناً، فيساعد في nginx إما وحدة Purge التجارية أو ngx_cache_purge الحرة. وفي أسوأ الأحوال تحذفون ملف الذاكرة المؤقتة مباشرة في نظام الملفات، إذ يمكن حساب المسار من مفتاح الذاكرة المؤقتة.
ما يحدث إذا تعطّل خادم الأصل؟
عند الضبط الصحيح تواصل الذاكرة المؤقتة التسليم، حتى لو كانت المدخلات منتهية الصلاحية فعلياً. ويتولى ذلك proxy_cache_use_stale مع error وtimeout، إضافة إلى proxy_cache_background_update. وبذلك يبقى موقعكم متاحاً أثناء صيانة الأصل أو إعادة تشغيله.
هل يعمل ذلك أيضاً مع ملفات الفيديو الكبيرة؟
نعم، لكن مع إعداد Slice وحده. فمن دونه تحمّل الذاكرة المؤقتة الملف كله من جديد من الأصل عند كل قفزة في الفيديو. وبـ slice يقسّم nginx الملف إلى كتل بحجم ميجابايت واحد مثلاً، ويخزّن الأجزاء المطلوبة فعلاً وحدها.
كم موقعاً أحتاج؟
ابدؤوا بموقع واحد، وليكن حيث يوجد معظم زوّاركم. ولا يستحق موقع ثانٍ الأمر إلا إذا كان جزء يُعتدّ به من الجمهور في قارة أخرى. ويجري التوزيع بعد ذلك عبر DNS، فيصل كل زائر إلى العقدة الأقرب إليه.
كيف أعرف أن الذاكرة المؤقتة تعمل؟
أخرِجوا المتغيّر upstream_cache_status كترويسة. فعند الاستدعاء الثاني للملف نفسه يجب أن تظهر فيها HIT. وإذا كتبتم القيمة نفسها في السجل أيضاً، أمكنكم عدّ نسبة الإصابة في أي وقت. وأقل من 90 بالمئة في الملفات الثابتة يعني غالباً أن شيئاً في الترويسات أو في الكوكيز ليس على ما ينبغي.

CDN Caching nginx Reverse Proxy Performance HTTP Unlimited Traffic