Pendahuluan
Selama penyempurnaan satu proyek, menjadi perlu untuk men-cache data yang sering diminta. Implementasi caching dimungkinkan dengan berbagai cara, tetapi saya ingin mengimplementasikannya dengan sedikit perubahan pada proyek asli. Hasilnya, pro dan kontra dijelaskan di bawah ini.
Bagaimana semuanya?
Awalnya, untuk setiap kueri yang berisi pengidentifikasi objek yang diminta, kueri dieksekusi dalam database PostgreSQL (DB). Lebih tepatnya, beberapa pertanyaan, karena untuk membentuk jawaban yang lengkap, perlu diterapkan ke beberapa tabel database. Sebagai hasil dari pemrosesan permintaan, objek yang agak rumit terbentuk, beberapa bidang diwakili oleh antarmuka. Dalam memori, objek ini menempati sekitar 250 kB.
Performa dengan implementasi ini tidak bagus, tidak lebih dari 3500 RPS (permintaan per detik) ketika meminta data yang sama dengan 1000 utas yang bersaing.
Pertanyaan segera muncul, tetapi bagaimana cara meningkatkan RPS: mengubah router, mengoptimalkan database, data cache? Router digunakan dengan cukup baik ( github.com/julienschmidt/httprouter ), dan mengganti router dalam proyek besar akan membutuhkan banyak waktu dan ada risiko tinggi akan terjadi kerusakan. Untuk mengoptimalkan pekerjaan dengan database, Anda juga perlu menulis ulang sebagian besar kode (sekarang github.com/jmoiron/sqlx digunakan). Jelas, caching adalah cara paling optimal untuk meningkatkan RPS.
Solusi sederhana
Hal paling sederhana yang terlintas dalam pikiran adalah penggunaan cache dalam memori. Saat menggunakan cache seperti itu, sekitar 20.000 RPS diperoleh. Kinerja cache dalam memori sangat baik, tetapi Anda tidak dapat menggunakan cache seperti itu dengan banyak contoh layanan. Anda tidak pernah tahu layanan dari mana permintaan akan terbang, dan mungkin ada permintaan tidak hanya untuk menerima data, tetapi juga untuk menghapus / memperbarui.
Kinerja yang diperoleh dengan cache di memori diambil sebagai standar dalam pencarian lebih lanjut untuk solusi.
Ide, ide buruk
Apakah mungkin untuk memasukkan hasil query seperti di database NoSQL Redis? Ini adalah solusi khas untuk caching permintaan respons. Data disimpan dalam memori, saat menggunakan beberapa contoh layanan, semuanya dapat menggunakan cache umum. Solusi ini cepat diimplementasikan. Dan tes menunjukkan ... Dan tes menunjukkan bahwa kinerjanya tidak meningkat banyak.
Penelitian lebih lanjut menunjukkan bahwa kerugian kinerja utama dikaitkan dengan marshaling dan unmarshaling. Mengubah struktur ke JSON dan sebaliknya membutuhkan penggunaan refleksi, yang sangat mahal dalam kinerja. Tidak mungkin untuk menolak marshaling / unmarshaling, karena itu perlu untuk mendapatkan objek penuh dari cache dengan kemampuan untuk memanggil metode struktur, dan tidak hanya mendapatkan nilai dari masing-masing bidang. Menggunakan berbagai pustaka dengan optimalisasi marshaling / unmarshaling juga tidak menyimpan, ada pertumbuhan, tetapi cache di dalam memori sangat jauh. Oleh karena itu, diputuskan untuk tidak berteman dengan "landak dan ular" dan membuat cache hybrid.
Ular dan landak hibrida
Anda tidak dapat menyebutnya hibrida penuh (lihat. Gbr.), Bahkan, ternyata cache dalam memori, tetapi dengan sinkronisasi melalui Redis (perpustakaan github.com/go-redis/redis digunakan ). Hanya pengidentifikasi unik objek yang diminta dari database (objek ID) yang akan disimpan dalam Redis. Ini akan ditambahkan ke Redis selama pemrosesan permintaan untuk membuat objek, atau permintaan untuk mendapatkan objek yang ada dari database. ID objek akan berfungsi sebagai kunci untuk nilai dalam Redis, dan nilainya akan menjadi UUID yang dihasilkan (pengidentifikasi unik universal, pengidentifikasi unik universal ”). UUID akan dihasilkan hanya ketika objek ditambahkan ke Redis. Mengapa UUID ini diperlukan akan dijelaskan nanti.
Blok diagram interaksi komponen untuk sinkronisasi cache melalui Redis
Cache dalam memori diimplementasikan berdasarkan sync.Map. Untuk item cache hybrid, TTL (waktu untuk hidup, seumur hidup) diatur, dan jika Redis membersihkan item "busuk", maka cache dalam memori dibersihkan oleh timer (time.AfterFunc). Itu melewati semua elemen cache dan memeriksa apakah elemen itu "busuk". Jika elemen cache diakses, masa pakainya diperpanjang, operasi serupa dilakukan dengan kunci di Redis.
Jadi, sekarang sesuai dengan algoritma. Jika permintaan datang dan kami perlu mengambil objek, urutan tindakan berikut dilakukan:
- Kami melihat apakah ada objek dengan ID-objek yang diberikan di Redis, jika demikian, maka kita dapat mengambil cache instance layanan dari dalam memori:
- Jika objek tidak ada dalam cache di-memori, maka kita mengambilnya dari database dan menambahkan cache dengan UUID dari Redis ke cache di-memori dan memperbarui TTL dari kunci di Redis.
- Jika objek berada di cache di memori, maka kami mengambilnya dari cache, memeriksa apakah UUID di cache dan di Redis cocok, dan jika demikian, kemudian perbarui TTL di cache dan di Redis. Jika UUID tidak cocok, maka hapus objek dari cache di memori, ambil dari database, tambahkan cache dengan UUID dari Redis ke dalam memori.
- Jika objek tidak ada di Redis, maka jika objek berada di cache, hapus dari cache. Ambil objek dari database dan tambahkan ke cache dan ke Redis. Untuk menghilangkan situasi ketika memperbarui / menghapus entri lebih cepat daripada menambahkan ke cache ( andreyverbin comment ), tambahkan objek dengan UUID nol ke cache. Kemudian pada akses pertama ke cache, perbedaan dalam UUID dengan Redis akan terungkap, dan data dari database akan diminta lagi.
Jika permintaan untuk menghapus suatu objek tiba, itu segera dihapus dari database, dan kemudian operasi cache:
- Hapus objek di Redis.
- Hapus objek dalam cache di memori.
Sekarang, jika permintaan serupa tiba di instance lain dari layanan, maka meskipun objek masih dalam cache di-memori, itu tidak akan digunakan.
Pembaruan objek, setelah memperbarui dalam database:
- Hapus objek di Redis.
- Hapus objek dalam cache di memori.
Ketika Anda meminta objek dalam instance lain dari layanan, itu akan terungkap bahwa itu bukan di Redis, jadi Anda perlu mengambilnya dari database. Jika ada contoh lain dari layanan, dan permintaan terbang ke sana setelah memperbarui objek dan setelah menambahkannya dengan contoh kedua di Redis, maka, ketika memeriksa UUID, perbedaan akan terungkap, dan contoh ketiga dari layanan juga akan mengambil objek dari database.
Yaitu pada kenyataannya, dalam situasi apa pun yang tidak dapat dipahami, kami percaya bahwa cache kami tidak benar, dan kami perlu mengambil data dari database.
Kesimpulan
Solusi yang dikembangkan memiliki pro dan kontra.
Pro
- Skema caching yang dikembangkan memungkinkan untuk mencapai sekitar 19000 RPS, yang hampir setara dengan tes dengan cache dalam memori.
- Kode proyek asli memiliki jumlah minimum perubahan.
Cons
- Jika Redis crash, layanan secara drastis menurun dalam kinerja dan bersandar pada bekerja dengan database.
- Setiap instance dari layanan akan membutuhkan lebih banyak memori karena memiliki cache di-memori sendiri.
Karena kinerja tinggi lebih penting, saya tidak menganggap minus menjadi penting. Di masa depan, ada ide untuk menulis perpustakaan untuk menyederhanakan implementasi cache hybrid, karena ada kebutuhan untuk menggunakan caching serupa di proyek lain.