Apa yang membuat OpenResty begitu istimewa
API7.ai
October 14, 2022
Dalam artikel sebelumnya, Anda telah mempelajari tentang dua pilar utama OpenResty: NGINX dan LuaJIT, dan saya yakin Anda sudah siap untuk mulai mempelajari API yang disediakan oleh OpenResty.
Namun, jangan terlalu terburu-buru. Sebelum melakukannya, Anda perlu menghabiskan sedikit waktu lagi untuk mengenal prinsip dan konsep dasar OpenResty.
Prinsip

Proses Master dan Worker OpenResty keduanya mengandung LuaJIT VM, yang dibagikan oleh semua korutin dalam proses yang sama, dan di mana kode Lua dijalankan.
Dan pada saat yang sama, setiap proses Worker hanya dapat menangani permintaan dari satu pengguna, yang berarti hanya satu korutin yang berjalan. Anda mungkin memiliki pertanyaan: Karena NGINX dapat mendukung C10K (puluhan ribu konkurensi), bukankah itu perlu menangani 10.000 permintaan secara bersamaan?
Tentu saja tidak. NGINX menggunakan epoll untuk menggerakkan peristiwa guna mengurangi waktu tunggu dan menganggur sehingga sebanyak mungkin sumber daya CPU dapat digunakan untuk memproses permintaan pengguna. Bagaimanapun, keseluruhan sistem hanya mencapai kinerja tinggi ketika permintaan individu diproses dengan cukup cepat. Jika mode multi-thread digunakan sehingga satu permintaan sesuai dengan satu thread, maka dengan C10K, sumber daya dapat dengan mudah habis.
Pada tingkat OpenResty, korutin Lua bekerja bersama dengan mekanisme peristiwa NGINX. Jika operasi I/O seperti mengkueri database MySQL terjadi dalam kode Lua, itu akan pertama-tama memanggil yield korutin Lua untuk menangguhkan dirinya sendiri dan kemudian mendaftarkan callback di NGINX; setelah operasi I/O selesai (yang juga bisa berupa timeout atau error), callback NGINX resume akan membangunkan korutin Lua. Ini menyelesaikan kerja sama antara konkurensi Lua dan penggerak peristiwa NGINX, menghindari penulisan callback dalam kode Lua.
Kita dapat melihat diagram berikut, yang menggambarkan seluruh proses. Baik lua_yield maupun lua_resume adalah bagian dari lua_CFunction yang disediakan oleh Lua.

Di sisi lain, jika tidak ada operasi I/O atau sleep dalam kode Lua, seperti semua operasi enkripsi dan dekripsi yang intensif, maka LuaJIT VM akan ditempati oleh korutin Lua hingga seluruh permintaan diproses.
Saya telah menyediakan cuplikan kode sumber untuk ngx.sleep di bawah ini untuk membantu Anda memahami ini dengan lebih jelas. Kode ini terletak di ngx_http_lua_sleep.c, yang dapat Anda temukan di direktori src dari proyek lua-nginx-module.
Di ngx_http_lua_sleep.c, kita dapat melihat implementasi konkret dari fungsi sleep. Anda harus terlebih dahulu mendaftarkan API Lua ngx.sleep dengan fungsi C ngx_http_lua_ngx_sleep.
void ngx_http_lua_inject_sleep_api(lua_State *L) { lua_pushcfunction(L, ngx_http_lua_ngx_sleep); lua_setfield(L, -2, "sleep"); }
Berikut adalah fungsi utama dari sleep, dan saya hanya mengekstrak beberapa baris kode utama di sini.
static int ngx_http_lua_ngx_sleep(lua_State *L) { coctx->sleep.handler = ngx_http_lua_sleep_handler; ngx_add_timer(&coctx->sleep, (ngx_msec_t) delay); return lua_yield(L, 0); }
Seperti yang Anda lihat:
- Di sini fungsi callback
ngx_http_lua_sleep_handlerditambahkan terlebih dahulu. - Kemudian memanggil
ngx_add_timer, antarmuka yang disediakan oleh NGINX, untuk menambahkan timer ke loop peristiwa NGINX. - Terakhir, menggunakan
lua_yielduntuk menangguhkan konkurensi Lua, memberikan kontrol ke loop peristiwa NGINX.
Fungsi callback ngx_http_lua_sleep_handler dipicu ketika operasi sleep selesai. Ini memanggil ngx_http_lua_sleep_resume dan akhirnya membangunkan korutin Lua menggunakan lua_resume. Anda dapat mengambil detail panggilan sendiri dalam kode sehingga saya tidak akan membahas detailnya di sini.
ngx.sleep hanyalah contoh paling sederhana, tetapi dengan membedahnya, Anda dapat melihat prinsip dasar dari modul lua-nginx-module.
Konsep dasar
Setelah menganalisis prinsip-prinsipnya, mari kita menyegarkan ingatan dan mengingat kembali dua konsep penting tentang tahapan dan non-blocking dalam OpenResty.
OpenResty, seperti NGINX, memiliki konsep tahapan, dan setiap tahapan memiliki peran yang berbeda:
set_by_lua, yang digunakan untuk mengatur variabel.rewrite_by_lua, untuk penerusan, pengalihan, dll.access_by_lua, untuk akses, izin, dll.content_by_lua, untuk menghasilkan konten kembali.header_filter_by_lua, untuk pemrosesan filter header respons.body_filter_by_lua, untuk pemfilteran tubuh respons.log_by_lua, untuk pencatatan.
Tentu saja, jika logika kode Anda tidak terlalu kompleks, mungkin untuk mengeksekusinya semua dalam fase rewrite atau content.
Namun, perhatikan bahwa API OpenResty memiliki batasan penggunaan fase. Setiap API memiliki daftar fase yang dapat digunakan, dan Anda akan mendapatkan kesalahan jika menggunakannya di luar cakupan. Ini sangat berbeda dengan bahasa pengembangan lainnya.
Sebagai contoh, saya akan menggunakan ngx.sleep. Dari dokumentasi, saya tahu bahwa itu hanya dapat digunakan dalam konteks berikut dan tidak termasuk fase log.
context: rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*_
Dan jika Anda tidak mengetahui hal ini, gunakan sleep dalam fase log yang tidak didukung:
location / { log_by_lua_block { ngx.sleep(1) } }
Dalam log error NGINX, ada indikasi tingkat error.
[error] 62666#0: *6 failed to run log_by_lua*: log_by_lua(nginx.conf:14):2: API disabled in the context of log_by_lua* stack traceback: [C]: in function 'sleep'
Jadi, sebelum Anda menggunakan API, selalu ingat untuk berkonsultasi dengan dokumentasi untuk menentukan apakah itu dapat digunakan dalam konteks kode Anda.
Setelah meninjau konsep fase, mari kita meninjau non-blocking. Pertama, mari kita klarifikasi bahwa semua API yang disediakan oleh OpenResty adalah non-blocking.
Saya akan melanjutkan dengan contoh sleep 1 detik. Jika Anda ingin mengimplementasikannya dalam Lua, Anda harus melakukan ini.
function sleep(s) local ntime = os.time() + s repeat until os.time() > ntime end
Karena Lua standar tidak memiliki fungsi sleep, saya menggunakan loop di sini untuk terus menentukan apakah waktu yang ditentukan telah tercapai. Implementasi ini bersifat blocking, dan selama detik itu sleep berjalan, Lua tidak melakukan apa-apa sementara permintaan lain yang perlu diproses hanya menunggu.
Namun, jika kita beralih ke ngx.sleep(1), menurut kode sumber yang kita analisis di atas, OpenResty masih dapat memproses permintaan lain (seperti permintaan B) selama detik ini. Konteks permintaan saat ini (sebut saja permintaan A) akan disimpan dan dibangunkan oleh mekanisme peristiwa NGINX dan kemudian kembali ke permintaan A, sehingga CPU selalu dalam keadaan bekerja alami.
Variabel dan siklus hidup
Selain dua konsep penting ini, siklus hidup variabel juga merupakan area pengembangan OpenResty yang mudah salah.
Seperti yang saya katakan sebelumnya, dalam OpenResty, saya menyarankan Anda untuk mendeklarasikan semua variabel sebagai variabel lokal dan menggunakan alat seperti luacheck dan lua-releng untuk mendeteksi variabel global. Ini sama untuk modul, seperti berikut.
local ngx_re = require "ngx.re"
Dalam OpenResty, kecuali untuk dua fase init_by_lua dan init_worker_by_lua, tabel variabel global yang terisolasi ditetapkan untuk semua fase untuk menghindari kontaminasi permintaan lain selama pemrosesan. Bahkan dalam dua fase ini di mana Anda dapat mendefinisikan variabel global, Anda harus mencoba menghindarinya.
Sebagai aturan, masalah yang mencoba diselesaikan dengan variabel global harus lebih baik diselesaikan dengan variabel dalam modul dan akan jauh lebih jelas. Berikut adalah contoh variabel dalam modul.
local _M = {} _M.color = { red = 1, blue = 2, green = 3 } return _M
Saya mendefinisikan modul dalam file bernama hello.lua, yang berisi tabel color, dan kemudian saya menambahkan konfigurasi berikut ke nginx.conf.
location / { content_by_lua_block { local hello = require "hello" ngx.say(hello.color.green) } }
Konfigurasi ini akan memerlukan modul dalam fase content dan mencetak nilai green sebagai tubuh respons HTTP.
Anda mungkin bertanya-tanya mengapa variabel modul begitu luar biasa?
Modul hanya akan dimuat sekali dalam proses Worker yang sama; setelah itu, semua permintaan yang ditangani oleh Worker akan berbagi data dalam modul. Kami mengatakan bahwa data "global" cocok untuk dikemas dalam modul karena Worker OpenResty sepenuhnya terisolasi satu sama lain, sehingga setiap Worker memuat modul secara independen, dan data modul tidak dapat melintasi Worker.
Adapun menangani data yang perlu dibagikan antara Worker, saya akan membahasnya di bab selanjutnya, jadi Anda tidak perlu menggali lebih dalam di sini.
Namun, ada satu hal yang bisa salah di sini: saat mengakses variabel modul, lebih baik Anda menjaga mereka hanya untuk dibaca dan tidak mencoba memodifikasinya, atau Anda akan mendapatkan race dalam kasus konkurensi tinggi, bug yang tidak dapat dideteksi oleh pengujian unit, yang kadang-kadang terjadi secara online dan sulit dilacak.
Misalnya, nilai saat ini dari variabel modul green adalah 3, dan Anda melakukan operasi tambah 1 dalam kode Anda, jadi apakah nilai green sekarang 4? Belum tentu; bisa jadi 4, 5, atau 6 karena OpenResty tidak mengunci saat menulis ke variabel modul. Kemudian ada persaingan, dan nilai variabel modul diperbarui oleh beberapa permintaan secara bersamaan.
Setelah membahas variabel global, lokal, dan modul, mari kita bahas variabel lintas fase.
Ada situasi di mana kita membutuhkan variabel yang melintasi fase dan dapat dibaca dan ditulis. Variabel seperti $host, $scheme, dll., yang sudah kita kenal dalam NGINX, tidak dapat dibuat secara dinamis meskipun memenuhi kondisi lintas fase, dan Anda harus mendefinisikannya dalam file konfigurasi sebelum dapat menggunakannya. Misalnya, jika Anda menulis sesuatu seperti berikut.
location /foo { set $my_var ; # perlu membuat variabel $my_var terlebih dahulu content_by_lua_block { ngx.var.my_var = 123 } }
OpenResty menyediakan ngx.ctx untuk menyelesaikan masalah semacam ini. Ini adalah tabel Lua yang dapat digunakan untuk menyimpan data Lua berbasis permintaan dengan siklus hidup yang sama dengan permintaan saat ini. Mari kita lihat contoh ini dari dokumentasi resmi.
location /test { rewrite_by_lua_block { ngx.ctx.foo = 76 } access_by_lua_block { ngx.ctx.foo = ngx.ctx.foo + 3 } content_by_lua_block { ngx.say(ngx.ctx.foo) } }
Anda dapat melihat bahwa kami telah mendefinisikan variabel foo yang disimpan dalam ngx.ctx. Variabel ini melintasi fase rewrite, access, dan content dan akhirnya mencetak nilai dalam fase content, yaitu 79 seperti yang kami harapkan.
Tentu saja, ngx.ctx memiliki batasannya sendiri.
Misalnya, permintaan anak yang dibuat dengan ngx.location.capture akan memiliki data ngx.ctx mereka sendiri, independen dari ngx.ctx permintaan induk.
Kemudian lagi, pengalihan internal yang dibuat dengan ngx.exec menghancurkan ngx.ctx permintaan asli dan menghasilkan ulang dengan ngx.ctx kosong.
Kedua batasan ini memiliki contoh kode detail dalam dokumentasi resmi, jadi Anda dapat memeriksanya sendiri jika Anda tertarik.
Ringkasan
Akhirnya, saya akan mengatakan beberapa kata lagi. Kami mempelajari prinsip-prinsip OpenResty dan beberapa konsep penting, tetapi Anda tidak perlu menghafalnya. Bagaimanapun, mereka selalu masuk akal dan hidup ketika digabungkan dengan kebutuhan dan kode dunia nyata.
Saya ingin tahu bagaimana Anda memahaminya? Silakan tinggalkan komentar dan diskusikan dengan saya, dan juga silakan bagikan artikel ini dengan rekan dan teman Anda. Kami berkomunikasi bersama, bersama dengan kemajuan.