Diese Artikel widmet sich dem Thema Overcommitment ((Overcommitment bezeichnet das Zuweisen von Ressourcen an virtuelle Maschinen, die auf dem Hostsystem in der Menge nicht vorhanden sind)) mittels KVM und LibVirt. Anmerkung: Gilt nur für Linux-Gastsysteme. Und ist nicht für den kommerziellen Einsatz gedacht. Warum? Siehe: Die “Lösung”.

Inhalt

 

Vorwort

Overcommitment ist bei den meisten Webhostern eine gängige Praxis. Hier wird versucht mehr Ressourcen zu verkaufen, als eigentlich vorhanden sind, in der Annahme, dass eh nicht alle Kunden ihre Ressourcen voll ausschöpfen.

Das Ganze ist allerdings mit Vorsicht zu genießen, denn wie gesagt, die virtuellen Maschinen könnten unter Umständen mehr Ressourcen anfordern als überhaupt vorhanden sind.

Das Problem

KVM emuliert die komplette Hardware eines Gast-Systems. Das heißt, innerhalb der virtuellen Maschine können alle gängigen Serverbetriebsysteme (Linux, Windows, BSD, etc.) ausgeführt werden. Die Benutzer der virtuellen Maschine können sich einen eigenen Bootloader und einen eigenen Kernel installieren, wenn sie dies möchten.

Kurz gesagt, für mich als Hostsystembetreiber ist nicht einsehbar, was in der virtuellen Maschine abläuft. Dies ist auf der einen Seite gut, da so der Datenschutz gewährleistet ist, auf der anderen Seite allerdings auch Problematisch, wenn man auf Overcommitment setzt. Denn so kann man nicht einschätzen, wie viel RAM die Maschine gerade wirklich braucht, oder wie viel davon z.B. reiner Filesystemcache ist.  

Die “Lösung”

Um dieses Problem zu lösen hat sich Adam Litke an die Arbeit gemacht und einen Daemon geschrieben, welcher von der VM aus mit dem Host kommuniziert. Sprich mit der Hilfe dieses Daemons weiß der Host, wieviel RAM der Gast momentan wirklich braucht, und kann so den RAM soweit verkleinern, dass dieser den Filesystemcache leert. Nachdem der Filesystemcache geleert worden ist, bekommt der Gast seinen RAM wieder zurück. Fazit: Der Host hat wieder Hauptspeicher zur Verfügung und der Gast, der ihm den Speicher zurückgegeben hat, hat bis auf den Filesystemcache keine weiteren Nachteile.

Soweit so gut. Warum habe ich das Wort Lösung in Anführungszeichen gesetzt? Scheint doch prima zu funktionieren. Die Antwort: Litkes Ansatz setzt voraus, dass auf dem Gast ein Daemon läuft, welcher mit dem Host kommuniziert. Möchte man dies also in einem kommerziellen Umfeld verwenden ist man dadurch natürlich aufgeschmissen. Denn ein Kunde könnte diesen Daemon einfach killen und müsste so keinen Speicher mehr an den Host zurück geben. Diese Lösung ist also nur für diejenigen gedacht, die die VMs selbst nutzen, oder den anderen Vertrauen, dass diese den Daemon nicht beenden.

Da ich die Person kenne, mit der ich mir einen Server teile bin ich bereit dieses “Risiko” einzugehen. Für alle, die auch bereit sind dieses “Risiko” einzugehen, die können weiter lesen, an alle anderen: Tut mir leid eure Zeit verschwendet zu haben ;) Ihr könnt aber gerne in den Comments alternativen Posten.

Host

Wie bereits beschrieben verwerden ich ein Debian Wheezy als Host. Hier muss zunächst die mom 0.2.2 von GitHub herunter geladen werden. Warum nicht die neuste?(0.3.0 zum Zeitpunkt des Artikels) Weil diese nicht mit Debian Wheezy kompatibel ist. Solltet ihr ein Squeeze(zu diesem Zeitpunkt die aktuelle stable) haben könnte die 0.3.0 für euch auch funktionieren, dies wurde von mir allerdings nicht getestet. Um diese zu installieren werden folgende Pakete benötigt:
  • python-libvirt
  • python-setuptools
Nachdem mom-0.2.2 entpackt ist wechseln wir in den Ordner und führen
python setup.py install
aus. Bei dem aktuellen Paketstand von Debian Wheezy muss leider auch die Version 0.2.2 leicht angepasst werden(früher hat sie auch so funktioniert). Hierzu müssen in der /usr/local/lib/python2.7/dist-packages/mom/Collectors/GuestLibvirt.py diese beiden Zeilen auskommentiert werden:
            for (src, target) in self.mem_stats.items():
                ret[target] = info[src]
Ich muss zugeben ich weiß nicht, wozu diese Zeilen gut sind, da der MOM auch ohne diese zwei Zeilen so funktioniert wie vorher. (Wie gesagt, die MOM-Version hat bis zu einem Wheezy update (ich glaube im August wars, keine Ahnung) auch unangepasst funktioniert, von daher kann ich die Funktionsweise vor und nach der Anpassung gut vergleichen)

Wem das zuviel Arbeit ist, hier noch einmal die komplette Datei.

# Memory Overcommitment Manager
# Copyright (C) 2010 Adam Litke, IBM Corporation
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public
# License along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA

from mom.Collectors.Collector import *

class GuestLibvirt(Collector):
    """
    This Collector uses libvirt to return guest memory statistics
        libvirt_state - The domain state defined by libvirt as:
                VIR_DOMAIN_NOSTATE  = 0 : no state
                VIR_DOMAIN_RUNNING  = 1 : the domain is running
                VIR_DOMAIN_BLOCKED  = 2 : the domain is blocked on resource
                VIR_DOMAIN_PAUSED   = 3 : the domain is paused by user
                VIR_DOMAIN_SHUTDOWN = 4 : the domain is being shut down
                VIR_DOMAIN_SHUTOFF  = 5 : the domain is shut off
                VIR_DOMAIN_CRASHED  = 6 : the domain is crashed
        libvirt_maxmem - The maximum amount of memory the guest may use
        libvirt_curmem - The current memory limit (set by ballooning)
        
    The following additional statistics may be available depending on the
    libvirt version, qemu version, and guest operation system version:
        mem_available - Total amount of memory available (kB)
        mem_unused - Amount of free memory not including caches (kB)
        major_fault - Total number of major page faults
        minor_fault - Total number of minor page faults
        swap_in - Total amount of memory swapped in (kB)
        swap_out - Total amount of memory swapped out (kB)
    """
    mem_stats = { 'available': 'mem_available', 'unused': 'mem_unused',
                  'major_fault': 'major_fault', 'minor_fault': 'minor_fault',
                  'swap_in': 'swap_in', 'swap_out': 'swap_out' }
    libvirt_stats = [ 'libvirt_state', 'libvirt_maxmem', 'libvirt_curmem' ]
    
    def getFields(self=None):
        return set(GuestLibvirt.mem_stats.values() + GuestLibvirt.libvirt_stats)
        
    def __init__(self, properties):
        self.iface = properties['libvirt_iface']
        self.domain = self.iface.getDomainFromID(properties['id'])
        self.logger = logging.getLogger('mom.Collectors.GuestLibvirt')
        self.memstats_available = True

    def stats_error(self, msg):
        """
        Only print stats interface errors one time when we first discover a
        problem.  Otherwise the log will be overrun with noise.
        """
        if self.memstats_available:
            self.logger.debug(msg)
        self.memstats_available = False

    def collect(self):
        info = self.iface.domainGetInfo(self.domain)
        if info is None:
            raise CollectionError('Failed to get domain info')

        ret =  {
            'libvirt_state': info[0], 'libvirt_maxmem': info[1],
            'libvirt_curmem': info[2],
        }

        # Try to collect memory stats.  This function may not be available
        try:
            info = self.iface.domainGetMemoryStats(self.domain)
            if info is None or len(info.keys()) == 0:
                self.stats_error('libvirt memoryStats() is not ready')
                return ret
#            for (src, target) in self.mem_stats.items():
#                ret[target] = info[src]
            self.memstats_available = True
        except AttributeError:
            self.stats_error('Memory stats API not available for guest')

        return ret
        
def instance(properties):
    return GuestLibvirt(properties)

Kommen wir nun zur Konfiguration von MOM. Hierzu ist es empfehlenswert die Examples zu übernehmen und auf seine Bedürfnisse anzupassen.

cp /path/to/mom-0.2.2/doc/balloon.rules /etc/mom.rules
cp /path/to/mom-0.2.2/doc/mom-balloon.conf /etc/mom.conf

Zuerst passen wir /etc/mom.rules an. Hier lässt sich einstellen, wie sich der MOM verhalten soll. Zum Beispiel, wie viel Prozent an freiem Speicher als schlecht oder sogar kritisch anzusehen ist. Oder wie viel freier Speicher dem Gast mindestens gelassen werden soll und wie viel ihm maximal in einem Schritt genommen werden sollen.

Da ich auf dem Host nicht wirklich viel Speicher habe, fahre ich mit folgenden Werten ganz gut:

(defvar pressure_threshold 0.25)
(defvar pressure_critical 0.15)
(defvar min_guest_free_percent 0.20)
(defvar max_balloon_change_percent 0.5)
(defvar min_balloon_change_percent 0.05)
Sprich ich möchte, dass auf dem Host nach Möglichkeit 25% RAM frei bleibt und dass 15% freier Speicher schon echt uncool ist. Darüber hinaus soll beachtet werden, dass der Gast immer 20% freien Speicher haben soll, aber wenn es auf dem Host knapp wird, ihm auch mal eben 50% auf einen Schlag abgezogen werden dürfen.

Bisher hatte ich mit diesen Werten gute Erfahrungen gemacht. Wenn man natürlich einen Host mit 32 GB RAM hat sind 25% freier Speicher reine Verschwendung.

In der /etc/mom.conf muss lediglich die Zeile collectors: in der Sektion [guest] um den GuestNetworkDaemon ergänzt werden und in der letzten Zeile der Pfad zum name-to-ip-helper angepasst werden.

Der name-to-ip-helper ist lediglich ein Script, welches beim Aufruf mit einem Gastnamen (z.b. ./name-to-ip meinevm) die entsprechende IP zurückliefert. Dies kann man recht einfach mit einem switch/case bewerkstelligen:

#!/bin/bash
case “$1” in
        meinevm1)
                echo -n “192.168.199.123”
        ;;
meinevm2) echo -n “192.168.199.124” ;;
meinevm3) echo -n “192.168.199.125” ;; *) echo -n “fail” ;; esac

Als letztes müssen wir beim Host noch über die Sicherheit sprechen. Da der MOM-Collector ein Netzwerkdaemon ist, bei dem zusätzlich keine Authentifizierung notwendig ist sollte man dafür sorgen, dass dieser von außen nicht erreichbar ist.

Dies ist ganz einfach mit folgenden zwei iptables Regeln gemacht.

iptables -A FORWARD -p tcp –dport 2187 -j DROP
ip6tables -A FORWARD -p tcp –dport 2187 -j DROP

Diese sorgen dafür, dass Pakete, die an Port 2187(der Port des mom-guestdaemon) nicht weitergeleitet werden. Sollte kein IPv6 vorhanden sein, erübrigt sich das Eintragen der zweiten Zeile in die Firewall.

Der Gast

Verglichen mit dem Host ist der Gast leicht einzurichten. Hier müssen lediglich diese beiden Pakete installiert sein (ausgegangen wird hier von einen Debian Gast. Bei anderen Distributionen können die Paketnamen abweichen):
  • python-setuptools
  • python-libvirt
Sind die Pakete installiert laden wir uns wieder die Version 0.2.2 vom MOM bei GitHub runter. Anschließend wird in dem entpackten Verzeichnis wieder
python setup.py install
ausgeführt.

In dem Ordner befindet sich ebenfalls noch ein python script namens “mom-guestd” welches am besten nach /usr/local/sbin kopiert wird.

Dies ist der Guestdaemon, welcher später mit dem Host kommuniziert. Aus diesem Grund sollte er immer gestartet sein. Zu diesem Zweck lässt man ihn am besten über die /etc/rc.local direkt beim Booten im Hintergrund starten. Sprich wir fügen “/usr/local/sbin/mom-guestd &” vor “exit 0” in die /etc/rc.local ein.

Von nun an bekommt die VM soviel RAM, wie in LibVirt konfiguriert wurde und gibt dem Host kurzzeitig RAM zurück, wenn dieser knapp ist.

LibVirt

Nun kann die Konfiguration von LibVirt angepasst werden. Hierzu werden folgende zwei Punkte bearbeitet:

  <memory unit='KiB'>2048000</memory>
  <currentMemory unit='KiB'>1024000</currentMemory>

Das Feld memory beschreibt wie viel die VM maximal an RAM belegen darf. Das Feld currentMemory beschreibt den RAM, den die Maschine beim start hat.