RE:NODE

Databáze14 min čtení

Pooly připojení k databázi: velikost a limity

Příliš mnoho klientů je chyba konfigurace, ne kapacity. Jak pool funguje, výpočet, který nikdo nedělá, rozumné velikosti, timeouty, PgBouncer a úniky.

Aktualizováno

3 čtenáři

Každá databáze má maximální počet souběžných připojení a ten je nižší, než většina lidí předpokládá. Překročení nezpůsobí elegantní zpomalení. Způsobí chybu, a to pod zátěží, tedy přesně když nejméně potřebuješ nový druh selhání:

code
FATAL: sorry, too many clients already

Ta zpráva není problém kapacity a větší tarif ji nevyřeší. Je to aritmetika: počet připojení, která mohou otevřít procesy tvé aplikace, vynásobený počtem procesů, je větší než číslo, které databáze přijímá. Tento článek je o tom, jak ten výpočet udělat dřív, než ho za tebe udělá produkce - kolik připojení stojí, proč malý pool porazí velký, z jakých čísel začít, jaké timeouty promění zaseknutí v čistou chybu a jak najít únik, když se pool během hodin vyčerpá.

Kolik připojení doopravdy stojí#

V PostgreSQL je připojení proces operačního systému. Postmaster pro každé vytvoří forkem backend, který žije, dokud se klient neodpojí. Má vlastní soukromou paměť o velikosti několika megabajtů, k tomu cache a struktury bufferů, kterých se dotýká, a k tomu to, co si jeho dotaz alokuje v work_mem, dokud běží. Sto nečinných připojení je sto procesů, které musí jádro plánovat, a pořádný kus paměti pryč dřív, než proběhne jediný dotaz.

Ani otevření není zadarmo. Nové připojení znamená fork, autentizaci, vyjednání TLS, pokud ho používáš, a nastavení relace, které spouští tvůj framework. V lokální síti jsou to jednotky milisekund; přes internet s TLS to mohou být desítky. U webového požadavku, který má 30 ms skutečné práce, zaplatit pokaždé 20 ms za otevření připojení zdvojnásobí latenci zbytečně.

MongoDB je na připojení levnější - vlákno místo procesu - ale ne zadarmo, a server má pořád strop. Tam skutečně ovládáš pool v ovladači a platí pro něj stejná úvaha.

Pool řeší oba problémy. Jednou otevře pevný počet připojení, rozdává je tomu, kdo si v kódu řekne, na konci požadavku je přebírá zpět a drží je zahřátá. Má ho každý běžný framework. Otázka nikdy nezní, zda poolovat; zní, jaké číslo dát do max.

acquirekdyž jsou všechna obsazená10 relacísamotný dotazPožadavky200 v běhuPoolmax 10Čekajícíacquire timeoutPostgreSQLmax_connections 100CPU a NVMeskutečný limit
Pool je fronta s pevným počtem dveří

Proč víc připojení věci zpomalí#

Tohle je ta neintuitivní část a je to důvod, proč je odpověď obvykle menší číslo, než lidé čekají.

Databázový server dokáže skutečně dělat omezený počet věcí najednou: zhruba tolik, kolik má jader CPU, plus nějaký překryv pro to, co čeká na disk. Za touto hranicí další připojení nic víc nestihnou. Řadí se do fronty - ale místo aby čekala slušně ve tvém poolu, kde to nic nestojí, čekají uvnitř databáze, kde každý čekající dotaz drží paměť, zámky a místo v plánovači, a kde přepínání kontextu mezi stovkami backendů spaluje CPU, které mohlo spouštět dotazy.

Výsledkem je křivka propustnosti, která stoupá, zplošťuje se a pak klesá. Deset připojení, která zůstávají vytížená, porazí sto, která se přetahují, a těch sto bude mít horší latenci v chvostu i horší propustnost, protože každý dotaz teď čeká za devadesáti devíti dalšími místo devíti.

Existuje elegantní způsob, jak to vidět. Souběžnost se rovná propustnosti krát latence. Pokud tvoje aplikace dělá 500 dotazů za sekundu a průměrný dotaz trvá 4 ms, jsou v každém okamžiku v průměru obsazena dvě připojení. Dvě. Pool o padesáti, který jsi opsal z blogu, nic nezrychluje; je to pojistka proti špičce, kterou by fronta zvládla lépe.

Výpočet, který nikdo nedělá#

Číslo v konfiguračním souboru platí na proces. Skoro každý incident začíná tím, že se na to zapomene.

SoučástPřipojeníPoznámka
Webová aplikace, 4 procesy, pool o 2080Číslo, které lidé uvádějí jako „20“
Worker na pozadí, 2 procesy, pool o 510Obvykle se zapomene úplně
Plánované úlohy, občas se překrývají5Špička, když běží noční report
Tvoje relace psql při ladění1Ta, kterou potřebuješ nejvíc
Monitoring nebo exportér metrik2Dotazuje se pořád, při chybě se znovu připojí
Celkem proti výchozímu `max_connections = 100`98Dvě volná

Výchozí max_connections v PostgreSQL je 100 a tři z nich drží stranou superuser_reserved_connections, aby se správce mohl pořád dostat dovnitř. Tvůj skutečný rozpočet je 97. Naplnění na 98 znamená, že selže příští deploy, který krátce provozuje staré a nové procesy dohromady.

Počítej každý proces, který se připojuje, ne každý server. V sestavě s PHP nebo WordPressem žádný pool v obvyklém smyslu neexistuje: každý worker PHP-FPM drží vlastní připojení, takže pm.max_children je velikost tvého poolu a čtyřicet potomků je čtyřicet připojení. V kontejnerizovaném prostředí zdvojnásobí přechod ze dvou kontejnerů na čtyři celkový počet, aniž by někdo sáhl na nastavení databáze. A cokoli, co spouští proces na každý požadavek - serverless platforma, hosting ve stylu CGI - se násobí bez omezení, což je přesně případ, pro který PgBouncer existuje.

Dimenzování poolu: vzorec a výchozí bod#

Nejstarší a pořád nejlepší pravidlo pochází z komunity PostgreSQL a proslavila ho dokumentace HikariCP:

code
connections = (core_count * 2) + effective_spindle_count

Na serveru se 2 vCPU a úložištěm NVMe to vychází na zhruba 5 nebo 6 pro celou aplikaci, ne na proces. Na 4 vCPU zhruba 9 nebo 10. Ta čísla vypadají absurdně malá každému, kdo roky provozoval pool o 50 a ničeho si nevšiml, a právě v tom je smysl: nikdy nevyužíval 50, jen se řadil ve frontě uvnitř databáze místo před ní.

Praktický výchozí bod pro malou aplikaci:

SituaceCelkem připojeníJak je rozdělit
Databáze s 1-2 vCPU, jeden proces aplikace5-10Jeden pool, max 8
Databáze se 2 vCPU, 4 procesy aplikace10-12max 3 na proces
Databáze se 4 vCPU, 4 procesy aplikace plus 2 workery16-20max 3 každý, 2 pro workery
Navíc vždy3-5Migrace, monitoring, ty

Pak upravuj podle důkazů. Pokud se pool nikdy nevyčerpá a dotazy jsou rychlé, je dost velký - příliš velký pool neukazuje žádný příznak, dokud nepřijde den, kdy ukáže. Pokud požadavky čekají na získání připojení, zatímco CPU databáze zahálí, zvětši ho. Pokud požadavky čekají a CPU databáze je nasycené, pool problém není: oprav dotazy pomocí EXPLAIN ANALYZE nebo přidej index, protože větší pool jen rozloží stejné CPU na tenčí vrstvu.

Na RE:NODE jsou databázové řady vlastní server, ne plátek sdíleného clusteru, se soubory i konzolí k dispozici, takže max_connections je to, co říká postgresql.conf na tom serveru, a můžeš ho měnit. Při zvyšování na malém tarifu buď opatrný: omezením je paměť, a když kontejner dosáhne svého limitu paměti, tady se zastaví a čistě restartuje, místo aby swapoval. Sto backendů na tarifu s 1 GB promění pomalé odpoledne v restart. Které číslo zvýšit jako první, popisuje ladění Postgresu pro malé servery a tohle to málokdy bývá.

Nastavení poolu v knihovnách, které používáš#

Každý pool vystavuje stejnou hrstku myšlenek pod různými názvy. Tohle jsou výchozí hodnoty, které stojí za to znát, protože několik z nich je pro serverovou aplikaci špatných.

KnihovnaNastavení velikostiVýchozíStojí za nastavení
node-postgres (pg)max10connectionTimeoutMillis, idleTimeoutMillis
SQLAlchemypool_size5 (plus max_overflow 10)pool_pre_ping, pool_recycle
HikariCP (Java)maximumPoolSize10connectionTimeout, maxLifetime
DjangoCONN_MAX_AGE0, nové připojení na požadavek60, nebo explicitní pool
PHP-FPMpm.max_childrenliší seBer ho jako velikost poolu
Ovladače MongoDBmaxPoolSize100Sniž ho, plus waitQueueTimeoutMS

Ke třem z těch výchozích hodnot je třeba komentář. max_overflow v SQLAlchemy znamená, že skutečný strop je 15 na proces, ne 5, což překvapí lidi, kteří dělají výše uvedený výpočet. Django historicky otevíralo a zavíralo připojení na každý požadavek, což je bezpečné a pomalé; CONN_MAX_AGE nastavené na něco jako 60 ho znovu používá a novější verze umějí použít skutečný pool s psycopg 3 - zkontroluj dokumentaci pro svou verzi, protože se to nedávno změnilo. A výchozích 100 na proces u ovladačů MongoDB je mnohem víc, než jakákoli malá aplikace potřebuje.

javascript
// node-postgres: one pool per process, created onceimport { Pool } from "pg";export const pool = new Pool({  connectionString: process.env.DATABASE_URL,  max: 5,  idleTimeoutMillis: 30_000,  connectionTimeoutMillis: 5_000,   // fail fast instead of hanging});
python
# SQLAlchemy: the real ceiling here is 8, not 5engine = create_engine(    os.environ["DATABASE_URL"],    pool_size=5,    max_overflow=3,    pool_timeout=5,       # seconds to wait for a connection    pool_recycle=1800,    # reconnect before anything else drops it    pool_pre_ping=True,   # check liveness before handing one out)

Pool vytvoř jednou, na úrovni modulu nebo při startu aplikace, a nikdy uvnitř handleru požadavku. Pool vytvořený na každý požadavek není pool; je to připojení navíc s dalšími kroky a je to nejčastější způsob, jak se to v malých kódových základnách pokazí.

Timeouty: rychle selhat místo hromadění#

Pool bez timeoutů promění pomalou databázi v zaseknutou aplikaci. Záleží na čtyřech timeoutech a měly by mít různé hodnoty.

  • Acquire timeout (connectionTimeoutMillis, pool_timeout, connectionTimeout). Jak dlouho požadavek čeká na volné připojení, než to vzdá. Nastav několik sekund. Požadavek, který už čekal pět sekund, je požadavek, na který se nikdo nedívá, a jeho selhání uvolní místo tomu, který se počítá.
  • Statement timeout. Nastavuje se na databázi nebo relaci a zabije dotaz, který běží příliš dlouho. SET statement_timeout = '10s' pro webový provoz, víc pro reporty, vypnuto pro migrace.
  • Idle in transaction timeout. idle_in_transaction_session_timeout má výchozí hodnotu 0, tedy nikdy, a ta výchozí hodnota způsobila víc výpadků než cokoli jiného. Připojení, které otevřelo transakci a pak odešlo, drží své zámky a donekonečna blokuje vacuum. Třicet sekund je štědrých.
  • Maximální životnost (maxLifetime, pool_recycle). Připojení pravidelně zavírej a otevírej znovu, aby ti firewall, proxy nebo failover nenechaly sockety, na které druhá strana už zapomněla.
sql
-- Sensible defaults for an application role, set onceALTER ROLE app_user SET statement_timeout = '15s';ALTER ROLE app_user SET idle_in_transaction_session_timeout = '30s';ALTER ROLE app_user SET lock_timeout = '3s';

Nastavení na roli místo v kódu aplikace znamená, že platí pro každé připojení, včetně toho, které si někdo ve dvě ráno otevře z notebooku. Jakou roli použít a proč by to neměl být superuživatel, popisuje role a oprávnění v Postgresu.

Smysl všech čtyř je stejný: proměnit neomezené čekání v rychlou, viditelnou chybu, kterou vidí tvoje health checky a logy. Aplikace, která vrátí 503 za dvě vteřiny, se dá zotavit; ta, která drží deset tisíc požadavků otevřených, dokud load balancer nevyprší, ne. Druhou polovinu tohoto chování pokrývá řádné ukončení a health checky.

PgBouncer a kdy ho potřebuješ#

PgBouncer je malá proxy předřazená PostgreSQL. Aplikace se připojují k ní a ona drží mnohem menší pool skutečných připojení k databázi a podle potřeby je rozděluje. Existuje proto, že model připojení v PostgreSQL je drahý a některé tvary nasazení vytvářejí připojení po tisících.

pgbouncer.ini
[databases]appdb = host=127.0.0.1 port=5432 dbname=appdb[pgbouncer]listen_port = 6432pool_mode = transactionmax_client_conn = 1000default_pool_size = 10

Tři režimy poolu a volba mezi nimi je celé rozhodnutí:

  • session - klient drží serverové připojení, dokud se neodpojí. Bezpečné, kompatibilní se vším a skoro nic neušetří.
  • transaction - serverové připojení se vrací na konci každé transakce. To je režim, který stojí za to, a ten, který namapuje tisíc klientů na deset backendů.
  • statement - vrací se po každém příkazu. Rozbíjí transakce o více příkazech. Málokdy vhodné.

Režim transaction má podmínky, protože všechno, co žije v relaci místo v transakci, přestává být spolehlivé: příkazy SET, advisory zámky na úrovni relace, LISTEN/NOTIFY a kurzory držené mimo transakci. Serverové prepared statements byly roky problém; novější verze PgBouncer je podporují přes protokol pomocí max_prepared_statements, ale než se na to spolehneš, zkontroluj svou verzi.

PgBouncer potřebuješ, když máš mnoho krátkodobých procesů, které chtějí každý připojení - sestavu s PHP a stovkami workerů, platformu, která spouští proces na požadavek, nebo několik aplikací sdílejících jednu databázi. Nepotřebuješ ho pro jednu aplikaci v Node nebo Pythonu se správně dimenzovaným poolem, kde by byl jen druhým procesem k provozu bez užitku. Přidávat ho „kvůli škálování“ při čtyřech procesech aplikace je klasický případ řešení problému, který nemáš.

Hledání úniku#

Pool, který se vyčerpává pomalu, během hodin, a po restartu se zotaví, nemá nedostatek kapacity. Má cestu kódem, která připojení získá a nikdy nevrátí.

Vzorec je vždy stejný: předčasný return, vyhozená výjimka nebo větev, která přeskočí uvolnění. Kdekoli vidíš ruční connect bez uvolnění v bloku finally nebo v kontextovém manažeru, máš kandidáta.

javascript
// The leak: an exception here never releases the clientconst client = await pool.connect();const rows = await client.query(sql);   // throwsclient.release();// The fixconst client = await pool.connect();try {  return await client.query(sql);} finally {  client.release();}

Ještě lepší je připojení si ručně nevypůjčovat. pool.query() v node-postgres, blok with v Pythonu, kontextový manažer nebo vrstva repozitáře - všechno to činí uvolnění automatickým a na žádné z toho se nedá zapomenout v kódové cestě přidané později.

Abys potvrdil, co vidíš, zeptej se databáze, místo abys hádal:

sql
-- Who is connected, and what are they doing?SELECT state, count(*) FROM pg_stat_activity GROUP BY state;-- The dangerous ones: open transactions doing nothingSELECT pid, usename, state,       now() - state_change AS idle_for,       left(query, 60) AS last_queryFROM pg_stat_activityWHERE state = 'idle in transaction'ORDER BY idle_for DESC;
javascript
// MongoDB: current, available and total ever createddb.serverStatus().connections

Hromada relací idle in transaction je uniklá transakce, ne uniklé připojení, a je to horší: drží zámky a brání vacuum v úklidu. Hromada obyčejných relací idle rovná nakonfigurovanému maximu je zdravý pool v klidu. Růst totalCreated v MongoDB nebo připojení, která dál rostou, když provoz zplaší, je podpis poolu, který se někde vytváří znovu místo aby se používal opakovaně.

Z poolu exportuj tři čísla a sleduj je: připojení v použití, čekající požadavky a 99. percentil doby čekání na získání. Samotné vytížení neříká nic - pool na 100 procentech, kde nikdo nečeká, je dokonale dimenzovaný. Smysl má doba čekání a obecnou úvahu o výběru metrik tímto způsobem předkládá monitoring, který ti něco řekne. Pokud je doba čekání vysoká, je databáze vytížená nebo jsou dotazy pomalé a kdy upgradovat začíná rozlišením těch dvou.

FAQ#

Co znamená „sorry, too many clients already“?

Že PostgreSQL dosáhl max_connections a odmítl novou relaci. Spočítej procesy své aplikace vynásobené velikostí jejich poolu, plus workery, úlohy z cronu a každého klienta, kterého máš otevřeného. Ten součet je nad limitem a oprava je téměř vždy menší pool, ne vyšší limit.

Mám prostě zvýšit max_connections?

Málokdy. Každé připojení je proces s vlastní pamětí, takže zvýšení limitu vymění paměť, kterou potřebuješ na dotazy, za připojení, která budou nečinná. Zvyš ho, jen když jsi potvrdil, že všechna připojení dělají užitečnou práci, a nejdřív sniž pool.

Jaká je dobrá velikost poolu?

Začni zhruba na dvojnásobku počtu jader CPU databáze, rozložených přes všechny procesy aplikace, plus pár navíc pro migrace a administraci. Pro malou aplikaci na databázi se 2 vCPU je to kolem 8 připojení celkem - ne na proces.

Potřebuji PgBouncer?

Jen pokud máš mnoho krátkodobých procesů, které chtějí každý připojení: sestavu s PHP a velkým pm.max_children, platformu s procesem na požadavek, nebo několik aplikací sdílejících jednu databázi. Jedna dobře poolovaná aplikace z něj nic nemá a pooling v režimu transaction omezuje funkce relace, které možná používáš.

Proč mi pool přes noc dojde, ale po restartu funguje?

To je únik. Nějaká kódová cesta získá připojení a nikdy ho neuvolní, obvykle proto, že výjimka přeskočila uvolnění. Každé ruční vypůjčení obal do finally nebo kontextového manažeru a dej přednost vlastnímu pomocníkovi poolu pro dotazy, aby se uvolnění nedalo zapomenout.

Má MongoDB stejný problém?

Stejný tvar, menší závažnost. Jeho připojení jsou vlákna, ne procesy, ale výchozí maxPoolSize 100 v ovladači na proces se pořád násobí přes tvoji aplikaci. Sniž ho, nastav timeout fronty čekání a když si nejsi jistý, zkontroluj db.serverStatus().connections.


Komentáře

Zcela anonymně: žádný účet, žádný e-mail, žádné cookie. Ukládáme jen jméno, které napíšeš, text a čas - nic dalšího. Počet odkazů je omezený a značkování se nevykresluje.

0/2000