Yük testi (k6-operator, dağıtık)
v2 Faz 16 madde 5 (docs/plan/10). Manifestler infra/load/, betikler tests/load/. Tek makineden koşulan Faz 14 prosedürü (10k olay/sn, 1M fan-out) tests/load/README.md içindedir; bu runbook Kubernetes'te dağıtık koşuyu…
v2 Faz 16 madde 5 (docs/plan/10). Manifestler infra/load/, betikler tests/load/. Tek makineden koşulan Faz 14 prosedürü
(10k olay/sn, 1M fan-out) tests/load/README.md içindedir; bu runbook Kubernetes'te dağıtık koşuyu anlatır.
1. Senaryolar ve kabul eşikleri
| Senaryo | Manifest / betik | Kabul (docs/plan/10 Faz 16) | k6 eşiği |
|---|---|---|---|
| A. Olay ingest 50k olay/sn, 15 dk | infra/load/testrun-events-ingest-50k.yaml / tests/load/events-ingest.js | 50k olay/sn kayıpsız; hata < %0,1 | events_accepted rate ≥ 50000/RUNNERS (koşucu başına), events_batch_failed ve http_req_failed rate < 0.001, http_req_duration p(95) < 500 ms, dropped_iterations count < 1 |
| B. 10M fan-out (mock sağlayıcı) | infra/load/testrun-fanout-10m.yaml / tests/load/fanout-campaign.js | başlama < 60 sn; gönderim ≥ 50k/sn | fanout_start_seconds max < 60, fanout_send_rate min ≥ 50000, fanout_missing_ratio max ≤ 0.001, fanout_completed rate == 1 |
Ek kabul ölçütleri (k6 dışında, §4.4 ve §5.4): A'da Postgres'e yazılan olay sayısı = kabul edilen olay sayısı ve events kuyruğu
yük bitince boşalır; B'de failed teslimat yok. Plan ayrıca "ingest → ClickHouse gecikmesi p95 < 30 sn" ister: ClickHouse
aktarımı (Faz 16 madde 1) devreye alındığında aynı RUN_ID ile ClickHouse tarafında ölçülür; bu altyapı bugün Postgres yolunu ölçer.
2. k6-operator kurulumu
helm repo add grafana https://grafana.github.io/helm-charts && helm repo update
K6_OPERATOR_VERSION=$(helm search repo grafana/k6-operator -o json | jq -r '.[0].version') # kayıt altına alın, sabitleyin
helm upgrade --install k6-operator grafana/k6-operator -n k6-operator-system --create-namespace --version "$K6_OPERATOR_VERSION"
kubectl get crd testruns.k6.io
kubectl apply -f infra/load/namespace.yaml # `load` ad alanı (Pod Security: restricted)
```text
Koşucular `grafana/k6:1.0.0` imajıyla çalışır (TestRun `runner.image`). 50k olay/sn için 10 koşucu × (1–2 vCPU, 1–2 GiB) gerekir;
`separate: true` her koşucuyu ayrı düğüme yerleştirir — en az 10 uygun düğüm (veya ayrı bir yük düğüm havuzu) olmalı, aksi
halde koşucular `Pending` kalır. Koşucular hedefle aynı bölgede olmalı (ağ gecikmesi p95'i şişirmesin).
## 3. Ortak hazırlık
1. **Hedef ortam**: staging, `infra/k8s/pushizo/values-staging.yaml` (`PROVIDER_MODE=mock`, `ALLOW_MOCK_PROVIDERS=true`;
`mock:ok-*` token'ları teslim edilmiş sayılır, gerçek APNs/FCM çağrısı yapılmaz). Ölçeklenme sınırları test boyunca
gözlenir: API HPA `maxReplicas`, worker `events` ve `dispatch` grupları (KEDA veya HPA), Postgres `max_connections`.
2. **Kiracı**: geçici org + app (`ios_push` mock kanal) + admin API anahtarı. Staging API'si ve Mailpit'e erişen bir makineden:
```bash
E2E_API_URL=https://api.staging.pushizo.example E2E_MAILPIT_URL=https://mailpit.staging.pushizo.example \
npx tsx tests/load/bench-tenant.ts > tenant.json # { appId, apiKey, orgId, subId }
export APP_ID=$(jq -r .appId tenant.json) API_KEY=$(jq -r .apiKey tenant.json) ORG_ID=$(jq -r .orgId tenant.json)
```text
3. **Plan ve kota**: yeni org Free plandadır (API anahtarı 20 istek/sn, 100k mesaj/ay). Platform yöneticisi dashboard
**Admin → Organizasyonlar → Plan** (ya da `POST /admin/v1/orgs/:orgId/plan {"plan_code":"enterprise"}`) ile `enterprise`
atar: 2000 istek/sn, sınırsız mesaj. Senaryo A ~102 istek/sn ister; daha düşük planda `API_KEYS` ile birden çok anahtar
verilebilir. **Yeni org ilk 24 saat günde en fazla 1000 mesaj gönderebilir** (kötüye kullanım koruması, docs/platform-admin.md
§3): Senaryo B için kiracı en az 24 saat önce oluşturulmalı ya da kalıcı bir yük testi org'u kullanılmalıdır.
4. **Hedef Secret'ı ve betikler** (`load` ad alanı; depoya yazılmaz):
```bash
NS_APP=pushizo-staging
kubectl -n load create secret generic pushizo-load-target \
--from-literal=API_URL=http://pushizo-api.$NS_APP.svc:4000 --from-literal=APP_ID="$APP_ID" --from-literal=API_KEY="$API_KEY"
kubectl -n load create configmap pushizo-k6-scripts \
--from-file=tests/load/events-ingest.js --from-file=tests/load/fanout-campaign.js
```text
`API_URL` küme içi Service'tir (uygulama kapasitesini ölçer; ingress/TLS dahil uçtan uca ölçüm için genel adres
verilebilir). Hedef ad alanında NetworkPolicy açıksa `load` ad alanına API portu açılmalıdır: `values-staging.yaml`
`networkPolicy.api.extraIngress` örneği.
## 4. Senaryo A — 50k olay/sn ingest (15 dk)
### 4.1 Çalıştırma
```bash
RUN_ID=ingest-$(date +%Y%m%d%H%M)
kubectl -n load create configmap pushizo-load-params --from-literal=RUN_ID=$RUN_ID --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f infra/load/testrun-events-ingest-50k.yaml
kubectl -n load get testrun pushizo-events-ingest-50k -w # stage: initialization → created → started → finished
```text
`parallelism: 10` koşucu sabit varış hızını (102 istek/sn × 500 olay = 51k olay/sn) yürütme dilimleriyle paylaşır. k6 hızı
testin tamamına (başlangıç ve bitiş dahil) böldüğü için %2 pay bırakılır: üretim 51k, eşik 50k. Eşikler **koşucu başına**
değerlendirilir (k6-operator sonuçları birleştirmez): hız eşiği `MIN_EPS/RUNNERS`; `parallelism` değişirse `RUNNERS` de değişmelidir.
### 4.2 Sonuç
```bash
kubectl -n load get pods -l k6_cr=pushizo-events-ingest-50k
for p in $(kubectl -n load get pods -l k6_cr=pushizo-events-ingest-50k,runner=true -o name); do
kubectl -n load logs $p | grep -E "✓|✗|events_accepted|http_req_duration|dropped_iterations"; done
```text
Her koşucu pod'u eşikler geçtiyse 0, eşik aşıldıysa 99 çıkış koduyla biter (`Completed` / `Error`). **Kabul için 10 koşucunun
tamamı geçmelidir.** Remote-write açıksa (§6) toplam değerler Prometheus'ta:
```promql
sum(rate(k6_events_accepted_total{testid="events-ingest-50k"}[1m])) # olay/sn (toplam)
sum(k6_events_accepted_total{testid="events-ingest-50k"}) # kabul edilen toplam olay
max(k6_http_req_duration_p95{testid="events-ingest-50k"}) # istek p95 (sn)
max(k6_http_req_failed_rate{testid="events-ingest-50k"}) # başarısız istek oranı (koşucu başına en kötü)
```text
### 4.3 Uygulama tarafı gözlem (yük sırasında)
- Grafana *API* panosu: 5xx oranı, `http_request_duration_seconds` p95, pod sayısı (HPA).
- *Kuyruklar* panosu: `queue_depth{queue="events",state=~"waiting|prioritized"}` sınırlı kalmalı; `state="failed"` artmamalı.
- *Veritabanı*: bağlantı doluluğu < %90 (`PostgresConnectionsHigh`), WAL/IO.
### 4.4 Kayıp denetimi
Olaylar `properties.run = RUN_ID` ile etiketlenir. Yük bittikten ve `events` kuyruğu boşaldıktan sonra Postgres'teki sayı, k6'nın
kabul ettiği toplamla (`sum(k6_events_accepted_total)` ya da koşucu loglarındaki `events_accepted` toplamı) eşit olmalıdır:
```bash
PGOPTIONS='-c app.rls_bypass=on' psql "$DATABASE_URL" -tAc \
"select count(*) from events where app_id = '$APP_ID' and properties->>'run' = '$RUN_ID' and occurred_at > now() - interval '3 hours'"
```text
## 5. Senaryo B — 10M fan-out
### 5.1 Alıcıları tohumlama
10M kullanıcı + mock `ios_push` test aboneliği doğrudan DB'ye yazılır (`tests/load/fanout-bench.mjs --phase seed`). Postgres'te
birkaç GB yer ve 20–60 dk sürer; staging DB depolaması önceden kontrol edilir.
```bash
TAG=fanout-$(date +%Y%m%d%H%M) IMG_TAG=0.1.0 # IMG_TAG: hedefte kurulu sürüm
kubectl -n $NS_APP create configmap pushizo-load-seed-script --from-file=tests/load/fanout-bench.mjs \
--dry-run=client -o yaml | kubectl apply -f -
kubectl -n $NS_APP create configmap pushizo-load-seed-params --from-literal=APP_ID="$APP_ID" \
--from-literal=FANOUT_PHASE=seed --from-literal=FANOUT_TAG=$TAG --from-literal=FANOUT_COUNT=10000000 \
--from-literal=FANOUT_PARALLEL=6 --from-literal=FANOUT_MESSAGE= --from-literal=PGSSLMODE=verify-full \
--dry-run=client -o yaml | kubectl apply -f -
sed "s#pushizo-migrate:0.1.0#pushizo-migrate:${IMG_TAG}#" infra/load/fanout-seed-job.yaml | kubectl -n $NS_APP apply -f -
kubectl -n $NS_APP wait --for=condition=complete job/pushizo-fanout-seed --timeout=3h
kubectl -n $NS_APP logs job/pushizo-fanout-seed | tail -1 # {"phase":"seed","tag":"...","count":10000000,...}
```text
Job yönetilen DB'ye doğrudan bağlanır (PgBouncer değil) ve chart etiketlerini taşımadığı için release NetworkPolicy'leri onu
kısıtlamaz; DB güvenlik grubu küme alt ağına açık olmalıdır. Alternatif: DB'ye erişen bir yönetim makinesinden
`node tests/load/fanout-bench.mjs --db "$DATABASE_URL" --app "$APP_ID" --phase seed --tag $TAG --count 10000000 --parallel 6`.
### 5.2 Çalıştırma
```bash
kubectl -n load create configmap pushizo-load-params --from-literal=RUN_ID=$TAG --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f infra/load/testrun-fanout-10m.yaml
kubectl -n load logs -f -l k6_cr=pushizo-fanout-10m,runner=true
```text
`setup()` tek bildirim gönderir (`test_devices_only`, `ios_push`, idempotency anahtarı `k6-<RUN_ID>`; aynı `RUN_ID` ikinci
gönderim yapmaz), tek VU mesaj `completed` olana kadar saniyede bir sayaçları okur. Logdaki JSON satırı mesaj kimliğini,
sayaçları, başlama ve toplam süreyi içerir.
### 5.3 Yorum
- `fanout_start_seconds`: kabul → ilk `sent` (scheduler + audience çözümleme + ilk dispatch işi). > 60 sn ise audience
sayfalama hızı ve `scheduler`/`audience` kuyruk derinliği incelenir.
- `fanout_send_rate`: ilk gönderimden tamamlanmaya ortalama hız. < 50k/sn ise sırayla: dispatch worker replikası (KEDA
`maxReplicaCount`) ve `concurrency`, Redis CPU (BullMQ), Postgres yazma gecikmesi (`deliveries` ekleme/güncelleme),
worker CPU kısıtlaması (throttling).
- `fanout_missing_ratio`: `COUNT − sent`; mock sağlayıcıda 0 beklenir. Sıfırdan büyükse `failed`/`suppressed` sayaçları ve
`quota_exceeded` (yeni org sınırı, plan) kontrol edilir.
### 5.4 Temizlik
```bash
MSG_ID=$(kubectl -n load logs -l k6_cr=pushizo-fanout-10m,runner=true | grep -o '"message_id":"[^"]*"' | head -1 | cut -d'"' -f4)
kubectl -n $NS_APP delete job pushizo-fanout-seed
kubectl -n $NS_APP create configmap pushizo-load-seed-params --from-literal=APP_ID="$APP_ID" \
--from-literal=FANOUT_PHASE=cleanup --from-literal=FANOUT_TAG=$TAG --from-literal=FANOUT_COUNT=10000000 \
--from-literal=FANOUT_PARALLEL=1 --from-literal=FANOUT_MESSAGE=$MSG_ID --from-literal=PGSSLMODE=verify-full \
--dry-run=client -o yaml | kubectl apply -f -
sed "s#pushizo-migrate:0.1.0#pushizo-migrate:${IMG_TAG}#" infra/load/fanout-seed-job.yaml | kubectl -n $NS_APP apply -f -
kubectl -n $NS_APP wait --for=condition=complete job/pushizo-fanout-seed --timeout=3h
```text
## 6. Sonuçları Prometheus'a gönderme (isteğe bağlı)
```bash
kubectl apply -f infra/load/k6-prometheus-rw.yaml # ConfigMap pushizo-k6-output (K6_OUT=experimental-prometheus-rw)
```text
- TestRun'lar bu ConfigMap'i `envFrom` (optional) ile okur; ConfigMap yoksa çıktı yalnızca koşucu loglarındadır.
- Prometheus'ta remote-write alıcısı açık olmalı (kube-prometheus-stack: `prometheus.prometheusSpec.enableRemoteWriteReceiver=true`)
ya da Mimir/Thanos Receive uç noktası verilmeli. Kimlik doğrulama gerekiyorsa `K6_PROMETHEUS_RW_USERNAME`/`PASSWORD` ayrı bir
Secret'tan TestRun `runner.envFrom`'a eklenir.
- Metrik adları `k6_` önekli; sayaçlar `_total`, trend'ler `K6_PROMETHEUS_RW_TREND_STATS` ile `_p95`, `_p99`, `_min`, `_max`,
`_avg` sonekli. Her koşu `testid` ve `scenario` etiketlerini taşır (TestRun `arguments`).
## 7. Sonuç yorumlama ve sık sorunlar
| Belirti | Olası neden | Bakılacak yer |
|---|---|---|
| `dropped_iterations > 0` | VU yetersiz (gecikme arttı) ya da koşucu CPU'su doydu | `MAX_VUS`, koşucu CPU; API p95 |
| `http_req_failed` 429 | plan `api_rps` (anahtar başına) | org planı, `API_KEYS` |
| `http_req_failed` 5xx/zaman aşımı | API CPU, DB havuzu, Redis | API panosu, HPA olayları, `PostgresConnectionsHigh` |
| p95 yüksek, hata yok | HPA geç büyüyor | `api.hpa.behavior.scaleUp`, `minReplicas`'ı test öncesi artırma |
| `events` kuyruğu sürekli büyüyor | events worker yetersiz | `worker.groups[events]` KEDA eşiği/`maxReplicaCount` |
| Kayıp denetiminde eksik | 2xx yanıt sonrası yazım hatası | API logları (`request_id`), `events` tablosu bölümleri |
| Fan-out başlamıyor | yeni org 1000/gün sınırı, plan mesaj kotası | API yanıtı `HS_QUOTA_EXCEEDED`, Admin → Kötüye kullanım |
Kabul: iki ardışık koşuda tüm eşikler yeşil ve yük sırasında `ApiHigh5xxRate`, `QueueDepthHigh`, `PostgresConnectionsHigh`
alarmları tetiklenmemiş olmalı. Sonuçlar `tests/load/README.md` sonuç tablosuna ve `docs/IMPLEMENTATION_STATUS.md` doğrulama
bölümüne işlenir (tarih, sürüm, ortam boyutu, koşucu sayısı, eşik değerleri).
## 8. Temizlik
```bash
kubectl -n load delete testrun pushizo-events-ingest-50k pushizo-fanout-10m --ignore-not-found
kubectl -n load delete secret pushizo-load-target
kubectl -n load delete configmap pushizo-k6-scripts pushizo-load-params --ignore-not-found
kubectl -n $NS_APP delete configmap pushizo-load-seed-script pushizo-load-seed-params --ignore-not-found
```text
Kiracı Admin → Organizasyonlar üzerinden silinir; `purge-deleted-tenants` 30 gün sonra verisini temizler. Senaryo A olayları
olay saklama süresi (`RETENTION_EVENTS_DAYS`) sonunda silinir; staging'de daha erken gerekiyorsa kiracı silinir.Go dispatcher
ADR-0026 §3. Push gönderimini (APNs, FCM, HMS, Web Push) Go servisine (services/dispatcher) taşır; render, sahiplenme ve sonuç uygulama worker'da kalır. İsteğe bağlıdır ve anında geri alınır: DISPATCHER_GO_CHANNELS…
Runbook: Kaos tatbikatı (aylık, staging)
Test matrisi (docs/plan/09): worker kill, Redis restart, sağlayıcı 5xx enjeksiyonu — aylık, elle. Betik: infra/scripts/chaos.sh.