معالجة طبقة Layer 4 من حركة المرور وتنفيذ خادم Memcached باستخدام OpenResty
API7.ai
November 10, 2022
في بعض المقالات السابقة، قدمنا بعض واجهات برمجة تطبيقات Lua للتعامل مع الطلبات، والتي كانت جميعها مرتبطة بالطبقة السابعة. بالإضافة إلى ذلك، يوفر OpenResty وحدة stream-lua-nginx-module
للتعامل مع حركة المرور من الطبقة الرابعة. وهي توفر تعليمات وواجهات برمجة تطبيقات تشبه إلى حد كبير وحدة lua-nginx-module
.
اليوم، سنتحدث عن تنفيذ خادم Memcached باستخدام OpenResty، والذي يحتاج فقط إلى حوالي 100 سطر من التعليمات البرمجية. في هذا التمرين العملي الصغير، سنستخدم الكثير مما تعلمناه سابقًا، وسنستعرض أيضًا بعض المحتوى من فصول الاختبار وتحسين الأداء لاحقًا.
ويجب أن نكون واضحين أن الهدف من هذه المقالة ليس فهم وظائف كل سطر من التعليمات البرمجية، ولكن فهم الصورة الكاملة لكيفية تطوير OpenResty لمشروع من الصفر، من منظور المتطلبات، الاختبار، التطوير، إلخ.
المتطلبات الأصلية والحلول التقنية
نحن نعلم أن حركة مرور HTTPS أصبحت هي السائدة، ولكن بعض المتصفحات القديمة لا تدعم session tickets
، لذا نحتاج إلى تخزين معرف الجلسة على جانب الخادم. إذا كانت مساحة التخزين المحلية غير كافية، نحتاج إلى مجموعة لتخزين البيانات، ويمكن التخلص من البيانات، لذا فإن Memcached هو الأنسب.
في هذه المرحلة، إدخال Memcached يجب أن يكون الحل الأكثر مباشرة. ومع ذلك، في هذه المقالة، سنختار استخدام OpenResty لبناء عجلة للأسباب التالية.
- أولاً، إدخال Memcached مباشرة سيؤدي إلى إدخال عملية إضافية، مما يزيد من تكاليف النشر والصيانة.
- ثانيًا، المتطلبات بسيطة بما يكفي، حيث تتطلب فقط عمليات
get
وset
، وتدعم انتهاء الصلاحية. - ثالثًا، OpenResty لديه وحدة
stream
، والتي يمكنها تنفيذ هذا المطلب بسرعة.
بما أننا نريد تنفيذ خادم Memcached، نحتاج أولاً إلى فهم بروتوكوله. بروتوكول Memcached يمكن أن يدعم TCP وUDP. هنا نستخدم TCP. فيما يلي البروتوكول المحدد لأوامر get
و set
.
Get
get value with key
Telnet command: get <key>*\r\n
Example:
get key
VALUE key 0 4 data END
Set
Save key-value to memcached
Telnet command:set <key> <flags> <exptime> <bytes> [noreply]\r\n<value>\r\n
Example:
set key 0 900 4 data
STORED
نحتاج أيضًا إلى معرفة كيفية تنفيذ "معالجة الأخطاء" في بروتوكول Memcached بالإضافة إلى get
و set
. "معالجة الأخطاء" مهمة جدًا لبرامج الخادم، ونحتاج إلى كتابة برامج تتعامل ليس فقط مع الطلبات العادية ولكن أيضًا مع الاستثناءات. على سبيل المثال، في سيناريو مثل التالي:
- Memcached يرسل طلبًا غير
get
أوset
، كيف أتعامل معه؟ - ما نوع التغذية الراجعة التي أقدمها لعميل Memcached عندما يكون هناك خطأ على جانب الخادم؟
أيضًا، نريد كتابة تطبيق عميل متوافق مع Memcached. بهذه الطريقة، لا يحتاج المستخدمون إلى التمييز بين الإصدار الرسمي لـ Memcached وتنفيذ OpenResty.
يوضح الشكل التالي من وثائق Memcached ما يجب إرجاعه في حالة حدوث خطأ والتنسيق الدقيق، والذي يمكنك استخدامه كمرجع.
الآن، دعنا نحدد الحل التقني. نعلم أن shared dict
في OpenResty يمكن استخدامه عبر worker
s، وأن وضع البيانات في shared dict
يشبه إلى حد كبير وضعها في Memcached. كلاهما يدعم عمليات get
و set
، وتفقد البيانات عند إعادة تشغيل العملية. لذلك، من المناسب استخدام shared dict
لمحاكاة Memcached، حيث أن مبادئهما وسلوكهما متشابهان.
التطوير القائم على الاختبار
الخطوة التالية هي البدء في العمل. ومع ذلك، بناءً على فكرة التطوير القائم على الاختبار، دعنا ننشئ أبسط حالة اختبار قبل أن نكتب الكود المحدد. بدلاً من استخدام إطار العمل test::nginx
، الذي يُعرف بصعوبة البدء به، دعنا نبدأ باختبار يدوي باستخدام resty
.
$ resty -e 'local memcached = require "resty.memcached"
local memc, err = memcached:new()
memc:set_timeout(1000) -- 1 sec
local ok, err = memc:connect("127.0.0.1", 11212)
local ok, err = memc:set("dog", 32)
if not ok then
ngx.say("failed to set dog: ", err)
return
end
local res, flags, err = memc:get("dog")
ngx.say("dog: ", res)'
يستخدم كود الاختبار هذا مكتبة العميل lua-rety-memcached
لبدء عمليات connect
و set
ويفترض أن خادم Memcached يستمع على المنفذ 11212
على الجهاز المحلي.
يبدو أنه يجب أن يعمل بشكل جيد. يمكنك تشغيل هذا الكود على جهازك، وليس من المستغرب أن يعيد خطأ مثل failed to set dog: closed
، حيث أن الخدمة لم تبدأ في هذه المرحلة.
في هذه المرحلة، الحل التقني واضح: استخدم وحدة stream
لاستقبال وإرسال البيانات واستخدم shared dict
لتخزينها.
المقياس لقياس اكتمال المطلب واضح: تشغيل الكود أعلاه وطباعة القيمة الفعلية لـ dog
.
بناء الإطار
إذن ما الذي تنتظره؟ ابدأ في كتابة الكود!
عادة ما أقوم ببناء إطار عمل كود قابل للتشغيل أولاً ثم أملأ الكود تدريجيًا. ميزة هذه الطريقة هي أنه يمكنك تحديد العديد من الأهداف الصغيرة خلال عملية الترميز، وستمنحك حالات الاختبار تغذية راجعة إيجابية عند تحقيق هدف صغير.
لنبدأ بإعداد ملف تكوين NGINX حيث يجب تعيين stream
و shared dict
مسبقًا فيه. فيما يلي ملف التكوين الذي قمت بإعداده.
stream {
lua_shared_dict memcached 100m;
lua_package_path 'lib/?.lua;;';
server {
listen 11212;
content_by_lua_block {
local m = require("resty.memcached.server")
m.run()
}
}
}
كما ترى، هناك عدة معلومات رئيسية في ملف التكوين هذا.
- أولاً، يعمل الكود في سياق
stream
لـ NGINX، وليس سياقHTTP
، ويستمع على المنفذ11212
. - ثانيًا، اسم
shared dict
هوmemcached
، والحجم هو100M
، ولا يمكن تغييره أثناء التشغيل. - بالإضافة إلى ذلك، يقع الكود في الدليل
lib/resty/memcached
، واسم الملف هوserver.lua
، ووظيفة الدخول هيrun()
، والتي يمكنك العثور عليها منlua_package_path
وcontent_by_lua_block
.
التالي، حان الوقت لبناء إطار الكود. يمكنك تجربته بنفسك، ثم دعنا ننظر إلى إطار الكود الخاص بي معًا.
local new_tab = require "table.new"
local str_sub = string.sub
local re_find = ngx.re.find
local mc_shdict = ngx.shared.memcached
local _M = { _VERSION = '0.01' }
local function parse_args(s, start)
end
function _M.get(tcpsock, keys)
end
function _M.set(tcpsock, res)
end
function _M.run()
local tcpsock = assert(ngx.req.socket(true))
while true do
tcpsock:settimeout(60000) -- 60 seconds
local data, err = tcpsock:receive("*l")
local command, args
if data then
local from, to, err = re_find(data, [[(\S+)]], "jo")
if from then
command = str_sub(data, from, to)
args = parse_args(data, to + 1)
end
end
if args then
local args_len = #args
if command == 'get' and args_len > 0 then
_M.get(tcpsock, args)
elseif command == "set" and args_len == 4 then
_M.set(tcpsock, args)
end
end
end
end
return _M
يحتوي مقتطف الكود هذا على المنطق الرئيسي لوظيفة الدخول run()
. على الرغم من أنني لم أقوم بأي معالجة استثناءات وأن التبعيات parse_args
، get
، و set
كلها وظائف فارغة، إلا أن هذا الإطار يعبر تمامًا عن منطق خادم Memcached.
ملء الكود
التالي، دعنا ننفذ هذه الوظائف الفارغة بالترتيب الذي يتم تنفيذ الكود به.
أولاً، يمكننا تحليل معلمات أمر Memcached وفقًا لوثائق بروتوكول Memcached.
local function parse_args(s, start)
local arr = {}
while true do
local from, to = re_find(s, [[\S+]], "jo", {pos = start})
if not from then
break
end
table.insert(arr, str_sub(s, from, to))
start = to + 1
end
return arr
end
نصيحتي هي تنفيذ إصدار بشكل بديهي أولاً، دون التفكير في أي تحسينات أداء. بعد كل شيء، الإكمال دائمًا أكثر أهمية من الكمال، والتحسين التدريجي بناءً على الإكمال هو الطريقة الوحيدة للاقتراب من الكمال.
التالي، دعنا ننفذ وظيفة get
. يمكنها الاستعلام عن عدة مفاتيح في وقت واحد، لذا أستخدم حلقة for
في الكود التالي.
function _M.get(tcpsock, keys)
local reply = ""
for i = 1, #keys do
local key = keys[i]
local value, flags = mc_shdict:get(key)
if value then
local flags = flags or 0
reply = reply .. "VALUE" .. key .. " " .. flags .. " " .. #value .. "\r\n" .. value .. "\r\n"
end
end
reply = reply .. "END\r\n"
tcpsock:settimeout(1000) -- one second timeout
local bytes, err = tcpsock:send(reply)
end
هناك سطر واحد فقط من الكود الأساسي هنا: local value, flags = mc_shdict:get(key)
، أي الاستعلام عن البيانات من shared dict
؛ أما بقية الكود فهو يتبع بروتوكول MemcAddress لتجميع السلسلة وإرسالها إلى العميل.
أخيرًا، دعنا ننظر إلى وظيفة set
. تقوم بتحويل المعلمات المستلمة إلى تنسيق واجهة برمجة تطبيقات shared dict
، وتخزين البيانات، وفي حالة الأخطاء، تتعامل معها وفقًا لبروتوكول Memcached.
function _M.set(tcpsock, res)
local reply = ""
local key = res[1]
local flags = res[2]
local exptime = res[3]
local bytes = res[4]
local value, err = tcpsock:receive(tonumber(bytes) + 2)
if str_sub(value, -2, -1) == "\r\n" then
local succ, err, forcible = mc_shdict:set(key, str_sub(value, 1, bytes), exptime, flags)
if succ then
reply = reply .. “STORED\r\n"
else
reply = reply .. "SERVER_ERROR " .. err .. “\r\n”
end
else
reply = reply .. "ERROR\r\n"
end
tcpsock:settimeout(1000) -- one second timeout
local bytes, err = tcpsock:send(reply)
end
بالإضافة إلى ذلك، يمكنك استخدام حالات الاختبار للتحقق والتصحيح باستخدام ngx.log
أثناء ملء الوظائف أعلاه. لسوء الحظ، نستخدم ngx.say
و ngx.log
للتصحيح حيث لا يوجد مصحح نقاط توقف في OpenResty، وهو ما يزال في عصر بدائي ينتظر المزيد من الاستكشاف.
الخلاصة
ينتهي هذا المشروع العملي الآن، وأخيرًا، أود أن أترك سؤالاً: هل يمكنك أخذ كود تنفيذ خادم Memcached أعلاه، تشغيله بالكامل، واجتياز حالة الاختبار؟
سؤال اليوم سيتطلب على الأرجح الكثير من الجهد، ولكن هذا لا يزال إصدارًا بدائيًا. لا يوجد معالجة أخطاء، تحسين أداء، واختبار آلي، والتي سيتم تحسينها لاحقًا.
إذا كان لديك أي شكوك حول شرح اليوم أو ممارستك، فلا تتردد في ترك تعليق ومناقشته معنا. يمكنك أيضًا مشاركة هذه المقالة مع زملائك وأصدقائك حتى نتمكن من الممارسة والتقدم معًا.