İçeriğe geç

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 — Sunumlar

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 boyutuTek bir tablo ne kadar büyüdü?
  • Yazma throughput'uPrimary saniyede ne kadar yazıyor?
  • Tutarlılık gereksinimiHangi 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-FPMuygulama
  2. pgBouncerbağlantı havuzu
  3. PostgreSQL 16tek primary
  4. Redis 7cache · 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 disiplinigereksiz taramayı kes
  2. Adım 2: Ölü veriyi ayırsıcak veriyi küçült
  3. Adım 3: Vertical scaleen 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

  • GecikmeReplication lag: replica geride kalır; read-after-write garantisi kaybolur.
  • İzleyinpg_stat_replication'da replay_lsn farkını lag_bytes olarak okuyun.
  • PrimaryBakiye, stok, yetki gibi tutarlılık isteyen okumalar primary'de kalır.
  • SınırRead 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 pruningSorgu yalnız ilgili partition'a bakar.
  • Ucuz silmeEski veri DROP TABLE events_2026_01 ile gider.
  • Sınırlı vacuumVacuum tüm tabloyu değil, partition'ı tarar.
  • Primary keyPartition anahtarını içermeli: (id, occurred_at).
  • BRINZamana 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 azaltidx_scan = 0 index'leri sil, fillfactor/HOT, batch insert
  2. Adım 2: Append verisini çıkarappend ağırlıklı veri primary'den ayrılır
  3. Adım 3: Veri alanını bölen 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ı

  • HamleVeri-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.
  • KuralAyı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ünenGerçekDoğru hamle
"Veritabanı yavaş"N+1: uygulama 200 sorgu atıyorEager loading; sorguları say
"Replica lazım"Tek sorguda eksik indexEXPLAIN ANALYZE, index ekle
"Sharding lazım"Replica, partitioning denenmediÖnce 2. ve 3. noktayı tüket
"NoSQL'e geçelim"jsonb, BRIN, partitioning atılPostgreSQL'i sonuna kadar kullan
"Yazma için cache"Cache okumayı azaltır, yazmayı değil4. 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 setindex + RAM
  2. Adım 2: Okuma yüküread replica
  3. Adım 3: Tek tablopartitioning
  4. Adım 4: Yazma yüküamplifikasyon + ayrıştırma
  5. Adım 5: Veri-baskın alanayrı 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. CloudflareDDoS, WAF, rate limiting, TLS, CDN
  2. NginxTLS, static asset, fastcgi tek config
  3. PHP-FPMopcache + preload
  4. pgBouncertransaction mode bağlantı havuzu
  5. PostgreSQLpgBackRest: full + incremental + WAL PITR
  6. Redis · RabbitMQcache/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

RedisRabbitMQ
PersistenceBellekten servis eder; diske RDB ya da AOF ile inerKalıcı mesajları diske yazar
Yeniden teslimretry_after zaman aşımına bakarConsumer'ın bağlantısına bakar
RoutingYönlendirme kararı uygulamada kalırExchange'ler karar verir
MonitoringHorizon; 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
pgBouncerConnection storm'larında PostgreSQL bağlantısı tükenir, request'ler 500 atar
SupervisorWorker'lar crash sonrası geri gelmez, alarm kurmak zorundasınız
HorizonGörünürlük çöker, "queue neden yavaş" sorusunu kör cevaplarsınız
opcacheHer request PHP dosyasını disk'ten okur ve parse eder, ~5x yavaşlama
pgBackRestpg_dump kalır: PITR gider, incident'ta hata payınız bir günlük yedek
Redis lockRace 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