İçeriğe geç
academia.sh

Ders 11 / 13

Önbellekleme

Bir parçanın bir kez üretilip sonraki isteklere sunulmasının, sorgu önbelleğinden farkının ve anahtar değişmediğinde içeriğin bayatlamasının ölçümü.

Yayın Güncelleme

Bu derste

  • Parça önbelleğini önbellek anahtarıyla anlatır ve aynı anahtarla bloğun bir kez koştuğunu ölçer
  • Parça önbelleğinin kalışını sorgu önbelleğinin kalışıyla karşılaştırır
  • Anahtar değişmedikçe içeriğin bayatladığını gösterir
  • Anahtara bir sürüm katarak geçersiz kılmayı ölçer ve eski girdinin bedelini söyler
İçindekiler

Önceki ders, kuyruğa konan bir işin kaydının kalıcı olduğunu ölçtü; kayıt, işi işleyen işçi gelene kadar bekliyordu. Bu derste kalıcı kılınan şey bir kayıt değil, bir hesabın sonucudur. Bir ürün listesinin bir satırını her istekte yeniden çizmek yerine, bir kez çizip sonucunu saklamak ve sonraki isteklere onu sunmak mümkündür. Soru şu: saklanan sonuç hangi isteğe kadar geçerli, ve ne zaman geçerli olmaktan çıkar?

Önbellek (cache), pahalı bir hesabın sonucunu, aynı hesap yeniden istendiğinde tekrar yapmamak için saklayan depodur. Parça önbelleği, bir görünümün bir parçasının, örneğin tek bir liste satırının çizilmiş çıktısını saklar. Hangi sonucun hangi hesaba ait olduğunu önbellek anahtarı belirler: aynı anahtarla gelen ikinci istek hesabı yapmaz, saklanan sonucu alır. M11/K03’te önbelleğin ortak bir depo olduğu görülmüştü; burada kimin gördüğünü sormuyoruz, ne kadar kaldığını soruyoruz. Ölçü ekseni kursun ekseni: bu değer hangi isteğe kadar kalıyor? Dört kalış: bu istek, sonraki istek, oturum, kalıcı.

Çekirdek

Ölçümler aynı küçük istek döngüsüne dayanır. Dosya kursun çekirdeğidir ve olduğu gibi verilir. Önbellek bu çekirdekte Uygulama#depo içindeki bir karma yapıdır; gerçek bir uygulamada önbellek ayrı bir depodur ve çekirdek onun davranışını taşır, kendisini taşımaz.

# kalis.rb — kucuk bir istek dongusu ve bir degerin hangi istege kadar kaldigini siniflandiran yardimci

# Flash: bu istekte yazilan deger yalniz bir sonraki istege tasinir.
class Flash
  def initialize(gelen)
    @gelen = gelen
    @yeni = {}
  end

  def [](anahtar) = @yeni.fetch(anahtar) { @gelen[anahtar] }

  def []=(anahtar, deger)
    @yeni[anahtar] = deger
  end

  def birakilan = @yeni
end

# Her istek yeni bir baglam alir: params, flash ve istek deposu her istekte bostur.
class Baglam
  attr_reader :uygulama, :params, :oturum, :flash, :istek_deposu

  def initialize(uygulama, oturum)
    @uygulama = uygulama
    @params = {}
    @oturum = oturum
    @flash = Flash.new(oturum.delete(:flash) || {})
    @istek_deposu = {}
  end
end

# Uygulamanin deposu butun isteklerde ve butun istemcilerde aynidir;
# oturum istemciye gore secilir.
class Uygulama
  attr_reader :depo

  def initialize
    @oturumlar = Hash.new { |h, istemci| h[istemci] = {} }
    @depo = { urunler: {}, sema: { urunler: %i[ad fiyat] }, sablonlar: {},
              kuyruk: [], onbellek: {}, sayac: 0 }
  end

  def istek(istemci)
    baglam = Baglam.new(self, @oturumlar[istemci])
    sonuc = yield baglam
    baglam.oturum[:flash] = baglam.flash.birakilan unless baglam.flash.birakilan.empty?
    sonuc
  end
end

# Kural: istek 1'de (istemci A) deger yazilir; istek 2 ve 3 (A) ile istek 4 (B) okur.
# B goruyorsa kalici; A'nin ucuncu istegi goruyorsa oturum; yalniz ikinci istek
# goruyorsa sonraki; hicbiri gormuyorsa bu_istek.
def kalis(yaz, oku)
  uygulama = Uygulama.new
  ilk = uygulama.istek("A") { |b| yaz.call(b); oku.call(b) }
  raise ArgumentError, "deger yazildigi istekte okunamadi" if ilk.nil?

  gorulen = [["A", 2], ["A", 3], ["B", 4]].map do |istemci, sira|
    uygulama.istek(istemci) { |b| oku.call(b) }.nil? ? "--" : "#{istemci}#{sira}"
  end
  tur = if gorulen[2] != "--" then "kalici"
        elsif gorulen[1] != "--" then "oturum"
        elsif gorulen[0] != "--" then "sonraki"
        else "bu_istek"
        end
  [tur, ["A1", *gorulen].join(" "), ilk]
end

Bu blok kaynaktır; dosya diske yazılır ve require_relative ile çekilir. Çekirdek bir çerçeve değil, bir çerçevenin modelidir. Kural değerin davranışını okur, saklandığı yeri okumaz; ölçümler tek bir uygulama süreci içinde anlamlıdır.

Parça Önbelleği

Önbellekleme yardımcısı tek satırdır: anahtar önbellekte varsa saklanan değeri döndürür, yoksa bloğu koşturur, sonucu anahtarın altına koyar ve döndürür. Dersin kesiti bunu, ölçümde kullanılacak iki küçük yardımcıyla birlikte verir. Urun modülü bir önceki dersin kısa bir kopyasıdır ve sorgu önbelleğiyle karşılaştırma için gerekir.

# onbellek.rb — onbellekle yardimcisi, parca ureten yardimci ve karsilastirma icin Urun
require_relative "kalis"

def onbellekle(b, anahtar) = b.uygulama.depo[:onbellek][anahtar] ||= yield

def kacisla(metin) = metin.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")

def parca(b, urun)
  onbellekle(b, "urun/#{urun[:id]}/#{urun[:surum]}") { "<li>#{kacisla(urun[:ad])}</li>" }
end

module Urun
  def self.olustur(b, nitelikler)
    hatalar = []
    hatalar << "ad bos olamaz" if nitelikler[:ad].to_s.empty?
    hatalar << "fiyat negatif olamaz" if nitelikler[:fiyat].to_i.negative?
    return { hatalar: } unless hatalar.empty?

    tablo = b.uygulama.depo[:urunler]
    id = tablo.size + 1
    tablo[id] = nitelikler.merge(id:)
    { id:, hatalar: }
  end

  def self.bul(b, id)
    sorgular = (b.istek_deposu[:sorgular] ||= {})
    sorgular.fetch([:urunler, id]) do
      b.istek_deposu[:sorgu_sayisi] = b.istek_deposu.fetch(:sorgu_sayisi, 0) + 1
      sorgular[[:urunler, id]] = b.uygulama.depo[:urunler][id]
    end
  end
end

def goster(ad, tur, kanit, deger)
  printf("%-14s%-14s%-13s%s\n", ad, tur, kanit, deger.inspect)
end

Bu blok da kaynaktır. onbellekle anahtarı ||= ile yazar: değer nil ya da false ise yeniden hesaplanır, ama parça çıktıları bir dizge olduğu için bu ölçümlerde sorun çıkarmaz. Birinci gözlem, önbelleğe yazılan parçanın kendisidir.

# parca.rb — parca onbellegi hangi istege kadar kaliyor
require_relative "onbellek"

yaz = ->(b) { onbellekle(b, "urun/1") { "<li>kalem</li>" } }
oku = ->(b) { b.uygulama.depo[:onbellek]["urun/1"] }
goster("parca", *kalis(yaz, oku))
parca         kalici        A1 A2 A3 B4  "<li>kalem</li>"

Parça dört isteğin dördünde de göründü; B’nin isteği de dahil. Bu kalıcı kalıştır ve önbelleğin amacı da budur: bir istemcinin ürettiği parçayı başka bir istemcinin isteği de kullanır. Çekirdekteki model bunu açıkça gösterir. UY7. Çekirdekte parça süreç belleğindedir ve hiçbir yaşam süresi yoktur. Gerçek bir önbellek girdilere bir yaşam süresi tanıyabilir ya da dolunca eskiyi atabilir; bu ölçüm onları modellemez.

Hesap Bir Kez Yapılır

Önbelleğin kazancı hesabın yinelenmemesidir. Bunu sayarak görelim: aynı anahtarla üç çağrı bir istekte, bir çağrı da başka bir istemcide yapılır; blok kaç kez koşar?

# sayim.rb — ayni anahtarla bloga kac kez girilir
require_relative "onbellek"

kosma = 0
uygulama = Uygulama.new
uygulama.istek("A") do |b|
  3.times { onbellekle(b, "urun/1") { kosma += 1; "<li>kalem</li>" } }
end
uygulama.istek("B") { |b| onbellekle(b, "urun/1") { kosma += 1; "<li>kalem</li>" } }
puts "ayni anahtar: #{kosma}"
uygulama.istek("A") { |b| onbellekle(b, "urun/2") { kosma += 1; "<li>silgi</li>" } }
puts "ikinci anahtar: #{kosma}"
ayni anahtar: 1
ikinci anahtar: 2

Dört çağrıya karşın blok bir kez koştu; farklı bir anahtar bloğu bir kez daha çalıştırdı. Anahtar, hesabın kimliğidir: iki çağrı aynı anahtarı kullanıyorsa onlar için aynı hesap yapılmış sayılır.

Sorgu Önbelleği ile Karşılaştırma

Önceki bir derste sorgu önbelleğini ölçmüştük. Aynı istekte aynı sorgu ikinci kez veritabanına gitmiyordu, ama bir sonraki istek sayacı sıfırdan başlatıyordu. Aynı kuralı iki önbelleğe birlikte uygulayalım.

# karsilastirma.rb — sorgu onbellegi ile parca onbellegi yan yana
require_relative "onbellek"

kalem = ->(b) { Urun.olustur(b, ad: "kalem", fiyat: 12) }
yaz = ->(b) { kalem.call(b); Urun.bul(b, 1) }
oku = ->(b) { b.istek_deposu.dig(:sorgular, [:urunler, 1], :ad) }
goster("sorgu", *kalis(yaz, oku))
yaz = ->(b) { onbellekle(b, "urun/1") { "<li>kalem</li>" } }
oku = ->(b) { b.uygulama.depo[:onbellek]["urun/1"] }
goster("parca", *kalis(yaz, oku))
sorgu         bu_istek      A1 -- -- --  "kalem"
parca         kalici        A1 A2 A3 B4  "<li>kalem</li>"

İkisi de önbellek adını taşır ama kalışları zıttır. Sorgu önbelleği bu istek kalışındadır: istek deposunda yaşar ve isteğin sonunda gider. Parça önbelleği kalıcı kalışındadır: uygulamanın deposunda yaşar. Aradaki fark, tekrar eden sorgunun aynı istek içindeki tekrar olması, tekrar eden parçanın ise istekler arasındaki tekrar olmasıdır. Sorgu önbelleğinin sınırı bir istektir; bu yüzden bayatlığı (kaynağın gerisinde kalma durumu) da yalnız o istekle sınırlıdır. Parça önbelleğinin sınırı bir istek değildir; bu yüzden eskime onun asıl sorunudur. Bu soruna sıra geldi. Bu iki önbellek aynı kuralla ölçüldüğü için ikisi de aynı çekirdeğin parçasıdır, ama biri isteği, öteki uygulamayı izler.

Geçersiz Kılma

Parça kalıcıysa, ürün değiştiğinde ne olur? Önbellek parçayı kaynağından bağımsız saklar; kaynak değiştiğini bilmez. Bu yüzden önbelleği yönetmek, eskiyen girdiyi yenisiyle değiştirmeyi de içerir. Girdiyi eskimiş saymak ve atmak geçersiz kılma (invalidation) adını alır. Bir yolu girdiyi silmektir. Öteki yol anahtarı değiştirmektir: eski girdi geçersiz kılınmaz, ama yeni anahtar ona hiç uğramaz. parca yardımcımız ikinci yolu kullanır; anahtara ürünün sürümünü katar. Ürün değişince sürüm artar ve anahtar farklı olur.

# gecersiz.rb — surumlu anahtar ile gecersiz kilma
require_relative "onbellek"

uygulama = Uygulama.new
urun = { id: 1, ad: "kalem", surum: 1 }
p uygulama.istek("A") { |b| parca(b, urun) }
urun = urun.merge(ad: "silgi", surum: 2)
p uygulama.istek("A") { |b| parca(b, urun) }
p uygulama.depo[:onbellek].keys
"<li>kalem</li>"
"<li>silgi</li>"
["urun/1/1", "urun/1/2"]

Ürün değişince çıktı da değişti. Ama önbelleğin anahtarlarına bakınca eski girdi hâlâ orada: sürümlü anahtar eski girdiyi geçersiz kılmaz, yalnız ona bir daha uğramaz. Eski girdi, hiç kullanılmadığı hâlde önbelleği doldurur; bu bedeli bir yaşam süresi ya da yer açma ilkesi öder. UY8. Burada eski girdinin silinmesi gösterilmedi; silmenin hangi anda ve kim tarafından yapılacağı önbelleğin yapılandırmasına bağlıdır.

Girdiyi Silerek Geçersiz Kılmak

Sürümlü anahtarın bıraktığı eski girdi, ilk yolun neden ayrı bir ölçüm istediğini gösterir: girdiyi doğrudan silmek. Ölçümde aynı anahtar iki kez kullanılır; arada girdi önbellekten silinir. Blok her koşuşta bir sayaç artırır.

# silme.rb — girdiyi silmek bloga yeniden girdirir mi
require_relative "onbellek"

kosma = 0
uygulama = Uygulama.new
2.times { uygulama.istek("A") { |b| onbellekle(b, "k") { kosma += 1; "eski" } } }
puts "silmeden once: #{kosma}"
uygulama.istek("A") { |b| b.uygulama.depo[:onbellek].delete("k") }
uygulama.istek("A") { |b| onbellekle(b, "k") { kosma += 1; "yeni" } }
puts "sildikten sonra: #{kosma}"
p uygulama.depo[:onbellek]
silmeden once: 1
sildikten sonra: 2
{"k" => "yeni"}

Silmeden önce blok bir kez koştu. Girdi silindikten sonra bir sonraki çağrı bloğu yeniden koşturdu ve önbellekte artık yeni değer duruyor. Silme, sürümlü anahtarın tersini yapar: eski girdi gider, anahtar aynı kalır. Bu yolda önbellek şişmez, ama silme işini yapan bir kodun kaynak değiştiğinde çalışması gerekir; çalışmazsa girdi bayat kalır.

nil Sonucu onbellekle Saklamaz

onbellekle yardımcısı değeri ||= ile yazar. Ruby’de nil ve false yanlış sayıldığı için bu iki değer önbellekte durmaz. Bir hesabın sonucu “bulunamadı” anlamında nil ise, hesap her çağrıda yeniden yapılır. Aynı anahtarla iki çağrı yapıp bloğun kaç kez koştuğuna bakalım.

# bos.rb — nil donduren hesap onbellege girer mi
require_relative "onbellek"

kosma = 0
uygulama = Uygulama.new
2.times { uygulama.istek("A") { |b| onbellekle(b, "yok") { kosma += 1; nil } } }
puts "nil: #{kosma}"
2.times { uygulama.istek("A") { |b| onbellekle(b, "var") { kosma += 1; "x" } } }
puts "dolu: #{kosma}"
nil: 2
dolu: 3

nil döndüren blok iki çağrının ikisinde de koştu; dolu bir değer döndüren blok ise yalnız bir kez koştu. Yardımcının kazancı yalnız önbelleğe giren değerler için vardır. Pahalı bir hesap “yok” sonucuna da varabilir ve bu sonuç her seferinde yeniden hesaplanır. Bunu önlemek isteyen kod, boş sonucu önbelleğe ayrı bir değerle, örneğin boş bir dizgeyle yazar.

Anahtar Kimin Değeri Olduğunu Söylemelidir

Parça önbelleği kalıcıdır ve istemciler arasında paylaşılır. Bu, parça bir istemciye özelse bir sorun doğurur. Anahtarda istemciyi ayıran bir şey yoksa ikinci istemci, birincinin parçasını alır. Ölçümde iki istemci oturumlarına farklı kullanıcı numaraları yazar ve aynı anahtarla bir karşılama parçası ister.

# paylasilan.rb — istemciye ozel parca ortak anahtarla saklanirsa
require_relative "onbellek"

uygulama = Uygulama.new
uygulama.istek("A") { |b| b.oturum[:kullanici_id] = 42 }
uygulama.istek("B") { |b| b.oturum[:kullanici_id] = 7 }
%w[A B].each do |istemci|
  parca = uygulama.istek(istemci) do |b|
    onbellekle(b, "karsilama") { "<p>kullanici #{b.oturum[:kullanici_id]}</p>" }
  end
  puts "#{istemci} #{parca}"
end
A <p>kullanici 42</p>
B <p>kullanici 42</p>

B, 7 numaralı kullanıcı olduğu hâlde 42 numaralı kullanıcının parçasını gördü. Parçayı üreten ilk istek A’nındı; B’nin isteği aynı anahtarı bulunca bloğa girmedi. Kalıcı kalışın bedeli budur: önbelleğe yazılan değer, yazan isteğin değil, uygulamanın malıdır. Anahtara kullanıcı numarasını katmak sorunu çözer, ama o zaman parçanın paylaşım kazancı kullanıcı sayısı kadar parçaya bölünür. Hangi parçanın herkese ortak, hangisinin kullanıcıya özel olduğuna karar vermek, anahtarı tasarlamanın asıl işidir. Bu karar verilmeden yazılan bir anahtar, ölçümde görüldüğü gibi, bir kullanıcının verisini başka bir kullanıcının yanıtına taşıyabilir ve hata, hiçbir istisna fırlatmadan, yalnız yanlış bir çıktı olarak ortaya çıkar.

Sınırlayıcı Ölçüm

Anahtar değişmiyorsa, yani sürüm katılmamışsa, kaynak değiştiğinde ne olur? Aynı anahtarla iki çağrı yapalım; ikincisinde blok farklı bir değer üretir.

# bayat.rb — anahtar ayni kalirsa icerik bayatlar
require_relative "onbellek"

uygulama = Uygulama.new
p uygulama.istek("A") { |b| onbellekle(b, "k") { "eski" } }
p uygulama.istek("A") { |b| onbellekle(b, "k") { "yeni" } }
"eski"
"eski"

İkinci çağrının bloğu hiç koşmadı. Önbellek anahtarı gördü, saklanan değeri döndürdü ve kaynağın artık yeni olduğundan habersiz kaldı. Buna bayat içerik denir. Kural yalnız kalışı ölçer; değerin hâlâ doğru olup olmadığı onun sorusu değildir, ve bu ölçüm iki sorunun neden ayrı olduğunu gösterir. Parça önbelleğinin kalıcı kalışı bir avantajdır: istekler arasında hesap paylaşılır. Aynı kalış, geçersiz kılma yapılmadığında bir bedeldir: bayat içerik bütün istemcilere, herkes için aynı biçimde sunulur. Önbelleğe alınan her parça için iki soru birlikte sorulmalıdır: bu hesap ne kadar pahalı, ve kaynağı değiştiğini önbellek nereden öğrenecek?

Özet

  • Önbellek, pahalı bir hesabın sonucunu saklar; parça önbelleği bir görünümün tek bir parçasını saklar ve anahtarı hesabın kimliğidir.
  • Parça önbelleği uygulamanın ortak deposundadır; bu dersin kalışı kalıcıdır ve başka bir istemcinin isteği de aynı parçayı kullanır.
  • Aynı anahtarla yapılan dört çağrıda blok bir kez koşar; farklı anahtar bloğu yeniden koşturur.
  • Sorgu önbelleği bu istek, parça önbelleği kalıcı kalışındadır; aynı ad iki ayrı kalışı gizler.
  • Anahtar değişmedikçe içerik bayatlar; anahtara sürüm katmak bayat girdiye uğramayı keser ama eski girdiyi bırakır, girdiyi silmek ise önbelleği şişirmez ama silme kodunun çalışmasını gerektirir.
  • onbellekle nil ve false sonucunu saklamaz; ortak anahtarla saklanan kullanıcıya özel parça başka kullanıcıya görünür.

Sonraki Adım

Kalıcı bir parçayı bayatlamadan kullanmak için önce parçanın doğru çalıştığını bilmek gerekir. Bir sonraki ders testi ölçer: bir denetleyicinin örnek değişkenini dışarıdan okumak ve her testin kendi uygulamasını kurduğu bir yalıtım düzeneği.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat