Why Does lua-resty-core Perform Better?
API7.ai
September 30, 2022
Seperti yang telah kami katakan dalam dua pelajaran sebelumnya, Lua adalah bahasa pengembangan yang tertanam yang menjaga intinya tetap pendek dan ringkas. Anda dapat menyematkan Lua di Redis dan NGINX untuk membantu Anda melakukan logika bisnis secara fleksibel. Lua juga memungkinkan Anda memanggil fungsi dan struktur data C yang sudah ada untuk menghindari pengulangan yang tidak perlu.
Di Lua, Anda dapat menggunakan Lua C API untuk memanggil fungsi C, dan di LuaJIT, Anda dapat menggunakan FFI. Untuk OpenResty.
- Di inti
lua-nginx-module, API untuk memanggil fungsi C dilakukan menggunakan Lua C API. - Di
lua-resty-core, beberapa API yang sudah ada dilua-nginx-modulediimplementasikan menggunakan model FFI.
Anda mungkin bertanya-tanya mengapa kita perlu mengimplementasikannya dengan FFI?
Jangan khawatir. Mari kita ambil ngx.base64_decode, sebuah API yang sederhana, sebagai contoh dan lihat bagaimana Lua C API berbeda dengan implementasi FFI. Anda juga dapat memiliki pemahaman intuitif tentang performa mereka.
Lua CFunction
Mari kita lihat bagaimana ini diimplementasikan di lua-nginx-module menggunakan Lua C API. Kita mencari decode_base64 dalam kode proyek dan menemukan implementasinya di ngx_http_lua_string.c.
lua_pushcfunction(L, ngx_http_lua_ngx_decode_base64); lua_setfield(L, -2, "decode_base64");
Kode di atas terlihat membingungkan, tetapi untungnya, kita tidak perlu menggali lebih dalam tentang dua fungsi yang dimulai dengan lua_ dan peran spesifik dari argumen mereka; kita hanya perlu tahu satu hal - ada CFunction yang terdaftar di sini: ngx_http_lua_ngx_decode_base64, dan itu sesuai dengan ngx.base64_decode, yang sesuai dengan API yang diekspos ke publik.
Mari kita lanjutkan dan "ikuti petanya" dan cari ngx_http_lua_ngx_decode_base64 dalam file C ini, yang didefinisikan di awal file pada:
static int ngx_http_lua_ngx_decode_base64(lua_State *L);
Untuk fungsi C yang dapat dipanggil Lua, antarmukanya harus mengikuti bentuk yang diperlukan oleh Lua, yaitu typedef int (*lua_CFunction)(lua_State* L). Ini berisi pointer L bertipe lua_State sebagai argumen; tipe nilai kembaliannya adalah integer yang menunjukkan jumlah nilai yang dikembalikan, bukan nilai kembalian itu sendiri.
Ini diimplementasikan sebagai berikut (di sini, saya telah menghapus kode penanganan kesalahan).
static int ngx_http_lua_ngx_decode_base64(lua_State *L) { ngx_str_t p, src; src.data = (u_char *) luaL_checklstring(L, 1, &src.len); p.len = ngx_base64_decoded_length(src.len); p.data = lua_newuserdata(L, p.len); if (ngx_decode_base64(&p, &src) == NGX_OK) { lua_pushlstring(L, (char *) p.data, p.len); } else { lua_pushnil(L); } return 1; }
Hal utama dalam kode ini adalah ngx_base64_decoded_length, dan ngx_decode_base64, keduanya adalah fungsi C yang disediakan oleh NGINX.
Kita tahu bahwa fungsi yang ditulis dalam C tidak dapat meneruskan nilai kembalian ke kode Lua tetapi perlu meneruskan parameter panggilan dan mengembalikan nilai antara Lua dan C melalui stack. Inilah mengapa ada banyak kode yang tidak bisa kita pahami pada pandangan pertama. Juga, kode ini tidak dapat dilacak oleh JIT, jadi untuk LuaJIT, operasi ini berada dalam kotak hitam dan tidak dapat dioptimalkan.
LuaJIT FFI
Berbeda dengan FFI, bagian interaktif dari FFI diimplementasikan dalam Lua, yang dapat dilacak oleh JIT dan dioptimalkan; tentu saja, kodenya juga lebih ringkas dan mudah dipahami.
Mari kita ambil contoh base64_decode, yang implementasi FFI-nya tersebar di dua repositori: lua-resty-core dan lua-nginx-module, dan mari kita lihat kode yang diimplementasikan di yang pertama.
ngx.decode_base64 = function (s) local slen = #s local dlen = base64_decoded_length(slen) local dst = get_string_buf(dlen) local pdlen = get_size_ptr() local ok = C.ngx_http_lua_ffi_decode_base64(s, slen, dst, pdlen) if ok == 0 then return nil end return ffi_string(dst, pdlen[0]) end
Anda akan menemukan bahwa dibandingkan dengan CFunction, kode implementasi FFI jauh lebih segar, implementasi spesifiknya adalah ngx_http_lua_ffi_decode_base64 di repositori lua-nginx-module. Jika Anda tertarik di sini, Anda dapat memeriksa performa fungsi ini sendiri. Ini sangat sederhana, saya tidak akan memposting kodenya di sini.
Namun, jika Anda teliti, apakah Anda menemukan beberapa aturan penamaan fungsi dalam cuplikan kode di atas?
Ya, semua fungsi di OpenResty memiliki konvensi penamaan, dan Anda dapat menyimpulkan penggunaannya dengan menamainya. Misalnya:
ngx_http_lua_ffi_, fungsi Lua yang menggunakan FFI untuk menangani permintaan HTTP NGINX.ngx_http_lua_ngx_, fungsi Lua yang menggunakan fungsi C untuk menangani permintaan HTTP NGINX.- Fungsi lain yang dimulai dengan ngx dan lua adalah fungsi bawaan untuk NGINX dan Lua masing-masing.
Lebih lanjut, kode C di OpenResty memiliki spesifikasi kode yang ketat, dan saya merekomendasikan membaca panduan gaya kode C resmi di sini. Ini adalah dokumen wajib bagi pengembang yang ingin mempelajari kode C OpenResty dan mengirimkan PR. Jika tidak, bahkan jika PR Anda ditulis dengan baik, Anda akan berulang kali dikomentari dan diminta untuk mengubahnya karena masalah gaya kode.
Untuk lebih banyak API dan detail tentang FFI, kami merekomendasikan Anda membaca tutorial resmi LuaJIT dan dokumentasi. Kolom teknis bukan pengganti dokumentasi resmi; saya hanya dapat membantu Anda menunjukkan jalur pembelajaran dalam waktu terbatas, dengan sedikit kesalahan; Masalah sulit masih perlu diselesaikan oleh Anda.
LuaJIT FFI GC
Saat menggunakan FFI, kita mungkin bingung: siapa yang akan mengelola memori yang diminta dalam FFI? Haruskah kita melepaskannya secara manual di C, atau haruskah LuaJIT mengembalikannya secara otomatis?
Berikut adalah prinsip sederhana: LuaJIT hanya bertanggung jawab atas sumber daya yang dialokasikan oleh dirinya sendiri; ffi.
Misalnya, jika Anda meminta blok memori menggunakan ffi.C.malloc, Anda perlu membebaskannya dengan ffi.C.free yang berpasangan. Dokumentasi resmi LuaJIT memiliki contoh yang setara.
local p = ffi.gc(ffi.C.malloc(n), ffi.C.free) ... p = nil -- Referensi terakhir ke p hilang. -- GC akhirnya akan menjalankan finalizer: ffi.C.free(p)
Dalam kode ini, ffi.C.malloc(n) meminta bagian memori, dan ffi.gc mendaftarkan fungsi callback destruktor ffi.C.free,ffi.C.free kemudian akan dipanggil secara otomatis ketika cdata p di-GC oleh LuaJIT untuk membebaskan memori tingkat C. Dan cdata di-GC oleh LuaJIT. LuaJIT akan secara otomatis membebaskan p dalam kode di atas.
Perhatikan bahwa jika Anda ingin meminta sejumlah besar memori di OpenResty, saya merekomendasikan menggunakan ffi.C.malloc alih-alih ffi.new. Alasannya juga jelas.
ffi.newmengembalikancdata, yang merupakan bagian dari memori yang dikelola oleh LuaJIT.- LuaJIT GC memiliki batas atas manajemen memori, dan LuaJIT di OpenResty tidak memiliki opsi GC64 yang diaktifkan. Oleh karena itu batas atas memori untuk satu worker hanya 2G. Setelah batas atas manajemen memori LuaJIT terlampaui, itu akan menyebabkan kesalahan.
Saat menggunakan FFI, kita juga perlu sangat memperhatikan kebocoran memori. Namun, semua orang membuat kesalahan, dan selama manusia menulis kode, selalu ada bug.
Di sinilah rangkaian pengujian dan debugging yang kuat di sekitar OpenResty berguna.
Mari kita bicara tentang pengujian terlebih dahulu. Dalam sistem OpenResty, kami menggunakan Valgrind untuk mendeteksi kebocoran memori.
Kerangka pengujian yang kami sebutkan dalam kursus sebelumnya, test::nginx, memiliki mode deteksi kebocoran memori khusus untuk menjalankan set kasus uji unit; Anda perlu mengatur variabel lingkungan TEST_NGINX_USE_VALGRIND=1. Proyek OpenResty resmi akan sepenuhnya terdaftar dalam mode ini sebelum merilis versi, dan kami akan membahas lebih detail di bagian pengujian nanti. Kami akan membahas lebih detail di bagian pengujian nanti.
CLI resty OpenResty juga memiliki opsi --valgrind, yang memungkinkan Anda menjalankan kode Lua sendiri, bahkan jika Anda belum menulis kasus uji.
Mari kita lihat alat debugging.
OpenResty menyediakan ekstensi berbasis systemtap untuk melakukan analisis dinamis langsung dari program OpenResty. Anda dapat mencari kata kunci gc dalam alat proyek ini, dan Anda akan melihat dua alat, lj-gc dan lj-gc-objs.
Untuk analisis offline seperti core dump, OpenResty menyediakan perangkat GDB, dan Anda juga dapat mencari gc di dalamnya dan menemukan tiga alat lgc, lgcstat dan lgcpath.
Penggunaan spesifik dari alat debugging ini akan dibahas secara rinci di bagian debugging nanti sehingga Anda bisa mendapatkan gambaran terlebih dahulu. Bagaimanapun, OpenResty memiliki rangkaian alat khusus untuk membantu Anda menemukan dan menyelesaikan masalah ini.
lua-resty-core
Dari perbandingan di atas, kita dapat melihat bahwa pendekatan FFI tidak hanya lebih bersih dalam kode, tetapi juga dapat dioptimalkan oleh LuaJIT, yang merupakan pilihan yang lebih baik. OpenResty telah menghentikan implementasi CFunction, dan performanya telah dihapus dari basis kode. API baru sekarang diimplementasikan di repositori lua-resty-core melalui FFI.
Sebelum OpenResty 1.15.8.1 dirilis pada Mei 2019, lua-resty-core tidak diaktifkan secara default, yang mengakibatkan kerugian performa dan potensi bug, jadi saya sangat merekomendasikan siapa pun yang masih menggunakan versi lama untuk secara manual mengaktifkan lua-resty-core. Anda hanya perlu menambahkan satu baris kode ke fase init_by_lua.
require "resty.core"
Tentu saja, direktif lua_load_resty_core telah ditambahkan dalam rilis 1.15.8.1 yang terlambat, dan lua-resty-core diaktifkan secara default.
Saya pribadi merasa bahwa OpenResty masih terlalu berhati-hati dalam mengaktifkan lua-resty-core, dan proyek open source harus mengatur fitur serupa untuk diaktifkan secara default secepat mungkin.
lua-resty-core tidak hanya mengimplementasikan ulang beberapa API dari proyek lua-nginx-module, seperti ngx.re.match, ngx.md5, dll., tetapi juga mengimplementasikan beberapa API baru, seperti ngx.ssl, ngx.base64, ngx.errlog, ngx.process, ngx.re.process, dan ngx.ngx.md5. ngx.re.split, ngx.resp.add_header, ngx.balancer, ngx.semaphore, dll. yang akan kita bahas nanti di bab API OpenResty.
Ringkasan
Setelah mengatakan semua ini, saya ingin menyimpulkan bahwa FFI, meskipun baik, bukanlah peluru perak untuk performa. Alasan utama mengapa itu efisien adalah karena itu dapat dilacak dan dioptimalkan oleh JIT. Jika Anda menulis kode Lua yang tidak dapat di-JIT dan perlu dieksekusi dalam mode interpretasi, maka FFI akan kurang efisien.
Jadi operasi apa yang dapat di-JIT dan apa yang tidak? Bagaimana kita bisa menghindari menulis kode yang tidak dapat di-JIT? Saya akan mengungkapkannya di bagian selanjutnya.
Terakhir, tugas praktikum: Bisakah Anda menemukan satu atau dua API di lua-nginx-module dan lua-resty-core, dan kemudian membandingkan perbedaan dalam tes performa? Anda dapat melihat seberapa signifikan peningkatan performa FFI.
Silakan tinggalkan komentar, dan saya akan berbagi pemikiran dan hasil Anda serta menyambut Anda untuk membagikan artikel ini dengan kolega dan teman-teman Anda, bersama dengan pertukaran dan kemajuan.