Haber akışı (news feed)
Problem
"Twitter, Instagram ya da LinkedIn benzeri bir sosyal ağ için ana sayfa akışını tasarla. Kullanıcı takip ettiği kişilerin gönderilerini görür; yeni gönderiler takipçilerin akışına kısa sürede düşmelidir. Akış sayfalanabilir ve sıralanabilir olmalı."
Bu sorunun kalbi tek bir karardır: bir gönderi yazılırken mi takipçilerin akışına dağıtılacak (fan-out on write), yoksa akış okunurken mi takip edilen kişilerin gönderilerinden toplanacak (fan-out on read)? Mülakatçı bu kararı sayılarla vermeni, iki uçta da çıkan problemi (yazma patlaması ve okuma maliyeti) görmeni ve 50 milyon takipçili bir hesabın ("celebrity problem") karışık bir model gerektirdiğini bulmanı bekler. Martin Kleppmann'ın DDIA kitabının ilk bölümünde Twitter'ın ana sayfa zaman çizelgesi tam da bu trade-off'un örneği olarak anlatılır.
Netleştirme soruları ve varsayımlar
| Soru | Neden önemli | Varsayım |
|---|---|---|
| Takip ilişkisi tek yönlü mü (Twitter), çift yönlü mü (Facebook arkadaşlık)? | Takipçi sayısı dağılımı | Tek yönlü; takipçi sayısı çok çarpık (birkaç hesapta on milyonlar) |
| Kaç aktif kullanıcı, akışı ne sıklıkla açıyorlar? | Okuma yükü | 200 milyon DAU, kişi başı günde 10 akış açma |
| Kaç gönderi atılıyor? | Yazma ve fan-out yükü | Günde 50 milyon gönderi |
| Akış kronolojik mi, sıralı (ranked) mı? | Okuma yolunun karmaşıklığı | Önce kronolojik; sonra sıralama katmanı eklenecek |
| Yeni gönderi takipçilere ne kadar sürede düşmeli? | Fan-out gecikmesi | Çoğu kullanıcı için saniyeler; dev hesaplarda biraz gecikme kabul |
| Gönderi içeriği: metin, görsel, video? | Depolama ve CDN | Metin + medya; medya ayrı depoda ve CDN'de |
| Kullanıcı kendi gönderisini akışında hemen görmeli mi? | Read-your-writes | Evet |
| Silinen veya düzenlenen gönderi? Engellenen kullanıcı? | Akıştan çıkarma stratejisi | Silinen gönderi akışta görünmemeli |
Kapsam dışı: arama, öneri ("tanıyor olabileceğin kişiler"), reklam yerleştirme, bildirimler, mesajlaşma.
Kapasite tahmini
Okuma:
200M DAU x 10 akış açma = 2 milyar akış okuması/gün
2.000.000.000 / 86.400 ≈ 23.000 /s ortalama, tepe (x3) ≈ 70.000 /s
Her okuma 20 gönderilik sayfa -> tepede ~1,4 milyon gönderi hidrasyonu /s (cache'ten)
Yazma:
50M gönderi/gün ≈ 580 /s ortalama, tepe ≈ 1.700 /s
Fan-out on write maliyeti:
gönderi atanların ortalama takipçisi 300 varsayımıyla
50M x 300 = 15 milyar timeline eklemesi/gün ≈ 174.000 /s ortalama, tepe ≈ 520.000 /s
Dev hesap:
50M takipçili bir hesabın tek gönderisi = 50M ekleme
fan-out kapasitesi 500.000 ekleme/s ise -> 100 saniye, ve bu süre boyunca diğer gönderiler bekler
Timeline cache (kullanıcı başına son 500 gönderi id'si), Redis 7.0'da ölçüldü:
sorted set (500 eleman, skiplist kodlaması) ≈ 57 KB/kullanıcı -> 200M x 57 KB ≈ 11,4 TB
list (500 id, quicklist) ≈ 5,2 KB/kullanıcı -> 200M x 5,2 KB ≈ 1,05 TB
Gönderi depolama:
50M x ~1 KB (metin + meta, medya hariç) ≈ 50 GB/gün ≈ 18 TB/yıl
Bu hesap mülakatın yönünü belirler:
- Okuma yazmanın yaklaşık 40 katı (23.000 akış okuması / 580 gönderi). Okumayı ucuzlatan her şey değerlidir; bu, fan-out on write'ı destekler.
- Fan-out on write, yazmayı 300 kat çoğaltır ve dev hesaplarda tek bir gönderi dakikalar süren bir iş haline gelir. Saf push modeli bu yüzden yetmez.
- Veri yapısı seçimi bellek faturasını 10 kat değiştirir. Timeline'ı sorted set yerine kompakt bir listede tutmak 11 TB ile 1 TB arasındaki farktır.
API
POST /api/v1/posts
Idempotency-Key: ...
{ "text": "...", "mediaIds": ["m_123"] }
-> 201 { "postId": "1849203948123451392", "createdAt": "..." }
GET /api/v1/feed?limit=20&cursor=1849203948123451392
-> 200 { "items": [ { "postId": "...", "author": {...}, "text": "...", "media": [...],
"likeCount": 1204, "likedByMe": false } ],
"nextCursor": "1849203801111111111" }
DELETE /api/v1/posts/{postId}
POST /api/v1/users/{userId}/follow
DELETE /api/v1/users/{userId}/follow
- Cursor tabanlı sayfalama.
OFFSETile sayfalama, kullanıcı sayfayı kaydırırken yeni gönderiler geldiğinde aynı gönderiyi iki kez ya da hiç göstermemeye yol açar. Cursor, son görülen gönderinin id'sidir; zamana göre sıralı (Snowflake benzeri) id'ler bunu doğal kılar. - Akış cevabı hidrate edilmiş gönderiler içerir (yazar adı, avatar, beğeni sayısı); istemci 20 gönderi için 20 ayrı istek atmaz.
Veri modeli
-- Gönderiler: yazar bazında, zamana göre sıralı (Cassandra / CQL)
CREATE TABLE posts_by_author (
author_id bigint,
post_id bigint, -- Snowflake benzeri, zamana göre sıralı
text text,
media_ids list<text>,
status text, -- ACTIVE, DELETED
PRIMARY KEY ((author_id), post_id)
) WITH CLUSTERING ORDER BY (post_id DESC);
-- Gönderiyi id ile okumak için (hidrasyon)
CREATE TABLE posts_by_id (
post_id bigint PRIMARY KEY,
author_id bigint,
text text,
media_ids list<text>,
status text
);
-- Sosyal graf: iki yönde de sorgulanır
CREATE TABLE followers (followee_id bigint, follower_id bigint, created_at timestamp,
PRIMARY KEY ((followee_id), follower_id));
CREATE TABLE following (follower_id bigint, followee_id bigint, created_at timestamp,
PRIMARY KEY ((follower_id), followee_id));
- Timeline cache (Redis):
tl:{userId}→ en yeni başta olacak şekilde gönderi id'lerinin listesi, en fazla 500 eleman (LPUSH+LTRIM). Sorted set'e göre yaklaşık 10 kat az bellek; bedeli, araya sıralı ekleme yapılamaması (ekleme her zaman başa). Gönderi id'leri zamana göre artan olduğu için bu kabul edilebilir; yine de farklı yazarların fan-out'u paralel işlendiğinden liste sadece yaklaşık sıralıdır, okuma tarafı adayları id'ye göre yeniden sıralar. - Gönderi cache'i:
post:{postId}→ gönderi gövdesi; hidrasyonMGETile toplu yapılır. - Sayaçlar: Beğeni ve yorum sayıları ayrı bir sayaç servisinde; sıcak gönderilerde sayaç tek satırda değil, parçalara bölünmüş (sharded counter) tutulur ve periyodik toplanır.
- Partition çarpıklığı:
followerstablosunda dev bir hesabın partition'ı 50 milyon satır olur. Bu partition zaman veya hash ile alt kovalara bölünür ((followee_id, bucket)), aksi halde tek bir node'u ısıtır ve partition boyutu sınırlarını zorlar.
Yüksek seviye tasarım
flowchart LR
U[İstemci] --> GW[API gateway]
GW --> PS[Gönderi servisi]
GW --> FS[Akış servisi]
PS --> PDB[(Gönderi deposu)]
PS --> PC[(Gönderi cache'i)]
PS -->|PostCreated| K[(Kafka)]
K --> FO[Fan-out worker'ları]
FO --> GS[(Sosyal graf)]
FO --> TC[(Timeline cache)]
FS --> TC
FS --> CEL[Dev hesap gönderileri cache'i]
FS --> PC
FS --> CNT[Sayaç servisi]
FS --> RK[Sıralama servisi]
MEDIA[Medya servisi] --> CDN[CDN]
U --> CDNYazma yolu: Gönderi servisi gönderiyi depoya ve cache'e yazar, outbox üzerinden PostCreated event'i yayınlar ve kullanıcıya hemen 201 döner. Fan-out worker'ları event'i tüketir, yazarın takipçilerini sayfalar halinde okur ve her takipçinin timeline listesinin başına gönderi id'sini ekler.
Okuma yolu: Akış servisi kullanıcının timeline listesinden cursor'dan sonraki id'leri alır, takip ettiği dev hesapların son gönderileriyle birleştirir, silinmiş/engellenmiş olanları ayıklar, gönderileri ve sayaçları toplu okuyarak hidrate eder, sıralama uygular ve sayfayı döner.
Derinlemesine
Fan-out on write, fan-out on read ve karışık model
| Fan-out on write (push) | Fan-out on read (pull) | |
|---|---|---|
| Gönderi atılınca | Her takipçinin timeline'ına id eklenir | Sadece yazarın gönderi listesine eklenir |
| Akış okununca | Hazır listeden 20 id okunur | Takip edilen her kişinin son gönderileri okunup birleştirilir |
| Okuma maliyeti | Çok düşük, sabit | Takip sayısıyla orantılı (300 takip = 300 okuma + k-yollu birleştirme) |
| Yazma maliyeti | Takipçi sayısıyla orantılı | Sabit |
| Dev hesap | Tek gönderi milyonlarca yazma | Sorun yok |
| Pasif kullanıcı | Hiç okunmayacak timeline'lara boşa yazma | Maliyet yok |
| Gecikme | Fan-out bitene kadar takipçi görmez | Okuma anında güncel |
Okuma yazmanın 40 katı olduğu için varsayılan model push'tur. Ama iki istisna vardır:
- Dev hesaplar: Takipçi sayısı bir eşiğin (ör. 100.000) üstündeyse gönderi fan-out edilmez. Bu hesapların son gönderileri ayrı bir "dev hesap gönderileri" cache'inde tutulur ve okuma anında, kullanıcının takip ettiği dev hesapların listesiyle birleştirilir. Ortalama bir kullanıcı birkaç ila birkaç düzine dev hesap takip ettiği için okuma tarafındaki ek maliyet sınırlıdır.
- Pasif kullanıcılar: Son 30 gündür giriş yapmamış takipçilere fan-out yapılmaz. Bu kullanıcı geri döndüğünde timeline'ı pull modeliyle bir kez yeniden kurulur (takip ettiklerinin son gönderilerinden) ve sonra push moduna döner.
Bu karışık modelde okuma yolu şöyle görünür:
feed(user, cursor):
ids_push = timeline_cache.range(user, after=cursor, limit=60)
celebs = following_celebrities(user) # cache'li, küçük liste
ids_pull = merge( celebrity_posts_cache.range(c, after=cursor, limit=20) for c in celebs )
candidates = merge_by_post_id_desc(ids_push, ids_pull)
candidates = filter(candidates, deleted, blocked, muted)
page = rank_or_take(candidates, 20)
return hydrate(page), next_cursor(page)
limit=60 gibi fazladan aday çekmek, filtreleme ve sıralama sonrası 20 gönderi kalmasını garanti etmek içindir.
Fan-out'un işletilmesi
- Kafka partition anahtarı
authorId: Aynı yazarın gönderileri sırayla fan-out edilir. Dev hesaplar zaten fan-out dışında olduğu için tek bir partition'ın ısınması sınırlanır. - Takipçi listesini sayfalı okumak: 80.000 takipçili bir yazar için takipçiler 1.000'lik sayfalar halinde okunur ve her sayfa ayrı bir alt iş (Kafka mesajı) olur; iş paralel worker'lara yayılır ve bir worker çökerse sadece o sayfa tekrar işlenir.
- Pipelining: 1.000 takipçinin timeline'ına yazmak Redis'e 1.000
LPUSH+LTRIMdemektir; bunlar pipeline'lanır ve hash tag olmadan farklı node'lara dağılır. - Idempotency: Aynı event tekrar işlenirse aynı id ikinci kez listeye eklenebilir. Okuma tarafında id bazında tekilleştirme yapmak, yazma tarafında "listede var mı" kontrolünden ucuzdur.
- Öncelik: Fan-out kuyruğu geride kalırsa yeni gönderiler takipçilere geç düşer. Lag metriği ve otomatik ölçekleme; aşırı durumda pasif kullanıcılara fan-out tamamen durdurulur (degradation).
Timeline cache'in ömrü ve yeniden kurulması
- Timeline listesi en fazla 500 id tutar; daha eski sayfalar istenirse (nadir) pull modeliyle depodan üretilir.
- Redis bir timeline'ı kaybederse (eviction, node kaybı) akış servisi pull modeliyle yeniden kurar ve cache'e yazar. Yani timeline cache türetilmiş veridir; doğruluk kaynağı gönderi deposu ve sosyal graftır. Bu, Redis'te kalıcılığın şart olmamasını sağlar ama toplu bir kayıp sonrası "yeniden kurma fırtınası" riskini doğurur: aynı anda milyonlarca kullanıcının timeline'ı pull ile kurulmaya çalışılır. Önlem: yeniden kurmayı kullanıcı başına tekil (single-flight) yapmak ve bir eşik üstünde geçici olarak sadece dev hesap + takip edilen ilk N kişiyle kısmi akış göstermek.
- Bellek: 200 milyon kullanıcı x 5,2 KB ≈ 1 TB. Pasif kullanıcıların timeline'ı tutulmadığında (ör. aktif 120 milyon) bu ≈ 630 GB'a iner; 64 GB'lık node'larda replikalarla birlikte birkaç düzine node.
Read-your-writes: kendi gönderini hemen görmek
Fan-out asenkron olduğu için kullanıcı gönderi atıp hemen akışını yenilediğinde gönderisini göremeyebilir. İki çözüm: (1) gönderi servisi yazarın kendi timeline'ına senkron ekleme yapar; (2) akış servisi okuma sırasında kullanıcının son birkaç dakikadaki kendi gönderilerini birleştirir. İkincisi fan-out hattına bağımlılığı tamamen kaldırır.
Silme, düzenleme, takipten çıkma ve engelleme
Silinen bir gönderiyi 50 milyon timeline'dan çıkarmak, onu eklemek kadar pahalıdır. Bunun yerine okuma anında filtreleme yapılır: hidrasyon sırasında gönderinin status alanı DELETED ise atlanır. Gönderi cache'i silmede hemen güncellenir (commit sonrası invalidation). Düzenleme de aynıdır: timeline'da sadece id durur, içerik hidrasyonda güncel okunur.
Takipten çıkma ve engelleme de aynı mantıkla okuma anında filtrelenir (kullanıcının takip/engel listesi cache'lidir), arka planda ise bir temizlik işi timeline'dan ilgili id'leri asenkron temizler.
Sıralama (ranked feed)
Kronolojik akıştan sıralı akışa geçiş iki aşamalı bir yapıdır:
- Aday üretimi: Timeline listesi + dev hesap gönderileri + (istenirse) öneri kaynakları; birkaç yüz aday.
- Sıralama: Her aday için özellikler (yazarla etkileşim geçmişi, gönderinin yaşı, beğeni hızı, içerik tipi) toplanır ve bir model puan verir. Özellik okuma maliyeti yüzünden özellikler önceden hesaplanıp bir feature store'da tutulur.
Gecikme bütçesi önemlidir: akış isteği için toplam 200 ms varsa sıralamaya 50-80 ms kalır. Sıralanmış sonuç kısa süreli (ör. oturum boyunca) cache'lenir; kullanıcı aşağı kaydırdıkça aynı sıralı listeden sayfalar verilir, her sayfada yeniden sıralanmaz (aksi halde gönderiler sayfalar arasında zıplar). Mülakatta model detayına girmek beklenmez; iki aşamalı yapıyı, gecikme bütçesini ve sıralamanın cache'lenmesini söylemek yeterlidir.
Sayaçlar ve hidrasyon
Bir akış sayfası 20 gönderi, 20 yazar ve 20 sayaç demektir. Hidrasyon tek tek değil toplu yapılır (MGET ve çoklu anahtar okuma); yazar profilleri de ayrı bir cache'tedir. Viral bir gönderinin beğeni sayacı saniyede binlerce kez artar: tek bir satır ya da tek bir Redis anahtarı sıcaklaşır. Sayaç N alt anahtara bölünür (likes:{postId}:0..15), artırma rastgele bir alt anahtara gider, okuma kısa TTL'li toplanmış bir değerden yapılır. Beğeni sayısının birkaç saniye geride olması kabul edilir.
Medya
Görsel ve videolar akış servisinden geçmez: istemci medya servisinden imzalı bir yükleme URL'i alır, dosyayı nesne deposuna yükler, gönderide sadece mediaId vardır. Akış cevabındaki medya URL'leri CDN'i gösterir. Akış servisinin bant genişliği bu sayede küçük kalır.
Darboğazlar, hatalar ve trade-off'lar
| Senaryo | Etki | Önlem |
|---|---|---|
| Dev hesap gönderi attı | Milyonlarca fan-out yazması, kuyruk gecikmesi | Eşik üstü hesaplar fan-out dışı; okuma anında birleştirme |
| Fan-out kuyruğu geride | Takipçiler yeni gönderileri geç görür | Lag alarmı, ölçekleme, pasif kullanıcıları atlama |
| Timeline cache node'u kaybolur | Etkilenen kullanıcıların akışı boş | Pull ile yeniden kurma, single-flight, kısmi akış |
followers partition'ı çok büyük | Tek node ısınır, okuma yavaş | (followee_id, bucket) ile alt kovalara bölme |
| Viral gönderinin sayacı | Sıcak anahtar | Parçalı sayaç, toplanmış değeri cache'lemek |
| Silinen gönderi akışta | İçerik/yasal risk | Hidrasyonda status filtresi, cache invalidation |
| Sıralama servisi yavaşlar | Akış gecikmesi | Zaman aşımında kronolojik akışa düşmek (graceful degradation) |
Trade-off'lar:
- Push ve pull: Okuma ucuzluğu için yazma çoğaltmasını kabul ettik; dev hesaplar ve pasif kullanıcılar için pull'a döndük.
- Kompakt liste ve sorted set: 10 kat bellek tasarrufu için sadece başa ekleme ve id sırasına dayandık.
- Okuma anında filtreleme: Silme ve takipten çıkma ucuz; bedeli her okumada filtreleme ve fazladan aday çekme.
- Eventual consistency: Takipçiler gönderiyi saniyeler (dev hesapta biraz daha) sonra görür; yazar kendi gönderisini hemen görür.
Üç seviyede cevap
Soru: "Fan-out on write mı, fan-out on read mi? Neden?"
seviye: Orta etiketler: [fan-out, feed] kaynak: Tahmini
Fan-out on write seçerim: gönderi atılınca bütün takipçilerin akışına eklerim, okuma çok hızlı olur.
Okumalar yazmalardan çok daha fazla olduğu için fan-out on write mantıklı; akış hazır bir listeden okunur. Ama milyonlarca takipçisi olan hesaplarda tek gönderi milyonlarca yazma demek. Bu hesaplar için fan-out yapmam, onların gönderilerini okuma anında akışa eklerim. Yani karışık model.
Kararı sayılarla veririm. Günde 2 milyar akış okuması ve 50 milyon gönderi var, okuma yazmanın yaklaşık 40 katı; bu push'u destekliyor. Ama ortalama 300 takipçiyle push günde 15 milyar timeline yazması, tepede saniyede yarım milyon demek; 50 milyon takipçili bir hesabın tek gönderisi ise 500 bin/s kapasiteyle 100 saniye sürer ve bu sürede diğer gönderiler kuyrukta bekler. Bu yüzden karışık model: varsayılan push, takipçisi 100 binin üstündeki hesaplar fan-out dışı, onların son gönderileri ayrı bir cache'te ve okuma anında kullanıcının takip ettiği dev hesap listesiyle birleştiriliyor. İkinci istisna pasif kullanıcılar: 30 gündür girmemiş takipçilere yazmam, geri döndüklerinde timeline'ı pull ile bir kez kurarım. Eşik sabit değil, fan-out kuyruğunun lag'ine ve okuma tarafındaki birleştirme maliyetine bakarak ayarlanan bir parametre.
Junior bir modeli seçiyor ama bedelini görmüyor. Mid celebrity problemini ve karışık modeli biliyor; bu, sorunun beklenen cevabı. Senior kararı oranlarla gerekçelendiriyor, dev hesabın etkisini süre olarak hesaplıyor, pasif kullanıcıları ikinci bir optimizasyon olarak ekliyor ve eşiği ölçümle ayarlanan bir parametre olarak görüyor. Fark, ezber "hybrid" kelimesi ile modelin neden ve nerede kırıldığını sayıyla göstermek arasında.
Takip sorusu: Bir hesap 99.000 takipçideyken eşiği geçip 101.000'e çıktı. Geçiş anında takipçilerinin akışında ne olur, nasıl yönetirsin?
Soru: "Timeline cache'i nasıl tutarsın? Bellek ne kadar tutar?"
seviye: İleri etiketler: [redis, capacity-estimation] kaynak: Tahmini
Redis'te her kullanıcı için bir sorted set tutarım, skor zaman damgası olur. Redis hızlı olduğu için sorun olmaz.
Her kullanıcı için son birkaç yüz gönderinin id'sini tutarım, gönderinin kendisini değil. Gönderi içeriği ayrı bir cache'ten toplu okunur. Kullanıcı başına 500 id, 8 byte'tan 4 KB eder; 200 milyon kullanıcı için 800 GB civarı, bir Redis Cluster'a sığar.
Sadece id tutarım, içerik hidrasyonda okunur; böylece düzenleme ve silme timeline'a dokunmadan yansır. Veri yapısını bellek üzerinden seçerim: Redis 7'de 500 elemanlı bir sorted set skiplist kodlamasına geçtiği için kullanıcı başına yaklaşık 57 KB tuttu, aynı 500 id bir listede yaklaşık 5 KB. 200 milyon kullanıcıda bu 11 TB ile 1 TB arasındaki fark. Gönderi id'leri zamana göre artan olduğu için ekleme hep başa yapılıyor ve LPUSH + LTRIM yeterli; sorted set'in esnekliğine gerek yok. Pasif kullanıcıların timeline'ını tutmayarak bunu daha da düşürürüm. Timeline türetilmiş veri: kaybolursa pull ile yeniden kurulur, bu yüzden Redis'te kalıcılık şart değil ama toplu kayıpta yeniden kurma fırtınasına karşı single-flight ve kısmi akış gerekir.
Junior veri yapısını alışkanlıkla seçiyor ve maliyeti hesaplamıyor. Mid id ile içeriği ayırıyor ve bir hesap yapıyor, ama veri yapısını söylemeden hesap eksik kalıyor (ham 8 byte'lık hesap listede gerçeğe yakın çıkar, ~5 KB; sorted set'te ise gerçek bellek ~57 KB, yani 14 kat fazla). Senior ölçülmüş sayılarla iki yapıyı karşılaştırıyor, seçimi id'lerin sıralı olmasına dayandırıyor ve cache'in türetilmiş veri olmasının operasyonel sonuçlarını düşünüyor.
Takip sorusu: Sıralı (ranked) akışa geçtik; gönderiler artık zamana göre değil puana göre sıralanıyor. Timeline cache yapısı değişmeli mi?
Soru: "Bir gönderi silindiğinde onu milyonlarca akıştan nasıl kaldırırsın?"
seviye: İleri etiketler: [fan-out, consistency] kaynak: Tahmini
Gönderiyi silerken, yazarın bütün takipçilerinin timeline'ından da siliyorum.
Milyonlarca timeline'dan silmek eklemek kadar pahalı. Timeline'da sadece id olduğu için gönderiyi depoda DELETED işaretlerim, akış okunurken hidrasyon sırasında silinmiş gönderileri atlarım. Gönderi cache'ini de silme anında güncellerim.
Okuma anında filtrelerim: gönderi depoda DELETED olur, gönderi cache'i commit sonrası güncellenir, hidrasyon silinmişleri atlar. Filtreleme yüzünden sayfa eksik kalmasın diye akış servisi sayfa boyutunun birkaç katı aday çeker. Ama burada bir uyumluluk boyutu var: yasal bir kaldırma talebinde içerik sadece görünmez olmamalı, saklandığı her yerden belirli bir süre içinde silinmeli; bu yüzden gönderi gövdesini depodan ve cache'ten fiziksel olarak silerim, timeline'lardaki id'ler zararsız artıklar olarak kalır ve listenin 500 sınırıyla doğal olarak düşer. Takipten çıkma ve engellemeyi de aynı şekilde okuma anında filtreleyip arka planda asenkron temizlerim. Medya varsa CDN cache'ini de geçersiz kılmayı unutmam.
Junior fan-out maliyetini silmede tekrar ödüyor. Mid okuma anında filtrelemeyi buluyor. Senior filtrelemenin sayfa boyutuna etkisini (fazladan aday), yasal silme gerekliliğini, id artıklarının doğal temizliğini ve CDN'i ekliyor; yani tasarımı ürün ve uyumluluk gereksinimleriyle birlikte düşünüyor.
Takip sorusu: Kullanıcı birini engelledi; engellenen kişinin gönderileri hangi gecikmeyle akıştan kaybolur ve bunu nasıl garanti edersin?
Mülakatçı için
- Takip ilişkisinin yönünü, okuma/yazma oranını ve takipçi dağılımının çarpıklığını soruyor.
- Fan-out on write ve on read'i okuma/yazma maliyetiyle karşılaştırıyor ve kararı kapasite hesabına dayandırıyor.
- Dev hesap problemini kendiliğinden ya da küçük bir ipucuyla buluyor ve karışık model öneriyor.
- Timeline'da içerik değil id tutuyor; hidrasyonu toplu yapıyor.
- Timeline cache'in bellek maliyetini hesaplıyor ve veri yapısı seçimini buna göre yapıyor.
- Cursor tabanlı sayfalamayı gerekçesiyle seçiyor.
- Silme, takipten çıkma ve engellemeyi okuma anında filtreleme ile ele alıyor.
- Read-your-writes problemini görüp çözüyor.
- Timeline cache'i türetilmiş veri olarak görüp kayıp durumunda yeniden kurma stratejisi veriyor.
- Sıcak sayaçlar, büyük partition'lar veya sıralama servisinin yavaşlaması gibi en az bir darboğazı ele alıyor.
Takip soruları:
- Kullanıcı yeni birini takip etti. O kişinin eski gönderileri akışa ne zaman ve nasıl girer?
- Akışa reklam ekleyeceğiz: her 5 gönderide bir. Sayfalama ve cache nasıl etkilenir?
- İki bölgeli bir kurulumda fan-out nerede yapılır?
- Sıralama modeli değişti; eski ve yeni modelin etkisini nasıl karşılaştırırsın?
Seviye beklentisi: Mid seviyede push/pull karşılaştırması, celebrity problemi, id tabanlı timeline ve cursor sayfalama beklenir. Senior'dan sayılarla karar, bellek hesabı, okuma anında filtreleme, cache'in yeniden kurulması ve sıralama katmanının gecikme bütçesi beklenir. Staff seviyesinde aday eşik geçişlerini, çok bölgeyi ve deney altyapısını tartışabilir.
Hızlı tekrar
S: Fan-out on write'ın en büyük avantajı ve dezavantajı nedir?
C: Akış okuması hazır listeden, ucuz ve sabit maliyetli. Dezavantajı yazma çoğaltması: takipçi sayısı kadar yazma; dev hesaplarda milyonlarca.
S: Celebrity problemi karışık modelde nasıl çözülür?
C: Eşik üstü takipçili hesapların gönderileri fan-out edilmez; ayrı bir cache'ten okuma anında, kullanıcının takip ettiği dev hesap listesiyle birleştirilir.
S: Pasif kullanıcılara neden fan-out yapılmaz?
C: Hiç okunmayacak timeline'lara yazmak israf. Kullanıcı geri dönünce timeline pull modeliyle bir kez yeniden kurulur.
S: Timeline'da neden gönderi içeriği değil id tutulur?
C: Bellek tasarrufu ve düzenleme/silmenin timeline'a dokunmadan yansıması; içerik hidrasyonda güncel okunur.
S: 500 elemanlı timeline Redis'te sorted set ve list olarak ne kadar tutar?
C: Redis 7.0 ölçümünde sorted set (skiplist) ~57 KB, list ~5,2 KB. 200 milyon kullanıcıda ~11 TB ile ~1 TB.
S: Akışta neden cursor tabanlı sayfalama kullanılır?
C: Yeni gönderiler geldikçe OFFSET kayar; aynı gönderi iki kez görünür ya da atlanır. Son görülen gönderi id'si cursor olur.
S: Silinen gönderi milyonlarca akıştan nasıl kalkar?
C: Okuma anında: depoda DELETED işaretlenir, cache güncellenir, hidrasyon silinmişleri atlar; fazladan aday çekilerek sayfa doldurulur.
S: Kullanıcının kendi gönderisini hemen görmesi nasıl sağlanır?
C: Yazarın timeline'ına senkron ekleme ya da akış okunurken kullanıcının son birkaç dakikadaki kendi gönderilerini birleştirme.
Kaynaklar
- Martin Kleppmann, Designing Data-Intensive Applications, Bölüm 1: Twitter ana sayfa zaman çizelgesi örneği üzerinden yük parametreleri ve fan-out trade-off'u.
- Alex Xu, System Design Interview: An Insider's Guide (Cilt 1): "Design a News Feed System" bölümü.
- Redis: Key eviction: Cache olarak kullanılan Redis'te bellek politikaları.
- Redis Cluster specification: Anahtarların slot'lara dağılımı ve asenkron replikasyon.
- Discord: How Discord Stores Billions of Messages: Büyük partition problemi ve zaman kovalarıyla bölme (takipçi tablolarındaki çarpıklık için de geçerli fikir).