Lua 級別 CPU 火焰圖簡介
在 OpenResty 或 Nginx 伺服器中執行 Lua 程式碼如今已經變得越來越常見,因為人們希望他們的非阻塞的 Web 伺服器能夠兼具超高的效能和很大的靈活性。有些人使用 Lua 完成一些非常簡單的任務,比如檢查和修改某些請求頭和響應體資料,而有些人則利用 Lua 建立非常複雜的 Web 應用、 CDN 軟體和 API 閘道器等等。Lua 以簡單、記憶體佔用小和執行效率高而著稱,尤其是在使用 LuaJIT 這樣的的即時編譯器 (JIT) 的時候。但有些時候,在 OpenResty 或 Nginx 伺服器上執行的 Lua 程式碼也會消耗過多的 CPU 資源。通常這是由於程式設計師的程式設計錯誤,比如呼叫了一些昂貴的 C/C++ 庫程式碼,或者其他原因。
要想在一個線上的 OpenResty 或 Nginx 伺服器中快速地定位所有的 CPU 效能瓶頸,最好的方法是使用 OpenResty XRay 產品提供的 Lua 語言級別 CPU 火焰圖的取樣工具。這個工具不 需要對 OpenResty 或 Nginx 的目標程序做任何修改,也不會對生產環境中的程序產生任何可覺察的影響。
本文將解釋甚麼是火焰圖,以及甚麼是 Lua 級別的 CPU 火焰圖,會穿插使用多個小巧且獨立的 Lua 程式碼例項來做演示。我們將利用 OpenResty XRay 來生成這些示例的火焰圖來進行講解和分析。我們選擇小例子的原因是,它們更容易預測和驗證各種效能分析的結果。相同的分析方法和工具也適用於那些最複雜的 Lua 應用。過去這幾年,我們使用這種技術和視覺化方式,成功地幫助了許多擁有繁忙網站或應用的企業客戶。
甚麼是火焰圖
火焰圖是由 Brendan Gregg 發明的一種視覺化方法,用於展示某一種系統資源或效能指標,是如何定量分佈在目標軟體裡所有的程式碼路徑上的。
這裡的“系統資源”或指標可以是 CPU 時間、off-CPU 時間、記憶體使用、硬碟使用、延時,或者任何其他你能想到的資源。
而“程式碼路徑”可以定義為目標軟體程式碼中的呼叫棧軌跡。
呼叫棧軌跡通常是由一組函式呼叫幀組成的,通常出現在 GDB 命令 bt
的輸出中,以及 Python
或 Java 程式的異常錯誤資訊當中。比如下面是一個 Lua 呼叫棧軌跡的樣例:
C:ngx_http_lua_ngx_timer_at
at
cache.lua:43
cache.lua:record_timing
router.lua:338
router.lua:route
v2_routing.lua:1214
v2_routing.lua:route
access_by_lua.lua:130
在這個例子中,Lua 棧是從基幀 access_by_lua.lua:130
一路生長到頂幀 C:ngx_http_lua_ngx_timer_at
。它清晰地顯示了不同的
Lua 或 C 函式之間是如何相互呼叫的,從而構成了“程式碼路徑”的一種近似表示。
而上文中的“所有程式碼路徑”,實際上是從統計學的角度來看,並不是要真的要去列舉和遍歷程式中的每一條程式碼路徑。顯然在現實中,後者的開銷極其高昂,因為組合爆炸的問題。我們只要確保所有那些開銷不太小的程式碼路徑,都有機會出現在我們的圖中,並且我們能以足夠小的誤差去量化他們的開銷。
本文會聚焦在一種特定型別的火焰圖上面。這種火焰圖專用於展示 CPU 時間(或 CPU 資源)是如何定量分佈在 所有的 Lua 程式碼路徑上的。特別地,我們這裡只關注 OpenResty 或 Nginx 目標程序裡的 Lua 程式碼。自然地,這類火焰圖被我們命名為“Lua 級別 CPU 火焰圖”(Lua-land CPU Flame Graphs)。
本文標題圖片是一個火焰圖示例,後文將提供更多示例。
為甚麼需要火焰圖
火焰圖僅用一張小圖,就可以定量展示所有的效能瓶頸的全景圖,而不論目標軟體有多麼複雜。
傳統的效能分析工具通常會給使用者展示大量的細節資訊和資料, 而使用者很難看到全貌,反而容易去最佳化那些並不重要的地方,經常浪費大量時間和精力卻看不到明顯效果。傳統分析器的另一個缺點是,它們通常會孤立地顯示每個函式呼叫的延時,但很難看出各個函式呼叫的上下文,而且使用者還須刻意區分當前函式本身執行的時間(exclusive time)和包括了其呼叫其他函式的時間在內的總時間(inclusive time)。
而相比之下,火焰圖可以把大量資訊壓縮到一個大小相對固定的圖片當中(通常一屏就可以顯示全)。 不怎麼重要的程式碼路徑會在圖上自然地淡化乃至消失,而真正重要的程式碼路徑則會自然地凸顯出來。越重要的,則會顯示得越明顯。火焰圖總是為使用者提供最適當的資訊量,不多,也不少。
如何解讀火焰圖
對於新手而言,正確地解讀火焰圖可能不太容易。但透過一些簡單的解釋,使用者就會發現火焰圖其實很直觀,很容易理解。火焰圖是一張二維圖。y 軸顯示是程式碼(或資料)上下文,比如目標程式語言的呼叫棧軌跡,而 x 軸則顯示的是各個呼叫棧所佔用的系統資源的比例。整個 x 軸通常代表了目標軟體所消耗的 100% 的系統資源(比如 CPU 時間)。x 軸上的各個呼叫棧軌跡的先後順序通常並不重要,因為這些呼叫棧只是根據函式幀名的字母順序來排列。當然,也會有一些例外,例如筆者發明了一種時序火焰圖,其中的 x 軸實際上是時間軸,此時呼叫棧的先後順序就是時間順序。本文將專注於討論經典的火焰圖型別,即圖中 x 軸上的順序並不重要。
要學會讀懂一張火焰圖,最好的方法是嘗試解讀真實的火焰圖樣本。下文將提供多個火焰圖例項,針對 OpenResty 和 Nginx 伺服器上執行的 Lua 應用,並提供詳細的解釋。
簡單的 Lua 樣例
本節將列舉幾個簡單的有明顯效能特徵的 Lua 樣例程式,並將使用 OpenResty XRay 分析真實的 nginx 程序,生成 Lua 級別的 CPU 火焰圖,並驗證圖中顯示的效能情況。我們將檢查不同的案例,例如開啟了 JIT 即時編譯的 Lua 程式碼、禁用了 JIT 編譯的 Lua 程式碼(即被解釋執行),以及呼叫外部 C 庫程式碼的 Lua 程式碼。
JIT 編譯過的 Lua 程式碼
首先,我們來研究一個開啟了 JIT 即時編譯的 Lua 樣本程式(LuaJIT 是預設開啟 JIT )。
考慮下面這個獨立的 OpenResty 小應用。本節將一直使用這個示例,但會針對不同情形的討論需求,適時對這個例子進行少許修改。
我們首先準備這個應用的目錄佈局:
mkdir -p ~/work
cd ~/work
mkdir conf logs lua
然後我們建立如下所示的 conf/nginx.conf
配置檔案:
master_process on;
worker_processes 1;
events {
worker_connections 1024;
}
http {
lua_package_path "$prefix/lua/?.lua;;";
server {
listen 8080;
location = /t {
content_by_lua_block {
require "test".main()
}
}
}
}
在 location /t
的 Lua 處理程式中,我們載入了名為 test
的外部 Lua
模組,並立即呼叫該模組的 main
函式。我們使用了 lua_package_path
配置指令,來把 lua/
目錄新增到 Lua 模組的搜尋路徑列表中 ,因為我們會把剛提及的 test
這個 Lua 模組檔案放到 lua/
目錄下。
這個 test
Lua 模組定義在 lua/test.lua
檔案中:
local _M = {}
local N = 1e7
local function heavy()
local sum = 0
for i = 1, N do
sum = sum + i
end
return sum
end
local function foo()
local a = heavy()
a = a + heavy()
return a
end
local function bar()
return (heavy())
end
function _M.main()
ngx.say(foo())
ngx.say(bar())
end
return _M
這裡我們定義了一個計算量較大的 Lua 函式 heavy()
,計算從 1 到 1000 萬 (1e7
)的數字之和。
然後我們在函式 foo()
中呼叫兩次 heavy()
函式,而在 bar()
函式中只呼叫一次
heavy()
函式。
最後,模組的入口函式 _M.main()
先後呼叫 foo
和 bar
各 一次,並透過
ngx.say
向 HTTP 響應體輸出它們的返回值。
顯然,在這個 Lua 處理程式中,foo()
函式佔用的 CPU 時間應當是 bar()
函式的兩倍,因為
foo()
函式呼叫了 heavy()
函式兩次,而 bar()
僅呼叫了一次。透過下文中由
OpenResty XRay
取樣生成的 Lua 級別的 CPU 火焰圖,我們可以很容易地驗證這裡的觀察結果。
因為在這個示例中,我們並沒有觸碰 LuaJIT 的 JIT 編譯器選項,因此 JIT 編譯便使用了預設的開啟狀態,並且現代的 OpenResty 平臺版本則總是隻使用 LuaJIT(對標準 Lua 5.1 直譯器的支援早已移除)。
現在,我們可以按下面的命令啟動這個 OpenResty 應用:
cd ~/work/
/usr/local/openresty/bin/openresty -p $PWD/
假設 OpenResty 安裝在當前系統的 /usr/local/openresty/
目錄下(這是預設的安裝位置)。
為了使 OpenResty 應用忙碌起來,我們可以使用 ab
或 weighttp
這樣的壓測工具,向
URI http://localhost:8080/t
施加請求壓力,或者使用 OpenResty XRay 產品自帶的負載生成器。無論使用何種方式,當目標 OpenResty 應用的 nginx 工作程序保持活躍時,我們可以在 OpenResty XRay
的 Web 控制檯裡得到類似下面這張 Lua 級別的 CPU 火焰圖:
我們從圖上可以觀察到下列現象:
- 圖中的所有 Lua 呼叫棧都源自同一個入口點,即
content_by_lua(nginx.conf:24)
。這符合預期。 - 圖中主要顯示了兩個程式碼路徑,分別是
以及content_by_lua -> test.lua:main -> test.lua:bar -> test.lua:heavy -> trace#2:test.lua:8
兩個程式碼路徑的唯一區別是中間的content_by_lua -> test.lua:main -> test.lua:foo -> test.lua:heavy -> trace#2:test.lua:8
foo
函式幀與bar
函式幀。這也不出所料。 - 左側涉及
bar
函式的程式碼路徑的寬度,是右側涉及foo
的程式碼路徑寬度的一半。 換言之,這兩個程式碼路徑在圖中 x 軸上的寬度比為 1:2,即bar
程式碼路徑佔用的 CPU 時間,只有foo
程式碼路徑的50%。將滑鼠移動到圖中的test.lua:bar
幀(即方框)上,我們可以看到它佔據總樣本量(即總 CPU 時間)的 33.3%,而test.lua:foo
所佔的比例為66.7%. 顯然,與我們之前的預測相比較,這個火焰圖提供的比例數字非常精確,儘管它所採取的是取樣和統計分析的方法。 - 我們在圖中沒有看到
ngx.say()
等其他程式碼路徑,畢竟它們與那兩個呼叫了heavy()
的 Lua 程式碼路徑相比,所佔用的 CPU 時間微乎其微。在火焰圖中,那些微不足道的程式碼路徑本就是小噪音,不會引起我們的關注。我們可以始終專注於那些真正重要的部分,而不會為其他東西分心。 - 那兩條熱程式碼路徑(即呼叫棧軌跡)的頂部幀是完全相同的,都是
trace#2:test.lua:8
. 它並不是真正的 Lua 函式呼叫幀,而是一個“偽函式幀”,用於表示它正在執行一個被 JIT 編譯了的 Lua 程式碼路徑。按照 LuaJIT 的術語,該路徑被稱為”trace“(因為 LuaJIT 是一種 tracing JIT 編譯器)。這個”trace“的編號為 2,而對應的被編譯的 Lua 程式碼路徑是從test.lua
檔案的第 8 行開始的。而test.lua:8
所指向的 Lua 程式碼行是:sum = sum + i
我們很高興地看到,這個非侵入的取樣工具,可以從一個沒有任何外掛模組、沒有被修改過、也沒有使用特殊編譯選項的標準
OpenResty 二進位制程式,得到如此準確的火焰圖。這個工具沒有使用 LuaJIT 執行時的任何特殊特性或介面,甚至沒有使用它的
LUAJIT_USE_PERFTOOLS
特性或者 LuaJIT 內建的效能分析器。相反,該工具使用的是先進的動態追蹤技術,僅讀取原始目標程序中原有的資訊。我們甚至可以從
JIT 編譯過的 Lua 程式碼中獲取足夠多的有用資訊。
解釋執行的 Lua 程式碼
解釋執行的 Lua 程式碼通常能夠得到最完美的的呼叫棧軌跡和火焰圖樣本。 如果我們的取樣工具能夠正確處理 JIT 即時編譯後的 Lua 程式碼,那麼在分析解釋的 Lua 程式碼時,效果只會更好。 LuaJIT 既有一個 JIT 編譯器,又同時有一個直譯器。它的直譯器的有趣之處在於,幾乎完全是用手工編寫的彙編程式碼實現的(當然,LuaJIT 引入了自己的一種組合語言記法,叫做 DynASM)。
對於我們一直在使用的那個 Lua 樣例程式,我們需要在此做少許修改,即在 server {}
配置塊中新增下面的
nginx.conf
配置片段:
init_by_lua_block {
jit.off()
}
然後重新載入(reload)或重啟伺服器程序,並保持流量負載。
這回我們得到了下面這張 Lua 級別 CPU 火焰圖:
這張新圖與前一張圖在以下方面都極其相似:
-
我們依舊只看到了兩條主要的程式碼路徑,分別是
bar
程式碼路徑和foo
程式碼路徑。 -
bar
程式碼路徑依舊佔用了總 CPU 時間的三分之一左右,而foo
佔用了餘下的所有部分(即大約三分之二)。 -
圖中顯示的所有程式碼路徑的入口都是
content_by_lua
那一幀。
然而,這張圖與前圖相比仍然有一個重要的區別:程式碼路徑的頂幀不再是 “trace” 偽幀了。
這個變化也是預期的,因為這一回沒有 JIT 編譯過的 Lua 程式碼路徑了,於是程式碼路徑的頂部或頂幀變成為
lj_BC_IFORL
和 lj_BC_ADDVV
等函式幀。而這些被 C:
字首標記出來的
C 函式幀其實也並非 C 語言函式,而是屬於彙編程式碼幀,對應於實現各個 LuaJIT
位元組碼的彙編例程,它們被標記成了 lj_BC_IFORL
等符號。自然地,lj_BC_IFORL
用於實現 LuaJIT
位元組碼指令 IFORL
,而 lj_BC_ADDVV
則用於位元組碼指令 ADDVV
。
IFORL
用於解釋執行 Lua程式碼中的 for
迴圈, 而 ADDVV
則用於算術加法。這些位元組碼的出現,都符合我們的
Lua 函式 heavy()
的實現方式。另外,我們還可以看到一些輔助的彙編例程,例如如 lj_meta_arith
和 lj_vm_foldarith
。
透過觀察這些函式幀的比例數值,我們還得以一窺 CPU 時間在 LuaJIT 虛擬機器和直譯器內部的分佈情況,為這個虛擬機器和直譯器本身的最佳化鋪平道路。
呼叫外部 C/C++ 函式
Lua 程式碼呼叫外部 C/C++ 庫函式的情況很常見。我們也希望透過 Lua 級別的 CPU 火焰圖,瞭解這些外部的 C 函式所佔用的 CPU 時間比例,畢竟這些 C 語言函式呼叫也是由 Lua 程式碼發起的。 這也是基於動態追蹤的效能分析的真正優勢所在:這些外部 C 語言函式呼叫在效能分析中永遠不會成為盲點1。
我們一直使用的 Lua 樣例在這裡又需要作少許修改,即需要將 heavy()
這個 Lua 函式修改成下面這個樣子:
local ffi = require "ffi"
local C = ffi.C
ffi.cdef[[
double sqrt(double x);
]]
local function heavy()
local sum = 0
for i = 1, N do
-- sum = sum + i
sum = sum + C.sqrt(i)
end
return sum
end
這裡我們使用 LuaJIT 的 FFI
API ,先宣告瞭一下標準 C 庫函式 sqrt()
,並直接在 Lua 函式 heavy()
內部呼叫了這個
C 庫函式。它應當會顯示在對應的 Lua 級別 CPU 火焰圖中。
此次我們得到了下面這張火焰圖:
有趣的是,我們果然在那兩條主要的 Lua 程式碼路徑的頂部,看到了 C 語言函式幀 C:sqrt
。
另外值得注意的是,我們在頂部附近依舊看到了 trace#N
這樣的偽幀,這說明我們透過 FFI
呼叫 C 函式的 Lua 程式碼,也是可以被 JIT 編譯的(這回我們從 init_by_lua_block
指令中刪除了 jit.off()
語句)。
程式碼行層面的火焰圖
上文展示的火焰圖其實都是函式層面的火焰圖,因為這些火焰圖中所顯示的所有呼叫幀都只有函式名,而沒有發起函式呼叫的原始碼行的資訊。
幸運的是, OpenResty XRay 的 Lua 級別效能分析工具支援生成程式碼行層面的火焰圖,會在圖中新增 Lua 原始碼行的檔名和行號,以方便使用者在較大的 Lua 函式體中直接定位到某一行 Lua 原始碼。下圖是我們一直使用的那個 Lua 樣例程式所對應的一張 Lua 程式碼行層面的 CPU 火焰圖:
我們可以看到在每一個函式幀上方都多了一個原始碼行的偽幀。例如,在函式 main
所在的 test.lua
原始檔的第 32 行 Lua 程式碼,呼叫了 foo()
函式。而在 foo()
函式所在的
test.lua:22
這一行,則呼叫了 heave()
函式。
程式碼行層面的火焰圖對於準確定位最熱的 Lua 原始碼行和 Lua 語句有非常大的幫助。當對應的 Lua 函式體很大的時候,程式碼行層面的火焰圖可以幫助節約排查程式碼行位置的大量時間。
多程序
在多核 CPU 的系統上,為單個 OpenResty 或 Nginx 伺服器例項配置多個 nginx 工作程序是很常見的做法。 OpenResty XRay 的分析工具支援同時對一個指定程序組中的所有程序進行取樣。當進來的流量不是很大,並且可能分佈在任意一個或幾個 nginx 工作程序上的時候,這種全程序組粒度的取樣分析是非常實用的。
複雜的 Lua 應用
我們也可以從非常複雜的 OpenResty/Lua 應用中得到 Lua 級別的 CPU 火焰圖。 例如,下面的 Lua 級別 CPU 火焰圖源自對執行了我們的 OpenResty Edge 產品的“迷你 CDN”伺服器進行了取樣。這是一款複雜的 Lua 應用,同時包含了全動態的 CDN 閘道器、地理敏感的 DNS 權威伺服器和一個 Web 應用防火牆(WAF):
從圖上可以看到,Web 應用防火牆(WAF)佔用的 CPU 時間最多,內建 DNS 伺服器也佔用了很大一部分 CPU 時間。我們佈署在全球範圍的”迷你 CDN“網路為我們自己運營的多個網站,比如 openresty.org 和 openresty.com.cn提供了安全和加速支援。
它還可以分析那些基於 OpenResty 的 API 閘道器軟體,例如 Kong
等等。
取樣開銷
我們使用的是基於取樣的方法,而不是全量埋點,因此為生成 Lua 級別 CPU 火焰圖所產生的執行時開銷通常可以忽略不計。無論是資料量還是 CPU 損耗都是極小的,所以這類工具非常適合於生產環境和線上環境。
如果我們透過固定速率的請求來訪問 nginx 目標程序,並且 Lua 級別 CPU 火焰圖工具同時在進行密集取樣,則該目標程序的 CPU 使用率隨時間的變化曲線如下所示:
該 CPU 使用率的變化曲線圖也是由 OpenResty XRay 自動生成和渲染的。
在我們停止工具取樣之後,同一個 nginx 工作程序的 CPU 使用量曲線仍然非常相似:
我們憑肉眼很難看出前後兩條曲線之間有甚麼差異。 所以,工具進行分析和取樣的開銷確實是非常低的。
而當工具不在取樣時,對目標程序的效能影響嚴格為零,畢竟我們並不需要對目標程序做任何的定製和修改。
安全性
由於使用了動態追蹤技術,我們不會改變目標程序的任何狀態,甚至不會修改其中哪怕一位元的資訊2。 這樣可以確保目標程序無論是在取樣時,還是沒有采樣時,其行為(幾乎)是完全相同的。這就保證了目標程序自身的可靠性(不會有意外的行為變化或程序崩潰),其行為不會因為分析工具的存在而受到任何影響。 目標程序的表現完全沒有變化,就像是為一隻活體動物拍攝 X 光片一樣。
傳統的應用效能管理(APM)產品可能要求在目標軟體中載入特殊的模組或外掛,甚至在目標軟體的可執行檔案或程序空間裡強行打上補丁或注入自己的機器程式碼或位元組碼,這都可能會嚴重影響使用者系統的穩定性和正確性。
因為這些原因,我們的工具可以安全應用到生產環境中,以分析那些在離線環境中很難復現的問題。
相容性
OpenResty XRay 產品提供的 Lua 級別 CPU 火焰圖的取樣工具,同時支援 LuaJIT 的 GC64 模式 或非 GC64 模式,也支援任意的 OpenResty 或 Nginx 的二進位制程式,包括使用者使用任意構建選項自己編譯的、最佳化或未最佳化的二進位制程式。
OpenResty XRay 也可以對在 Docker 或 Kubernetes 容器內執行的 OpenResty 和 Nginx 伺服器程序進行透明的分析,並生成完美的 Lua 級別的 CPU 火焰圖,不會有任何問題。
我們的工具還可以分析由 resty
或 luajit
命令列工具執行的那些基於控制檯的使用者 Lua 程式。
我們也支援較老的 Linux 作業系統和核心,比如使用 2.6.32 核心的 CentOS 6 老系統。
其他型別的 Lua 級別火焰圖
如前文所述,火焰圖可以用於視覺化任意一種系統資源或效能指標,而不僅限於 CPU 時間。 因此,我們的 OpenResty XRay 產品中也提供了其他型別的 Lua 級別火焰圖,比如 off-CPU 火焰圖、垃圾回收(GC)物件大小和資料引用路徑火焰圖、新 GC 物件分配火焰圖、Lua 協程棄權(yield)時間火焰圖、檔案 I/O 延時火焰圖等等。
我們的部落格網站將會發文詳細介紹這些不同型別的火焰圖。
結論
我們在本文中介紹了一種非常實用的視覺化方法,火焰圖,可以直觀地分析任意軟體系統的效能。 我們深入講解了其中的一類火焰圖,即 Lua 級別 CPU 火焰圖。這種火焰圖可用於分析在 OpenResty 和 Nginx 伺服器上執行的 Lua 應用。我們分析了多個 Lua 樣例程式,簡單的和複雜的,同時使用 OpenResty XRay 生成的對應的 Lua 級別 CPU 火焰圖,展示了動態追蹤工具的威力。最後,我們檢查了取樣分析的效能損耗,以及線上使用時的安全性和可靠性。
關於作者
章亦春是開源 OpenResty® 專案創始人兼 OpenResty Inc. 公司 CEO 和創始人。
章亦春(Github ID: agentzh),生於中國江蘇,現定居美國灣區。他是中國早期開源技術和文化的倡導者和領軍人物,曾供職於多家國際知名的高科技企業,如 Cloudflare、雅虎、阿里巴巴, 是 “邊緣計算“、”動態追蹤 “和 “機器程式設計 “的先驅,擁有超過 22 年的程式設計及 16 年的開源經驗。作為擁有超過 4000 萬全球域名使用者的開源專案的領導者。他基於其 OpenResty® 開源專案打造的高科技企業 OpenResty Inc. 位於美國矽谷中心。其主打的兩個產品 OpenResty XRay(利用動態追蹤技術的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最適合微服務和分散式流量的全能型閘道器軟體),廣受全球眾多上市及大型企業青睞。在 OpenResty 以外,章亦春為多個開源專案貢獻了累計超過百萬行程式碼,其中包括,Linux 核心、Nginx、LuaJIT、GDB、SystemTap、LLVM、Perl 等,並編寫過 60 多個開源軟體庫。
關注我們
如果您喜歡本文,歡迎關注我們 OpenResty Inc. 公司的部落格網站 。也歡迎掃碼關注我們的微信公眾號:
翻譯
我們提供了英文版原文和中譯版(本文) 。我們也歡迎讀者提供其他語言的翻譯版本,只要是全文翻譯不帶省略,我們都將會考慮採用,非常感謝!