PushizoDocs
OperasyonRunbook'lar

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

SenaryoManifest / betikKabul (docs/plan/10 Faz 16)k6 eşiği
A. Olay ingest 50k olay/sn, 15 dkinfra/load/testrun-events-ingest-50k.yaml / tests/load/events-ingest.js50k olay/sn kayıpsız; hata < %0,1events_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.jsbaşlama < 60 sn; gönderim ≥ 50k/snfanout_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.

Bu sayfada