كيفية التعامل مع حركة المرور المتقطعة: خوارزميات Leaky Bucket و Token Bucket
API7.ai
January 5, 2023
في المقالات السابقة، تعلمنا عن تحسين الكود وتصميم الذاكرة المؤقتة، والتي ترتبط ارتباطًا وثيقًا بأداء التطبيق العام وتستحق اهتمامنا. ومع ذلك، في سيناريوهات الأعمال الحقيقية، نحتاج أيضًا إلى النظر في تأثير الزيادة المفاجئة في حركة المرور على الأداء. قد تكون الزيادة المفاجئة في حركة المرور هنا طبيعية، مثل حركة المرور من الأخبار العاجلة، العروض الترويجية، إلخ، أو قد تكون غير طبيعية، مثل هجمات DDoS.
يُستخدم OpenResty الآن بشكل رئيسي كطبقة وصول لتطبيقات الويب مثل جدران حماية تطبيقات الويب (WAFs) وبوابات API، والتي يجب أن تتعامل مع الزيادة المفاجئة في حركة المرور الطبيعية وغير الطبيعية المذكورة أعلاه. بعد كل شيء، إذا لم تتمكن من التعامل مع حركة المرور المفاجئة، يمكن أن تنهار الخدمات الخلفية بسهولة، ولن يستجيب العمل بشكل مناسب. لذا، سننظر اليوم في طرق التعامل مع الزيادة المفاجئة في حركة المرور.
التحكم في حركة المرور
التحكم في حركة المرور هو ميزة أساسية لجدران حماية تطبيقات الويب (WAF) وبوابات API. فهو يضمن أن الخدمات العلوية يمكن أن تعمل بشكل مناسب من خلال توجيه والتحكم في حركة المرور الواردة باستخدام بعض الخوارزميات، مما يحافظ على صحة النظام.
نظرًا لأن قدرة المعالجة في الخلفية محدودة، نحتاج إلى النظر في جوانب متعددة مثل التكلفة، تجربة المستخدم، واستقرار النظام. بغض النظر عن الخوارزمية المستخدمة، ستؤدي حتمًا إلى إبطاء أو حتى رفض طلبات المستخدمين العاديين، مما يضحي بجزء من تجربة المستخدم. لذلك، نحتاج إلى التحكم في حركة المرور مع تحقيق التوازن بين استقرار الأعمال وتجربة المستخدم.
في الواقع، هناك العديد من حالات "التحكم في حركة المرور" في الحياة الواقعية. على سبيل المثال، خلال موسم السفر في عيد الربيع الصيني، يتدفق الناس بشكل مزدحم في محطات المترو، محطات القطار، المطارات، وغيرها من مراكز النقل لأن قدرة التعامل مع هذه المركبات محدودة. لذلك، يجب على الركاب الانتظار في طابور والدخول إلى المحطة على دفعات لضمان سلامتهم وتشغيل حركة المرور بشكل منتظم.
هذا يؤثر بشكل طبيعي على تجربة الركاب، ولكن بشكل عام، يضمن التشغيل الفعال والآمن للنظام. على سبيل المثال، إذا لم يكن هناك طابور وتقسيم إلى دفعات، ولكن بدلاً من ذلك، تم السماح للجميع بالدخول إلى المحطة بشكل جماعي، فإن النتيجة ستكون انهيار النظام بأكمله.
بالعودة إلى التكنولوجيا، على سبيل المثال، لنفترض أن خدمة علوية مصممة للتعامل مع 10,000 طلب في الدقيقة. في أوقات الذروة، إذا لم يكن هناك تحكم في حركة المرور عند نقطة الدخول ووصلت مهام المكدس إلى 20,000 طلب في الدقيقة، فإن أداء معالجة هذه الخدمة العلوية سينخفض إلى ربما 5,000 طلب في الدقيقة وسيستمر في التدهور، مما قد يؤدي في النهاية إلى عدم توفر الخدمة. هذه ليست النتيجة التي نريد رؤيتها.
الخوارزميات الشائعة للتحكم في حركة المرور التي نستخدمها للتعامل مع هذه الزيادة المفاجئة في حركة المرور هي خوارزمية الدلو المتسرب وخوارزمية دلو الرموز.
خوارزمية الدلو المتسرب
لنبدأ بالنظر إلى خوارزمية الدلو المتسرب، التي تهدف إلى الحفاظ على معدل طلبات ثابت وتنعيم الزيادة المفاجئة في حركة المرور. ولكن كيف يتم تحقيق ذلك؟ أولاً، انظر إلى التجريد المفاهيمي التالي من مقدمة ويكيبيديا لخوارزمية الدلو المتسرب.
يمكننا تخيل حركة مرور العميل كالماء المتدفق من أنبوب ماء بمعدل تدفق غير مؤكد، أحيانًا سريع وأحيانًا بطيء. وحدة معالجة حركة المرور الخارجية، وهي الدلو الذي يستقبل الماء، به ثقب في الأسفل للتسرب. هذا هو أصل اسم خوارزمية الدلو المتسرب، والتي لها الفوائد التالية:
أولاً، سواء كان التدفق إلى الدلو عبارة عن تدفق بسيط أو فيضان هائل، فإنه يتم ضمان أن معدل تدفق الماء من الدلو ثابت. هذه الحركة المرورية الثابتة صديقة للخدمات العلوية، وهذا هو المقصود بتشكيل حركة المرور.
ثانيًا، الدلو نفسه له حجم معين ويمكن أن يتراكم كمية معينة من الماء. هذا يعادل طلبات العملاء التي يمكن أن تصطف في طابور إذا لم تتم معالجتها على الفور.
ثالثًا، الماء الذي يتجاوز حجم الدلو لن يتم قبوله من قبل الدلو ولكن سيتدفق بعيدًا. التشبيه المقابل هنا هو أنه إذا كانت هناك طلبات عملاء كثيرة تتجاوز طول الطابور، فسيتم إرجاع رسالة فشل مباشرة إلى العميل. في هذه اللحظة، لا يمكن للخادم التعامل مع الكثير من الطلبات ويصبح الطابور غير ضروري.
إذن كيف يجب تنفيذ هذه الخوارزمية؟ لنأخذ مكتبة resty.limit.req
التي تأتي مع OpenResty كمثال. إنها وحدة تحديد معدل تم تنفيذها باستخدام خوارزمية الدلو المتسرب. سنقدم المزيد عنها في المقال التالي. اليوم سنبدأ بنظرة سريعة على الأسطر التالية من الكود، وهي الأسطر الرئيسية:
local elapsed = now - tonumber(rec.last)
excess = max(tonumber(rec.excess) - rate * abs(elapsed) / 1000 + 1000,0)
if excess > self.burst then
return nil, "rejected"
end
-- return the delay in seconds, as well as excess
return excess / rate, excess / 1000
لنقرأ هذه الأسطر من الكود باختصار. حيث elapsed
هو عدد المللي ثانية بين الطلب الحالي والطلب الأخير، وrate
هو المعدل الذي نضبطه في الثانية. نظرًا لأن أصغر وحدة لـ rate
هي 0.001 ثانية/طلب، فإن الكود المذكور أعلاه يحتاج إلى الضرب في 1000
لحسابه.
excess
يشير إلى عدد الطلبات التي لا تزال في الطابور، 0
يعني أن الدلو فارغ، ولا توجد طلبات في الطابور، وburst
يشير إلى حجم الدلو بأكمله. إذا كان excess
أكبر من burst
، فهذا يعني أن الدلو ممتلئ، وستتم التخلص من حركة المرور الواردة مباشرة؛ إذا كان excess
أكبر من 0
وأقل من burst
، فستدخل الطابور للانتظار للمعالجة، وexcess/rate
الذي يتم إرجاعه هنا هو وقت الانتظار.
بهذه الطريقة، يمكننا التحكم في طول طابور حركة المرور المفاجئة عن طريق ضبط حجم burst
، بينما تظل قدرة معالجة الخدمة الخلفية كما هي. ولكن، بالطبع، يعتمد ذلك على سيناريو عملك سواء كنت تخبر المستخدم بأن هناك طلبات كثيرة ويجب إعادة المحاولة لاحقًا، أو تترك المستخدم ينتظر لفترة أطول.
خوارزمية دلو الرموز
كل من خوارزمية دلو الرموز وخوارزمية الدلو المتسرب لهما نفس الهدف، وهو ضمان أن الخدمات الخلفية لا تتعرض للضغط من الزيادة المفاجئة في حركة المرور، على الرغم من أن الاثنين لا يتم تنفيذهما بنفس الطريقة.
تستخدم خوارزمية الدلو المتسرب عنوان IP للقيام بأساسيات التحكم في حركة المرور وتحديد المعدل. بهذه الطريقة، يكون معدل خروج كل عميل من خوارزمية الدلو المتسرب ثابتًا. ومع ذلك، هذا يطرح مشكلة:
لنفترض أن طلبات المستخدم A
متكررة وطلبات المستخدمين الآخرين غير متكررة. في هذه الحالة، ستؤدي خوارزمية الدلو المتسرب إلى إبطاء أو رفض بعض طلبات A
، على الرغم من أن الخدمة يمكنها التعامل معها في ذلك الوقت، حتى لو لم يكن ضغط الخدمة العام مرتفعًا جدًا.
هنا يأتي دور دلو الرموز.
بينما تهتم خوارزمية الدلو المتسرب بتنعيم حركة المرور، يسمح دلو الرموز بمرور الزيادة المفاجئة في حركة المرور إلى الخدمة الخلفية. مبدأ دلو الرموز هو وضع الرموز في الدلو بمعدل ثابت والاستمرار في وضعها طالما أن الدلو ليس ممتلئًا. بهذه الطريقة، تحتاج جميع الطلبات القادمة من النهاية إلى الذهاب إلى دلو الرموز للحصول على الرمز أولاً قبل أن تتمكن الخدمة الخلفية من معالجتها؛ إذا لم يكن هناك رمز داخل الدلو، فسيتم رفض الطلب.
ومع ذلك، لا ينفذ OpenResty دلاء الرموز للتحكم في حركة المرور وتحديد المعدل في مكتبته. لذا، هنا مقدمة موجزة لوحدة تحديد المعدل القائمة على دلو الرموز lua-resty-limit-rate
، والتي تم فتح مصدرها من قبل UPYUN، كمثال:
local limit_rate = require "resty.limit.rate"
-- global 20r/s 6000r/5m
local lim_global = limit_rate.new("my_limit_rate_store", 100, 6000, 2)
-- single 2r/s 600r/5m
local lim_single = limit_rate.new("my_limit_rate_store", 500, 600, 1)
local t0, err = lim_global:take_available("__global__", 1)
local t1, err = lim_single:take_available(ngx.var.arg_userid, 1)
if t0 == 1 then
return -- global bucket is not hungry
else
if t1 == 1 then
return -- single bucket is not hungry
else
return ngx.exit(503)
end
end
في هذا الكود، قمنا بإعداد دلوين للرموز: دلو رمز عام، ودلو رمز مع ngx.var.arg_userid
كمفتاح، مقسم حسب المستخدم. هناك مزيج من الدلوين، والذي له الفائدة الرئيسية التالية:
- عدم الحاجة إلى تحديد دلو رمز المستخدم إذا كانت هناك رموز متاحة في دلو الرموز العام، وتقديم أكبر عدد ممكن من الطلبات المفاجئة من المستخدمين إذا كانت الخدمة الخلفية تعمل بشكل صحيح.
- في حالة عدم وجود دلو رمز عام، لا يمكن رفض الطلبات بشكل عشوائي، لذا يجب تحديد دلو رمز المستخدمين الفرديين ورفض الطلبات من المستخدمين الذين لديهم طلبات مفاجئة أكثر. بهذه الطريقة، يتم ضمان عدم تأثر طلبات المستخدمين الآخرين.
من الواضح أن دلاء الرموز أكثر مرونة من دلاء التسرب، مما يسمح بمرور الزيادة المفاجئة في حركة المرور إلى الخدمات الخلفية. ولكن، بالطبع، لكل منهما إيجابيات وسلبيات، ويمكنك اختيار استخدامهما وفقًا لظروفك.
وحدة تحديد المعدل في NGINX
بعد الانتهاء من هاتين الخوارزميتين، لننظر أخيرًا إلى كيفية تنفيذ تحديد المعدل في NGINX. في NGINX، وحدة limit_req
هي وحدة تحديد المعدل الأكثر استخدامًا، وفيما يلي تكوين بسيط:
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
server {
location /search/ {
limit_req zone=one burst=5;
}
}
يأخذ هذا الكود عنوان IP العميل كمفتاح، ويطلب مساحة ذاكرة تبلغ 10M
تسمى one
، ويحدد المعدل إلى 1
طلب في الثانية.
في موقع الخادم، يتم أيضًا الإشارة إلى قاعدة تحديد المعدل one
، ويتم ضبط brust
على 5
. إذا تجاوز المعدل 1 طلب/ثانية، يمكن أن تصطف 5
طلبات في نفس الوقت، مما يعطي منطقة عازلة معينة. إذا لم يتم ضبط brust
، سيتم رفض الطلبات التي تتجاوز المعدل مباشرة.
تعتمد وحدة NGINX هذه على دلو التسرب، لذا فهي في الأساس نفس resty.limit.req
في OpenResty، والتي وصفناها أعلاه.
الخلاصة
أكبر مشكلة في تحديد المعدل في NGINX هي أنه لا يمكن تعديلها ديناميكيًا. بعد كل شيء، تحتاج إلى إعادة تشغيل ملف التكوين بعد تعديله لجعله فعالًا، وهو أمر غير مقبول في بيئة سريعة التغير. لذلك، سننظر في المقال التالي في تنفيذ التحكم في حركة المرور وتحديد المعدل ديناميكيًا في OpenResty.
أخيرًا، لنفكر في سؤال. من منظور جدران حماية تطبيقات الويب (WAF) وبوابات API، هل هناك طريقة أفضل لتحديد ما هي طلبات المستخدمين العاديين وما هي الطلبات الخبيثة؟ لأنه بالنسبة لحركة المرور المفاجئة من المستخدمين العاديين، يمكننا بسرعة توسيع الخدمات الخلفية لزيادة سعة الخدمة، بينما بالنسبة للطلبات الخبيثة، من الأفضل رفضها مباشرة في طبقة الوصول.
نرحب بنشر هذه المقالة بين زملائك وأصدقائك للتعلم والتقدم معًا.