Inti dari OpenResty: cosocket

API7.ai

October 28, 2022

OpenResty (NGINX + Lua)

Hari ini kita akan mempelajari tentang teknologi inti di OpenResty: cosocket.

Kita telah menyebutkannya berkali-kali dalam artikel sebelumnya, cosocket adalah dasar dari berbagai pustaka lua-resty-* yang non-blocking. Tanpa cosocket, pengembang tidak dapat menggunakan Lua untuk terhubung ke layanan web eksternal dengan cepat.

Dalam versi awal OpenResty, jika Anda ingin berinteraksi dengan layanan seperti Redis dan memcached, Anda perlu menggunakan modul C seperti redis2-nginx-module, redis-nginx-module, dan memc-nginx-module. Modul-modul ini masih tersedia dalam distribusi OpenResty.

Namun, dengan penambahan fitur cosocket, modul C telah digantikan oleh lua-resty-redis dan lua-resty-memcached. Tidak ada lagi yang menggunakan modul C untuk terhubung ke layanan eksternal.

Apa itu cosocket?

Jadi, apa sebenarnya cosocket itu? cosocket adalah istilah khusus di OpenResty. Nama cosocket terdiri dari coroutine + socket.

cosocket membutuhkan dukungan fitur konkurensi Lua dan mekanisme event dasar di NGINX, yang digabungkan untuk memungkinkan I/O jaringan non-blocking. cosocket juga mendukung TCP, UDP, dan Unix Domain Socket.

Implementasi internalnya terlihat seperti diagram berikut jika kita memanggil fungsi yang terkait dengan cosocket di OpenResty.

memanggil fungsi terkait cosocket

Saya juga menggunakan diagram ini dalam artikel sebelumnya tentang prinsip dan konsep dasar OpenResty. Seperti yang dapat Anda lihat dari diagram, untuk setiap operasi jaringan yang dipicu oleh skrip Lua pengguna, keduanya akan memiliki yield dan resume dari coroutine.

Ketika menemui I/O jaringan, ia mendaftarkan event jaringan ke daftar NGINX Listener dan mentransfer kontrol (yield) ke NGINX. Ketika event NGINX mencapai kondisi pemicu, ia akan membangunkan coroutine untuk melanjutkan pemrosesan (resume).

Proses di atas adalah cetak biru yang digunakan OpenResty untuk mengemas operasi connect, send, receive, dll., yang membentuk API cosocket yang kita lihat saat ini. Saya akan menggunakan API untuk menangani TCP sebagai contoh. Antarmuka untuk mengontrol UDP dan Unix Domain socket sama dengan TCP.

Pengenalan API dan perintah cosocket

API cosocket yang terkait dengan TCP dapat dibagi menjadi beberapa kategori berikut.

  • Membuat objek: ngx.socket.tcp.
  • Mengatur timeout: tcpsock:settimeout dan tcpsock:settimeouts.
  • Membangun koneksi: tcpsock:connect.
  • Mengirim data: tcpsock:send.
  • Menerima data: tcpsock:receive, tcpsock:receiveany, dan tcpsock:receiveuntil.
  • Pool koneksi: tcpsock:setkeepalive.
  • Menutup koneksi: tcpsock:close.

Kita juga harus memperhatikan konteks di mana API ini dapat digunakan.

rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*_

Satu hal lain yang ingin saya tekankan adalah bahwa ada banyak lingkungan yang tidak tersedia karena berbagai batasan dalam kernel NGINX. Misalnya, API cosocket tidak tersedia di set_by_lua*, log_by_lua*, header_filter_by_lua*, dan body_filter_by_lua*. Saat ini, API ini juga tidak tersedia di init_by_lua* dan init_worker_by_lua*, tetapi kernel NGINX tidak membatasi dua fase ini, sehingga dukungan untuknya dapat ditambahkan nanti.

Ada delapan perintah NGINX yang dimulai dengan lua_socket_ yang terkait dengan API ini. Mari kita lihat sekilas.

  • lua_socket_connect_timeout: timeout koneksi, default 60 detik.
  • lua_socket_send_timeout: timeout pengiriman, default 60 detik.
  • lua_socket_send_lowat: ambang pengiriman (low water), default adalah 0.
  • lua_socket_read_timeout: timeout pembacaan, default 60 detik.
  • lua_socket_buffer_size: ukuran buffer untuk membaca data, default 4k/8k.
  • lua_socket_pool_size: ukuran pool koneksi, default 30.
  • lua_socket_keepalive_timeout: waktu idle objek cosocket di pool koneksi, default 60 detik.
  • lua_socket_log_errors: apakah akan mencatat kesalahan cosocket ketika terjadi, default adalah on.

Di sini Anda juga dapat melihat bahwa beberapa perintah memiliki fungsi yang sama dengan API, seperti mengatur timeout dan ukuran pool koneksi. Namun, jika ada konflik antara keduanya, API memiliki prioritas lebih tinggi daripada perintah dan akan menggantikan nilai yang ditetapkan oleh perintah. Jadi, secara umum, kami merekomendasikan menggunakan API untuk melakukan pengaturan, yang juga lebih fleksibel.

Selanjutnya, mari kita lihat contoh konkret untuk memahami cara menggunakan API cosocket ini. Fungsi dari kode berikut ini sederhana, yaitu mengirim permintaan TCP ke sebuah situs web dan mencetak konten yang dikembalikan:

$ resty -e 'local sock = ngx.socket.tcp() sock:settimeout(1000) -- timeout satu detik local ok, err = sock:connect("api7.ai", 80) if not ok then ngx.say("failed to connect: ", err) return end local req_data = "GET / HTTP/1.1\r\nHost: api7.ai\r\n\r\n" local bytes, err = sock:send(req_data) if err then ngx.say("failed to send: ", err) return end local data, err, partial = sock:receive() if err then ngx.say("failed to receive: ", err) return end sock:close() ngx.say("response is: ", data)'

Mari kita analisis kode ini secara detail.

  • Pertama, buat objek TCP cosocket dengan nama sock menggunakan ngx.socket.tcp().
  • Kemudian, gunakan settimeout() untuk mengatur timeout menjadi 1 detik. Perhatikan bahwa timeout di sini tidak membedakan antara koneksi dan penerimaan; ini adalah pengaturan yang seragam.
  • Selanjutnya, gunakan API connect() untuk terhubung ke port 80 dari situs web yang ditentukan dan keluar jika gagal.
  • Jika koneksi berhasil, gunakan send() untuk mengirim data yang telah dibangun dan keluar jika gagal.
  • Jika pengiriman data berhasil, gunakan receive() untuk menerima data dari situs web. Di sini, parameter default dari receive() adalah *l, yang berarti hanya baris pertama data yang dikembalikan. Jika parameter diatur ke *a, ia akan menerima data hingga koneksi ditutup.
  • Terakhir, panggil close() untuk menutup koneksi socket secara aktif.

Seperti yang dapat Anda lihat, menggunakan API cosocket untuk melakukan komunikasi jaringan sangat sederhana hanya dalam beberapa langkah. Mari kita lakukan beberapa penyesuaian untuk mengeksplorasi contoh ini lebih dalam.

1. Mengatur waktu timeout untuk setiap dari tiga tindakan: koneksi socket, pengiriman, dan pembacaan.

settimeout() yang kita gunakan untuk mengatur waktu timeout ke satu nilai. Untuk mengatur waktu timeout secara terpisah, Anda perlu menggunakan fungsi settimeouts(), seperti berikut.

sock:settimeouts(1000, 2000, 3000)

Parameter settimeouts adalah dalam milidetik. Baris kode ini menunjukkan timeout koneksi 1 detik, timeout pengiriman 2 detik, dan timeout pembacaan 3 detik.

Di OpenResty dan pustaka lua-resty, sebagian besar parameter API yang terkait dengan waktu adalah dalam milidetik. Tetapi ada pengecualian yang perlu Anda perhatikan saat memanggilnya.

2. Menerima konten dengan ukuran yang ditentukan.

Seperti yang baru saja saya katakan, API receive() dapat menerima satu baris data atau menerima data secara terus-menerus. Namun, jika Anda hanya ingin menerima data sebesar 10K, bagaimana Anda harus mengaturnya?

Di sinilah receiveany() hadir. Ini dirancang untuk memenuhi kebutuhan ini, jadi lihat baris kode berikut.

local data, err, partial = sock:receiveany(10240)

Kode ini berarti hanya akan menerima data hingga 10K.

Tentu saja, kebutuhan pengguna umum lainnya untuk receive() adalah terus mengambil data hingga menemukan string yang ditentukan.

receiveuntil() dirancang untuk menyelesaikan masalah semacam ini. Alih-alih mengembalikan string seperti receive() dan receiveany(), ia akan mengembalikan iterator. Dengan cara ini, Anda dapat memanggilnya dalam loop untuk membaca data yang cocok secara segmen dan mengembalikan nil ketika pembacaan selesai. Berikut adalah contohnya.

local reader = sock:receiveuntil("\r\n") while true do local data, err, partial = reader(4) if not data then if err then ngx.say("failed to read the data stream: ", err) break end ngx.say("read done") break end ngx.say("read chunk: [", data, "]") end

receiveuntil mengembalikan data sebelum \r\n dan membaca empat byte sekaligus melalui iterator.

3. Alih-alih menutup socket langsung, masukkan ke dalam pool koneksi.

Seperti yang kita ketahui, tanpa pool koneksi, koneksi baru harus dibuat, menyebabkan objek cosocket dibuat setiap kali permintaan datang dan sering dihancurkan, mengakibatkan kehilangan performa yang tidak perlu.

Untuk menghindari masalah ini, setelah Anda selesai menggunakan cosocket, Anda dapat memanggil setkeepalive() untuk memasukkannya ke dalam pool koneksi, seperti berikut.

local ok, err = sock:setkeepalive(2 * 1000, 100) if not ok then ngx.say("failed to set reusable: ", err) end

Kode ini mengatur waktu idle koneksi menjadi 2 detik dan ukuran pool koneksi menjadi 100 sehingga ketika fungsi connect() dipanggil, objek cosocket akan diambil dari pool koneksi terlebih dahulu.

Namun, ada dua hal yang perlu kita perhatikan saat menggunakan pool koneksi.

  • Pertama, Anda tidak dapat memasukkan koneksi yang error ke dalam pool koneksi. Jika tidak, saat Anda menggunakannya lagi, pengiriman dan penerimaan data akan gagal. Ini adalah salah satu alasan mengapa kita perlu menentukan apakah setiap panggilan API berhasil atau tidak.
  • Kedua, kita perlu memahami jumlah koneksi. Pool koneksi adalah tingkat Worker, dan setiap Worker memiliki pool koneksinya sendiri. Jika Anda memiliki 10 Worker dan ukuran pool koneksi diatur ke 30, itu berarti 300 koneksi untuk layanan backend.

Ringkasan

Untuk merangkum, kita telah mempelajari konsep dasar, perintah terkait, dan API dari cosocket. Sebuah contoh praktis membuat kita familiar dengan cara menggunakan API yang terkait dengan TCP. Penggunaan UDP dan Unix Domain Socket mirip dengan TCP. Anda dapat dengan mudah menangani semua pertanyaan ini setelah memahami apa yang kita pelajari hari ini.

Kita tahu bahwa cosocket relatif mudah digunakan, dan kita dapat terhubung ke berbagai layanan eksternal dengan menggunakannya dengan baik.

Terakhir, kita dapat memikirkan dua pertanyaan.

Pertama, dalam contoh hari ini, tcpsock:send mengirim string; bagaimana jika kita perlu mengirim tabel yang terdiri dari string?

Kedua, seperti yang Anda lihat, cosocket tidak dapat digunakan di banyak tahap, jadi dapatkah Anda memikirkan beberapa cara untuk menghindarinya?

Silakan tinggalkan komentar dan bagikan dengan saya. Selamat berbagi artikel ini dengan kolega dan teman-teman Anda sehingga kita dapat berkomunikasi dan berkembang bersama.