Non-blocking I/O - Kunci untuk meningkatkan performa OpenResty
API7.ai
December 2, 2022
Dalam bab Optimasi Performa, saya akan membawa Anda melalui semua aspek optimasi performa di OpenResty dan merangkum berbagai hal yang disebutkan dalam bab-bab sebelumnya menjadi panduan pengkodean OpenResty yang komprehensif sehingga Anda dapat menulis kode OpenResty dengan kualitas yang lebih baik.
Meningkatkan performa tidaklah mudah. Kita harus mempertimbangkan optimasi arsitektur sistem, optimasi database, optimasi kode, pengujian performa, analisis flame graph, dan langkah-langkah lainnya. Namun, mengurangi performa sangatlah mudah, dan seperti yang disarankan oleh judul artikel hari ini, Anda dapat mengurangi performa hingga 10 kali lipat atau lebih hanya dengan menambahkan beberapa baris kode. Jika Anda menggunakan OpenResty untuk menulis kode, tetapi performanya tidak meningkat, maka kemungkinan besar itu disebabkan oleh I/O yang memblokir.
Jadi, sebelum kita masuk ke detail optimasi performa, mari kita lihat prinsip penting dalam pemrograman OpenResty: Non-blocking I/O pertama.
Sejak kecil, kita diajarkan oleh orang tua dan guru untuk tidak bermain api dan tidak menyentuh colokan listrik, karena itu adalah perilaku berbahaya. Jenis perilaku berbahaya yang sama juga ada di OpenResty. Jika Anda harus melakukan operasi I/O yang memblokir dalam kode Anda, itu akan menyebabkan penurunan performa yang dramatis, dan tujuan awal menggunakan OpenResty untuk membangun server berkinerja tinggi akan gagal.
Mengapa kita tidak bisa menggunakan operasi I/O yang memblokir?
Memahami perilaku mana yang berbahaya dan menghindarinya adalah langkah pertama dalam optimasi performa. Mari kita mulai dengan meninjau mengapa operasi I/O yang memblokir dapat memengaruhi performa OpenResty.
OpenResty dapat mempertahankan performa tinggi karena meminjam penanganan event dari NGINX dan coroutine dari Lua, sehingga:
- Ketika Anda menemukan operasi seperti I/O jaringan yang mengharuskan Anda menunggu kembalian sebelum melanjutkan, Anda memanggil coroutine Lua
yielduntuk menangguhkan diri Anda sendiri dan kemudian mendaftarkan callback di NGINX. - Setelah operasi I/O selesai (atau terjadi timeout atau error), NGINX memanggil
resumeuntuk membangunkan coroutine Lua.
Proses seperti ini memastikan bahwa OpenResty selalu dapat menggunakan sumber daya CPU secara efisien untuk memproses semua permintaan.
Dalam alur pemrosesan ini, LuaJIT tidak memberikan kontrol ke event loop NGINX jika tidak menggunakan metode I/O non-blocking seperti cosocket, tetapi malah menggunakan fungsi I/O yang memblokir untuk menangani I/O. Hal ini mengakibatkan permintaan lain harus mengantri menunggu event I/O yang memblokir selesai diproses sebelum mereka mendapatkan respons.
Singkatnya, dalam pemrograman OpenResty, kita harus sangat berhati-hati dengan panggilan fungsi yang mungkin memblokir I/O; jika tidak, satu baris kode I/O yang memblokir dapat menurunkan performa seluruh layanan.
Di bawah ini, saya akan memperkenalkan beberapa masalah umum, beberapa fungsi I/O yang memblokir yang sering disalahgunakan; mari kita juga merasakan bagaimana cara termudah untuk "mengacaukan" dan dengan cepat membuat performa layanan Anda turun 10 kali lipat.
Menjalankan perintah eksternal
Dalam banyak skenario, pengembang tidak hanya menggunakan OpenResty sebagai server web tetapi juga memberinya lebih banyak logika bisnis. Dalam hal ini, memanggil perintah dan alat eksternal mungkin diperlukan untuk membantu menyelesaikan beberapa operasi.
Misalnya, untuk menghentikan proses.
os.execute("kill -HUP " .. pid)
Atau untuk operasi yang lebih memakan waktu seperti menyalin file, menggunakan OpenSSL untuk menghasilkan kunci, dll.
os.execute(" cp test.exe /tmp ") os.execute(" openssl genrsa -des3 -out private.pem 2048 ")
Secara sekilas, os.execute adalah fungsi bawaan di Lua, dan di dunia Lua, itu memang cara untuk memanggil perintah eksternal. Namun, penting untuk diingat bahwa Lua adalah bahasa pemrograman yang disematkan dan akan memiliki rekomendasi penggunaan yang berbeda dalam konteks lain.
Di lingkungan OpenResty, os.execute memblokir permintaan saat ini. Jadi, jika waktu eksekusi perintah ini sangat singkat, maka dampaknya tidak terlalu besar. Namun, jika perintah tersebut memerlukan waktu ratusan milidetik atau bahkan detik untuk dieksekusi, maka akan terjadi penurunan performa yang tajam.
Kita memahami masalahnya, jadi bagaimana kita harus menyelesaikannya? Secara umum, ada dua solusi.
1. Jika ada library FFI yang tersedia, maka kita lebih memilih cara FFI untuk memanggilnya
Misalnya, jika kita menggunakan perintah OpenSSL untuk menghasilkan kunci di atas, kita dapat mengubahnya menggunakan FFI untuk memanggil fungsi C OpenSSL untuk menghindarinya.
Untuk menghentikan proses, Anda dapat menggunakan lua-resty-signal, sebuah library yang disertakan dengan OpenResty, untuk menyelesaikannya secara non-blocking. Implementasi kodenya adalah sebagai berikut. Tentu saja, di sini, lua-resty-signal juga diselesaikan menggunakan FFI untuk memanggil fungsi sistem.
local resty_signal = require "resty.signal" local pid = 12345 local ok, err = resty_signal.kill(pid, "KILL")
Selain itu, situs resmi LuaJIT memiliki halaman khusus yang memperkenalkan berbagai library binding FFI dalam kategori yang berbeda. Misalnya, saat menangani gambar, enkripsi, dan dekripsi operasi yang intensif CPU, Anda dapat melihat terlebih dahulu apakah ada library yang telah dikemas dan dapat digunakan langsung.
2. Gunakan library lua-resty-shell berbasis ngx.pipe
Seperti yang telah dijelaskan sebelumnya, Anda dapat menjalankan perintah Anda di shell.run, sebuah operasi I/O non-blocking.
$ resty -e 'local shell = require "resty.shell" local ok, stdout, stderr, reason, status = shell.run([[echo "hello, world"]]) ngx.say(stdout) '
Disk I/O
Mari kita lihat skenario penanganan disk I/O. Dalam aplikasi server-side, membaca file konfigurasi lokal adalah operasi yang umum, seperti kode berikut.
local path = "/conf/apisix.conf" local file = io.open(path, "rb") local content = file:read("*a") file:close()
Kode ini menggunakan io.open untuk mendapatkan konten file tertentu. Namun, meskipun ini adalah operasi I/O yang memblokir, jangan lupa bahwa hal-hal harus dipertimbangkan dalam skenario nyata. Jadi jika Anda memanggilnya di init dan init worker, itu adalah tindakan satu kali yang tidak memengaruhi permintaan klien apa pun dan sepenuhnya dapat diterima.
Tentu saja, itu menjadi tidak dapat diterima jika setiap permintaan pengguna memicu pembacaan atau penulisan ke disk. Pada saat itu, Anda perlu serius mempertimbangkan solusinya.
Pertama, kita dapat menggunakan lua-io-nginx-module, sebuah modul C pihak ketiga. Ini menyediakan API Lua I/O non-blocking untuk OpenResty, tetapi Anda tidak bisa menggunakannya sembarangan seperti cosocket. Karena konsumsi disk I/O tidak hilang begitu saja, itu hanya cara yang berbeda untuk melakukannya.
Pendekatan ini berfungsi karena lua-io-nginx-module memanfaatkan thread pooling NGINX untuk memindahkan operasi disk I/O dari thread utama ke thread lain untuk memprosesnya sehingga thread utama tidak terblokir oleh operasi disk I/O.
Anda perlu mengkompilasi ulang NGINX saat menggunakan library ini karena ini adalah modul C. Ini digunakan dengan cara yang sama seperti library I/O Lua.
local ngx_io = require "ngx.io" local path = "/conf/apisix.conf" local file, err = ngx_io.open(path, "rb") local data, err = file: read("*a") file:close()
Kedua, coba tweak arsitektur. Bisakah kita mengubah cara kita untuk jenis disk I/O ini dan berhenti membaca dan menulis ke disk lokal?
Saya akan memberikan contoh agar Anda dapat belajar dengan analogi. Beberapa tahun yang lalu, saya sedang mengerjakan sebuah proyek yang memerlukan logging pada disk lokal untuk tujuan statistik dan pemecahan masalah.
Pada saat itu, pengembang menggunakan ngx.log untuk menulis log ini, seperti berikut.
ngx.log(ngx.WARN, "info")
Baris kode ini memanggil API Lua yang disediakan oleh OpenResty, dan terlihat tidak ada masalah. Namun, kelemahannya adalah Anda tidak bisa memanggilnya terlalu sering. Pertama, ngx.log sendiri adalah panggilan fungsi yang mahal; kedua, bahkan dengan buffer, penulisan disk yang besar dan sering dapat berdampak serius pada performa.
Jadi bagaimana kita menyelesaikannya? Mari kita kembali ke kebutuhan awal - statistik, pemecahan masalah, dan menulis log ke disk lokal hanyalah salah satu cara untuk mencapai tujuan.
Jadi Anda juga dapat mengirim log ke server logging jarak jauh untuk menggunakan cosocket untuk melakukan komunikasi jaringan non-blocking; yaitu, melempar disk I/O yang memblokir ke layanan logging untuk menghindari memblokir layanan eksternal. Anda dapat menggunakan lua-resty-logger-socket untuk melakukan ini.
local logger = require "resty.logger.socket" if not logger.initted() then local ok, err = logger.init{ host = 'xxx', port = 1234, flush_limit = 1234, drop_limit = 5678, } local msg = "foo" local bytes, err = logger.log(msg)
Seperti yang seharusnya Anda perhatikan, kedua metode di atas sama: jika I/O yang memblokir tidak dapat dihindari, jangan memblokir thread worker utama; lempar ke thread lain atau layanan di luar.
luasocket
Terakhir, mari kita bicara tentang luasocket, sebuah library bawaan Lua yang mudah digunakan oleh pengembang dan sering membingungkan antara luasocket dan cosocket yang disediakan oleh OpenResty. luasocket juga dapat melakukan fungsi komunikasi jaringan. Namun, itu tidak memiliki keuntungan non-blocking. Akibatnya, jika Anda menggunakan luasocket, performa turun drastis.
Namun, luasocket juga memiliki skenario penggunaan yang unik. Misalnya, saya tidak tahu apakah Anda ingat bahwa cosocket tidak tersedia di beberapa fase, dan kita biasanya dapat menghindarinya dengan menggunakan ngx.timer. Juga, Anda dapat menggunakan luasocket untuk fungsi cosocket dalam fase satu kali seperti init_by_lua* dan init_worker_by_lua*. Semakin Anda familiar dengan persamaan dan perbedaan antara OpenResty dan Lua, semakin banyak solusi menarik seperti ini yang akan Anda temukan.
Selain itu, lua-resty-socket adalah wrapper sekunder untuk library open-source yang membuat luasocket dan cosocket` kompatibel. Konten ini juga layak untuk dipelajari lebih lanjut. Jika Anda masih tertarik, saya telah menyiapkan materi untuk Anda terus belajar.
Ringkasan
Secara umum, di OpenResty, mengenali jenis operasi I/O yang memblokir dan solusinya adalah dasar dari optimasi performa yang baik. Jadi, apakah Anda pernah menemukan operasi I/O yang memblokir dalam pengembangan aktual? Bagaimana Anda menemukan dan menyelesaikannya? Jangan ragu untuk berbagi pengalaman Anda dengan saya di komentar, dan jangan ragu untuk membagikan artikel ini.