Skip to content
ZERONE
Natrag na uvide
Distribuirani sustavi2026-07-06 · 7 min čitanja

Ako Redis startaš samo za job-trigger, izbaci ga natrag: Postgres pg_notify kao worker queue

Naš cinema-orch-worker morao je odmah vidjeti nove render jobove — 5-sekundno polling bilo je presporo i guralo je Postgres-load u peteroznamenkasti QPS-raspon. Redis dodati bio je očit refleks. Umjesto toga uzeli smo \`LISTEN/NOTIFY\`, spustili latenciju ispod 200 ms i cijelu komponentu držali izvan stacka.

Worker u našem cinema-orchu (Sprint 3.2) ima vrlo jasan posao: čim korisnik pošalje novi render request kroz Next.js aplikaciju i estimate check prođe, worker mora izvući job iz `jobs` tablice, spawnati pod i pokrenuti frame loop. Cilj latencije: ispod 1 sekunde od „user klikne Render" do „pod je provisioniran".

Naivno rješenje je polling — svakih 5 sekundi `SELECT ... WHERE state = 'queued' ORDER BY created_at LIMIT 1`. Funkcionira, ali ima dva problema: latencija je u medianu 2,5 sekunde (pola polling intervala), a Postgres-load po workeru × frekvencija pollinga × concurrent-workeri kod 20 workera i 5-sekundnog intervala postaje 4 QPS baseline-loada. Dodaš još kategorija workera (render-worker, upload-worker, cleanup-worker) i to brzo postane 15–30 QPS bez pravog user-prometa. Na Hetzner CX22 s 2-vCPU Postgresom to je polovica IO-budgeta.

Refleks u njemačkom SaaS-ekosustavu tada je: dodati Redis. Redis Pub/Sub za worker-notifikacije, Postgres samo kao persistence layer. Nismo to napravili.

Postgres iz kutije zna worker-queues

Postgres ima `LISTEN` i `NOTIFY` od verzije 7.4 (2003). Te dvije naredbe rade točno ono zbog čega se inače pokreće Redis Pub/Sub: jedan klijent se subscribea na kanal, drugi klijent šalje signal kanalu, a Postgres isporučuje signal svim subscriberima. Payload-size je 8 KB (kod standardne Postgres konfiguracije).

Cinema-orch-worker u Pythonu s asyncpg-om radi:

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

Na Next.js app strani, izravno nakon uspješnog Inserta novog joba:

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

Dvije napomene o SQL-u: `NOTIFY` i `INSERT` moraju biti u istoj transakciji, inače worker može primiti Notify prije nego što je Insert commit-an. I payload može opcionalno sadržavati payload-string (`NOTIFY new_render_job, 'job_id=42'`), ali mi šaljemo samo kanal — worker sam dohvaća job s `SELECT ... FOR UPDATE SKIP LOCKED`.

Tri trade-offa koja treba poznavati

1. Notify-delivery je at-most-once. Postgres Notify ne sprema perzistentno. Ako worker upravo ne slušanja (restart, mrežna particija), propušta signal. Fix: worker nakon reconnecta uvijek prvo napravi full-scan `jobs` tablice za `state = 'queued'` rowove prije nego što čeka nove Notifyje. Tako propušteni Notify nije bitan.

2. Payload-size je 8 KB. Ako želiš slati cijele job-objekte preko Notifyja, ne ide. Mi ionako šaljemo samo kanal — worker detalje dohvaća iz tablice.

3. Notify je broadcast, ne pop. Svi workeri koji slušanjaju isti kanal vide signal. Ako želiš osigurati da samo jedan uzme job, trebaš `SELECT ... FOR UPDATE SKIP LOCKED` pri Picku — Postgres lockira row na DB-razini, drugi workeri je pri sljedećem `SELECT`-u vide kao „skipped".

Naš cinema-orch ima jedan worker po server-instanci, zato `SKIP LOCKED` kod nas nije ni potreban — ali to je standardna preporuka ako se ikad horizontalno skalira.

Brojke nakon prepravke

  • Median-latencija „user klikne" do „pod spawna": 5,2 s → 620 ms. Najveći dio sada je 400 ms Runpod-pod-provisioninga, više ne polling-wait-time.
  • Postgres-load baseline bez pravog user-prometa: 4 QPS → 0,1 QPS (samo heartbeat-tickovi).
  • Nema Redisa u stacku. Jedan Docker container manje, jedan backup-target manje, jedna kategorija procesa manje u monitoringu.

Kada Redis ipak ima smisla

Ako trebaš Delayed Jobs (`SCHEDULE FOR '2026-08-15T09:00:00Z'`), zakašnjeli retry s eksponencijalnom backoff-krivuljom, ili kompleksne priority-queues s Weighted-Round-Robinom. `LISTEN/NOTIFY` ti ne daje delayed-jobs. Za instant-jobove s jednostavnom FIFO-semantikom Postgres je dovoljan.

Ako trebaš > 1000 Notifyja u sekundi — i tada. Postgres NOTIFY ima overhead po Notify-emitu, od 1k/s to postaje vidljivo. Kod nas je trenutačno < 10 Notifyja po minuti. Nema razloga za pogon drugog storage-sustava.

Meta-lekcija

Njemački SaaS-refleks „dodaj Redis" dolazi iz doba kad je Postgres uistinu bio samo baza. Post-2015 Postgres je message-bus + KV-store + search-engine + JSON-store + time-series u jednom — i sve te značajke dovoljno su dobre za 90 % use-caseova. Ako nemaš specifičan zahtjev koji Postgres uistinu ne može pokriti, jeftinije je i jednostavnije pogoniti jedan server nego istovremeno održavati dva.

Kompletan setup je live na ZER0ONE Lab Cinema Render i radi već 4 tjedna u beti bez ijednog propuštenog joba. Ako za vlastiti backend evaluiraš `LISTEN/NOTIFY`-baziranu job-queue i imaš pitanja — javi se na [email protected], rado ćemo 30 minuta besplatno raspraviti setup.

Sličan izazov i kod vas?

Vjerojatno smo već vidjeli nešto slično. Razgovarajmo.

Započnite razgovor