Palvelinraudan arkkitehtuuri vs. palvelinsoftan arkkitehtuuri

Yhteenveto ongelmanratkaisussa kirjoittamastani yhteistyösanastosta, jonka tarkoitus oli antaa DevOps hengessä tuotanto-ongelmaa ratkovalle porukalle yhteinen kieli läpi koko pinon.

Rauta

Useimmat enterprise-palvelimet, räkki ja blade, tulevat tänä päivänä kaupasta Haswell EP / EX arkkitehtuureilla.

Suosituimmat (hintatehokkaimmat, ja blade-maailmassa kooltaan/energiatiheydeltään sopivat) konfiguraatiot sovelluspalvelinten virtualisointialustoille ovat 2- ja 4-prosessorisia.

Palvelinten emolevyjen perusarkkitehtuuri näyttää tältä:

Käytännössä se tarkoittaa sitä, että emolevyllä on kaksi tai useampia prosessoreita, ja jokaisella prosessorilla on sekä oma L3 paikallinen cache-muistialue, että 6-12 kappaletta DDR3 (yleensä) 16 GB muistipiiriä kiinni jokaisessa prosessorissa.

Se, että tietyt muistipiirit ovat kiinni tietyissä prosessoreissa on tärkeää myöhemmin NUMA kohdassa.

Tietyn prosessorin L3 cache on kaikkien kyseisen prosessorin corejen käytettävissä olevaa paikallista muistia. Core saa haettua tietoa L3 cachestaan muutamassa nanosekunnissa, kun taas keskusmuistista haetaan tietoa kymmenistä satoihin nanosekunteja. Kahdella prosessorilla voi olla L3 cachessaan näkymä samaan keskusmuistin kohtaan, mistä lisää JVM-kappaleen concurrency-osiossa.

Muistin teknisistä yksityiskohdista kiinnostuneelle suosittelen erittäin lämmöllä seuraavaa loistavaa Ulrich Drepperin kirjoitusta vuodelta 2007, joka on edelleen validi:

http://lwn.net/Articles/250967/ - What every programmer should know about memory

Muista referenssejä:

http://software.intel.com/en-us/articles/intel-performance-counter-monitor

http://www.anandtech.com/print/5553/the-xeon-e52600-dual-sandybridge-for-servers


Non-uniform memory access (NUMA)

Kun ohjelma jota ajetaan CPU0:la haluaa lukea/kirjoittaa muistia CPU1:n muistikontrollerissa kiinni olevalle DIMM-tikulle, ohjelman pitää tehdä tämä pyyntö prosessorien välisen jaetun bussin ylitse.

Tämä on erittäin hidasta suhteessa muistin lukemiseen/kirjoittamiseen samassa CPU:ssa kiinni olevaan DIMM:iin.

Siksi sekä VMWare että Linux pitävät kirjaa siitä, millä fyysisellä prosessorilla tietty prosessi pyörii, ja antavat ohjelmalle mahdollisuuden / pakottavat sen (riippuen konfiguraatiosta) pyörimään tietyllä CPU:lla ja varaamaan myös siinä kiinni olevaa muistia.

Jos tätä ei oteta huomioon, kuten esim. pari vuotta sitten varsin yleisessä MySQL casessa, jossa kaupasta saatavat palvelinarkkitehtuurit muuttuivat sysadminien huomaamatta, voi tulla tuotannossa tenkkapoo, jos eri vastuutahot eivät kommunikoi.

Referenssejä:

http://lwn.net/Articles/254445/ - Memory part 4: NUMA support

http://blog.jcole.us/2010/09/28/mysql-swap-insanity-and-the-numa-architecture/


VMWare, memory ballooning, idleTax

Vuosina 2015-2016 sovelluspalvelimia harvemmin asennetaan suoraan raudalle asennettuun käyttöjärjestelmään, vaan fyysiset palvelimet konfiguroidaan yleensä virtuaalialustoiksi, joissa pyöriviin virtuaalikoneisiin asennetaan sovelluksia.

Tämä on fiksua, sillä raudan suorituskyky on kasvanut merkittävästi nopeammin, kuin ihmisten vaatimukset enterprise-sovelluksille.

Yhdellä uudella palvelimella voi ajaa yhtä paljon teoreettista keskivertosoftaa, kuin kolmella pari sukupolvea vanhemmalla.

Suomen kokoisessa maassa sama on totta myös suomenkielisten kuluttajasovellusten osalta, 5-6 miljoonaa markkinoilla toimijaa ei yht'äkkiä kasvakaan 10 miljoonaksi.

VMWare on suosituin ja paras virtualisointialusta. Se ei ole kuitenkaan asiakkaille ainoa harkittava vaihtoehto, vaan sen lisenssin kalleus voi tehdä esim. KVM-tuotteen järkeväksi yksinkertaisemmaksi vaihtoehdoksi.

Toisaalta Docker ja muut virtualisoinnin hyödyt muilla keinoin tuottavat container-tuotteet ovat tekemässä voimakkaasti tuloaan 2016 alkupuolella.

Kannattaa erityisesti pitää silmällä Dockeria, Googlen Kubernetestä, ja Red Hatin OpenShiftiä.

Oman kokemuspiirini sisällä, 90% suomalaisten yritysten enterprise-sovelluksista pyöritetään virtualisoidun Linuxin päällä.

VMWare on rakentanut Linuxille omat ajurit, jotka osaavat mm. hallita käyttöjärjestelmän muistinkäyttöä eri tavoin, kuten ballooning, tai Kernel Samepage Merging.

Jos VMWare alustan ja sillä pyörivien guestien kapasiteetti mitoitetaan siten, että guesteille ei luvata kauheasti enemmän muistia, kuin mitä alusta voi fyysisesti tarjota, nämä ajurit eivät välttämättä koskaan aktivoidu.

VMWare pitää kirjaa virtuaalikoneiden muistinvarauksesta ja muistinkäytöstä. Se antaa virtuaalikoneille "idleTax" pisteitä siitä, jos virtuaalikone varaa muistia, mutta ei kuitenkaan käy koskemassa kaikkia varattuja sivuja jatkuvasti.

Kun VMWare alustan vapaa muisti putoaa 6% kokonaismuistista, alusta alkaa käskeä korkean "idleTax" pistemäärän omaavien virtuaalikoneiden käyttöjärjestelmiä swappaamaan käyttöjärjestelmätasolla harvoin käytettyjä muistisivuja käyttöjärjestelmän omaan swappitiedostoon.

Tässä tilanteessa, jos käyttöjärjestelmällä ei ole omaa swappia riittävästi, virtuaalikoneen Linuxin oom-killer voi tappaa virtuaalikoneen pääsovelluksen.

Näitä defaulttiasetuksia voi ja pitää myös hallita VMWare alustan konfiguraatiosta, virtuaalikone- ja sovelluskohtaisesti.

Uudet virtualisointialustat voivat lukea raudan NUMA-konfiguraation, ja tarvittaessa esittää sen virtuaalikoneille tavalla, jota ne voivat hyödyntää muistiallokaatiota optimoidessaan.

Referenssejä:

http://frankdenneman.nl/2010/06/11/memory-reclaimation-when-and-how/

http://kb.vmware.com/kb/1003586 - The balloon driver, vmmemctl, is unaware of pinned pages


Linux, overcommit, virtual memory, swap

Linux on muistinhallinnan osalta vähän paha poika.

$ free -m
             total       used       free     shared    buffers     cached
Mem:          7985       4666       3319          0        466       1304
-/+ buffers/cache:       2894       5090
Swap:         8189          0       8189

Tässä virtuaalikoneen sisällä ajettu "free -m" komento antoi minulle tietoa sen käytettävissä olevasta muistista, megatavuissa.

7985 on tässä virtuaalikoneen kokonaismuistimäärä. 5090 taas kuinka paljon fyysistä RAM-muistia voi juuri nyt teoriassa varata, jos haluaa käynnistää sovelluksen. 8189 kertoo kuinka paljon swappia on yhteensä käytettävissä.

2894 kertoo kuinka monta megatavua palvelimen vapaasta muistista Linux on käyttänyt pitääkseen usein levyltä luettuja tiedostoja suoraan välimuistissa. Tämä välimuisti vapautetaan heti jos sovellus sitä pyytää.

Tämän virtuaalikoneen varattavissa olevan "virtuaalimuistin" (eli fyysinen muisti + swap) koko on siis 7985 MB + 8189 MB = 16174 MB.

$ sudo sysctl vm.swappiness
vm.swappiness = 60

Tämä komento antaa 0-100 luvun, joka kuvaa sitä millä innolla Linux swappaa varattua ei-cache muistia levylle.

https://github.com/torvalds/linux/blob/master/Documentation/sysctl/vm.txt#L683

$ sudo sysctl vm.overcommit_memory
vm.overcommit_memory = 0

$ sudo sysctl vm.overcommit_ratio
vm.overcommit_ratio = 50

Tämä komento näyttää joskus vähän ikävän Linuxin featuren, overcommitin, asetukset. Asetuksella 0 Linux antaa sovellusten varata enemmän muistia, kuin mitä käyttöjärjestelmällä on käytettävissä virtuaalimuistia.

https://github.com/torvalds/linux/blob/master/Documentation/sysctl/vm.txt#L577

Kun sovellukset ovat varanneet enemmän muistia kuin mitä käyttöjärjestelmällä on oikeasti antaa, ja sitten sovellukset yrittävät oikeasti käyttää tätä muistia jossain välissä, tällöin Linuxin oom-killer käy kylmästi tappamassa pois jonkun muistia paljon käyttävän prosessin.

Linuxissa on erittäin helppo lisätä pyörivälle virtuaalikoneelle dynaamisesti swappia. Se voidaan tehdä ilman käyttökatkoa, joko siten että VMWare-alustasta allokoidaan virtuaalikoneelle uusi virtuaalinen kovalevy, tai hätätilanteessa rootti voi luoda jo käytettävissä olevalle levylle tavallisen tiedoston, joka voidaan mountata tilapäisesti swappina.

Linuxissa voidaan "numactl" työkalulla kontrolloida aiemmin esitellyn Non-uniform Memory Access muistiarkkitehtuurin aiheuttamia haasteita sovelluksille. Tällöin voidaan kiinnittää tietty sovellus tietylle prosessorille, ja käskeä sitä varaamaan vain tuolla prosessorilla suoraan kiinni olevaa muistia. Toisaalta voidaan myös tehdä niin, että hyväksytään se hinta joka maksetaan muistibussin käytöstä, ja annetaan esimerkiksi tietokantapalvelimen käyttää kaikkea muistia samanarvoisesti, koska se on joka tapauksessa "halvempaa" kuin tiedon lukeminen levyltä. Kts. aiemmin linkattu "MySQL swap insanity" linkki.

Linux C-standardikirjastot tarjoavat POSIX-standardin määrittelemän "malloc" komennon, jolla sovellukset varaavat muistia. Perusimplementaatio on OK ja hyvin testattu miljoonilla palvelimilla. Mutta parempia muistinvaraajaimplementaatioita löytyy, kuten tcmalloc ja jmalloc. Esim. Facebook käyttää näitä hyvällä menestyksellä omien tietokantapalvelintensa kanssa.


JVM, garbage collection (GC), generations

Päällimmäinen taso sovelluspalvelinten muistinkäytössä on Java-virtuaalikone.

Ohjelmointimallin vuoksi kahdeksan merkin merkkijono vaatii 64 byteä muistia.

Tästä hyviä presentaatiota: http://www.youtube.com/watch?v=FLcXf9pO27w

http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-efficient-java-tutorial.pdf

Java-virtuaalikone pitää huolta muistin allokoinnista ja vapauttamisesta.

Sovelluskehittäjän ja virtuaalikoneen välinen "sopimus" siitä, kuinka tämä tapahtuu, on nimeltään Java Memory Model, joka on osa The Java Language Specification dokumenttia.

http://docs.oracle.com/javase/specs/jls/se8/html/index.html http://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4

Tämän dokumentin on hyvä olla jokaisen Java-kehittäjän selaimen linkkilistassa, ja ainakin JMM pitää lukea ja ymmärtää.

Aiheesta on myös speksiä havainnollisempia presentaatioita kuten http://www.youtube.com/watch?v=1FX4zco0ziY ja myös erittäin hyviä kirjoja.

"Palvelinrauta"-kohdassa mainitsin L3 cachen ja sen, että eri prosessorit voivat viitata omassa L3-välimuistissaan samaan keskusmuistin kohtaan.

Tämän haasteen kontrolloimiseksi JMM/JLS tarjoaa final ja volatile määreet, ja JDK tarjoaa java.util.concurrent paketin, jossa on erittäin helppokäyttöisiä luokkia jotka hoitavat homman puolestasi.

Jokaisen Java-kehittäjän tulee lukea java.util.concurrent paketin luokkien päätason JavaDocit.

Yleisin ja ilmiselvin tilanne, joka aiheuttaa sovelluskehittäjälle tai -ylläpitäjälle haasteita JVM:n muistinhallinnan kanssa on se, että sovellus kuolee ja lokissa lukee OutOfMemoryError.

Tämä tarkoittaa sitä, että JVM on pyytänyt käyttöjärjestelmältä niin paljon muistia kun sille on annettu -Xmx parametrilla lupa pyytää, se on käyttänyt niin paljon aikaa roskienkeruuseen kuin sillä on lupa käyttää, mutta siitä huolimatta JVM ei pystynyt löytämään allokoitavalle objektille riittävästi vapaata muistia.

Joskus sovelluksen lokeissa lukee OutOfMemoryError, mutta sovellus jatkaa silti pyörimistään lähes normaalitilassa. Tällöin sovellus on viipymättä uudelleenkäynnistettävä, sillä OOM on saattanut lentää sellaisessa kohdassa, joka on saattanut jättää JVM:n sisuskalut tilaan, jossa se ei voi täyttää spekseissä annettuja sitoumuksia, vaan se saattaa pahimmassa tapauksessa rikkoa sovelluksen datan näkymättömästi.

JVM:n "OutOfMemoryError" ja Linuxin "oom-killer" ovat aivan eri asioita, ja yleensä johtuvat aivan eri loppusyistä.

Joskus sovelluksen käyttämä datasetti vaan on alkanut vaatia enemmän tilaa kuin on käytettävissä. Tällöin yleensä kannattaa vaan antaa enemmän tilaa sovellukselle. 64-bittisen Java 6+ JVM tapauksessa kannattaa myös googlettaa ns. CompressedOops asetus, ja laittaa se päälle, se on aika riskitöntä.

Joskus sovelluksen kehityksessä on tehty virhe, ja sovellus pitää muistissa tilapäisiä tietoja ikuisesti, tai kauemmin kuin olisi tarvis. Tällaisia tilanteita kannattaa selvittää siihen erikoistuneilla työkaluilla kuten YourKit. Mittauksen 500€ lisenssihinta on halpa verrattuna pään raapimisen tuntihintaan.

JVM-prosessien muistinkäyttöä kannattaa jatkuvasti seurata yrityksen seurantajärjestelmällä. Jos yrityksessä ei ole käytössä seurantajärjestelmää, tai sen lisenssit ovat esimerkiksi kalliita, seurantaan on kehitetty ilmaisia valmistuotteita kuten http://rhq-project.github.io/rhq/.

Järjestelmäylläpitäjät ja –kehittäjät voivat seurata konsolista palvelimella pyörivän JVM:n muistinkäyttöä JDK:n mukana tulevalla "jstat" sovelluksella, jonka käytön suosittelen opettelemaan.

Johtuen Linuxin CAPABILITY-bugista, vanhemmilla kerneleillä/glibcillä joudut (lol) ajamaan komennon sudona, mutta ei suinkaan sudo roottina, vaan sudo oma käyttäjätunnus.

Yleisimmät yritysten käyttämät Java-virtuaalikone (JVM) toteutukset ovat Oraclen HotSpot (OpenJDK) ja IBM:n J9.

Nämä kummatkin toteuttavat muistinhallinnan ns. generational garbage collection menetelmällä.

HotSpot/OpenJDK sisältää useita vaihtoehtoisia GC implementaatioita, joista oletusvalinta on tarkoitettu soveltuvaksi useimpiin sovelluksiin, ja erilaisissa patologisissa tai erityistä suorituskykyä / latenssia vaativissa sovelluksissa kehittäjät ja järjestelmäylläpitäjät valitsevat oletuksesta poikkeavat asetukset.

Tästä lisätietoa Oraclen dokumentaatiossa:

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ ja

Understanding Java Garbage Collection and what you can do about it

The JVM and Java Garbage Collection

Uusimmissa JDK8 JVM:issä tulee mukana uusi G1 (Garbage First) kerääjä, jota käytettäessä JDK8:sta poistuu Permanent Generation, joka on ollut merkittävä OOM virheiden lähde vuosien varrella.

G1 Garbage Collector Performance Tuning

http://mail.openjdk.java.net/pipermail/hotspot-dev/2012-September/006679.html


Oma suositukseni JVM-sovellusten pyörittämiseen moderni palvelin + VMWare + Linux + HotSpot yhdistelmällä

Pidä huolta siitä, että VMWare-alustalla on ainakin 10% vapaata muistia jatkuvasti, äläkä tuplabookkaa törkeästi kriittisiä sovelluksia pyörittäviä alustoja, vaikka rahan kilinä kuulostaisi kuinka kauniilta korvissa.

Konfiguroi virtuaalikoneet niin, ettei VMWaren "vmmemctl" ajuri koskaan käske Linuxia swappaamaan muistia.

Anna Linuxille swappia n. 65% RAM:ista. Pidä silmällä sitä, että koneet swappaavat (lukevat ja kirjoittavat swappiin, ns. paging) vain vähän. Jos pitää joku numero sanoa, sata megaa päivässä on ihan OK.

Konfiguroi Linuxia, aseta overcommit pois päältä. Varaa SSH-ylläpitotoimenpiteille (kts. sama dokumentti) ainakin 256MB RAM:ia. Saman verran monitorointityökaluille. Palvelimen pääsovellus tai -sovellukset saisivat käyttää vain 80% palvelimen RAM:ista, jotta mm. thread stackeille jää tilaa.

Konfiguroi JVM-käynnistys niin, että sovelluksille asetetaan sekä -Xmx että -Xms samaksi arvoksi, eli "näin paljon tälle softalle on varattu muistia".

Pidä seurantatyökaluilla silmällä, että JVM:n sisäiset muistipoolit ovat riittävän kokoisia, ja että mahdolliset GC-paussit eivät haittaa sovelluksen käyttäjiä, ja GC:n käyttämä CPU ei haittaa sovelluksen toimintaa.

Pidä silmällä kuinka monta threadia sovellus pitää elossa, koska jokainen thread syö stackia pelkästä elämisen ilosta, ja jokaiselle threadille on varattu 8MB virtuaalimuistia, jonka todellista käyttöä ei voi seurata optimointisyistä.

(Thread stackin kokoa on mahdollista muuttaa, ja pitääkin etenkin jos disabloi Linux-overcommitin.)

Päivitä palvelinten JVM:t uusimpaan stabiiliin kuukausittain.