Önbellek (Cache), kullanıcı isteklerine daha hızlı cevap verebilmek, uygulama performansını arttırmak, oluşabilecek dar boğazları önlemek ve sistemlere gelen yükü minimuma indirmek gibi amaçlarla ihtiyaç duyulan verilerin geçici olarak depolandığı, hızlı erişim sağlanabilen yapılardır.
1. Dağıtık Önbellekleme (Distributed Caching) İhtiyacı
Geliştirmekte olduğumuz uygulama, birçok farklı modüle sahiptir. Bu modüllerin her biri, kendi veritabanını kullanmakta ve istenildiğinde her bir modülün backend servisleri ayrı deploy edilebilmektedir. Buradaki yaklaşım mikroservis ve mikro veritabanı gibi düşünülebilir.
Dağıtık bir uygulama söz konusu olduğunda, servisler aralarında haberleşmek için Event/Message Bus, Rest/RPC call gibi farklı araç ve teknolojiler kullanılmaktadır. Dağıtık bir uygulama, monolit yapıya göre daha fazla ağ trafiği oluşturmakta ve uygulama performansı (throughput) bundan olumsuz etkilenmektedir. Uygulamamızda servislerin cevap süresini arttırabilmek, servisler arası iletişim ihtiyacını minimuma indirmek ve en önemlisi veritabanları üzerindeki yükü azaltmak için dağıtık önbellekleme mimarisi kurguladık.
2. Önbellekleme Çözümü Olarak Redis
Redis, açık kaynak, in-memory çalışan key-value bir NoSQL veritabanıdır. Redis; yük altında yüksek erişilebilirlik ve performans sağlayabilmektedir, bu sebeple önbellekleme için güçlü bir alternatiftir. Projenize ve ihtiyacınıza göre alternatif Memcached ve Hazelcast ürünlerine göz atabilirsiniz. Bizim Redis’i tercih etmemizdeki başlıca faktörler şunlardır:
- Ürün hakkında ki know-how.
- Pub/Sub yeteneği.
- Sharding, persistence ve replication desteği.
Memcached, Hazelcast ve Redis arasında, detaylı bir karşılaştırma için buraya göz atabilirsiniz.
3. Mimari
Üç temel önbellekleme yöntemi kurguladık;
- In-Memory Caching (Main Memory) — Server Side
- Distributed Caching (Redis) — Server Side
- Browser Client Caching (IndexedDB) — Client Side
Bu üçü dışındaki CDN, API Gateway, LB gibi network yapıları üzerindeki önbelleklemeler, burada anlatacağımız mimarinin dışında tutulmuştur.
Dağıtık önbellek yapımızı Redis üzerine inşa ettik. Redis Pub/Sub yapısını kullanarak, in-memory tutulan önbellek verileri arasındaki senkronizasyonu sağladık. ReactJS ile geliştirilen web uygulaması için ise IndexedDB kullanarak istemci üzerinde önbellekleme yapısı geliştirdik. İstemci tarafındaki önbelleği temizleyebilmek (invalidation) için SignalR üzerinden gerçek zamanlı event akışı sağladık.
Önbellekleme ve önbellek temizleme için LRU ve LFU (Least Frequently Used) algoritmik yaklaşımlarını hibrit olarak kullanmaktayız (servislerin ihtiyaçları doğrultusunda değişiklik yapılabilmektedir). Bu algoritmalar, önbellek değiştirme politikaları (cache replacement policies) olarak adlandırılır.
LRU, önbelleğe yeni bir ekleme yapılacağı zaman önbellekteki en eski elemanın ön bellekten silinmesini amaçlayan hızlı bir algoritmadır. LFU, önbelleğin kapasite sınırına her ulaşıldığında, önbellekteki en az kullanılan verinin kaldırıldığı, LRU algoritmasına göre daha efektif fakat görece daha yavaş bir algoritmadır.
3.1 In-Memory Önbellekleme
En hızlı ve en kolay önbellekleme yöntemlerinden birisidir. Bu yapıda uygulamalar, üzerinde çalıştığı sunucunun kullanılabilir RAM miktarının bir bölümünü, önbellekleme için geçici depolama alanı olarak rezerve eder. Uygulama, sık erişmek istediği verileri RAM üzerindeki rezerve edilen bu alanda tutar. RAM, yapısı itibari ile yüksek performans ve erişebilirlik sağlasada depolama kapasitesi olarak sınırlıdır.
Yukarıdaki görselde, backend servisleri üzerindeki in-memory önbellekleme akışı gösterilmektedir.
- (1, 2) Kullanıcı bir servis isteği yapar.
- (3) Backend servisi, istenilen verinin RAM’de olup olmadığını kontrol eder.
- (4) Veri, önbellekte yoksa ilgili veritabanından getirilir ve önbellekte depolanır.
- İlgili servis, aynı veriye ihtiyaç duyduğunda önbellekten kullanır.
Depolama esnasında veri Key Value olarak saklanır. Bizim için Key, kullanıcının yaptığı servis isteğinin body’sinin hash’i veya geliştirici tarafından belirli bir şablona göre oluşturulan veridir. Value ise, veritabanındaki ihtiyaç duyulan verinin kendisidir.
In-Memory önbellekleme implementasyonu için açık kaynak EasyCache projesinin in-memory paketine göz atabilirsiniz.
3.2 Dağıtık Önbellekleme
In-Memory önbellekleme her ne kadar hızlı ve kolay olsa da, dağıtık uygulamalar için tek başına yeterli değildir. Dağıtık yapıda, sunucular arası yüksek erişilebilirlik ve hız sağlayabilen, bunun yanı sıra önbellekleme için daha büyük kapasitesi olan, işlem gücü ve uygulama sunucuları arasındaki senkronizasyonu yapabilen merkezi bir önbellek çözümüne ihtiyaç vardır.
Biz, dağıtık önbellekleme ve önbellek temizleme mimarimizi Redis üzerine inşaa ettik. Backend servislerimizin büyük çoğunluğu .Net Core ile geliştirildi. Redis istemci olarak StackExchange.Redis paketini kullandık. StackExchange.Redis gerek performans gerekse connection yönetiminde sağladığı avantajlar ve herhangi bir bağlantı limiti olmaması sebebiyle alternatifi olan ServiceStack.Redis gibi kütüphanelere göre daha avantajlıdır.
Yukarıdaki görselde, dağıtık önbellekleme mimarimiz gösterilmektedir. Backend servisleri, bir veriye ihtiyaç duyduğunda veritabanına gitmeden önce in-memory önbelleğe baktığından bahsetmiştik. Sunucunun önbelleğinde ilgili verinin olmaması durumunda istek doğrudan veritabanına gönderilmez. Verinin dağıtık önbellekte yani Redis’te olup olmadığı kontrol edilir. Bu süreç aşağıdaki şekilde işlemektedir.
- (4) Gelen kullanıcı isteğinden bir Key oluşturulur ve sunucu önbelleğinde verinin varlığı kontrol edilir.
- (5) Redis üzerinde bu Key’in karşılığı olup olmadığı kontrol edilir.
- Redis üzerinde olması durumunda bu veri getirilir ve servis isteği bu veri kullanılarak döndürülür.
- (3) Redis üzerinde veri yoksa ilgili veri tabanı sunucusundan getirilir.
- (4) Veritabanından getirilen veri asenkron şekilde Redis’e ve RAM’e in-memory olarak depolanır.
3.3 İstemci Üzerinde Önbellekleme
Servisler üzerinde önbellekleme olsa da tek başına yeterli değildir. Kullanıcı deneyimini iyileştirmek, veri yüklenme süresini azaltmak ve sayfa açılışlarını hızlandırmak amacıyla kritik olmayan veriler istemci üzerinde depolanabilirler.
Kullanıcı web uygulamalarımızı ilk açtığında, profil bilgisi, konfigürasyon tanımları, internationalization(i18n), localization (l10n) gibi birçok veri yüklemektedir. Kullanıcıların intranet de dahi olsa bile bağlantı hızına bağlı olarak bir süre beklemesi kaçınılmazdır. Bu beklemenin önüne geçmek ve istemci tarafındaki servis isteklerini en aza indirmek için tarayıcı üzerinde, IndexedDB ile önbellekleme yapısı kurguladık bu sayede servisler üzerine gelen yükü azaltmayı başardık.
Yukarıdaki görselde istemci tarafındaki önbellekleme yapısı yer almaktadır. İstemci tarafından yapılan bir servis isteğinin cevabı, server side önbellekten (In-Memory, Redis) karşılandıysa, response içerisinde önbellekten getirdiğini belirten bir property (örneğin; isCachable) ekler. ReactJS uygulaması, bu property sayesinde hangi veriyi tarayıcı üzerinde önbelleklemesi gerektiğini bilmektedir.
- (1) Kullanıcı bir API isteği yapar.
- (2) Servis isteğinden bir KEY oluşturulur ve IndexDB’de bu verinin varlığı kontrol edilir.
- (3) Uygulama, verinin IndexedDB olmaması durumunda Rest request yapar.
- Backend servisi bu veriyi önbelleğine aldıysa response içerisine, flag ve versiyon bilgisi ekler.
- İstemci uygulama, servis response ile gelen flag’i kontrol eder ve önbelleğe alınması gerekiyorsa KEY oluşturarak bu veriyi IndexedDB’de depolar.
4 Önbellek Temizleme (Cache Invalidation)
“There are only two hard things in Computer Science: cache invalidation and naming things.”
Önbellek temizleme, önbellekte tutulan verinin değişmesi gerektiği durumda, kaynak ile önbellek arasındaki veri tutarlılığını sağlayabilmek için önbelleğin geçersiz kılınması işlemidir.
Bu bölümde server side ve client side olarak adlandırdığımız önbelleklerin temizlenmesi sürecine değineceğiz.
Tek bir uygulama sunucusu (instance) olduğunda, RAM’deki önbelleği temizlemek uygulama için kolay bir işlemdir, Key kullanarak verinin ana bellekten silinmesi yeterli olacaktır. Uygulamanın, birden fazla sunucu üzerinde çalışması durumunda (distributed application), sunucuların önbelleklerini koordineli bir şekilde temizlemek, silme işleminden diğer servislerinde haberdar olmasını sağlamak karmaşık bir iştir. Biz, uygulama sunucuları arasındaki önbellek senkronizasyonu için Redis’in Pub/Sub yeteneğinden faydalandık. Redis, Pub/Sub için kanal (channel) yapısını kullanır, bu sayede bir invalidation işlemi Redis üzerinden tüm uygulamalara iletilir.
Yukarıdaki görselde, Payment servisinin bir numaralı instance’ı önbelleğini temizlemektedir, aynı uygulamanın çalıştığı diğer instance bu verinin temizlendiğinden haberdar olmalıdır. Servislerden herhangi birisi, önbellekteki (In-Memory veya Redis) veriyi kaldırmak veya değiştirmek istediğini durumda akış, aşağıdaki gibidir.
- (1) Payment servisi (Instance 1), çalıştığı sunucudaki önbelleği temizler.
- (2) Payment servisi (Instance 1), dağıtık önbelleği temizlemek için Redis’e DEL komutu gönderir.
- (3) Redis üzerinde veri silinir ve gerçekleştirilen Del komutu, KeySpace Notification kanalı üzerinden tüm servislere event olarak gönderilir (Publisher).
- (4) Tüm servisler KeySpaceNotification kanalını dinler (subscriber) ve DEL, SET eventlerini işler ve önbelleklerini günceller. Event’ler değişen Key’i ve işlemi (SET, DEL) içerirler.
- (5) BFF servis, istemci uygulaması ile arasındaki SignalR Hub üzerinden bir event gönderir. Bu event içerisinde istemci tarafında silinmesi veya güncellenmesi gereken key listesi mevcuttur.
- (6) İstemci, önbelleğini temizler.
Yukarıdaki akış, istemci uygulamasının açık olduğu durumda başarıyla çalışmaktadır fakat önbellek temizlenmesi sürecinde kullanıcı online değilse, websocket bağlantısı olmadığı için event’leri yakalayamayacak ve tarayıcısında eski veriler durmaya devam edecektir. Bu sorunu sormak için offline önbellek versiyon doğrulama işlemi yapılmaktadır.
Kullanıcı ilk oturumunu açtığında, IndexedDB’de bulunan tüm Key’leri ve Value içerisinde bulunan versiyon bilgilerini asenkron olarak BFF servisine gönderir. Servis, dağıtık önbellekte (Redis) tutulan Key’ler ile karşılaştır, burada olmayan Key’leri ve versiyon bilgisi eşleşmeyen Key’leri istemciye döndürür. İstemci uygulama, bu Key listesini kullanarak temizlenmesi gereken verileri IndexedDB’den kaldırılır.