إصدارات `lua-resty-*` التغليفية تحرر المطورين من التخزين المؤقت متعدد المستويات
API7.ai
December 30, 2022
في المقالتين السابقتين، تعلمنا عن التخزين المؤقت في OpenResty ومشكلة انهيار التخزين المؤقت، والتي كانت جميعها على الجانب الأساسي. في تطوير المشاريع الفعلية، يفضل المطورون مكتبة جاهزة للاستخدام مع جميع التفاصيل المعالجة والمخفية ويمكن استخدامها لتطوير كود الأعمال مباشرة.
هذه فائدة من تقسيم العمل، حيث يركز مطورو المكونات الأساسية على بنية مرنة وأداء جيد واستقرار الكود دون الاهتمام بمنطق الأعمال العلوي؛ بينما يهتم مهندسو التطبيقات أكثر بتنفيذ الأعمال والتكرار السريع، على أمل ألا يتم تشتيت انتباههم بالتفاصيل الفنية المختلفة للطبقة السفلية. يمكن ملء الفجوة بينهما بواسطة مكتبات التغليف.
يواجه التخزين المؤقت في OpenResty نفس المشكلة. shared dict
و lru caches
مستقران وفعالان بما فيه الكفاية، ولكن هناك الكثير من التفاصيل التي يجب التعامل معها. يمكن أن تكون "الميل الأخير" لمهندسي تطوير التطبيقات شاقًا دون بعض التغليف المفيد. هنا يأتي دور أهمية المجتمع. المجتمع النشط سيتخذ المبادرة للعثور على الفجوات وملئها بسرعة.
lua-resty-memcached-shdict
لنعد إلى تغليف التخزين المؤقت. lua-resty-memcached-shdict
هو مشروع OpenResty رسمي يستخدم shared dict
لعمل طبقة تغليف لـ memcached
، معالجة تفاصيل مثل انهيار التخزين المؤقت والبيانات المنتهية الصلاحية. إذا كانت بياناتك المخزنة مؤقتًا مخزنة في memcached
في الخلفية، فيمكنك تجربة استخدام هذه المكتبة.
إنها مكتبة تم تطويرها بواسطة OpenResty رسميًا، ولكنها ليست في حزمة OpenResty بشكل افتراضي. إذا كنت تريد اختبارها محليًا، تحتاج إلى تنزيل الكود المصدري إلى مسار البحث المحلي لـ OpenResty أولاً.
هذه المكتبة المغلقة هي نفس الحل الذي ذكرناه في المقال السابق. تستخدم lua-resty-lock
لضمان التبادل المتبادل. في حالة فشل التخزين المؤقت، يذهب طلب واحد فقط إلى memcached
لجلب البيانات وتجنب عواصف التخزين المؤقت. يتم إرجاع البيانات القديمة إلى النهاية إذا لم يتم جلب أحدث البيانات.
ومع ذلك، هذه المكتبة lua-resty
، على الرغم من كونها مشروع OpenResty رسمي، ليست مثالية:
- أولاً، لا يوجد تغطية لحالات الاختبار، مما يعني أن جودة الكود لا يمكن ضمانها بشكل متسق.
- ثانيًا، تعرض الكثير من معلمات الواجهة، مع 11 معلمة مطلوبة و7 معلمات اختيارية.
local memc_fetch, memc_store =
shdict_memc.gen_memc_methods{
tag = "my memcached server tag",
debug_logger = dlog,
warn_logger = warn,
error_logger = error_log,
locks_shdict_name = "some_lua_shared_dict_name",
shdict_set = meta_shdict_set,
shdict_get = meta_shdict_get,
disable_shdict = false, -- optional, default false
memc_host = "127.0.0.1",
memc_port = 11211,
memc_timeout = 200, -- in ms
memc_conn_pool_size = 5,
memc_fetch_retries = 2, -- optional, default 1
memc_fetch_retry_delay = 100, -- in ms, optional, default to 100 (ms)
memc_conn_max_idle_time = 10 * 1000, -- in ms, for in-pool connections,optional, default to nil
memc_store_retries = 2, -- optional, default to 1
memc_store_retry_delay = 100, -- in ms, optional, default to 100 (ms)
store_ttl = 1, -- in seconds, optional, default to 0 (i.e., never expires)
}
معظم المعلمات المعروضة يمكن تبسيطها عن طريق "إنشاء معالج memcached
جديد". الطريقة الحالية لتغليف جميع المعلمات عن طريق إلقائها على المستخدم ليست صديقة للمستخدم، لذا أرحب بالمطورين المهتمين للمساهمة بطلبات سحب لتحسين هذا.
أيضًا، يتم ذكر المزيد من التحسينات في الاتجاهات التالية في وثائق هذه المكتبة المغلقة.
- استخدام
lua-resty-lrucache
لزيادة التخزين المؤقت على مستوىWorker
، بدلاً من مجرد التخزين المؤقت على مستوىserver
باستخدامshared dict
. - استخدام
ngx.timer
للقيام بعمليات تحديث التخزين المؤقت بشكل غير متزامن.
الاتجاه الأول هو اقتراح جيد جدًا، حيث أن أداء التخزين المؤقت داخل العامل أفضل؛ الاقتراح الثاني هو شيء تحتاج إلى التفكير فيه بناءً على سيناريوك الفعلي. ومع ذلك، لا أوصي عمومًا بالثاني، ليس فقط لأن هناك حد لعدد المؤقتات، ولكن أيضًا لأنه إذا حدث خطأ في منطق التحديث هنا، فلن يتم تحديث التخزين المؤقت مرة أخرى، مما له تأثير كبير.
lua-resty-mlcache
بعد ذلك، دعونا نقدم تغليف التخزين المؤقت الشائع الاستخدام في OpenResty: lua-resty-mlcache
، الذي يستخدم shared dict
و lua-resty-lrucache
لتنفيذ آلية تخزين مؤقت متعددة الطبقات. دعونا نلقي نظرة على كيفية استخدام هذه المكتبة في المثالين التاليين.
local mlcache = require "resty.mlcache"
local cache, err = mlcache.new("cache_name", "cache_dict", {
lru_size = 500, -- حجم التخزين المؤقت L1 (Lua VM)
ttl = 3600, -- 1h ttl للضربات
neg_ttl = 30, -- 30s ttl للفشل
})
if not cache then
error("failed to create mlcache: " .. err)
end
لنلق نظرة على الكود الأول. بداية هذا الكود تقدم مكتبة mlcache
وتحدد المعلمات للتهيئة. نضع هذا الكود عادةً في مرحلة init
ونحتاج إلى القيام بذلك مرة واحدة فقط.
بالإضافة إلى المعلمتين المطلوبتين، اسم التخزين المؤقت واسم القاموس، المعلمة الثالثة هي قاموس مع 12 خيارًا اختياريًا يستخدم القيم الافتراضية إذا لم يتم ملؤها. هذا أكثر أناقة من lua-resty-memcached-shdict
. إذا كنا نصمم الواجهة بأنفسنا، سيكون من الأفضل تبني نهج mlcache
- الحفاظ على الواجهة بسيطة قدر الإمكان مع الاحتفاظ بمرونة كافية.
هنا الكود الثاني، وهو الكود المنطقي عند معالجة الطلب.
local function fetch_user(id)
return db:query_user(id)
end
local id = 123
local user , err = cache:get(id , nil , fetch_user , id)
if err then
ngx.log(ngx.ERR , "failed to fetch user: ", err)
return
end
if user then
print(user.id) -- 123
end
كما ترى، يتم إخفاء التخزين المؤقت متعدد الطبقات، لذا تحتاج إلى استخدام كائن mlcache
لجلب التخزين المؤقت وتعيين وظيفة الاستدعاء عند انتهاء صلاحية التخزين المؤقت. يمكن إخفاء المنطق المعقد وراء هذا تمامًا.
قد تكون فضوليًا حول كيفية تنفيذ هذه المكتبة داخليًا. بعد ذلك، دعونا نلقي نظرة أخرى على بنية وتنفيذ هذه المكتبة. الصورة التالية هي شريحة من حديث قدمه Thibault Charbonnier، مؤلف mlcache
، في OpenResty Con 2018.
كما ترى من الرسم البياني، يقسم mlcache
البيانات إلى ثلاث طبقات، وهي L1
، L2
و L3
.
التخزين المؤقت L1
هو lua-resty-lrucache
، حيث يكون لكل Worker
نسخته الخاصة، ومع N
Worker
s، هناك N
نسخ من البيانات، لذا هناك تكرار للبيانات. نظرًا لأن تشغيل lrucache
داخل Worker
واحد لا يؤدي إلى تشغيل الأقفال، فإنه يتمتع بأداء أعلى وهو مناسب كتخزين مؤقت من المستوى الأول.
التخزين المؤقت L2
هو shared dict
. جميع Worker
s يشاركون نسخة واحدة من البيانات المخزنة مؤقتًا وسيتم استعلام التخزين المؤقت L2
إذا لم يتم ضرب التخزين المؤقت L1
. يوفر ngx.shared.DICT
واجهة برمجية تستخدم أقفال الدوران لضمان ذرية العمليات، لذا لا داعي للقلق بشأن حالات السباق هنا.
L3
هو الحالة التي لا يتم فيها ضرب التخزين المؤقت L2
أيضًا، ويحتاج إلى تنفيذ وظيفة الاستدعاء لاستعلام مصدر البيانات، مثل قاعدة بيانات خارجية، ثم تخزينها مؤقتًا في L2
. هنا، لتجنب عواصف التخزين المؤقت، يستخدم lua-resty-lock
لضمان أن Worker
واحد فقط يذهب إلى مصدر البيانات للحصول على البيانات.
من منظور الطلب:
- أولاً، سيتم استعلام التخزين المؤقت
L1
داخلWorker
وإرجاعه مباشرة إذا تم ضربL1
. - إذا لم يتم ضرب
L1
أو فشل التخزين المؤقت، يتم استعلام التخزين المؤقتL2
بينWorker
s. إذا تم ضربL2
، يتم إرجاعه وتخزين النتيجة فيL1
. - إذا لم يتم ضرب
L2
أيضًا أو تم إبطال التخزين المؤقت، يتم استدعاء وظيفة الاستدعاء للبحث عن البيانات من مصدر البيانات وكتابتها في التخزين المؤقتL2
، وهي وظيفة طبقة البياناتL3
.
يمكنك أيضًا أن ترى من هذه العملية أن تحديثات التخزين المؤقت يتم تشغيلها بشكل سلبي بواسطة طلبات النهاية. حتى إذا فشل طلب في جلب التخزين المؤقت، يمكن للطلبات اللاحقة أن تطلق منطق التحديث لتعظيم أمان التخزين المؤقت.
ومع ذلك، على الرغم من أن mlcache
تم تنفيذها بشكل مثالي، لا يزال هناك نقطة ألم - تسلسل وإلغاء تسلسل البيانات. هذه ليست مشكلة مع mlcache
، ولكن الفرق بين lrucache
و shared dict
، الذي ذكرناه مرارًا وتكرارًا. في lrucache
، يمكننا تخزين أنواع بيانات Lua المختلفة، بما في ذلك table
؛ ولكن في shared dict
، يمكننا فقط تخزين السلاسل النصية.
التخزين المؤقت L1
، lrucache
، هو طبقة البيانات التي يلمسها المستخدمون، ونريد تخزين جميع أنواع البيانات فيه، بما في ذلك string
، table
، cdata
، وما إلى ذلك. المشكلة هي أن L2
يمكنه فقط تخزين السلاسل النصية، وعندما يتم رفع البيانات من L2
إلى L1
، نحتاج إلى إجراء طبقة تحويل من السلاسل النصية إلى أنواع البيانات التي يمكننا تقديمها مباشرة للمستخدم.
لحسن الحظ، أخذ mlcache
هذا الوضع في الاعتبار وقدم وظائف اختيارية l1_serializer
في واجهات new
و get
، مصممة خصيصًا للتعامل مع معالجة البيانات عند رفع L2
إلى L1
. يمكننا رؤية الكود النموذجي التالي، الذي استخرجته من مجموعة حالات الاختبار الخاصة بي.
local mlcache = require "resty.mlcache"
local cache, err = mlcache.new("my_mlcache", "cache_shm", {
l1_serializer = function(i)
return i + 2
end,
})
local function callback()
return 123456
end
local data = assert(cache:get("number", nil, callback))
assert(data == 123458)
دعني أشرح بسرعة. في هذه الحالة، تقوم وظيفة الاستدعاء بإرجاع الرقم 123456
؛ في new
، تقوم وظيفة l1_serializer
التي قمنا بتعيينها بإضافة 2
إلى الرقم الوارد قبل تعيين التخزين المؤقت L1
، مما يصبح 123458
. مع مثل هذه الوظيفة التسلسلية، يمكن أن تكون البيانات أكثر مرونة عند التحويل بين L1
و L2
.
الخلاصة
مع طبقات التخزين المؤقت المتعددة، يمكن تعظيم أداء الخادم، ويتم إخفاء الكثير من التفاصيل في الوسط. في هذه المرحلة، توفر لنا مكتبة التغليف المستقرة والفعالة الكثير من الجهد. آمل أيضًا أن تساعدك المكتبتان المقدمتان اليوم على فهم التخزين المؤقت بشكل أفضل.
أخيرًا، فكر في هذا السؤال: هل طبقة القاموس المشترك للتخزين المؤقت ضرورية؟ هل يمكن استخدام lrucache
فقط؟ لا تتردد في ترك تعليق ومشاركة رأيك معي، وأنت أيضًا مرحب بك لمشاركة هذه المقالة مع المزيد من الأشخاص للتواصل والتقدم معًا.