OpenResty FAQ | Dynamic Load, NYI, dan Caching dari Shared Dict
API7.ai
January 19, 2023
Seri artikel Openresty telah diperbarui sejauh ini, dan bagian tentang optimasi performa adalah semua yang telah kita pelajari. Selamat kepada Anda yang tidak tertinggal, masih aktif belajar dan berlatih, serta antusias meninggalkan pemikiran Anda.
Kami telah mengumpulkan banyak pertanyaan yang lebih khas dan menarik, dan berikut adalah lima di antaranya.
Pertanyaan 1: Bagaimana cara melakukan pemuatan dinamis modul Lua?
Deskripsi: Saya memiliki pertanyaan tentang pemuatan dinamis yang diimplementasikan di OpenResty. Bagaimana cara menggunakan fungsi
loadstringuntuk menyelesaikan pemuatan file baru setelah diganti? Saya memahami bahwaloadstringhanya dapat memuat string, jadi jika saya ingin memuat ulang file/modul lua, bagaimana cara melakukannya di OpenResty?
Seperti yang kita ketahui, loadstring digunakan untuk memuat string, sedangkan loadfile dapat memuat file yang ditentukan, misalnya: loadfile("foo.lua"). Kedua perintah ini mencapai hasil yang sama. Adapun cara memuat modul Lua, berikut adalah contohnya:
resty -e 'local s = [[ local ngx = ngx local _M = {} function _M.f() ngx.say("hello world") end return _M ]] local lua = loadstring(s) local ret, func = pcall(lua) func.f()'
Konten dari string s adalah modul Lua yang lengkap. Jadi, ketika Anda menemukan perubahan dalam kode modul ini, Anda dapat memulai ulang pemuatan dengan loadstring atau loadfile. Dengan cara ini, fungsi dan variabel di dalamnya akan diperbarui bersamanya.
Untuk melangkah lebih jauh, Anda juga dapat membungkus pengambilan perubahan dan pemuatan ulang dengan lapisan yang disebut fungsi code_loader.
local func = code_loader(name)
Ini membuat pembaruan kode menjadi jauh lebih ringkas. Pada saat yang sama, code_loader umumnya menggunakan lru cache untuk menyimpan s agar tidak perlu memanggil loadstring setiap kali.
Pertanyaan 2: Mengapa OpenResty tidak melarang operasi yang memblokir?
Deskripsi: Selama bertahun-tahun, saya selalu bertanya-tanya, karena panggilan yang memblokir ini secara resmi tidak disarankan, mengapa tidak langsung menonaktifkannya? Atau menambahkan flag untuk memungkinkan pengguna memilih untuk menonaktifkannya?
Ini adalah pendapat pribadi saya. Pertama, karena ekosistem di sekitar OpenResty tidak sempurna, terkadang kita harus memanggil library yang memblokir untuk mengimplementasikan beberapa fungsi. Misalnya, sebelum versi 1.15.8, Anda harus menggunakan library Lua os.execute alih-alih lua-resty-shell untuk memanggil perintah eksternal. Misalnya, di OpenResty, membaca dan menulis file masih hanya mungkin dengan library I/O Lua, dan belum ada alternatif non-blocking.
Kedua, OpenResty sangat berhati-hati dalam optimasi semacam ini. Misalnya, lua-resty-core telah dikembangkan untuk waktu yang lama, tetapi belum pernah diaktifkan secara default, mengharuskan Anda untuk memanggil require 'resty.core' secara manual. Baru diaktifkan hingga rilis terbaru 1.15.8.
Terakhir, para pengelola OpenResty lebih suka menstandarisasi panggilan yang memblokir dengan menghasilkan kode Lua yang sangat dioptimalkan melalui kompiler dan DSL. Jadi, tidak ada upaya untuk melakukan sesuatu seperti opsi flag pada platform OpenResty itu sendiri. Tentu saja, saya tidak yakin apakah arah ini dapat menyelesaikan masalah.
Dari sudut pandang pengembang eksternal, masalah yang lebih praktis adalah bagaimana menghindari pemblokiran semacam itu. Kita dapat memperluas alat deteksi kode Lua, seperti luacheck, untuk menemukan dan mengingatkan operasi pemblokiran umum, atau kita dapat secara intrusif menonaktifkan atau menulis ulang beberapa fungsi langsung dengan menulis ulang _G, misalnya:
resty -e '_G.ngx.print = function() ngx.say("hello") end ngx.print()' # hello
Dengan kode contoh ini, Anda dapat menulis ulang fungsi ngx.print secara langsung.
Pertanyaan 3: Apakah operasi NYI LuaJIT memiliki dampak besar pada performa?
Deskripsi:
loadstringmenunjukkanneverdalam daftar NYI LuaJIT. Apakah ini akan memiliki dampak besar pada performa?
Mengenai NYI LuaJIT, kita tidak perlu terlalu ketat. Untuk operasi yang dapat di-JIT, pendekatan JIT tentu yang terbaik; tetapi untuk operasi yang belum dapat di-JIT, kita dapat terus menggunakannya.
Untuk optimasi performa, kita perlu mengambil pendekatan ilmiah berbasis statistik, itulah yang dilakukan oleh sampling flame graph. Optimasi prematur adalah akar dari segala kejahatan. Kita hanya perlu mengoptimalkan kode panas yang banyak dipanggil dan mengonsumsi banyak CPU.
Kembali ke loadstring, kita hanya akan memanggilnya untuk memuat ulang ketika kode berubah, bukan saat diminta, jadi ini bukan operasi yang sering. Pada titik ini, kita tidak perlu khawatir tentang dampaknya terhadap performa keseluruhan sistem.
Sejalan dengan masalah pemblokiran kedua, di OpenResty, kita terkadang juga memanggil operasi I/O file yang memblokir selama fase init dan init worker. Operasi ini lebih mengorbankan performa daripada NYI, tetapi karena hanya dilakukan sekali saat layanan dimulai, ini dapat diterima.
Seperti biasa, optimasi performa harus dilihat dari perspektif makro, poin yang perlu Anda perhatikan dengan baik. Jika tidak, dengan terobsesi pada detail tertentu, Anda kemungkinan akan mengoptimalkan untuk waktu yang lama tetapi tidak memiliki efek yang baik.
Pertanyaan 4: Bisakah saya mengimplementasikan upstream dinamis sendiri?
Deskripsi: Untuk upstream dinamis, pendekatan saya adalah menyiapkan 2 upstream untuk satu layanan, memilih upstream yang berbeda sesuai dengan kondisi routing, dan langsung memodifikasi IP di upstream ketika IP mesin berubah. Apakah ada kelemahan atau jebakan dalam pendekatan ini dibandingkan dengan menggunakan
balancer_by_lualangsung?
Keuntungan dari balancer_by_lua adalah memungkinkan pengguna untuk memilih algoritma load balancing, misalnya, apakah menggunakan roundrobin atau chash, atau algoritma lain yang diimplementasikan pengguna, yang fleksibel dan berkinerja tinggi.
Jika Anda melakukannya dengan cara aturan routing, hasilnya sama. Tetapi pemeriksaan kesehatan upstream perlu diimplementasikan oleh Anda, menambah banyak pekerjaan tambahan.
Kita juga dapat memperluas pertanyaan ini dengan menanyakan bagaimana kita harus mengimplementasikan skenario ini untuk abtest, yang memerlukan upstream yang berbeda.
Anda dapat memutuskan upstream mana yang akan digunakan dalam fase balancer_by_lua berdasarkan uri, host, parameter, dll. Anda juga dapat menggunakan gateway API untuk mengubah penilaian ini menjadi aturan routing, memutuskan rute mana yang akan digunakan dalam fase access awal, dan kemudian menemukan upstream yang ditentukan melalui hubungan binding antara rute dan upstream. Ini adalah pendekatan umum untuk gateway API, dan kita akan membahasnya lebih spesifik nanti di bagian praktik.
Pertanyaan 5: Apakah caching shared dict wajib?
Deskripsi:
Dalam aplikasi produksi nyata, saya pikir lapisan cache
shared dictadalah suatu keharusan. Sepertinya semua orang hanya mengingat kebaikanlru cache, tidak ada batasan format data, tidak perlu deserialisasi, tidak perlu menghitung ruang memori berdasarkan volume k/v, tidak ada persaingan antar worker, tidak ada kunci baca/tulis dan performa tinggi.Namun, jangan mengabaikan bahwa salah satu kelemahan paling fatalnya adalah bahwa siklus hidup
lru cachemengikutiWorker. Setiap kali NGINX reload, bagian cache ini akan hilang sepenuhnya, dan pada saat ini, jika tidak adashared dict, sumber dataL3akan tergantung dalam hitungan menit.Tentu saja, ini adalah kasus konkurensi yang lebih tinggi, tetapi karena caching digunakan, volume bisnis pasti tidak kecil, yang berarti analisis yang baru saja disebutkan masih berlaku. Jika saya benar dalam pandangan ini?
Dalam beberapa kasus, memang benar, seperti yang Anda katakan, shared dict tidak hilang selama reload, jadi itu diperlukan. Tetapi ada kasus khusus di mana hanya lru cache yang dapat diterima jika semua data tersedia secara aktif dari sumber data L3 dalam fase init atau init_worker.
Misalnya, jika gateway API open source APISIX memiliki sumber datanya di etcd, itu hanya mengambil data dari etcd. Ini menyimpannya dalam lru cache selama fase init_worker, dan pembaruan cache selanjutnya diambil secara aktif melalui mekanisme watch etcd. Dengan cara ini, bahkan jika NGINX reload, tidak akan ada cache stampede.
Jadi, kita dapat memiliki preferensi dalam memilih teknologi tetapi jangan menggeneralisasi secara absolut karena tidak ada solusi tunggal yang cocok untuk semua skenario caching. Ini adalah cara yang baik untuk membangun solusi minimal yang tersedia sesuai dengan kebutuhan skenario aktual dan kemudian meningkatkannya secara bertahap.