Dit artikel analyseert de werking van de randompagecost parameter binnen het kostenmodel van een database-planner. De auteur stelt dat de standaardwaarden niet overeenkomen met de werkelijke I/O-kosten van zowel moderne als oudere opslagmedia.
Het kernprobleem is dat het kostenmodel grotendeels blind is voor caching-effecten en het gebruik van het systeemgeheugen. Waar sequentiële scans grote hoeveelheden data uit de cache kunnen verdrijven, zijn index-scans (die random I/O gebruiken) vaak gelokaliseerd tot een zogenaamde 'active set'.
De conclusie is dat randompagecost dient als een proxy om de planner te sturen naar efficiëntere, gelokaliseerde plannen. Omdat dit effect sterk systeemspecifiek is, kan de optimale waarde niet via een generieke tool worden bepaald, maar moet deze worden afgestemd op basis van monitoring van specifieke queries (bijv. via pgstatstatements).
Nog enkele gedachten over randompagecost
Vorige week vond POSETTE plaats, inclusief mijn vooraf opgenomen presentatie over precies dit onderwerp. Dit herinnerde me eraan dat ik anders ben gaan denken over randompagecost. Hier volgt een update met enkele aanvullende gedachten. Ik heb sommige van deze punten al aangestipt in mijn eerdere bericht en uitgebreider behandeld in de POSETTE-presentatie.
De kloof tussen theorie en praktijk
Voordat ik verder ga, deel ik een grafiek met randompagecost-resultaten van roterende SATA-schijven. Ik was volledig vergeten dat ik deze schijven in mijn machine had, totdat ik de kast opende voor onderhoud. Deze schijven liggen waarschijnlijk veel dichter bij de opslag die werd gebruikt voor de oorspronkelijke experimenten rond het jaar 2000 (waarschijnlijk PATA- of SCSI-schijven, maar nog steeds roterend). Men zou verwachten dat dit waarden oplevert die dichter bij de standaardwaarde van 4.0 liggen.
Dat is overduidelijk niet het geval. In feite is de geschatte randompagecost ongeveer 125, wat 2 tot 4 keer hoger is dan de schatting voor SSD-opslag. Bij SSD's komt de waarde dus dichter bij de standaardwaarde, maar dat is puur toeval.
Misschien is er een fundamenteel onderdeel van het oude experiment dat we over het hoofd zien, of zijn de "ruwe" resultaten destijds op een bepaalde manier aangepast. Het lijkt er in ieder geval op dat de standaardwaarde van 4.0 nooit de "ruwe" kosten van random I/O vertegenwoordigde.
Waarom een hogere waarde de prestaties vaak verslechtert
Ik heb veel feedback gekregen van mensen die in het verleden hebben geprobeerd randompagecost te verhogen. In hun ervaring verbeterde dit de prestaties absoluut niet; het verslechterde ze juist. Hoe is dat mogelijk, als een hogere waarde de kostenberekening juist nauwkeuriger zou maken?
Ik geloof dat dit komt doordat randompagecost "compenseert" voor een onvolledig kostenmodel. Het model houdt namelijk geen rekening met diverse caching-effecten en resources die gepaard gaan met plannen die veel random I/O uitvoeren.
Elk kostenmodel is een benadering, en vaak een vrij grove. Het is niet mogelijk om een snel en goedkoop kostenmodel te hebben dat elk klein detail nauwkeurig nabootst. Je kunt het gedetailleerder maken, maar op een gegeven moment wordt het model net zo groot als het systeem zelf, waardoor het nutteloos wordt.
Ons kostenmodel heeft een paar hiaten die hier volgens mij een rol spelen.
Het kostenmodel negeert (grotendeels) het geheugen
De kosten van een operatie worden berekend op basis van de hoeveelheid CPU en I/O die nodig is voor de uitvoering. Dit zijn twee cruciale resources, maar het model negeert het geheugen, wat een andere belangrijke resource is.
We hebben work_mem, maar dat is meer een veiligheidslimiet die de grootte van werkbuffers beperkt (bijvoorbeeld voor sorteren of hashing). Dit kan wel invloed hebben op de hoeveelheid I/O die een operatie moet doen (bijvoorbeeld: kleinere buffers bij een hash join betekenen meer spilling naar disk), maar het trackt niet het overige geheugen dat door een plan wordt "gebruikt".
Stel je een tabel van 100 GB voor, met daarin 1 GB aan "interessante" data (die voldoet aan het query-predicaat). We kunnen de tabel sequentieel scannen of via een index. Als de tabel "koud" is, kan een sequentiële scan ongeveer 100 GB aan andere data uit het geheugen (shared buffers of page cache) duwen. De query kan prima draaien met work_mem=4MB, maar gebruikt effectief 100 GB aan geheugen.
Aan de andere kant hoeft een index mogelijk maar toegang te hebben tot ongeveer 1% van de tabel (de 1 GB aan interessante data). De random I/O kan dan wel meer tijd in beslag nemen dan de sequentiële scan, maar aan de andere kant wordt er slechts 1 GB aan geheugen "gebruikt". Er wordt dus veel minder andere data uit de cache verdreven.
Het kostenmodel is echter volledig blind voor dit effect.
Localiteit van toegang en de 'active set'
Een andere reden is het concept van de active set. Je kunt een database van 1 TB hebben, maar in de meeste praktische systemen wordt slechts een fractie van de data daadwerkelijk benaderd (bijvoorbeeld: gebruikers kijken alleen naar recente bestellingen). Dit noemen we de "active set" van de database.
De sleutel is om deze active set in het geheugen te houden. Plannen die veel random I/O uitvoeren, hebben de neiging meer gelokaliseerd te zijn; ze benaderen alleen de "interessante" data. Een index scan zal doorgaans een veel kleiner deel van de data benaderen dan een sequentiële scan. De I/O is wellicht duurder (random, pagina's meerdere keren bezoeken), maar het beperkt zich tot de relevante data.
Sequentiële scans kunnen de active set massaal uitbreiden—mogelijk tot de gehele database. Dat is onwenselijk, tenzij je daadwerkelijk genoeg RAM hebt om de volledige database te huisvesten. Index scans maken een veel kleinere active set mogelijk.
De planner is echter niet op de hoogte van de active set. Hij plant alsof alle queries beginnen met koude data. Caching-effecten worden alleen in zeer beperkte gevallen meegerekend, zoals caching binnen dezelfde query.
Daarnaast is de planner blind voor diverse andere zaken. Hij plant queries in isolatie, alsof er geen andere queries zijn die strijden om dezelfde resources en alsof de bandbreedte oneindig is. Een sequentiële scan kan geweldig zijn voor één enkel backend-proces, maar wanneer 100 backends gelijktijdig sequentiële scans uitvoeren, loop je tegen de limieten van de opslagbandbreedte aan.
Conclusies
Ik zie randompagecost nu als een proxy voor al deze effecten.
De planner berekent de kosten van geheugen niet op dezelfde manier als CPU of disk, en begrijpt caching-effecten niet. Lagere waarden voor randompagecost sturen de planner richting plannen die meer gelokaliseerd zijn en de active set onder controle houden. Op de lange termijn lijkt dit de winnende strategie.
Dit betekent dat er geen tool kan bestaan die je de "juiste" waarde voor randompagecost geeft na het profileren van je opslag en het meten van de prestaties van sequentiële en random I/O. Ik zou dat graag willen, maar als randompagecost geen ruwe I/O-kost is, zou zo'n tool niet werken.
Het afstellen van randompagecost moet systeemspecifiek gebeuren, gestuurd door feedback uit monitoring. Je kunt bijvoorbeeld kijken naar de top-queries in pgstatstatements en beoordelen of deze baat zouden hebben bij een lagere waarde voor randompagecost. Pas vervolgens de waarde aan en monitor of dit het gewenste effect heeft (of dat het andere queries schaadt).
Nog enkele gedachten over randompagecost
Vorige week vond POSETTE plaats, inclusief mijn vooraf opgenomen presentatie over precies dit onderwerp. Dit herinnerde me eraan dat ik anders ben gaan denken over randompagecost. Hier volgt een update met enkele aanvullende gedachten. Ik heb sommige van deze punten al aangestipt in mijn eerdere bericht en uitgebreider behandeld in de POSETTE-presentatie.
De kloof tussen theorie en praktijk
Voordat ik verder ga, deel ik een grafiek met randompagecost-resultaten van roterende SATA-schijven. Ik was volledig vergeten dat ik deze schijven in mijn machine had, totdat ik de kast opende voor onderhoud. Deze schijven liggen waarschijnlijk veel dichter bij de opslag die werd gebruikt voor de oorspronkelijke experimenten rond het jaar 2000 (waarschijnlijk PATA- of SCSI-schijven, maar nog steeds roterend). Men zou verwachten dat dit waarden oplevert die dichter bij de standaardwaarde van 4.0 liggen.
Dat is overduidelijk niet het geval. In feite is de geschatte randompagecost ongeveer 125, wat 2 tot 4 keer hoger is dan de schatting voor SSD-opslag. Bij SSD's komt de waarde dus dichter bij de standaardwaarde, maar dat is puur toeval.
Misschien is er een fundamenteel onderdeel van het oude experiment dat we over het hoofd zien, of zijn de "ruwe" resultaten destijds op een bepaalde manier aangepast. Het lijkt er in ieder geval op dat de standaardwaarde van 4.0 nooit de "ruwe" kosten van random I/O vertegenwoordigde.
Waarom een hogere waarde de prestaties vaak verslechtert
Ik heb veel feedback gekregen van mensen die in het verleden hebben geprobeerd randompagecost te verhogen. In hun ervaring verbeterde dit de prestaties absoluut niet; het verslechterde ze juist. Hoe is dat mogelijk, als een hogere waarde de kostenberekening juist nauwkeuriger zou maken?
Ik geloof dat dit komt doordat randompagecost "compenseert" voor een onvolledig kostenmodel. Het model houdt namelijk geen rekening met diverse caching-effecten en resources die gepaard gaan met plannen die veel random I/O uitvoeren.
Elk kostenmodel is een benadering, en vaak een vrij grove. Het is niet mogelijk om een snel en goedkoop kostenmodel te hebben dat elk klein detail nauwkeurig nabootst. Je kunt het gedetailleerder maken, maar op een gegeven moment wordt het model net zo groot als het systeem zelf, waardoor het nutteloos wordt.
Ons kostenmodel heeft een paar hiaten die hier volgens mij een rol spelen.
Het kostenmodel negeert (grotendeels) het geheugen
De kosten van een operatie worden berekend op basis van de hoeveelheid CPU en I/O die nodig is voor de uitvoering. Dit zijn twee cruciale resources, maar het model negeert het geheugen, wat een andere belangrijke resource is.
We hebben work_mem, maar dat is meer een veiligheidslimiet die de grootte van werkbuffers beperkt (bijvoorbeeld voor sorteren of hashing). Dit kan wel invloed hebben op de hoeveelheid I/O die een operatie moet doen (bijvoorbeeld: kleinere buffers bij een hash join betekenen meer spilling naar disk), maar het trackt niet het overige geheugen dat door een plan wordt "gebruikt".
Stel je een tabel van 100 GB voor, met daarin 1 GB aan "interessante" data (die voldoet aan het query-predicaat). We kunnen de tabel sequentieel scannen of via een index. Als de tabel "koud" is, kan een sequentiële scan ongeveer 100 GB aan andere data uit het geheugen (shared buffers of page cache) duwen. De query kan prima draaien met work_mem=4MB, maar gebruikt effectief 100 GB aan geheugen.
Aan de andere kant hoeft een index mogelijk maar toegang te hebben tot ongeveer 1% van de tabel (de 1 GB aan interessante data). De random I/O kan dan wel meer tijd in beslag nemen dan de sequentiële scan, maar aan de andere kant wordt er slechts 1 GB aan geheugen "gebruikt". Er wordt dus veel minder andere data uit de cache verdreven.
Het kostenmodel is echter volledig blind voor dit effect.
Localiteit van toegang en de 'active set'
Een andere reden is het concept van de active set. Je kunt een database van 1 TB hebben, maar in de meeste praktische systemen wordt slechts een fractie van de data daadwerkelijk benaderd (bijvoorbeeld: gebruikers kijken alleen naar recente bestellingen). Dit noemen we de "active set" van de database.
De sleutel is om deze active set in het geheugen te houden. Plannen die veel random I/O uitvoeren, hebben de neiging meer gelokaliseerd te zijn; ze benaderen alleen de "interessante" data. Een index scan zal doorgaans een veel kleiner deel van de data benaderen dan een sequentiële scan. De I/O is wellicht duurder (random, pagina's meerdere keren bezoeken), maar het beperkt zich tot de relevante data.
Sequentiële scans kunnen de active set massaal uitbreiden—mogelijk tot de gehele database. Dat is onwenselijk, tenzij je daadwerkelijk genoeg RAM hebt om de volledige database te huisvesten. Index scans maken een veel kleinere active set mogelijk.
De planner is echter niet op de hoogte van de active set. Hij plant alsof alle queries beginnen met koude data. Caching-effecten worden alleen in zeer beperkte gevallen meegerekend, zoals caching binnen dezelfde query.
Daarnaast is de planner blind voor diverse andere zaken. Hij plant queries in isolatie, alsof er geen andere queries zijn die strijden om dezelfde resources en alsof de bandbreedte oneindig is. Een sequentiële scan kan geweldig zijn voor één enkel backend-proces, maar wanneer 100 backends gelijktijdig sequentiële scans uitvoeren, loop je tegen de limieten van de opslagbandbreedte aan.
Conclusies
Ik zie randompagecost nu als een proxy voor al deze effecten.
De planner berekent de kosten van geheugen niet op dezelfde manier als CPU of disk, en begrijpt caching-effecten niet. Lagere waarden voor randompagecost sturen de planner richting plannen die meer gelokaliseerd zijn en de active set onder controle houden. Op de lange termijn lijkt dit de winnende strategie.
Dit betekent dat er geen tool kan bestaan die je de "juiste" waarde voor randompagecost geeft na het profileren van je opslag en het meten van de prestaties van sequentiële en random I/O. Ik zou dat graag willen, maar als randompagecost geen ruwe I/O-kost is, zou zo'n tool niet werken.
Het afstellen van randompagecost moet systeemspecifiek gebeuren, gestuurd door feedback uit monitoring. Je kunt bijvoorbeeld kijken naar de top-queries in pgstatstatements en beoordelen of deze baat zouden hebben bij een lagere waarde voor randompagecost. Pas vervolgens de waarde aan en monitor of dit het gewenste effect heeft (of dat het andere queries schaadt).