Nagsusulat kami sa PostgreSQL sa sublight: 1 host, 1 araw, 1TB

Kamakailan ay sinabi ko sa iyo kung paano, gamit ang mga karaniwang recipe pataasin ang pagganap ng SQL read query mula sa database ng PostgreSQL. Ngayon ay pag-uusapan natin kung paano ang pagre-record ay maaaring gawin nang mas mahusay sa database nang hindi gumagamit ng anumang "twists" sa config - sa pamamagitan lamang ng wastong pag-aayos ng mga daloy ng data.

Nagsusulat kami sa PostgreSQL sa sublight: 1 host, 1 araw, 1TB

#1. Pag-section

Isang artikulo tungkol sa kung paano at bakit ito nagkakahalaga ng pag-aayos inilapat na paghahati "sa teorya" ay naging, dito ay pag-uusapan natin ang tungkol sa pagsasanay ng paglalapat ng ilang mga diskarte sa loob ng ating serbisyo sa pagsubaybay para sa daan-daang mga server ng PostgreSQL.

"Mga bagay sa nakalipas na mga araw..."

Sa una, tulad ng anumang MVP, nagsimula ang aming proyekto sa medyo magaan na pagkarga - ang pagsubaybay ay isinasagawa lamang para sa sampung pinaka-kritikal na server, ang lahat ng mga talahanayan ay medyo compact... Ngunit habang tumatagal, ang bilang ng mga sinusubaybayang host ay dumami , at muli ay sinubukan naming gumawa ng isang bagay sa isa sa mga talahanayan na 1.5TB ang laki, napagtanto namin na kahit na posible na magpatuloy sa pamumuhay tulad nito, ito ay napaka-abala.

Ang mga oras ay halos tulad ng epic na panahon, ang iba't ibang bersyon ng PostgreSQL 9.x ay may kaugnayan, kaya ang lahat ng partitioning ay kailangang gawin "manual" - sa pamamagitan ng table inheritance at trigger pagruruta na may dynamic EXECUTE.

Nagsusulat kami sa PostgreSQL sa sublight: 1 host, 1 araw, 1TB
Ang resultang solusyon ay naging sapat na unibersal na maaari itong isalin sa lahat ng mga talahanayan:

  • Ang isang walang laman na talahanayan ng magulang na "header" ay idineklara, na naglalarawan sa lahat kinakailangang mga index at trigger.
  • Ang rekord mula sa pananaw ng kliyente ay ginawa sa talahanayang "ugat", at panloob na ginagamit routing trigger BEFORE INSERT ang tala ay "pisikal" na ipinasok sa kinakailangang seksyon. Kung wala pang ganoon, nakahuli kami ng exception at...
  • … sa pamamagitan ng paggamit CREATE TABLE ... (LIKE ... INCLUDING ...) ay nilikha batay sa template ng parent table seksyon na may paghihigpit sa nais na petsaupang kapag ang data ay nakuha, ang pagbabasa ay isinasagawa lamang dito.

PG10: unang pagtatangka

Ngunit ang paghahati sa pamamagitan ng mana ay dating hindi angkop sa pagharap sa isang aktibong write stream o isang malaking bilang ng mga child partition. Halimbawa, maaalala mo na mayroon ang algorithm para sa pagpili ng kinakailangang seksyon parisukat na kumplikado, na ito ay gumagana sa 100+ na seksyon, ikaw mismo ang nauunawaan kung paano...

Sa PG10 ang sitwasyong ito ay lubos na na-optimize sa pamamagitan ng pagpapatupad ng suporta katutubong paghahati. Samakatuwid, agad naming sinubukang ilapat ito kaagad pagkatapos ilipat ang imbakan, ngunit...

Tulad ng nangyari pagkatapos maghukay sa manual, ang natively partitioned table sa bersyong ito ay:

  • ay hindi sumusuporta sa mga paglalarawan ng index
  • ay hindi sumusuporta sa mga nag-trigger dito
  • hindi maaaring maging "kaapu-apuhan" ng sinuman
  • huwag sumuporta INSERT ... ON CONFLICT
  • hindi awtomatikong makabuo ng seksyon

Ang pagkakaroon ng isang masakit na suntok sa noo gamit ang isang rake, napagtanto namin na imposibleng gawin nang hindi binabago ang aplikasyon, at ipinagpaliban ang karagdagang pananaliksik sa loob ng anim na buwan.

PG10: pangalawang pagkakataon

Kaya, sinimulan naming lutasin ang mga problema na lumitaw nang paisa-isa:

  1. Dahil nag-trigger at ON CONFLICT Nalaman namin na kailangan pa rin namin sila dito at doon, kaya gumawa kami ng isang intermediate na yugto upang ayusin ang mga ito proxy table.
  2. Inalis ang "pagruruta" sa mga nag-trigger - iyon ay, mula sa EXECUTE.
  3. Inilabas nila ito nang hiwalay talahanayan ng template na may lahat ng mga indexpara hindi man lang sila naroroon sa proxy table.

Nagsusulat kami sa PostgreSQL sa sublight: 1 host, 1 araw, 1TB
Sa wakas, pagkatapos ng lahat ng ito, na-partition namin ang pangunahing talahanayan nang natively. Ang paglikha ng isang bagong seksyon ay naiwan pa rin sa budhi ng aplikasyon.

Mga diksyonaryo ng "Sawing".

Tulad ng sa anumang analytical system, mayroon din kami "mga katotohanan" at "mga pagbawas" (mga diksyunaryo). Sa aming kaso, sa kapasidad na ito kumilos sila, halimbawa, katawan ng template mga katulad na mabagal na query o ang teksto ng query mismo.

Ang "Mga Katotohanan" ay na-section sa araw sa loob ng mahabang panahon, kaya mahinahon naming tinanggal ang mga hindi napapanahong seksyon, at hindi nila kami inistorbo (mga log!). Ngunit may problema sa mga diksyunaryo...

Hindi para sabihing marami sila, ngunit humigit-kumulang Ang 100TB ng "mga katotohanan" ay nagresulta sa isang 2.5TB na diksyunaryo. Hindi mo maginhawang tanggalin ang anumang bagay mula sa naturang talahanayan, hindi mo ito mai-compress sa sapat na oras, at ang pagsulat dito ay unti-unting naging mabagal.

Tulad ng isang diksyunaryo... sa loob nito, ang bawat entry ay dapat iharap nang isang beses nang eksakto... at ito ay tama, ngunit!.. Walang pumipigil sa atin na magkaroon ng isang hiwalay na diksyunaryo para sa bawat araw! Oo, nagdudulot ito ng tiyak na kalabisan, ngunit pinapayagan nito ang:

  • magsulat/magbasa nang mas mabilis dahil sa mas maliit na sukat ng seksyon
  • kumonsumo ng mas kaunting memorya sa pamamagitan ng pagtatrabaho sa mas compact na mga index
  • mag-imbak ng mas kaunting data dahil sa kakayahang mabilis na alisin ang lipas na

Bilang resulta ng buong kumplikadong mga hakbang Ang CPU load ay nabawasan ng ~30%, ang disk load ng ~50%:

Nagsusulat kami sa PostgreSQL sa sublight: 1 host, 1 araw, 1TB
Kasabay nito, nagpatuloy kami sa pagsulat ng eksaktong parehong bagay sa database, na may mas kaunting pag-load.

#2. Ebolusyon at refactoring ng database

So we settled on what we have bawat araw ay may kanya-kanyang section may data. sa totoo lang, CHECK (dt = '2018-10-12'::date) β€” at mayroong isang partitioning key at ang kundisyon para sa isang rekord na mahulog sa isang partikular na seksyon.

Dahil ang lahat ng mga ulat sa aming serbisyo ay binuo sa konteksto ng isang tiyak na petsa, ang mga index para sa mga ito mula noong "hindi nahahati na mga oras" ay lahat ng uri (Server, petsa, Template ng Plano), (Server, petsa, Plan node), (petsa, Error class, Server), ...

Ngunit ngayon nakatira sila sa bawat seksyon iyong mga kopya bawat naturang index... At sa loob ng bawat seksyon ang petsa ay pare-pareho... Ito ay lumiliko na ngayon tayo ay nasa bawat naturang index ipasok lamang ang isang pare-pareho bilang isa sa mga patlang, na nagpapataas ng parehong dami nito at ang oras ng paghahanap para dito, ngunit hindi nagdadala ng anumang resulta. Iniwan nila ang kalaykay sa kanilang sarili, oops...

Nagsusulat kami sa PostgreSQL sa sublight: 1 host, 1 araw, 1TB
Ang direksyon ng pag-optimize ay malinaw - simple alisin ang field ng petsa mula sa lahat ng mga index sa mga naka-partition na mesa. Dahil sa aming mga volume, ang pakinabang ay tungkol sa 1TB/linggo!

Ngayon tandaan natin na ang terabyte na ito ay kailangan pa ring maitala kahit papaano. Ibig sabihin, kami din ang disk ay dapat na ngayong mag-load nang mas kaunti! Ang larawang ito ay malinaw na nagpapakita ng epekto na nakuha mula sa paglilinis, kung saan kami ay nakatuon sa isang linggo:

Nagsusulat kami sa PostgreSQL sa sublight: 1 host, 1 araw, 1TB

#3. "Pagkakalat" ng peak load

Isa sa mga malaking problema ng load system ay kalabisan synchronization ilang mga operasyon na hindi nangangailangan nito. Minsan "dahil hindi nila napansin", minsan "mas madali sa ganoong paraan", ngunit maaga o huli kailangan mong alisin ito.

Mag-zoom in tayo sa nakaraang larawan at tingnan na mayroon tayong disk "mga bomba" sa ilalim ng pagkarga na may dobleng amplitude sa pagitan ng mga katabing sample, na malinaw na "istatistika" ay hindi dapat mangyari sa ganoong bilang ng mga operasyon:

Nagsusulat kami sa PostgreSQL sa sublight: 1 host, 1 araw, 1TB

Ito ay medyo madaling makamit. Nagsimula na kaming magmonitor halos 1000 server, ang bawat isa ay pinoproseso ng isang hiwalay na lohikal na thread, at nire-reset ng bawat thread ang naipon na impormasyon na ipapadala sa database sa isang tiyak na dalas, tulad nito:

setInterval(sendToDB, interval)

Ang problema dito ay tiyak na nakasalalay sa katotohanang iyon ang lahat ng mga thread ay nagsisimula sa humigit-kumulang sa parehong oras, kaya ang kanilang mga oras ng pagpapadala ay halos palaging nag-tutugma β€œto the point.” Oops #2...

Sa kabutihang palad, ito ay medyo madaling ayusin, pagdaragdag ng "random" na run-up sa oras:

setInterval(sendToDB, interval * (1 + 0.1 * (Math.random() - 0.5)))

#4. Ini-cache namin ang kailangan namin

Ang ikatlong tradisyunal na problema sa highload ay walang cache kung nasaan siya maaari maging.

Halimbawa, ginawa naming posible na pag-aralan sa mga tuntunin ng mga node ng plano (lahat ng mga ito Seq Scan on users), ngunit agad na isipin na sila ay, para sa karamihan, pareho - nakalimutan nila.

Hindi, siyempre, walang nakasulat sa database muli, pinuputol nito ang trigger gamit ang INSERT ... ON CONFLICT DO NOTHING. Ngunit ang data na ito ay umaabot pa rin sa database, at ito ay hindi kailangan pagbabasa upang suriin kung may salungatan kailangang gawin. Oops #3...

Ang pagkakaiba sa bilang ng mga tala na ipinadala sa database bago/pagkatapos paganahin ang caching ay halata:

Nagsusulat kami sa PostgreSQL sa sublight: 1 host, 1 araw, 1TB

At ito ang kasamang pagbaba ng load ng imbakan:

Nagsusulat kami sa PostgreSQL sa sublight: 1 host, 1 araw, 1TB

Sa kabuuan

Nakakatakot lang ang "Terabyte-per-day". Kung gagawin mo ang lahat ng tama, ito ay tama 2^40 byte / 86400 segundo = ~12.5MB/sna kahit desktop IDE screws hawak. πŸ™‚

Ngunit seryoso, kahit na may sampung beses na "skew" ng pagkarga sa araw, madali mong matutugunan ang mga kakayahan ng mga modernong SSD.

Nagsusulat kami sa PostgreSQL sa sublight: 1 host, 1 araw, 1TB

Pinagmulan: www.habr.com

Magdagdag ng komento