詳解 APISIX Lua 動態調試插件 inspect

来源:https://www.cnblogs.com/apisix/archive/2023/04/14/17317405.html
-Advertisement-
Play Games

作者羅錦華,API7.ai 技術專家/技術工程師,開源項目 pgcat,lua-resty-ffi,lua-resty-inspect 的作者。 原文鏈接 為什麼需要 Lua 動態調試插件? Apache APISIX 有很多 Lua 代碼,如何在運行時不觸碰源代碼的情況下,檢查代碼裡面的變數值? ...


作者羅錦華,API7.ai 技術專家/技術工程師,開源項目 pgcat,lua-resty-ffi,lua-resty-inspect 的作者。

原文鏈接

為什麼需要 Lua 動態調試插件?

Apache APISIX 有很多 Lua 代碼,如何在運行時不觸碰源代碼的情況下,檢查代碼裡面的變數值?

修改 Lua 源碼來調試有如下缺點:

  • 生產環境不允許也不應該修改源碼
  • 修改源碼需要 reload,使得業務功能失效
  • 容器環境難以修改源碼
  • 產生的臨時代碼容易忘記回滾,導致維護問題

很多時候我們不僅僅需要在函數開始或結束的時候去檢查變數,而且需要在滿足一定條件,例如某個迴圈體被迴圈到了一定次數,
或者某個條件判斷為真的時候我們才查看變數值,並且也不僅僅是簡單列印變數值,有時候還可能需要將相關信息發送到外圍系統。
並且,這個過程如何做到動態化呢?而且,開啟調試後,能否不影響程式運行的性能呢?

Lua 動態調試插件就是輔助你完成以上需求的插件,該插件被命名為 inspect 插件。

  • 斷點處理可定製
  • 斷點設置動態化
  • 多個斷點
  • 斷點可被定義為只生效一次
  • 可控制性能影響範圍

插件原理

它充分利用了 Lua 提供的 Debug API 來實現功能。解釋器模式執行的每一個位元組碼都可以對應到它所屬的文件以及行號,我們只需要判斷行號是否等於期望值,然後執行我們定義的斷點函數,對該行對應的上下文信息,包括 upvalue ,局部變數,還有一些元信息,例如堆棧,進行處理即可。

APISIX 使用的是 Lua 的 JIT 實現:LuaJIT,很多熱點代碼路徑會被編譯成機器碼執行,而它們是不受 Debug API 的影響的,所以我們需要在開啟斷點前清空 JIT 緩存。關鍵就在這裡了,我們可以選擇只清空某個具體 Lua 函數的 JIT 緩存,減小對全局性能的影響。一個程式運行起來,會有很多 JIT 編譯代碼塊,在 LuaJIT 里被稱為 trace,這些 trace 跟 Lua 函數是關聯起來的,一個 Lua 函數可能包括多個 trace ,指代函數內不同的熱點路徑。

對於全局函數、模塊級別的函數,我們可以指定它們的函數對象,清空它們的 JIT 緩存。但是如果某行號對應的是其他函數類型,例如匿名函數,我們無法在全局獲取函數的對象,那麼只能清空所有 JIT 緩存了。在調試開啟期間,新的 trace 無法被生成,但是已有的未被清理的 trace 還繼續運行,所以只要控制的好,程式性能不會受到影響,因為一個已經運行很久的線上系統,基本不會有新 trace 的生成。當調試結束後,也就是所有斷點都被撤銷後,系統會恢復正常的 JIT 模式,被清理掉的 JIT 緩存,一旦重新進入熱點,會被重新生成 trace。

安裝與配置

該插件預設被啟用。

配置好 conf/confg.yaml 啟用插件:

plugins:
...
  - inspect

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

插件預設每隔3秒從文件 /usr/local/apisix/plugin_inspect_hooks.lua 讀取斷點定義,想調試就編輯該文件即可。

建議創建軟鏈接到該路徑,這樣比較方便地存檔不同歷史版本的斷點文件。

註意每次該文件的更改時間有變,插件會清空所有舊的斷點,並且啟用斷點文件所定義的所有新斷點。斷點將在所有工作進程生效。

一般情況下不需要刪除該文件,因為定義斷點的時候,可以定義什麼時候撤銷斷點。

刪除文件會取消所有工作進程的所有斷點。

斷點的啟停都會通過 WARN 日誌級別列印日誌。

定義斷點

require("apisix.inspect.dbg").set_hook(file, line, func, filter_func)
  • file 文件名,可以是任何無歧義的文件名部分,可包含路徑
  • line 文件的行號,註意斷點跟行號是密切掛鉤的,所以如果代碼變了,行號就得跟著變。
  • func 要清除哪個函數的 trace,如果為 nil,則清除 luajit vm 裡面所有 trace
  • filter_func 處理該斷點的自定義 Lua 函數
    • 函數的入參為一個 table,包含以下內容
      • finfo: debug.getinfo(level, "nSlf")的返回值
      • uv: upvalues hash table
      • vals: local variables hash table
    • 函數的返回值為 true,則該斷點自動註銷,返回為 false,則該斷點繼續生效

例子:

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)

--- more breakpoints ...

註意到 demo 這個斷點,它將一些信息整理後發送到外部的伺服器上,使用的 resty.http 庫是基於 cosocket 的非同步庫。

凡是調用 OpenResty 的非同步 API ,必須使用 timer 延遲發送,因為在斷點上執行函數是同步阻塞的,不會再返回到 nginx 的主程式做非同步處理,所以需要延後發送。

使用示例

根據請求體的內容來決定路由

假設我們有個需求,如何設置讓某個路由僅接受請求體中攜帶了 APISIX: 666 的 POST 請求?

路由配置裡面有個 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 對該變數的文檔說明:

The variable’s value is made available in locations processed by the proxy_pass, fastcgi_pass, uwsgi_pass, and scgi_pass directives when the request body was read to a memory buffer.

也就是說,使用該變數前需要先讀取 request body 。

那是不是匹配路由的時候,這個變數為空呢?我們可以使用 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
...

我們就在 515 行,也就是 router.router_http.match(api_ctx) 這行驗證一下變數 request_body 吧。

設置斷點

編輯文件 /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 級別然後 reload,因為這樣做有兩個缺點:

  • 日誌太多,影響性能和加大檢查難度
  • reload 導致長連接被斷開,影響線上流量

一般我們只需要檢查具體某個點的日誌,例如我們都知道 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(),但是它是通過 INFO 級別來列印從 etcd watch 到的增量數據的。

那麼我們來試一下使用 inspect plugin 來顯示一下?只顯示路由資源的變化。

編輯 /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)

這個斷點處理函數的邏輯很好表達了過濾能力,如果 watch 的 key/routes,以及 err 為空的情況下,就列印 etcd 返回的數據,並且列印一次就夠了,就取消斷點。

註意 sync_data() 是局部函數,所以無法獲取它的引用,我們只能設置 set_hook 的第三個參數為 nil,這樣做的副作用就是它會清空所有 trace

上面例子我們已經創建了軟鏈接,所以編輯後保存文件即可。等幾秒鐘後,斷點就會被啟用,可觀察日誌確認。

檢查日誌,我們可以得到我們需要的信息,而這些信息用 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 動態調試是很重要的輔助功能。我們可以通過 APISIX inspect 插件來做很多事情,例如:

  • 排查問題,定位原因
  • 列印一些被屏蔽的日誌,按需獲取各種信息
  • 通過調試來學習 Lua 代碼

更多詳情請查閱相關文檔介紹

關於 API7.ai 與 APISIX

API7.ai 是一家提供 API 處理和分析的開源基礎軟體公司,於 2019 年開源了新一代雲原生 API 網關 -- APISIX 並捐贈給 Apache 軟體基金會。此後,API7.ai 一直積極投入支持 Apache APISIX 的開發、維護和社區運營。與千萬貢獻者、使用者、支持者一起做出世界級的開源項目,是 API7.ai 努力的目標。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。 GreatSQL是MySQL的國產分支版本,使用上與MySQL一致。 作者: Yejinrong/葉金榮 文章來源:GreatSQL社區投稿 背景介紹 編譯環境 編譯前準備工作 編譯GreatSQL 初始化並啟動Great ...
  • 本文由葡萄城技術團隊於博客園原創並首發轉載請註明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。 項目想做數據可視化,想同時在PC端、手機端查看數據怎麼辦?業務主要關心的數據包括:銷售數據、業績達成、同比、環比,各產品銷售情況及潛客商機、未來收入預測等數據,最好附加人 ...
  • 新媒體時代,廣告樣式越來越豐富。相較於傳統的圖文信息,視頻類廣告更具有直觀性,能夠讓消費者在瞭解產品知識和功能的同時加深對產品的印象。 因此在各類網站或App上投放視頻類廣告是個很好的宣傳方式,但廣告商們如果想在網站上展示視頻廣告,必須確保視頻廣告投放協議與發佈渠道的播放器相容;如果不能相容,廣告商 ...
  • “我苦心鍛煉了三年,我變禿了,也變強了。” —— 琦玉老師 0x00 大綱 0x01 前言 四個月前,我在《你是來找茬的吧?對自己的博客進行調優》一文中探討了以博客的使用者而不是開發者身份去進行優化,究竟能做到何種程度的問題。當時以 Edge 瀏覽器的開發者工具里的 lighthouse 評分和載入 ...
  • 有一朋友想把網頁內容變成PDF下載下來。問我有沒有好辦法。 這還真巧了,咱公司也有這個需求,就是網頁生成合同,然後可以直接列印合同內容。最早吧,就是可以直接列印就好了。 當時為解決完美列印的問題,挺費勁的,當時第三方插件還有BUG(當然把解決放給發給作者了,作者早已經修複了),正經反覆折騰了好一陣子 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 Vue.js是一個基於組件化和響應式數據流的前端框架。當我們在Vue中編寫模板代碼時,它會被Vue編譯器處理並轉換為可被瀏覽器解析的JavaScript代碼。Vue中的模板實際上是HTML標記和Vue指令的組合,它們會被Vue編譯器處理並 ...
  • <!-- 封裝的模板下載和導入按鈕和功能組件--> <template> <span style="margin-left: 10px"> <el-button size="mini" class="el-icon-download" @click="downFiles"> 下載模板</el-but ...
  • 前言 前端開發者若要進行後端開發,大多都會選擇node.js,在node生態下是有大量框架的,其中最受新手喜愛的便是老牌的express.js,接下來我們就從零創建一個express項目。 安裝node 在這裡:https://nodejs.org/dist/v16.14.0/node-v16.14 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...