İçeriğe atla

Benzersiz ID Üretici Sistem Tasarımı: Snowflake, UUIDv7 ve ULID Karşılaştırması

Dağıtık sistemde benzersiz ID nasıl üretilir: sequence, UUIDv4, UUIDv7, ULID ve Snowflake karşılaştırması, ardından adım adım Snowflake tarzı bir üretici tasarımı.

· Kurucu ve geliştirici

Yayımlandı:

Mimari diyagram. Payment ve order servisleri, 0'dan 2'ye worker ID'lere sahip üç instance'lı bir ID servisinde NextId() çağırır; ID servisi etcd kümesinden ID kiralar ve saati NTP ile senkronlar. Order servisi Postgres'e satır ekler. Bir çubuk 64 biti 1, 41, 10, 12 olarak böler.

Order servisiniz yıllardır Postgres BIGSERIAL anahtarı kullanıyor. Sonra orders tablosu dört shard'a bölünüyor, her shard'ın sequence'i 10452 numaralı siparişi veriyor ve destek ekibi tek sipariş numarasının arkasında iki müşteri buluyor. Ya da mobil uygulamanın sunucuya hiç ulaşamadan bir ID'ye ihtiyacı var.

ID'ler, birden fazla makine ürettiğinde bedava olmaktan çıkar. Seçenekler boyutu, sıralamayı, ID başına koordinasyonu ve B-tree indeksine ne kadar nazik davrandıklarını takas eder. Bu yazı sequence'leri, ticket server'ları, UUIDv4, UUIDv7, ULID ve Snowflake tarzı ID'leri karşılaştırır, ardından Snowflake tarzı bir üretici tasarlar: bit düzeni, worker ID lease'leri, saat sıçramaları ve çakışma hesabı.

Dağıtık ID'ler için gereksinimler

  • Benzersiz, sonsuza dek. Shard'lar, bölgeler ve yeniden başlatmalar boyunca.
  • Zamana göre kabaca sıralı, böylece WHERE id > ? sayfalaması ve zamana göre sıralı indeksler çalışır. Twitter'ın orijinal Snowflake'i makineler arasında bir saniye içinde sıralama vaat ediyordu.
  • Kompakt. 64 bit bir BIGINT'e sığar; 128 bit her anahtarı ikiye katlar.
  • ID başına koordinasyon yok. Snowflake'in hedefi süreç başına saniyede en az 10.000 ID'ydi, her biri 2 ms içinde.
  • Açığa çıkarması güvenli, istediğinizden fazlasını sızdırmayan.

ID üretmenin altı yolu

Yaklaşım Boyut Sıralama Koordinasyon B-tree insert'leri
Veritabanı sequence'i 64 bit Artan Her ID tek bir veritabanından Sağ uç
Ticket server 64 bit Sunucu başına artan ID başına veya aralık başına bir çağrı Sağ uç
UUIDv4 128 bit Yok Yok Rastgele sayfalar
UUIDv7 128 bit Milisaniyeye göre Yok Sağ uca yakın
ULID 128 bit Milisaniyeye göre Yok Sağ uca yakın
Snowflake tarzı 64 bit Milisaniyeye göre, kabaca Üretici başına bir worker ID Sağ uca yakın

Veritabanı sequence'i, tüm satırları tek bir veritabanı yazdığı sürece doğru seçimdir; birden fazla veritabanı aynı ID alanını paylaşınca bozulur. nextval asla geri alınmadığı için boşluklar bırakır, PostgreSQL'de 1'den büyük bir CACHE ise değerleri farklı tutar ama oturumlar arasında sırasız hale getirir. Ardışık sipariş numaraları ayrıca sipariş hacminizi ele verir.

Ticket server sequence'i merkezileştirir. Flickr'ın MySQL ticket server'ları iki makineydi; biri tek, diğeri çift sayılar veriyor, istemciler ikisi arasında dönüşümlü gidiyordu. ID bloklarını kiralamak, URL kısaltıcı tasarımında olduğu gibi ID başına çağrıyı ortadan kaldırır.

UUIDv7, ULID ve Snowflake zaman damgasını başa koyar, böylece birlikte üretilen ID'ler birlikte sıralanır; yalnızca Snowflake 64 bite sığar ve bunun bedelini worker ID'lerle ve saat bağımlılığıyla öder.

Snowflake bit düzeni

Bit Alan Kapasite
1 İşaret biti, her zaman 0 ID'ler işaretli 64 bit tamsayı olarak pozitif kalır
41 Özel bir epoch'tan beri milisaniye 2^41 ms, yaklaşık 69,7 yıl
10 Worker ID 1.024 üretici
12 Milisaniye içindeki sıra numarası Worker başına milisaniyede 4.096 ID

Twitter'ın 2010 sürümü bu bölünmeyi Kasım 2010'daki bir epoch ile kullandı; bu yüzden ID'leri Temmuz 2080'de tükenir. Unix epoch'undan başlarsa 41 bit Eylül 2039'da tükenirdi, bu yüzden epoch'u yayına çıkış tarihinize yakın seçin. Her instance son milisaniyesini ve bir sayaç tutar:

// Next is called with g.mu held.
func (g *Generator) Next() (int64, error) {
	if time.Now().After(g.leaseDeadline) {
		return 0, ErrLeaseLost // another instance may own this worker ID now
	}
	now := time.Now().UnixMilli()
	if now < g.lastMs {
		if g.lastMs-now > 5 {
			return 0, ErrClockMovedBack
		}
		now = g.waitUntil(g.lastMs) // small step back: wait it out
	}
	if now == g.lastMs {
		g.seq = (g.seq + 1) & 0xFFF // 12 bits
		if g.seq == 0 {             // 4,096 IDs used in this millisecond
			now = g.waitUntil(g.lastMs + 1)
		}
	} else {
		g.seq = 0
	}
	g.lastMs = now
	return (now-epochMs)<<22 | g.workerID<<12 | g.seq, nil
}

Bir worker saniyede 4,1 milyona kadar ID verir; asıl sınır etrafındaki RPC'dir, bu yüzden istemcilerin ID'leri toplu çekmesine izin verin. ID'ler epoch'tan yaklaşık 25 gün sonra 2^53'ü geçer ve JavaScript'in Number.MAX_SAFE_INTEGER değerini aşar; bu yüzden X ve Discord API'lerinin yaptığı gibi onları JSON string olarak gönderin.

Bölünme bir bütçedir. Discord zaman için 42 bit kullanır ve node bitlerini worker ve process olarak ayırır. Instagram, 13 bitlik bir shard ID ile ID'leri her PostgreSQL shard'ının içinde üretti; bu, veritabanını shard'ladığınızda işe yarar. Sonyflake, 174 yıl dayanmak için 10 ms'lik birimleri sayar ve instance başına 10 ms'de 256 ID verir.

Worker ID'lerin atanması

Snowflake'in ID başına koordinasyona ihtiyacı yoktur, yalnızca süreç başına gerekir: aynı worker ID'ye sahip iki canlı üretici aynı ID'yi verebilir.

  1. Statik yapılandırma. Her host bir WORKER_ID alır. Sabit bir filo için uygundur; kopyalanmış tek bir yapılandırma dosyası yinelenenler yaratır.
  2. StatefulSet sıra numaraları. Kubernetes bir StatefulSet'in pod'larını 0'dan başlayarak numaralandırır ve her birine apps.kubernetes.io/pod-index etiketini koyar (v1.32'den beri kararlı); bunu downward API iletir. Sıra numaraları yalnızca tek bir kümedeki tek bir StatefulSet içinde benzersizdir ve bir pod'u zorla silmek iki pod'un aynı sıra numarasına sahip kalmasına yol açabilir.
  3. etcd veya ZooKeeper'da lease'ler. Açılışta instance, anahtar yoksa başarılı olan bir transaction ile en düşük boş anahtarı, örneğin /idgen/workers/17, 3 saniyede bir yenilenen 10 saniyelik bir lease üzerinde talep eder. Instance ölürse anahtarın süresi dolar. Twitter'ın 2010 Snowflake'i bir ZooKeeper ephemeral node kullandı ve saati eşlerinin ortalamasından 10 saniyeden fazla sapıyorsa başlamayı reddetti.
env:
  - name: WORKER_ID
    valueFrom:
      fieldRef:
        fieldPath: metadata.labels['apps.kubernetes.io/pod-index']

Lease'ler iki kural gerektirir. Son başarılı keepalive, verilen TTL eksi bir marjdan daha eskiyse ID vermeyi bırakın; böylece duraklatılmış veya ağdan kopmuş bir instance, başkası ID'sini talep edemeden durur. Ve bir high-water mark kaydedin: birkaç saniyede bir, ayrı bir anahtarda saatin biraz ilerisinde bir zaman damgası saklayın, ondan ötesine asla ID vermeyin ve bir sonraki sahibin saati onu geçene kadar beklemesini sağlayın. etcd ikisini de Raft ile tutarlı tutar.

Saat geriye gittiğinde

NTP daemon'ları genellikle saati slew eder, yani biraz hızlı veya yavaş çalıştırır; ancak büyük sapmalarda step yapar: ntpd varsayılan olarak 128 ms üzerinde step yapar ve chrony'nin varsayılan leap second modunda, eklenen bir artık saniye için çekirdek saati bir saniye geri alır. chrony belgeleri step'i yalnızca açılışta yapmayı önerir:

makestep 1.0 3      # step (if off by over 1 s) only in the first three updates
leapsecmode slew    # absorb a leap second by slewing, not by stepping back

Geçip giden bir step için bir kural seçin:

Strateji Üretici ne yapar Maliyet
Fail Saat last_ms değerini geçene kadar istekleri reddeder Step sürdüğü sürece hata
Wait Saat last_ms değerini geçene kadar uyur Birkaç ms için uygun; uzun step'ler her çağıranı bekletir
Logical clock last_ms değerinden saymaya devam eder Hiç bloklamaz; zaman damgaları gerçek zamanın ilerisine geçer

Twitter'ın orijinali fail ediyordu. Birkaç milisaniye beklemek, ardından bir alarmla fail etmek mantıklı bir varsayılandır. Asla geriye gitmeyen CLOCK_MONOTONIC ile zamanlama çalışan bir süreci korur; high-water mark yeniden başlatmaları kapsar.

UUIDv7 ve B-tree indeksleri

Mayıs 2024'te yayımlanan RFC 9562, RFC 4122'nin yerini aldı ve UUIDv7'yi tanımladı: milisaniye cinsinden 48 bitlik Unix zaman damgası, 4 sürüm biti, 2 variant biti ve rastgelelik için 74 bit; milisaniye içinde kesin sıra gerekiyorsa bu bitler sayaç için kullanılır. Zaman damgası 10889 yılına kadar yeter.

RFC, zaman sırası olmayan UUID'lerin indeks yerelliğinin zayıf olduğu konusunda uyarır; B-tree'ler üzerindeki etkileri çarpıcı olabilir. PostgreSQL'de yeni v7 anahtarları en sağdaki yaprak sayfaya gider ve oradaki split'ler sayfaları varsayılan B-tree fillfactor değeri olan %90 dolulukta bırakır. Rastgele v4 anahtarları herhangi bir sayfaya çarpar: indeksin tamamı çalışma kümesi olur, dolu sayfalar split olur ve her checkpoint'ten sonra her sayfadaki ilk değişiklik sayfanın tamamını WAL'a yazar. MySQL'in InnoDB'si ayrıca birincil anahtarı her ikincil indekse kopyalar, bu yüzden 16 baytlık bir anahtar hepsini büyütür.

PostgreSQL 18, Eylül 2025'te uuidv7() işlevini ekledi. UUID'leri 36 karakterlik metin olarak değil, 16 baytlık ikili olarak saklayın. Ayrıca v7 oluşturulma zamanını açığa çıkarır; RFC, bir ID'nin güvenlikle ilgili bir rol oynadığı her yerde v4 önerir.

ULID

ULID spesifikasyonu aynı yapıya sahiptir: 48 bitlik milisaniye zaman damgası ve 80 rastgele bit. I, L, O ve U içermeyen 26 karakterlik Crockford base32 metni string olarak doğru sıralanır ve URL'lerde iyi okunur. Monotonic mod bir milisaniye içinde 1 ekler; bu sırayı korur ama sonraki değeri tahmin edilebilir yapar. ULID bir topluluk spesifikasyonudur; veritabanı anahtarları için UUIDv7 aynısını yerel destekle sunar.

Çakışma ihtimali ne kadar?

b rastgele bitli n ID için herhangi bir çakışmanın olasılığı yaklaşık n² / 2^(b+1)'dir. Zaman önekli ID'ler yalnızca aynı milisaniye içinde çakışabilir, bu yüzden toplam değil hız önemlidir. Bir yıl boyunca saniyede 1 milyon ID, eşit dağılımla:

Şema Rastgele bit Çakışabileceği ID'ler Bir yılda olasılık
UUIDv4 122 Diğer herhangi bir ID Yaklaşık 10 milyarda 1
ULID 80 Aynı milisaniyedeki ID'ler Yaklaşık 77 milyonda 1
UUIDv7 74 Aynı milisaniyedeki ID'ler Yaklaşık 1,2 milyonda 1
Snowflake 0 Aynı worker ID ve milisaniye Sıfır; worker ID'ler benzersiz ve saatler ileri gittiği sürece

Rastgele şemalar küçük, hesaplanabilir bir olasılıkla başarısız olur. Snowflake yalnızca operasyonel hatalarla başarısız olur, ama o zaman kesin olarak. Her iki durumda da bir unique constraint tutun.

Snowflake tarzı ID üretici diyagramı

Mimari diyagram. Payment ve order servisleri, 0'dan 2'ye worker ID'lere sahip üç instance'lı bir ID servisinde NextId() çağırır; ID servisi etcd kümesinden ID kiralar ve saati NTP ile senkronlar. Order servisi Postgres'e satır ekler. Bir çubuk 64 biti 1, 41, 10, 12 olarak böler.

18 bileşen · 5 bağlantı

Archboard'da aç
Diyagram içeriği
  • ID service · 3 replicas
  • id-gen-0 · worker ID 0
  • id-gen-1 · worker ID 1
  • id-gen-2 · worker ID 2
  • Payment service
  • Order service
  • orders · id BIGINT PRIMARY KEY
  • etcd cluster
  • NTP servers
  • 0
  • 41 bits · milliseconds since the custom epoch 2^41 ms ≈ 69.7 years
  • 10 bits · worker ID 1,024 instances
  • 12 bits · sequence 4,096 IDs per ms
  • NextId()
  • INSERT row with the new id
  • lease worker ID
  • time sync
Snowflake tarzı bir ID servisi: üç instance etcd'den worker ID kiralar, saati NTP ile senkronlar ve NextId() çağrılarını bellekten yanıtlar. Benzersiz worker ID'ler ve geriye gitmeyen bir saat her 64 bitlik ID'yi benzersiz tutar; çubuk bit düzenini gösterir.

Nasıl okunur:

  • İstemciler üç instance'tan herhangi birinde NextId() çağırır; instance'lar bellekten yanıtlar: son milisaniye ve 12 bitlik bir sayaç.
  • etcd her worker ID'yi 10 saniyelik bir lease üzerinde tutar, ayrıca kaydedilmiş zaman damgalarını.
  • NTP saatleri birbirine yakın tutar (kesikli kenar); chrony yalnızca açılışta step yapar.
  • Order servisi satırları Postgres'e yazar; artan ID'ler orada en sağdaki B-tree sayfasına düşer.
  • Çubuk 64 biti ölçekli gösterir; Markdown bloğu ayarları listeler.

ID üretimini net çizmek

  • Bit düzenini ölçekli çizin. Bütçeyi bir bakışta gösterir.
  • Worker ID'lerin nereden geldiğini gösterin, lease TTL ile birlikte. Yinelenenler orada başlar.
  • Saati bir bağımlılık olarak çizin. Üretici ancak saati kadar doğrudur.
  • Hata kurallarını tuvale yazın: saat geri gitti, lease kayboldu, yeniden başlatma. Sistem tasarımı diyagramı örneği sayıları kutuların yanına aynı şekilde koyar.

Archboard'da oluşturun

  1. Google veya GitHub ile giriş yapın ve bir diyagram oluşturun.
  2. ID servisi için Temel paketinden bir Çerçeve ekleyin; içine C4 Model paketinden üç kehribar rengi Konteyner düğümü yerleştirin: id-gen-0 ile id-gen-2 arası, her birine bir worker ID.
  3. Sola ödeme ve sipariş servisleri için iki Konteyner düğümü ekleyin, altlarına da Veri paketinden bir Postgres düğümü.
  4. Sağa etcd için mor bir Konteyner, NTP için Ağ paketinden mavi bir Sunucu ve ayarlarla birlikte Temel paketinden bir Markdown bloğu ekleyin.
  5. Bunları bağlayın: her iki istemciden çerçeveye NextId(), Postgres'e INSERT, etcd'ye lease ve NTP'den kesikli bir zaman senkronu kenarı.
  6. Aşağıya, bit düzeni için Temel paketinden 1 : 41 : 10 : 12 oranında boyutlandırılmış dört dikdörtgen Elips düğümü ve birkaç Temel Metin notu ekleyin.

Tasarımın bir Mermaid akış şeması, Mermaid dönüştürücü ile düzenlenebilir bir ilk taslağa dönüşür. Örnek 14 diyagram düğümü kullanıyor ve Free planın sınırları içinde kalıyor.

SSS

Snowflake ID nedir? Bir zaman damgasından, bir worker ID'den ve milisaniye başına bir sıra numarasından oluşan, Twitter'ın 2010'da tanıttığı 64 bitlik ID. Üreticiler birbirleriyle konuşmadan çalışır ve ID'ler kabaca oluşturulma zamanına göre sıralanır.

UUIDv7, ULID'den daha mı iyi? İkisi de 48 bitlik milisaniye zaman damgası ve rastgele bitler taşır. UUIDv7 veritabanı desteği olan bir IETF standardıdır, örneğin PostgreSQL 18'deki uuidv7(); ULID'in metin biçimi daha kısadır. Veritabanı anahtarları için UUIDv7'yi tercih edin.

Birincil anahtar olarak UUIDv4 mü UUIDv7 mi kullanmalıyım? Çoğu tablo için UUIDv7: yeni değerler eskilerden sonra sıralanır, bu yüzden insert'ler indeksin sağ ucuna yakın kalır. Bir ID'nin oluşturulma zamanını açığa çıkarmaması gereken yerde UUIDv4 kullanın.

Bir Snowflake üreticisi saniyede kaç ID üretebilir? Worker başına milisaniyede 4.096, yani saniyede yaklaşık 4,1 milyon; 1.024 worker genelinde 4,2 milyar. Üreticinin etrafındaki ağ çağrısı genellikle asıl sınırdır.

Saat geriye giderse ne olur? Üretici, kullanmış olabileceği bir milisaniyeyi yeniden kullanmamalıdır. Saat yetişene kadar fail edebilir, bekleyebilir veya gerçek zamanın ilerisinde giden bir logical clock tutabilir; Twitter'ın orijinali fail ediyordu.

Ücretsiz başlayın — tek kullanıcı, kredi kartı gerekmez.

İlgili yazılar

← Blog'a dön

Bir dakikada canvas açın

Google veya GitHub ile giriş yapın. Tek koltuk ücretsiz; ekip katılınca planı yükseltin.