Więcej na rubyonrails.pl: Start | Pobierz | Wdrożenie | Kod (en) | Screencasty | Dokumentacja | Ekosystem | Forum | IRC

Cache’owanie w Rails: Omówienie

Ten podręcznik nauczy Cię wszystkiego, czego potrzebujesz, aby uniknąć kosztownego i czasochłonnego wnikania do bazy danych oraz zwracania tego co chcesz zwrócić przez web klienta w jak najkrótszym czasie.

Po przeczytaniu tego przewodnika powinieneś być w stanie użyć i skonfigurować:

1 Podstawowe Cache’owanie

Jest to wprowadzenie do trzech rodzajów technik cache’owania, które domyślnie oferuje Rails, bez użycia jakichkolwiek wtyczek.

Jeśli pracujemy w trybie developerskim i chcemy rozpocząć testowanie będziemy musieli upewnić się, że zmienna config.action_controller.perform_caching jest ustawiony na wartość true. Ta flaga jest zazwyczaj ustawiona w odpowiednich plikach config/environments/*.rb. Cache’owanie jest domyślnie wyłączone dla trybów developerskiego i testowania, a włączone w trybie produkcji.

config.action_controller.perform_caching = true

1.1 Cache’owanie strony

Cache’owanie strony to mechanizm Rails, który pozwala serwerowi WWW (np. Apache lub nginx) wypełnić żądanie dla generowanej strony, bez konieczności przechodzenia przez stos Rails. Oczywiście jest to bardzo szybkie rozwiązanie. Niestety, nie można zastosować go do każdej sytuacji (np. dla stron wymagających autoryzacji). Utrata możliwości cache’owania to zagadnienie, z którym trzeba sobie poradzić, szczególnie od czasu gdy webserwer tak naprawdę udostępnia tylko pliki z systemu.

Zatem, jak włączyć to superszybkie zachowanie cache? Jest to proste. Powiedzmy, że masz kontroler działania zwany ProductsController oraz index, który zawiera wszystkie produkty.

class ProductsController < ActionController caches_page :index def index @products = Products.all end end

Przy pierwszym wywołaniu /products Rails zawsze wygeneruje plik o nazwie products.html. Natomiast serwer będzie szukać tego pliku, zanim przejdzie do zapytania o /products do Twojej aplikacji Rails.

Domyślnie, katalog stron cache jest ustawiony na Rails.public_path (który zwykle jest ustawiony jako folder public) i może być skonfigurowany poprzez zmianę ustawień konfiguracyjnych config.action_controller.page_cache_directory. Od momentu, gdy zechcesz umieszczać w folderze public inne statyczne pliki HTML, zmiana domyślnego ustawienia public pomaże Ci uniknąć konfliktów nazw. Jednak będzie to wymagać rekonfiguracji serwera, aby poinformować go, gdzie przechować pliki z pamięci cache.

Na żądanie stron, które nie mają rozszerzenia, mechanizm cache’owania stron automatycznie doda rozszerzenie .html, aby ułatwić serwerowi ich znalezienie. Możemy to zmienić poprzez zmianę ustawień konfiguracyjnych config.action_controller.page_cache_extension.

W przypadku wygaśnięcia strony, kiedy nowy produkt zostaje dodany, możemy rozszerzyć nasz przykładowy kontroler w taki oto sposób:

class ProductsController < ActionController caches_page :index def index @products = Products.all end def create expire_page :action => :list end end

Jeśli potrzebujesz bardziej zaawansowanego systemu ważności, możesz użyć sweeperów cache do automatycznego czyszczenia bufora, kiedy elementy ulegają zmianie. Tę funkcję obejmuje sekcja dotycząca sweeperów.

Uwaga: Cache’owanie strony ignoruje wszystkie parametry. Na przykład strona /products?page=1 zostanie zapisana w systemie plików jako products.html bez żadnego odniesienia do parametru page. Tak więc, jeśli ktoś wywoła później stronę /products?page=2, otrzyma zachowaną pierwszą stronę. Należy być bardzo ostrożnym, gdy cache’ujemy strony z parametrami GET w adresach URL!

Cache’owanie stron przebiega po filtrowaniu. Tak więc, błędne zapytania nie będą generować fałszywych wpisów w pamięci cache, dopóki ich nie zatrzymamy. Zazwyczaj całą pracę wykonują przekierowania, które sprawdzają warunki zapytania. W niektórych przypadkach następuje to przed filtrowaniem.

1.2 Cache’owanie akcji

Jednym z problemów, związanych z cache’owaniem stron jest to, że nie można użyć go do stron, które w jakiś sposób ograniczają do nich dostęp. W tym właśnie momencie używanye jest Cache’owanie akcji. Działa ono jak Cache’owanie stron z wyjątkiem faktu, że przychodzące żądania webowe wychodzą z serwera i są rozpatrywane wewnątrz Railsów oraz w module Action Pack, tak aby filtr wstępny mógł zostać uruchomiony zanim obsłużymy cache. Umożliwia to uruchomienie uwierzytelniania i innych ograniczeń w trakcie zapisywania wyników wyjścia z cache’owanej kopii.

Wyczyszczenie pamięci cache działa dokładnie tak samo jak w cache’owaniu stron. Powiedzmy, że chcemy, aby tylko uwierzytelnieni użytkownicy mogli wywołać akcje na kontrolerze ProductsController.

class ProductsController < ActionController before_filter :authenticate caches_action :index def index @products = Product.all end def create expire_action :action => :index end end

Można również użyć metody :if (lub :unless), aby przejść Proc, która określa kiedy działania powinny być cache’owane. W dodatku możemy użyć do cache’owania, kodu bez layoutów :layout => false, tak aby informacje dynamiczne znajdujące się w layoucie, takie jak informacje o zalogowanym użytkowniku lub liczba przedmiotów w koszyku, mogły być niecache’owane. Funkcja ta jest dostępna w Rails 2.2.

Można również zmodyfikować domyślną ścieżkę ActionCache dodając opcję :cache_path. Są one przekazywane bezpośrednio do ActionCachePath.path_for. Jest to przydatne w przypadku działań z wielu możliwych tras, które powinny być cache’owane w różny sposób. Jeśli blok jest podany, to wywoływany jest z bieżącej instancji kontrolera.

Wreszcie, jeśli używamy memcached, możemy także przekazać parametr :expires_in. W rzeczywistości wszystkie parametry, które nie są wykorzystywane przez caches_action wysyłane zostają do podstawowego magazynu cache.

cache’owanie stron przebiega po filtrowaniu. Tak więc, błędne zapytania nie będą generować fałszywych wpisów w pamięci cache, dopóki ich nie zatrzymamy. Zazwyczaj całą pracę wykonują przekierowania, w niektórych przypadkach przed filtrowaniem, które sprawdzają warunki zapytania.

1.3 Cache’owanie fragmentów

Życie byłoby idealnie, gdybyśmy mogli cache’ować całą zawartość strony lub działań i serwerujących ją na świat. Niestety, dynamiczne aplikacje WWW zwykle budowane są z różnych komponentów, z których nie wszystkie mają takie same cechy cache’owania. W celu zaadresowania tych dynamicznie tworzonych stron, gdzie poszczególne części strony muszą być cache’owane i wygaszane w inny sposób, Rails dostarcza mechanizm zwany cache’owaniem fragmentów (Fragment Caching).

Cache’owanie fragmentów pozwala, aby fragment widoku logicznego został zapakowany w bloku cache i przechowywany w magazynie cache w momencie gdy nadchodzi następne wywołanie.

Na przykład, jeśli chcemy pokazać wszystkie zamówienia złożone na swojej stronie www w czasie rzeczywistym i nie chcemy cache’ować tej części naszej strony, natomiast cache’ować chcemy część zawierającą pełną listę dostępnych produktów, powinniśmy użyć tego fragmentu kodu:

<% Order.find_recent.each do |o| %> <%= o.buyer.name %> bought <% o.product.name %> <% end %> <% cache do %> All available products: <% Product.all.each do |p| %> <%= link_to p.name, product_url(p) %> <% end %> <% end %>

W naszym przykładzie, blok pamięci będzie dotyczył akcji, które wywołują go i zapisują w tym samym miejscu jako Action Cache, co oznacza, że jeśli chcesz cache’ować różnorodne fragmenty danej akcji, to należy dodać metodę action_suffix do wywołania cache:

<% cache(:action => 'recent', :action_suffix => 'all_products') do %> All available products:

Za pomocą metody expire_fragment można wyłączyć je tak jak poniżej:

expire_fragment(:controller => 'products', :action => 'recent', :action_suffix => 'all_products')

Jeśli nie chcesz blokować cache do wiązania się z akcjami, które go wywołują, można również używać globalnie zakodowanych fragmentów wywołując metodę z kluczem cache tak jak poniżej:

<% cache('all_available_products') do %> All available products: <% end %>

Fragment ten jest dostępny przy użyciu klucza, dla wszystkich działań w kontrolerze ProductsController i można go wyłączyć w ten sam sposób:

expire_fragment('all_available_products')

1.4 Sweepers

Cache Sweepers to mechanizm, który pozwala poruszać się po kodzie mimo bardzo dużej ilości wywołań expire_{page,action,fragment}. Czyni to poprzez przeniesienie do klasy ActionController::Caching::Sweeper wszystkich działań niezbędnych do wygaśnięcia cache’owanej zawartości. Klasa ta jest obserwatorem i poprzez wywołania zwrotne szuka zmian w obiekcie, a gdy jakąś znajdzie – usuwa cache związany z obiektem podczas filtrowania, w trakcie lub po nim.

Kontynuując nasz przykład kontrolera produktu, moglibyśmy przepisać go z sweeperem w taki oto sposób:

class ProductSweeper < ActionController::Caching::Sweeper observe Product # This sweeper is going to keep an eye on the Product model # If our sweeper detects that a Product was created call this def after_create(product) expire_cache_for(product) end # If our sweeper detects that a Product was updated call this def after_update(product) expire_cache_for(product) end # If our sweeper detects that a Product was deleted call this def after_destroy(product) expire_cache_for(product) end private def expire_cache_for(product) # Expire the index page now that we added a new product expire_page(:controller => 'products', :action => 'index') # Expire a fragment expire_fragment('all_available_products') end end

Można zauważyć, że bieżący produkt zostanie przekazany do sweepera, tak jakbyśmy cache’owali edytowaną akcję dla każdego produktu, zatem moglibyśmy dodać metodę wygaszającą, która określałaby stronę, którą to chcemy wygasić:

expire_action(:controller => 'products', :action => 'edit', :id => product)

Następnie dodamy ją do naszego kontrolera, aby wywołał on sweeper, w przypadku wywołania pewnych działań. Zatem, jeśli chcielibyśmy usunąć cache’owaną zawartość z listy i edytować akcje w chwili wywołania tworzenia akcji, możemy wykonać następujące czynności:

class ProductsController < ActionController before_filter :authenticate caches_action :index cache_sweeper :product_sweeper def index @products = Product.all end end

1.5 Cache’owanie SQL

Cache’owanie zapytań sql jest funkcją Railsów, która cache’uje wyniki zwracane przez każde zapytanie tak, że jeśli Rails napotyka ponownie takie samo zapytanie do tego wywołania, to będzie korzystał z pamięci podręcznej zestawu wyników, a nie będzie ponownie uruchamiał wywołania w bazie danych.

Na przykład:

class ProductsController < ActionController def index # Run a find query @products = Product.all ... # Run the same query again @products = Product.all end end

Po raz drugi to samo zapytanie zostało wysłane do bazy danych, jednak w rzeczywistości nie dojdzie ono do bazy. Za pierwszym razem wynik jest zwracany przez zapytanie i przechowywany w pamięci podręcznej zapytań, zaś drugi raz jest ono wyciągane z pamięci.

Warto jednak pamiętać, że cache’owanie zapytań jest tworzone na początku działania i niszczone pod jego koniec – tym samym zapytania zapamiętywane są tylko na czas działania. Jeśli chcesz przechowywać wyniki zapytań w sposób bardziej trwały, można używać w Rails cache’owania niskiego poziomu.

2 Sposoby przechowywania cache’u – Cache Store

Railsy przewidują różne sposoby przechowywania cache’owanych danych utworzonych przez cache’owanie fragmentów i akcji. Cache’owane strony przechowywane są zawsze na dysku.

Railsy w wersji 2.1 i wyższych zapewniają sposoby przechowywania cache’u ActiveSupport::Cache::Store, które mogą zostać wykorzystane do łańcuchów cache. Implementacje niektórych sposobów, takich jak MemoryStore, mogą cache’ować dowolne obiekty Rubiego, ale nie licz, że każdy sposób przechowywania będzie w stanie to zrobić.

Domyślne sposoby przechowywania cache’u dostarczane przez Rails obejmują:

1) ActiveSupport::Cache::MemoryStore: wdrożenie sposobu przechowywania cache’u, który przechowuje wszystko w swojej pamięci w tym samym procesie. Jeśli uruchomiłeś wiele procesów serwera Ruby on Rails (co ma miejsce, jeśli korzystasz z mongrel_cluster lub Phusion Passenger), to oznacza to, że Twoje instancje procesów serwera Rails nie będą mogły dzielić się ze sobą danymi cache. Jeśli Twoja aplikacja nie wykonuje instrukcji wygaśnięcia pozycji cache (np. gdy używasz generowanych kluczy cache), wtedy użycie MemoryStore jest właściwe. W przeciwnym razie, starannie rozważ, czy należy używać tego sposobu.

MemoryStore jest zdolny do przechowywania nie tylko łancuchów znaków, ale i dowolnych obiektów Ruby.

1) ActiveSupport::Cache::MemoryStore: A cache store implementation which stores everything into memory in the same process. If you’re running multiple Ruby on Rails server processes (which is the case if you’re using mongrel_cluster or Phusion Passenger), then this means that your Rails server process instances won’t be able to share cache data with each other. If your application never performs manual cache item expiry (e.g. when you‘re using generational cache keys), then using MemoryStore is ok. Otherwise, consider carefully whether you should be using this cache store.

MemoryStore nie jest bezpieczno-wątkowy. Jeśli potrzebujesz takiego bezpieczeństwa użyj magazynu SynchronizedMemoryStore.

ActionController::Base.cache_store = :memory_store

2) ActiveSupport::Cache::FileStore: cache’owane dane są przechowywane na dysku, jest to domyślny sposób przechowywania cache’u, domyślna ścieżka to tmp/cache. Pracuje on również dla wszystkich typów środowisk i pozwala, aby wszystkie procesy z tego samego katalogu aplikacji miały dostęp do zawartości pamięci podręcznej. Jeśli katalog tmp/cache nie istnieje, domyślnym sposobem staje się MemoryStore.

ActionController::Base.cache_store = :file_store, "/path/to/cache/directory"

3) ActiveSupport::Cache::DRbStore: cache’owane dane są przechowywane w oddzielnych procesach DRb, które komunikują się z wszystkimi serwerami. Cache’owanie to działa we wszystkich środowiskach i zachowuje jeden cache dla wszystkich procesów, ale wymaga uruchomienia i zarządzania na oddzielnych procesach DRb.

ActionController::Base.cache_store = :drb_store, "druby://localhost:9192"

4) ActiveSupport::Cache::MemCacheStore: ten sposób przechowywania działa podobnie do DRbStore, ale zamiast niego używa memcached Dangi. Rails wykorzystuje domyślnie instalowany gem memcached-client. Jest to obecnie najbardziej popularny sposób przechpwywania cache’u dla produkcji stron www.

Cechy szczególne:

  • Klastrowanie i równoważenie obciążenia. Pierwszy może określać wiele serwerów memcache, zaś MemCacheStore będzie równoważył obciążenie między wszystkimi dostępnymi serwerami. Jeśli serwer przestanie działać, to MemCacheStore zignoruje go, dopóki nie powróci do trybu on-line.
  • Time-based expiry support – czasowe wygaśnięcie wsparcia. Zobacz opcje write i :expires_in.
  • Tworzony per żądanie cache przechowywany w pamięci dla całej komunikacji z swerwerami memcached.

Sposób ten akceptuje również mieszanie dodatkowych opcji:

  • :namespace: określa ciąg znaków, który będzie automatycznie dodawany do kluczy podczas dostępu do memcached.
  • :readonly: wartość logiczna, gdy jest ustawiona na true uznaje cach jako tylko do odczytu (read-only), wyrzuci błąd podczas wszelkich prób pisania. :multithread: wartość logiczna, która podnosi bezpieczeństwo wątku podczas operacji odczytu/zapisu – jest mało prawdopodobne, że będziemy potrzebowali użyć tej opcji jako Rails threadsafe (Railsy w wersji wielowątkowej). Metoda ta oferuje taką samą funkcjonalność.

Metody read i write w sposobie MemCacheStore akceptują również tablicę asocjacyjną opcji. Przy odczytywaniu możesz sprecyzować :raw => true, aby zapobiec przyporządkowaniu obiektu (domyślnie jest to false, co oznacza, że wartość raw w cache jest przekazywana do Marshal.load zanim zostanie zwrócona.)

Przy zapisywaniu do cache można również sprecyzować :raw => true, aby nie przekazywać do Marshal.dump przed złożeniem w cache (domyślnie jest to false). Metoda write akceptuje również flagę :unless_exist, która określa, czy użyć metody memcached add (gdy true), czy set (gdy false) do przechowania elementu w cache i opcji :expires_in, która określa długość życia cachowanego elementu w sekundach.

ActionController::Base.cache_store = :mem_cache_store, "localhost"

5) ActiveSupport::Cache::SynchronizedMemoryStore: taki sam sposób przechowywania cache’u jak MemoryStore, ale bezpieczno-wątkowy.

ActionController::Base.cache_store = :synchronized_memory_store

6) ActiveSupport::Cache::CompressedMemCacheStore: działa podobnie jak regularny MemCacheStore, ale używa GZip do rozpakowywania/pakowania podczas odczytu/zapisu.

ActionController::Base.cache_store = :compressed_mem_cache_store, "localhost"

7) Przechowywanie niestandardowe: Możesz określić swój własny sposób przechowywania pamięci cache (nowość w Rails 2.1).

ActionController::Base.cache_store = MyOwnStore.new("parameter")

UWAGA: config.cache_store może być używany w miejsce ActionController::Base.cache_store w Twoim bloku Rails::Initializer.run w pliku environment.rb

Poza tym, Rails dodaje także metodę ActiveRecord::Base#cache_key, która generuje klucz używając nazwy klasy, id oraz znacznik czasu updated_at (jeżeli jest dostępny).

Możesz również uzyskać dostęp do cache store o niskim poziomie, dla przechowywania zapytań i innych obiektów. Oto przykład:

Rails.cache.read("city") # => nil Rails.cache.write("city", "Duckburgh") Rails.cache.read("city") # => "Duckburgh"

3 Wsparcie dla warunkowego GETa

Warunkowe GETy są elementami specyfikacji protokołu HTTP, które zapewniają webserwerom sposób na dostarczenie przeglądarkom informacji, że odpowiedź na żądanie GET nie została zmieniona od czasu ostatniego żądania i może być bezpiecznie wyciągnięta z pamięci cache przeglądarki.

Pracują one przy użyciu nagłówków HTTP_IF_NONE_MATCH i HTTP_IF_MODIFIED_SINCE, aby przesyłać tam i z powrotem, zarówno unikatowy identyfikator zawartości jak i znacznik czasu, kiedy zawartość została zmieniona. Jeśli przeglądarka wysyła żądanie gdzie identyfikator zawartości(etag) lub ostatnia modyfikacja znacznika czasu pasuje do wersji serwera, to serwer musi jedynie odesłać pustą wiadomość z niezmodyfikowanym statusem.

Odpowiedzialnością serwera (czyli de facto naszą) jest znalezienie ostatnio zmodyfikowanego znacznika czasu i – jeśli nie został on dopasowany – nagłówka oraz określenie czy odesłać pełną odpowiedź, czy nie. Z wsparciem warunkowego GETa w Railsach jest to całkiem proste zadanie:

class ProductsController < ApplicationController def show @product = Product.find(params[:id]) # If the request is stale according to the given timestamp and etag value # (i.e. it needs to be processed again) then execute this block if stale?(:last_modified => @product.updated_at.utc, :etag => @product) respond_to do |wants| # ... normal response processing end end # If the request is fresh (i.e. it's not modified) then you don't need to do # anything. The default render checks for this using the parameters # used in the previous call to stale? and will automatically send a # :not_modified. So that's it, you're done. end

Jeżeli używamy domyślnego mechanizmu renderowania (czyli nie używamy metody respond_to lub nie wywołujemy renderowania samoistnego) oraz nie mamy żadnego przetwarzania specjalnych odpowiedzi, to mamy możliwość skorzystania z łatwego helpera fresh_when:

class ProductsController < ApplicationController # This will automatically send back a :not_modified if the request is fresh, # and will render the default template (product.*) if it's stale. def show @product = Product.find(params[:id]) fresh_when :last_modified => @product.published_at.utc, :etag => @product end end

4 Zaawansowane Cache’owanie

Wraz z wbudowanymi mechanizmami opisanymi powyżej, istnieje wiele świetnych wtyczek pomagających w kontroli nad cache’owaniem. Są to doskonałe pluginy cache_fu stworzone przez Chrisa Wanstratha(więcej informacji tutaj) i interlock plugin stworzony przez Evana Tkacza (więcej informacji tutaj). Obie te wtyczki dobrze współdziałają z memcached, dlatego każdy kto poważnie myśli o zoptymalizowaniu swojego cache’owania musi je zobaczyć i przeanalizować. Also the new Cache money plugin is supposed to be mad cool.

Także nowy plugin “Cache money”: http://github.com/nkallen/cache-money/tree/master prawdopodobnie będzie bardzo interesujący.

5 Referencje

6 Changelog

Bilet Lighthouse

  • 02 maja 2009: Formatowanie.
  • 26 kwietnia 2009: Czyszczenie i poprawa literówek w zaproponowanych patchach.
  • 1 kwietnia 2009: Kilka drobnych poprawek.
  • 22 lutego 2009: Poprawienie sekcji cache_stores.
  • 27 grudnia 2008: Poprawa literówek.
  • 23 listopada 2008: Przyrostowe aktualizacje z różnych propozycji zmian oraz formatowanie.
  • 15 września 2008: Wersja wstępna, stworzona przez Aditya Chadha.