Instagram Sistem Tasarımı: Fotoğraftan Newsfeed’e Ölçeklenebilir Mimari

Instagram gibi bir uygulamayı tasarlarken mesele sadece fotoğraf yüklemek değildir. Asıl zorluk, kullanıcıların birbirini takip ettiği, gönderilere yorum ve beğeni bıraktığı, milyonlarca kişiye kişiselleştirilmiş akış gösterilen bir sistemi ölçeklenebilir hale getirmektir. Bu yazıda Instagram benzeri bir sistemin temel veri modelini, servis mimarisini ve newsfeed üretme yaklaşımını Mermaid diyagramlarıyla adım adım kuracağız.

Instagram gibi bir uygulamayı tasarlarken mesele sadece fotoğraf yüklemek değildir. Asıl zorluk, kullanıcıların birbirini takip ettiği, gönderilere yorum ve beğeni bıraktığı, milyonlarca kişiye kişiselleştirilmiş akış gösterilen bir sistemi ölçeklenebilir hale getirmektir. Bu yazıda Instagram benzeri bir sistemin temel veri modelini, servis mimarisini ve newsfeed üretme yaklaşımını Mermaid diyagramlarıyla adım adım kuracağız.

Önce Kapsamı Netleştirelim

Bir sistem tasarımı görüşmesinde en kritik hata, ürünü bildiğini varsayıp bütün özellikleri tasarlamaya çalışmaktır. Instagram dediğimizde akla hikayeler, mesajlaşma, keşfet, reels, reklamlar ve daha birçok şey gelir. Ama görüşmede genellikle beklenen şey daha dar ve tasarlanabilir bir çekirdektir.

Bu yazıda dört temel özelliğe odaklanacağız: fotoğraf gönderisi oluşturma ve görüntüleme, gönderilere beğeni ve yorum ekleme, kullanıcı takip sistemi ve kişiselleştirilmiş newsfeed üretimi.

flowchart TD
    A["Instagram Benzeri Sistem"] --> B["Gönderi ve Görsel Yönetimi"]
    A --> C["Beğeni ve Yorumlar"]
    A --> D["Takip İlişkileri"]
    A --> E["Kullanıcı Newsfeed'i"]

    B --> B1["Görsel dosyası saklama"]
    B --> B2["CDN üzerinden hızlı erişim"]

    C --> C1["Gönderi beğenisi"]
    C --> C2["Yorum beğenisi"]
    C --> C3["Gönderiye yorum"]

    D --> D1["Kimi takip ediyorum?"]
    D --> D2["Beni kim takip ediyor?"]

    E --> E1["Akışı okuma"]
    E --> E2["Akışı önceden hesaplama"]

Bu diyagram sistemin ana kapsamını gösteriyor. Böyle düşünmek tasarımı sadeleştirir çünkü her servisin hangi probleme cevap verdiği baştan belli olur.

Gönderi ve Görsel Saklama

Instagram’ın en görünür tarafı görsellerdir. Burada dosyanın kendisini veritabanına koymak genellikle iyi fikir değildir. Büyük medya dosyaları için object storage veya dağıtık dosya sistemi kullanılır. Object storage, yani şöyle düşün: dosyayı bir tablo satırı gibi değil, erişilebilir bir nesne olarak saklayan ucuz ve ölçeklenebilir depolama katmanıdır.

Görsel yüklendiğinde sistem dosyayı depolar, dosyanın URL veya object key bilgisini ise post verisinde tutar. Kullanıcı görseli görüntülediğinde dosya doğrudan CDN üzerinden sunulur. CDN, yani şöyle düşün: kullanıcıya en yakın lokasyondan statik içeriği veren dağıtık önbellek ağıdır.

flowchart LR
    U["Mobil Uygulama"] --> G["Gateway"]
    G --> P["Post Service"]
    P --> DB[("Post Metadata DB")]
    P --> S[("Object Storage")]
    S --> CDN["CDN"]
    CDN --> U

    DB --> M["post_id, user_id, image_url, created_at"]

Bu yapıda veritabanı gönderinin kim tarafından, ne zaman oluşturulduğunu ve görselin nerede olduğunu bilir. Görselin kendisi ise daha ucuz ve hızlı servis edilebilir bir depolama katmanında durur.

Beğeni ve Yorum Modeli

Beğeni sistemi ilk bakışta basit görünür: bir kullanıcı bir gönderiyi beğenir. Ama hemen ardından şu soru gelir: yorumlar da beğenilebilir mi? Eğer evetse, beğeni sadece gönderiye bağlı olmamalıdır. Beğenilen şeyin türünü ayrıca tutmak daha temiz bir model sağlar.

Burada polymorphic association kullanabiliriz. Polymorphic association, yani şöyle düşün: aynı tablo satırı farklı türde nesnelere bağlanabilir; bazen gönderiye, bazen yoruma işaret eder.

erDiagram
    USER ||--o{ POST : creates
    USER ||--o{ COMMENT : writes
    USER ||--o{ LIKE : performs
    POST ||--o{ COMMENT : has
    POST ||--o{ ACTIVITY_COUNTER : counted_by
    COMMENT ||--o{ ACTIVITY_COUNTER : counted_by

    USER {
        string user_id PK
        string username
        string profile_image_url
        datetime created_at
    }

    POST {
        string post_id PK
        string user_id FK
        string image_url
        string caption
        datetime created_at
    }

    COMMENT {
        string comment_id PK
        string post_id FK
        string user_id FK
        string text
        datetime created_at
    }

    LIKE {
        string like_id PK
        string user_id FK
        string parent_id
        string parent_type
        boolean active
        datetime created_at
    }

    ACTIVITY_COUNTER {
        string parent_id PK
        int like_count
        int comment_count
        datetime updated_at
    }

Bu ER diyagramında LIKE.parent_type alanı beğenilen nesnenin gönderi mi yorum mu olduğunu belirtir. ACTIVITY_COUNTER ise sık okunan sayaçları ayrı bir yerde tutar; böylece her newsfeed yüklemesinde milyonlarca beğeni satırı sayılmaz.

Neden Sayaçları Ayrı Tutuyoruz?

Bir gönderinin kaç beğeni aldığını her seferinde COUNT(*) ile hesaplamak küçük ölçekte çalışır. Fakat newsfeed içinde 20 gönderi gösteriyorsan ve her gönderi için ayrı sayım yapıyorsan sistem gereksiz yere zorlanır.

Aggregation, yani şöyle düşün: ham olaylardan türetilmiş özet bilgidir. Beğeni sayısı aslında tek tek beğeni satırlarından hesaplanabilir, ama sürekli hesaplamak pahalıdır. Bu yüzden sayaçları ayrı bir tabloda veya cache katmanında tutmak daha pratiktir.

flowchart TD
    A["Kullanıcı gönderiyi beğenir"] --> B["Like kaydı oluştur veya aktif hale getir"]
    B --> C{"Parent type nedir?"}
    C -->|"post"| D["Post için sayaç artır"]
    C -->|"comment"| E["Comment için sayaç artır"]
    D --> F[("Activity Counter")]
    E --> F
    F --> G["Newsfeed okumasında hızlı gösterim"]

Bu akışta beğeni olayı hem detaylı kayıt olarak saklanır hem de okunması hızlı bir sayaç güncellenir. Böylece sistem hem geçmişi bilir hem de kullanıcıya hızlı cevap verir.

Takip İlişkisi

Takip sistemi için tek bir tablo çoğu temel ihtiyacı karşılar. Bir kullanıcı başka bir kullanıcıyı takip ettiğinde follower_user_id ve followee_user_id alanlarıyla ilişki kaydedilir.

erDiagram
    USER ||--o{ FOLLOW : follows_as_follower
    USER ||--o{ FOLLOW : followed_as_followee

    USER {
        string user_id PK
        string username
    }

    FOLLOW {
        string follower_user_id FK
        string followee_user_id FK
        datetime created_at
        boolean active
    }

Bu model iki önemli soruya cevap verir: “Ben kimleri takip ediyorum?” ve “Beni kimler takip ediyor?” İlk soru için follower_user_id, ikinci soru için followee_user_id üzerinden sorgu yapılır. Büyük ölçekte bu iki alan için ayrı index kullanmak gerekir. Index, yani şöyle düşün: veritabanının ilgili satırları tüm tabloyu taramadan bulmasını sağlayan arama yapısıdır.

Servis Mimarisi

Uygulama tarafında mobil istemci doğrudan veritabanlarına gitmez. İstek önce gateway katmanına gelir. Gateway, yani şöyle düşün: dış dünyadan gelen istekleri karşılayan, kimlik doğrulama, yönlendirme ve protokol dönüşümü gibi işleri üstlenen giriş kapısıdır.

Ardından istek ilgili servise yönlendirilir. Newsfeed için User Feed Service, gönderiler için Post Service, takip ilişkileri için Follow Service gibi ayrımlar yapılabilir.

flowchart LR
    M["Mobil Uygulama"] --> GW["Gateway"]

    subgraph Edge["Giriş Katmanı"]
        GW --> LB["Load Balancer / Service Registry Snapshot"]
    end

    subgraph Services["İç Servisler"]
        GW --> UFS["User Feed Service"]
        UFS --> PS["Post Service"]
        UFS --> FS["Follow Service"]
        PS --> IS["Image Service"]
        UFS --> AS["Activity Service"]
    end

    subgraph Storage["Veri Katmanı"]
        PS --> PDB[("Post DB")]
        FS --> FDB[("Follow DB")]
        UFS --> CACHE[("Feed Cache")]
        IS --> OBJ[("Object Storage")]
        AS --> ADB[("Activity DB")]
    end

Bu yapı servisleri birbirinden ayırır. User Feed Service doğrudan tüm veriyi sahiplenmez; gerektiğinde Post Service ve Follow Service gibi uzman servislerden bilgi alır.

Newsfeed’i Anlık Hesaplamak Neden Zor?

En basit yaklaşım şudur: kullanıcı akışını açınca önce takip ettiği kişileri bul, sonra bu kişilerin son gönderilerini al, sonra sırala ve ilk 20 tanesini dön. Küçük kullanıcı sayısında çalışır ama büyüdükçe pahalı hale gelir.

sequenceDiagram
    participant App as Mobil Uygulama
    participant Gateway as Gateway
    participant Feed as User Feed Service
    participant Follow as Follow Service
    participant Post as Post Service

    App->>Gateway: getUserFeed(user_id)
    Gateway->>Feed: getUserFeed(user_id)
    Feed->>Follow: getFollowees(user_id)
    Follow-->>Feed: takip edilen kullanıcılar
    loop Her takip edilen kullanıcı için
        Feed->>Post: getRecentPosts(user_id)
        Post-->>Feed: son gönderiler
    end
    Feed->>Feed: gönderileri birleştir ve sırala
    Feed-->>Gateway: ilk 20 gönderi
    Gateway-->>App: newsfeed

Bu diyagram “fan-out on read” yaklaşımını gösterir. Fan-out on read, yani şöyle düşün: kullanıcı akışı istediği anda birçok kaynaktan veri toplayıp sonucu o anda üretirsin. Okuma sayısı çok yüksekse bu yaklaşım servisleri ve veritabanlarını yorar.

Daha İyi Yaklaşım: Feed’i Önceden Hazırlamak

Büyük sistemlerde newsfeed genellikle önceden hesaplanır. Bir kullanıcı yeni gönderi oluşturduğunda sistem o kullanıcının takipçilerini bulur ve ilgili takipçilerin feed cache’ine bu gönderiyi ekler.

Buna “fan-out on write” denir. Yani şöyle düşün: gönderi yazıldığı anda maliyeti ödersin, böylece kullanıcı akışı okurken hızlı cevap alır.

flowchart TD
    A["Kullanıcı yeni gönderi oluşturur"] --> B["Post Service gönderiyi kaydeder"]
    B --> C[("Post DB")]
    B --> D["Feed Update Event yayınlanır"]

    D --> E["User Feed Service"]
    E --> F["Follow Service'ten takipçileri al"]
    F --> G["Takipçi listesi"]

    G --> H["Her takipçinin feed cache'ine post_id ekle"]
    H --> I[("Feed Cache")]
    I --> J["Kullanıcı feed açınca ilk 20 kayıt hızlı döner"]

Buradaki önemli nokta feed’in tamamını sürekli yeniden üretmemektir. Yeni gönderi geldiğinde sadece ilgili kullanıcıların hazır akış listesine küçük bir ekleme yapılır.

Feed Cache Nasıl Çalışır?

Feed cache içinde her kullanıcı için son gösterilecek gönderilerin kısa bir listesi tutulabilir. Örneğin her kullanıcı için en güncel 20, 50 veya 100 gönderi saklanır. Cache, yani şöyle düşün: sık okunan veriyi ana veritabanına gitmeden hızlıca cevaplamak için kullanılan geçici ve hızlı depolama alanıdır.

flowchart LR
    U1["user_1 feed"] --> L1["post_91 → post_84 → post_73"]
    U2["user_2 feed"] --> L2["post_91 → post_80 → post_77"]
    U3["user_3 feed"] --> L3["post_88 → post_84 → post_60"]

    NP["Yeni post_92"] --> F["Takipçileri bul"]
    F --> U1
    F --> U2

Bu diyagram aynı gönderinin birden fazla kullanıcının cache listesine eklenebileceğini gösterir. Cache kaybolursa sistem brute force yöntemle feed’i yeniden hesaplayabilir; bu yüzden cache’i tek gerçek kaynak gibi düşünmemek gerekir.

Ünlü Kullanıcı Problemi

Fan-out on write her kullanıcı için iyi çalışmaz. Milyonlarca takipçisi olan bir hesap gönderi paylaştığında, bu gönderiyi aynı anda milyonlarca feed listesine yazmaya çalışmak sistemi zorlayabilir.

Bu noktada hibrit model kullanılır. Normal kullanıcılar için gönderi takipçilerin feed cache’ine yazılır. Çok yüksek takipçili hesaplar için ise gönderi herkese tek tek itilmez; kullanıcı feed açtığında bu hesapların son gönderileri ayrıca çekilir ve akışa dahil edilir.

flowchart TD
    A["Yeni gönderi"] --> B{"Gönderen yüksek takipçili mi?"}

    B -->|"Hayır"| C["Fan-out on write"]
    C --> D["Takipçilerin feed cache'ine ekle"]
    D --> E["Anlık ve hızlı feed"]

    B -->|"Evet"| F["Fan-out sınırlanır"]
    F --> G["Gönderi merkezi listede tutulur"]
    G --> H["Kullanıcı feed açınca pull modeliyle eklenir"]

    H --> I["Hibrit newsfeed"]
    E --> I

Bu model push ve pull yaklaşımını birlikte kullanır. Push, yani şöyle düşün: sistem yeni içeriği kullanıcının hazır alanına kendisi iter. Pull ise kullanıcının ihtiyacı olduğunda veriyi çekmesidir.

Bildirim ve Gerçek Zamanlı Güncellemeler

Kullanıcıya yeni gönderi geldiğini göstermek için birkaç yöntem vardır. Mobil uygulama belirli aralıklarla sunucuya sorabilir, uzun süre açık kalan bağlantılar kullanılabilir ya da WebSocket ile sunucu istemciye doğrudan mesaj gönderebilir.

WebSocket, yani şöyle düşün: istemci ve sunucu arasında açık kalan çift yönlü bağlantıdır. Sürekli yeni HTTP isteği açmak yerine aynı bağlantı üzerinden güncelleme taşınabilir.

sequenceDiagram
    participant Post as Post Service
    participant Feed as User Feed Service
    participant Notify as Notification Service
    participant App as Mobil Uygulama

    Post->>Feed: yeni gönderi olayı
    Feed->>Feed: ilgili kullanıcıların feed'ini güncelle
    Feed->>Notify: yeni içerik bildirimi oluştur
    Notify-->>App: WebSocket veya push notification
    App->>Feed: güncel feed'i iste
    Feed-->>App: hazır feed listesi

Bu yapı gerçek zaman hissini güçlendirir. Yine de her gönderi için herkese anlık bildirim göndermek doğru değildir; özellikle yüksek takipçili hesaplarda rate limit ve önceliklendirme gerekir.

Genel Mimariyi Birleştirelim

Tüm parçaları bir araya getirdiğimizde Instagram benzeri çekirdek sistem şu şekilde düşünülebilir: görseller object storage ve CDN ile servis edilir, gönderi ve aktivite bilgileri ayrı servislerde tutulur, takip ilişkileri Follow Service tarafından yönetilir, User Feed Service ise cache tabanlı kişiselleştirilmiş akış üretir.

flowchart TD
    App["Mobil Uygulama"] --> Gateway["Gateway"]

    Gateway --> PostService["Post Service"]
    Gateway --> FeedService["User Feed Service"]
    Gateway --> ActivityService["Activity Service"]
    Gateway --> FollowService["Follow Service"]

    PostService --> PostDB[("Post DB")]
    PostService --> ObjectStorage[("Object Storage")]
    ObjectStorage --> CDN["CDN"]

    ActivityService --> ActivityDB[("Like / Comment DB")]
    FollowService --> FollowDB[("Follow DB")]

    PostService --> EventBus["Event Bus"]
    EventBus --> FeedService

    FeedService --> FollowService
    FeedService --> FeedCache[("Feed Cache")]
    FeedService --> PostService

    CDN --> App
    FeedCache --> FeedService

Event Bus, yani şöyle düşün: servislerin birbirini doğrudan sıkı sıkıya çağırmadan olay yayınlayıp tüketmesini sağlayan ara katmandır. Yeni gönderi olayı Post Service’ten çıkar, User Feed Service bu olayı dinler ve gerekli feed güncellemelerini yapar.

Özet

Instagram benzeri bir sistemde asıl mesele tek tek özellikleri yazmak değil, okuma ve yazma maliyetlerini doğru yere taşımaktır. Görseller CDN destekli bir depolama katmanından sunulur, beğeni ve yorum gibi aktiviteler ayrı modellenir, sayaçlar hızlı okuma için önceden tutulur. Takip ilişkisi basit bir tabloyla başlar ama doğru indexlerle ölçeklenir. Newsfeed tarafında ise anlık hesaplama küçük ölçekte işe yarasa da büyük ölçekte cache tabanlı ön hesaplama gerekir. En sağlıklı çözüm genellikle hibrittir: normal kullanıcılar için push, çok yüksek takipçili hesaplar için pull yaklaşımı birlikte kullanılır.

Bu yazı Designing INSTAGRAM: System Design of News Feed videosundan ilham alınarak yazılmıştır.


Kaynakça

Leave a Reply

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir