2015. május 19., kedd

Konténerezett Hadoop és Cassandra cluster konfigurálása - első rész

A következő cikk-sorozatban egy Hadoop cluster telepítését mutatom be Cassandra támogatással, a méltán népszerű Docker konténerekbe zárva. Ebben a cikkben a Hadoop telepítése és konfigurálása kerül terítékre. Bár mindkét rendszer elindítható egyke módban, azért az elsődleges cél területük a nagy mennyiségű adat kezelés elosztott rendszereken, ha pedig a skálázhatóság is fontos szerepet játszik a kiszolgálásban, akkor a Docker kézenfekvő és divatos megoldás. A konténereket ésszerűen nem 0-ról építettem fel, hanem újrahasznosítottam a SequenceIQ Hadoop, és a Spotify Cassandra konténerét. Mindkét csapatról elmondható, hogy van sejtésük :) a témáról, így biztos alapnak éreztem az ő megoldásaikból kiindulni. A cikk készítése során fontos szempontnak tartottam, hogy bárki, aki megfelelő mennyiségű memóriával rendelkezik, a saját gépén próbálhassa ki a Hadoop és Cassandra házasságát, és ne kelljen valamilyen olcsó vagy drága felhő szolgáltatást előfizetnie. Választásom a Vagrantra esett, és segítségével virtualizálom a gépeket, a hálózatot, és egyéb erő forrásokat, és készítek privát hálózatot, amin keresztül a gépek elérik egymást. További előnye a döntésnek, hogy így a Hadoop memória kezelésébe is el kell mélyednünk, hiszen mindent minimalizálni kényszerültem, mivel a Hadoop alap konfiguráció óriási méretekre van optimalizálva, a konténerenknéti maximális 8192 Mb memória és 8 vcore legalábbis erről árulkodik.

A teljes projekt forrását elérhetővé tettem, így az első lépés a forrás megszerzése, és a Vagrant gépek elindítása. Az egyes gépek között 4.5 Gb memóriát osztottam szét (+oprendszer, +vagrant,+a böngésző amiben olvasod a cikket),  amennyiben nincs elegendő memória, kézzel kell a főbb gépeket elindítani. Türelmetlenek a cassandra.sh fájlban letilthatják a Cassandra konténerek építését, egy darabig úgysem lesz rájuk szükség.
git clone https://github.com/mhmxs/vagrant-host-hadoop-cassadra-cluster.git
cd vagrant-host-hadoop-cassadra-cluster
git submodule update --init
cd hadoop-docker && git checkout 2.6.0-static-ip
cd .. && git checkout 2.6.0-static-ip
vagrant up
Amíg a letöltés, telepítés, és egyéb gyártási folyamatok zajlanak kukkantsunk kicsit a motor háztető alá, lesz rá időnk bőven, ugyanis ezen a ponton nem sokat optimalizáltam, így sok tartalom többször is letöltésre kerül az egyes virtuális gépeken (javaslatokat várom a hozzászólásban). Íme a vagrant konfiguráció:
 config.vm.box = "ubuntu/vivid64"
 config.vm.provider "virtualbox" do |v|
  v.memory = 1024
 end
 config.vm.box_check_update = false
 config.vm.define "slave1" do |slave|
  slave.vm.network "private_network", ip: "192.168.50.1"
  slave.vm.provision :shell, inline: "hostname slave1 && sh /vagrant/cassandra.sh"
 end
 config.vm.define "slave2" do |slave|
  slave.vm.network "private_network", ip: "192.168.50.2"
  slave.vm.provision :shell, inline: "hostname slave2 && sh /vagrant/cassandra.sh"
 end
 config.vm.define "slave3" do |slave|
  slave.vm.network "private_network", ip: "192.168.50.3"
  slave.vm.provision :shell, inline: "hostname slave3 && sh /vagrant/cassandra.sh"
 end
 config.vm.define "master" do |master|
  master.vm.network "private_network", ip: "192.168.50.4"
  config.vm.provision :shell, inline: "hostname master && sh /vagrant/hadoop.sh"
  config.vm.provider :virtualbox do |vb|
   vb.customize ["modifyvm", :id, "--memory", "1536"]
  end
 end
Tehát van 4 gépünk egy hálózaton, egy mester és 3 szolga. A host név beállítás sajnos nem perzisztens, de a hibát már csak a következő verzióban javítottam, elnézést. A gépek száma tovább növelhető, az ököl szabály, hogy ami 3 gépen működik, az jó eséllyel működik többön is, ezért én a minimális darabszámot preferáltam. Elég sok leírást olvastam végig a Hadoop fürtök konfigurálásáról, és azonnal meg is jegyezném az egyik legnagyobb problémát az ökoszisztémával kapcsolatban. Olyan ütemben fejlődik a platform, hogy amit ma megírtam, az lehet, hogy holnap már nem is érvényes, vonatkozik ez az architektúrára (természetesen nem messziről szemlélve, az ördög a részletekben lakik), a beállításokra, és a fellépő hibákra is. Számtalan esetben az általam használt 2.6.0-ás verziójú Hadoop komponensben már nem is létezett az a kapcsoló, amivel javasolták az elcsípett kivétel kezelését, és kivétel akadt bőven.

Vegyük sorra milyen változtatásokat eszközöltem a beállításokban, de előtte tisztázzunk pár paramétert:
  • konténerek minimum memória foglalása: 4 Gb memória alatt 256 Mb az ajánlott
  • konténerek száma: min (2 * processzor magok, 1.8 * merevlemezek száma, Összes memória / konténer minimum memória foglalásával) - min(2, 1.8, 1024 / 256) = 2 egy kis csalással
  • rendszernek fenntartott memória: 4 Gb esetén 1 Gb, ennél kisebb értékre nincs ajánlás, így én az 512 Mb-ot választottam
  • összes felhasználható memória: rendelkezésre álló - fenntartott, 1024 - 512 = 512 Mb (gépenként 2 konténer)
További részletek és az ajánlott matek itt található.

Dockerfile
  • Telepítettem a Cassandrát
  • Mivel a Cassandra függősége az Open JDK 8, ezért eredeti Java konfigurációt eltávolítottam
  • Közös RSA kulcsot generáltam, hogy a konténerek között megoldható legyen a bejelentkezés jelszó nélkül
  • A Hadoop classpathjára betettem a Cassandrát
  • Pár további portot kitettem, exposoltam magyarul
  • Kicsit átrendeztem a parancsok sorrendjét, hogy minél gyorsabban lehessen konfigurációt változtatni, hála a rétegelt fájl-rendszernek a Docker csak a különbségig görgeti vissza a konténert, és onnan folytatja az építési műveletet tovább
core-site.xml
  • fs.defaultFS: hdfs://master:9000 - a master gépen fog futni a NameNode, és ezt jó, ha mindenkinek tudja
hdfs-site.xml
  • dfs.replication: 2 - a fájlrendszer replikációs faktorát emeltem meg 2-re
  • dfs.client.use.datanode.hostname: true - a dfs kliensek alap értelmezetten IP alapján kommunikálnak, ha több interfészünk, vagy több hálózati rétegünk van, akkor ez a működés okozhat fejfájást
  • dfs.datanode.use.datanode.hostname: true - szintén zenész
  • dfs.namenode.secondary.http-address: master:50090 - a másodlagos NameNode elérhetősége
  • dfs.namenode.http-address: master:50070 - az elsődleges NameNode elérhetősége
mapred-site.xml
  • mapreduce.jobtracker.address: master:8021 - szükségünk van egy JobTrackerre
  • mapreduce.map.memory.mb: 256 - map műveletet végző konténer memória foglalása
  • mapreduce.reduce.memory.mb: 512 - és a reducet végzőké
  • yarn.app.mapreduce.am.resource.mb: 512 - az AppMaster által felhasználható memória
  • yarn.app.mapreduce.am.job.client.port-range: 50200-50201 - alapértelmezetten un. ephemeral portot használ az AppMaster, az egyszerűség (tűzfal, port publikálás, stb.) kedvéért rögzítettem a tartományt
  • yarn.app.mapreduce.am.command-opts: -Xmx409m - AppMaster processz konfigurációja, ez okozott egy kis fejfájást, végül úgy találtam meg a problémát, hogy az eredeti forrásra egyesével rápakoltam a változtatásaimat, jó móka volt
  • mapreduce.jobhistory.address: master:10020 - elosztott környezetben szükség van JobHistory szerverre
  • mapreduce.jobhistory.webapp.address: master:19888 - igény esetén webes felületre is
  • mapreduce.application.classpath: ..., /usr/share/cassandra/*, /usr/share/cassandra/lib/* - az alapértelmezett útvonalakhoz hozzáadtam a Cassandrát
yarn-site.xml
  • yarn.resourcemanager.hostname: master
  • yarn.nodemanager.vmem-check-enabled: false - Centos 6 specifikus hiba jött elő, amit a virtuális memória ellenőrzésének kikapcsolásával tudtam megoldani. Bővebben a hibáról (6. Killing of Tasks Due to Virtual Memory Usage).
  • yarn.nodemanager.resource.cpu-vcores: 2 - a virtuális processzorok számát csökkentettem, így gépenként csak 2 osztható szét a konténerek között
  • yarn.nodemanager.resource.memory-mb: 512 - az a memória, amivel a helyi NodeManager rendelkezik
  • yarn.scheduler.minimum-allocation-mb: 256 - minimális memória foglalás konténerenként
  • yarn.scheduler.maximum-allocation-mb: 512 - és a maximális párja
  • yarn.nodemanager.address: ${yarn.nodemanager.hostname}:36123 - a konténer menedzser címe, alapértelmezetten a NodeManager választ portot magának, szintén specifikáltam
  • yarn.application.classpath: ..., /usr/share/cassandra/*, /usr/share/cassandra/lib/* - a Cassandrát elérhetővé tettem a Yarn számára
bootstrap.sh
echo $SLAVES | sed "s/,/\n/g" > /tmp/slaves
while read line; do printf "\n$line" >> /etc/hosts; done < /tmp/slaves
if [ -z "$(cat /etc/hosts | grep master)" ]; then
 printf "\n$MASTER_IP master" >> /etc/hosts
fi
if [ -n "$MASTER" ]; then
 echo "Starting master"
 
 rm /tmp/*.pid
 
 echo "master" > $HADOOP_PREFIX/etc/hadoop/masters
 echo "master" > $HADOOP_PREFIX/etc/hadoop/slaves
 while read line; do echo $line | sed -e "s/.*\s//" >> $HADOOP_PREFIX/etc/hadoop/slaves; done < /tmp/slaves
 
 $HADOOP_PREFIX/sbin/start-dfs.sh
 $HADOOP_PREFIX/bin/hdfs dfs -mkdir -p /user/root
 $HADOOP_PREFIX/sbin/start-yarn.sh
 $HADOOP_PREFIX/sbin/mr-jobhistory-daemon.sh  --config $HADOOP_PREFIX/etc/hadoop start historyserver
else
 echo "Starting slave"

 if [[ $1 == "-bash" ]]; then
  echo "To read logs type: tail -f $HADOOP_PREFIX/logs/*.log"
 fi
fi
Gondoskodtam a megfelelő DNS beállításokról, szétválasztottam a futás jellegét a MASTER környezeti változó alapján, amit a konténer futtatásakor tudunk megadni, valamint indítottam egy JobHistory szervert a masteren. Az echo "master" > $HADOOP_PREFIX/etc/hadoop/slaves sor gondoskodik arról, hogy a mester is végezzen szolgai munkát, igény szerint ez eltávolítható, és egyúttal némi memória is felszabadítható a mester gépen.

Miután befejeződött a Vagrant gépek előkészítése, és a konténerek is elkészültek, nincs más hátra, mint elindítani a konténereket, vagy mégsem? Sajnos nem, ugyanis egy kivételbe futunk a job futtatása során, de menjünk egy kicsit bele a részletekbe.
vagrant ssh slave[1-3]
docker run --name hadoop -h slave[1-3] -e "MASTER_IP=192.168.50.4" -e "SLAVES=192.168.50.1 slave1,192.168.50.2 slave2,192.168.50.3 slave3" -p 50020:50020 -p 50010:50010 -p 50075:50075 -p 8040:8040 -p 8042:8042 -p 49707:49707 -p 2122:2122 -p 36123:36123 -p 50200-50210:50200-50210 -it mhmxs/hadoop-docker:2.6.0 /etc/bootstrap.sh -bash
vagrant ssh master
docker run --name hadoop -h master -e "MASTER=true" -e "MASTER_IP=192.168.50.4" -e "SLAVES=192.168.50.1 slave1,192.168.50.2 slave2,192.168.50.3 slave3" -p 50020:50020 -p 50090:50090 -p 50070:50070 -p 50010:50010 -p 50075:50075 -p 9000:9000 -p 8021:8021 -p 8030:8030 -p 8031:8031 -p 8032:8032 -p 8033:8033 -p 8040:8040 -p 8042:8042 -p 49707:49707 -p 2122:2122 -p 8088:8088 -p 10020:10020 -p 19888:19888 -p 36123:36123 -p 50200-50210:50200-50210 -it mhmxs/hadoop-docker:2.6.0 /etc/bootstrap.sh -bash
Bizonyosodjunk meg róla, hogy mind a 4 konténer rendben elindult-e és, hogy a cluster összeállt-e megfelelően:
bin/hdfs dfsadmin -report
bin/yarn node -list
Tegyünk egy próbát:
bin/hdfs dfs -mkdir -p input && bin/hdfs dfs -put $HADOOP_PREFIX/etc/hadoop/* input
bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-2.6.0.jar wordcount input output
WARN [main] org.apache.hadoop.mapred.YarnChild: Exception running child : java.net.NoRouteToHostException: No Route to Host from master/172.17.0.155 to 172.17.0.159:40896 failed on
socket timeout exception: java.net.NoRouteToHostException: No route to host; For more details see: http://wiki.apache.org/hadoop/NoRouteToHost
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
at org.apache.hadoop.net.NetUtils.wrapWithMessage(NetUtils.java:791)
at org.apache.hadoop.net.NetUtils.wrapException(NetUtils.java:757)
at org.apache.hadoop.ipc.Client.call(Client.java:1472)
at org.apache.hadoop.ipc.Client.call(Client.java:1399)
at org.apache.hadoop.ipc.WritableRpcEngine$Invoker.invoke(WritableRpcEngine.java:244)
at com.sun.proxy.$Proxy8.getTask(Unknown Source)
at org.apache.hadoop.mapred.YarnChild.main(YarnChild.java:132)
Caused by: java.net.NoRouteToHostException: No route to host
at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)
at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:717)
at org.apache.hadoop.net.SocketIOWithTimeout.connect(SocketIOWithTimeout.java:206)
at org.apache.hadoop.net.NetUtils.connect(NetUtils.java:530)
at org.apache.hadoop.net.NetUtils.connect(NetUtils.java:494)
at org.apache.hadoop.ipc.Client$Connection.setupConnection(Client.java:607)
at org.apache.hadoop.ipc.Client$Connection.setupIOstreams(Client.java:705)
at org.apache.hadoop.ipc.Client$Connection.access$2800(Client.java:368)
at org.apache.hadoop.ipc.Client.getConnection(Client.java:1521)
at org.apache.hadoop.ipc.Client.call(Client.java:1438)
Miközben újra futtattam a jobot megpróbáltam elcsípni a folyamatot, ami az imént a 40896 porton futott, most pedig a 39552-őt használta volna. Elég rövid volt az idő ablak, de én is elég fürge voltam.
# lsof -i tcp:39552
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 2717 root 235u IPv4 147112 0t0 TCP namenode:60573->172.17.0.15:39552 (SYN_SENT)
java 2733 root 235u IPv4 147111 0t0 TCP namenode:60572->172.17.0.15:39552 (SYN_SENT)
-bash-4.1# ps x | grep 2717
2717 ? Sl 0:01 /usr/lib/jvm/jre-1.8.0-openjdk/bin/java -Djava.net.preferIPv4Stack=true -Dhadoop.metrics.log.level=WARN -Xmx200m -Djava.io.tmpdir=/tmp/hadoop-root/nm-local-dir/usercache/root/appcache/application_1431716284376_0003/container_1431716284376_0003_01_000012/tmp -Dlog4j.configuration=container-log4j.properties -Dyarn.app.container.log.dir=/usr/local/hadoop/logs/userlogs/application_1431716284376_0003/container_1431716284376_0003_01_000012 -Dyarn.app.container.log.filesize=0 -Dhadoop.root.logger=INFO,CLA org.apache.hadoop.mapred.YarnChild 172.17.0.15 39552 attempt_1431716284376_0003_m_000008_1 12
Elindul a szolgán egy YarnChild processz, ami az első paraméterben megadott IP-n szeretne kommunikálni a második paraméterben megadott ephemeral porton (a forráskódból derült ki), és a mester nem tud hozzá csatlakozni, hiszen nem a virtuális gép IP címe van megadva, amin a kommunikáció valójában zajlik, és nem is a szolga host neve, hanem a Docker konténer IP-je, amit a mester nem ér el. Kutakodtam a konfigurációk között, próbáltam a forrásból kideríteni, hogy mely beállítással lehetnék hatással erre a működésre, és szomorúan kellett tudomásul vennem, hogy erre nincs kapcsoló vagy nagyon elrejtették.

Miután a port publikálási megoldással zsákutcába jutottam az alábbi megoldások jöhettek számításba:
  • Használom a --net=host Docker kapcsolót, és akkor a konténerek közvetlenül a virtuális gép hálózati csatolójára ülnek - tiszta és száraz érzés
  • Privát hálózatot hozok létre pl. VPN - sajtreszelővel ...
  • Konfigurálok egy Weave hálózatot - na ez már izgalmasan hangzik
Az egyszerűség kedvéért elsőként kipróbáltam a --net=host kapcsolót, hogy lássam egyáltalán működik-e az összerakott rendszer.
vagrant ssh slave[1-3]
docker run --name hadoop --net=host -e "MASTER_IP=192.168.50.4" -e "SLAVES=192.168.50.1 slave1,192.168.50.2 slave2,192.168.50.3 slave3" -it mhmxs/hadoop-docker:2.6.0 /etc/bootstrap.sh -bash
vagrant ssh master
docker run --name hadoop --net=host -e "MASTER=true" -e "MASTER_IP=192.168.50.4" -e "SLAVES=192.168.50.1 slave1,192.168.50.2 slave2,192.168.50.3 slave3" -it mhmxs/hadoop-docker:2.6.0 /etc/bootstrap.sh -bash
Ezúttal tökéletesen lefutott a job, örülünk, de elégedettek még nem vagyunk. A jelenlegi állapot forrása itt érhető el, a Vagrantos környezeté pedig itt.

Előzetes a következő rész tartalmából. Hűsünk beállítja a Swarm clustert, majd némi küzdelem árán lecseréli a kézi DNS konfigurációt automatizált megoldásra. Vajon melyik implementációt választja? Kiderül a folytatásban...