Menangani Layer 4 dari Traffic dan Menerapkan Memcached Server dengan OpenResty

API7.ai

November 10, 2022

OpenResty (NGINX + Lua)

Dalam beberapa artikel sebelumnya, kami memperkenalkan beberapa API Lua untuk menangani permintaan, yang semuanya terkait dengan Lapisan 7. Selain itu, OpenResty menyediakan modul stream-lua-nginx-module untuk menangani lalu lintas dari Lapisan 4. Modul ini menyediakan instruksi dan API yang pada dasarnya sama dengan lua-nginx-module.

Hari ini, kita akan membahas tentang implementasi server Memcached dengan OpenResty, yang hanya membutuhkan sekitar 100 baris kode. Dalam praktik kecil ini, kita akan menggunakan banyak hal yang telah kita pelajari sebelumnya, dan kita juga akan memasukkan beberapa konten dari bab pengujian dan optimasi performa nanti.

Dan kita harus memahami bahwa poin dari artikel ini bukan untuk memahami fungsi setiap baris kode, tetapi untuk memahami gambaran lengkap tentang bagaimana OpenResty mengembangkan sebuah proyek dari awal, dari perspektif kebutuhan, pengujian, pengembangan, dll.

Kebutuhan asli dan solusi teknis

Kita tahu bahwa lalu lintas HTTPS semakin menjadi arus utama, tetapi beberapa browser lama tidak mendukung session tickets, sehingga kita perlu menyimpan session ID di sisi server. Jika ruang penyimpanan lokal tidak mencukupi, kita memerlukan kluster untuk penyimpanan, dan data dapat dibuang, sehingga Memcached lebih cocok.

Pada titik ini, memperkenalkan Memcached seharusnya menjadi solusi yang paling langsung. Namun, dalam artikel ini, kita akan memilih untuk menggunakan OpenResty untuk membangun roda karena alasan berikut.

  • Pertama, memperkenalkan Memcached secara langsung akan menambahkan proses tambahan, meningkatkan biaya penyebaran dan pemeliharaan.
  • Kedua, kebutuhan cukup sederhana, hanya memerlukan operasi get dan set, dan mendukung kedaluwarsa.
  • Ketiga, OpenResty memiliki modul stream, yang dapat dengan cepat memenuhi kebutuhan ini.

Karena kita ingin mengimplementasikan server Memcached, kita perlu memahami protokolnya terlebih dahulu. Protokol Memcached dapat mendukung TCP dan UDP. Di sini kita menggunakan TCP. Berikut adalah protokol spesifik dari perintah get dan set.

Get get value with key Telnet command: get <key>*\r\n Contoh: get key VALUE key 0 4 data END
Set Simpan key-value ke memcached Telnet command:set <key> <flags> <exptime> <bytes> [noreply]\r\n<value>\r\n Contoh: set key 0 900 4 data STORED

Kita juga perlu tahu bagaimana "penanganan kesalahan" dari protokol Memcached diimplementasikan selain get dan set. "Penanganan kesalahan" sangat penting untuk program sisi server, dan kita perlu menulis program yang tidak hanya menangani permintaan normal tetapi juga pengecualian. Misalnya, dalam skenario seperti berikut:

  • Memcached mengirimkan permintaan selain get atau set, bagaimana cara menanganinya?
  • Jenis umpan balik apa yang harus diberikan ke klien Memcached ketika ada kesalahan di sisi server?

Selain itu, kita ingin menulis aplikasi klien yang kompatibel dengan Memcached. Dengan cara ini, pengguna tidak perlu membedakan antara versi resmi Memcached dan implementasi OpenResty.

Gambar berikut dari dokumentasi Memcached menjelaskan apa yang harus dikembalikan dalam kasus kesalahan dan format yang tepat, yang dapat Anda gunakan sebagai referensi.

format kesalahan

Sekarang, mari kita definisikan solusi teknis. Kita tahu bahwa shared dict OpenResty dapat digunakan di seluruh worker dan bahwa menyimpan data di shared dict sangat mirip dengan menyimpannya di Memcached. Keduanya mendukung operasi get dan set, dan data akan hilang ketika proses dimulai ulang. Oleh karena itu, menggunakan shared dict untuk meniru Memcached adalah tepat, karena prinsip dan perilakunya sama.

Pengembangan Berbasis Pengujian

Langkah selanjutnya adalah mulai mengerjakannya. Namun, berdasarkan ide pengembangan berbasis pengujian, mari kita buat kasus uji paling sederhana sebelum menulis kode spesifik. Alih-alih menggunakan kerangka kerja test::nginx, yang terkenal sulit untuk memulai, mari kita mulai dengan pengujian manual menggunakan resty.

$ resty -e 'local memcached = require "resty.memcached" local memc, err = memcached:new() memc:set_timeout(1000) -- 1 detik local ok, err = memc:connect("127.0.0.1", 11212) local ok, err = memc:set("dog", 32) if not ok then ngx.say("failed to set dog: ", err) return end local res, flags, err = memc:get("dog") ngx.say("dog: ", res)'

Kode uji ini menggunakan pustaka klien lua-rety-memcached untuk memulai operasi connect dan set dan mengasumsikan bahwa server Memcached mendengarkan pada port 11212 di mesin lokal.

Sepertinya ini akan berfungsi dengan baik. Anda dapat menjalankan kode ini di mesin Anda, dan, tidak mengherankan, ini akan mengembalikan kesalahan seperti failed to set dog: closed, karena layanan belum dimulai pada titik ini.

Pada titik ini, solusi teknis Anda jelas: gunakan modul stream untuk menerima dan mengirim data dan gunakan shared dict untuk menyimpannya.

Metrik untuk mengukur penyelesaian kebutuhan jelas: jalankan kode di atas dan cetak nilai aktual dari dog.

Membangun kerangka kerja

Jadi apa yang Anda tunggu? Mulailah menulis kode!

Kebiasaan saya adalah membangun kerangka kode yang dapat dijalankan minimal terlebih dahulu dan kemudian secara bertahap mengisi kode. Keuntungan dari ini adalah Anda dapat menetapkan banyak tujuan kecil selama proses pengkodean, dan kasus uji akan memberikan umpan balik positif ketika Anda mencapai tujuan kecil.

Mari kita mulai dengan menyiapkan file konfigurasi NGINX karena stream dan shared dict harus diatur sebelumnya di dalamnya. Berikut adalah file konfigurasi yang saya siapkan.

stream { lua_shared_dict memcached 100m; lua_package_path 'lib/?.lua;;'; server { listen 11212; content_by_lua_block { local m = require("resty.memcached.server") m.run() } } }

Seperti yang Anda lihat, beberapa informasi kunci ada dalam file konfigurasi ini.

  • Pertama, kode berjalan dalam konteks stream NGINX, bukan konteks HTTP, dan mendengarkan pada port 11212.
  • Kedua, nama shared dict adalah memcached, dan ukurannya adalah 100M, yang tidak dapat diubah saat runtime.
  • Selain itu, kode berada di direktori lib/resty/memcached, nama file adalah server.lua, dan fungsi masuk adalah run(), yang dapat Anda temukan dari lua_package_path dan content_by_lua_block.

Selanjutnya, saatnya membangun kerangka kode. Anda dapat mencobanya sendiri, dan kemudian mari kita lihat kerangka kode saya bersama-sama.

local new_tab = require "table.new" local str_sub = string.sub local re_find = ngx.re.find local mc_shdict = ngx.shared.memcached local _M = { _VERSION = '0.01' } local function parse_args(s, start) end function _M.get(tcpsock, keys) end function _M.set(tcpsock, res) end function _M.run() local tcpsock = assert(ngx.req.socket(true)) while true do tcpsock:settimeout(60000) -- 60 detik local data, err = tcpsock:receive("*l") local command, args if data then local from, to, err = re_find(data, [[(\S+)]], "jo") if from then command = str_sub(data, from, to) args = parse_args(data, to + 1) end end if args then local args_len = #args if command == 'get' and args_len > 0 then _M.get(tcpsock, args) elseif command == "set" and args_len == 4 then _M.set(tcpsock, args) end end end end return _M

Cuplikan kode ini mengimplementasikan logika utama dari fungsi masuk run(). Meskipun saya belum melakukan penanganan kesalahan apa pun dan dependensi parse_args, get, dan set semuanya adalah fungsi kosong, kerangka kerja ini sudah sepenuhnya mengekspresikan logika server Memcached.

Mengisi kode

Selanjutnya, mari kita implementasikan fungsi-fungsi kosong ini sesuai urutan eksekusi kode.

Pertama, kita dapat mengurai parameter perintah Memcached sesuai dengan dokumentasi protokol Memcached.

local function parse_args(s, start) local arr = {} while true do local from, to = re_find(s, [[\S+]], "jo", {pos = start}) if not from then break end table.insert(arr, str_sub(s, from, to)) start = to + 1 end return arr end

Saran saya adalah mengimplementasikan versi yang paling intuitif terlebih dahulu, tanpa memikirkan optimasi performa apa pun. Bagaimanapun, penyelesaian selalu lebih penting daripada kesempurnaan, dan optimasi bertahap berdasarkan penyelesaian adalah satu-satunya cara untuk mendekati kesempurnaan.

Selanjutnya, mari kita implementasikan fungsi get. Ini dapat mengkueri beberapa kunci sekaligus, jadi saya menggunakan loop for dalam kode berikut.

function _M.get(tcpsock, keys) local reply = "" for i = 1, #keys do local key = keys[i] local value, flags = mc_shdict:get(key) if value then local flags = flags or 0 reply = reply .. "VALUE" .. key .. " " .. flags .. " " .. #value .. "\r\n" .. value .. "\r\n" end end reply = reply .. "END\r\n" tcpsock:settimeout(1000) -- timeout satu detik local bytes, err = tcpsock:send(reply) end

Hanya ada satu baris kode inti di sini: local value, flags = mc_shdict:get(key), yaitu mengkueri data dari shared dict; selebihnya, kode mengikuti protokol Memcached untuk menyambung string dan akhirnya mengirimkannya ke klien.

Terakhir, mari kita lihat fungsi set. Ini mengubah parameter yang diterima menjadi format API shared dict, menyimpan data, dan dalam kasus kesalahan, menanganinya sesuai protokol Memcached.

function _M.set(tcpsock, res) local reply = "" local key = res[1] local flags = res[2] local exptime = res[3] local bytes = res[4] local value, err = tcpsock:receive(tonumber(bytes) + 2) if str_sub(value, -2, -1) == "\r\n" then local succ, err, forcible = mc_shdict:set(key, str_sub(value, 1, bytes), exptime, flags) if succ then reply = reply .. “STORED\r\n" else reply = reply .. "SERVER_ERROR " .. err .. “\r\n” end else reply = reply .. "ERROR\r\n" end tcpsock:settimeout(1000) -- timeout satu detik local bytes, err = tcpsock:send(reply) end

Selain itu, Anda dapat menggunakan kasus uji untuk memeriksa dan men-debug dengan ngx.log saat mengisi fungsi-fungsi di atas. Sayangnya, kita menggunakan ngx.say dan ngx.log untuk men-debug karena tidak ada debugger break points di OpenResty, yang masih merupakan era slash-and-burn yang menunggu eksplorasi lebih lanjut.

Ringkasan

Proyek praktik ini sekarang berakhir, dan akhirnya, saya ingin meninggalkan pertanyaan: Bisakah Anda mengambil kode implementasi server Memcached di atas, menjalankannya sepenuhnya, dan melewati kasus uji?

Pertanyaan hari ini mungkin membutuhkan banyak usaha, tetapi ini masih versi primitif. Tidak ada penanganan kesalahan, optimasi performa, dan pengujian otomatis, yang akan ditingkatkan nanti.

Jika Anda memiliki keraguan tentang penjelasan hari ini atau praktik Anda, Anda dipersilakan untuk meninggalkan komentar dan mendiskusikannya dengan kami. Anda juga dipersilakan untuk membagikan artikel ini dengan rekan dan teman Anda sehingga kita dapat berlatih dan berkembang bersama.