`lua-resty-*` Enkapsulasi Membebaskan Pengembang dari Multi-Level Caching
API7.ai
December 30, 2022
Dalam dua artikel sebelumnya, kita telah mempelajari tentang caching di OpenResty dan masalah cache stampede, yang semuanya berada di sisi dasar. Dalam pengembangan proyek aktual, para pengembang lebih memilih library yang siap pakai dengan semua detail yang ditangani dan disembunyikan, sehingga dapat digunakan langsung untuk mengembangkan kode bisnis.
Ini adalah manfaat dari pembagian tugas, di mana pengembang komponen dasar fokus pada arsitektur yang fleksibel, kinerja yang baik, dan stabilitas kode tanpa perlu memikirkan logika bisnis tingkat atas; sementara insinyur aplikasi lebih memperhatikan implementasi bisnis dan iterasi cepat, berharap tidak terganggu oleh berbagai detail teknis dari lapisan bawah. Celah di antara keduanya dapat diisi oleh library wrapper.
Caching di OpenResty menghadapi masalah yang sama. shared dict dan lru caches cukup stabil dan efisien, tetapi ada terlalu banyak detail yang harus ditangani. "Mil terakhir" bagi insinyur pengembangan aplikasi bisa sangat sulit tanpa beberapa enkapsulasi yang berguna. Di sinilah pentingnya komunitas berperan. Komunitas yang aktif akan secara proaktif menemukan celah dan mengisinya dengan cepat.
lua-resty-memcached-shdict
Mari kembali ke enkapsulasi cache. lua-resty-memcached-shdict adalah proyek resmi OpenResty yang menggunakan shared dict untuk membuat lapisan enkapsulasi untuk memcached, menangani detail seperti cache stampede dan data yang kedaluwarsa. Jika data cache Anda kebetulan disimpan di memcached di backend, maka Anda dapat mencoba menggunakan library ini.
Ini adalah library yang dikembangkan oleh OpenResty, tetapi tidak termasuk dalam paket OpenResty secara default. Jika Anda ingin mengujinya secara lokal, Anda perlu mengunduh source code terlebih dahulu ke jalur pencarian OpenResty lokal.
Library enkapsulasi ini adalah solusi yang sama yang kami sebutkan dalam artikel sebelumnya. Ini menggunakan lua-resty-lock untuk saling eksklusif. Jika cache gagal, hanya satu permintaan yang akan pergi ke memcached untuk mengambil data dan menghindari badai cache. Data yang sudah kedaluwarsa akan dikembalikan ke endpoint jika data terbaru tidak berhasil diambil.
Namun, library lua-resty ini, meskipun merupakan proyek resmi OpenResty, tidak sempurna:
- Pertama, tidak ada cakupan kasus uji, yang berarti kualitas kode tidak dapat dijamin secara konsisten.
- Kedua, terlalu banyak parameter antarmuka yang diekspos, dengan 11 parameter yang diperlukan dan 7 parameter opsional.
local memc_fetch, memc_store = shdict_memc.gen_memc_methods{ tag = "my memcached server tag", debug_logger = dlog, warn_logger = warn, error_logger = error_log, locks_shdict_name = "some_lua_shared_dict_name", shdict_set = meta_shdict_set, shdict_get = meta_shdict_get, disable_shdict = false, -- optional, default false memc_host = "127.0.0.1", memc_port = 11211, memc_timeout = 200, -- in ms memc_conn_pool_size = 5, memc_fetch_retries = 2, -- optional, default 1 memc_fetch_retry_delay = 100, -- in ms, optional, default to 100 (ms) memc_conn_max_idle_time = 10 * 1000, -- in ms, for in-pool connections,optional, default to nil memc_store_retries = 2, -- optional, default to 1 memc_store_retry_delay = 100, -- in ms, optional, default to 100 (ms) store_ttl = 1, -- in seconds, optional, default to 0 (i.e., never expires) }
Sebagian besar parameter yang diekspos dapat disederhanakan dengan "membuat handler memcached baru". Cara enkapsulasi saat ini dengan melemparkan semua parameter kepada pengguna tidak ramah pengguna, jadi saya akan menyambut baik pengembang yang tertarik untuk berkontribusi PR untuk mengoptimalkan ini.
Selain itu, dokumentasi library enkapsulasi ini menyebutkan beberapa arah optimasi lebih lanjut.
- Gunakan
lua-resty-lrucacheuntuk meningkatkan cache tingkatWorker, bukan hanya cache tingkatserverdenganshared dict. - Gunakan
ngx.timeruntuk melakukan operasi pembaruan cache secara asinkron.
Arah pertama adalah saran yang sangat baik, karena kinerja cache dalam worker lebih baik; saran kedua adalah sesuatu yang perlu Anda pertimbangkan berdasarkan skenario aktual Anda. Namun, saya umumnya tidak merekomendasikan yang kedua, tidak hanya karena ada batasan jumlah timer, tetapi juga karena jika logika pembaruan di sini salah, cache tidak akan pernah diperbarui lagi, yang memiliki dampak besar.
lua-resty-mlcache
Selanjutnya, mari kita perkenalkan enkapsulasi caching yang umum digunakan di OpenResty: lua-resty-mlcache, yang menggunakan shared dict dan lua-resty-lrucache untuk mengimplementasikan mekanisme caching multi-layer. Mari kita lihat bagaimana library ini digunakan dalam dua contoh kode berikut.
local mlcache = require "resty.mlcache" local cache, err = mlcache.new("cache_name", "cache_dict", { lru_size = 500, -- ukuran cache L1 (Lua VM) ttl = 3600, -- 1h ttl untuk hits neg_ttl = 30, -- 30s ttl untuk misses }) if not cache then error("failed to create mlcache: " .. err) end
Mari kita lihat potongan kode pertama. Awal kode ini memperkenalkan library mlcache dan mengatur parameter untuk inisialisasi. Biasanya kita akan menempatkan kode ini di fase init dan hanya perlu melakukannya sekali.
Selain dua parameter yang diperlukan, nama cache dan nama dictionary, parameter ketiga adalah dictionary dengan 12 opsi yang bersifat opsional dan menggunakan nilai default jika tidak diisi. Ini jauh lebih elegan daripada lua-resty-memcached-shdict. Jika kita merancang antarmuka sendiri, lebih baik mengadopsi pendekatan mlcache - menjaga antarmuka sesederhana mungkin sambil mempertahankan fleksibilitas yang cukup.
Berikut adalah potongan kode kedua, yang merupakan kode logika saat permintaan diproses.
local function fetch_user(id) return db:query_user(id) end local id = 123 local user , err = cache:get(id , nil , fetch_user , id) if err then ngx.log(ngx.ERR , "failed to fetch user: ", err) return end if user then print(user.id) -- 123 end
Seperti yang Anda lihat, cache multi-layer disembunyikan, jadi Anda perlu menggunakan objek mlcache untuk mengambil cache dan mengatur fungsi callback saat cache kedaluwarsa. Logika kompleks di balik ini dapat sepenuhnya disembunyikan.
Anda mungkin penasaran bagaimana library ini diimplementasikan secara internal. Selanjutnya, mari kita lihat arsitektur dan implementasi library ini. Gambar berikut adalah slide dari presentasi yang diberikan oleh Thibault Charbonnier, penulis mlcache, di OpenResty Con 2018.

Seperti yang dapat Anda lihat dari diagram, mlcache membagi data menjadi tiga lapisan, yaitu L1, L2, dan L3.
Cache L1 adalah lua-resty-lrucache, di mana setiap Worker memiliki salinannya sendiri, dan dengan N Worker, ada N salinan data, sehingga ada redundansi data. Karena mengoperasikan lrucache dalam satu Worker tidak memicu kunci, ini memiliki kinerja yang lebih tinggi dan cocok sebagai cache tingkat pertama.
Cache L2 adalah shared dict. Semua Worker berbagi satu salinan data cache dan akan mengkueri cache L2 jika cache L1 tidak mengenai. ngx.shared.DICT menyediakan API yang menggunakan spinlock untuk memastikan atomisitas operasi, jadi kita tidak perlu khawatir tentang kondisi balapan di sini.
L3 adalah kasus di mana cache L2 juga tidak mengenai, dan fungsi callback perlu dieksekusi untuk mengkueri sumber data, seperti database eksternal, dan kemudian menyimpannya ke L2. Di sini, untuk menghindari badai cache, ia menggunakan lua-resty-lock untuk memastikan hanya satu Worker yang pergi ke sumber data untuk mengambil data.
Dari perspektif permintaan:
- Pertama, ia akan mengkueri cache
L1dalamWorkerdan mengembalikan langsung jikaL1mengenai. - Jika
L1tidak mengenai atau cache gagal, ia mengkueri cacheL2antarWorker. JikaL2mengenai, ia mengembalikan dan menyimpan hasilnya diL1. - Jika
L2juga tidak mengenai atau cache tidak valid, fungsi callback dipanggil untuk mencari data dari sumber data dan menuliskannya ke cacheL2, yang merupakan fungsi dari lapisan dataL3.
Anda juga dapat melihat dari proses ini bahwa pembaruan cache dipicu secara pasif oleh permintaan endpoint. Bahkan jika permintaan gagal mengambil cache, permintaan berikutnya masih dapat memicu logika pembaruan untuk memaksimalkan keamanan cache.
Namun, meskipun mlcache telah diimplementasikan dengan sempurna, masih ada satu titik sakit - serialisasi dan deserialisasi data. Ini bukan masalah dengan mlcache, tetapi perbedaan antara lrucache dan shared dict, yang kami sebutkan berulang kali. Di lrucache, kita dapat menyimpan berbagai tipe data Lua, termasuk table; tetapi di shared dict, kita hanya dapat menyimpan string.
L1, cache lrucache, adalah lapisan data yang disentuh pengguna, dan kita ingin menyimpan semua jenis data di dalamnya, termasuk string, table, cdata, dan sebagainya. Masalahnya adalah L2 hanya dapat menyimpan string, dan ketika data dinaikkan dari L2 ke L1, kita perlu melakukan konversi dari string ke tipe data yang dapat kita berikan langsung kepada pengguna.
Untungnya, mlcache telah mempertimbangkan situasi ini dan menyediakan fungsi opsional l1_serializer dalam antarmuka new dan get, yang dirancang khusus untuk menangani pemrosesan data saat L2 dinaikkan ke L1. Kita dapat melihat kode contoh berikut, yang saya ekstrak dari set kasus uji saya.
local mlcache = require "resty.mlcache" local cache, err = mlcache.new("my_mlcache", "cache_shm", { l1_serializer = function(i) return i + 2 end, }) local function callback() return 123456 end local data = assert(cache:get("number", nil, callback)) assert(data == 123458)
Izinkan saya menjelaskannya dengan cepat. Dalam kasus ini, fungsi callback mengembalikan angka 123456; di new, fungsi l1_serializer yang kita atur akan menambahkan 2 ke angka yang masuk sebelum mengatur cache L1, yang menjadi 123458. Dengan fungsi serialisasi seperti ini, data dapat lebih fleksibel saat dikonversi antara L1 dan L2.
Ringkasan
Dengan beberapa lapisan caching, kinerja server dapat dimaksimalkan, dan banyak detail disembunyikan di antaranya. Pada titik ini, library wrapper yang stabil dan efisien menghemat banyak usaha kita. Saya juga berharap dua library wrapper yang diperkenalkan hari ini akan membantu Anda lebih memahami caching.
Terakhir, pikirkan pertanyaan ini: Apakah lapisan cache shared dictionary diperlukan? Apakah mungkin hanya menggunakan lrucache? Jangan ragu untuk meninggalkan komentar dan berbagi pendapat Anda dengan saya, dan Anda juga dipersilakan untuk membagikan artikel ini kepada lebih banyak orang untuk berkomunikasi dan berkembang bersama.