Mert Tosun
← Yazılar
PostgreSQL Sharding ve GORM ile Kullanim: Detayli Teknik Rehber

PostgreSQL Sharding ve GORM ile Kullanim: Detayli Teknik Rehber

Blog YazariVeritabani

Tek bir PostgreSQL instance'i bir noktadan sonra CPU, IOPS, storage ve baglanti limitlerine takilir. Vertical scaling bir sure daha nefes aldirir ama buyume devam ediyorsa er ya da gec yatay olceklenme ihtiyaci dogar. Bu noktada sharding devreye girer.

Bu yazida PostgreSQL sharding modelini, tasarim kararlarini ve Go ekosisteminde GORM ile nasil uygulanabilecegini detayli olarak ele aliyoruz.

Sharding Nedir?

Sharding, veriyi birden fazla bagimsiz veritabani node'una yatay olarak bolme teknigidir. Her node datanin bir alt kumesini tutar. Uygulama katmani, kaydin hangi shard'da oldugunu bilir ve sorguyu ilgili node'a yonlendirir.

Cizim: Router + Shard Mimarisi

                    +-----------------------+
                    |   API / Service       |
                    +-----------+-----------+
                                |
                       shard key hesapla
                                |
                    +-----------v-----------+
                    |   Query Router        |
                    |  (hash/range map)     |
                    +----+----------+-------+
                         |          |
              +----------+--+    +--+----------+
              |  Shard-01  |    |  Shard-02   |
              | PostgreSQL |    | PostgreSQL  |
              +------+-----+    +------+------+
                     |                 |
                     |                 |
                 +---v---+         +---v---+
                 | Read  |         | Read  |
                 |Replica|         |Replica|
                 +-------+         +-------+

Bu yapida "global tek tablo" yoktur; her shard kendi tablosunu tutar.

Ne Zaman Partitioning Yetmez, Sharding Gerekir?

Partitioning ve sharding farkli problemleri cozer:

  • Partitioning: Tek cluster icinde tablo yonetimi ve pruning optimizasyonu
  • Sharding: Veriyi birden fazla bagimsiz cluster/node'a dagitma

Asagidaki durumlarda sharding ciddi aday olur:

  • Tek node kaynak limitlerine duzenli yaklasma
  • Yazma trafigi tek node'u saturate etmesi
  • Multi-tenant modelde tenant bazli izolasyon ihtiyaci
  • Buyuk veri hacminde operational blast radius'i kucultme hedefi

Shard Key Secimi (En Kritik Karar)

Yanlis shard key tum mimariyi kilitleyebilir. Iyi shard key su ozellikleri tasir:

  • Yuksek kardinalite
  • Duzgun dagilim
  • Sorgu desenleriyle uyum
  • Gelecekte yeniden shardlama maliyetini minimize etme

Yaygin yaklasimlar

  1. Tenant ID tabanli
    • SaaS sistemlerde en yaygin
    • Veri izolasyonu ve uyumluluk avantajli
  2. User ID hash tabanli
    • Genis user dagiliminda iyi load balancing
  3. Zaman tabanli shard
    • Bazi event sistemlerinde kullanilir ancak hotspot riski vardir

Cizim: Hash Tabanli Yönlendirme

shard = hash(tenant_id) % N

tenant_1001 -> hash -> 1 -> shard_1
tenant_1002 -> hash -> 0 -> shard_0
tenant_1003 -> hash -> 3 -> shard_3
tenant_1004 -> hash -> 1 -> shard_1

Not: N degistiginde tum dagilim bozulabilir. Bu nedenle consistent hashing veya virtual node stratejileri sik kullanilir.

PostgreSQL Tarafinda Shard Semasi

Her shard ayni schema'yi tasir:

CREATE TABLE orders (
  id           bigserial PRIMARY KEY,
  tenant_id    bigint NOT NULL,
  customer_id  bigint NOT NULL,
  amount       numeric(12,2) NOT NULL,
  created_at   timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX idx_orders_tenant_created
ON orders (tenant_id, created_at DESC);

Uygulama tarafinda kural net: tenant_id olmadan sorgu yok. Aksi halde cross-shard scan kacirilmaz hale gelir.

GORM ile Sharding Mimarisine Giris

GORM tek basina "dagitimi otomatik yoneteyim" demez. Genellikle uygulama katmaninda bir router katmani yazilir ve dogru *gorm.DB pool secilir.

1) Shard Manager yapisi

type ShardManager struct {
    shards []*gorm.DB
}

func (sm *ShardManager) shardIndex(tenantID int64) int {
    h := fnv.New32a()
    _, _ = h.Write([]byte(strconv.FormatInt(tenantID, 10)))
    return int(h.Sum32()) % len(sm.shards)
}

func (sm *ShardManager) DBForTenant(tenantID int64) *gorm.DB {
    return sm.shards[sm.shardIndex(tenantID)]
}

2) Repository katmaninda routing

type Order struct {
    ID         int64     `gorm:"primaryKey"`
    TenantID   int64     `gorm:"index"`
    CustomerID int64
    Amount     float64
    CreatedAt  time.Time
}

type OrderRepo struct {
    shardManager *ShardManager
}

func (r *OrderRepo) Create(ctx context.Context, o *Order) error {
    db := r.shardManager.DBForTenant(o.TenantID).WithContext(ctx)
    return db.Create(o).Error
}

func (r *OrderRepo) ListByTenant(ctx context.Context, tenantID int64, limit int) ([]Order, error) {
    db := r.shardManager.DBForTenant(tenantID).WithContext(ctx)
    var out []Order
    err := db.Where("tenant_id = ?", tenantID).
        Order("created_at desc").
        Limit(limit).
        Find(&out).Error
    return out, err
}

Bu modelde uygulama verinin hangi shard'a gidecegini deterministik olarak bilir.

Cross-Shard Query Problemi

Sharding'in en zorlu alani budur. "Tum tenant'larda aylik toplam gelir" gibi sorgular dogal olarak tum shardlara dagilir.

Yaklasimlar:

  • Uygulama seviyesinde parallel fan-out + aggregate
  • Ayrik analytics pipeline (ETL -> DWH)
  • Materialized summary tablolar

OLTP shard cluster'ini OLAP sorgulari ile yormamak genellikle en dogru secimdir.

Transaction ve Tutarlilik Gercegi

Tek shard icindeki transactionlar kolaydir. Birden fazla shard'i kapsayan transactionlar ise dagitik sistem problemidir.

Pratik cozumler:

  • Is akisini tek shard sinirlarinda tasarlamak
  • SAGA / outbox pattern kullanmak
  • Idempotent operasyonlar tasarlamak

2PC benzeri yaklasimlar teoride guzel, pratikte operasyonel maliyeti yuksektir.

Connection Pool ve Operasyonel Ayarlar

Shard sayisi arttikca toplam DB baglanti sayisi patlayabilir.

GORM + pgx / database/sql tarafinda:

  • Her shard icin makul MaxOpenConns
  • MaxIdleConns ve ConnMaxLifetime tuning
  • Read replica kullaniminda read/write ayrimi

Basit bir kontrol:

toplam_conn = shard_sayisi * servis_instance_sayisi * max_open_conns

Bu sayi PostgreSQL limitlerini asmamali.

Migration Stratejisi

Monolitik tek DB'den sharding'e gecis genellikle kademeli yapilir:

  1. Shard map tablosu / servis katmani ekle
  2. Yeni tenant'lari shard'li yapida ac
  3. Eski tenant'lari batch halinde tasima (dual write opsiyonel)
  4. Dogrulama (count/checksum/domain-level invariants)
  5. Eski path'i kapatma

Keskin bir big-bang gecis yerine adim adim ilerlemek risk ve downtime'i azaltir.

Gozlemlenebilirlik Checklist'i

  • Shard bazli p95/p99 latency
  • Shard bazli QPS ve error rate
  • Uneven shard dagilimi (hot shard)
  • Replica lag
  • Connection saturation

Hot shard tespiti erken yapilmazsa tum sistem performansi en yavas shard'a baglanir.

Sık Yapılan Hatalar

  • Shard key'i query patternlerinden bagimsiz secmek
  • Cross-shard query hacmini hafife almak
  • Rebalancing planini en basa koymamak
  • Observability ve shard-level alarmlari kurmamak

Sonuc

PostgreSQL sharding, tek node sinirlarini asmak icin guclu bir mimari yaklasimdir. Ancak basari sadece "veriyi bolmekle" gelmez; shard key tasarimi, query routing, transaction sinirlari ve operasyonel olgunluk birlikte ele alinmalidir.

GORM ile uygulama tarafinda temiz bir shard router/repository mimarisi kuruldugunda hem kod okunabilirligi korunur hem de sistem buyumeye hazir hale gelir.