"inspect": مكون تصحيح Lua الديناميكي في Apache APISIX
JinHua Luo
January 29, 2023
لماذا نحتاج إلى إضافة 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
: جدول التجزئة للـ upvaluesvals
: جدول التجزئة للمتغيرات المحلية
- إذا كانت القيمة المرجعة من الوظيفة
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 من خلال التصحيح
يرجى قراءة هذه الوثائق ذات الصلة لمزيد من التفاصيل.