什麼是 LuaJIT?為什麼 Apache APISIX 選擇了 LuaJIT?

語言: CN / TW / HK

本文介紹了 LuaJIT 的高靈活性和高性能,以及 APISIX 作為雲原生 API 網關選擇 LuaJIT 的原因。

作者楊陶,API7.ai 技術工程師。

原文鏈接

什麼是 LuaJIT

定義

簡單地説,LuaJIT 是 Lua 這種編程語言的實時編譯(JIT,Just-In-Time Compilation)器的實現。 對於不太瞭解 LuaJIT 的讀者,我們可以將 LuaJIT 拆成 Lua 和 JIT 兩個部分來理解。

Lua

Lua 是一種優雅、易於學習的編程語言,具有自動內存管理、完整的詞法作用域、閉包、迭代器、協程、正確的尾部調用以及使用關聯數組進行非常實用的數據處理。本文不會涉及 Lua 的語法,有關內容歡迎閲讀 Getting Started With Lua

Lua 的設計目標是能與 C 或其它常用的編程語言相互集成,這樣就可以利用其它語言已經做好的方面;而它提供的特性又恰好是 C 這類語言不太擅長的,比如相對於硬件層的高層抽象,動態的結構,簡易的測試等等。其袖珍的語言內核和只依賴於 ANSI C 標準的特點,使之在各平台上的可移植性變得非常高。因此 Lua 不僅是一個可以作為獨立程序運行的腳本語言,也是一個可以嵌入其它應用的嵌入式語言。

Apache APISIX 就是一個在底層同時使用 Lua 和 C 的極佳例子。

但此時的 Lua 還有傳統腳本語言常見的兩個問題:效率低和代碼暴露。而 LuaJIT 引入的 JIT 技術能夠有效地解決了這兩個問題。

JIT

JIT(Just-In-Time Compilation),實時編譯,是動態編譯的一種形式。在計算機科學中,動態編譯並不是唯一的編譯形式,比如現今仍然流行的 C 語言使用的就是另一種形式:靜態編譯。

需要指出的是,我們也常常將 C 語言的這種與動態編譯相反的編譯方式稱為提前編譯(AOT,Ahead-of-Time Compilation),但二者並不是完全對等的。AOT 僅是描述在執行程序前,將某種“高級”語言編譯為某種“低級”語言的行為。其編譯的目標語言並不一定特定於程序宿主機上的機器碼,而是任意定義的。比如將 Java 編譯為 C,或者將 JavaScript 編譯為 V8 等等這些行為也會被視為 AOT。由於所有靜態編譯在技術上都是提前執行的,所以在這種特定的上下文中使用時,我們可以將 AOT 視為與 JIT 相反的靜態編譯。

拋開這些宂雜的名詞,想到靜態編譯的產物,你可能會發現,Lua 語言面臨的問題也可以通過靜態編譯來解決。但事實上,這就丟失了 Lua 作為腳本語言的優勢:熱更新的靈活性和良好的平台兼容性。所以目前除了有特殊需求的腳本語言外,大部分腳本語言都在使用 JIT 嘗試提高語言性能,比如 Chromium 平台上使用 V8 的 JavaScript 和使用 YJIT 的 Ruby。

JIT 嘗試將 Lua 的動態解釋和 C 的靜態編譯兩者的優缺點相結合,在腳本語言的執行期間,通過不斷地分析正在執行的代碼片段,編譯或重新編譯這段代碼,以得到執行效率的提升。此時,JIT 假設的目標是,由此得到的性能提升能夠高於編譯或重新編譯這段代碼的開銷。理論上説,由於能夠進行動態地重新編譯,JIT 在此過程中,可以針對正在運行程序的特定平台架構進行優化、加速,在某些情況下,能產生比靜態編譯更快的執行速度。

JIT 分為傳統的 Method JIT 和 LuaJIT 正在使用的 Trace JIT 兩種。Method JIT 是將每一個方法(Method)翻譯為機器碼;而如下圖所示,更先進的Trace JIT 假定 “對只執行一兩次的代碼,解釋執行比 JIT 編譯執行要快”,以此為依據對傳統 JIT 進行優化,具體表現為將頻繁執行的代碼片段(即熱路徑上的代碼)認定為需要跟蹤的代碼,將這部分代碼編譯成機器碼執行。

LuaJIT 的原理

LuaJIT

而 LuaJIT(2.x 版本)在 Trace JIT 的基礎上,集成了使用匯編編寫的高速解釋器和基於 SSA 並進行優化的代碼生成器後端,大幅提高了 JIT 的表現,最終使得 LuaJIT 成為最快的動態語言實現之一。

除此之外,相對於原生 Lua 中為了與 C 交互而需要編寫 Lua 與 C 的繁複綁定,LuaJIT 還實現了 FFI(外部函數接口,Foreign Function Interface)。該技術允許了我們在不清楚參數個數和類型的情況下,從 Lua 代碼中直接調用外部的 C 函數和使用 C 的數據結構。由此功能,我們也可以直接使用 FFI 實現所需的數據結構,而非 Lua 原生的 Table 類型,進一步在性能敏感的場景下,提升程序運行的速度。有關使用 FFI 提高性能的技巧並非本文討論的範疇,更深入的內容可以參閲 Why Does lua-resty-core Perform Better?

總而言之,LuaJIT 在 Lua 語法的基礎上,實現了迄今為止腳本語言中最快的 Trace JIT 之一,並提供了 FFI 等功能,解決了 Lua 效率低和代碼暴露的問題,讓 Lua 真正成為了高靈活性、高性能和超低內存佔用的腳本語言和嵌入式語言。

與其它語言、WASM 的對比

相對於 Lua 和 LuaJIT,我們可能對其它的一些語言更加熟悉,比如 JavaScript (Node.js),Python,Golang,Java 等。對比這些大眾化的語言,我們可以看到更多 LuaJIT 的特性和優勢,下面簡單羅列一些 Lua/LuaJIT 與這些語言的對比:

  • Lua 的語法設計是針對非軟件工程師所設計的。所以像 R 語言一樣,Lua 也擁有數組下標從 1 開始等適合普通人的設計。
  • Lua 非常適合作為嵌入式語言。Lua 本身擁有一個輕量的 VM,而 LuaJIT 在添加各種功能和優化後,也仍然很輕量。所以相對 Node.js 和 Python 之類龐大的運行環境,LuaJIT 直接集成到 C 編寫的程序中後也不會增大太多體積。因此,實際上 Lua 是所有嵌入式語言中使用量比較大且主流的選擇。
  • Lua 也很適合做“膠水”語言。類似 JavaScript(Node.js) 和 Python,Lua 也能很好地連接不同的庫和代碼。但稍有不同的是,Lua 與底層生態的耦合性更高,所以在不同的領域中,Lua 的生態可能並不通用。

WASM(Web Assembly)是一種新興的跨平台技術。這種起初設計為補充而非取代 JavaScript 的技術,因為能夠將其它的語言編譯成 WASM 字節碼,同時還能作為安全沙箱運行代碼,使得越來越多的程序也在考慮使用 WASM 作為嵌入或者膠水的平台。即便如此,Lua/LuaJIT 在對比新興的 WASM 時,也仍然有不少優勢:

  • WASM 的性能是受限的,無法達到彙編的水準。普遍場景下的性能,WASM 肯定好過 Lua,但與 LuaJIT 有所差距。
  • WASM 與宿主程序的數據傳遞效率比較低。而 LuaJIT 可以通過 FFI 進行高效率的數據傳遞。

為什麼 Apache APISIX 選擇 LuaJIT

儘管上文描述了 LuaJIT 自身的諸多優勢,但對於大部分開發者而言,Lua 不是一門大眾的語言,LuaJIT 更不是一個大眾的選擇。那為什麼 Apache 基金會所屬的雲原生 API 網關 Apache APISIX 還是選擇了 LuaJIT 呢?

作為雲原生的 API 網關,Apache APISIX 兼具動態、實時、高性能等特點,提供了負載均衡、動態上游、灰度發佈(金絲雀發佈)、服務熔斷、身份認證、可觀測性等豐富的流量管理功能。我們可以使用 Apache APISIX 來處理傳統的南北向流量,也可以處理服務間的東西向流量,還可以用作 k8s 的 Ingress Controller。

而這一切都建立在 Apache APISIX 所選擇的 NGINX 和 LuaJIT 技術棧之上。

LuaJIT 與 NGINX 結合帶來的優勢

NGINX 是一個知名的高性能 HTTP 、TCP/UDP 代理和反向代理的 Web 服務器。

但在使用中,我們會發現很惱人的是,每次修改 NGINX 的配置文件後,都需要使用 nginx -s reload 重新加載 NGINX 配置。

不僅如此,頻繁地使用該命令重新加載配置可能會造成連接的不穩定,增加業務丟失的可能性;而在某些情況下,NGINX 重載配置的機制也可能會造成舊進程的回收時間過長,影響正常的業務。對於該問題的深入討論,可以閲讀 為什麼 NGINX 的 reload 不是熱加載?,這裏不進行深入展開。

Apache APISIX 誕生的目的之一就是解決 NGINX 的動態配置問題,LuaJIT 的高靈活性、高性能和超低內存佔用帶來了這種可能性。 以最具普遍性的路由為例,Apache APISIX 通過在 NGINX 配置文件中只配置單個 location 作為主入口,而後續的路由分發則由 APISIX 的路由分發模塊完成,以此實現了路由的動態配置。

為了實現足夠高的性能,Apache APISIX 使用 C 編寫了基於前綴樹的匹配路由算法,並在此基礎上使用 LuaJIT 提供的 FFI 編寫了適用於 Lua 的接口。而 Lua 的靈活性,也使得 Apache APISIX 的路由分發模塊,可以輕易地支持通過特定的表達式等方法,對同一前綴的下級路由進行匹配。最終在替代 NGINX 原生路由分發功能的前提下,實現了兼具高性能、高靈活性的動態配置功能。有關這部分功能的詳細實現,可以查看 lua-resty-radixtreeroute.lua

另外,不只是路由,從負載均衡、健康檢查,到上游節點配置、服務端證書,以及擴展 APISIX 能力的插件本身,都能在 APISIX 不重啟的情況下重新加載。

同時,除了在使用 LuaJIT 進行插件等功能的開發,Apache APISIX 還支持了 Java、Go、Node、Python 以及 WASM 等多種方式開發插件,也讓 Apache APISIX 的二次開發門檻大大降低,使 Apache APISIX 獲得了豐富的插件生態和活躍的開源社區。

Apache APISIX 的插件原理和生態

總結

LuaJIT 是 Lua 的實時編譯器實現。

Apache APISIX 作為一個動態、實時、高性能的開源 API 網關,基於 NGINX 與 LuaJIT 帶來的高性能、高靈活等特性,提供了負載均衡、動態上游、灰度發佈、服務熔斷、身份認證、可觀測性等豐富的流量管理功能。

目前 Apache APISIX 已經來到了全新的 3.x 版本,並帶來了更多的開源項目集成、雲供應商集成,原生的 gRPC 支持,更多的插件開發方式選擇,以及服務網格支持等功能。歡迎加入 Apache APISIX 社區,瞭解更多 LuaJIT 在雲原生 API 網關中的應用。

關於 API7.ai 與 APISIX

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