Mert Özen Her Satırda Daha İleri
Yazılım Mimarisi System Design — #02

Back-of-the-Envelope Estimation: QPS, Storage, Bandwidth ve Availability Hesabı

Mert Özen 8 Kas 2026 13 dk 3 görüntülenme
Back-of-the-Envelope Estimation: QPS, Storage, Bandwidth ve Availability Hesabı

Back-of-the-envelope estimation, DAU'dan ortalama ve peak QPS'e, oradan storage, bandwidth ve availability'ye giden ve mertebe olarak doğru sonuç veren kısa bir hesap zinciridir. 10 milyon DAU'lu URL shortener'da peak ~1.700 QPS ve 5 yılda ~1 TB veri tek veritabanına sığarken, fotoğraf uygulamasında 55,6 Gbps egress CDN'i zorunlu kılıyor; %99,9 yılda 8,76 saat kesinti demek ve seri bileşenlerde availability çarpılıyor.

Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.

Önce Tahmin Et

İki ürün düşün; ikisinin de günde 10 milyon aktif kullanıcısı var. Biri URL (Uniform Resource Locator) shortener: kullanıcılar uzun link'leri kısaltıyor, başkaları kısa link'e tıklayıp yönlendiriliyor. Diğeri bir fotoğraf paylaşım uygulaması: kullanıcılar fotoğraf yüklüyor, akışlarında başkalarının fotoğraflarını görüyor.

Üç soru. URL shortener'ın redirect trafiği için saniyede kaç istek konuşuyoruz: yüz mü, bin mi, yüz bin mi? Beş yılda link tablosu bir sunucunun diskine sığar mı? Fotoğraf uygulamasında ilk zorlanacak şey hangisi: veritabanı mı, disk mi, network mü?

Cevapları hissinle ver, sonra aşağıdaki hesapla karşılaştır. Bu yazının bütün amacı, bu hissin yerine beş dakikalık bir hesap koymak.

Neden Kâğıt Üstü Hesap?

Back-of-the-envelope estimation, bir sistemin yükünü ve kaynak ihtiyacını kaba ama mertebe olarak doğru sayılarla tahmin etmek. Adı, Jeff Dean'in Google'da yaygınlaştırdığı "zarfın arkasına yapılan hesap" fikrinden geliyor: amaç 1.736'yı bulmak değil, cevabın 2.000 civarı mı yoksa 200.000 civarı mı olduğunu bilmek. Bu iki sayı arasındaki fark, tek bir veritabanı ile sharding'li bir cluster arasındaki fark.

Bir önceki yazıda, "System Design Nedir, Nasıl Çalışılır? Requirement'lardan Deep Dive'a 4 Adımlı Yöntem"de, Little's law ile kampanya gününün darboğazını kâğıt üstünde bulmuştuk. Burada aynı disiplini sistematik hale getiriyoruz: DAU (Daily Active Users) sayısından saniyedeki isteğe, oradan storage, bandwidth ve availability'ye.

Sayıların Alfabesi: Powers of Two ve Latency Numbers

Hesaba başlamadan iki tablo ezberde olmalı. İlki boyutlar. Bilgisayar ikilik tabanda sayar ama tahminde onluk tabanla yuvarlamak yeterince doğru: 2 üzeri 10 = 1.024 ≈ bin.

2^10 ~ 1 thousand     1 KB    a short JSON response
2^20 ~ 1 million      1 MB    a photo, compressed
2^30 ~ 1 billion      1 GB    fits in RAM easily
2^40 ~ 1 trillion     1 TB    one disk
2^50 ~ 1 quadrillion  1 PB    a storage cluster

int 4 B, long / DateTime 8 B, Guid 16 B, ASCII char 1 B, UTF-16 char 2 B

Fark yüzdesi büyüdükçe artıyor (KB'de %2,4, TB'de yaklaşık %10), o yüzden hassas kapasite planlamasında KiB/GiB ile hesaplanır; estimation'da onluk taban yeterli. Bu yazıdaki kod da 1 KB = 1.000 byte alıyor.

İkincisi latency. Jeff Dean'in derlediği ve Peter Norvig'in yaygınlaştırdığı "her programcının bilmesi gereken latency sayıları" listesinden system design için önemli satırlar (sayılar 2010'ların başına ait; bugünkü donanım daha hızlı ama oranlar hâlâ yol gösteriyor):

İşlemSüreKabaca
Main memory referansı100 nsbellek
1 Gbps network'te 1 KB göndermek10 µs100 × bellek
SSD'den (Solid State Drive) rastgele 4 KB okumak150 µs1.500 × bellek
Bellekten sıralı 1 MB okumak250 µs
Aynı datacenter içinde round trip500 µsyarım milisaniye
SSD'den sıralı 1 MB okumak1 msbellekten 4 kat yavaş
Disk seek10 ms
Kaliforniya → Hollanda → Kaliforniya paket150 mskıtalar arası

Tablodan çıkan üç ders yeter. Bellek, network'ten ve diskten mertebelerce hızlı; cache'in var olma sebebi bu. Aynı datacenter içindeki bir round trip yarım milisaniye; bir sayfa 20 servis çağrısını sırayla yapıyorsa yalnız network 10 ms tutar. Kıtalar arası bir round trip 150 ms; ışık hızını optimize edemezsin, kullanıcıya yakın olmak için CDN (Content Delivery Network) ve bölgesel deployment gerekir. SSD ise rastgele okumada disk seek'ten yaklaşık 70 kat hızlı.

DAU'dan QPS'e: Ortalama ve Peak

Trafik hesabı hep aynı zincir: kullanıcı sayısı × kullanıcı başına günlük işlem = günlük işlem; günlük işlem ÷ 86.400 saniye = ortalama QPS (queries per second). Sektörde QPS ile RPS (requests per second) çoğu zaman aynı anlamda kullanılıyor; burada ikisi de "saniyedeki istek".

Hızlı hesap için 1 gün ≈ 10^5 saniye (gerçekte 86.400) kabul edilir; günde 1 milyon işlem ≈ saniyede 10–12 işlem. Bunu bilmek, "günde 100 milyon istek" cümlesini duyduğunda kafandan "saniyede ~1.200" demeni sağlıyor.

Ortalama tek başına işe yaramaz, çünkü trafik gün içinde düz dağılmıyor. Peak'i tahmin etmenin dürüst yolu bir trafik şekli varsaymak: günlük trafiğin yarısı akşamın 4 saatinde geliyorsa, peak saatteki hız ortalamanın 0,5 / (4/24) = 3 katı. Bu yazıda peak factor'ı böyle 3 aldık. Kampanya, maç sonucu, push notification gibi olaylarda bu çarpan 10'u kolayca geçer; Bookstore'un kampanya günündeki 10 katı bunun örneği. Kapasiteyi ortalamaya göre değil peak'e göre planlarsın.

Okuma ve yazmayı ayrı hesaplamak da şart. Read:write oranı, cache'in ne kadar işe yarayacağını ve veritabanında replica mı yoksa sharding mi gerektiğini belirleyen ilk sayı.

Storage ve Bandwidth

Storage hesabı: günlük yeni kayıt × kayıt boyutu × saklama süresi. Kayıt boyutunu tahmin ederken yalnız field'ları değil, index'i ve satır ek yükünü de say; ham verinin 2–3 katı pratik bir tahmin. Replikasyon varsa (veritabanında bir replica, object storage'da genelde 3 kopya ya da erasure coding) ham ihtiyacı ona göre çarp.

Bandwidth hesabı: QPS × istek ya da yanıt boyutu. Ingress (sisteme giren) ve egress (sistemden çıkan) ayrı hesaplanır; bulut sağlayıcılarında egress ücretlidir ve çoğu zaman beklenmedik faturanın kaynağıdır. Bayt'ı bit'e çevirmek için 8 ile çarpmayı unutma: network kapasitesi bit/saniye ile konuşulur.

Uçtan Uca: URL Shortener ve Fotoğraf Paylaşımı

Varsayımları koda yazıp hesaplatalım. Önemli olan kod değil, her satırdaki varsayımın görünür olması:

const double SecondsPerDay = 86_400;
const double PeakFactor = 3; // günlük trafiğin %50'si akşamın 4 saatinde: 0.5 / (4 / 24.0)

Console.WriteLine("== URL shortener, 10M DAU");
double redirectsPerDay = 10_000_000 * 5.0;     // her kullanıcı günde 5 kısa link'e tıklıyor
double newLinksPerDay = 10_000_000 * 0.10;     // kullanıcıların %10'u günde bir link oluşturuyor
Rate redirects = new(redirectsPerDay / SecondsPerDay, PeakFactor);
Rate writes = new(newLinksPerDay / SecondsPerDay, PeakFactor);
Console.WriteLine($"read QPS         avg {redirects.Average,8:N0}   peak {redirects.Peak,8:N0}");
Console.WriteLine($"write QPS        avg {writes.Average,8:N1}   peak {writes.Peak,8:N1}");
Console.WriteLine($"read:write       {redirectsPerDay / newLinksPerDay:N0}:1");
double rows = newLinksPerDay * 365 * 5;
Console.WriteLine($"rows in 5 years  {rows:N0}");
Console.WriteLine($"storage 5 years  {Size.Format(rows * 500)}   (500 B per row incl. index)");
Console.WriteLine($"base62 codes     6 chars = {Math.Pow(62, 6):N0}, 7 chars = {Math.Pow(62, 7):N0}");
Console.WriteLine($"egress at peak   {Size.Format(redirects.Peak * 300)}/s = {redirects.Peak * 300 * 8 / 1e6:N1} Mbps (300 B per redirect)");
Console.WriteLine($"cache (20% hot)  {Size.Format(redirectsPerDay * 0.20 * 500)}");

Console.WriteLine();
Console.WriteLine("== Photo sharing, 10M DAU");
double uploadsPerDay = 10_000_000 * 0.10 * 2;  // kullanıcıların %10'u günde 2 fotoğraf yüklüyor
double viewsPerDay = 10_000_000 * 100.0;       // her kullanıcı günde 100 thumbnail görüyor
Rate uploads = new(uploadsPerDay / SecondsPerDay, PeakFactor);
Rate views = new(viewsPerDay / SecondsPerDay, PeakFactor);
Console.WriteLine($"upload QPS       avg {uploads.Average,8:N1}   peak {uploads.Peak,8:N1}");
Console.WriteLine($"view QPS         avg {views.Average,8:N0}   peak {views.Peak,8:N0}");
double perPhoto = 2_000_000 + 500_000;         // orijinal 2 MB + küçültülmüş varyantlar 0,5 MB
Console.WriteLine($"blob storage     {Size.Format(uploadsPerDay * perPhoto)}/day, {Size.Format(uploadsPerDay * perPhoto * 365)}/year");
Console.WriteLine($"metadata         {Size.Format(uploadsPerDay * 1_000 * 365)}/year (1 KB per photo)");
Console.WriteLine($"ingress at peak  {uploads.Peak * 2_000_000 * 8 / 1e9:N2} Gbps");
Console.WriteLine($"egress avg/peak  {views.Average * 200_000 * 8 / 1e9:N1} / {views.Peak * 200_000 * 8 / 1e9:N1} Gbps (200 KB thumbnail)");

public readonly record struct Rate(double Average, double PeakFactor)
{
    public double Peak => Average * PeakFactor;
}

Size.Format byte'ı 1.000'lik adımlarla KB, MB, GB, TB'ye çeviren küçük bir yardımcı. Çıktı:

== URL shortener, 10M DAU
read QPS         avg      579   peak    1,736
write QPS        avg     11.6   peak     34.7
read:write       50:1
rows in 5 years  1,825,000,000
storage 5 years  912.5 GB   (500 B per row incl. index)
base62 codes     6 chars = 56,800,235,584, 7 chars = 3,521,614,606,208
egress at peak   520.83 KB/s = 4.2 Mbps (300 B per redirect)
cache (20% hot)  5 GB

== Photo sharing, 10M DAU
upload QPS       avg     23.1   peak     69.4
view QPS         avg   11,574   peak   34,722
blob storage     5 TB/day, 1.83 PB/year
metadata         730 GB/year (1 KB per photo)
ingress at peak  1.11 Gbps
egress avg/peak  18.5 / 55.6 Gbps (200 KB thumbnail)

Açılıştaki soruların cevapları burada. URL shortener'da redirect trafiği peak'te saniyede ~1.700; düzgün index'lenmiş tek bir ilişkisel veritabanı bunu kaldırabilir, ama 50:1'lik read:write oranı buranın cache'in en iyi çalıştığı yer olduğunu söylüyor: günün sıcak link'lerinin %20'si 5 GB ile tek bir Redis instance'ına sığıyor. Beş yılda 1,8 milyar satır ve yaklaşık 1 TB veri tek bir sunucunun diskine sığar; sharding'i storage değil, ileride yazma hızı ya da availability gerektirirse düşünürsün. 6 karakterlik base62 kod 56 milyar link'e yetiyor, yani 7 karaktere bile gerek yok. Bandwidth ise önemsiz: 4,2 Mbps.

Fotoğraf uygulamasında tablo tersine dönüyor. Yükleme sayısı küçük (peak'te saniyede ~70), ama günde 5 TB, yılda 1,8 PB (petabyte) blob birikiyor; bu bir veritabanının değil object storage'ın işi, veritabanında yalnız yılda 730 GB'lık metadata kalıyor. Asıl darboğaz network: thumbnail egress'i ortalamada 18,5, peak'te 55,6 Gbps. Bunu kendi sunucularından servis etmek gerçekçi değil; görsellerin CDN'den dağıtılması bir optimizasyon değil, tasarımın ön koşulu. Açılıştaki üçüncü sorunun cevabı da bu: ilk zorlanan network.

Availability: Nines, Seri ve Paralel Bileşenler

Availability, sistemin istenen işi yapabildiği zamanın oranı. Google SRE (Site Reliability Engineering) kitabının "Embracing Risk" bölümü bunu iki yoldan ölçmeyi anlatıyor: zaman bazlı (uptime / toplam süre) ve, dağıtık sistemlerde daha anlamlı olan, istek bazlı (başarılı istek / toplam istek). Hedef SLO (Service Level Objective) olarak yazılır, müşteriye verilen ve ihlali bir yaptırım doğuran söz ise SLA'dır (Service Level Agreement). "Nines" bu oranın kısa adı:

public static string Downtime(double availability, TimeSpan period)
{
    TimeSpan down = period * (1 - availability);
    return down.TotalHours >= 1 ? $"{down.TotalHours:0.##} h" : down.TotalMinutes >= 1 ? $"{down.TotalMinutes:0.##} min" : $"{down.TotalSeconds:0.#} s";
}

// seri: hepsi ayakta olmalı
public static double Serial(params double[] parts) => parts.Aggregate(1.0, (acc, a) => acc * a);

// paralel: en az biri ayakta olmalı
public static double Parallel(params double[] replicas) => 1 - replicas.Aggregate(1.0, (acc, a) => acc * (1 - a));
     99%  down 87.6 h         a year, 7.2 h        a month
   99.9%  down 8.76 h         a year, 43.2 min     a month
  99.95%  down 4.38 h         a year, 21.6 min     a month
  99.99%  down 52.56 min      a year, 4.32 min     a month
 99.999%  down 5.26 min       a year, 25.9 s       a month
serial   LB 99.99% -> app 99.95% -> DB 99.95% = 99.89%
parallel 2 x app 99.5%                      = 99.9975%

%99,9 yılda 8,76 saat, ayda 43 dakika kesinti demek; her ek dokuz, izin verilen kesintiyi on kat azaltıyor ve maliyeti kabaca o oranda artırıyor. Bookstore'un checkout hedefi aylık %99,9: ayda 43 dakika. Haftada bir yapılan ve her seferinde beş dakika kesinti yaratan bir deployment, bu bütçenin yarısını tek başına yiyor.

Daha önemli olan çarpım kuralı. Bir istek sırayla load balancer (LB), uygulama ve veritabanından geçiyorsa hepsinin ayakta olması gerekir ve availability'ler çarpılır: tek tek %99,99, %99,95 ve %99,95 olan üç bileşenin zinciri %99,89'a düşüyor, yani zincir en zayıf halkasından da kötü. Paralel bileşende tersi oluyor: tek başına %99,5 olan iki instance'tan en az birinin ayakta olması %99,9975 veriyor. Bunun şartı, instance'ların birbirinden bağımsız düşmesi; ikisi aynı rack'te, aynı veritabanına bağlı ya da aynı hatalı deployment'la geliyorsa çarpım teoride kalır. Redundancy'nin değeri, hataların bağımsızlığı kadardır.

Ortalama Latency Yalan Söyler: p50, p95, p99

Latency hedefleri ortalama ile değil percentile ile yazılır. Arama isteklerinin %97'si 30–70 ms'de, %3'ü yavaş bir yoldan 0,9–1,5 saniyede dönen 10.000 isteklik bir simülasyon:

Random rng = new(42);
double[] latencies = [.. Enumerable.Range(0, 10_000)
    .Select(_ => rng.NextDouble() < 0.97 ? 30 + rng.NextDouble() * 40 : 900 + rng.NextDouble() * 600)
    .Order()];
Console.WriteLine($"average {latencies.Average(),6:N0} ms");
Console.WriteLine($"p50     {Percentile(latencies, 0.50),6:N0} ms");
Console.WriteLine($"p95     {Percentile(latencies, 0.95),6:N0} ms");
Console.WriteLine($"p99     {Percentile(latencies, 0.99),6:N0} ms");
foreach (int calls in (int[])[1, 10, 100])
    Console.WriteLine($"page with {calls,3} backend calls: {(1 - Math.Pow(0.99, calls)) * 100,5:0.0}% of pages wait for a p99 call");

static double Percentile(double[] sorted, double p) => sorted[(int)Math.Ceiling(p * sorted.Length) - 1];
average     81 ms
p50         51 ms
p95         69 ms
p99      1,262 ms
page with   1 backend calls:   1.0% of pages wait for a p99 call
page with 10 backend calls:   9.6% of pages wait for a p99 call
page with 100 backend calls:  63.4% of pages wait for a p99 call

Ortalama 81 ms ve hiçbir isteği anlatmıyor: hızlı istekler 30–70 ms'de, yavaşlar bir saniyenin üstünde, 81 ms'de dönen tek bir istek yok. p95 bile 69 ms ile iyi görünüyor; yavaş yolu ancak p99 gösteriyor: 1,26 saniye. Günde 50 milyon istek alan bir sistemde %1, günde 500.000 yavaş istek demek.

Son üç satır, Jeff Dean ve Luiz André Barroso'nun "The Tail at Scale" makalesindeki (Communications of the ACM (Association for Computing Machinery), 2013) argümanın hesabı. Bir sayfa 100 backend çağrısını paralel yapıyor ve her çağrının %1 ihtimalle yavaş olduğunu varsayıyorsak, sayfaların %63'ü en az bir yavaş çağrıyı bekliyor. Fan-out arttıkça kullanıcının gördüğü latency, backend'lerin p99'u hatta p99.9'u tarafından belirleniyor. Microservice'li bir sayfada p99 hedefi koymamak, kullanıcı deneyimini şansa bırakmak demek.

Yuvarlama Disiplini

Estimation'ın amacı hız; bunun için birkaç kural tutuyorum. Sayıları 1–2 anlamlı basamağa yuvarla: 86.400 yerine 10^5, 1.736 yerine "~2.000". Varsayımı her zaman birimiyle yaz ve yanına koy: "kullanıcı başına günde 5 redirect". Hesabı ortalamadan peak'e, ham veriden replika ve index'li hâle sırayla taşı; bir adımı atlarsan hatayı nerede yaptığını bulamazsın. Son olarak sonucu bir karşılaştırmayla bitir: "tek sunucuya sığar", "tek Redis yeter", "CDN şart". Sayının kendisi değil, verdirdiği karar önemli.

Bir uyarı: yuvarlama hatası katlanarak büyür. Beş varsayımın her biri 2 kat yanlışsa, sonuç 32 kat yanlış olabilir. Bu yüzden hassas olmayan ama makul varsayımlar seçip hangilerinin sonucu en çok etkilediğini söylemek, tek bir "kesin" sayı vermekten daha değerli. Fotoğraf örneğinde sonucu belirleyen varsayım thumbnail boyutu: 200 KB yerine 50 KB'lık WebP thumbnail egress'i dörtte birine indiriyor.

Sık Düşülen Tuzaklar

Peak'i unutmak. Ortalama QPS'e göre boyutlanan bir sistem, trafiğin geldiği saatte yetersiz kalır; peak factor'ı varsayımlar listesine açıkça yaz.

Ortalama latency ile p99'u karıştırmak. "Ortalama yanıt süremiz 80 ms" cümlesi, yüz binlerce isteğin bir saniyeden uzun sürdüğünü gizleyebilir.

Byte ile bit'i karıştırmak. 55,6 Gbps ile 55,6 GB/s arasında 8 kat fark var; network kapasitesi bit ile, storage byte ile konuşulur.

Index'i ve replikasyonu saymamak. Ham veri 300 GB ise index, satır ek yükü, bir replica ve yedeklerle birlikte gerçek ihtiyaç 1 TB'ı rahat geçer.

Seri bileşenlerde availability'yi toplamak ya da en düşüğünü almak. Zincir çarpılır ve en zayıf halkadan daha kötü olur.

Hesabı süs olarak yapmak. Sonuç bir karara bağlanmıyorsa ("Redis'e sığar", "object storage ve CDN şart"), estimation yapılmamış sayılır.

Küçük Bir Deneme

★☆☆ Peak factor'ı 3 yerine 10 yap (kampanya günü) ve URL shortener için yazma ve okuma QPS'ini yeniden hesapla. Tek bir veritabanının hâlâ yeteceğini düşünüyor musun?

★★☆ Fotoğraf uygulamasında thumbnail'i 200 KB yerine 50 KB yap ve kullanıcı başına günlük görüntülemeyi 300'e çıkar. Peak egress kaç Gbps oluyor? Hangi varsayım sonucu daha çok etkiledi?

★★★ Bookstore'un checkout zinciri için availability hesapla: load balancer %99,99, iki uygulama instance'ı tek başına %99,5, SQL (Structured Query Language) Server %99,95, ödeme sağlayıcısı %99,9. Ödeme sağlayıcısı yanıt vermediğinde siparişin "PaymentPending" olması (Bookstore'un aldığı karar) checkout'un availability'sini nasıl değiştirir? Seri ve paralel formülleri birlikte kullan.

Hesaplar artık elimizde: tek bir sunucunun nerede yetmeyeceğini sayılarla söyleyebiliyoruz. Bir sonraki yazıda o noktadan sonra ne olduğuna bakıyoruz: vertical ve horizontal scaling, uygulama tier'ını stateless yapmak ve tek kutudan çok bölgeye giden altı adımlık mimari evrim.