Veri yoğunluklu sistemlerde 5 kırılma noktası ve Laravel production stack

Klavye: ← → ile gezinin, F tam ekran, O genel bakış.

Tan, bir pano ekranının başında

Muhammet Şafak — Görseller

Veri yoğunluklu sistemlerde 5 kırılma noktası

En ucuz müdahaleden en pahalısına bir sıra ve her bileşeni gerekçeli bir Laravel production stack'i.

Muhammet Şafak

Bölüm 01

Beş kırılma noktası

Working set'ten ayrı veri katmanına: en ucuz müdahaleden en pahalısına.

Ölçeği belirleyen

Request değil, veri profili.

Bir sistem dakikada 50 request alıp yine de veri tarafından boğulabilir; bir başkası saniyede 5.000 request'i rahat karşılarken veri katmanı el değmemiş durur.

Veri profili

Beş eksen

  • Working set / RAM oranı Sık okunan veri belleğe sığıyor mu?
  • Okuma / yazma oranı Yük hangi yöne ağır basıyor?
  • Tek tablo boyutu Tek bir tablo ne kadar büyüdü?
  • Yazma throughput'u Primary saniyede ne kadar yazıyor?
  • Tutarlılık gereksinimi Hangi okuma en güncel veriyi görmek zorunda?

Başlangıç mimarisi

Tek primary ile başlamak

PHP-FPM, PostgreSQL'e pgBouncer üzerinden, Redis'e doğrudan bağlanır. Birkaç on GB veri ve dakikada binlerce request için yeterli; working set RAM'e sığdığı sürece.

  1. PHP-FPM uygulama
  2. pgBouncer bağlantı havuzu
  3. PostgreSQL 16 tek primary
  4. Redis 7 cache · session · lock

Kırılma noktası 01 · Working set

Sinyal: cache hit ratio

Sık okunan veri RAM’e sığmıyor. %99 üzeri sağlıklı. %95’e doğru düşüş, disk IOPS’u ile birlikte okunduğunda working set’in RAM’den taştığını gösterir.

pg_stat_database.sql
SELECTsum(blks_hit) * 100  / nullif(sum(blks_hit) + sum(blks_read), 0)  AS cache_hit_ratioFROM pg_stat_database;

Müdahale sırası

Önce ucuz olanı tüketin

  1. Adım 1: Index disiplini gereksiz taramayı kes
  2. Adım 2: Ölü veriyi ayır sıcak veriyi küçült
  3. Adım 3: Vertical scale en son, daha çok RAM

Kırılma noktası 02 · Okuma yükü

Okuma replica'ya, yazma primary'ye

Sinyal: CPU yüksek, SELECT baskın, pgBouncer’da cl_waiting birikiyor. sticky, aynı request içinde yazmadan sonraki okumaları primary’ye yollar.

config/database.php
'pgsql' => [  'driver'  => 'pgsql',  'read'  => ['host' => ['10.0.0.2']],   // replica  'write' => ['host' => ['10.0.0.1']],   // primary  'sticky' => true,  // ...ortak ayarlar],

Bedeli

Replica'nın getirdiği yeni sorun

  • Gecikme Replication lag: replica geride kalır; read-after-write garantisi kaybolur.
  • İzleyin pg_stat_replication'da replay_lsn farkını lag_bytes olarak okuyun.
  • Primary Bakiye, stok, yetki gibi tutarlılık isteyen okumalar primary'de kalır.
  • Sınır Read replica yazma kapasitesine hiçbir şey katmaz.

Kırılma noktası 03 · Tek tablo

Zamana göre bölünen tablo

Sinyal: n_dead_tup birikiyor, last_autovacuum geride kalıyor. Partition’ları elle açmak yerine pg_partman kullanılabilir.

events.sql
CREATE TABLE events (  id          bigserial,  occurred_at timestamptz NOT NULL,  payload     jsonb       NOT NULL) PARTITION BY RANGE (occurred_at);CREATE TABLE events_2026_05 PARTITION OF events  FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');

Kazanım ve koşul

Partitioning ne kazandırır?

  • Partition pruning Sorgu yalnız ilgili partition'a bakar.
  • Ucuz silme Eski veri DROP TABLE events_2026_01 ile gider.
  • Sınırlı vacuum Vacuum tüm tabloyu değil, partition'ı tarar.
  • Primary key Partition anahtarını içermeli: (id, occurred_at).
  • BRIN Zamana göre sıralı veride küçük ve ucuz index.

Kırılma noktası 04 · Yazma yükü

Sinyal: I/O wait, checkpoint ve WAL baskısı

  1. Adım 1: Amplifikasyonu azalt idx_scan = 0 index'leri sil, fillfactor/HOT, batch insert
  2. Adım 2: Append verisini çıkar append ağırlıklı veri primary'den ayrılır
  3. Adım 3: Veri alanını böl en son: functional sharding

Gerçekçi beklenti

Çoğu sistem 4. noktaya varmaz.

Acele etmeyin: çoğu sistem 4. noktaya hiç ulaşmaz, ulaşanların çoğu da aslında 1. noktayı (eksik index, şişmiş working set) atlamış olduğu için erken gelir.

Kırılma noktası 05

Ayrı veri katmanı

  • Hamle Veri-baskın alan ayrı bir PostgreSQL instance'ına taşınır.
  • Geçiş pgBackRest restore, logical replication ve kısa bir cutover.
  • Bedel İkinci backup, monitoring ve upgrade; iki veri arasında JOIN yok.
  • Kural Ayırma kararı veri profiliyle verilir, organizasyon şemasıyla değil.

Yanlış teşhis

Yanlış kırılma noktaları

Read replica okumayı ölçekler; cache okumayı azaltır; partitioning tek tabloyu evcilleştirir. Hiçbiri yazma throughput'unu artırmaz.
Görünen Gerçek Doğru hamle
"Veritabanı yavaş" N+1: uygulama 200 sorgu atıyor Eager loading; sorguları say
"Replica lazım" Tek sorguda eksik index EXPLAIN ANALYZE, index ekle
"Sharding lazım" Replica, partitioning denenmedi Önce 2. ve 3. noktayı tüket
"NoSQL'e geçelim" jsonb, BRIN, partitioning atıl PostgreSQL'i sonuna kadar kullan
"Yazma için cache" Cache okumayı azaltır, yazmayı değil 4. noktaya bak

Ortak hata kalıbı

Hiçbiri yazma kapasitesi eklemez.

Read replica okumayı ölçekler. Cache okumayı azaltır. Partitioning tek tabloyu evcilleştirir. Hiçbiri yazma throughput'unu artırmaz.

jsonb · LISTEN/NOTIFY · partitioning · BRIN · logical replication

%20'si kullanıldı. Bu bir sınır değil.

Çoğu ekip PostgreSQL'in %20'sini kullanıp "PostgreSQL yetmiyor" sonucuna varır.

Özet

En ucuzdan en pahalıya

  1. Adım 1: Working set index + RAM
  2. Adım 2: Okuma yükü read replica
  3. Adım 3: Tek tablo partitioning
  4. Adım 4: Yazma yükü amplifikasyon + ayrıştırma
  5. Adım 5: Veri-baskın alan ayrı veri katmanı

Bölüm 02

Laravel stack

"Her şey gerekli" demiyorum — gerekli olduğunu anlayana kadar minimum tutmayı savunan biriyim — ama her bileşenin kazandırdığı somut.

Anatomi

Her bileşen neden orada?

İstek Cloudflare'den PHP-FPM'e iner. PHP-FPM üç arka servise ayrı bağlanır: pgBouncer üzerinden PostgreSQL, Redis ve RabbitMQ. Worker'lar Supervisor altında Horizon ile koşar.

  1. Cloudflare DDoS, WAF, rate limiting, TLS, CDN
  2. Nginx TLS, static asset, fastcgi tek config
  3. PHP-FPM opcache + preload
  4. pgBouncer transaction mode bağlantı havuzu
  5. PostgreSQL pgBackRest: full + incremental + WAL PITR
  6. Redis · RabbitMQ cache/session/lock · queue

Redis

Tek instance, üç şapka

Cache (Cache::remember), session (SESSION_DRIVER=redis) ve lock (Cache::lock) tek Redis’te güvenle çalışır; yeter ki bu üç ayar yerinde olsun.

redis.conf
maxmemory-policy volatile-lruappendonly nosave 900 1 300 10 60 10000

Queue (opsiyonel)

RabbitMQ ile Redis kuyruğu

Redis RabbitMQ
Persistence Bellekten servis eder; diske RDB ya da AOF ile iner Kalıcı mesajları diske yazar
Yeniden teslim retry_after zaman aşımına bakar Consumer'ın bağlantısına bakar
Routing Yönlendirme kararı uygulamada kalır Exchange'ler karar verir
Monitoring Horizon; uygulamanın dispatch ettiğiyle sınırlı Broker düzeyinde yönetim paneli

Çıkarınca olan

Hangi parçası kaldırılırsa ne kaybedersiniz?

Hangi parçası kaldırılırsa ne kaybedersiniz?
Bileşen Çıkarınca olan
pgBouncer Connection storm'larında PostgreSQL bağlantısı tükenir, request'ler 500 atar
Supervisor Worker'lar crash sonrası geri gelmez, alarm kurmak zorundasınız
Horizon Görünürlük çöker, "queue neden yavaş" sorusunu kör cevaplarsınız
opcache Her request PHP dosyasını disk'ten okur ve parse eder, ~5x yavaşlama
pgBackRest pg_dump kalır: PITR gider, incident'ta hata payınız bir günlük yedek
Redis lock Race condition'lara açıksınız, distributed-safe mutex'iniz kalmaz

Boring stack

Nicel değerli, değişimi kolay.

Boring stack'in gücü tam burada: her parça nicel olarak değerli ve değişimi kolay.

Tan, gülümserken

Teşekkürler

sade.dev

Önce veri profilini ölçün, sonra en ucuz müdahaleyi seçin.

Paylaş, göm, indir