Parallella förfrÄgningar i PostgreSQL

Parallella förfrÄgningar i PostgreSQL
I moderna CPU:er finns det mÄnga kÀrnor. Under flera Är har applikationer skickat förfrÄgningar till databaser parallellt. Om det Àr en rapportförfrÄgan om mÄnga rader i en tabell utförs den snabbare nÀr flera CPU:er Àr involverade, och i PostgreSQL Àr detta möjligt sedan version 9.6.

Det tog tre Ă„r att implementera funktionen för parallella förfrĂ„gningar – koden behövde skrivas om vid olika steg i förfrĂ„gningarnas utförande. PostgreSQL 9.6 introducerade infrastrukturen för att ytterligare förbĂ€ttra koden. I senare versioner utförs Ă€ven andra typer av förfrĂ„gningar parallellt.

BegrÀnsningar

  • Aktivera inte parallell exekvering om alla kĂ€rnor redan Ă€r upptagna, annars kommer andra förfrĂ„gningar att bromsa.
  • Det viktigaste Ă€r att parallell behandling med höga values WORK_MEM anvĂ€nder mycket minne – varje hash-förbindelse eller sortering tar minne motsvarande work_mem.
  • OLTP-förfrĂ„gningar med lĂ„g latens kan inte öka hastigheten med parallell exekvering. Om en förfrĂ„gan returnerar en rad kommer den parallella behandlingen bara att sakta ner den.
  • Utvecklare Ă€lskar att anvĂ€nda TPC-H-benchmarken. Kanske har du liknande förfrĂ„gningar för perfekt parallell exekvering.
  • Endast SELECT-förfrĂ„gningar utan predikantlĂ„sning utförs parallellt.
  • Ibland kan rĂ€tt indexering vara bĂ€ttre Ă€n sekventiell skanning av tabellen i parallell lĂ€ge.
  • Stopp av förfrĂ„gningar och markörer stöds inte.
  • Fönsterfunktioner och aggregerade funktioner av ordnade uppsĂ€ttningar Ă€r inte parallella.
  • Du vinner inget i in- och utdataarbetsbelastning.
  • Det finns inga parallella sorteringsalgoritmer. Men förfrĂ„gningar med sorteringar kan exekveras parallellt i vissa aspekter.
  • ErsĂ€tt CTE (WITH 
) med en inbĂ€ddad SELECT för att inkludera parallell bearbetning.
  • Wraps för tredjepartsdata stöder fortfarande inte parallell bearbetning (men det skulle kunna!)
  • FULL OUTER JOIN stöds inte.
  • max_rows inaktiverar parallell bearbetning.
  • Om en förfrĂ„gan innehĂ„ller en funktion som inte Ă€r markerad som PARALLEL SAFE kommer den att vara en-trĂ„dad.
  • TransaktionsisoleringsnivĂ„n SERIALIZABLE inaktiverar parallell bearbetning.

Testmiljö

PostgreSQL-utvecklarna försökte minska svarstiden för TPC-H-benchmarkfĂ„gor. Ladda ner benchmarken och anpassa den till PostgreSQL. Detta Ă€r ett icke-officiellt anvĂ€ndande av TPC-H-benchmarken – inte för att jĂ€mföra databaser eller hĂ„rdvara.

  1. Ladda ner TPC-H_Tools_v2.17.3.zip (eller nyare version) frÄn TPC:s officiella webbplats.
  2. Ändra namnet pĂ„ makefile.suite till Makefile och som beskrivs hĂ€r: https://github.com/tvondra/pg_tpch . Kompilera koden med kommandot make.
  3. Generera data: ./dbgen -s 10 skapar en databas pÄ 23 GB. Det rÀcker för att se skillnaden i prestanda mellan parallella och icke-parallella förfrÄgningar.
  4. Konvertera filer tbl pÄ csv till för och sed.
  5. Klona repositoriet pg_tpch och kopiera filerna csv pÄ pg_tpch/dss/data.
  6. Skapa förfrÄgningar med kommandot qgen.
  7. Ladda upp data i databasen med kommandot ./tpch.sh.

Parallell sekventiell skanning

Det kan vara snabbare, inte för att data lÀses parallellt, utan eftersom data sprids över mÄnga CPU-kÀrnor. I moderna operativsystem cachas PostgreSQL-datafiler vÀl. Med förhandslÀsning kan man hÀmta fler block frÄn lagringen Àn vad PG-processen begÀr. DÀrför begrÀnsas förfrÄgningsprestandan inte av diskens I/O. Den anvÀnder CPU-cykler för att:

  • lĂ€sa rader en och en frĂ„n tabellens sidor;
  • jĂ€mföra vĂ€rden och villkor för rader. DÄR.

LÄt oss utföra en enkel förfrÄgning select:

tpch=# explain analyze select l_quantity as sum_qty from lineitem where l_shipdate <= date '1998-12-01' - interval '105' day;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Seq Scan on lineitem (cost=0.00..1964772.00 rows=58856235 width=5) (actual time=0.014..16951.669 rows=58839715 loops=1)
Filter: (l_shipdate <= '1998-08-18 00:00:00'::timestamp without time zone)
Rows Removed by Filter: 1146337
Planning Time: 0.203 ms
Execution Time: 19035.100 ms

Sekventiell skanning ger för mÄnga rader utan aggregering, sÄ förfrÄgan körs med en enda CPU-kÀrna.

Om man lÀgger till SUM(), syns det att tvÄ arbetstrÄdar hjÀlper till att pÄskynda förfrÄgan:

explain analyze select sum(l_quantity) as sum_qty from lineitem where l_shipdate  Gather (cost=1589701.91..1589702.12 rows=2 width=32) (actual time=8553.241..8555.067 rows=3 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Partial Aggregate (cost=1588701.91..1588701.92 rows=1 width=32) (actual time=8547.546..8547.546 rows=1 loops=3)
-> Parallel Seq Scan on lineitem (cost=0.00..1527393.33 rows=24523431 width=5) (actual time=0.038..5998.417 rows=19613238 loops=3)
Filter: (l_shipdate <= '1998-08-18 00:00:00'::timestamp without time zone)
Rows Removed by Filter: 382112
Planning Time: 0.241 ms
Execution Time: 8555.131 ms

Parallell aggregering

Noden «Parallel Seq Scan» producerar rader för partiell aggregering. Noden «Partial Aggregate» filtrerar dessa rader med hjÀlp av SUM(). I slutet sammanstÀlls SUM-rÀknaren frÄn varje arbetstrÄd av noden «Gather».

Det slutgiltiga resultatet berÀknas av noden "Finalize Aggregate". Om du har egna aggregeringsfunktioner, glöm inte att mÀrka dem som "parallel safe".

Antal arbetsprocesser

Antalet arbetsprocesser kan ökas utan att starta om servern:

explain analyze select sum(l_quantity) as sum_qty from lineitem where l_shipdate  Gather (cost=1589701.91..1589702.12 rows=2 width=32) (actual time=8553.241..8555.067 rows=3 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Partial Aggregate (cost=1588701.91..1588701.92 rows=1 width=32) (actual time=8547.546..8547.546 rows=1 loops=3)
-> Parallel Seq Scan on lineitem (cost=0.00..1527393.33 rows=24523431 width=5) (actual time=0.038..5998.417 rows=19613238 loops=3)
Filter: (l_shipdate <= '1998-08-18 00:00:00'::timestamp without time zone)
Rows Removed by Filter: 382112
Planning Time: 0.241 ms
Execution Time: 8555.131 ms

Vad hÀnder hÀr? Antalet arbetsprocesser har blivit dubbelt sÄ mÄnga, men förfrÄgan blev bara 1,6599 gÄnger snabbare. BerÀkningarna Àr intressanta. Vi hade 2 arbetsprocesser och 1 ledare. Efter förÀndringen blev det 4+1.

VÄr maximala hastighetsökning frÄn parallell bearbetning: 5/3 = 1,66(6) gÄnger.

Hur fungerar det?

Processer

Utförandet av en förfrÄgan börjar alltid med ledarprocessen. Ledaren gör allt icke-parallellt och en del av den parallella bearbetningen. Andra processer som utför samma förfrÄgningar kallas arbetsprocesser. Parallell bearbetning anvÀnder infrastrukturen av dynamiska bakgrundsarbetare (frÄn version 9.4). Eftersom andra delar av PostgreSQL anvÀnder processer istÀllet för trÄdar, kunde en förfrÄgan med 3 arbetsprocesser vara 4 gÄnger snabbare Àn traditionell bearbetning.

Interaktion

Arbetsprocesserna kommunicerar med ledaren via ett meddelandekö (baserat pÄ delad minne). Varje process har 2 köer: för fel och för tupler.

Hur mÄnga arbetsprocesser behövs?

Det minimi begrÀnsning som stÀlls in av parametern max_parallel_workers_per_gather. Sedan hÀmtar frÄgeexecutorn arbetsprocesser frÄn poolen, begrÀnsad av parametern max_parallel_workers size. Den sista begrÀnsningen Àr max_worker_processes, det vill sÀga det totala antalet bakgrundsprocesser.

Om det inte gÄr att tilldela en arbetsprocess, kommer bearbetningen att vara enprocessig.

FrÄgeplaneraren kan minska arbetsprocesser beroende pÄ storleken av tabellen eller indexet. Det finns parametrar för detta: min_parallel_table_scan_size och min_parallel_index_scan_size.

set min_parallel_table_scan_size='8MB'
8MB-tabell => 1 arbetare
24MB-tabell => 2 arbetare
72MB-tabell => 3 arbetare
x => log(x / min_parallel_table_scan_size) / log(3) + 1 arbetare

Varje gÄng tabellen Àr tre gÄnger större Àn min_parallel_(index|table)_scan_size, lÀgger Postgres till en arbetsprocess. Antalet arbetsprocesser baseras inte pÄ kostnader. Cykelberoendet gör komplexa implementationer svÄra. IstÀllet anvÀnder planeraren enkla regler.

I praktiken Ă€r dessa regler inte alltid lĂ€mpliga för produktion, sĂ„ man kan Ă€ndra antalet arbetsprocesser för en specifik tabell: ALTER TABLE 
 SET (parallel_workers = N).

Varför anvÀnds inte parallell bearbetning?

Förutom den lÄnga listan över begrÀnsningar finns det Àven kostnadskontroller:

parallel_setup_cost — för att undvika parallell bearbetning av korta förfrĂ„gningar. Denna parameter uppskattar tiden för att förbereda minnet, starta processen och initialt utbyta data.

parallel_tuple_cost: ledarens kommunikation med arbetarna kan förlÀngas proportionellt mot antalet tuples frÄn arbetande processer. Denna parameter berÀknar kostnaderna för datautbyte.

NĂ€stlad loopsammanfogning — Nested Loop Join

PostgreSQL 9.6+ kan utföra nĂ€stlade loopar parallellt — det Ă€r en enkel operation.

explain (costs off) select c_custkey, count(o_orderkey)
 from customer left outer join orders on
 c_custkey = o_custkey and o_comment not like '%specialposits%'
 group by c_custkey;
 QUERY PLAN
--------------------------------------------------------------------------------------
 Finalize GroupAggregate
 Group Key: customer.c_custkey
 -> Gather Merge
 Workers Planned: 4
 -> Partial GroupAggregate
 Group Key: customer.c_custkey
 -> Nested Loop Left Join
 -> Parallel Index Only Scan using customer_pkey on customer
 -> Index Scan using idx_orders_custkey on orders
 Index Cond: (customer.c_custkey = o_custkey)
 Filter: ((o_comment)::text !~~ '%specialposits%'::text)

Samlingsprocessen sker i det sista steget, sÄ Nested Loop Left Join Àr en parallell operation. Parallel Index Only Scan introducerades först i version 10. Det fungerar pÄ samma sÀtt som ett parallellt sekventiellt scan. Villkoret c_custkey = o_custkey lÀser en order för varje kundrad. SÄ det Àr inte parallellt.

Hash-sammanfogning — Hash Join

Varje arbetsprocess skapar sin egen hash-tabell fram till PostgreSQL 11. Och om det finns fler Àn fyra sÄdana processer ökar inte prestandan. I den nya versionen Àr hash-tabellen gemensam. Varje arbetsprocess kan anvÀnda WORK_MEM för att skapa en hash-tabell.

vÀlj
        l_shipmode,
        summa(fall
                nÀr o_orderpriority = '1-URGENT'
                        eller o_orderpriority = '2-HIGH'
                        dÄ 1
                annars 0
        slut) som high_line_count,
        summa(fall
                nÀr o_orderpriority <> '1-URGENT'
                        och o_orderpriority <> '2-HIGH'
                        dÄ 1
                annars 0
        slut) som low_line_count
frÄn
        orders,
        lineitem
var
        o_orderkey = l_orderkey
        och l_shipmode i ('MAIL', 'AIR')
        och l_commitdate < l_receiptdate
        och l_shipdate < l_commitdate
        och l_receiptdate >= datum '1996-01-01'
        och l_receiptdate < datum '1996-01-01' + intervall '1' Är
grupp efter
        l_shipmode
ordna efter
        l_shipmode
BEGRÄNSA 1;
                                                                                                                                    QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 BegrÀnsning  (kostnad=1964755.66..1964961.44 rader=1 bred=27) (aktuell tid=7579.592..7922.997 rader=1 loop=1)
   ->  Avsluta GroupAggregate  (kostnad=1964755.66..1966196.11 rader=7 bred=27) (aktuell tid=7579.590..7579.591 rader=1 loop=1)
         Gruppnyckel: lineitem.l_shipmode
         ->  Samla Samman  (kostnad=1964755.66..1966195.83 rader=28 bred=27) (aktuell tid=7559.593..7922.319 rader=6 loop=1)
               Arbetare Planerade: 4
               Arbetare Startade: 4
               ->  Delad GroupAggregate  (kostnad=1963755.61..1965192.44 rader=7 bred=27) (aktuell tid=7548.103..7564.592 rader=2 loop=5)
                     Gruppnyckel: lineitem.l_shipmode
                     ->  Sortera  (kostnad=1963755.61..1963935.20 rader=71838 bred=27) (aktuell tid=7530.280..7539.688 rader=62519 loop=5)
                           Sorteringsnyckel: lineitem.l_shipmode
                           Sorteringsmetod: extern sammanfogning  Disk: 2304kB
                           Arbetare 0:  Sorteringsmetod: extern sammanfogning  Disk: 2064kB
                           Arbetare 1:  Sorteringsmetod: extern sammanfogning  Disk: 2384kB
                           Arbetare 2:  Sorteringsmetod: extern sammanfogning  Disk: 2264kB
                           Arbetare 3:  Sorteringsmetod: extern sammanfogning  Disk: 2336kB
                           ->  Parallell Hash Join  (kostnad=382571.01..1957960.99 rader=71838 bred=27) (aktuell tid=7036.917..7499.692 rader=62519 loop=5)
                                 Hash Villkor: (lineitem.l_orderkey = orders.o_orderkey)
                                 ->  Parallell Sekventiell Skanning pÄ lineitem  (kostnad=0.00..1552386.40 rader=71838 bred=19) (aktuell tid=0.583..4901.063 rader=62519 loop=5)
                                       Filter: ((l_shipmode = VALFRI ('{MAIL,AIR}'::bpchar[])) OCH (l_commitdate < l_receiptdate) OCH (l_shipdate < l_commitdate) OCH (l_receiptdate >= '1996-01-01'::datum) OCH (l_receiptdate < '1997-01-01 00:00:00'::tidsstÀmpel utan tidszon))
                                       Rader Borttagna av Filter: 11934691
                                 ->  Parallell Hash  (kostnad=313722.45..313722.45 rader=3750045 bred=20) (aktuell tid=2011.518..2011.518 rader=3000000 loop=5)
                                       Bucket: 65536  Batch: 256  Minnesbruk: 3840kB
                                       ->  Parallell Sekventiell Skanning pÄ orders  (kostnad=0.00..313722.45 rader=3750045 bred=20) (aktuell tid=0.029..995.948 rader=3000000 loop=5)
 Planeringstid: 0.977 ms
 Utförandeavdelningstid: 7923.770 ms

FrÄga 12 frÄn TPC-H visar tydligt en parallell hash-förbindelse. Varje arbetare deltar i att skapa en gemensam hash-tabell.

SammanlĂ€ggning — Merge Join

SammanlÀggning som inte Àr parallell till sin natur. Oroa dig inte om detta Àr det sista steget i förfrÄgan, den kan fortfarande utföras parallellt.

-- Förfrågan 2 från TPC-H
förklara (kostnader av) välj s_acctbal, s_name, n_name, p_partkey, p_mfgr, s_address, s_phone, s_comment
från part, supplier, partsupp, nation, region
var
		p_partkey = ps_partkey
		och s_suppkey = ps_suppkey
		och p_size = 36
		och p_type som '%BRASS'
		och s_nationkey = n_nationkey
		och n_regionkey = r_regionkey
		och r_name = 'AMERIKA'
		och ps_supplycost = (
			välj
			min(ps_supplycost)
		från partsupp, supplier, nation, region
			var
				p_partkey = ps_partkey
				och s_suppkey = ps_suppkey
				och s_nationkey = n_nationkey
				och n_regionkey = r_regionkey
				och r_name = 'AMERIKA'
		)
ordna efter s_acctbal desc, n_name, s_name, p_partkey
BEGRÄNSA 100;
                                                FRÅGEPLAN
----------------------------------------------------------------------------------------------------------
 Begränsa
   -&gt;  Sortera
         Sorteringsnyckel: supplier.s_acctbal DESC, nation.n_name, supplier.s_name, part.p_partkey
         -&gt;  Sammanfoga
               Sammanfoga villkor: (part.p_partkey = partsupp.ps_partkey)
               Join Filter: (partsupp.ps_supplycost = (SubPlan 1))
               -&gt;  Samla samman
                     Planerade arbetare: 4
                     -&gt;  Parallell indexskanning med hjälp av <strong>part_pkey</strong> på del
                           Filter: (((p_type)::text ~~ '%BRASS'::text) OCH (p_size = 36))
               -&gt;  Materialisera
                     -&gt;  Sortera
                           Sorteringsnyckel: partsupp.ps_partkey
                           -&gt;  Nästlad slinga
                                 -&gt;  Nästlad slinga
                                       Join Filter: (nation.n_regionkey = region.r_regionkey)
                                       -&gt;  Sekventiell skanning på region
                                             Filter: (r_name = 'AMERIKA'::bpchar)
                                       -&gt;  Hash-sammanfogning
                                             Hash-villkor: (supplier.s_nationkey = nation.n_nationkey)
                                             -&gt;  Sekventiell skanning på leverantör
                                             -&gt;  Hash
                                                   -&gt;  Sekventiell skanning på nation
                                 -&gt;  Indexskanning med hjälp av idx_partsupp_suppkey på partsupp
                                       Indexvillkor: (ps_suppkey = supplier.s_suppkey)
               SubPlan 1
                 -&gt;  Aggregat
                       -&gt;  Nästlad slinga
                             Join Filter: (nation_1.n_regionkey = region_1.r_regionkey)
                             -&gt;  Sekventiell skanning på region region_1
                                   Filter: (r_name = 'AMERIKA'::bpchar)
                             -&gt;  Nästlad slinga
                                   -&gt;  Nästlad slinga
                                         -&gt;  Indexskanning med hjälp av idx_partsupp_partkey på partsupp partsupp_1
                                               Indexvillkor: (part.p_partkey = ps_partkey)
                                         -&gt;  Indexskanning med hjälp av supplier_pkey på supplier supplier_1
                                               Indexvillkor: (s_suppkey = partsupp_1.ps_suppkey)
                                   -&gt;  Indexskanning med hjälp av nation_pkey på nation nation_1
                                         Indexvillkor: (n_nationkey = supplier_1.s_nationkey)

Noden «Merge Join» ligger ovanför «Gather Merge». SÄ sammanlÀggningen anvÀnder inte parallell bearbetning. Men noden «Parallel Index Scan» hjÀlper ÀndÄ till med segmentet. part_pkey.

SammanlÀggning efter sektioner

I PostgreSQL 11 sammanlÀggning efter sektioner Àr stÀngd som standard: den har mycket kostsam planering. Tabeller med liknande sektionering kan sammanlÀggas sektion för sektion. SÄ kommer Postgres att anvÀnda mindre hash-tabeller. Varje sektion kan vara parallell.

tpch=# set enable_partitionwise_join=t;
tpch=# explain (costs off) select * from prt1 t1, prt2 t2
where t1.a = t2.b and t1.b = 0 and t2.b between 0 and 10000;
                    QUERY PLAN
---------------------------------------------------
 Append
   ->  Hash Join
         Hash Cond: (t2.b = t1.a)
         ->  Seq Scan on prt2_p1 t2
               Filter: ((b >= 0) AND (b <= 10000))
         ->  Hash
               ->  Seq Scan on prt1_p1 t1
                     Filter: (b = 0)
   ->  Hash Join
         Hash Cond: (t2_1.b = t1_1.a)
         ->  Seq Scan on prt2_p2 t2_1
               Filter: ((b >= 0) AND (b <= 10000))
         ->  Hash
               ->  Seq Scan on prt1_p2 t1_1
                     Filter: (b = 0)
tpch=# set parallel_setup_cost = 1;
tpch=# set parallel_tuple_cost = 0.01;
tpch=# explain (costs off) select * from prt1 t1, prt2 t2
where t1.a = t2.b and t1.b = 0 and t2.b between 0 and 10000;
                        QUERY PLAN
-----------------------------------------------------------
 Gather
   Workers Planned: 4
   ->  Parallel Append
         ->  Parallel Hash Join
               Hash Cond: (t2_1.b = t1_1.a)
               ->  Parallel Seq Scan on prt2_p2 t2_1
                     Filter: ((b >= 0) AND (b <= 10000))
               ->  Parallel Hash
                     ->  Parallel Seq Scan on prt1_p2 t1_1
                           Filter: (b = 0)
         ->  Parallel Hash Join
               Hash Cond: (t2.b = t1.a)
               ->  Parallel Seq Scan on prt2_p1 t2
                     Filter: ((b >= 0) AND (b <= 10000))
               ->  Parallel Hash
                     ->  Parallel Seq Scan on prt1_p1 t1
                           Filter: (b = 0)

Huvudsaken Àr att sammanlÀggning efter sektioner endast kan vara parallell om dessa sektioner Àr tillrÀckligt stora.

Parallell bilaga — Parallel Append

Parallel Append kan anvÀndas istÀllet för olika block i olika arbetsprocesser. Detta intrÀffar vanligtvis med UNION ALL-förfrÄgningar. Nackdelen Àr mindre parallellism, eftersom varje arbetsprocess bara behandlar 1 förfrÄgan.

HÀr körs 2 arbetsprocesser, Àven om 4 Àr aktiverade.

tpch=# explain (costs off) select sum(l_quantity) as sum_qty from lineitem where l_shipdate <= date '1998-12-01' - interval '105' day union all select sum(l_quantity) as sum_qty from lineitem where l_shipdate   Parallel Append
         ->  Aggregate
               ->  Seq Scan on lineitem
                     Filter: (l_shipdate   Aggregate
               ->  Seq Scan on lineitem lineitem_1
                     Filter: (l_shipdate <= '1998-08-18 00:00:00'::timestamp without time zone)

De viktigaste variablerna

  • WORK_MEM begrĂ€nsar mĂ€ngden minne för varje process, inte bara för frĂ„gor: work_mem processer anslutningar = vĂ€ldigt mycket minne.
  • max_parallel_workers_per_gather — hur mĂ„nga arbetsprocesser programmet kommer att anvĂ€nda för parallell bearbetning frĂ„n planen.
  • max_worker_processes — justerar det totala antalet arbetsprocesser baserat pĂ„ antalet CPU-kĂ€rnor pĂ„ servern.
  • max_parallel_workers —samma, men för parallella arbetsprocesser.

Sammanfattning

FrÄn och med version 9.6 kan parallell bearbetning avsevÀrt förbÀttra prestandan för komplexa frÄgor som skannar mÄnga rader eller index. I PostgreSQL 10 Àr parallell bearbetning aktiverad som standard. Glöm inte att stÀnga av den pÄ servrar med hög OLTP-belastning. Sekventiella skanningar eller indexskanningar förbrukar mycket resurser. Om du inte kör en rapport pÄ hela datamÀngden kan frÄgor göras mer effektiva genom att bara lÀgga till saknade index eller anvÀnda korrekt partitionering.

LĂ€nkar

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster