Skip to content
ZERONE
Nazaj na vpoglede
Porazdeljeni sistemi2026-07-06 · 7 min branja

Če Redis zaganjaš samo za job-trigger, ga boš spet vrgel ven: Postgres pg_notify kot worker-queue

Naš cinema-orch-worker je moral nove render-jobe videti takoj — 5-sekundni polling je bil prepočasen in je Postgres-load poganjal v petmestno QPS-področje. Dodati Redis je bil očiten refleks. Namesto tega smo vzeli \`LISTEN/NOTIFY\`, spustili latenco pod 200 ms in celotno komponento izpustili iz stacka.

Worker v našem cinema-orch (Sprint 3.2) ima zelo jasno nalogo: brž ko uporabnik preko Next.js-aplikacije odda nov render-order in gre estimate-check skozi, mora worker vzeti job iz tabele `jobs`, spawnati pod in zagnati frame-loop. Cilj latence: pod 1 sekundo od „user klikne render“ do „pod je provisioniran“.

Naivna rešitev je polling — vsakih 5 sekund `SELECT ... WHERE state = 'queued' ORDER BY created_at LIMIT 1`. Deluje, a ima dva problema: latenca v medianu leži pri 2,5 sekunde (polovica polling-intervala), in Postgres-load per worker × polling-frequency × concurrent-workers pri 20 workerjih in 5-sekundnem intervalu naraste na 4 QPS baseline-load. Če dodaš dodatne kategorije workerjev (Render-Worker, Upload-Worker, Cleanup-Worker), to hitro postane 15–30 QPS brez pravega uporabniškega prometa. Na Hetzner-CX22 z 2-vCPU-Postgresom je to polovica IO-budgeta.

Refleks v nemškem SaaS-ekosistemu je potem: dodati Redis. Redis Pub/Sub za worker-notifikacije, Postgres samo še kot persistence-layer. Tega nismo naredili.

Postgres zna worker-queues iz škatle

Postgres ima `LISTEN` in `NOTIFY` od verzije 7.4 (2003). Ta dva ukaza počneta točno to, za kar se običajno zažene Redis Pub/Sub: en client se subscribe-a na channel, drug client pošlje signal na channel, in Postgres signal dostavi vsem subscriberjem. Payload-Size je 8 KB (pri standardni Postgres-konfiguraciji).

Cinema-orch-worker v Pythonu z asyncpg naredi:

```python async def wait_for_next_job(conn): await conn.execute("LISTEN new_render_job") while True: # blokira, dokler ne pride NOTIFY await conn.connection.notifies.get() job = await pick_next_queued_job(conn) if job: return job ```

Na strani Next.js-aplikacije, takoj po uspešnem insertu novega joba:

```typescript await db.execute(sql` INSERT INTO jobs (user_id, state, ...) VALUES (${userId}, 'queued', ...); NOTIFY new_render_job; `); ```

Dve opombi k SQL-u: `NOTIFY` in `INSERT` morata biti v isti transakciji, sicer lahko worker prejme NOTIFY, preden je insert commit-an. In payload lahko opcijsko vsebuje payload-string (`NOTIFY new_render_job, 'job_id=42'`), a mi predajamo samo channel — worker job vzame sam s `SELECT ... FOR UPDATE SKIP LOCKED`.

Trije trade-offi, ki jih moraš poznati

1. Notify-delivery je at-most-once. Postgres notify-jev ne shranjuje persistentno. Če worker ravno ne listen-a (restart, network-partition), zamudi signal. Fix: worker po vsakem reconnectu najprej naredi full-scan tabele `jobs` za vrstice `state = 'queued'`, preden začne čakati na nove notify-je. Tako je zamujen NOTIFY brez posledic.

2. Payload-Size je 8 KB. Če želiš cele job-objekte pošiljati per NOTIFY, ne bo delovalo. Mi tako ali tako pošiljamo le channel — worker si detajle pobere iz tabele.

3. NOTIFY je broadcast, ne pop. Vsi workerji, ki listenajo isti channel, vidijo signal. Če želiš zagotoviti, da job vzame le eden, potrebuješ `SELECT ... FOR UPDATE SKIP LOCKED` pri picku — Postgres vrstico locka na DB-ravni, drugi workerji jo pri naslednjem `SELECT`-u vidijo kot „skipped“.

Naš cinema-orch ima en sam worker per server-instanci, zato `SKIP LOCKED` pri nas niti ni potreben — je pa standardno priporočilo, če kdaj horizontalno skaliraš.

Številke po prenovi

  • Median-latenca „user klikne“ do „pod spawna“: 5,2 s → 620 ms. Največji delež so zdaj 400 ms Runpod-Pod-provisioninga, ne več polling-čakanje.
  • Postgres-Load-Baseline brez pravega uporabniškega prometa: 4 QPS → 0,1 QPS (samo heartbeat-tiki).
  • Nobenega Redis-a v stacku. En Docker-container manj, en backup-cilj manj, ena procesna kategorija manj v monitoringu.

Kdaj je Redis vseeno pravi

Če potrebuješ delayed jobs (`SCHEDULE FOR '2026-08-15T09:00:00Z'`), zakasnjeni retry z eksponentno backoff-krivuljo ali kompleksne priority-queues z weighted-round-robinom. `LISTEN/NOTIFY` ti delayed-jobov ne da. Za instant-jobe s preprosto FIFO-semantiko Postgres zadošča.

Če potrebuješ > 1000 notify-jev na sekundo — tudi tedaj. Postgres NOTIFY ima overhead per Notify-Emit, od 1k/s dalje postane to vidno. Pri nas je trenutno < 10 notify-jev na minuto. Ni razloga za drug storage-system.

Meta-lekcija

Nemški SaaS-refleks „dodati Redis“ izvira iz obdobja, ko je bil Postgres res le baza. Post-2015 je Postgres message-bus + KV-store + search-engine + JSON-store + time-series v enem — in vse te funkcije so dovolj dobre za 90 % use-caseov. Če nimaš specifične zahteve, ki je Postgres ne zmore, je ceneje in enostavneje voditi en server namesto dveh.

Celoten setup je v našem ZER0ONE Lab Cinema Render live in teče že 4 tedne v Beti brez enega samega job-missa. Če za lasten backend evalviraš `LISTEN/NOTIFY`-based job-queue in imaš vprašanja — kontakt@zer0one.codes, z veseljem 30 minut brezplačno.

Podoben izziv tudi pri vas?

Verjetno smo že videli kaj podobnega. Pogovorimo se.

Začnimo pogovor