Anzeige
Anzeige
UX & Design
Artikel merken

Die Grundlagen der Cache-Mechanismen: Caching in Ruby on Rails

Eine Webanwendung ist mit Ruby on Rails sehr schnell und komfortabel entwickelt. Was aber, wenn sie anschließend auch schnellen Erfolg hat? Was, wenn zum Beispiel die Erwähnung in der Presse für einen unerwarteten Ansturm sorgt – einen Ansturm, dem die Anwendung und die Server nicht gewachsen sind? Für diesen und ähnliche Fälle gibt es durchaus ein paar Dinge zu tun, um die Server auf die erhöhte Nachfrage vorzubereiten.

9 Min. Lesezeit
Anzeige
Anzeige

Unter Beachtung einiger Regeln skalieren Rails-Anwendungen gut mit zusätzlicher Hardware. Allerdings ist die Lösung unter Umständen teuer und sie erhöht durch den höheren Wartungsaufwand auch die Folgekosten. Günstiger ist in der Regel ein anderer Ansatz: das Caching, also das Zwischenspeichern. Dabei geht man davon aus, dass das Generieren der Webseiten, also das Ausführen der eigentlichen Webapplikation der „teure“, weil rechenaufwändige Vorgang ist, während das eigentliche Ausliefern der fertig „gerenderten“ Seite billig und schnell zu erledigen ist. Das gilt insbesondere, wenn ein Webserver wie Apache anstelle eines Applikationsservers die Arbeit erledigt.

Ganz einfach: Page-Caching

Anzeige
Anzeige

In Rails gibt es „out of the box“ drei verschiedene Ebenen des Cachings. An erster Stelle steht das performanteste, aber auch unflexibelste Verfahren, das so genannte „Page-Caching“. Page-Caching wird in einem Controller aktiviert, wobei die entsprechenden Actions zum Page-Caching angemeldet werden:

RUBY
class MeinController < ApplicationController
		caches_page :index
	def index
			[...]
		end
	end

Listing 1

Wird die index-Action das erste Mal aufgerufen, liefert Rails zunächst die Webseite aus, speichert sie aber gleichzeitig als HTML-Datei im Webroot des Webservers (üblicherweise das „public/“-Verzeichnis, in dem auch die Bilder, Stylesheets und JavaScripts der Applikation liegen). Beim nächsten Request wird bei entsprechender Konfiguration des Webservers statt der Anfrage an die Rails-Applikation die gespeicherte Webseite ausgeliefert. Der Request kommt also gar nicht erst bei der Rails-Applikation an.

Anzeige
Anzeige

Dass es schneller geht, eine statische HTML-Seite vom Apache-Server ausliefern zu lassen als von der Rails-Applikation leuchtet ein. Zweckmäßig ist das Vorgehen aber nur dann, wenn die Action unabhängig von irgendwelchen Session-Daten oder zusätzlichen URL-Parametern ist. Allerdings muss die Seite nicht notwendigerweise statisch beziehungsweise unveränderlich sein, da es durchaus möglich ist, innerhalb der Applikation Caches zu leeren.

Anzeige
Anzeige

Page-Caching eignet sich also vor allem für Seiten, deren Inhalt sich nicht abhängig vom eingeloggten Benutzer oder ähnlichen Dingen ändert. Sehr gute Kandidaten dafür sind zum Beispiel Hilfeseiten, die zwar von einem Redaktionsteam über die Applikation gepflegt werden (weshalb sie nicht als statische HTML-Seiten unter „public/“ abgelegt sind), aber für jeden Benutzer gleich angezeigt werden.

Cache-Inhalte löschen

Werden Cache-Inhalte angelegt, sollte es auch eine Möglichkeit geben, sie zu löschen, beziehungsweise den Cache zu invalidieren. Das kann zum Beispiel explizit geschehen, wenn nach einer Änderung an einer Seite durch folgende Zeilen der Cache invalide wird:

Anzeige
Anzeige
RUBY
def update
		[...]
	expire_page :controller => 'mein', :action => 'index'
	[...]
	end

Listing 2

Das Vorgehen ist allerdings meist nicht sinnvoll, da es die Cache-Funktionalität unnötigerweise an die beteiligten Controller koppelt. Üblicherweise hängt eine Invalidierung vielmehr mit den Modellen zusammen, das heißt sobald sich ein Modellobjekt ändert, das Daten für eine zwischengespeicherte Action bereitstellt, sollte der Cache gelöscht werden. Dehalb kann die Invalidierung von Caches über so genannte „Cache-Sweeper“ an Modelle gebunden werden.

Angenommen, über die oben genannte Action soll eine Liste von Büchern angezeigt werden. Es gäbe eine ActiveRecord-Klasse namens Book, und jedes Mal, wenn sich ein Buch ändert, dann würde der Cache invalide. Ein Sweeper (der üblicherweise unter „app/models/“ abgelegt wird) könnte folgendermaßen aussehen:

RUBY
class BookSweeper < ActionController::Caching::Sweeper 
		observe Book
		def after_create(book) 
			expire_public_page 
	end
		def after_update(book)
expire_public_page
	end   
	
	def after_destroy(book) 
		expire_public_page 
	end  
private 
	def expire_public_page 
		expire_page(:controller => "mein", :action => 'index') 
	end 
end

Listing 3

Die Zeile „observe Book“ sorgt dafür, dass die nachfolgenden Methoden an den entsprechenden Stellen des Lebenszyklus des Book-Modells als Callback aufgerufen werden.

Anzeige
Anzeige

Bitte mit Filter: Action-Caching

Der Anwendungsbereich des Page-Cachings ist begrenzt. Vollkommen ausgeschlossen ist es für Controller beziehungsweise Actions, die eine Benutzerauthentifizierung voraussetzen, welche zum Beispiel über einen „before_filter“ implementiert ist. Der before_filter würde beim Page-Caching gar nicht erst aufgerufen.

Genau für diese Fälle ist das „Action-Caching“ gedacht. Beim Action-Caching wird in jedem Fall die Rails-Applikation angesprochen und alle before_filter im Controller werden aufgerufen. Somit steht einer Authentifizierung nichts im Wege: Ein before_filter könnte einen Redirect auf die Login-Seite ausführen, sollte der Benutzer nicht berechtigt sein, die Action aufzurufen. Das folgende Listing zeigt den entsprechenden Controller:

RUBY
class MeinController < ApplicationController
		caches_action :index
		[...]
	end

Listing 4

Der Cache-Store kommt ins Spiel

Statt wie beim Page-Caching eine statische HTML-Seite im Dateisystem anzulegen, benutzt das Action-Caching den so genannten „Fragment-Cache-Store“ (zum eigentlichen „Fragment-Caching“ siehe unten), der prinzipiell wie ein Ruby-Hash funktioniert: Beliebige Dinge (in diesem Fall gerenderte HTML-Seiten) werden unter einem Key (hier Cache-Key genannt) abgelegt und mit Hilfe der Keys auch wieder hervorgeholt. Ganz ähnlich dem Dateinamen der gerenderten Page-Cache-Seite, besteht der Cache-Key aus der Action-URL, die, falls es sich um die index-Action handelt, um das Wort „index“ ergänzt wird.

Anzeige
Anzeige

Die Implementierung des Fragment-Cache-Stores ist wählbar: Der Standard ist das Ablegen der Keys als Datei im Dateisystem, was allerdings Schwierigkeiten beim Invalidieren macht, wenn mehrere Applikationsserver betrieben werden. Eine weitere Möglichkeit ist der Einsatz von „memcached“, einem sagenhaft performanten, über das Netzwerk anzusprechenden Datencontainer, der auch gern als Session-Store verwendet wird. Eine detailliertere Beschreibung der Optionen würde den Rahmen des Artikels sprengen, sodass hier auf die Hilfe im Internet verwiesen sei [1].

Auch die Invalidierung von Cache-Inhalten funktioniert bei Action-Caches analog zu den Page-Caches (wie auch das Schreiben von Observern):

RUBY
def update
		[...]
		expire_action :controller => 'mein', :action => 'index'
		[...]
	end

Listing 5

Bedingungsloses Caching?

Manchmal kommt es vor, dass nicht nur before_filter wie oben beschrieben vor dem Cache ausgeführt werden sollen, sondern dass das Caching nur bedingt durchgeführt werden muss. Das klassische Beispiel dafür ist eine Seite, die für eingeloggte Benutzer einen personalisierten Bereich enthält, der keinesfalls im Cache landen darf, während die Seite, wie sie sich anonymen Benutzern darstellt, durchaus zwischengespeichert werden sollte. Für den Fall bietet Rails leider keine mitgelieferte Lösung. Es wäre möglich, das Problem zu umschiffen, indem registrierte Benutzer auf eine andere URL weitergeleitet werden. Doch ist das auch nicht die optimale Lösung, denn unter Umständen sind viele Actions im Controller gedoppelt, was nebenbei dem REST-Design einer Applikation widerspricht. Die alternative Lösung ist das „conditional_caching-Plugin“ der kanadischen Firma Redline Software [2]. Nach der Installation wird die Anweisung „caches_action“ um den Parameter „:if“ ergänzt, mit dem eine Methode als Callback angegeben wird. So lassen sich Bedingungen definieren, die erfüllt sein müssen, damit das Caching greift. Das folgende Listing zeigt, wie das aussehen kann:

Anzeige
Anzeige
RUBY
class MeinController < ApplicationController
		caches_action :index, :if => :cache?
		[...]
		private
		def cache?
			!permit?('user')
		end
	end

Listing 6

Nur in Teilen: Fragment-Caching

Die letzte Ebene des Rails-Caching ist das „Fragment-Caching“. In Webanwendungen kommt es häufiger vor, dass bestimmte Seiten ihren Inhalt zum großen Teil aus dynamischen, benutzerabhängigen Daten beziehen, die sich nicht beziehungsweise kaum cachen lassen. Gibt es aber Bereiche auf den Seiten, die nicht personalisiert sind und gegebenenfalls aufwändig zu berechnen, liegt es nahe, nur diese Blöcke zwischenzuspeichern. Genau dafür existiert das Fragment-Caching. Um einen Block in einem Template zu cachen, wird folgender Code benötigt:

RUBY
[...]
	<% cache do %>
		<ul>
			<% for book in @books %>
				<li><%= book.title %> 
			<% end %>
		</ul>
	<% end %>
	[...]

Listing 7

Der Key, unter dem das Fragment abgelegt wird, besteht nun wieder aus der URL der generierten Seite. Das hat hier allerdings den Nachteil, dass nur ein Cache-Fragment pro Action erlaubt ist, weil sich sonst die Cache-Keys überschneiden würden. Auch lassen sich so die Blöcke nicht wiederverwendbar zwischenspeichern, was zum Beispiel ein Partial, das in mehreren Seiten eingebunden wird, unmöglich macht. Für diese Zwecke lassen sich Cache-Keys selber bauen und an die Cache-Methode übergeben:

RUBY
[...]
	<% cache :controller => 'meiner', :action => 'list', :part => 'books' do %>
		<ul>
			<% for book in @books %>
				<li><%= book.title %> 
			<% end %>
		</ul>
	<% end %>
	[...]

Listing 8

Da hier der normale Routing-Mechanismus von Rails verwendet wird, ist zu beachten, dass es entsprechende Routes gibt. Das heißt es lassen sich nicht beliebige Parameter verwenden.

Anzeige
Anzeige

Die Invalidierung wiederum funktioniert genauso wie bei den anderen Cache-Arten – so wie in Listing 9 zu sehen.

RUBY
def update
		[...]
		expire_fragment :controller => 'mein', :action => 'list', :part => 'books'
	[...]
	end

Listing 9

Der aufmerksame Leser wird sich bei dem in Listing 8 angegebenen Code allerdings eine Frage stellen: Woher kommt eigentlich die Liste der Bücher in @books? Die Antwort ist: aus dem Controller, wie üblich. Allerdings ist es nicht sinnvoll, die Liste durch den Controller wahllos aus der Datenbank zu zerren, denn dann wäre das Caching ja vermutlich überflüssig. In der Regel ist schließlich nicht das Rendern der HTML-Seite langsam, sondern die Datenbankabfrage. Es gilt also, eine Methode zu finden, die das Kapseln und die Abfrage nur dann ausführt, wenn man die Daten braucht. Eine oft genannte Variante dazu finden Sie in Listing 10. Dort wird im Controller nachgesehen, ob ein Fragment bereits vorhanden ist und die Abfrage gegebenenfalls unterbunden.

RUBY
def list
		unless read_fragment(:controller => 'meiner', :action => 'list', :part => 'books')
			@books = Book.find(:all)
		end
	end

Listing 10

In 99 Prozent der Fälle wird das Vorgehen funktionieren. Liegt allerdings viel Last auf der Applikation und die Seite wird sehr oft aufgerufen, kommt es unter Umständen zu einer unangenehmen Race Condition: Der Controller hat beschlossen, die Daten für das Fragment nicht zu besorgen, weil ein Fragment vorhanden war. Während anschließend der Rails-Prozess den View rendert, hat ein Administrator auf einem anderen Rails-Prozess beschlossen, ein Buch in der Datenbank zu löschen, was einen Observer anspringen lässt, der das Fragment löscht. Wird jetzt der View-Teil gerendert, der die „cache do…end“-Anweisung enthält, wird der Block ausgeführt und nicht das nicht mehr vorhandene Cache-Fragment ausgeliefert. Nun ist die benötigte @books-Instanzvariabel leer. Bei einem sehr defensiv programmierten View bleibt der Block leer, was (nur) unschön ist, während bei einem „normal“ programmierten View dem Benutzer eine weniger schöne 500er-Fehlermeldung entgegenspringt.

Anzeige
Anzeige

Pragmatismus über Prinzipien

Ganz genau genommen, muss die Abfrage der Daten innerhalb des Cache-Blocks erfolgen, doch heißt das nicht, dass nun ActiveRecord-Methoden im View aufgerufen werden. Einerseits handelt es sich beim Caching um Maßnahmen, auf die nur im Notfall zurückgegriffen wird, und dementsprechend ist Pragmatismus statt Paragraphenreiterei gefragt. Andererseits ist es dennoch sinnvoll, das Holen der Daten wenigstens in einen „Helper“ zu verpacken, wie es Listing 11 und 12 zeigen:

RUBY
def book_list
		Book.find(:all)
	end

Listing 11

RUBY
[...]
	<% 
		cache :controller => 'meiner', :action => 'list', :part => 'books' do 
		books = book_list
	%>
		<ul>
			<% for book in books %>
				<li><%= book.title %> 
			<% end %>
		</ul>
	<% end %>
	[...]

Listing 12

Fazit

Mit den drei Ebenen Page-Caching, Action-Caching und Fragment-Caching bietet Rails smarte und funktionierende Lösungen, um eine Anwendung besser auf einen Benutzeransturm vorzubereiten. Der zu verwendende Mechanismus ergibt sich aus der Struktur der Anwendung und dem Anteil an dynamischen und vor allem Session-abhängigen Seitenbestandteilen.

Damit ist das Thema „Caching in Rails“ keineswegs erschöpfend behandelt. Vollkommen ausgespart bleibt hier zum Beispiel das Thema „Caching testen“. Außerdem gibt es weitere Möglichkeiten, um zum Beispiel Ergebnisse von Berechnungen oder von komplexen Datenbankabfragen auf Modellebene zu cachen (siehe z. B. das „Cache_Fu-Plugin“ von Chris Wanstrath [3] ). Für viele Fälle dürften allerdings die hier gezeigten Mechanismen vollkommen ausreichen.

Mehr zu diesem Thema
Fast fertig!

Bitte klicke auf den Link in der Bestätigungsmail, um deine Anmeldung abzuschließen.

Du willst noch weitere Infos zum Newsletter? Jetzt mehr erfahren

Anzeige
Anzeige
Schreib den ersten Kommentar!
Bitte beachte unsere Community-Richtlinien

Wir freuen uns über kontroverse Diskussionen, die gerne auch mal hitzig geführt werden dürfen. Beleidigende, grob anstößige, rassistische und strafrechtlich relevante Äußerungen und Beiträge tolerieren wir nicht. Bitte achte darauf, dass du keine Texte veröffentlichst, für die du keine ausdrückliche Erlaubnis des Urhebers hast. Ebenfalls nicht erlaubt ist der Missbrauch der Webangebote unter t3n.de als Werbeplattform. Die Nennung von Produktnamen, Herstellern, Dienstleistern und Websites ist nur dann zulässig, wenn damit nicht vorrangig der Zweck der Werbung verfolgt wird. Wir behalten uns vor, Beiträge, die diese Regeln verletzen, zu löschen und Accounts zeitweilig oder auf Dauer zu sperren.

Trotz all dieser notwendigen Regeln: Diskutiere kontrovers, sage anderen deine Meinung, trage mit weiterführenden Informationen zum Wissensaustausch bei, aber bleibe dabei fair und respektiere die Meinung anderer. Wir wünschen Dir viel Spaß mit den Webangeboten von t3n und freuen uns auf spannende Beiträge.

Dein t3n-Team

Melde dich mit deinem t3n Account an oder fülle die unteren Felder aus.

Bitte schalte deinen Adblocker für t3n.de aus!
Hallo und herzlich willkommen bei t3n!

Bitte schalte deinen Adblocker für t3n.de aus, um diesen Artikel zu lesen.

Wir sind ein unabhängiger Publisher mit einem Team von mehr als 75 fantastischen Menschen, aber ohne riesigen Konzern im Rücken. Banner und ähnliche Werbemittel sind für unsere Finanzierung sehr wichtig.

Schon jetzt und im Namen der gesamten t3n-Crew: vielen Dank für deine Unterstützung! 🙌

Deine t3n-Crew

Anleitung zur Deaktivierung
Artikel merken

Bitte melde dich an, um diesen Artikel in deiner persönlichen Merkliste auf t3n zu speichern.

Jetzt registrieren und merken

Du hast schon einen t3n-Account? Hier anmelden

oder
Auf Mastodon teilen

Gib die URL deiner Mastodon-Instanz ein, um den Artikel zu teilen.

Anzeige
Anzeige