Artikel mit Tag feedtragón
Performance-Tuning einer Webapp mit Index und Arbeitsvermeidung
Monday, 2. February 2015
Meinen Feedreader feedtragón nutze ich praktisch jeden Tag. Ich bin soweit zufrieden, aber nach der Eingewöhnungsphase fiel mir auf, dass er etwas langsam war. Gemessen und uff: Die längste Wartezeit kam durchs Warten auf eine Antwort vom Server. Kein gutes Zeichen. In diesem Artikel werde ich beschreiben, wie ich das Problem anging und dabei diese Verbesserung produzierte:
Die Wartezeit wurde reduziert auf ein 84stel, von 2,8 Sekunden am Anfang auf ~33ms.
Schritt 1: Index setzen
Jedes mal, wenn eine Seite geladen wird, holt feedtragón eine Liste aller abonnierten Feeds vom Server, und prüft dann jeden Feed auf ungelesene Einträge. Dafür muss er die Einträge aus der Datenbank holen, das macht er mit diesem Query:
SELECT url, title, content, id FROM entries WHERE feed = ? AND read = 0 AND id > ? LIMIT 10;
Er beschränkt sich also auf 10 Einträge mit einer höheren ID als die Start-ID, so funktioniert das Endless-Scrolling, aber das ist hier nicht weiter wichtig. Wichtig ist der Rest des WHERE-Statemends: feed = ? AND read = 0
. Da werden also zwei Spalten abgefragt. Und insgesamt dauerte das alles 2,8 Sekunden - also nicht nur dieser Query, sondern alle zusammen.
Ich bin kein Datenbankexperte, aber ich vermutete, dass diese Abfrage schneller funktionieren könnte. Das ist eine SQLite-Datenbank, dort kann man das mit EXPLAIN QUERY PLAN
prüfen:
sqlite> EXPLAIN QUERY PLAN SELECT url, title, content, id FROM entries WHERE feed = ? AND read = 0 AND id > ? LIMIT 10; 0|0|0|SEARCH TABLE entries USING INTEGER PRIMARY KEY (rowid>?)
Was heißt das jetzt? Ich musste es nachgucken. Dass er hier nur mit dem Primary Key sucht heißt, dass er für read und feed keine andere Möglichkeit hat als manuell zu suchen. Also muss dafür je ein Index her:
sqlite> CREATE INDEX entries_read_idx ON entries(read); sqlite> CREATE INDEX entries_feed_idx ON entries(feed); sqlite> EXPLAIN QUERY PLAN SELECT url, title, content, id FROM entries WHERE feed = ? AND read = 0 AND id > ? LIMIT 10; 0|0|0|SEARCH TABLE entries USING INDEX entries_feed_idx (feed=? AND rowid>?)
feedtragón gestartet, den Browser aufgemacht, in die Netzwerkanalyse geschaut und mich gefreut: Die Wartezeit auf den Server schrumpfte von 2,8s auf 0,5s. Damit war die Sache für mich erstmal abgehakt und feedtragón ausreichend optimiert.
Schritt 2: Join (Arbeitsvermeidung)
Später kam mir dann der Gedanke, dass es besser gehen müsste. Denn das Vorgehen, das ich oben beschrieb - alle Feeds holen, ans Template schicken, dort deren Einträge holen, dann nur die Feeds mit ungelesenen Einträgen anzeigen - ist keineswegs effizient. Es folgt aus einer strikten Einhaltung der für dsnblog skizzierten Architekturprinzipen: Objekte ohne Controller, die für ihre Daten zuständig sind. Das ist toll zu schreiben, besonders das Datenbanklayer bleibt einfach beherrschbar, aber Dee erwähnte damals das möglicherweise kostspielige Befüllen der Daten. Und die Einträge des Feeds aus der Datenbank zu holen ist kostspielig.
Vorher holte er also alle Feeds aus der Datenbank:
SELECT url, id, name FROM feeds;
und filterte dann im Template die Feeds aus, die keine neuen Einträge haben:
feeds.each do |feed| erb :feedlink, :locals => {:feed => feed, :current_feed_id => current_feed_id} if ! feed.entries.nil? && feed.entries.size > 0 end
wobei feed.entries() wieder die Datenbank aufruft, mit dem im ersten Schritt optimierten Query.
Stattdessen könnte man auch direkt nur die Feeds übergeben, die ungelesen Einträge haben. Dafür muss nur die Feed- und die Eintragstabelle gejoint werden:
SELECT DISTINCT feeds.url, feeds.id, feeds.name FROM feeds JOIN entries ON (entries.feed = feeds.id) WHERE entries.read = 0;
Das Filtern kann dann entfallen. Und so schrumpften die 0,5s noch einmal, auf diesmal 0,033s.
Das ist vielleicht etwas spezifisch. Der Kern hier ist: Unnötige Datenbankabfragen durch einen geschickten Join zu vermeiden ist eine gute Idee, und es auch wert wenn es etwas gegen die vorhandene Struktur geht.
Schlusswort
Natürlich sind das keine konstanten Werte. Wären mehr Feeds und mehr Einträge in der Datenbank bräuchte alles länger. Der Server hat wahrscheinlich eine langsamere Platte als mein Rechner mit seiner SSD. Außerdem ist nun nur die Wartezeit auf den Server minimiert, er muss immer noch danach etwaige Bilder herunterladen, außerdem kommt die Transportzeit zum Server dazu.
Trotzdem: Der Unterschied ist auch auf dem Server da. Statt "Klick, Warten, Rendern, da" ist es nun "Klick, Rendern, da" - den Schritt auf realistisch gesehen etwa unter eine Sekunde Ladezeit, von vorher über 3, spürt man deutlich.
feedtragón 0.1
Saturday, 15. November 2014
Ich habe etwas getan, was ich noch nie getan habe: Ein Release auf Github zu taggen.
Aber bei feedtragón erschien es jetzt wie der richtige Zeitpunkt. Ich hatte am Anfang eine Liste von Features im Kopf, die er können muss, um für mich den Digg Reader zu ersetzen - und die ist jetzt abgehakt.
Er kann:
- Feeds updaten und neue Artikel sofort anzeigen
- Artikel automatisch als gelesen markieren
- Feeds abonnieren und opml-Dateien importieren
- Abonnements kündigen
- Google Reader Shortcuts (j,k,n,p,v,m)
Es ist natürlich nur eine Alpha, keine 1.0. Es fehlen noch Funktionen wie das Exportieren von Feeds, die ich bisher nicht brauchte. Aber auch das Aufdröseln von relativen Links und das Markieren von Artikeln, die dann per RSS-Feed ausgegeben werden.
Aber ab jetzt ist der Feedreader langsam in einem Zustand, in dem auch andere mit ihm zufrieden sein könnten. Und bei mir Digg Reader mit einer selbstgehosteten Lösung ersetzt.
feedtragón
Monday, 13. October 2014
Ich habe heute meinen Feedreader auf github gepusht. Frische Alpha-Software, das momentane Ziel ist, ihn zu einem vollwertigen Digg-Reader Ersatz zu machen, meinen jetzigen Feedreader. Also bin ich absolut noch nicht fertig.
Aber er kann schon ein bisschen was. Feeds können abonniert werden, Einträge werden angezeigt und beim Scrollen als gelesen markiert, Blog-Updates kommen an.
Je nachdem wo man anfängt hat dieser Feedreader eine lange Geschichte oder ist zwei Tage alt. Vor einigen Jahren stolperte ich über pubsubhubbub und wollte das für Serendipity. Ich war und bin kein Gläubiger des damaligen realtime-Hypes, glaubte aber doch und immer noch, dass einige nette Dinge mit solchen Mechanismen umgesetzt werden können. Und es war einfach völlig einleuchtend für mich, wieviel effektiver es sein muss, wenn Blogs bei Updates den Reader anpingen und nicht der Reader alle 5 Minuten den Feed herunterladen muss.
Wie auch immer, ich scheiterte damals daran das ordentlich umzusetzen, aber es blieb in meinem Hinterkopf.
Ein ganzes Stück später versuchte ich mich nochmal daran, zweimal sogar, und diesmal funktionierte es. Zum einen implementierte ich es in dsnblog auf Blogseite. Und - wichtiger noch - daraufhin in rsspusher als Hubclient. Was fehlte war das User-Frontend, eben der Feedreader den ich nun gebaut habe.
Entsprechend der Hintergrundgeschichte hat feedtragón eine Besonderheit: Im Gegensatz zu tt-rss und anderen selbstgehosteten Readern betreibt er kein Polling. Updates der abonnierten Blogs werden zum Reader gepusht, dieser soll sie nur anzeigen und den externen Dienst notdürftig verwalten.
Das erspart, mit einem poll-daemon den Server unnötig zu belasten, für den kleinen Preis der Erreichbarkeit von außen. Derzeit übernimmt superfeedr diese Aufgabe, 10000 kostenlose Benachrichtigungen sollten völlig ausreichen und die Rack-Middleware funktioniert hervorragend. Sollte das doch mal ein Problem werden, steht mit rsspusher eine (solange viel zu arbeitsintensive) Alternative bereit.
Ich werde den Reader auf jeden Fall für mich vervollständigen. Wer helfen will sei eingeladen.