Bagian 3: Cara Membangun Microservices API Gateway Menggunakan OpenResty

API7.ai

February 3, 2023

OpenResty (NGINX + Lua)

Dalam artikel ini, pembangunan gateway API microservices telah mencapai akhir. Mari kita gunakan contoh minimal untuk menyatukan komponen-komponen yang telah dipilih sebelumnya dan menjalankannya sesuai dengan cetak biru yang telah dirancang!

Konfigurasi dan Inisialisasi NGINX

Kita tahu bahwa gateway API digunakan untuk menangani lalu lintas masuk, jadi pertama-tama kita perlu melakukan konfigurasi sederhana di nginx.conf agar semua lalu lintas ditangani melalui kode Lua dari gateway.

server { listen 9080; init_worker_by_lua_block { apisix.http_init_worker() } location / { access_by_lua_block { apisix.http_access_phase() } header_filter_by_lua_block { apisix.http_header_filter_phase() } body_filter_by_lua_block { apisix.http_body_filter_phase() } log_by_lua_block { apisix.http_log_phase() } } }

Di sini kita menggunakan gateway API open-source Apache APISIX sebagai contoh, sehingga contoh kode di atas memiliki kata kunci apisix di dalamnya. Dalam contoh ini, kita mendengarkan port 9080 dan memotong semua permintaan ke port ini melalui location /, dan memprosesnya melalui fase access, rewrite, header filter, body filter, dan log, memanggil fungsi plugin yang sesuai di setiap fase. Fase rewrite digabungkan dalam fungsi apisix.http_access_phase.

Inisialisasi sistem ditangani dalam fase init_worker, yang mencakup membaca parameter konfigurasi, mengatur direktori di etcd, mendapatkan daftar plugin dari etcd, dan mengurutkan plugin berdasarkan prioritas, dll. Saya telah mencantumkan dan menjelaskan bagian kunci dari kode di sini, dan Anda dapat melihat fungsi inisialisasi yang lebih lengkap di GitHub.

function _M.http_init_worker() -- Inisialisasi Routing, Services, dan Plugins - tiga bagian terpenting router.init_worker() require("apisix.http.service").init_worker() require("apisix.plugins.ext-plugin.init").init_worker() end

Seperti yang dapat Anda lihat dari kode ini, inisialisasi bagian router dan plugin sedikit lebih rumit, terutama melibatkan membaca parameter konfigurasi dan membuat beberapa pilihan tergantung pada parameter tersebut. Karena ini melibatkan membaca data dari etcd, kita menggunakan ngx.timer untuk menghindari batasan "tidak dapat menggunakan cosocket dalam fase init_worker". Jika Anda tertarik dengan bagian ini, kami merekomendasikan untuk membaca kode sumber untuk memahaminya dengan lebih baik.

Pencocokan Rute

Pada awal fase access, pertama-tama kita perlu mencocokkan rute berdasarkan permintaan yang membawa uri, host, args, cookies, dll., dengan aturan rute yang telah ditetapkan.

router.router_http.match(api_ctx)

Satu-satunya kode yang diekspos ke publik adalah baris di atas, di mana api_ctx menyimpan informasi uri, host, args, dan cookie dari permintaan. Implementasi spesifik dari fungsi pencocokan menggunakan lua-resty-radixtree yang telah kita sebutkan sebelumnya. Jika tidak ada rute yang cocok, permintaan tidak memiliki upstream yang sesuai, dan akan mengembalikan 404.

local router = require("resty.radixtree") local match_opts = {} function _M.match(api_ctx) -- Dapatkan parameter permintaan dari ctx dan gunakan sebagai kondisi penilaian untuk rute match_opts.method = api_ctx.var.method match_opts.host = api_ctx.var.host match_opts.remote_addr = api_ctx.var.remote_addr match_opts.vars = api_ctx.var -- Panggil fungsi penilaian rute local ok = uri_router:dispatch(api_ctx.var.uri, match_opts, api_ctx) -- Jika tidak ada rute yang cocok, kembalikan 404 if not ok then core.log.info("not find any matched route") return core.response.exit(404) end return true end

Memuat Plugin

Tentu saja, jika rute dapat ditemukan, langkah selanjutnya adalah memfilter dan memuat plugin, yang merupakan inti dari gateway API. Mari kita mulai dengan kode berikut.

local plugins = core.tablepool.fetch("plugins", 32, 0) -- Daftar plugin di etcd dan daftar plugin di file konfigurasi lokal diinterseksikan api_ctx.plugins = plugin.filter(route, plugins) -- Jalankan fungsi yang dipasang oleh plugin dalam fase rewrite dan access secara berurutan run_plugin("rewrite", plugins, api_ctx) run_plugin("access", plugins, api_ctx)

Dalam kode ini, pertama-tama kita meminta tabel dengan panjang 32 melalui pool tabel, yang merupakan teknik optimasi performa yang telah kita perkenalkan sebelumnya. Kemudian datang fungsi filter dari plugin. Anda mungkin bertanya-tanya mengapa langkah ini diperlukan. Dalam fase init worker dari plugin, bukankah kita sudah mendapatkan daftar plugin dari etcd dan mengurutkannya?

Pemfilteran di sini dilakukan dengan membandingkan dengan konfigurasi lokal karena dua alasan berikut:

  1. Pertama, plugin yang baru dikembangkan perlu dirilis secara canary. Pada saat ini, plugin baru ada dalam daftar etcd tetapi hanya dalam keadaan terbuka di beberapa node gateway. Jadi, kita perlu melakukan operasi interseksi tambahan.
  2. Untuk mendukung mode debug. Plugin mana yang diproses oleh permintaan klien? Apa urutan pemuatan plugin ini? Informasi ini akan berguna saat debugging, sehingga fungsi filter juga akan menentukan apakah sedang dalam mode debug, dan mencatat informasi ini di header respons.

Jadi pada akhir fase access, kita mengambil plugin yang telah difilter ini dan menjalankannya satu per satu sesuai urutan prioritas, seperti yang ditunjukkan dalam kode berikut.

local function run_plugin(phase, plugins, api_ctx) for i = 1, #plugins, 2 do local phase_fun = plugins[i][phase] if phase_fun then -- Kode pemanggilan inti phase_fun(plugins[i + 1], api_ctx) end end return api_ctx end

Saat mengulang plugin, Anda dapat melihat bahwa kita melakukannya dalam interval 2. Ini karena setiap plugin akan memiliki dua komponen: objek plugin dan parameter konfigurasi plugin. Sekarang, mari kita lihat baris kode inti dalam contoh kode di atas.

phase_fun(plugins[i + 1], api_ctx)

Jika baris kode ini sedikit abstrak, mari kita ganti dengan plugin limit_count yang konkret, yang akan jauh lebih jelas.

limit_count_plugin_rewrite_function(conf_of_plugin, api_ctx)

Pada titik ini, kita hampir selesai dengan alur keseluruhan gateway API. Semua kode ini berada dalam file yang sama, yang berisi lebih dari 400 baris kode, tetapi inti dari kode adalah beberapa puluh baris yang telah kita jelaskan di atas.

Sekarang, ada satu hal yang tersisa sebelum demo penuh dapat dijalankan, yaitu menulis plugin. Mari kita ambil plugin limit-count sebagai contoh. Implementasi lengkapnya hanya lebih dari 60 baris kode, yang dapat Anda lihat dengan mengklik tautan. Di sini, saya akan menjelaskan baris kode kunci secara detail:

Pertama, kita akan memperkenalkan lua-resty-limit-traffic sebagai library dasar untuk membatasi jumlah permintaan.

local limit_count_new = require("resty.limit.count").new

Kemudian, menggunakan json schema dalam rapidjson untuk mendefinisikan apa parameter dari plugin ini:

local schema = { type = "object", properties = { count = {type = "integer", minimum = 0}, time_window = {type = "integer", minimum = 0}, key = {type = "string", enum = {"remote_addr", "server_addr"}, }, rejected_code = {type = "integer", minimum = 200, maximum = 600}, }, additionalProperties = false, required = {"count", "time_window", "key", "rejected_code"}, }

Parameter plugin ini sesuai dengan sebagian besar parameter resty.limit.count, yang berisi kunci pembatasan, ukuran jendela waktu, dan jumlah permintaan yang akan dibatasi. Selain itu, plugin menambahkan satu parameter: rejected_code, yang mengembalikan kode status yang ditentukan ketika permintaan dibatasi.

Pada langkah terakhir, kita memasang fungsi handler plugin ke fase rewrite:

function _M.rewrite(conf, ctx) -- Dapatkan objek limit count dari cache, jika tidak, gunakan fungsi `create_limit_obj` untuk membuat objek baru dan menyimpannya dalam cache local lim, err = core.lrucache.plugin_ctx(plugin_name, ctx, create_limit_obj, conf) -- Dapatkan nilai kunci dari `ctx.var` dan gabungkan dengan tipe konfigurasi dan nomor versi konfigurasi untuk membuat kunci baru local key = (ctx.var[conf.key] or "") .. ctx.conf_type .. ctx.conf_version -- Fungsi untuk menentukan apakah batasan telah tercapai local delay, remaining = lim:incoming(key, true) if not delay then local err = remaining -- Jika nilai ambang batas terlampaui, kembalikan kode status yang ditentukan if err == "rejected" then return conf.rejected_code end core.log.error("failed to limit req: ", err) return 500 end -- Jika nilai ambang batas tidak terlampaui, lepaskan dan atur header respons yang sesuai core.response.set_header("X-RateLimit-Limit", conf.count, "X-RateLimit-Remaining", remaining) end

Hanya ada satu baris logika dalam kode di atas yang membuat penentuan batasan, sisanya di sini untuk melakukan pekerjaan persiapan dan mengatur header respons. Jika nilai ambang batas tidak terlampaui, itu akan terus menjalankan plugin berikutnya sesuai dengan prioritas.

Ringkasan

Terakhir, saya akan meninggalkan Anda dengan pertanyaan yang merenungkan. Kita tahu bahwa gateway API dapat menangani tidak hanya lalu lintas Layer 7 tetapi juga lalu lintas Layer 4. Berdasarkan ini, dapatkah Anda memikirkan beberapa skenario penggunaannya? Silakan tinggalkan komentar Anda dan bagikan artikel ini untuk belajar dan berkomunikasi dengan lebih banyak orang.

Sebelumnya: Bagian 1: Cara Membangun Gateway API Microservices menggunakan OpenResty Bagian 2: Cara Membangun Gateway API Microservices menggunakan OpenResty