"inspect": مكون تصحيح Lua الديناميكي في Apache APISIX

JinHua Luo

January 29, 2023

Technology

لماذا نحتاج إلى إضافة Lua Dynamic Debugging Plugin؟

يحتوي Apache APISIX على كمية كبيرة من كود Lua. هل هناك طريقة لفحص قيم المتغيرات في الكود أثناء التشغيل دون تعديل الكود المصدري؟

تعديل كود Lua لأغراض التصحيح له عدة عيوب:

  • بيئات الإنتاج لا يجب ولا تسمح عادةً بتعديل الكود المصدري
  • تعديل الكود المصدري يتطلب إعادة التحميل، مما يعطل سير العمليات التجارية
  • قد يكون من الصعب تعديل الكود المصدري في البيئات المعتمدة على الحاويات
  • الأكواد المؤقتة التي يتم إنشاؤها عرضة للإهمال عند التراجع، مما يؤدي إلى مشاكل في الصيانة

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

إضافة Lua dynamic debugging، inspect، تساعدك على تحقيق المتطلبات المذكورة أعلاه.

  • قابلية تخصيص معالجة نقاط التوقف
  • إعداد نقاط التوقف بشكل ديناميكي
  • يمكن تعيين نقاط توقف متعددة
  • يمكن تعيين نقاط التوقف لتنشيط مرة واحدة فقط
  • يمكن التحكم في تأثير الأداء وإبقائه ضمن نطاق محدد

مبدأ عمل الإضافة

تستخدم الإضافة بشكل كامل وظيفة Debug API في Lua لتنفيذ ميزاتها. أثناء وضع المفسر، يمكن تعيين كل بايت كود يتم تنفيذه إلى ملف معين ورقم سطر محدد. لتعيين نقاط التوقف، نحتاج فقط إلى التحقق مما إذا كان رقم السطر يساوي القيمة المتوقعة وتنفيذ وظيفة نقطة التوقف التي قمنا بتعريفها مسبقًا. هذا يسمح لنا بمعالجة معلومات السياق الخاصة بالسطر المقابل، بما في ذلك upvalue، المتغيرات المحلية، وبعض البيانات الوصفية مثل المكدس.

يستخدم APISIX تنفيذ Lua JIT: LuaJIT، حيث يتم تجميع العديد من مسارات الكود الساخنة إلى كود آلة للتنفيذ. ومع ذلك، هذه لا تتأثر بـ Debug API، لذا نحتاج إلى مسح ذاكرة التخزين المؤقت لـ JIT قبل تفعيل نقاط التوقف. المفتاح هو أنه يمكننا اختيار مسح ذاكرة التخزين المؤقت لـ JIT لوظيفة Lua محددة فقط، مما يقلل من التأثير على الأداء العام. عند تشغيل البرنامج، سيتم تسمية العديد من كتل الكود التي تم تجميعها بواسطة JIT باسم trace في LuaJIT. هذه الـ traces مرتبطة بوظائف Lua، وقد تتضمن وظيفة Lua واحدة عدة traces، تشير إلى مسارات ساخنة مختلفة داخل الوظيفة.

يمكننا تحديد كائنات الوظائف الخاصة بها ومسح ذاكرة التخزين المؤقت لـ JIT للوظائف العامة وعلى مستوى الوحدة. ومع ذلك، إذا كان رقم السطر يتوافق مع أنواع وظائف أخرى، مثل الوظائف المجهولة، لا يمكننا الحصول على كائن الوظيفة بشكل عام. في مثل هذه الحالات، يمكننا فقط مسح جميع ذاكرة التخزين المؤقت لـ JIT. لا يمكن إنشاء traces جديدة أثناء التصحيح، ولكن الـ traces الموجودة التي لم يتم مسحها تستمر في التشغيل. طالما لدينا تحكم كافٍ، لن يتأثر أداء البرنامج، حيث أن النظام الذي يعمل لفترة طويلة عادةً لن يولد traces جديدة. بمجرد انتهاء التصحيح وإلغاء جميع نقاط التوقف، سيعود النظام إلى وضع JIT العادي، وسيتم إعادة إنشاء ذاكرة التخزين المؤقت لـ JIT التي تم مسحها بمجرد العودة إلى النقطة الساخنة.

التثبيت والإعداد

هذه الإضافة مفعلة بشكل افتراضي.

قم بتكوين conf/confg.yaml بشكل صحيح لتمكين هذه الإضافة:

plugins:
...
  - inspect
plugin_attr:
  inspect:
    delay: 3
    hooks_file: "/usr/local/apisix/plugin_inspect_hooks.lua"

تقرأ الإضافة تعريفات نقاط التوقف من الملف '/usr/local/apisix/plugin_inspect_hooks.lua' كل 3 ثوانٍ بشكل افتراضي. لتصحيح الأخطاء، تحتاج فقط إلى تعديل هذا الملف.

نوصي بإنشاء رابط رمزي إلى هذا المسار لتسهيل أرشفة الإصدارات التاريخية المختلفة لملف نقاط التوقف.

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

بشكل عام، ليس من الضروري حذف الملف، حيث يمكنك عند تعريف نقاط التوقف تحديد متى يتم إلغاؤها.

حذف هذا الملف سيؤدي إلى إلغاء جميع نقاط التوقف لجميع عمليات العامل.

سيتم تسجيل سجلات بدء وإيقاف نقاط التوقف عند مستوى سجل 'WARN'.

تعريف نقاط التوقف

require("apisix.inspect.dbg").set_hook(file, line, func, filter_func)
  • file، اسم الملف، والذي يمكن أن يكون أي اسم ملف أو مسار غير غامض.
  • line، رقم السطر في الملف، يرجى ملاحظة أن نقاط التوقف مرتبطة ارتباطًا وثيقًا بأرقام الأسطر، لذا إذا تغير الكود، يجب أيضًا تغيير رقم السطر.
  • func، اسم الوظيفة التي يجب مسح traces الخاصة بها. إذا كانت nil، فسيتم مسح جميع traces في luajit vm.
  • filter_func، وظيفة Lua مخصصة تعالج نقطة التوقف
    • المعامل المدخل هو table يتضمن ما يلي:
      • finfo: القيمة المرجعة من debug.getinfo(level, "nSlf")
      • uv: جدول التجزئة للـ upvalues
      • vals: جدول التجزئة للمتغيرات المحلية
    • إذا كانت القيمة المرجعة من الوظيفة true، فسيتم إلغاء نقطة التوقف تلقائيًا. وإلا، ستستمر نقطة التوقف في أن تكون فعالة.

على سبيل المثال:

local dbg = require "apisix.inspect.dbg"
dbg.set_hook("limit-req.lua", 88, require("apisix.plugins.limit-req").access,
function(info)
    ngx.log(ngx.INFO, debug.traceback("foo traceback", 3))
    ngx.log(ngx.INFO, dbg.getname(info.finfo))
    ngx.log(ngx.INFO, "conf_key=", info.vals.conf_key)
    return true
end)
dbg.set_hook("t/lib/demo.lua", 31, require("t.lib.demo").hot2, function(info)
    if info.vals.i == 222 then
        ngx.timer.at(0, function(_, body)
            local httpc = require("resty.http").new()
            httpc:request_uri("http://127.0.0.1:9080/upstream1", {
                method = "POST",
                body = body,
            })
        end, ngx.var.request_uri .. "," .. info.vals.i)
        return true
    end
    return false
end)
--- المزيد من نقاط التوقف ...

يرجى ملاحظة أن نقطة التوقف demo تنظم بعض المعلومات وترسلها إلى خادم خارجي. أيضًا، مكتبة resty.http المستخدمة هي مكتبة غير متزامنة تعتمد على cosocket.

عند استدعاء واجهة برمجة التطبيقات غير المتزامنة لـ OpenResty، يجب إرسالها بتأخير باستخدام timer، لأن تنفيذ الوظائف على نقاط التوقف هو متزامن ومتوقف، ولن يعود إلى البرنامج الرئيسي لـ nginx للمعالجة غير المتزامنة، لذا يجب تأخيره.

حالات الاستخدام

تحديد المسارات بناءً على محتوى جسم الطلب

لنفترض أن لدينا متطلبًا: كيف يمكننا إعداد مسار يقبل فقط طلبات POST تحتوي على السلسلة APISIX: 666 في جسم الطلب؟

في تكوين المسار، يوجد حقل vars، والذي يمكن استخدامه للتحقق من قيمة متغيرات nginx لتحديد ما إذا كان يجب أن يتطابق المسار. المتغير $request_body المقدم من nginx يحتوي على قيمة جسم الطلب، لذا يمكننا استخدام هذا المتغير لتنفيذ متطلبنا.

لنجرب تكوين المسارات:

curl http://127.0.0.1:9180/apisix/admin/routes/var_route \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
    "uri": "/anything",
    "methods": ["POST"],
    "vars": [["request_body", "~~", "APISIX: 666"]],
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "httpbin.org": 1
        }
    }
}'

ثم يمكننا تجربة هذا:

curl http://127.0.0.1:9080/anything
{"error_msg":"404 Route Not Found"}
curl -i http://127.0.0.1:9080/anything -X POST -d 'hello, APISIX: 666.'
HTTP/1.1 404 Not Found
Date: Thu, 05 Jan 2023 03:53:35 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/3.0.0
{"error_msg":"404 Route Not Found"}

غريب، لماذا لا يمكننا مطابقة هذا المسار؟

سنلقي نظرة بعد ذلك على وثائق هذا المتغير في nginx:

قيمة المتغير متاحة في المواقع التي تتم معالجتها بواسطة التوجيهات proxy_pass، fastcgi_pass، uwsgi_pass، و scgi_pass عندما يتم قراءة جسم الطلب إلى ذاكرة مؤقتة.

بمعنى آخر، نحتاج أولاً إلى قراءة جسم الطلب قبل استخدام هذا المتغير.

عند مطابقة المسار، هل سيكون هذا المتغير فارغًا؟ يمكننا استخدام إضافة inspect للتحقق من ذلك.

وجدنا سطر الكود الذي يطابق المسار:

apisix/init.lua

...
api_ctx.var.request_uri = api_ctx.var.uri .. api_ctx.var.is_args .. (api_ctx.var.args or "")
router.router_http.match(api_ctx)
local route = api_ctx.matched_route
if not route then
...

لنتحقق من المتغير request_body في السطر 515، وهو router.router_http.match(api_ctx).

إعداد نقاط التوقف

قم بتحرير الملف /usr/local/apisix/example_hooks.lua:

local dbg = require("apisix.inspect.dbg")
dbg.set_hook("apisix/init.lua", 515, require("apisix").http_access_phase, function(info)
    core.log.warn("request_body=", info.vals.api_ctx.var.request_body)
    return true
end)

قم بإنشاء رابط رمزي إلى مسار ملف نقاط التوقف:

ln -sf /usr/local/apisix/example_hooks.lua /usr/local/apisix/plugin_inspect_hooks.lua

تحقق من السجلات للتأكد من أن نقطة التوقف فعالة.

2023/01/05 12:02:43 [warn] 1890559#1890559: *15736 [lua] init.lua:68: setup_hooks():
set hooks: err: true, hooks: ["apisix\/init.lua#515"], context: ngx.timer

أعد تشغيل مطابقة المسارات:

curl -i http://127.0.0.1:9080/anything -X POST -d 'hello, APISIX: 666.'

تحقق من السجلات:

2023/01/05 12:02:59 [warn] 1890559#1890559: *16152
[lua] [string "local dbg = require("apisix.inspect.dbg")..."]:39:
request_body=nil, client: 127.0.0.1, server: _,
request: "POST /anything HTTP/1.1", host: "127.0.0.1:9080"

بالطبع، request_body فارغ!

الحل

نظرًا لأننا نعلم أننا بحاجة إلى قراءة جسم الطلب لاستخدام المتغير request_body، لذا لا يمكننا استخدام vars للقيام بذلك. بدلاً من ذلك، يمكننا استخدام الحقل filter_func في المسار لتحقيق متطلبنا.

curl http://127.0.0.1:9180/apisix/admin/routes/var_route \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
    "uri": "/anything",
    "methods": ["POST"],
    "filter_func": "function(_) return require(\"apisix.core\").request.get_body():find(\"APISIX: 666\") end",
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "httpbin.org": 1
        }
    }
}'

لنتحقق:

curl http://127.0.0.1:9080/anything -X POST -d 'hello, APISIX: 666.'
{
  "args": {},
  "data": "",
  "files": {},
  "form": {
    "hello, APISIX: 666.": ""
  },
  "headers": {
    "Accept": "*/*",
    "Content-Length": "19",
    "Content-Type": "application/x-www-form-urlencoded",
    "Host": "127.0.0.1",
    "User-Agent": "curl/7.68.0",
    "X-Amzn-Trace-Id": "Root=1-63b64dbd-0354b6ed19d7e3b67013592e",
    "X-Forwarded-Host": "127.0.0.1"
  },
  "json": null,
  "method": "POST",
  "origin": "127.0.0.1, xxx",
  "url": "http://127.0.0.1/anything"
}

تم حل المشكلة!

طباعة بعض السجلات التي تم حظرها بواسطة مستوى السجل.

بشكل عام، لا نفعّل سجلات مستوى INFO في بيئة الإنتاج، ولكن أحيانًا نحتاج إلى التحقق من بعض المعلومات التفصيلية. كيف يمكننا القيام بذلك؟

عادةً ما لن نقوم بتعيين مستوى INFO مباشرةً ثم إعادة التحميل، حيث أن لهذا عيبين:

  • الكثير من السجلات، مما يؤثر على الأداء ويزيد من صعوبة التحقق
  • إعادة التحميل تؤدي إلى قطع الاتصالات المستمرة، مما يؤثر على حركة المرور عبر الإنترنت

عادةً، نحتاج فقط إلى التحقق من سجلات نقطة محددة؛ على سبيل المثال، نعلم جميعًا أن APISIX يستخدم etcd كقاعدة بيانات لتوزيع التكوين، لذا هل يمكننا رؤية متى يتم تحديث تكوين المسار بشكل تدريجي إلى مستوى البيانات؟ ما هي البيانات المحددة التي تم تحديثها؟

apisix/core/config_etcd.lua

local function sync_data(self)
...
    log.info("waitdir key: ", self.key, " prev_index: ", self.prev_index + 1)
    log.info("res: ", json.delay_encode(dir_res, true), ", err: ", err)
...
end

وظيفة Lua للمزامنة التدريجية هي sync_data()، ولكنها تطبع البيانات التدريجية من etcd watch في مستوى INFO.

لذا دعونا نجرب استخدام إضافة inspect لعرض ذلك. سنعرض فقط تغييرات موارد المسارات.

قم بتحرير /usr/local/apisix/example_hooks.lua:

local dbg = require("apisix.inspect.dbg")
local core = require("apisix.core")
dbg.set_hook("apisix/core/config_etcd.lua", 393, nil, function(info)
    local filter_res = "/routes"
    if info.vals.self.key:sub(-#filter_res) == filter_res and not info.vals.err then
        core.log.warn("etcd watch /routes response: ", core.json.encode(info.vals.dir_res, true))
        return true
    end
    return false
end)

منطق وظيفة معالجة نقطة التوقف يوضح بوضوح قدرة التصفية. إذا كان key من watch هو /routes، وكان err فارغًا، فسيتم طباعة البيانات التي تم إرجاعها من etcd مرة واحدة فقط، ثم إلغاء نقطة التوقف.

لاحظ أن sync_data() هي وظيفة محلية، لذا لا يمكن الرجوع إليها مباشرة. في هذه الحالة، يمكننا فقط تعيين المعامل الثالث لـ set_hook ليكون nil، والذي له تأثير جانبي يتمثل في مسح جميع traces.

في المثال أعلاه، قمنا بإنشاء رابط رمزي، لذا نحتاج فقط إلى حفظ الملفات بعد التحرير. سيتم تفعيل نقاط التوقف بعد بضع ثوانٍ؛ يمكنك التحقق من السجل للتأكد.

من خلال التحقق من السجل، يمكننا الحصول على المعلومات التي نحتاجها، والتي يتم طباعتها عند مستوى سجل 'WARN'. كما يظهر الوقت الذي نحصل فيه على البيانات التدريجية من etcd على مستوى البيانات.

2023/01/05 14:33:10 [warn] 1890562#1890562: *231311
[lua] [string "local dbg = require("apisix.inspect.dbg")..."]:41:
etcd watch /routes response: {"headers":{"X-Etcd-Index":"24433"},
"body":{"node":[{"value":{"uri":"\/anything",
"plugins":{"request-id":{"header_name":"X-Request-Id","include_in_response":true,"algorithm":"uuid"}},
"create_time":1672898912,"status":1,"priority":0,"update_time":1672900390,
"upstream":{"nodes":{"httpbin.org":1},"hash_on":"vars","type":"roundrobin","pass_host":"pass","scheme":"http"},
"id":"reqid"},"key":"\/apisix\/routes\/reqid","modifiedIndex":24433,"createdIndex":24429}]}}, context: ngx.timer

الخلاصة

تصحيح Lua الديناميكي هو وظيفة مساعدة مهمة. مع إضافة inspect في APISIX، يمكننا القيام بالعديد من الأشياء مثل:

  • استكشاف الأخطاء وإصلاحها وتحديد سبب المشكلات
  • طباعة بعض السجلات المحظورة واسترداد المعلومات المختلفة حسب الحاجة
  • تعلم كود Lua من خلال التصحيح

يرجى قراءة هذه الوثائق ذات الصلة لمزيد من التفاصيل.

Tags: