
Nem kell öt adatbázis (még)
Közzétéve: 2026. augusztus 26.
Nézd meg videón
Ugyanez a tartalom YouTube-on is elérhető angolul, ha inkább videóban hallanád.
Megnézem YouTube-onHétfő reggel van és épp egy architektúra diagramot nézel.
Az egyik oldalon öt dobozt látsz: Redis a queue-hoz, Elasticsearch a kereséshez, Pinecone az embeddingekhez, egy managed cron service az ütemezett jobokhoz, Kafka pedig az event lognak. A másik oldalon egy doboz - a Postgres, ami csendben mind az öt említett feladatot ellátja.
Melyiket fogod előbb megépíteni? Az igazat megvallva erre nincs se jó, se rossz válasz. Viszont csak az egyik helyes ott, ahol most valójában tartasz.
Öt doboz az első naptól
Ha az öt doboz mellett döntesz, akkor sem hozol rossz döntést - legalábbis nem eleve rosszat. Mindegyik felsorolt eszköz tényleg jó abban, amire való. A Redis gyors. Az Elasticsearch jó a keresésben. A Kafka valódi throughputra lett építve. Ezt senki nem vitatja.
Amit viszont könnyű elveszíteni a nagy jó szándék mögött, az az, amit valójában magadra vállalsz ezekkel. Öt dolog, amit provisionolni kell. Öt dolog, amit monitorozni kell. Öt jelszó páros, amit rotálni kell. Öt vendor, amelyik bármelyike egymástól függetlenül felébreszthet hajnali háromkor. És mindezt olyan terhelésért vállalod be, amid még nincs is - negyven ügyfél vagy száz, szinte mindegy, a szám még mindig bőven elég kicsi ahhoz, hogy mind az öt rendszer nagyjából nulla százalékon fusson ahhoz képest, amire épültek.
Nem kapacitást vettél. Öt várótermet vettél, és a bérleti díjukat már mától fizeted.
Mire képes valójában az az egy postgres
Ha inkább az egy doboz mellett döntesz, nagyjából ezek várnak rád, amint belemész a részletekbe.
A job queue egyetlen lekérdezésre redukálódik:
SELECT id, payload
FROM jobs
WHERE status = 'pending'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 10;
Több worker is húzhat jobokat ugyanabból a táblából anélkül, hogy egymásba akadnának, mert a SKIP LOCKED megmondja a Postgresnek, hogy minden workernek más sorokat adjon, ahelyett hogy sorba állítaná őket ugyanarra a lockra várva. Nincs broker, amit futtatni kell, nincs semmi extra, amit hajnali háromkor életben kellene tartani.
A keresés is majdnem ugyanilyen tisztán megoldódik. A full-text search egy tsvector oszlop és egy GIN index, a pg_trgm extension pedig ráteszi a fuzzy, typo-toleráns keresést is, ha az is kell:
ALTER TABLE articles ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || body)) STORED;
CREATE INDEX articles_search_idx ON articles USING GIN (search_vector);
SELECT title FROM articles
WHERE search_vector @@ plainto_tsquery('english', 'connection pool')
ORDER BY ts_rank(search_vector, plainto_tsquery('english', 'connection pool')) DESC;
Ezzel rangsorolt keresést kapsz olyan adaton, ami már ott van, ugyanabban a tranzakcióban, amelyben az adat létrejött. Nincs külön index, amit szinkronban kell tartani, nincs második rendszer, ami valahogy elcsúszhat az elsőtől.
A lista többi tagja is hasonlóan kitart. A vektoros hasonlóság a pgvector extension: az embeddinget a leírt sor mellett tárolod, HNSW-vel indexeled, sima SQL-lel lekérdezed. Az ütemezett jobok a pg_cron-nal mennek, szóval a cron tábla ugyanabban az adatbázisban lakik, mint amin a job dolgozik, nem egy külön schedulerben, ami sosem hallott a schemádról. Az event log pedig egy append-only tábla, vagy egy rendes outbox pattern, ami logical replication-nel táplálja a downstream fogyasztókat, ha más szolgáltatásoknak valamiért reagálniuk kell arra, ami történt.
És a lényeg: ezek egyike sem valami hack, amivel megúszod a dolgot. Mindegyik egy karbantartott extension vagy dokumentált pattern, és mindet prodban, valós terhelésen futtatják, mind a mai napig.
Az első napon tehát az egy doboz nem valami beérős, óvatos választás, hanem ez a tényleg helyes.
Ahol az a bizonyos egy doboz fájni kezd
Csakhogy ennek is van egy lejárati ideje, és érdemes végigmenni azon, mi történik pontosan, amikor eléred. A hibák szerencsére elég konkrétak ahhoz, hogy figyelni lehessen rájuk.
A connection pool probléma
Tegyük fel, hogy a max_connections kezdetben a default értéken áll, nagyjából száz. A legtöbb csapat sosem fog ehhez a számhoz nyúlni, hiszen minek, egészen addig, amíg valami rájuk nem kényszeríti a kérdést. Az app szervereid nagyjából húsz kapcsolatot tartanak lekötve ebből, bármelyik pillanatban. Aztán a queue-d ötven workerre nő, mert valaki tavaly megduplázta a worker számot, hogy gyorsabban ledolgozzák a felhalmozódott hátralékot. Minden worker nyitva tart egy kapcsolatot, amíg pollozik, szóval már hetven kapcsolat el van foglalva, és itt jön a lényeg: mindez mielőtt egyetlen ügyfél megnyitná a checkout oldalt.
Aztán elindul a nyári készletkisöprési akció. A forgalom megháromszorozódik. A checkout megpróbálja megnyitni a szükséges kapcsolatokat, de egyszerűen nincs több szabad a poolban. Az ügyfeleid nem valami barátságos „az adatbázis ma egy kicsit lassú" üzenetet kapnak, hanem egy jókora 500-as hibát, vagy még rosszabb esetben egy requestet, ami csak lóg, amíg timeoutol. Mindez úgy, hogy senki nem nyúlt a checkout kódhoz. Egyszerűen a queue csendben megette a kapcsolatokat, amikre a checkoutnak szüksége lett volna, és senki nem vette észre, amíg a grafikonok meg nem mutatták.
A vacuum probléma
Tegyük fel, hogy a queue-d napi ötvenezer jobot dolgoz fel: beszúrsz egy sort, feldolgozod a jobot, törlöd a sort - ötvenezerszer egymás után. Viszont egy törölt sor nem tűnik el abban a pillanatban, amikor törlöd - dead tuple lesz belőle, ami ott ül, amíg az autovacuum sort nem kerít rá, hogy visszaszerezze a helyét. Normál körülmények között ez teljesen rendben van, az autovacuum pontosan erre a mintára lett kitalálva.
Csakhogy tegyük fel, hogy valaki elindít egy hosszú analitikai lekérdezést ugyanazon az adatbázison - egy riportot, ami negyven percig fut. Amíg ez fut, a Postgres nem tudja biztonságosan eltakarítani azokat a dead tuple-öket, amiket a még nyitott tranzakció esetleg még látni akar, szóval erre a negyven percre a vacuum egyszerűen vár a sorára. Csinálj ilyet néhány napi riporttal, és a queue táblád, aminek pár megabájtosnak kellene lennie, csendben felfúvódik húsz gigabájtra, aminek jó része szemét. Természetesen minden lekérdezés lassabb is lesz rajta - és nem azért, mert maga a queue nőtt volna meg, hanem mert a tábla alatta sosem zsugorodott vissza.
A buffer cache probléma
Ez az, ami a legtöbbeket váratlanul éri, mert semmi nem néz ki benne hibának. Tegyük fel, hogy futtatsz egy éjszakai jobot, ami újra embeddel mindent, amit aznap valaki szerkesztett, és újraépíti a HNSW indexed egy részét. Ez hajnali 2-kor indul, végigpásztáz néhány millió vektort, és egy jókora darab indexet húz be a memóriába, hogy ezt elvégezze.
Talán nem mondok vele újat, de a Postgres buffer cache-e nem végtelen, szóval valaminek helyet kell csinálnia mindehhez, és ami helyet csinál, általában az, amit a legrégebben nem használtak (LRU). Hajnali 2-kor ez simán lehet az orders tábla, amit a checkout flow napközben folyamatosan használ. Mire megjönnek a reggel első ügyfelek, azok a lapok már nincsenek a cacheben, és a nap első checkout lekérdezései csendben elmennek a lemezhez, ahelyett hogy memóriából ki lehetne őket szolgálni. Ismét: Senki nem deployolt semmit az éjszaka. Senki nem nyúlt a checkouthoz. Egyszerűen a vektoros keresésed átrendezte a cache-t, amíg mindenki nyugodtan aludt.
Tedd össze ezt a hármat, és már látod is az igazi árát annak, ha egy doboz öt feladatot lát el. Nem az a baj, hogy bármelyik feladat önmagában túl nehéz lenne a Postgresnek - hanem hogy mind az öt csendben ugyanazokért a kapcsolatokért, ugyanazokért a vacuum ciklusokért és ugyanazért a memóriáért versenyeznek, és egyik sem látja, hogy a másik ugyanezt csinálja.
Ahol a két út újra találkozik
Van azért egy megnyugtató része is: mivel ez a két út nem áll olyan távol egymástól, mint amilyennek a diagramon tűnik. Az egy dobozos verzióból való leválasztás döntése nem érzésre megy. Ez egy viszonylag konkrét szám, és meg is tudod keresni.
- Queue: emelkedik-e hétről hétre a vacuum lag azon a táblán, akkor is, ha kifejezetten arra hangoltad az autovacuumot?
- Search: mérhetően romlik-e a checkout p99-ed azokban a percekben, amikor a legnehezebb search lekérdezéseid futnak?
- Event log: megugrik-e a replica lag minden alkalommal, amikor egy downstream fogyasztó felzárkózik?
Van ennek egy élesebb változata is, amiről érdemes tudni. Ha egy downstream fogyasztó (pl. Debezium, ami Kafkánka továbbít) teljesen leáll, a logical replication slot nem csak csendben lemarad - megtart minden WAL szegmenst az utolsó óta, amit az a fogyasztó ténylegesen olvasott, mert nem tudja, hogy az mikor is tér vissza - lehet 10 perc, de lehet hogy hetek óta halott. Ezek a szegmensek felhalmozódnak a lemezen, és magukra hagyva csendben be is töltik azt, jóval azelőtt, hogy bárki észrevenné, hogy a fogyasztó egyáltalán eltűnt, így érdemes ezt is monitorozni.
Bármelyik ilyen jel, és egy valódi szám mögötte, igen erős érv arra, hogy leválaszd azt az egy feature-t - nem mind az ötöt egyszerre, hanem csak azt, ami tényleg fáj, és csak arra a rendszerre, amit pontosan erre a feladatra építettek.
És mivel sima SQL-lel és pár extensionnel indultál egy egyedi integráció helyett, ez a lépés data pipeline probléma, nem pedig egy újraírás. Már ismered az adat pontos alakját. Csak jobb otthont adsz neki, miután kiérdemelte azt. Ez lesz a két út találkozása középen - csak épp ezúttal az öt dobozos útnak csak azt a részét fizetted ki, amire tényleg szükséged lett.
Kezdd eggyel
Szóval melyik dobozt építed meg először? Kezdd eggyel. Akkor adj hozzá speciális rendszert, amikor egy konkrét, mért költség azt mondja - nem akkor, amikor egy diagram mondja, és nem akkor, amikor egy konferencia-előadás mondja.
Zárásként az említett öt dobozos architektúra sosem volt igazán téves. Csak egy olyan kérdésre válasz, amit még senki nem tett fel neked. Hat hónap múlva, ha a vacuum lag tényleg emelkedni kezd, az lesz a nap, amikor érdemes ticketet nyitni a Redisre és nem előbb.
