URL kısaltıcı
Problem
"bit.ly benzeri bir servis tasarla: kullanıcı uzun bir URL verir, sistem kısa bir kod üretir. Kısa koda gelen istek uzun URL'ye yönlendirilir. Link sahibi tıklama istatistiklerini de görmek istiyor."
Soru masum görünür; çoğu aday beş dakikada "hash al, veritabanına yaz, cache koy" diyip bitirir. Mülakatçının asıl ölçtüğü şeyler daha incedir:
- ID üretimi: Kısa, benzersiz, tahmin edilemeyen kodu çok sunuculu bir ortamda koordinasyonsuz ama çakışmasız nasıl üretirsin?
- Okuma ağırlıklı sistem: Yönlendirme yolu milisaniyeler içinde ve çok yüksek erişilebilirlikle cevap vermeli; cache katmanlarını, hot key'i ve negatif cache'i bilmek.
- HTTP semantiği: 301 ile 302 arasındaki fark bir "detay" değil, analytics ve link yönetimi özelliklerinin var olup olmamasını belirleyen bir karardır.
- Kapsam yönetimi: Analytics, yazma yolunun on katı büyüklüğünde bir veri akışıdır; yönlendirmeyi yavaşlatmamalıdır.
Netleştirme soruları ve varsayımlar
| Soru | Neden önemli | Varsayım |
|---|---|---|
| Günde kaç yeni kısa link oluşturuluyor? | Kod uzunluğu, depolama, ID üretim hızı | 100 milyon/gün |
| Okuma/yazma oranı nedir? | Cache boyutu, okuma kapasitesi | 10:1 (günde 1 milyar yönlendirme) |
| Kod ne kadar kısa olmalı, hangi karakterler? | Kod uzayı ve uzunluk hesabı | Mümkün olan en kısa; [0-9A-Za-z] (base62) |
| Kullanıcı kendi alias'ını seçebilir mi? | Benzersizlik kontrolü ve üretilen kodlarla çakışma | Evet, opsiyonel |
| Aynı uzun URL iki kez kısaltılırsa aynı kodu mu almalı? | Dedup maliyeti, kampanya bazlı istatistik | Hayır; her istek yeni kod (istenirse sahip bazında dedup) |
| Linkler silinebilir, hedefi değiştirilebilir, süresi dolabilir mi? | 301/302 kararı, cache invalidation | Evet; opsiyonel expiresAt |
| Linkler ne kadar saklanacak? | Depolama hesabı | 10 yıl |
| İstatistik gerçek zamanlı mı olmalı? | Senkron mu asenkron mu | Birkaç dakika gecikme kabul |
| Kodlar tahmin edilebilir olursa sorun mu? | Sıralı sayaç kullanılabilir mi | Evet sorun: özel dokümanlara verilen linkler taranmamalı |
Fonksiyonel olmayan gereksinimler: yönlendirme yolu için %99,99 erişilebilirlik ve sunucu tarafında p99 birkaç on milisaniyenin altında; oluşturma yolu için %99,9 yeterli. Yönlendirme, analytics'ten daha önemlidir: analytics bozulursa yönlendirme çalışmaya devam etmeli.
Kapsam dışı: kullanıcı yönetimi, faturalandırma, QR kod, link önizleme sayfası. Bunları sesli söylemek "unuttum" değil "kapsamı yönetiyorum" mesajı verir.
Kapasite tahmini
Yazma : 100.000.000 / 86.400 s ≈ 1.160 /s ortalama, tepe (x3) ≈ 3.500 /s
Okuma : 10 x 100M = 1 milyar /gün ≈ 11.600 /s ortalama, tepe (x3) ≈ 35.000 /s
10 yılda: 100M x 365 x 10 = 365 milyar kayıt
Kod uzayı (base62):
62^6 ≈ 56,8 milyar -> 365 milyara yetmez
62^7 ≈ 3,52 trilyon -> 365 milyarın ~9,6 katı, 7 karakter yeter
Kayıt boyutu: kod 7 B + uzun URL ort. 100 B + sahip, zaman damgaları ~50 B
+ depolama motoru ve index yükü ≈ 250 B
Depolama : 365 milyar x 250 B ≈ 91 TB, replikasyon faktörü 3 ile ≈ 274 TB
Cache : günlük okumaların %20'sini oluşturan URL'ler için üst sınır
1 milyar x 0,2 x 250 B ≈ 50 GB (aynı URL tekrar okunduğu için gerçekte daha az)
Analytics : 1 milyar tıklama/gün x ~200 B ≈ 200 GB/gün ham event
Bant : 11.600 /s x ~500 B (302 cevabı + header) ≈ 5,8 MB/s
Bu sayılardan çıkan teşhis, mülakatta söylenecek asıl cümledir:
- Okuma ağırlıklı ve okumalar tek anahtarla. Join yok, aralık sorgusu yok; anahtar-değer erişimi. Cache isabet oranı her şeydir.
- Yazma hızı küçük (binler/s), ama 10 yıllık veri yüzlerce TB. Tek bir PostgreSQL instance'ı değil, yatay bölünebilen bir depo gerekir.
- En büyük yazma akışı analytics'tir (günde 1 milyar event, link oluşturmanın 10 katı). Senkron yazılırsa yönlendirme yolunu boğar; asenkron olmalı.
API
POST /api/v1/urls
Idempotency-Key: 6f1c...
Content-Type: application/json
{ "longUrl": "https://www.ornek.com/kampanya?utm_source=x", "customAlias": "kasim-indirim", "expiresAt": "2027-01-01T00:00:00Z" }
-> 201 Created { "code": "aZ3kP9q", "shortUrl": "https://kisa.lt/aZ3kP9q" }
-> 400 Bad Request geçersiz URL, desteklenmeyen şema
-> 409 Conflict alias kullanımda
-> 429 Too Many Requests oluşturma limiti (bkz. rate-limiter case'i)
GET /{code}
-> 302 Found Location: https://www.ornek.com/kampanya?utm_source=x
-> 404 Not Found böyle bir kod yok
-> 410 Gone silinmiş veya süresi dolmuş
GET /api/v1/urls/{code}/stats?from=2026-10-01&to=2026-10-31&granularity=day
DELETE /api/v1/urls/{code}
Tasarım notları:
- Yönlendirme endpoint'i ayrı, kısa bir domain'de (
kisa.lt) ve path'in kökünde durur. Kimlik doğrulama yoktur; her milisaniye önemlidir. - Oluşturma
POST'u idempotency key alır: mobil istemci timeout sonrası tekrar denerse ikinci bir kod üretilmez (Kitap Bölüm 18). 410 Gone, "bu link vardı ama artık yok" bilgisini404'ten ayırır; tarayıcı ve arama motorları için anlamlıdır.
Veri modeli
Erişim deseni tek anahtarla okuma olduğu için doğal seçim geniş sütunlu ya da anahtar-değer bir depodur (Cassandra, DynamoDB benzeri). Ekip PostgreSQL'de uzmansa, kod üzerinden hash ile bölünmüş PostgreSQL shard'ları da işi görür; önemli olan partition anahtarının code olmasıdır.
-- Cassandra (CQL)
CREATE TABLE urls (
code text PRIMARY KEY, -- partition key: yük kodlar arasında eşit dağılır
long_url text,
owner_id bigint,
created_at timestamp,
expires_at timestamp,
status text -- ACTIVE, DELETED
);
-- "Linklerim" sayfası için ayrı bir sorgu tablosu (denormalizasyon)
CREATE TABLE urls_by_owner (
owner_id bigint,
created_at timestamp,
code text,
long_url text,
PRIMARY KEY ((owner_id), created_at, code)
) WITH CLUSTERING ORDER BY (created_at DESC, code ASC);
- Üretilen kodlar yapısı gereği benzersizdir (aşağıda); düz
INSERTyeterlidir. - Custom alias ise kullanıcıdan gelir ve çakışabilir. Cassandra'da koşullu yazma (
INSERT ... IF NOT EXISTS) Paxos tabanlı lightweight transaction kullanır ve normal yazmadan belirgin şekilde pahalıdır; ama sadece alias yolunda kullanıldığı için kabul edilir. - Üretilen kodlarla alias'ların çakışmaması için alias'lar ayrı bir biçimle sınırlanır (ör. en az 8 karakter ya da
-içermeli). Yine de koşullu yazma son savunmadır. - Tıklama event'leri bu depoya yazılmaz; analytics için ayrı, kolon tabanlı bir OLAP deposu (ClickHouse, Druid benzeri) kullanılır.
Yüksek seviye tasarım
flowchart LR
C[İstemci] --> EDGE[CDN ve edge]
EDGE --> LB[Load balancer]
LB --> RS[Redirect servisi]
LB --> WS[Kısaltma servisi]
WS --> SAFE[Zararlı URL kontrolü]
WS --> IDS[ID blok servisi]
WS --> DB[(URL deposu)]
RS --> LC[Pod içi cache]
RS --> RC[(Redis cache)]
RS --> DB
RS -->|click event, asenkron| K[(Kafka)]
K --> AGG[Analytics consumer]
AGG --> OLAP[(OLAP deposu)]Yazma yolu: İstek doğrulanır (şema sadece http/https, uzunluk sınırı), zararlı URL kontrolünden geçer, kısaltma servisi kendi belleğindeki ID bloğundan bir sayı alır, base62'ye çevirir, depoya yazar ve cevabı döner.
Okuma yolu: Redirect servisi önce pod içi cache'e, sonra Redis'e, en son depoya bakar; bulduğu hedefle 302 döner ve tıklama event'ini bloklamadan Kafka'ya bırakır.
Okuma ve yazma servisleri ayrı deploy edilir: farklı ölçeklenirler (okuma yazmanın 10 katı), farklı erişilebilirlik hedefleri vardır ve yazma tarafındaki bir hata yönlendirmeyi etkilememelidir.
Derinlemesine
ID üretimi: dört seçenek
1. Uzun URL'nin hash'i. SHA-256(longUrl)'nin ilk bitlerini base62'ye çevirip 7 karakter almak. Artısı: aynı URL aynı kodu alır (dedup bedava). Eksileri: 7 karaktere kesilmiş hash çakışır; her yazmada "bu kod başka bir URL'ye ait mi?" diye okumak, çakışırsa tuz ekleyip tekrar denemek gerekir. Ayrıca iki farklı kampanyanın aynı URL için ayrı istatistik istemesi mümkün olmaz.
2. Rastgele kod + koşullu ekleme. 7 rastgele base62 karakter üret, IF NOT EXISTS ile ekle, çakışırsa tekrar dene. Tahmin edilemez, koordinasyon gerektirmez. Ama çakışma olasılığı doluluk oranıyla büyür: 365 milyar kod kullanılmışken yeni bir denemenin çakışma olasılığı 365 milyar / 3,52 trilyon ≈ %10,4'tür. Her yazma da pahalı koşullu yazma olur. 8 karaktere çıkarsan (62^8 ≈ 218 trilyon) olasılık ≈ %0,17'ye düşer; bedeli bir karakter daha uzun link.
3. Sayaç + base62. Her yeni link için artan bir sayı al, base62'ye çevir. 62^7 uzayı tam doldurulabilir, çakışma imkansızdır. Sorun, sayacın kendisidir: tek bir veritabanı sequence'ine her istekte gitmek hem darboğaz hem tek hata noktasıdır. Çözüm blok tahsisi: bir ID servisi (ya da veritabanında tek satırlık bir sayaç) her kısaltma pod'una 10.000'lik bloklar verir; pod bloğu bellekte tüketir.
-- ID blok servisinin tek işi: atomik olarak bir blok ayırmak
UPDATE id_blocks
SET next_value = next_value + 10000
WHERE name = 'short-url'
RETURNING next_value - 10000 AS block_start;
Tepe yazma 3.500/s iken 10.000'lik bloklarla tüm filoda yaklaşık 3 saniyede bir blok istenir: sayaç artık darboğaz değildir. Pod bloğun yarısını tükettiğinde bir sonrakini önceden alır; ID servisi birkaç dakika çökse bile yazma devam eder. Pod çökerse bloğun kalanı kaybolur; kodlarda boşluk olması sorun değildir.
Sıralı sayacın kalan sorunu tahmin edilebilirliktir: aZ3kP9q'yu gören biri aZ3kP9r'yi deneyerek başkalarının linklerini tarayabilir. Çözüm, sayacı 62^7 uzayında gizli anahtarlı bir permütasyondan geçirmektir: her sayı uzayda başka bir sayıya, çakışmasız (bijektif) eşlenir. Basit bir afin dönüşüm (a * x + b) mod 62^7 ancak a modülle aralarında asalsa bijektiftir (62^7 = 2^7 x 31^7 olduğu için a tek ve 31'e bölünmeyen bir sayı olmalı), ama kolay tersine çevrilir. Daha sağlamı, anahtarlı birkaç turluk bir Feistel ağı gibi format koruyan bir şifrelemedir. Mülakatta "sayaç + gizli permütasyon" demek yeterli; matematiğine inmek bonus.
4. Snowflake tarzı 64 bit ID. Twitter'ın 2010'da duyurduğu Snowflake, ID'yi zaman damgası, worker numarası ve sıra numarasından oluşturur; amaç saniyede on binlerce ID'yi koordinasyonsuz, kabaca zamana göre sıralı ve 64 bite sığacak şekilde üretmektir. Yaygın bilinen düzen: 41 bit milisaniye (yaklaşık 69 yıl), 10 bit makine (1.024 worker), 12 bit sıra (worker başına milisaniyede 4.096 ID). Kısa link için uygun değildir: 63 bitlik bir sayı base62'de 11 karakter tutar (62^10 ≈ 8,4 x 10^17, 2^63 ≈ 9,2 x 10^18). Ayrıca saat geri giderse (NTP düzeltmesi) aynı ID üretilebilir; implementasyonlar bu durumda bekler ya da hata verir. Snowflake, sistemin iç ID'leri (tıklama event'i, kullanıcı) için iyi bir seçimdir.
| Yöntem | Uzunluk | Çakışma | Koordinasyon | Tahmin edilebilir mi |
|---|---|---|---|---|
| Hash + kesme | 7 | Var, doluluğa göre artar | Yok, ama her yazmada okuma | Hayır |
| Rastgele + koşullu ekleme | 7-8 | Var, retry ile çözülür | Yok, koşullu yazma pahalı | Hayır |
| Blok sayaç + base62 | 7 | Yok | Blok başına bir çağrı | Evet, permütasyonla hayır |
| Snowflake | 11 | Yok (saat doğruysa) | Worker id ataması | Kısmen (zaman sızar) |
Seçimim: blok tahsisli sayaç + gizli permütasyon + base62, sabit 7 karakter. Kod uzayının tamamı kullanılır, koşullu yazma gerekmez, ID servisi kritik yolda değildir.
public final class Base62 {
private static final char[] ALPHABET =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray();
private Base62() {
}
/** value'yu tam olarak length karakterlik base62 dizgesine çevirir (baştaki sıfırlar korunur). */
public static String encode(long value, int length) {
if (value < 0) {
throw new IllegalArgumentException("negatif değer: " + value);
}
char[] out = new char[length];
for (int i = length - 1; i >= 0; i--) {
out[i] = ALPHABET[(int) (value % 62)];
value /= 62;
}
if (value != 0) {
throw new IllegalArgumentException("değer " + length + " karaktere sığmıyor");
}
return new String(out);
}
}
Yönlendirme: 301 mi, 302 mi?
| 301 Moved Permanently | 302 Found | |
|---|---|---|
| Anlam | Kaynak kalıcı olarak taşındı | Geçici yönlendirme |
| Tarayıcı cache'i | Kalıcı yönlendirme olarak cache'lenebilir; açık Cache-Control olmasa da HTTP spesifikasyonu 301'i sezgisel (heuristic) olarak cache'lenebilir sayar | Varsayılan olarak her tıklamada sunucuya sorulur |
| Sunucu yükü | Tekrarlayan tıklamalar sunucuya hiç gelmeyebilir | Her tıklama sunucuya gelir |
| Analytics | Tekrarlayan tıklamalar görünmez | Her tıklama sayılır |
| Hedefi değiştirmek, silmek, süre sonu | Cache'lemiş tarayıcılarda etkisiz | Anında etkili |
| SEO | Arama motorları hedefi kanonik URL kabul eder, link değerini hedefe aktarır | Geçici sayılır; kanonik olarak genelde kısa URL tutulur |
POST isteğine gelen 301/302'de tarayıcılar metodu GET'e çevirebilir; metodun korunması gerekiyorsa 308/307 kullanılır. Kısa link tıklaması zaten GET olduğu için bu fark burada önemsizdir.
Karar: Gereksinimde analytics, silme ve süre sonu var; bu yüzden 302 (ya da 307) ve Cache-Control: private, max-age=0. Sadece trafik maliyetini düşürmek isteyen ve analytics istemeyen bir ürün için 301 mantıklıdır. Arada bir seçenek: CDN'de 60 saniyelik kısa TTL ile yönlendirmeyi edge'de cache'lemek ve tıklamaları CDN loglarından saymak. Bu, origin yükünü dramatik düşürür ama silme en fazla 60 saniye gecikir ve analytics hattı CDN log formatına bağlanır.
Okuma yolu: üç katmanlı cache
- Pod içi cache (Caffeine): En sıcak birkaç yüz bin kod; mikro saniyeler. Viral bir link Redis'teki tek bir shard'ı boğmadan burada emilir.
- Redis (cache-aside): Kod →
{longUrl, expiresAt, status}. Eşleme neredeyse hiç değişmediği için TTL uzun tutulur (ör. 24 saat + jitter); değişiklikte (silme, hedef güncelleme) anahtar commit sonrası silinir (Kitap Bölüm 11). Bellek politikasıallkeys-lruya daallkeys-lfu: cache olarak kullanılan Redis'tenoevictionyazmaları hataya düşürür. - Depo: Cache ıskalamasında tek anahtar okuması.
Dikkat edilecek üç durum:
- Negatif cache: Bot'lar ve yanlış yazılmış linkler var olmayan kodlar için gelir; her biri cache'i ıskalayıp depoya gider (cache penetration). "Yok" sonucu kısa TTL ile (ör. 60 saniye) cache'lenir. Alternatif: var olan kodların Bloom filter'ı.
- Süre sonu:
expiresAtcache'teki değerin içinde durur; redirect servisi kontrol edip410döner. Süresi dolan kayıtların fiziksel silinmesi ayrı bir temizlik işidir. Cassandra'da satır TTL'i kullanılabilir, ama silinen her satırın tombstone ürettiğini ve compaction'a kadar okuma maliyeti yarattığını unutma. - Cache-miss fırtınası: Redis düşerse tüm okuma depoya iner (11.600/s ortalama, tepe 35.000/s). Depo bu yükü kaldıracak şekilde boyutlandırılır ya da aynı kod için eşzamanlı ıskalamalar tek bir okumada birleştirilir (request coalescing).
Yazma yolu
- URL doğrulama: Sadece
httpvehttps;javascript:gibi şemalar reddedilir. Uzunluk sınırı (ör. 2.048 karakter). - Zararlı URL kontrolü: Bilinen phishing/malware listelerine karşı kontrol; şüpheli linkler oluşturulur ama bir uyarı sayfasına yönlendirilir. Liste sonradan güncellenirse var olan kodlar da işaretlenir (yine 302'nin bir faydası).
- Idempotency:
Idempotency-Key+ kullanıcı kapsamında unique kayıt; aynı anahtarla gelen ikinci istek ilk kodu döner. - Dedup (opsiyonel): Aynı sahip aynı URL'yi tekrar kısaltırsa eski kodu dönmek için
(owner_id, hash(long_url))üzerinde bir index. Global dedup istenmez: farklı kişilerin istatistikleri karışır.
Analytics: yönlendirmeyi yavaşlatmadan saymak
Redirect servisi her tıklamada bir event üretir: code, zaman, ülke (IP'den türetilmiş), cihaz tipi, referrer. Ham IP saklanmaz ya da kısaltılarak/hash'lenerek saklanır; kişisel veri (KVKK, GDPR) en baştan tasarıma girer.
- Asenkron gönderim: Kafka producer zaten batch'leyerek ve asenkron gönderir; uygulama
send()sonucunu beklemez. Ama dikkat: tampon (buffer.memory) dolarsa ya da metadata alınamazsasend()varsayılan olarakmax.block.mskadar (60 saniye) bloklar. Bu yüzdenmax.block.msçok küçük tutulur ve hata yakalanır: tampon dolunca event düşürülür ve bir metrik artar; yönlendirme bloklanmaz. Analytics best-effort'tur; yönlendirme değildir. Daha sıkı bir gereksinim varsa pod'da lokal disk tamponu kullanılır. - Partition anahtarı: Sıra gerekmediği için anahtar
nullya da rastgele.codeanahtar olursa viral link tek bir partition'ı ve tek bir consumer'ı ezer. - Toplama: Bir stream consumer event'leri dakika bazında
(code, dakika, ülke)sayaçlarına toplar ve OLAP'a yazar; ya da OLAP Kafka'dan ham event'leri doğrudan alıp kendi rollup'larını yapar. Tekil ziyaretçi sayısı için HyperLogLog gibi olasılıksal bir yapı (Redis'te %0,81 standart hatayla, kod başına en fazla 12 KB) yeterlidir. - Bot filtreleme: Link önizleme bot'ları (sohbet uygulamaları linki paylaşınca otomatik açar) tıklama sayısını şişirir; user-agent ve davranış kurallarıyla ayrılır.
- Idempotency gerekmez mi? Kafka en az bir kez teslim eder; aynı event iki kez sayılabilir. İstatistik için ±%0,1 hata çoğu zaman kabul edilir; faturalandırma bu sayılara bağlıysa event'e Snowflake ID verilip toplama sırasında tekilleştirilir.
Çok bölgeli dağıtım
Yönlendirme okuma yoludur ve kod → URL eşlemesi neredeyse değişmez; her bölgede bir okuma kopyası ve cache tutmak kolaydır. Yazmalar tek bir "ev" bölgesine gidebilir; asenkron replikasyon nedeniyle yeni oluşturulan link birkaç yüz milisaniye diğer bölgede görünmeyebilir. Çözüm: oluşturan kullanıcıya dönen ilk yönlendirmeyi ev bölgesinden okumak ya da diğer bölgede ıskalama olursa ev bölgesine sormak (read-repair benzeri). ID blokları bölge başına ayrı aralıklardan verilir; bölgeler arası koordinasyon gerekmez.
Darboğazlar, hatalar ve trade-off'lar
| Senaryo | Etki | Önlem |
|---|---|---|
| Redis cluster düşer | Tüm okuma depoya iner | Pod içi cache, depoyu cache'siz tepe yüke göre boyutlandırmak, request coalescing, circuit breaker |
| Viral link (hot key) | Tek Redis shard'ı ve tek depo partition'ı ısınır | Pod içi cache, CDN'de kısa TTL, gerekirse anahtarı kopyalamak |
| ID blok servisi düşer | Yeni link oluşturulamaz | Pod'larda önceden alınmış ikinci blok; dakikalarca tampon |
| Kafka yavaşlar | Producer tamponu dolar | Event düşür + metrik; yönlendirme etkilenmez |
| Bot'lar var olmayan kodları tarar | Cache penetration | Negatif cache, Bloom filter, IP bazlı rate limit |
| Saat geri gider (Snowflake) | Tekrarlanan ID | Saat geriye giderse üretimi durdur/bekle; kısa kod için zaten sayaç kullanılıyor |
| Zararlı link yayılır | Marka ve kullanıcı güvenliği | Oluşturmada kontrol, sonradan işaretleme, 302 sayesinde anında devre dışı bırakma |
Bilinçli trade-off'lar:
- 302 ve sunucu yükü: Analytics ve link yönetimi için her tıklamayı sunucuda karşılamayı seçtik; bedeli daha fazla trafik.
- Sayaç ve tahmin edilebilirlik: Kod uzayını verimli kullanmak için sayaç seçtik; tahmin edilebilirliği permütasyonla kapattık.
- Analytics best-effort: Yönlendirmenin erişilebilirliği için event kaybını kabul ettik ve ölçüyoruz.
- Tek ev bölgesi: Yazma tutarlılığı basit; yeni linkin diğer bölgede görünmesi birkaç yüz milisaniye gecikebilir.
Üç seviyede cevap
Soru: "Kısa kodu nasıl üretirsin? İki sunucu aynı kodu üretmesin."
seviye: Orta etiketler: [id-generation, base62] kaynak: Tahmini
Uzun URL'nin MD5 hash'ini alıp ilk 7 karakterini kullanırım. Aynı URL hep aynı kodu verir, çakışma olursa veritabanındaki unique constraint hata verir.
Hash'i kesmek çakışma üretir ve her yazmada kontrol gerektirir. Bunun yerine artan bir sayı alıp base62'ye çeviririm; 62^7 yaklaşık 3,5 trilyon, 10 yıllık 365 milyar link için yeter. Tek bir sequence darboğaz olmasın diye her sunucuya 10.000'lik bloklar veren bir ID servisi kullanırım; sunucular bloğu bellekte tüketir, çakışma imkansızdır.
Önce kod uzayını hesaplarım: 365 milyar link için 6 karakter (56 milyar) yetmez, 7 karakter (3,5 trilyon) yeter. Dört seçenek var: hash, rastgele, sayaç, Snowflake. Snowflake 64 bit olduğu için base62'de 11 karakter tutar, kısa link için uzun; iç ID'ler için kullanırım. Rastgele kod tahmin edilemez ama doluluk arttıkça çakışma artar, 365 milyarda her denemede yaklaşık yüzde 10, ve her yazma koşullu yazma olur. Seçimim blok tahsisli sayaç: ID servisi atomik bir UPDATE ... RETURNING ile 10.000'lik blok verir, tepe 3.500 yazma/s'de bu üç saniyede bir istek demek, pod'lar bir sonraki bloğu önceden alır, yani ID servisi kritik yolda değil. Sıralı sayaç linkleri taranabilir yapar; sayıyı 62^7 uzayında gizli anahtarlı bir permütasyondan geçiririm, böylece çakışmasızlık korunur ama ardışık kodlar ilişkisiz görünür. Custom alias ayrı bir biçimle sınırlanır ve koşullu yazmayla eklenir.
Junior çakışmayı veritabanına havale ediyor ama 7 karaktere kesilmiş hash'in ne sıklıkla çakışacağını ve bunun her yazmada okuma demek olduğunu düşünmüyor. Mid doğru mekanizmayı (blok tahsisi) buluyor. Senior seçenekleri sayılarla eliyor (Snowflake'in 11 karakteri, rastgelenin %10 çakışması), ID servisini kritik yoldan çıkarıyor ve sıralı sayacın güvenlik bedelini görüp kapatıyor. Fark, tek bir çözümü bilmek ile seçenekleri ölçülebilir kriterlerle karşılaştırmak arasında.
Takip sorusu: ID servisi bir blok verdi, pod bloğun yarısını kullanıp çöktü. Ne kaybedersin, bu bir problem mi?
Soru: "Yönlendirmede 301 mi döndürürsün, 302 mi?"
seviye: Başlangıç etiketler: [http, redirect] kaynak: Tahmini
301 döndürürüm çünkü link kalıcı, SEO açısından da doğrusu bu.
301'i tarayıcı cache'ler; ikinci tıklama sunucuya gelmez ve istatistik kaybolur. İstatistik istendiği için 302 döndürürüm. 301 sunucu yükünü azaltır, analytics istenmiyorsa onu seçerim.
Bu bir ürün kararı, gereksinime bağlarım. 301 kalıcıdır ve açık cache başlığı olmasa bile cache'lenebilir; tekrar eden tıklamalar sunucuya gelmez, yani analytics eksik kalır, linki silmek veya hedefini değiştirmek o tarayıcılarda etkisiz olur, zararlı çıkan bir linki kapatamam. Gereksinimde analytics, süre sonu ve silme olduğu için 302 ve Cache-Control: private, max-age=0 döndürürüm. Yükü azaltmak gerekirse ara çözüm CDN'de 60 saniyelik TTL ile edge cache: origin yükü düşer, silme en fazla bir dakika gecikir, tıklamaları CDN loglarından sayarım. Metot korunması gereken bir senaryo olsaydı 307/308 kullanırdım, ama kısa link tıklaması zaten GET.
Junior ezber bir cevap veriyor ve analytics gereksinimini kaçırıyor. Mid temel trade-off'u biliyor. Senior kararı gereksinimlere (analytics, silme, güvenlik) bağlıyor, cache davranışının sonuçlarını sayıyor ve iki uç arasında edge cache gibi üçüncü bir seçenek öneriyor. Mülakatçı burada "doğru cevap"tan çok kararın gerekçesini dinler.
Takip sorusu: Pazarlama ekibi "301'e geçelim, trafik maliyeti yarıya iner" diyor. Neyi kaybedeceğimizi nasıl ölçer ve nasıl karar verirsin?
Soru: "Bir link viral oldu, saniyede 100 bin tıklama alıyor. Ne olur?"
seviye: İleri etiketler: [hot-key, cache] kaynak: Tahmini
Redis cache'te olduğu için sorun olmaz, Redis çok hızlı.
Tek bir anahtar olduğu için Redis Cluster'da tek bir node'a gider, o node ısınır. Pod içinde Caffeine ile kısa ömürlü yerel cache koyarım; istekler Redis'e hiç gitmez. Analytics tarafında da Kafka key'i code olursa tek partition ısınır, key'siz gönderirim.
Üç yerde etkisi olur: cache, depo ve analytics. Cache'te tek anahtar tek slot'a, tek node'a düşer; pod içi cache bunu emer, viral linkte isabet oranı neredeyse yüzde yüz olur ve Redis'e pod başına saniyede bir istek bile gitmez. Gerekirse CDN'de kısa TTL'li edge cache ile istek origin'e hiç gelmez. Depo cache ıskalamasında sadece bir kez okunur; aynı anahtar için eşzamanlı ıskalamaları tek yükleyicide birleştiririm. Asıl sürpriz analytics'te: event'leri code ile key'lersem 100 bin event/s tek partition'a ve tek consumer'a gider, lag büyür. Sıra gerekmediği için key'siz gönderir, toplamayı consumer'da yaparım. Bu senaryoyu yük testinde tek anahtar profiliyle denerim; ortalama dağılımlı test bunu göstermez.
Junior "Redis hızlı" diyerek hot key kavramını kaçırıyor. Mid cache tarafını doğru çözüyor. Senior aynı problemin cache, depo ve analytics hattında farklı yüzlerini görüyor ve özellikle Kafka partition anahtarının yarattığı ikinci darboğazı yakalıyor. Ayrıca bunun ortalama dağılımlı yük testinde görünmeyeceğini söylemesi operasyonel olgunluk sinyali.
Takip sorusu: Pod içi cache'i olan bir linki sahibi sildi. Silme kaç saniyede tüm pod'lara yansır, bunu nasıl kısaltırsın?
Mülakatçı için
- Kapasite hesabına geçmeden önce yazma hacmi, okuma/yazma oranı, saklama süresi ve analytics gereksinimini soruyor.
- Kod uzayını hesaplayıp 7 karakteri gerekçelendiriyor (62^6 ve 62^7'yi 365 milyarla karşılaştırıyor).
- En az üç ID üretim yöntemini karşılaştırıyor ve seçimini çakışma, uzunluk ve koordinasyon üzerinden savunuyor.
- Tek bir global sayacın darboğaz/tek hata noktası olduğunu görüp blok tahsisi gibi bir çözüm öneriyor.
- Sıralı kodların tahmin edilebilirlik riskinden kendiliğinden bahsediyor.
- 301/302 kararını analytics, silme ve cache davranışına bağlıyor.
- Okuma yolunda cache katmanlarını, negatif cache'i ve hot key'i ele alıyor.
- Analytics'i yönlendirme yolundan ayırıyor (asenkron, best-effort) ve Kafka partition anahtarının etkisini düşünüyor.
- Okuma ve yazma servislerini ayrı ölçeklenen bileşenler olarak çiziyor.
- En az iki arıza senaryosu (Redis düşmesi, ID servisi düşmesi) için davranışı anlatıyor.
- Kişisel veri (IP) ve kötüye kullanım (phishing, tarama) konularına değiniyor.
Takip soruları:
- Custom alias ile üretilen kodlar nasıl çakışmaz? Koşullu yazmanın maliyeti nedir?
- Bir link silindi; Redis'te, pod içi cache'te ve CDN'de ne kadar yaşar?
- Analytics'te "son 5 dakikada en çok tıklanan 10 link" isteniyor. Hattı nasıl değiştirirsin?
- 10 yılda 274 TB: süresi dolan linkleri nasıl temizlersin, tombstone ne getirir?
Seviye beklentisi: Mid seviyede blok sayaç + base62, cache-aside ve 302 kararı yeterlidir. Senior'dan seçeneklerin sayılarla karşılaştırılması, hot key'in analytics hattındaki etkisi, tahmin edilebilirlik ve arıza modları beklenir. Sadece "hash al, DB'ye yaz, Redis koy" diyen ve sayı kullanmayan aday zayıf sinyal verir.
Hızlı tekrar
S: 365 milyar link için base62 kaç karakter gerekir?
C: 7. 62^6 ≈ 56,8 milyar yetmez; 62^7 ≈ 3,52 trilyon, ihtiyacın yaklaşık 9,6 katı.
S: Global sayacın darboğazını blok tahsisi nasıl çözer?
C: ID servisi her pod'a 10.000'lik blok verir; pod bloğu bellekte tüketir ve sıradakini önceden alır. 3.500 yazma/s'de birkaç saniyede bir istek; ID servisi kritik yolda değildir.
S: Snowflake ID neden kısa link için uygun değil?
C: 64 bitliktir; base62'de 11 karakter tutar. Ayrıca saat geri giderse çakışma riski vardır. İç ID'ler için uygundur.
S: Rastgele 7 karakterlik kodda 365 milyar kod doluyken çakışma olasılığı?
C: Yaklaşık 365 milyar / 3,52 trilyon ≈ %10,4 (deneme başına). 8 karakterde yaklaşık %0,17.
S: 301 ile 302'nin analytics açısından farkı?
C: 301 kalıcıdır ve tarayıcıda cache'lenebilir; tekrarlayan tıklamalar sunucuya gelmez. 302'de her tıklama sunucuya gelir, sayılır; silme ve hedef değişikliği anında etkilidir.
S: Negatif cache neyi çözer?
C: Var olmayan kodlar için gelen isteklerin her seferinde depoya gitmesini (cache penetration). "Yok" sonucu kısa TTL ile cache'lenir.
S: Tıklama event'lerinde Kafka anahtarı neden code olmamalı?
C: Viral link tek partition'ı ve tek consumer'ı ezer. Sıra gerekmediği için anahtarsız gönderilir, toplama consumer'da yapılır.
S: Sıralı sayacın tahmin edilebilirliği nasıl kapatılır?
C: Sayı, 62^7 uzayında gizli anahtarlı bijektif bir permütasyondan (ör. Feistel tabanlı format koruyan şifreleme) geçirilir; çakışmasızlık korunur, ardışık kodlar ilişkisiz görünür.
Kaynaklar
- Alex Xu, System Design Interview: An Insider's Guide (Cilt 1): "Design a URL Shortener" ve "Design a Unique ID Generator in Distributed Systems" bölümleri.
- Martin Kleppmann, Designing Data-Intensive Applications: Partitioning ve replikasyon bölümleri.
- Twitter Engineering: Announcing Snowflake: Zaman damgası + worker + sıra numarasından oluşan, kabaca sıralı 64 bit ID'nin gerekçesi.
- MDN: 301 Moved Permanently: Kalıcı yönlendirme, SEO etkisi ve metodun korunması için 308.
- MDN: 302 Found: Geçici yönlendirme, 307 ve 303 alternatifleri.
- Redis: Key eviction:
maxmemory-policyseçenekleri ve yaklaşık LRU/LFU.