Alan Düzeyinde Rails Önbellek Geçersizleştirme: Bir DSL Çözümü
Yayınlanan: 2022-03-11Modern web geliştirmede önbelleğe alma, işleri hızlandırmanın hızlı ve güçlü bir yoludur. Doğru yapıldığında önbelleğe alma, uygulamanızın genel performansında önemli iyileştirmeler sağlayabilir. Yanlış yapıldığında, kesinlikle felaketle sonuçlanacak.
Önbellek geçersiz kılma, bildiğiniz gibi, bilgisayar bilimindeki en zor üç sorundan biridir; diğer ikisi, bir şeyleri adlandırma ve tek tek hatalardır. Kolay bir çıkış yolu, bir şey değiştiğinde, sol ve sağ her şeyi geçersiz kılmaktır. Ancak bu, önbelleğe alma amacını yener. Önbelleği yalnızca kesinlikle gerekli olduğunda geçersiz kılmak istiyorsunuz.
Önbelleğe alma işleminden en iyi şekilde yararlanmak istiyorsanız, neyi geçersiz kıldığınız konusunda çok dikkatli olmanız ve uygulamanızı tekrarlanan işlerde değerli kaynakları boşa harcamaktan kurtarmanız gerekir.
Bu blog gönderisinde, Rails önbelleklerinin nasıl davrandığı üzerinde daha iyi kontrol sahibi olmak için bir teknik öğreneceksiniz: özellikle, alan düzeyinde önbellek geçersiz kılma uygulamak. Bu teknik, touch
yöntemi davranışının manipülasyonunun yanı sıra Rails ActiveRecord ve ActiveSupport::Concern
dayanır.
Bu blog yazısı, alan düzeyinde önbellek geçersiz kılmayı uyguladıktan sonra performansta önemli bir gelişme gördüğümüz bir projedeki son deneyimlerime dayanmaktadır. Gereksiz önbellek geçersiz kılmalarının ve şablonların tekrar tekrar oluşturulmasının azaltılmasına yardımcı oldu.
Raylar, Ruby ve Performans
Ruby en hızlı dil değildir, ancak genel olarak geliştirme hızı söz konusu olduğunda uygun bir seçenektir. Ayrıca, metaprogramlama ve yerleşik alana özgü dil (DSL) yetenekleri, geliştiriciye muazzam bir esneklik sağlar.
Jakob Nielsen'in çalışması gibi, bir görev 10 saniyeden fazla sürerse odağımızı kaybedeceğimizi gösteren çalışmalar var. Ve odağımızı yeniden kazanmak zaman alır. Yani bu beklenmedik bir şekilde maliyetli olabilir.
Ne yazık ki, Ruby on Rails'de şablon oluşturma ile bu 10 saniyelik eşiği aşmak çok kolaydır. Bunun herhangi bir "merhaba dünya" uygulamasında veya küçük ölçekli evcil hayvan projesinde olduğunu görmeyeceksiniz, ancak birçok şeyin tek bir sayfaya yüklendiği gerçek dünya projelerinde, inanın bana, şablon oluşturma çok kolay sürüklenmeye başlayabilir.
Ve projemde tam olarak çözmem gereken şey buydu.
Basit Optimizasyonlar
Ama işleri tam olarak nasıl hızlandırırsınız?
Cevap: Kıyaslayın ve optimize edin.
Projemde optimizasyonda çok etkili iki adım şunlardı:
- N+1 sorgularını ortadan kaldırma
- Şablonlar için iyi bir önbelleğe alma tekniğiyle tanışın
N+1 Sorguları
N+1 sorgularını düzeltmek kolaydır. Yapabileceğiniz şey, günlük dosyalarınızı kontrol etmektir; günlüklerinizde aşağıdakiler gibi birden fazla SQL sorgusu gördüğünüzde, bunları istekli yükleme ile değiştirerek ortadan kaldırın:
Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ? Learning Load (0.3ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ? Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ?
Bu verimsizliği tespit etmeye yardımcı olmak için mermi adı verilen bir mücevher var. Ayrıca kullanım durumlarının her birini gözden geçirebilir ve bu arada günlükleri yukarıdaki modele göre inceleyerek kontrol edebilirsiniz. Tüm N+1 verimsizliklerini ortadan kaldırarak, veritabanınızı aşırı yüklemeyeceğinizden ve ActiveRecord'da geçirdiğiniz sürenin önemli ölçüde azalacağından yeterince emin olabilirsiniz.
Bu değişiklikleri yaptıktan sonra projem zaten daha hızlı çalışıyordu. Ama onu bir sonraki seviyeye taşımaya karar verdim ve bu yükleme süresini daha da azaltabilecek miyim diye baktım. Şablonlarda hala oldukça fazla gereksiz işleme oluyordu ve nihayetinde, parça önbelleğe almanın yardımcı olduğu yer burasıydı.
Parça Önbelleğe Alma
Parça önbelleğe alma, genellikle şablon oluşturma süresini önemli ölçüde azaltmaya yardımcı olur. Ancak varsayılan Rails önbellek davranışı, projem için onu kesmiyordu.
Rails parça önbelleğe almanın ardındaki fikir harika. Süper basit ve etkili bir önbelleğe alma mekanizması sağlar.
Ruby On Rails'in yazarları, parça önbelleğe almanın nasıl çalıştığı hakkında Signal v. Noise'da çok iyi bir makale yazmışlardır.
Diyelim ki bir varlığın bazı alanlarını gösteren bir miktar kullanıcı arayüzünüz var.
- Sayfa yüklendiğinde, Rails varlığın sınıfına ve
updated_at
alanına dayalı olarakcache_key
hesaplar. - Bu
cache_key
kullanarak, o anahtarla ilişkili önbellekte herhangi bir şey olup olmadığını kontrol eder. - Önbellekte hiçbir şey yoksa, o parçanın HTML kodu görünüm için oluşturulur (ve yeni oluşturulan içerik önbellekte depolanır).
- Önbellekte bu anahtarla mevcut herhangi bir içerik varsa, görünüm önbelleğin içeriğiyle oluşturulur.
Bu, önbelleğin hiçbir zaman açıkça geçersiz kılınmasına gerek olmadığı anlamına gelir. Varlığı değiştirip sayfayı yeniden yüklediğimizde, varlık için yeni önbellek içeriği oluşturulur.
Rails, varsayılan olarak, alt öğenin değişmesi durumunda ana varlıkların önbelleğini geçersiz kılma yeteneği de sunar:
belongs_to :parent_entity, touch: true
Bu, bir modele dahil edildiğinde, çocuğa dokunulduğunda ebeveyne otomatik olarak dokunacaktır . touch
hakkında daha fazla bilgiyi buradan edinebilirsiniz. Bununla, Rails bize, alt varlıkların önbelleğiyle aynı anda üst varlıklarımız için önbelleği geçersiz kılmanın basit ve etkili bir yolunu sunar.
Rails'de Önbelleğe Alma
Ancak, Rails'de önbelleğe alma, üst öğeyi temsil eden HTML parçasının yalnızca üst öğenin alt varlıklarını temsil eden HTML parçalarını içerdiği kullanıcı arabirimlerine hizmet etmek için oluşturulur. Başka bir deyişle, bu paradigmada alt varlıkları temsil eden HTML parçası, ana varlıktan alanlar içeremez.
Ama gerçek dünyada böyle olmuyor. Rails uygulamanızda bu koşulu ihlal eden şeyler yapmanız gerekebilir.
Kullanıcı arabiriminin, alt varlığı temsil eden HTML parçasının içindeki bir ana varlığın alanlarını gösterdiği bir durumu nasıl ele alırsınız?
Alt öğe, ana varlıktan alanlar içeriyorsa, Rails'in varsayılan önbellek geçersiz kılma davranışıyla başınız belada demektir.

Ana varlıktan sunulan bu alanlar her değiştirildiğinde, o ebeveyne ait tüm alt varlıklara dokunmanız gerekecektir. Örneğin, Parent1
değiştirilirse, hem Child1
hem de Child2
görünümleri için önbelleğin geçersiz kılındığından emin olmanız gerekir.
Açıkçası, bu büyük bir performans darboğazına neden olabilir. Bir ebeveyn değiştiğinde her alt varlığa dokunmak, iyi bir sebep olmadan çok sayıda veritabanı sorgusuna neden olur.
Başka bir benzer senaryo, has_and_belongs_to
ilişkilendirmesiyle ilişkili varlıkların listede sunulması ve bu varlıkların değiştirilmesinin ilişkilendirme zinciri aracılığıyla bir önbellek geçersiz kılma kademesini başlatmasıdır.
class Event < ActiveRecord::Base has_many :participants has_many :users, through: :participants end class Participant < ActiveRecord::Base belongs_to :event belongs_to :user end class User < ActiveRecord::Base has_many :participants has_many :events, through :participants end
Bu nedenle, yukarıdaki kullanıcı arayüzü için, kullanıcının konumu değiştiğinde katılımcıya veya etkinliğe dokunmak mantıksız olacaktır. Ama kullanıcı adı değiştiğinde hem etkinliğe hem de katılımcıya dokunmalıyız değil mi?
Dolayısıyla Signal v. Noise makalesindeki teknikler, yukarıda açıklandığı gibi belirli UI/UX örnekleri için yetersizdir.
Rails basit şeyler için süper etkili olsa da, gerçek projelerin kendi komplikasyonları vardır.
Alan Düzeyinde Raylar Önbellek Geçersizleştirme
Projelerimde, yukarıdaki gibi durumları ele almak için küçük bir Ruby DSL kullanıyorum. İlişkilendirmeler aracılığıyla önbellek geçersizliğini tetikleyecek alanları bildirimsel olarak belirlemenizi sağlar.
Bunun gerçekten yardımcı olduğu birkaç örneğe bir göz atalım:
Örnek 1:
class Event < ActiveRecord::Base include Touchable ... has_many :tasks ... touch :tasks, in_case_of_modified_fields: [:name] ... end class Task < ActiveRecord::Base belongs_to :event end
Bu pasaj, Ruby'nin metaprogramlama yeteneklerinden ve dahili DSL yeteneklerinden yararlanır.
Daha açık olmak gerekirse, yalnızca olaydaki bir ad değişikliği, ilgili görevlerinin parça önbelleğini geçersiz kılacaktır. Etkinliğin amaç veya konum gibi diğer alanlarını değiştirmek, görevin parça önbelleğini geçersiz kılmaz. Bu alan düzeyindeki ince taneli önbellek geçersiz kılma denetimi olarak adlandırırdım.
Örnek 2:
has_many
ilişkilendirme zinciri aracılığıyla önbellek geçersizliğini gösteren bir örneğe bakalım.
Aşağıda gösterilen kullanıcı arabirimi parçası, bir görevi ve sahibini gösterir:
Bu kullanıcı arabirimi için, görevi temsil eden HTML parçası, yalnızca görev değiştiğinde veya sahibinin adı değiştiğinde geçersiz kılınmalıdır. Sahibin diğer tüm alanları (saat dilimi veya tercihler gibi) değişirse, görev önbelleği olduğu gibi bırakılmalıdır.
Bu, burada gösterilen DSL kullanılarak elde edilir:
class User < ActiveRecord::Base include Touchable touch :tasks, in_case_of_modified_fields: [:first_name, :last_name] ... end class Task < ActiveRecord::Base has_one owner, class_name: :User end
DSL'nin Uygulanması
DSL'nin ana özü touch
yöntemidir. İlk argümanı bir ilişkilendirmedir ve sonraki argüman, bu ilişkiye touch
tetikleyen alanların bir listesidir:
touch :tasks, in_case_of_modified_fields: [:first_name, :last_name]
Bu yöntem Touchable
modülü tarafından sağlanır:
module Touchable extend ActiveSupport::Concern included do before_save :check_touchable_entities after_save :touch_marked_entities end module ClassMethods def touch association, options @touchable_associations ||= {} @touchable_associations[association] = options end end end
Bu kodda ana nokta, touch
çağrının argümanlarını saklamamızdır. Ardından, varlığı kaydetmeden önce, belirtilen alan değiştirilmişse ilişkilendirmeyi kirli olarak işaretliyoruz. Dernek kirliyse kaydettikten sonra o dernekteki varlıklara dokunuyoruz.
O zaman, endişenin özel kısmı:
... private def klass_level_meta_info self.class.instance_variable_get('@touchable_associations') end def meta_info @meta_info ||= {} end def check_touchable_entities return unless klass_level_meta_info.present? klass_level_meta_info.each_pair do |association, change_triggering_fields| if any_of_the_declared_field_changed?(change_triggering_fields) meta_info[association] = true end end end def any_of_the_declared_field_changed?(options) (options[:in_case_of_modified_fields] & changes.keys.map{|x|x.to_sym}).present? end …
check_touchable_entities
yönteminde, belirtilen alanın değişip değişmediğini kontrol ederiz. Öyleyse, meta_info[association]
true
olarak ayarlayarak ilişkilendirmeyi kirli olarak işaretleriz.
Ardından varlığı kaydettikten sonra kirli ilişkilerimizi kontrol ediyor ve gerekirse içindeki varlıklara dokunuyoruz:
… def touch_marked_entities return unless klass_level_meta_info.present? klass_level_meta_info.each_key do |association_key| if meta_info[association_key] association = send(association_key) association.update_all(updated_at: Time.zone.now) meta_info[association_key] = false end end end …
Ve işte bu! Artık basit bir DSL ile Rails'de alan düzeyinde önbellek geçersiz kılma işlemi gerçekleştirebilirsiniz.
Çözüm
Rails önbelleğe alma, uygulamanızda nispeten kolaylıkla performans iyileştirmeleri vaat ediyor. Ancak, gerçek dünyadaki uygulamalar karmaşık olabilir ve çoğu zaman benzersiz zorluklar doğurur. Varsayılan Rails önbellek davranışı çoğu senaryo için iyi çalışır, ancak önbellek geçersiz kılmada biraz daha fazla optimizasyonun uzun bir yol kat edebileceği bazı senaryolar vardır.
Artık Rails'de alan düzeyinde önbellek geçersiz kılmayı nasıl uygulayacağınızı bildiğinize göre, uygulamanızda gereksiz önbellek geçersiz kılmalarını önleyebilirsiniz.