Der Home-Feed ist in jedem Social-Produkt die meistgelesene Seite und eine der teuersten. Jeder Nutzer folgt Hunderten Accounts, jeder Account postet in seinem eigenen Tempo, und der Feed muss jedes Mal, wenn jemand die App öffnet, gerankt in unter ein paar hundert Millisekunden zurückkommen.
Die zentrale Designfrage hat zwei klassische Antworten, Fan-out beim Schreiben und Fan-out beim Lesen, und jeder große Feed landet bei einer Mischung aus beiden. Dieser Beitrag vergleicht sie mit Zahlen, zeigt den Hybrid und endet mit einem Diagramm des vollständigen Lese- und Schreibpfads.
Anforderungen
- Ein Nutzer sieht neue Beiträge der Accounts, denen er folgt, gerankt oder chronologisch.
- Das Öffnen des Feeds dauert im 99. Perzentil deutlich unter einer Sekunde.
- Ein neuer Beitrag erscheint innerhalb von Sekunden in den Feeds der Follower.
- Endloses Scrollen ohne Duplikate oder Lücken.
- Es funktioniert weiter, wenn ein Account zig Millionen Follower hat.
Lesezugriffe dominieren. Twitter berichtete 2013, dass es etwa 300.000 Timeline-Lesezugriffe pro Sekunde bediente, bei rund 6.000 Tweet-Schreibvorgängen pro Sekunde, laut dieser Beschreibung seiner Architektur. Entwirf zuerst für das Lesen.
Überschlagsrechnung
| Annahme | Wert |
|---|---|
| Täglich aktive Nutzer | 10 Millionen |
| Feed-Aufrufe pro Nutzer und Tag | 10, also im Schnitt etwa 1.160 Lesezugriffe/s |
| Neue Beiträge pro Tag | 1 Million, etwa 12 Beiträge/s |
| Durchschnittliche Follower pro Autor | 200, also etwa 2.300 Fan-out-Schreibvorgänge/s |
| Größe des gecachten Feeds | 800 Post-IDs zu je 8 Byte, etwa 64 GB für alle Nutzer |
Die Durchschnitte wirken harmlos. Die Ausreißer nicht: Ein Autor mit 20 Millionen Followern erzeugt mit einem einzigen Beitrag 20 Millionen Fan-out-Schreibvorgänge, ein Zehntel der täglichen Fan-out-Arbeit aller anderen. Genau mit diesem Ausreißer muss das folgende Design umgehen.
Fan-out beim Schreiben (Push)
Wenn jemand postet, sucht ein Hintergrundjob dessen Follower heraus und fügt die Post-ID oben in den gecachten Feed jedes Followers ein. Den Feed zu lesen ist dann ein einziger Lookup: die neuesten IDs aus der eigenen Liste nehmen und die Beiträge holen.
- Stärke: Lesezugriffe sind schnell und vorhersehbar.
- Kosten: Ein Beitrag erzeugt so viele Schreibvorgänge, wie der Autor Follower hat. Ein Account mit 30 Millionen Followern macht aus einem Beitrag 30 Millionen Cache-Schreibvorgänge, und diese Follower sehen ihn spät. Dieselbe Beschreibung erwähnt, dass Beiträge der größten Accounts Minuten brauchten, bis sie alle Follower erreichten.
- Verschwendung: Du schreibst in Feeds von Nutzern, die die App diese Woche gar nicht öffnen.
Halte jeden gecachten Feed kurz. Twitter begrenzte die Home-Timelines damals auf 800 Einträge in Redis; ältere Beiträge kommen aus der Datenbank, wenn jemand so weit scrollt.
Fan-out beim Lesen (Pull)
Tu nichts, wenn jemand postet. Wenn ein Nutzer den Feed öffnet, hol die neuen Beiträge aller Accounts, denen er folgt, führe sie zusammen und ranke sie.
- Stärke: Schreibvorgänge sind günstig, und große Accounts kosten nichts extra.
- Kosten: Jeder Feed-Aufruf berührt Hunderte Autoren. Bei 300.000 Lesezugriffen pro Sekunde sind das sehr viele Abfragen für Arbeit, die sich größtenteils wiederholt.
Der Hybrid
Die meisten großen Feeds kombinieren beides:
- Beiträge gewöhnlicher Accounts in die gecachten Feeds der Follower schieben.
- Für Accounts über einer Follower-Schwelle den Fan-out auslassen und ihre neuen Beiträge beim Lesen holen.
- Beim Lesen den gecachten Feed mit den geholten Beiträgen zusammenführen und dann ranken.
Die Schwelle ist ein Stellrad, kein Gesetz. Setz sie dort, wo die Kosten der Fan-out-Schreibvorgänge die Kosten der zusätzlichen Lesezugriffe zu übersteigen beginnen, und prüf sie neu, wenn sich deine Follower-Verteilung ändert. Nutzer, die keinem großen Account folgen, zahlen nie für den Pull-Pfad.
Ranking
Ein chronologischer Feed sortiert nach Zeit. Ein gerankter Feed arbeitet in drei Schritten: Kandidaten sammeln (die zusammengeführte Liste oben, plus Empfehlungen, falls vorhanden), jeden Kandidaten mit Signalen wie Aktualität, Häufigkeit der Interaktion des Nutzers mit dem Autor und Engagement auf dem Beitrag bewerten, dann filtern (blockierte Accounts, bereits gesehene Beiträge, Beinahe-Duplikate).
Halte das Ranking aus dem Fan-out-Pfad heraus. Fan-out speichert Kandidaten; gerankt wird beim Lesen mit den neuesten Signalen. So braucht eine Änderung am Ranking-Modell kein Backfill der gecachten Feeds.
Löschungen, Blockierungen und private Accounts
Feeds enthalten IDs, also ist das Löschen eines Beitrags ein Schreibvorgang im Post-Speicher; der Feed-Service verwirft IDs, die sich nicht mehr auflösen lassen. Blockierungen und Accounts, die auf privat umstellen, brauchen denselben Filter beim Lesen, denn eine ID aus Millionen gecachter Feeds zu entfernen ist langsam und fehleranfällig.
Paginierung
Paginiere einen Feed nie mit Offsets. Neue Beiträge kommen oben dazu, während der Nutzer scrollt, also verschiebt sich „Seite 2“ und zeigt Duplikate. Gib einen Cursor zurück, der die Position kodiert, etwa die ID oder den Score des zuletzt ausgelieferten Eintrags, und frag nach „Einträgen nach diesem Cursor“. Bei gerankten Feeds speicherst du die gerankte Liste für die Sitzung kurz zwischen, damit spätere Seiten zur ersten passen.
Das Architekturdiagramm
Diagramminhalt
- User
- Post service
- Posts store
- Post events
- Fan-out workers
- Social graph
- Author over 1M followers?
- Feed cache
- Pulled at read time
- Feed service
- Ranking
- Media CDN
- POST /posts
- post.created
- followers
- no: push post ID
- yes
- GET /feed
- cached timeline
- big accounts
- images and video
So liest du es:
- Schreibpfad (oben): Der Post-Service speichert den Beitrag und veröffentlicht ein Event. Fan-out-Worker lesen den Social Graph und prüfen die Follower-Zahl des Autors.
- Die Entscheidung: Bei den meisten Autoren schieben die Worker die Post-ID in den Feed-Cache jedes Followers. Bei Accounts über der Schwelle wird nichts geschoben; ihre Beiträge werden beim Lesen geholt.
- Lesepfad (unten): Der Feed-Service liest den gecachten Feed des Nutzers und neue Beiträge großer Accounts aus dem Post-Speicher, führt sie zusammen und gibt sie ans Ranking.
- Medien laufen über ein CDN und nie über den Feed-Service.
- Der Markdown-Block hält die Faustregeln neben dem Design.
Was ins Diagramm gehört
- Beide Pfade, klar getrennt. Schreiben und Lesen haben unterschiedliche Skalierungsprobleme.
- Die Fan-out-Schwelle. Schreib die Zahl an die Entscheidung, auch wenn sie sich später ändert.
- Was wo gespeichert ist. Feed-Caches halten Post-IDs, keine Beiträge; der Post-Speicher hält die Inhalte.
- Wo gerankt wird. Leser sollten sehen, dass es beim Lesen passiert.
Der Cache in der Mitte dieses Designs verdient ein eigenes Diagramm; sieh dir den Beitrag zu Caching-Strategien an. Der Beitrag zur ereignisgesteuerten Architektur behandelt die Event- und Consumer-Seite, und das Beispiel für ein System-Design-Diagramm zeigt dieselben Konventionen an einem anderen System.
In Archboard nachbauen
- Melde dich mit Google oder GitHub an und leg ein Diagramm an.
- Füge aus dem C4-Modell-Pack eine Person hinzu, dann Container-Nodes für Post-Service, Fan-out-Worker, Feed-Service und Ranking.
- Füge aus dem C4-Modell-Pack Datenbank-Nodes für Post-Speicher und Social Graph hinzu, aus dem Daten-Pack Kafka und Redis für Post-Events und Feed-Caches und aus dem Cloud-Pack ein CDN.
- Füge für die Follower-Schwelle eine Entscheidung und ein Start / Ende aus dem Flussdiagramm-Pack hinzu.
- Zeichne den Schreibpfad oben und den Lesepfad darunter und füge einen Markdown-Block aus dem Basis-Pack mit deinen Faustregeln hinzu.
Das Beispiel nutzt 13 Diagramm-Nodes und bleibt damit im Free-Plan.
FAQ
Ist Fan-out beim Schreiben oder beim Lesen besser? Keins von beiden allein. Fan-out beim Schreiben liefert schnelle Lesezugriffe, explodiert aber bei Accounts mit riesiger Followerschaft; Fan-out beim Lesen ist günstig beim Schreiben, aber langsam beim Lesen. Große Feeds nutzen einen Hybrid.
Was sollte der Feed-Cache speichern? Post-IDs, neueste zuerst, begrenzt auf ein paar hundert pro Nutzer. Hol die Inhalte separat, damit Bearbeitungen und Löschungen nicht jeden Feed neu schreiben müssen.
Wie gehe ich mit einem Beitrag eines Prominenten um? Lass den Fan-out für Accounts über einer Follower-Schwelle aus und hol ihre neuen Beiträge, wenn Follower den Feed öffnen.
Wie sollte die Feed-Paginierung funktionieren? Mit einem Cursor auf Basis des zuletzt ausgelieferten Eintrags, nicht mit Seitenzahlen oder Offsets, damit neue Beiträge beim Scrollen keine Duplikate verursachen.
Wo wird gerankt? Beim Lesen, auf der zusammengeführten Kandidatenliste. Hältst du es aus dem Fan-out heraus, kannst du das Ranking ändern, ohne gecachte Feeds neu aufzubauen.