# Kapacita: 200 firem, 10 automatizací, 15 lidí na firmu Otázka zněla: zvládne to současný stav s Postgresem, a co to bude chtít za server. Odpověď je změřená, ne odhadnutá. **Krátce: v současném stavu ne.** Runtime běží, ale datová vrstva je psaná na stovky až tisíce ticketů, ne na miliony. Co konkrétně to zastaví a v jakém pořadí to opravit, je níž. ## Co se měřilo Jeden proces, režim souboru, tickety se posílaly přes příjem událostí. Měřeno na vývojovém stroji, tedy horní hranice latence, ne serveru. | Ticketů | Výpis seznamu | Statistiky řešitelů | Soubor | | ------- | ------------- | ------------------- | ------ | | 103 | 1,9 ms | 1,6 ms | 153 kB | | 503 | 6,1 ms | 12,1 ms | 729 kB | | 1 003 | 14,2 ms | 2,7 ms | 1,4 MB | | 2 003 | 18,8 ms | 21,1 ms | 2,9 MB | | 5 003 | 52,9 ms | 1,7 ms | 7,2 MB | Příjem událostí: **130 až 190 událostí za sekundu** včetně celého kola HTTP, uložení a zápisu do logu. Z toho plyne: - **jeden ticket = 1,4 kB** v uložišti, - **výpis roste lineárně**, zhruba 10 mikrosekund na ticket, protože se filtruje pole v paměti, - v režimu souboru se **při každé změně přepisuje celý soubor**. Při pěti tisících ticketech je to 7 MB zápisu kvůli jednomu komentáři. S Postgresem to odpadá, tam je to jeden řádek. ## Kolik toho bude 200 firem, 10 automatizací každá, 15 lidí. Odhad provozu: | Veličina | Výpočet | Za den | Za měsíc | | ----------------- | ---------------------------------- | -------- | -------- | | Běhy automatizací | 2 000 automatizací, 100 běhů denně | 200 000 | 6 mil. | | Kroky | 3 kroky na běh | 600 000 | 18 mil. | | Tickety | 200 firem, 200 denně | 40 000 | 1,2 mil. | | Řádky logu | 5 na ticket plus kroky | ~800 000 | 24 mil. | Špička není průměr. 7 kroků za sekundu v průměru znamená ve špičce klidně 100 za sekundu, protože e-shopy neposílají objednávky rovnoměrně. ## Co to v současném stavu zastaví ### 1. Všechna data jsou v paměti procesu `withMirror` drží tickety, automatizace a incidenty v poli a při startu je **všechny načte**. Při 1,2 milionu ticketů měsíčně je to 1,7 GB jen na tickety, a to bez logu. Aplikace se nenastartuje. Opravit se musí tak, že se tickety čtou dotazem s indexem a stránkováním, ne z pole. `withMirror` má zůstat na tom, co je malé a čte se pořád (rozložení dashboardu), ne na provozních datech. ### 2. Výpis prochází všechno `listTickets` filtruje pole přes všechny firmy. Změřeno 10 mikrosekund na ticket, takže při milionu ticketů je jeden výpis 10 sekund. Musí to být `WHERE tenant_id = ... AND status = ... ORDER BY updated_at LIMIT 50` nad indexem, tedy jednotky milisekund bez ohledu na objem. ### 3. Log je uvnitř ticketu Ticket si nese `trace` i `events` jako součást jednoho záznamu. Každý řádek logu tím přepisuje celý ticket i s celým dosavadním logem. Po padesáti krocích je to padesát zápisů, každý delší než ten předchozí. Log patří do vlastní tabulky s cizím klíčem na ticket a s retencí. Při 24 milionech řádků měsíčně a plné odpovědi služby u každého je to řádově **stovky GB za měsíc**, takže retence není detail, ale podmínka provozu. ### 4. Runtime běží v požadavku `runFlow` vykoná strom rovnou v HTTP požadavku. U ruční akce je to správně, u webhooku ne: - odesílatel čeká, než doběhnou všechny kroky, - při pádu procesu se rozdělaný běh ztratí, protože není kde by byl zapsaný, - není retry, není omezení souběhu na jednu službu, nic to nebrzdí. ### 5. Statistiky se počítají celé `getAgentStats` projde všechny tickety firmy při každém zobrazení widgetu. Při desítkách tisíc na firmu to jde, při milionu ne. Patří to do agregace počítané dávkově, nebo aspoň do dotazu s `GROUP BY` nad indexem. ### 6. Jedna instance Živý stream (SSE), mezipaměť widgetů nad konektory i kopie konfigurace v paměti jsou vázané na proces. Při dvou instancích za load balancerem se rozejdou. Řeší to `LISTEN/NOTIFY` na obnovu kopií a sdílený kanál na stream. ## Konektory nejsou v naší moci Za rychlost a výsledek cizí služby neručíme, a proto se s tím musí počítat v návrhu, ne v provozu: | Riziko | Co s tím | | ------------------------------------ | ------------------------------------------------------------------------ | | Služba odpovídá pomalu | Timeout na krok, ne na celý běh. Běh se uspí a pokračuje. | | Služba je chvíli mimo | Opakování s rostoucí prodlevou, ne hned a ne donekonečna. | | Služba je mimo dlouho | Vypnout ji po sérii chyb a nezkoušet každý běh znovu, ať netrpí ostatní. | | Služba má limit volání | Strop souběžných volání **na dvojici firma a služba**, ne globálně. | | Služba odpoví dvakrát jinak | Klíč proti dvojímu provedení u kroku, aby se nevystavila druhá faktura. | | Služba je pomalá jen pro jednu firmu | Fronta po firmách, aby jedna firma nezablokovala ostatní. | Timeout a rozlišení "zkusit znovu" a "marné" už v `src/runtime/scripts/http.ts` je, klíč proti dvojímu provedení taky. Chybí to, co je nad tím: fronta, opakování a vypínání služby po sérii chyb. ## Co je potřeba udělat, v pořadí 1. **Tickety, běhy a log do Postgresu jako tabulky**, ne jako JSON v paměti. Indexy na `(tenant_id, updated_at)`, `(tenant_id, external_id)` unikátní, `(tenant_id, assignee_id, status)`. Stránkování na všech výpisech. 2. **Log a události zvlášť** od ticketu, s retencí. Bez toho to zaroste. 3. **Fronta běhů.** Tabulka `run_queue`, výběr přes `SELECT ... FOR UPDATE SKIP LOCKED`, worker jako druhý proces téhož obrazu. Webhook jen zapíše událost a odpoví, běh se stane na pozadí. 4. **Opakování a vypínání služby** kolem kroku. 5. **Agregace statistik** dávkově, ne při každém zobrazení. 6. **`LISTEN/NOTIFY`** na obnovu kopií v paměti a sdílený kanál na živý stream. Body 1 až 3 jsou podmínka, aby to vůbec šlo pustit. Zbytek je o tom, aby to bylo použitelné. ## Kolik serveru to bude chtít Po těch úpravách, pro zadaných 200 firem: | Část | Kolik | Proč | | ----------- | --------------------------------- | ------------------------------------------------------------------------------------------------------- | | Web a API | 2 instance, 1 vCPU a 1 GB každá | Požadavky jsou krátké, jde hlavně o dostupnost při restartu. | | Worker běhů | 2 instance, 1 vCPU a 512 MB každá | Kroky čekají na cizí službu, procesor se skoro nepoužije. Jeden proces zvládne stovky souběžných kroků. | | Postgres | 4 vCPU, 8 GB RAM, 200 GB disku | 10 až 20 zápisů za sekundu v průměru je málo, disk sežere log. | | Celkem | ~8 vCPU, ~11 GB RAM | | Kritické číslo není procesor, ale **disk pod databází** a retence logu. S plnou odpovědí služby u každého kroku je to zhruba 20 GB měsíčně na každých 20 milionů řádků, takže 200 GB je půl roku provozu. Buď se odpovědi po měsíci zahazují a nechá se jen shrnutí, nebo se počítá s tím, že disk poroste. Škálování je vodorovné: víc událostí znamená víc workerů, ne větší stroj. Jediné, co se škáluje svisle, je databáze, a ta má u tohoto objemu ještě velkou rezervu. ## Co funguje už teď Aby to nevypadalo hůř, než to je: runtime **běží**. Webhook vykoná strom, akce na ticketu vykoná strom, kroky si předávají výstupy, podmínka větví a celý průběh se zapíše do logu ticketu i s tím, co která služba vrátila. Na jednu firmu s desítkami ticketů denně je současný stav použitelný. Na 200 firem ne.