Tips untuk Peningkatan Performa 10x di OpenResty: Struktur Data `Table`
API7.ai
December 9, 2022
Di OpenResty, selain masalah performa yang sering terjadi pada operasi string, operasi table juga menjadi hambatan performa. Dalam artikel sebelumnya, kita telah membahas fungsi-fungsi terkait table secara sporadis, tetapi tidak secara khusus dalam hal peningkatan performa. Hari ini, saya akan membawa Anda memahami dampak performa dari operasi table.
Ada dua alasan utama mengapa pengembang kurang mengetahui tentang optimasi performa terkait table dibandingkan dengan operasi string.
- Lua yang digunakan di OpenResty adalah cabang LuaJIT yang dikelola sendiri, bukan LuaJIT standar atau Lua standar. Kebanyakan pengembang tidak menyadari perbedaan ini dan cenderung menggunakan pustaka
tableLua standar untuk menulis kode OpenResty. - Baik di LuaJIT standar maupun di cabang LuaJIT yang dikelola oleh OpenResty sendiri, dokumentasi terkait operasi
tabletersembunyi dalam dan sulit ditemukan oleh pengembang. Selain itu, tidak ada contoh kode dalam dokumentasi, sehingga pengembang perlu mencari contoh di proyek-proyek sumber terbuka.
Kedua hal ini membuat pembelajaran OpenResty memiliki hambatan kognitif yang relatif tinggi, menghasilkan hasil yang terpolarisasi — pengembang OpenResty yang berpengalaman dapat menulis kode yang sangat performa tinggi, sementara mereka yang baru memulai akan bertanya-tanya apakah performa tinggi OpenResty itu nyata. Namun, setelah Anda mempelajari pelajaran ini, Anda dapat dengan mudah melampaui hambatan persepsi dan mencapai peningkatan performa 10 kali lipat.
Sebelum masuk ke detail optimasi table, saya ingin menekankan prinsip sederhana terkait optimasi table.
Cobalah untuk menggunakan kembali table dan hindari pembuatan table yang tidak perlu.
Kami akan memperkenalkan optimasi dalam hal pembuatan table, penyisipan elemen, pengosongan, dan penggunaan perulangan.
Array yang Dihasilkan Sebelumnya
Langkah pertama adalah membuat array. Di Lua, cara kita membuat array sangat sederhana.
local t = {}
Baris kode di atas membuat array kosong. Anda juga dapat menambahkan data yang diinisialisasi saat membuat:
local color = {first = "red", "blue", third = "green", "yellow"}
Namun, cara penulisan kedua lebih mahal dalam hal performa karena melibatkan alokasi ruang, resize, dan rehash dari array setiap kali elemen array ditambahkan dan dihapus.
Jadi, bagaimana cara mengoptimalkannya? Mengorbankan ruang untuk waktu adalah ide optimasi yang umum. Karena hambatan performa di sini adalah alokasi ruang array yang dinamis, maka kita dapat menghasilkan array dengan ukuran yang ditentukan sebelumnya. Meskipun ini mungkin membuang beberapa ruang memori, beberapa alokasi ruang, resize, dan rehash dapat digabungkan menjadi satu, yang jauh lebih efisien.
Fungsi table.new(narray, nhash) di LuaJIT ditambahkan.
Fungsi ini mengalokasikan ruang array dan hash yang ditentukan sebelumnya alih-alih tumbuh sendiri saat menyisipkan elemen. Ini adalah makna dari dua parameternya, narray dan nhash.
Berikut adalah contoh sederhana untuk melihat cara menggunakannya. Karena fungsi ini adalah ekstensi LuaJIT, kita perlu memerlukan berikut ini sebelum dapat menggunakannya.
local new_tab = require "table.new" local t = new_tab(100, 0) for i = 1, 100 do t[i] = i end
Juga, karena OpenResty sebelumnya tidak sepenuhnya mengikat LuaJIT dan masih mendukung Lua standar, beberapa kode lama akan melakukan ini untuk kompatibilitas. Jika fungsi table.new tidak ditemukan, fungsi kosong akan disimulasikan untuk memastikan keseragaman pemanggil.
local ok, new_tab = pcall(require, "table.new") if not ok then new_tab = function (narr, nrec) return {} end end
Menghitung Subskrip table Secara Manual
Setelah Anda memiliki objek table, langkah selanjutnya adalah menambahkan elemen ke dalamnya. Cara paling langsung untuk menyisipkan elemen adalah dengan memanggil fungsi table.insert:
local new_tab = require "table.new" local t = new_tab(100, 0) for i = 1, 100 do table.insert(t, i) end
Atau, dapatkan panjang array saat ini terlebih dahulu dan sisipkan elemen menggunakan indeks:
local new_tab = require "table.new" local t = new_tab(100, 0) for i = 1, 100 do t[#t + 1] = i end
Namun, keduanya perlu menghitung panjang array terlebih dahulu dan kemudian menambahkan elemen baru. Waktu kompleksitas operasi ini adalah O(n). Dalam contoh kode di atas, perulangan for akan menghitung panjang array sebanyak 100 kali, sehingga performanya tidak baik, dan semakin besar array, semakin rendah performanya.
Mari kita lihat bagaimana pustaka resmi lua-resty-redis menyelesaikan masalah ini.
local function _gen_req(args) local nargs = #args local req = new_tab(nargs * 5 + 1, 0) req[1] = "*" .. nargs .. "\r\n" local nbits = 2 for i = 1, nargs do local arg = args[i] req[nbits] = "$" req[nbits + 1] = #arg req[nbits + 2] = "\r\n" req[nbits + 3] = arg req[nbits + 4] = "\r\n" nbits = nbits + 5 end return req end
Fungsi ini menghasilkan array req sebelumnya, ukurannya ditentukan oleh parameter input fungsi, untuk memastikan bahwa ruang yang terbuang seminimal mungkin.
Fungsi ini menggunakan variabel nbits untuk mempertahankan subskrip req secara manual, alih-alih menggunakan fungsi table.insert bawaan Lua dan operator # untuk mendapatkan panjangnya.
Anda dapat melihat bahwa dalam perulangan for, beberapa operasi seperti nbits + 1 menyisipkan elemen langsung sebagai subskrip dan mempertahankan subskrip pada nilai yang benar di akhir dengan nbits = nbits + 5.
Keuntungannya jelas, ini menghilangkan operasi O(n) untuk mendapatkan ukuran array dan mengaksesnya langsung dengan indeks, dan waktu kompleksitas menjadi O(1). Kerugiannya juga jelas, ini mengurangi keterbacaan kode, dan kemungkinan kesalahan sangat meningkat, sehingga ini adalah pedang bermata dua.
Menggunakan Kembali Satu table
Karena overhead pembuatan table sangat tinggi, kita secara alami ingin menggunakannya kembali sebanyak mungkin. Namun, ada syarat untuk penggunaan kembali. Pertama, kita perlu membersihkan data asli dalam table untuk menghindari dampak pada pengguna berikutnya.
Di sinilah fungsi table.clear berguna. Dari namanya, Anda dapat melihat apa yang dilakukannya, yaitu membersihkan semua data dalam array, tetapi panjang array tidak berubah. Artinya, jika Anda menggunakan table.new(narray, nhash) untuk menghasilkan array dengan panjang 100, setelah membersihkannya, panjangnya tetap 100.
Untuk memberi Anda gambaran yang lebih baik tentang implementasinya, saya telah memberikan contoh kode di bawah ini yang kompatibel dengan Lua standar:
local ok, clear_tab = pcall(require, "table.clear") if not ok then clear_tab = function (tab) for k, _ in pairs(tab) do tab[k] = nil end end end
Seperti yang Anda lihat, fungsi clear mengatur setiap elemen menjadi nil.
Umumnya, kita menempatkan jenis table siklus ini di tingkat atas modul. Dengan cara ini, saat Anda menggunakan fungsi dalam modul, Anda dapat memutuskan apakah akan menggunakannya langsung atau setelah membersihkannya, tergantung pada situasi Anda.
Mari kita lihat contoh aplikasi praktis. Berikut adalah pseudo-code yang diambil dari gateway API mikroservis sumber terbuka Apache APISIX, dan ini adalah logikanya saat memuat plugin.
local local_plugins = core.table.new(32, 0) local function load(plugin_names, wasm_plugin_names) local processed = {} for _, name in ipairs(plugin_names) do if processed[name] == nil then processed[name] = true end end for _, attrs in ipairs(wasm_plugin_names) do if processed[attrs.name] == nil then processed[attrs.name] = attrs end end core.log.warn("new plugins: ", core.json.delay_encode(processed)) for name, plugin in pairs(local_plugins_hash) do local ty = PLUGIN_TYPE_HTTP if plugin.type == "wasm" then ty = PLUGIN_TYPE_HTTP_WASM end unload_plugin(name, ty) end core.table.clear(local_plugins) core.table.clear(local_plugins_hash) for name, value in pairs(processed) do local ty = PLUGIN_TYPE_HTTP if type(value) == "table" then ty = PLUGIN_TYPE_HTTP_WASM name = value end load_plugin(name, local_plugins, ty) end -- sort by plugin's priority if #local_plugins > 1 then sort_tab(local_plugins, sort_plugin) end for i, plugin in ipairs(local_plugins) do local_plugins_hash[plugin.name] = plugin if enable_debug() then core.log.warn("loaded plugin and sort by priority:", " ", plugin.priority, " name: ", plugin.name) end end _M.load_times = _M.load_times + 1 core.log.info("load plugin times: ", _M.load_times) return true end
Anda dapat melihat bahwa array local_plugins adalah variabel tingkat atas untuk modul plugin. Di awal fungsi load, table dibersihkan, dan daftar plugin baru dihasilkan berdasarkan situasi saat ini.
tablepool
Sampai saat ini, Anda telah menguasai optimasi perulangan melalui satu table. Kemudian Anda dapat melangkah lebih jauh dan menggunakan kolam cache untuk menyimpan beberapa table yang tersedia untuk diakses kapan saja, yang merupakan fungsi dari lua-tablepool resmi.
Kode berikut menunjukkan penggunaan dasar kolam table. Kita dapat mengambil table dari kolam yang ditentukan dan melepaskannya kembali setelah selesai menggunakannya:
local tablepool = require "tablepool" local tablepool_fetch = tablepool.fetch local tablepool_release = tablepool.release local pool_name = "some_tag" local function do_sth() local t = tablepool_fetch(pool_name, 10, 0) -- -- using t for some purposes tablepool_release(pool_name, t) end
tablepool menggunakan beberapa metode yang kami perkenalkan sebelumnya, dan memiliki kurang dari seratus baris kode, jadi saya sangat merekomendasikan untuk mencari dan mempelajarinya sendiri. Di sini, saya terutama akan memperkenalkan dua API-nya.
Yang pertama adalah metode fetch, yang mengambil argumen yang sama dengan table.new, tetapi dengan satu tambahan pool_name. fetch akan memanggil table.new untuk membuat array baru jika tidak ada array yang tersedia di kolam.
tablepool.fetch(pool_name, narr, nrec)
Yang kedua adalah release, fungsi yang mengembalikan table ke kolam. Di antara argumennya, yang terakhir, no_clear, digunakan untuk mengonfigurasi apakah akan memanggil table.clear untuk membersihkan array.
tablepool.release(pool_name, tb, [no_clear])
Lihat bagaimana semua metode yang kami perkenalkan sebelumnya sekarang saling terkait? Namun, berhati-hatilah untuk tidak menyalahgunakan tablepool karena alasan ini. tablepool tidak banyak digunakan dalam proyek nyata. Misalnya, Kong tidak menggunakannya, dan APISIX hanya memiliki beberapa panggilan terkait dengannya. Dalam kebanyakan kasus, bahkan tanpa enkapsulasi lapisan tablepool ini, itu sudah cukup untuk kita gunakan.
Ringkasan
Optimasi performa, area yang sulit di OpenResty, adalah titik panas. Hari ini saya memperkenalkan tips optimasi performa terkait table. Saya harap ini dapat membantu Anda dengan proyek aktual Anda.
Terakhir, pikirkan pertanyaan ini: Dapatkah Anda melakukan tes performa sendiri dan membandingkan perbedaan performa sebelum dan sesudah menggunakan teknik optimasi terkait table? Sekali lagi, selamat berkomentar dan berkomunikasi dengan saya karena saya ingin mengetahui praktik dan pandangan Anda. Selamat berbagi artikel ini sehingga lebih banyak orang dapat berpartisipasi bersama.