لماذا يعمل lua-resty-core بشكل أفضل؟

API7.ai

September 30, 2022

OpenResty (NGINX + Lua)

كما قلنا في الدرسين السابقين، Lua هي لغة تطوير مدمجة تحافظ على النواة قصيرة ومضغوطة. يمكنك تضمين Lua في Redis وNGINX لمساعدتك في تنفيذ منطق الأعمال بشكل مرن. كما تسمح لك Lua باستدعاء دوال C وهياكل البيانات الموجودة لتجنب إعادة اختراع العجلة.

في Lua، يمكنك استخدام Lua C API لاستدعاء دوال C، وفي LuaJIT، يمكنك استخدام FFI. بالنسبة لـ OpenResty.

  • في النواة lua-nginx-module، يتم تنفيذ API لاستدعاء دوال C باستخدام Lua C API.
  • في lua-resty-core، يتم تنفيذ بعض الواجهات البرمجية الموجودة بالفعل في lua-nginx-module باستخدام نموذج FFI.

ربما تتساءل لماذا نحتاج إلى تنفيذها باستخدام FFI؟

لا تقلق. لنأخذ ngx.base64_decode، وهي واجهة برمجية مباشرة، كمثال ونرى كيف يختلف تنفيذ Lua C API عن تنفيذ FFI. يمكنك أيضًا الحصول على فهم بديهي لأدائهم.

Lua CFunction

لنلقي نظرة على كيفية تنفيذ هذا في lua-nginx-module باستخدام Lua C API. نبحث عن decode_base64 في كود المشروع ونجد تنفيذه في ngx_http_lua_string.c.

lua_pushcfunction(L, ngx_http_lua_ngx_decode_base64);
lua_setfield(L, -2, "decode_base64");

الكود أعلاه مزعج للنظر، لكن لحسن الحظ، لا يتعين علينا الخوض في الدوال التي تبدأ بـ lua_ والدور المحدد لوسائطها؛ نحتاج فقط إلى معرفة شيء واحد - هناك CFunction مسجل هنا: ngx_http_lua_ngx_decode_base64، وهو يتوافق مع ngx.base64_decode، والذي يتوافق مع الواجهة البرمجية المعروضة للعامة.

لنستمر في "اتباع الخريطة" ونبحث عن ngx_http_lua_ngx_decode_base64 في ملف C هذا، والذي تم تعريفه في بداية الملف في:

static int ngx_http_lua_ngx_decode_base64(lua_State *L);

بالنسبة لتلك الدوال C التي يمكن استدعاؤها من Lua، يجب أن تتبع واجهتها الشكل المطلوب من Lua، وهو typedef int (*lua_CFunction)(lua_State* L). يحتوي على مؤشر L من نوع lua_State كوسيطة؛ نوع قيمته المرجعة هو عدد صحيح يشير إلى عدد القيم المرجعة، وليس القيمة المرجعة نفسها.

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

static int
 ngx_http_lua_ngx_decode_base64(lua_State *L)
 {
     ngx_str_t p, src;

    src.data = (u_char *) luaL_checklstring(L, 1, &src.len);

     p.len = ngx_base64_decoded_length(src.len);

     p.data = lua_newuserdata(L, p.len);

     if (ngx_decode_base64(&p, &src) == NGX_OK) {
         lua_pushlstring(L, (char *) p.data, p.len);

     } else {
         lua_pushnil(L);
     }

     return 1;
 }

الشيء الرئيسي في هذا الكود هو ngx_base64_decoded_length، وngx_decode_base64، وكلاهما دوال C مقدمة من NGINX.

نحن نعلم أن الدوال المكتوبة بلغة C لا يمكنها تمرير القيمة المرجعة إلى كود Lua ولكن تحتاج إلى تمرير وسائط الاستدعاء وقيمة مرجعة بين Lua وC عبر المكدس. هذا هو السبب في وجود الكثير من الكود الذي لا يمكننا فهمه للوهلة الأولى. أيضًا، لا يمكن تتبع هذا الكود بواسطة JIT، لذلك بالنسبة لـ LuaJIT، هذه العمليات في صندوق أسود ولا يمكن تحسينها.

LuaJIT FFI

على عكس FFI، يتم تنفيذ الجزء التفاعلي من FFI في Lua، والذي يمكن تتبعه بواسطة JIT وتحسينه؛ بالطبع، الكود أيضًا أكثر إيجازًا وأسهل في الفهم.

لنأخذ مثال base64_decode، الذي يتم تنفيذه باستخدام FFI في مستودعين: lua-resty-core وlua-nginx-module، ولنلق نظرة على الكود المنفذ في الأول.

ngx.decode_base64 = function (s)
     local slen = #s
     local dlen = base64_decoded_length(slen)

     local dst = get_string_buf(dlen)
     local pdlen = get_size_ptr()
     local ok = C.ngx_http_lua_ffi_decode_base64(s, slen, dst, pdlen)
     if ok == 0 then
         return nil
     end
     return ffi_string(dst, pdlen[0])
 end

ستلاحظ أن مقارنةً بـ CFunction، كود تنفيذ FFI أكثر وضوحًا، تنفيذه المحدد هو ngx_http_lua_ffi_decode_base64 في مستودع lua-nginx-module. إذا كنت مهتمًا هنا، يمكنك التحقق من أداء هذه الدالة بنفسك. إنها مباشرة، لن أنشر الكود هنا.

ومع ذلك، إذا كنت دقيقًا، هل لاحظت بعض قواعد تسمية الدوال في مقتطف الكود أعلاه؟

نعم، جميع الدوال في OpenResty لها قواعد تسمية، ويمكنك استنتاج استخدامها من خلال تسميتها. على سبيل المثال:

  • ngx_http_lua_ffi_، الدالة Lua التي تستخدم FFI لمعالجة طلبات HTTP في NGINX.
  • ngx_http_lua_ngx_، دالة Lua تستخدم دالة C لمعالجة طلبات HTTP في NGINX.
  • الدوال الأخرى التي تبدأ بـ ngx و lua هي دوال مدمجة لـ NGINX وLua على التوالي.

علاوة على ذلك، كود C في OpenResty له مواصفات كود صارمة، وأوصي بقراءة دليل نمط كود C الرسمي هنا. هذا وثيقة ضرورية للمطورين الذين يرغبون في تعلم كود C لـ OpenResty وإرسال PRs. وإلا، حتى إذا كان PR الخاص بك مكتوبًا بشكل جيد، سيتم التعليق عليه بشكل متكرر وطلب تغييره بسبب مشاكل نمط الكود.

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

LuaJIT FFI GC

عند استخدام FFI، قد نكون في حيرة: من سيدير الذاكرة المطلوبة في FFI؟ هل يجب علينا تحريرها يدويًا في C، أم يجب أن يقوم LuaJIT باستعادتها تلقائيًا؟

هنا مبدأ بسيط: LuaJIT مسؤول فقط عن الموارد التي يخصصها بنفسه؛ ffi.

على سبيل المثال، إذا طلبت كتلة ذاكرة باستخدام ffi.C.malloc، ستحتاج إلى تحريرها باستخدام ffi.C.free المقترن. وثائق LuaJIT الرسمية تحتوي على مثال مكافئ.

local p = ffi.gc(ffi.C.malloc(n), ffi.C.free)
 ...
 p = nil -- Last reference to p is gone.
 -- GC will eventually run finalizer: ffi.C.free(p)

في هذا الكود، ffi.C.malloc(n) يطلب قسمًا من الذاكرة، وffi.gc يسجل دالة رد فعل تدمير ffi.C.free، سيتم استدعاء ffi.C.free تلقائيًا عندما يتم GC لـ cdata p بواسطة LuaJIT لتحرير الذاكرة على مستوى C. و cdata يتم GC بواسطة LuaJIT. LuaJIT سيحرر p تلقائيًا في الكود أعلاه.

لاحظ أنه إذا كنت ترغب في طلب كتلة كبيرة من الذاكرة في OpenResty، أوصي باستخدام ffi.C.malloc بدلاً من ffi.new. الأسباب واضحة أيضًا.

  1. ffi.new يعيد cdata، وهو جزء من الذاكرة التي يديرها LuaJIT.
  2. LuaJIT GC لديه حد أعلى لإدارة الذاكرة، وLuaJIT في OpenResty لا يحتوي على خيار GC64 ممكّن. وبالتالي الحد الأعلى للذاكرة لعامل واحد هو فقط 2G. بمجرد تجاوز الحد الأعلى لإدارة الذاكرة في LuaJIT، سيؤدي ذلك إلى حدوث خطأ.

عند استخدام FFI، نحتاج أيضًا إلى الانتباه بشكل خاص إلى تسريبات الذاكرة. ومع ذلك، الجميع يرتكبون أخطاء، وطالما أن البشر يكتبون الكود، ستكون هناك دائمًا أخطاء.

هنا تأتي أدوات الاختبار والتصحيح القوية المحيطة بـ OpenResty في متناول اليد.

لنتحدث عن الاختبار أولاً. في نظام OpenResty، نستخدم Valgrind للكشف عن تسريبات الذاكرة.

إطار الاختبار الذي ذكرناه في الدرس السابق، test::nginx، لديه وضع خاص للكشف عن تسريبات الذاكرة لتشغيل مجموعات حالات الاختبار الوحدوية؛ تحتاج إلى تعيين متغير البيئة TEST_NGINX_USE_VALGRIND=1. سيتم تسجيل المشروع الرسمي لـ OpenResty بشكل كامل في هذا الوضع قبل إصدار الإصدار، وسنتحدث بالتفصيل في قسم الاختبار لاحقًا. سنتحدث بالتفصيل في قسم الاختبار لاحقًا.

CLI resty الخاص بـ OpenResty يحتوي أيضًا على خيار --valgrind، والذي يسمح لك بتشغيل كود Lua بمفرده، حتى إذا لم تكتب حالة اختبار.

لنلق نظرة على أدوات التصحيح.

يوفر OpenResty امتدادات تعتمد على systemtap لإجراء تحليل ديناميكي مباشر لبرامج OpenResty. يمكنك البحث عن الكلمة الرئيسية gc في مجموعة أدوات هذا المشروع، وسترى أداتين، lj-gc وlj-gc-objs.

للتحليل غير المتصل مثل core dump، يوفر OpenResty مجموعة أدوات GDB، ويمكنك أيضًا البحث عن gc فيها والعثور على الأدوات الثلاث lgc، lgcstat وlgcpath.

سيتم تغطية الاستخدام المحدد لأدوات التصحيح هذه بالتفصيل في قسم التصحيح لاحقًا حتى تتمكن من الحصول على انطباع أولي. بعد كل شيء، OpenResty لديه مجموعة مخصصة من الأدوات لمساعدتك في تحديد وحل هذه المشاكل.

lua-resty-core

من المقارنة أعلاه، يمكننا أن نرى أن طريقة FFI ليست فقط أكثر وضوحًا في الكود، ولكن يمكن أيضًا تحسينها بواسطة LuaJIT، مما يجعلها الخيار الأفضل. OpenResty قد ألغى تنفيذ CFunction، وتمت إزالة الأداء من قاعدة الكود. يتم الآن تنفيذ الواجهات البرمجية الجديدة في مستودع lua-resty-core عبر FFI.

قبل إصدار OpenResty 1.15.8.1 في مايو 2019، لم يتم تمكين lua-resty-core بشكل افتراضي، مما أدى إلى خسائر في الأداء وأخطاء محتملة، لذلك أوصي بشدة بأن يقوم أي شخص لا يزال يستخدم الإصدار التاريخي بتمكين lua-resty-core يدويًا. تحتاج فقط إلى إضافة سطر واحد من الكود في مرحلة init_by_lua.

require "resty.core"

بالطبع، تمت إضافة توجيه lua_load_resty_core في الإصدار المتأخر 1.15.8.1، وتم تمكين lua-resty-core بشكل افتراضي.

أشعر شخصيًا أن OpenResty لا يزال حذرًا جدًا بشأن تمكين lua-resty-core، ويجب على المشاريع مفتوحة المصدر تعيين ميزات مماثلة ليتم تمكينها بشكل افتراضي في أسرع وقت ممكن.

lua-resty-core لا يعيد فقط تنفيذ بعض الواجهات البرمجية من مشروع lua-nginx-module، مثل ngx.re.match، ngx.md5، إلخ، ولكنه ينفذ أيضًا عدة واجهات برمجية جديدة، مثل ngx.ssl، ngx.base64، ngx.errlog، ngx.process، ngx.re.process، وngx.ngx.md5. ngx.re.split، ngx.resp.add_header، ngx.balancer، ngx.semaphore، إلخ، والتي سنغطيها لاحقًا في فصل OpenResty API.

الخلاصة

بعد كل هذا، أود أن أختتم بأن FFI، على الرغم من كونه جيدًا، ليس حلاً سحريًا للأداء. السبب الرئيسي لكفاءته هو أنه يمكن تتبعه وتحسينه بواسطة JIT. إذا كتبت كود Lua لا يمكن JIT ويحتاج إلى التنفيذ في وضع التفسير، فإن FFI سيكون أقل كفاءة.

إذن ما هي العمليات التي يمكن JIT وما لا يمكن؟ كيف يمكننا تجنب كتابة كود لا يمكن JIT؟ سأكشف عن هذا في القسم التالي.

أخيرًا، مهمة عملية: هل يمكنك العثور على واجهة برمجية أو اثنتين في كل من lua-nginx-module وlua-resty-core، ثم مقارنة الاختلافات في اختبارات الأداء؟ يمكنك أن ترى مدى تحسن أداء FFI.

مرحبًا بترك تعليق، وسأشارك أفكارك ومكاسبك وأرحب بك لمشاركة هذه المقالة مع زملائك وأصدقائك، معًا للتبادل والتقدم.