Dynamic Rate-Limiting di OpenResty

API7.ai

January 6, 2023

OpenResty (NGINX + Lua)

Pada artikel sebelumnya, saya memperkenalkan Anda pada algoritma leaky bucket dan token bucket, yang umum digunakan untuk menangani lalu lintas yang meledak. Selain itu, kita juga belajar bagaimana melakukan pembatasan kecepatan permintaan menggunakan konfigurasi NGINX. Namun, menggunakan konfigurasi NGINX hanya berada pada tingkat kegunaan dan masih jauh dari menjadi berguna.

Masalah pertama adalah bahwa kunci pembatasan kecepatan terbatas pada rentang variabel NGINX dan tidak dapat diatur secara fleksibel. Misalnya, tidak ada cara untuk menetapkan ambang batas kecepatan yang berbeda untuk provinsi yang berbeda dan saluran klien yang berbeda, yang merupakan kebutuhan umum dengan NGINX.

Masalah lain yang lebih besar adalah bahwa kecepatan tidak dapat disesuaikan secara dinamis, dan setiap perubahan memerlukan reload layanan NGINX. Akibatnya, membatasi kecepatan berdasarkan periode yang berbeda hanya dapat diimplementasikan dengan cara yang kurang optimal melalui skrip eksternal.

Penting untuk memahami bahwa teknologi melayani bisnis, dan pada saat yang sama, bisnis mendorong teknologi. Pada saat kelahiran NGINX, hampir tidak ada kebutuhan untuk menyesuaikan konfigurasi secara dinamis; lebih banyak tentang reverse proxying, load balancing, penggunaan memori rendah, dan kebutuhan serupa lainnya yang mendorong pertumbuhan NGINX. Dalam hal arsitektur dan implementasi teknologi, tidak ada yang bisa memprediksi ledakan besar kebutuhan untuk kontrol dinamis dan granular halus dalam skenario seperti Internet seluler, IoT, dan microservices.

Penggunaan OpenResty dengan skrip Lua mengatasi kekurangan NGINX dalam hal ini, menjadikannya sebagai pelengkap yang efektif. Inilah mengapa OpenResty sangat banyak digunakan sebagai pengganti NGINX. Dalam beberapa artikel berikutnya, saya akan terus memperkenalkan Anda pada skenario dan contoh yang lebih dinamis di OpenResty. Mari kita mulai dengan melihat bagaimana menggunakan OpenResty untuk mengimplementasikan pembatasan kecepatan dinamis.

Di OpenResty, kami merekomendasikan penggunaan lua-resty-limit-traffic untuk membatasi lalu lintas. Ini mencakup limit-req (membatasi kecepatan permintaan), limit-count (membatasi jumlah permintaan), dan limit-conn (membatasi koneksi bersamaan); dan menyediakan limit.traffic untuk menggabungkan ketiga metode ini.

Membatasi kecepatan permintaan

Mari kita mulai dengan melihat limit-req, yang menggunakan algoritma leaky bucket untuk membatasi kecepatan permintaan.

Pada bagian sebelumnya, kami memperkenalkan secara singkat kode implementasi kunci dari algoritma leaky bucket dalam library resty ini, dan sekarang kita akan belajar bagaimana menggunakan library ini. Pertama, mari kita lihat kode contoh berikut.

resty --shdict='my_limit_req_store 100m' -e 'local limit_req = require "resty.limit.req" local lim, err = limit_req.new("my_limit_req_store", 200, 100) local delay, err = lim:incoming("key", true) if not delay then if err == "rejected" then return ngx.exit(503) end return ngx.exit(500) end if delay >= 0.001 then ngx.sleep(delay) end'

Kita tahu bahwa lua-resty-limit-traffic menggunakan shared dict untuk menyimpan dan menghitung kunci, jadi kita perlu mendeklarasikan ruang 100m untuk my_limit_req_store sebelum kita dapat menggunakan limit-req. Ini serupa untuk limit-conn dan limit-count, yang keduanya memerlukan ruang shared dict terpisah untuk dibedakan.

limit_req.new("my_limit_req_store", 200, 100)

Baris kode di atas adalah salah satu baris kode yang paling kritis. Ini berarti bahwa shared dict yang disebut my_limit_req_store digunakan untuk menyimpan statistik, dan kecepatan per detik diatur ke 200, sehingga jika melebihi 200 tetapi kurang dari 300 (nilai ini dihitung dari 200 + 100), itu akan diantrekan, dan jika melebihi 300, itu akan ditolak.

Setelah pengaturan selesai, kita harus memproses permintaan dari klien. lim: incoming("key", true) ada di sini untuk melakukan ini. incoming memiliki dua parameter, yang perlu kita baca secara detail.

Parameter pertama, kunci yang ditentukan pengguna untuk pembatasan kecepatan, adalah konstanta string dalam contoh di atas, yang berarti bahwa pembatasan kecepatan harus seragam untuk semua klien. Jika Anda ingin membatasi kecepatan sesuai dengan provinsi dan saluran yang berbeda, sangat sederhana untuk menggunakan kedua informasi tersebut sebagai kunci, dan berikut adalah kode pseudo untuk mencapai kebutuhan ini.

local province = get_ province(ngx.var.binary_remote_addr) local channel = ngx.req.get_headers()["channel"] local key = province .. channel lim:incoming(key, true)

Tentu saja, Anda juga dapat menyesuaikan arti kunci dan kondisi untuk memanggil incoming, sehingga Anda bisa mendapatkan efek pembatasan kecepatan yang sangat fleksibel.

Mari kita lihat parameter kedua dari fungsi incoming, dan itu adalah nilai boolean. Defaultnya adalah false, yang berarti bahwa permintaan tidak akan dicatat dalam shared dict untuk statistik; itu hanya latihan. Jika diatur ke true, itu akan memiliki efek nyata. Oleh karena itu, dalam kebanyakan kasus, Anda perlu mengaturnya ke true secara eksplisit.

Anda mungkin bertanya-tanya mengapa parameter ini ada. Pertimbangkan skenario di mana Anda mengatur dua instance limit-req yang berbeda dengan kunci yang berbeda, satu kunci adalah nama host dan kunci lainnya adalah alamat IP klien. Kemudian, ketika permintaan klien diproses, metode incoming dari kedua instance ini dipanggil secara berurutan, seperti yang ditunjukkan oleh kode pseudo berikut.

local limiter_one, err = limit_req.new("my_limit_req_store", 200, 100) local limiter_two, err = limit_req.new("my_limit_req_store", 20, 10) limiter_one :incoming(ngx.var.host, true) limiter_two:incoming(ngx.var.binary_remote_addr, true)

Jika permintaan pengguna melewati deteksi ambang batas limiter_one tetapi ditolak oleh deteksi limiter_two, maka panggilan fungsi limiter_one:incoming harus dianggap sebagai latihan dan kita tidak perlu menghitungnya.

Dalam kasus ini, logika kode di atas tidak cukup ketat. Kita perlu melakukan walkthrough dari semua limiter terlebih dahulu sehingga jika ambang batas limiter dipicu yang dapat menolak permintaan klien, itu dapat langsung kembali.

for i = 1, n do local lim = limiters[i] local delay, err = lim:incoming(keys[i], i == n) if not delay then return nil, err end end

Inilah yang dimaksud dengan argumen kedua dari fungsi incoming. Kode ini adalah kode inti dari modul limit.traffic, yang digunakan untuk menggabungkan beberapa pembatas kecepatan.

Membatasi jumlah permintaan

Mari kita lihat limit.count, sebuah library yang membatasi jumlah permintaan. Ini bekerja seperti GitHub API Rate Limiting, yang membatasi jumlah permintaan pengguna dalam jendela waktu tetap. Seperti biasa, mari kita mulai dengan kode contoh.

local limit_count = require "resty.limit.count" local lim, err = limit_count.new("my_limit_count_store", 5000, 3600) local key = ngx.req.get_headers()["Authorization"] local delay, remaining = lim:incoming(key, true)

Anda dapat melihat bahwa limit.count dan limit.req digunakan dengan cara yang sama. Kita mulai dengan mendefinisikan shared dict di nginx.conf.

lua_shared_dict my_limit_count_store 100m;

Kemudian new sebuah objek limiter, dan akhirnya menggunakan fungsi incoming untuk menentukan dan memprosesnya.

Namun, perbedaannya adalah bahwa nilai kembalian kedua dari fungsi incoming dalam limit-count mewakili panggilan yang tersisa, dan kita dapat menambahkan bidang ke header respons sesuai untuk memberikan indikasi yang lebih baik kepada klien.

ngx.header["X-RateLimit-Limit"] = "5000" ngx.header["X-RateLimit-Remaining"] = remaining

Membatasi jumlah koneksi bersamaan

limit.conn adalah library untuk membatasi jumlah koneksi bersamaan. Ini berbeda dari dua library yang disebutkan sebelumnya karena memiliki API leaving khusus, yang akan saya jelaskan secara singkat di sini.

Membatasi kecepatan permintaan dan jumlah permintaan, seperti yang disebutkan di atas, dapat dilakukan langsung dalam fase access. Tidak seperti membatasi jumlah koneksi bersamaan, yang memerlukan tidak hanya menentukan apakah ambang batas terlampaui dalam fase access tetapi juga memanggil API leaving dalam fase log.

log_by_lua_block { local latency = tonumber(ngx.var.request_time) - ctx.limit_conn_delay local key = ctx.limit_conn_key local conn, err = lim:leaving(key, latency) }

Namun, kode inti dari API ini cukup sederhana, yaitu baris kode berikut yang mengurangi jumlah koneksi dengan satu. Jika Anda tidak membersihkan dalam fase log, jumlah koneksi akan terus naik dan segera mencapai ambang batas konkurensi.

local conn, err = dict:incr(key, -1)

Kombinasi pembatas kecepatan

Ini adalah akhir dari pengenalan masing-masing dari ketiga metode ini. Akhirnya, mari kita lihat bagaimana menggabungkan limit.rate, limit.conn dan limit.count. Di sini kita perlu menggunakan fungsi combine dalam limit.traffic.

local lim1, err = limit_req.new("my_req_store", 300, 200) local lim2, err = limit_req.new("my_req_store", 200, 100) local lim3, err = limit_conn.new("my_conn_store", 1000, 1000, 0.5) local limiters = {lim1, lim2, lim3} local host = ngx.var.host local client = ngx.var.binary_remote_addr local keys = {host, client, client} local delay, err = limit_traffic.combine(limiters, keys, states)

Kode ini seharusnya mudah dipahami dengan pengetahuan yang baru saja Anda dapatkan. Kode inti dari fungsi combine, yang sudah kami sebutkan dalam analisis limit.rate di atas, terutama diimplementasikan dengan bantuan fungsi drill dan fungsi uncommit. Kombinasi ini memungkinkan Anda untuk menetapkan ambang batas dan kunci yang berbeda untuk beberapa limiter untuk mencapai kebutuhan bisnis yang lebih kompleks.

Ringkasan

Tidak hanya limit.traffic mendukung tiga pembatas kecepatan yang disebutkan hari ini, tetapi selama pembatas kecepatan memiliki API incoming dan uncommit, itu dapat dikelola oleh fungsi combine dari limit.traffic.

Terakhir, saya akan memberikan Anda tugas rumah. Bisakah Anda menulis contoh yang menggabungkan pembatas kecepatan token dan bucket yang kami perkenalkan sebelumnya? Jangan ragu untuk menulis jawaban Anda di bagian komentar untuk berdiskusi dengan saya, dan Anda juga dipersilakan untuk membagikan artikel ini dengan rekan dan teman Anda untuk belajar dan berkomunikasi bersama.