So, nach längerer Abstinenz hier mal ein kleiner Rundumschlag. Ich habe zuhause einen kleinen Atom-Rechner im mini-ITX-Format, der leider kürzlich den Geist aufgegeben hat. Zeit für etwas Neues. Das System dient hauptsächlich als Fileserver, aber auch als Spielwiese. Damit man auch wirklich mal einige Dinge ausprobieren kann ohne das eigentliche System vollzusauen habe ich mich für ein Xen-Setup entschieden.

Wie man ein System mit Xen aufsetzt habe ich ja schon zu genüge hier im Blog breitgetreten. Hier soll es jetzt darum gehen, ein Cluster-Setup auf dem System einzurichten. Klar, mir nur einem Server ist das nicht so wahnsinning zielführend, aber es geht ja um eine Spielwiese.

Da der Artikel etwas umfangreicher ist, habe ich ihn in mehrere Abschnitte aufgeteilt:

Das neue System besteht aus:

  • 1 x Super Micro D-X7SPA-HF-D525-O Mainboard
  • 2 x 2 GB RAM
  • 4 x 3 TB SATA-HDD
  • einem billigen 19″-Rack-Gehäuse

Die Platten besitzen eine große GPT-Partition zudem habe ich sie zu einem RAID10 zusammengefasst und darüber einen LVM-Layer gelegt (mehr zu LVM steht im oben verlinkten Artikel zu Xen). Die Dom0 (also das physikalische System) hat 32 GB für das root-Dateisystem und 4 GB Swap. Außerdem gibt es noch auf jedem Laufwerk eine 16 MB umfassende “Reserved BIOS boot area”. (Mehr dazu gibt es unter: http://jasonschaefer.com/transition-away-from-mbr-to-gpt-booting/)

Die Dom0 verfügt über 512 MB RAM, da dort so gut wie keine Dienste laufen sollte das locker reichen. Als System wird, wie gewohnt, Debian Wheezy verwendet.

Da das Board über zwei Netzwerkkarten verfügt habe ich mich dazu entschlossen die beiden Netzwerkkarten zusammenzuschalten (Bonding). Hierzu ist zunächst das Paket ifenslave-2.6 zu installieren. Anschließend wird die Datei /etc/network/interfaces wie folgt angepasst:

auto lo
iface lo inet loopback

auto bond0
iface bond0 inet static
  address 192.168.42.42
  netmask 255.255.255.0
  network 192.168.42.0
  broadcast 192.168.42.255
  gateway 192.168.42.1
  slaves eth0 eth1
  bond-mode active-backup
  bond-miimon 100
  bond-downdelay 200
  bond-updelay 200

auto xenbr0
iface xenbr0 inet static
  bridge_ports bond0
  bridge_stp no
  address 192.168.42.42
  netmask 255.255.255.0
  network 192.168.42.0
  broadcast 192.168.42.255
  gateway 192.168.42.1

Durch das Bonding kann eine Netzwerkkarte ausfallen und die andere übernimmt in diesem Fall deren Funktion.

Damit es wirklich etwas neues für das Blog gibt habe ich mich entschieden das System redundant auszulegen (Cluster). Mir ist natürlich bewusst dass dies auf derselben Hardware nicht so wahnsinnig viel Sinn macht, aber es soll ja eine Spielwiese sein, da muss nicht immer alles Sinn ergeben. Ich habe also mit Xen zwei identische Systeme (DomUs, also virtuelle Xen-Maschinen) aufgesetzt.

Die beiden DomUs haben ebenfalls je eine 32 GB root-Partition und 2 GB Swap, außerdem jeweils 1 GB RAM. Zusammen mit den 512 MB RAM der Dom0 sind also 2,5 GB RAM bereits vergeben. Bleiben aber immer noch 1,5 GB für weitere Testmaschinen, in Anbetracht der doch eher geringen CPU-Leistung sollte das mehr als ausreichend sein.

Da ich nun zwei Systeme habe, die im Grunde identisch konfiguriert sein sollten/müssen habe ich mich dazu entschlossen eine System-Management-Software zu verwenden. Meine Wahl fiel auf SaltStack, andere Tools aus diesem Bereich sind Puppet und Chef. Eigentlich ist sowas natürlich totaler Overkill, aber Spielwiese und so. ;)

Saltstack erlaubt es auf dem Salt-Master (der Einfachheit halber habe ich diesen auf dem Dom0 installiert) sogenannte States zu definieren und zu bestimmen auf welche Maschinen diese States angewendet werden sollen. Die an den Salt-Master angebundenen Maschinen werden Minions genannt.

Die Installation des Salt-Masters ist sehr einfach, hierzu wird das Salt-Repository der Datei /etc/apt/sources.list hinzugefügt, der GPG-Key des Repositories importiert, der apt-Katalog aktualisiert und das Paket salt-master installiert:

echo “deb http://debian.saltstack.com/debian wheezy-saltstack main” >> /etc/apt/sources.list
wget -q -O- “http://debian.saltstack.com/debian-salt-team-joehealy.gpg.key” | apt-key add –
apt-get update
apt-get install salt-master

In der Konfigurationsdatei /etc/salt/master werden nun noch folgende Zeilen von ihrem Kommentarzeichen (#) befreit und der salt-master neu gestartet (`/etc/init.d/salt-master restart):

file_roots:
  base:
    – /srv/salt

file_ignore_regex:
  – ‘/\.git($|/)’

In /srv/salt/ werden später die sog. States abgelegt. Da ich die States mit git versionieren möchte wird der Salt-Master angewiesen alle Dateien in /srv/salt/.git/ zu ignorieren.

Auf den Minions (die beiden DomUs) wird jetzt ebenfalls das Repository und der Key hinterlegt. Hier wird dann allerdings das Paket salt-minion installiert. In /etc/salt/minion wird nun noch in der Zeile master: die IP bzw. der Hostname des Salt-Masters (die Dom0) eingetragen und der Salt-Minion neu gestartet:

echo “deb http://debian.saltstack.com/debian wheezy-saltstack main” >> /etc/apt/sources.list
wget -q -O- “http://debian.saltstack.com/debian-salt-team-joehealy.gpg.key” | apt-key add –
apt-get update
apt-get install salt-minion
sed -i.bak ‘s/#master: salt/master: 192.168.42.42/’ /etc/salt/minion
/etc/init.d/salt-minion restart

Nun führt man auf dem Salt-Master den Befehl salt-key -L aus. Dieser sollte nun die Hostnamen der beiden Minions unter Unaccepted Keys aufführen. Um die Key zu akzeptieren und so die Minions mit dem Master zu verbinden muss der Befehl salt-key -A ausgeführt werden.

Mit dem Befehl salt ‘*’ test.ping kann nun die Kommunikation des Masters mit seinen Minions überprüft werden:

root@doctormoon:/srv/salt# salt ‘*’ test.ping
thelibrary-node2:
True
thelibrary-node1:
True

Nun können wir uns an unseren ersten State wagen. Hierzu wird zunächst die Datei /srv/salt/top.sls erstellt, diese steuert welche States an welche Minions verteilt werden:

base:
‘*’:
– standard_packages

Die Datei besagt dass alle Minions (’*’) mit dem State standard_packages versorgt werden sollen. Dieser State wird über die Datei /srv/salt/standard_packages.sls definiert:

base:
  pkg.installed:
    – pkgs:
      – htop
      – ifto1p
      – iotop
      – nload
      – etckeeper
      – unp
      – mc
      – pciutils
      – lsof
      – debian-goodies
      – molly-guard
      – logwatch
      – apticron

build-essential:
  pkg.installed

python-pip:
  pkg.installed

python-dev:
  pkg.installed

glances:
  pip.installed:
    – name: glances
    – require:
      – pkg: build-essential
      – pkg: python-pip
      – pkg: python-dev

Der State sorgt dafür dass die unter pkgs aufgelisteten Debian-Pakete auf den Minions installiert werden. Außerdem werden noch die Pakete build-essential, python-pip und python-dev installiert. Über die Python-Repository-Verwaltung pip wird anschließend glances installiert. Allerdings dann des require-Abschnitts nur dann, wenn die Pakete build-essential, python-pip und python-dev auf dem Minion vorhanden sind bzw. deren Installation erfolgreich verlaufen ist.

Mit dem Befehl salt ‘*’ state.highstate kann der State nun angewendet werden. Nach einiger Zeit sollte der Befehl eine Auflistung der Pakete ausgeben die auf den Minions installiert wurden. Führt man den Befehl erneut aus wird geprüft ob der im State definierte Zustand immer noch korrekt ist, falls nicht wird er wieder hergestellt. Ist alles so wie es sein soll sieht der Output so aus:

root@doctormoon:/srv/salt# salt ‘*’ state.highstate
thelibrary-node2:
———-
State: – pkg
Name: build-essential
Function: installed
Result: True
Comment: Package build-essential is already installed
Changes:
———-
State: – pkg
Name: python-pip
Function: installed
Result: True
Comment: Package python-pip is already installed
Changes:
———-
State: – pkg
Name: python-dev
Function: installed
Result: True
Comment: Package python-dev is already installed
Changes:
———-
State: – pip
Name: glances
Function: installed
Result: True
Comment: Package already installed
Changes:
———-
State: – pkg
Name: base
Function: installed
Result: True
Comment: All specified packages are already installed.
Changes:
thelibrary-node1:
———-
State: – pkg
Name: build-essential
Function: installed
Result: True
Comment: Package build-essential is already installed
Changes:
———-
State: – pkg
Name: python-pip
Function: installed
Result: True
Comment: Package python-pip is already installed
Changes:
———-
State: – pkg
Name: python-dev
Function: installed
Result: True
Comment: Package python-dev is already installed
Changes:
———-
State: – pip
Name: glances
Function: installed
Result: True
Comment: Package already installed
Changes:
———-
State: – pkg
Name: base
Function: installed
Result: True
Comment: All specified packages are already installed.
Changes:

Salt Stack kann noch bedeutend mehr, damit könnte man wahrscheinlich problemlos ein weiteres Blog befüllen. Zum Glück ist aber die Doku recht gut gelungen (ich empfehle die PDF oder ePub-Variante): http://docs.saltstack.com/index.html

Ich werde aber natürlich die Einrichtung meines kleinen Systems hier im Blog noch weiter beschreiben und dabei auch noch einige Beispiele für die Verwendung von salt anbringen. Im nächsten (oder eher übernächsten) Artikel wird es dann um den Zusammenbau des Clusters auf Basis von corosync und pacemaker gehen.

Bevor wir mit dem Cluster anfangen brauchen wir natürlich erst einmal einige Dienste welche der Cluster dann verwalten kann. Den Anfang macht nginx und natürlich verwenden wir salt für die Einrichtung.

Zunächst legen wir einen neuen Order unterhalb von /srv/salt/ an.

mkdir /srv/salt/nginx

In diesen Ordner kommt nun die Datei init.sls:

nginx:
  pkg.installed

Damit dieser State auch Anwendung findet wird er nun noch in /srv/salt/top.sls referenziert:

base:
  ‘*’:
    – standard_packages
    – nginx

Der State kann nun wie gewohnt mit salt ‘*’ state.highstate angewendet werden. Da ich jedoch weiß dass sich in standard_packages nichts geändert hat kann man auf den kompletten Statedurchlauf verzichten und nur den Nginx-State zur Anwendung bringen, dies geht mit: salt ‘*’ state.sls nginx

Der Output (gekürzt):

root@doctormoon:/srv/salt/nginx# salt ‘*’ state.sls nginx
thelibrary-node1:
———-
State: – pkg
Name: nginx
Function: installed
Result: True
Comment: The following packages were installed/updated: nginx.

thelibrary-node2:
———-
State: – pkg
Name: nginx
Function: installed
Result: True
Comment: The following packages were installed/updated: nginx.

Natürlich ist der State in der derzeitigen Form noch etwas dünn. Schließlich will nginx ja auch konfiguriert werden. Hierzu kopieren wir die Datei /etc/nginx/sites-available/default von einem der Minions auf den Salt-Master:

scp /etc/nginx/sites-available/default 192.168.42.42:/srv/salt/nginx/default

Auf dem Master passen wir die nginx-Konfiguration nun unseren Wünschen entsprechend an (vim /srv/salt/nginx/default):

server {
  listen *:80;
  server_name foobar.example.com;

  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;

  location / {
    root /var/www;
  }
}

Damit die default-Konfiguration an die Minions verteilt wird erweitern unsere /srv/salt/nginx/init.sls:

/etc/nginx/sites-available/default:
  file:
    – managed
    – backup: minion
    – source: salt://nginx/default
    – user: www-data
    – group: www-data
    – mode: 664
    – require:
      – pkg: nginx

Zeile 1 gibt das Ziel für die Datei auf dem Minion an. Zeile 4 sorgt für ein Backup der jeweils überschriebenen Version der Datei (siehe: http://docs.saltstack.com/ref/states/backup_mode.html). Zeile 9 und 10 sorgen dafür dass die Datei nur geschrieben wird wenn nginx erfolgreich auf dem Minion installiert wurde, bzw. bereits vorhanden war. Die restlichen Zeilen sollten selbsterklärend sein.

Nun ist es natürlich nur mit dem Kopieren der Konfiguration auf den Minion nicht getan, die Konfiguration muss ja auch noch getestet und angewendet werden, also erweitern wir unsere init.sls erneut:

nginx_configtest:
  module.wait:
    – name: nginx.configtest
    – watch:
      – file: /etc/nginx/sites-available/default
      – pkg: nginx

nginx_signal:
  cmd.wait:
    – name: ‘/etc/init.d/nginx reload’
    – watch:
      – pkg: nginx
      – file: /etc/nginx/sites-available/default
      – module.run: nginx_configtest

Der Abschnitt nginx_configtest verwendet das Salt-Modul nginx um die Konfiguration zu testen.

Eine Anleitung zur generellen Verwendung von Modulen findet sich unter: http://docs.saltstack.com/ref/states/all/salt.states.module.html

Die Befehle des nginx-Modules findet man hier:
http://docs.saltstack.com/ref/modules/all/salt.modules.nginx.html

Eine Auflistung aller verfügbaren Module gibt es unter:
http://docs.saltstack.com/ref/modules/all/index.html

Der Configtest wird natürlich nur dann ausgeführt wenn die Datei /etc/nginx/sites-available/default auf den Minion geschrieben wurde (z.B. weil die Fassung dort abweichte).

Ist der Configtest erfolgreich verlaufen kümmert sich dann noch der Abschnitt nginx_signal darum nginx neu zu starten bzw. die Konfiguration neu einzulesen. Hier hätte man auch das nginx-Modul verwenden können (salt.modules.nginx.signal) aber ich wollte einfach einmal die cmd-Funktion aufzeigen. cmd.wait benötigt immer eine watch-Anweisung. Will man einen Befehl immer ausführen kann cmd.run verwendet werden.

Da der State nun etwas umfangreicher ist sollten wir ihn nicht direkt auf die Minion loslassen, sondern zunächst einen Testlauf durchführen, dies erfolgt mit dem Befehl:

salt ‘*’ state.sls nginx -v test=True

Zeigt der Output keine Auffälligkeiten können wir die Änderungen endgültig anwenden.

Noch ein Hinweis: Das hier gemachte Beispiel würde derzeit noch nicht dafür sorgen dass der nginx-Server überhaupt gestartet würde (es wird ja ein reload und kein restart durchgeführt). Salt kann auch dies erledigen:

service:
  – running
    – enable: True
    – restart: True
    – watch:
      – pkg: nginx
      – module.run: nginx_configtest

Da bei mir später der Cluster den Start von nginx übernimmt (und überwacht) habe ich in meiner init.sls auf diesen Abschnitt verzichtet.

Der geplante Cluster wird im sog. Active/Passive-Verfahren arbeiten. Hierzu wird eine sogenannte Service-IP eingeführt. Über diese Service-IP spricht der jeweils aktive Clusternode mit der Außenwelt. Fällt ein von Cluster überwachter Dienst aus wird die Service-IP an den bisher passiven Node übergeben. Außerdem werden ggf. alle benötigten Dienste auf dem Node gestartet.

Auf beiden Nodes läuft ja bereits ein Webserver (nginx), es soll außerdem noch ein MySQL-Server hinzukommen. Natürlich sollen die Daten im Docroot des Webservers als auch die Daten in der MySQL-Datenbank natürlich auf beiden Clusternode stets identisch sein. Beim Webserver könnte man natürlich per rsync oder ähnlichen Lösungen (z.B. auch per Salt Stack) für eine Synchronisation sorgen. Bei der MySQL-Datenbank ist das schon schwieriger.

Die Lösung lautet hier: DRBD (Distributed Replicated Block Device)

DRBD ist quasi ein RAID 1 über eine Netzwerkverbindung. Daten die auf dem aktiven Node auf das DRBD-Laufwerk geschrieben werden, werden im Hintergrund automatisch auf das DRBD-Laufwerk im passiven Node weitergereicht. Fällt der aktive Node aus verfügt der bisher passive Node bereits über eine exakte Kopie aller Daten.

Um ein DRBD-Laufwerk zu erzeugen benötigen wir zunächst zwei neue LVM-Laufwerke die wir dann in unsere Xen-DomUs einbinden, diese erstellen wir auf der Dom0:

lvcreate -n thelibrary-node1-www -L 4G vg0
lvcreate -n thelibrary-node2-www -L 4G vg0

Diese binden wir nun in die DomUs ein, die zuvor heruntergefahren werden müssen:

xen shutdown -a
sed -i.bak “/swap,xvda1,w’,/a\’phy:\/dev\/vg0\/thelibrary-node1-www,xvda3,w’,” /etc/xen/thelibrary-node1.cfg
sed -i.bak “/swap,xvda1,w’,/a\’phy:\/dev\/vg0\/thelibrary-node2-www,xvda3,w’,” /etc/xen/thelibrary-node2.cfg
xen create /etc/xen/thelibrary-node1.cfg
xen create /etc/xen/thelibrary-node2.cfg

Die beiden sed-Befehle fügen die Zeile ’phy:/dev/vg0/thelibrary-node1-www,xvda3,w’, unterhalb des bereits bestehenden Eintrags ’phy:/dev/vg0/thelibrary-node1-swap,xvda1,w’, in die Xen-Konfiguration der beiden DomUs ein.

Nun können wir DRBD auf den DomUs einrichten, hierzu nehmen wir natürlich Salt Stack zur Hilfe und erzeugen einen neuen Ordner: /srv/salt/drbd/. In diesem Erzeugen wir zunächst die Datei drbd.conf:

### globale Angaben ###
global {
  # an Statistikauswertung auf usage.drbd.org teilnehmen?
  usage-count no;
}

### Optionen, die an alle Ressourcen vererbt werden ###
common {
    syncer {
    rate 33M;
  }
}

### Ressourcenspezifische Optionen
resource drbd0 {
  # Protokoll-Version
  protocol C;

  startup {
    # Timeout (in Sekunden) für Verbindungsherstellung beim Start
    wfc-timeout 60;
    # Timeout (in Sekunden) für Verbindungsherstellung beim Start
    # nach vorheriger Feststellung von Dateninkonsistenz
    # (“degraded mode”)
    degr-wfc-timeout 120;
  }
  disk {
    # Aktion bei EA-Fehlern: Laufwerk aushängen
    on-io-error pass_on;
  }
  net {
    ### Verschiedene Netzwerkoptionen, die normalerweise nicht gebraucht werden, ###
    ### die HA-Verbindung sollte generell möglichst performant sein… ###
    # timeout 60;
    # connect-int 10;
    # ping-int 10;
    # max-buffers 2048;
    # max-epoch-size 2048;
    after-sb-0pri discard-zero-changes;
    after-sb-1pri discard-secondary;
    after-sb-2pri disconnect;
  }
  syncer {
    # Geschwindigkeit der HA-Verbindung
    rate 33M;
  }
  handlers {
    split-brain “/usr/lib/drbd/notify.sh root”;
    fence-peer “/usr/lib/drbd/crm-fence-peer.sh”;
    after-resync-target “/usr/lib/drbd/crm-unfence-peer.sh”;
  }

  on thelibrary-node1 {
    ### Optionen für Master-Server ###
    # Name des bereitgestellten Blockdevices
    device /dev/drbd0;
    # dem DRBD zugrunde liegendes Laufwerk
    disk /dev/xvda3;
    # Adresse und Port, über welche die Synchr. läuft
    address 192.168.42.43:7788;
    # Speicherort der Metadaten, hier im Laufwerk selbst
    meta-disk internal;
  }
  on thelibrary-node2 {
    ### Optionen für Slave-Server ###
    # Name des bereitgestellten Blockdevices
    device /dev/drbd0;
    # dem DRBD zugrunde liegendes Laufwerk
    disk /dev/xvda3;
    # Adresse und Port, über welche die Synchr. läuft
    address 192.168.42.44:7788;
    # Speicherort der Metadaten, hier im Laufwerk selbst
    meta-disk internal;
  }
}

Ein DRBD-Laufwerk kann eine heikle Angelegenheit sein, wenn beide Nodes gleichzeitig Daten auf das Laufwerk schreiben (z.B. weil sie den Kontakt zueinander verloren haben und sich plötzlich beide für den aktiven Node halten), entsteht natürlich eine Inkonsistenz die im Zweifel nicht mehr ohne teilweisen Datenverlust zu beheben ist. Wer DRBD ernsthaft Einsetzen möchte sollte auf jeden Fall einen Blick in die Dokumentation werfen: http://www.drbd.org/users-guide-emb/ch-configure.html

Wer etwas in der Doku nicht versteht kann gerne in den Kommentaren nachfragen, ich werde ansonsten aber hier keine Erklärung zu der oben aufgeführten Konfiguration geben.

Nun aber zum Salt State, den wir über die Datei /srv/salt/drbd/init.sls definieren:

drbd8-utils:
  pkg.installed

/etc/drbd.conf:
  file:
    – managed
    – source: salt://drbd/drbd.conf
    – backup: minion
    – user: root
    – group: root
    – mode: 664
    – require:
     – pkg: drbd8-utils

drbd_signal:
  cmd.wait:
    – name: ‘/etc/init.d/drbd reload’
    – watch:
      – file: /etc/drbd.conf

Was der State genau tut sollte ja mittlerweile jeder wissen. Im Groben beschrieben wird das Paket drbd8-utils installiert, die drbd.conf nach /etc/drbd.conf kopiert und eingelesen.

Das DRBD-Laufwerk erzeugen wir per Hand, da dies natürlich nur einmal notwendig ist. Den State können wir in Zukunft weiter nutzen, wenn Anpassungen an der DRBD-Konfiguration notwendig werden. Hierzu sollte der Befehl salt ‘*’ state.sls drbd -b2 genutzt werden, damit der State möglichst zeitgleich auf beiden Minions ausgeführt wird.

Wir können aber dennoch auf Salt Stack zurückgreifen um die DRBD-Laufwerke zu erzeugen indem wir folgenden Befehle nutzen:

salt ‘*’ cmd.run ‘modprobe drbd’
salt ‘*’ state.sls drbd -b2
salt ‘*’ cmd.run ‘drbdadm create-md drbd0’
salt ‘*’ cmd.run ‘drbdadm up drbd0’

Anschließend sollte das Laufwerk auf beiden Minions zur Verfügung stehen, was wir wie folgt prüfen:

salt ‘*’ cmd.run ‘cat /proc/drbd’

Der Output sollte so aussehen:

thelibrary-node1:
version: 8.3.11 (api:88/proto:86-96)
srcversion: F937DCB2E5D83C6CCE4A6C9
0: cs:Connected ro:Secondary/Secondary ds:Inconsistent/Inconsistent C r—–
ns:0 nr:0 dw:0 dr:0 al:0 bm:0 lo:0 pe:0 ua:0 ap:0 ep:1 wo:f oos:4194140
thelibrary-node2:
version: 8.3.11 (api:88/proto:86-96)
srcversion: F937DCB2E5D83C6CCE4A6C9
0: cs:Connected ro:Secondary/Secondary ds:Inconsistent/Inconsistent C r—–
ns:0 nr:0 dw:0 dr:0 al:0 bm:0 lo:0 pe:0 ua:0 ap:0 ep:1 wo:f oos:4194140

Nun müssen wir eines der Laufwerke in den sog. primary-Mode bringen, da nur in diesem Mode auf das Laufwerk geschrieben werden kann:

salt ‘thelibrary-node1’ cmd.run ‘drbdadm — -o primary drbd0’

Durch das ’thelibrary-node1’ anstelle des üblichen ’*’ wird der Befehl nur auf diesem Minion ausgeführt. /proc/drbd sollte nun wie folgt aussehen:

version: 8.3.11 (api:88/proto:86-96)
srcversion: F937DCB2E5D83C6CCE4A6C9
0: cs:SyncSource ro:Primary/Secondary ds:UpToDate/Inconsistent C r—–
ns:2553284 nr:0 dw:0 dr:2554648 al:0 bm:155 lo:0 pe:15 ua:6 ap:0 ep:1 wo:f oos:1642716
[===========>……..] sync’ed: 60.9% (1642716/4194140)K
finish: 0:00:45 speed: 36,052 (34,948) K/sec

Nun erzeugen wir noch ein Dateisystem, mounten das Laufwerk und erzeugen Unterverzeichnisse für MySQL und das nginx-Docroot:

salt ‘thelibrary-node1’ cmd.run ‘mkfs.ext4 -j -m 0 /dev/drbd0’
salt ‘thelibrary-node1’ cmd.run ‘mkdir /var/drbd/’
salt ‘*’ cmd.run ‘mkdir -p /var/drbd/mysql’
salt ‘*’ cmd.run ‘mkdir -p /var/drbd/www/html’
salt ‘*’ cmd.run ‘mkdir -p /var/drbd/log’
salt ‘thelibrary-node1’ cmd.run ‘mount /dev/drbd0 /var/drbd’
salt ‘thelibrary-node1’ cmd.run ‘mkdir -p /var/drbd/mysql’
salt ‘thelibrary-node1’ cmd.run ‘mkdir -p /var/drbd/www/html’
salt ‘thelibrary-node1’ cmd.run ‘mkdir -p /var/drbd/log’

Die Verzeichnisse legen wir auf beiden Minions an, damit der configtest von nginx nicht aufgrund fehlender Verzeichnisse fehlschlägt.

Jetzt biegen wir die nginx-Konfiguration auf die neuen Verzeichnisse um:

/srv/salt/nginx/default:

server {
  listen *:80;
  server_name foobar.example.com;

  access_log /var/drbd/log/nginx_access.log;
  error_log /var/drbd/log/nginx_error.log;

  location / {
  root /var/drbd/www/html/;
  }
}

Die MySQL-Installation führen wir zunächst ohne State-File durch, da wir einige Modifikationen durchführen müssen, die nur einmalig nötig sind:

salt ‘*’ pkg.install ‘mysql-server-5.5’

Die MySQL-Datenbank wurde nun natürlich zunächst in /var/lib/mysql/ abgelegt. Wir verschieben sie nun nach /var/drbd/mysql/:

salt ‘*’ cmd.run ‘/etc/init.d/mysql stop’
salt ‘thelibrary-node1’ cmd.run ‘mv /var/lib/mysql/ /var/drbd/’
salt ‘thelibrary-node2’ cmd.run ‘rm -rf /var/lib/mysql/’

Nun kopieren wir die Dateien /etc/mysql/my.cnf und /etc/mysql/debian.cnf von thelibrary-node1 in unseren Salt Stack-State-Tree:

scp /etc/mysql/my.cnf doctormoon:/srv/salt/mysql/my.cnf
scp /etc/mysql/debian.cnf doctormoon:/srv/salt/mysql/debian.cnf

Dort passen wir folgende Zeilen an:

sed -i.bak ‘s/\/var\/lib\/mysql/\/var\/drbd\/mysql/g’ /srv/salt/mysql/my.cnf

Jetzt können wir unseren State /srv/salt/mysql/init.sls definieren:

mysql-server-5.5:
pkg.installed

/etc/mysql/my.cnf:
  file:
    – managed
    – backup: minion
    – source: salt://mysql/my.cnf
    – user: root
    – group: root
    – mode: 644
    – require:
      – pkg: mysql-server-5.5

/etc/mysql/debian.cnf:
  file:
    – managed
    – backup: minion
    – source: salt://mysql/debian.cnf
    – user: root
    – group: root
    – mode: 600
    – require:
      – pkg: mysql-server-5.5

Jetzt zur Anwendung:

salt ‘*’ state.sls mysql

Wichtig hier ist dass die debian.cnf auf dem zweiten Node ersetzt wird, da wird ja nun nur noch die Datenbank von Node1 verwenden würde sonst das Kennwort für den debian-maintenance-User auf Node2 nicht passen.

Nun können wir MySQL auf Node1 starten:

salt ‘*node1’ cmd.run ‘/etc/init.d/mysql start’

Im letzten Artikel haben wir unser DRBD-Laufwerk gebastelt, nun wird es dann auch Zeit dafür zu Sorgen dass unsere Dienste im Fehlerfall zwischen den Nodes hin- und hergeschoben werden.

Vorweg aber noch ein wichtiger Hinweis: Ein Clustersystem bietet viele Stolperfallen und man kommt sehr leicht in Situationen in denen man die Verfügbarkeit durch den Cluster eher reduziert statt erhöht (z.B. wenn der Server unter hoher Last steht und die Timeouts für die Überwachung zu niedrig eingestellt sind). Dieser Artikel kann daher allenfalls ein Einstiegspunkt in das Thema sein. Wer einen Cluster im professionellen Umfeld betreiben möchte muss sich deutlich intensiver mit der Thematik befassen. Auch ist hier nochmal der Hinweis angebracht dass das Setup so wie es hier beschrieben ist natürlich nur bedingt Sinnvoll ist, da sich beide Nodes auf der physikalisch gleichen Plattform befinden, in der Praxis macht man so etwas natürlich nicht, sondern nutzt mind. zwei physikalische Maschinen (die dann natürlich auch weitere Xen-DomUs betreiben können, hier gäbe es dann auch die Möglichkeit nicht einzelne Dienste sondern die kompletten virtuellen Maschinen hin- und her zu migrieren).

OK. Wir haben also zwei Xen-Nodes. Das stellt uns vor das Problem dass, wenn die Kommunikation zwischen den beiden Nodes gestört ist, eine dritte Instanz fehlt, die eventuell noch feststellen kann welcher der beiden Nodes korrekt funktioniert. Um diesem Makel zumindest etwas entgegenzuwirken habe ich mich dazu entschlossen die Dom0 ebenfalls in den Cluster aufzunehmen, allerdings ohne die überwachten Dienste dort einzurichten. Damit im Fehlerfall der Cluster nicht versucht Dienste auf die Dom0 zu migrieren, wird die Dom0 einfach im Cluster in den Standby-Mode geschoben. So kann sie noch darüber Abstimmen welcher Node im Cluster am Besten funktioniert, hat aber darüberhinaus keine weiteren Aufgaben. Auch dies ist natürlich ein Vorgehen was man in einem professionellen Umfeld tunlichst lassen sollte.

Damit die Dom0 Teil des Clusters werden kann müssen wir leider das Bonding der Netzwerkkarten aus dem ersten Artikel wieder über Bord werfen. Stattdessen werden beide Netzwerkkarten unabhängig voneinander eingerichtet und eine versorgt den Cluster, die andere die Xen-Bridge.

Wir beginnen unsere Konfiguration indem wir einen neuen Ordner cluster in unserem Salt Stack State-Tree anlegen und dort folgende init.sls hinterlegen:

corosync:
  pkg.installed

pacemaker:
  pkg.installed

/etc/default/corosync:
  file:
    – managed
    – source: salt://cluster/default
    – user: root
    – group: root
    – mode: 644
    – require:
      – pkg: corosync

/etc/corosync/corosync.conf:
  file:
    – managed
    – source: salt://cluster/corosync.conf
    – user: root
    – group: root
    – mode: 644
    – template: jinja
    – context:
      bind: {{ salt‘grains.get’ }}
    – require:
      – pkg: corosync

corosync_signal:
  service:
    – name: corosync
    – running
    – enable: True
    – reload: True
    – watch:
      – pkg: corosync
      – file: /etc/default/corosync
      – file: /etc/corosync/corosync.conf

Der größte Teil der Konfiguration sollte anhand der Vorgängerartikel klar sein, der Abschnitt context ist jedoch neu. Salt verwaltet bestimmte Statusinformationen über die Minions in sog. grains. Eine komplette Übersicht über die verfügbaren Grains erhält man mit dem Befehl salt ‘*’ grains.items. Im Grain ip_interfaces sind Informationen zu den verfügbaren Netzwerkkarten des jeweiligen Minions hinterlegt:

salt ‘*’ grains.item ip_interfaces
thelibrary-node2:
ip_interfaces: {‘lo’: [‘127.0.0.1’], ‘eth0’: [‘192.168.42.44’]}
thelibrary-node1:
ip_interfaces: {‘lo’: [‘127.0.0.1’], ‘eth0’: [‘192.168.42.43’, ‘192.168.42.23’]}

Die Zeile bind: {{ salt‘grains.get’ }} liest also die IP-Adresse von eth0 auf dem Minion aus und speichert den Wert unter bind ab. In Verbindung mit der Anweisung -template: jinja können wir ihn in der corosync.conf verwenden. Die folgende corosync.conf legen wir unter /srv/salt/cluster/corosync.conf ab:

totem {
version: 2

# How long before declaring a token lost (ms)
token: 3000

# How many token retransmits before forming a new configuration
token_retransmits_before_loss_const: 10

# How long to wait for join messages in the membership protocol (ms)
join: 60

# How long to wait for consensus to be achieved before starting a new round of membership configuration (ms)
consensus: 3600

# Turn off the virtual synchrony filter
vsftype: none

# Number of messages that may be sent by one processor on receipt of the token
max_messages: 20

# Limit generated nodeids to 31-bits (positive signed integers)
clear_node_high_bit: yes

# Disable encryption
secauth: off

# How many threads to use for encryption/decryption
threads: 0

# Optionally assign a fixed node id (integer)
# nodeid: 1234

# This specifies the mode of redundant ring, which may be none, active, or passive.
rrp_mode: none

interface {
    # The following values need to be set based on your environment 
    ringnumber: 0
    bindnetaddr: {{ bind[0] }}
    mcastaddr: 226.94.1.10
    mcastport: 5405
}
}

amf {
  mode: disabled
}

service {
# Load the Pacemaker Cluster Resource Manager
  ver: 0
  name: pacemaker
}

aisexec {
  user: root
  group: root
}

logging {
  fileline: off
  to_stderr: yes
  to_logfile: no
  to_syslog: yes
  syslog_facility: daemon
  debug: off
  timestamp: on
  logger_subsys {
    subsys: AMF
    debug: off
    tags: enter|leave|trace1|trace2|trace3|trace4|trace6
  }
}

In Zeile 42 setzen wir nun den in der init.sls ausgelesen IP-Adressen-Wert. Nachdem wir den State mit salt ‘‘ state.sls cluster angewendet haben (wir könnten ihn natürlich auch unserem Top-File (/srv/salt/top.sls) hinzufügen und salt ‘‘ state.highstate verwenden) sollten die Nodes bereits miteinander sprechen. Auf der Dom0 führen wir die Konfigurationsschritte manuell aus, es handelt sich hier ja um unseren Salt-Master und nicht um einen Minion.

Ist dies erfolgt sollte der Befehl crm status in etwa folgendes zurückliefern:

Last updated: Sat Sep 7 17:14:05 2013
Last change: Sat Sep 7 11:44:10 2013 via crm_resource on thelibrary-node2
Stack: openais
Current DC: thelibrary-node2 – partition with quorum
Version: 1.1.7-ee0730e13d124c3d58f00016c3376a1de5323cff
3 Nodes configured, 3 expected votes

0 Resources configured.
Online: [ thelibrary-node1 thelibrary-node2 doctormoon ]

Auf der Dom0 führen wir nun noch den Befehl crm node standby aus. Anschließend sollte der Output von crm status so aussehen:

Last updated: Sat Sep 7 17:15:42 2013
Last change: Sat Sep 7 11:44:10 2013 via crm_resource on thelibrary-node2
Stack: openais
Current DC: thelibrary-node2 – partition with quorum
Version: 1.1.7-ee0730e13d124c3d58f00016c3376a1de5323cff
3 Nodes configured, 3 expected votes

0 Resources configured.
Node doctormoon: standby
Online: [ thelibrary-node1 thelibrary-node2 ]

Soweit so gut, aber in diesem Zustand hilft uns der Cluster noch nicht weiter. Wir müssen jetzt die Dienste definieren die der Cluster überwachen soll. Dies erfolgt über den Befehl crm configure. Im ersten Schritt sollten wir festlegen was im Worst-Case mit dem Cluster passieren soll. Der Worst-Case ist hier der komplette Verlust der Kommunikation der Nodes untereinander (Verlust des Quorums). In einem solchen Fall könnte es passieren dass sich beide Nodes für aktiv halten und anfangen Daten auf ihr DRBD-Laufwerk zu schreiben. Ist die Kommunikation wieder hergestellt gibt es ein böses erwachen, die sich die Datenstände nun natürlich unterscheiden. Da meine Daten mir lieb sind hat deren Integrität natürlich Vorrang vor der Verfügbarkeit eines Dienstes. Daher schicken wir nun den Befehl crm configure no-quorum-policy=”stop” ab. Dieser sorgt dafür dass im oben beschrieben Worst-Case-Fall alle Clusterdienste (auf allen Nodes) sofort gestoppt werden (mit ein Grund warum Cluster-Nodes immer eine redundante Netzwerkverbindung an getrennten Switches haben sollten).

Weiter geht es mit crm configure stonith-enabled=”false”. Ebenfalls eine Sache die auf professionellen Systemen so nicht sein sollte. Wann immer ein Node in einen undefinierbaren Zustand gerät und nicht mehr automatisch in den korrekten Status gebracht werden kann ist es das Sicherste ihn temporär aus dem Cluster zu entfernen. Dies geschieht mit einem sog. STONITH-Device (Shoot the offening node in the head). Ich habe kein STONITH-Device, also keine Option, unser (dünnes) Sicherheitsnetz besteht in Form der Zeile no-quorum-policy=”stop”, nicht sonderlich super aber besser als nichts.

Zu guter Letzt setzen wir noch crm configure property default-resource-stickiness=”100″. Dies sorgt dafür dass ein Dienst der vom aktiven auf den passiven Node verschoben wurde, weil er dort nicht mehr korrekt funktioniert, nicht wieder zum aktiven Node zurückkehrt wenn der Fehler dort behoben wurde. Dies minimiert die Migrationen der Dienste was im Zweifel fehleranfällig ist (never change a running system). Dies sollte natürlich nur gesetzt werden wenn beide Nodes in etwa dieselbe Hardwareausstattung aufweisen.

Der Befehl crm configure show sollte nun in etwa folgendes liefern:

node doctormoon \
attributes standby=”on”
node thelibrary-node1
node thelibrary-node2
property $id=”cib-bootstrap-options” \
dc-version=”1.1.7-ee0730e13d124c3d58f00016c3376a1de5323cff” \
cluster-infrastructure=”openais” \
expected-quorum-votes=”3″ \
stonith-enabled=”false” \
no-quorum-policy=”stop” \
default-resource-stickiness=”100″ \
last-lrm-refresh=”1378547037″

Damit sind die Grundeinstellungen vorgenommen und wir können uns den Diensten zuwerden. Ich poste hier einfach zunächst einmal die komplette Konfiguration:

primitive fs_drbd ocf:heartbeat:Filesystem \
params device=”/dev/drbd0″ directory=”/var/drbd” fstype=”ext4″ \
op start interval=”0″ timeout=”60″ \
op stop interval=”0″ timeout=”60″
primitive p_drbd ocf:linbit:drbd \
params drbd_resource=”drbd0″ \
op start interval=”0″ timeout=”240″ \
op stop interval=”0″ timeout=”100″
primitive p_mysql lsb:mysql \
op monitor interval=”10″ timeout=”60″ \
op start interval=”0″ timeout=”30″ \
op stop interval=”0″ timeout=”30″ \
meta target-role=”Started”
primitive p_nginx lsb:nginx \
op monitor interval=”10″ timeout=”60″ \
op start interval=”0″ timeout=”30″ \
op stop interval=”0″ timeout=”20″ \
meta target-role=”Started”
primitive p_sip ocf:heartbeat:IPaddr2 \
params ip=”192.168.42.23″ nic=”eth0″ \
op monitor interval=”10″ timeout=”20″ \
meta target-role=”Started”
primitive p_pingtest ocf:pacemaker:ping \
params multiplier=”1000″ host_list=”8.8.8.8 193.99.144.80 89.18.172.97″ \
op monitor interval=”5″
ms ms_drbd p_drbd \
meta master-max=”1″ master-node-max=”1″ clone-max=”2″ clone-node-max=”1″ notify=”true”
clone cl_pingtest p_pingtest
group g_all p_sip p_nginx p_mysql
order o_afterdrbd inf: ms_drbd:promote g_all:start
colocation c_drbd inf: g_all ms_drbd:Master
location l_all_ping g_all \
rule $id=”l_all_ping-rule” -inf: not_defined pingd or pingd lt 1000

Wir sehen es gibt verschiedene Konfigurationstypen (Resourcen):

  • primitive
  • group
  • ms
  • colocation
  • order
  • location
  • clone

Die primitive-Resource fs_drbd definiert über einen sog. Resource-Agent (RA) einen zu überwachenden Dienst. Im Falle von fs_drbd lautet der RA ocf:heartbeat:Filesystem. Er sorgt dafür das unser DRBD-Laufwerk gemountet wird, benötigt hierzu aber natürlich einige Parameter (params), die aber wohl selbsterklärend sind. op start timeout=60 besagt dass der Mountvorgang bis zu 60 Sekunden dauern darf. Wird dieser Zeitrahmen überschritten wird die Ressource auf einem anderen Node gestartet. Der stop-Vorgang (hier also ein umount) darf ebenfalls max. 60 Sekunden dauern bevor der Vorgang als Fehlgeschlagen gewertet wird.

Die Resource p_drbd sorgt für die Überwachung unseres DRBD-Laufwerks auf Blockdeviceebene. Hier darf der Start bis zu 240 und der Stop bis zu 100 Sekunden dauern.

Die Resource p_mysql nutzt das normale init-Script (/etc/init.d/mysql) zur Überwachung. Hierbei wird einfach zum Start des Dienstes vom Cluster /etc/init.d/mysql start und zum Stoppen /etc/init.d/mysql stop ausgeführt. Um den Status zu überwachen wird /etc/init.d/mysql status aufgerufen. Durch die Angabe op monitor interval=”10″ wird festgelegt das der Status alle 10 Sekunden geprüft wird. Das Init-Script hat dann 60 Sekunden Zeit eine Rückmeldung zu liefern (timeout=60). Bleibt diese aus gilt der Dienst als Fehlerhaft und wird auf einem anderen Node gestartet. meta target-role=”Started” besagt lediglich dass der Dienst beim Start des Cluster mitgestartet werden soll. Damit dies klappt sollte MySQL nicht schon bereits beim Systemstart selbst starten, hierzu wird update-rc.d mysql remove ausgeführt, oder als salt-Befehl: salt ‘thelibrary*’ cmd.run ‘update-rc.d mysql remove’. Dasselbe gilt für nginx.

Die Resource p_sip vergibt die sog. Service-IP über die dann der aktive Cluster-Node erreichbar ist, in unserem Fall wird die IP 192.168.42.23 dem Interface eth0 zugewiesen. Hierbei spielt es keine Rolle dass eth0 bereits eine andere IP-Addresse zugewiesen ist, es funktionieren dann beide. Diese sollten sich natürlich im gleichen Subnetz befinden und dasselbe Gateway nutzen.

Die Resource p_pingtest pingt alle 5 Sekunden die unter host_list angegebenen IP-Addressen. Für jeden erfolgreichen Ping bekommt der Node 1000 “Punkte”. Hierzu später mehr.

Die bereits erwähnt darf das DRBD-Laufwerk nur auf einem Node des Status primary annehmen, in diesem Zustand kann es gemountet werden. Die Resource ms_drbd stellt dies sicher. ms steht hierbei für Master/Slave, dieser Resourcentyp kann für zahlreiche Dienste die einen Master/Slave-Zustand kennen/benötigen verwendet werden. ms_drbd ist hierbei mit p_drbd verbunden und ersetzt diesen. Wenn man den Dienst also per Hand starten will muss man crm resource start ms_drbd anstelle von crm resource start p_drbd aufrufen.

Die Resource cl_pingtest sorgt dafür dass die Resource p_pingtest stets auf alle Nodes ausgeführt wird. Alle anderen Resourcen sind jeweils nur auf einem Node aktiv (alles andere würde in unserem Setup keinen Sinn ergeben, da sowohl MySQL als auch Nginx auf DRBD-Daten zurückgreift die auf dem passiven Node nicht verfügbar sind).

Da es ebenfalls unsinnig (und wie im Absatz zuvor gezeigt, in unserem Setup, unmöglich) wäre auf dem einen Node mysql und auf dem anderen nginx laufen zu lassen werden die beiden Resourcen zusammen mit der Service-IP (p_sip) in der Gruppe g_all zusammengefasst.

Die Order o_afterdrbd legt fest dass zunächst das DRBD-Laufwerk “promoted” (also in den Primary-Mode) gesetzt werden muss, bevor die Resourcen der Gruppe g_all gestartet werden können.

Die Colocation c_drbd sorgt dann noch dafür dass die Resourcen der Gruppe p_all immer auf dem Node laufen der das DRBD-Laufwerk im Primary-Mode fährt.

Die Location l_all_ping sorgt nun noch dafür dass die Resourcen nur auf einem Node laufen dürfen der über den p_pingtest mind. 1000 Punkte erreicht hat. So können die Resourcen auf den Node mit der besten Internetanbindung geschoben werden. In der Praxis nutze ich diese Möglichkeit nicht, da der Pingtest so seine Tücken hat. Ich wollte ihn der Vollständigkeit halber hier aber erwähnen (insbesondere die Clone- und die Location-Anweisung).

Die oben aufgeführten Konfigurationseinstellungen können über crm configure einzeln oder über crm configure edit gesammelt der Konfiguration hinzugefügt werden. Wenn man crm configure nutzt muss Abschließend der Befehl commit in der crm-Shell ausgeführt werden. Mit bye oder quit kann man die crm-Shell dann verlassen.

Der Befehl crm status sollte nun etwa folgendes zurückliefern:

Last updated: Sat Sep 7 17:14:05 2013
Last change: Sat Sep 7 11:44:10 2013 via crm_resource on thelibrary-node2
Stack: openais
Current DC: thelibrary-node2 – partition with quorum
Version: 1.1.7-ee0730e13d124c3d58f00016c3376a1de5323cff
3 Nodes configured, 3 expected votes

6 Resources configured.
Node doctormoon: standby
Online: [ thelibrary-node1 thelibrary-node2 ]

fs_drbd (ocf::heartbeat:Filesystem): Started thelibrary-node2
Master/Slave Set: ms_drbd [p_drbd]
Masters: [ thelibrary-node2 ]
Slaves: [ thelibrary-node1 ]
Resource Group: g_all
p_sip (ocf::heartbeat:IPaddr2): Started thelibrary-node2
p_nginx (lsb:nginx): Started thelibrary-node2
p_mysql (lsb:mysql): Started thelibrary-node2

Failed actions:
p_nginx_monitor_0 (node=doctormoon, call=14, rc=5, status=complete): not installed
p_drbd:0_monitor_0 (node=doctormoon, call=11, rc=5, status=complete): not installed
p_mysql_monitor_0 (node=doctormoon, call=12, rc=5, status=complete): not installed

Die Failed actions gehen in Ordnung da es sich hierbei um die Dom0 (im Standby) handelt, die ja nur des Quorums wegen mitspielt und keine der Resourcen jemals verwenden soll.

Zur Verwaltung der Clusterkonfiguration über salt habe ich bereits eine Idee, dazu aber später mehr. Da die Konfiguration aber sowieso automatisch an alle Nodes verteilt wird ist dies (zumindest in unserem Mini-Setup) eh kein Problem.


Kommentare

Kommentare unterstützt von Disqus.

Nächster Beitrag Vorheriger Beitrag