Virtuellen Speicher überbelegen

So etwas müßte ja offensichtlich nicht gehen, man kann ja nur das belegen, was man hat…

Ich schreibe hier mal wieder mit Blick auf die Linux-Speicherverwaltung, die ich am besten kenne, aber die Ideen stammen teilweise von früheren Unix-Systemen.

Ein System hat einen physikalischen Speicher (RAM) von einer bestimmten Größe, heute meistens ein paar Gigabyte, aber mein erstes Linux lief in den frühen 90er-Jahren auch mit 4 Megabyte irgendwie. Oft wird das noch ergänzt durch einen sogenannten Swap- oder Paging-Bereich. Das bedeutet, daß man mehr Speicher belegen kann, als da ist und der weniger genutzte Bereich wird auf die Festplatte ausgelagert. Im Falle von ausführbaren Programmen reicht es dafür oft, diese zu memory-mappen, das heißt, daß man die Stelle auf der Platte, wo das Binary steht, verwendet und von dem Programm jeweils nur die aktuell benötigten Teile in den Hauptspeicher lädt, statt das im Swap-Bereich zu duplizieren.

Schön ist es immer, wenn man diesen Swap-Bereich nicht braucht, denn dann läuft alles schneller und RAM ist ja so billig geworden. Gerade wenn man nur SSDs hat, ist das durchaus eine valable Überlegung, weil die SSDs nicht so viel billiger als RAM sind und weil die SSDs durch das besonders häufige Schreiben bei Verwendung als Swap relativ schnell aufgebraucht werden. Mit guten SSDs sollte das durchaus noch brauchbar funktionieren, weil deren Lebensdauer so gestiegen ist, aber dafür muß man schon genau schauen, daß man solche mit vielen Schreibzyklen und einer hohen Schreibgeschwindigkeit kauft und die sind dann wirklich nicht mehr so billig.

Letztlich funktoniert es aber mit magnetischen Festplatten oder mit SSD prinzipiell genauso, solange die Hardware mitmacht. Einen Teil des belegten Speichers im Swap zu haben, ist nicht unbedingt falsch, denn viele Programme belegen Speicherbereiche und benutzen die nur sehr selten. Und weil in der Zeit, wo Speicherbereiche in den Swap-Bereich ein- und ausgelagert werden, andere Threads noch weiterrechnen können, kann es durchaus auch sein, daß man mit einem Teil der häufiger gebrauchten Speicherbereiche im Swap noch eine passable Performance erreichen kann. Das ist vor allem für seltener gebrauchte oft Software vertretbar, wobei man aber den Anwendungsfall genau anschauen muß. Zum Beispiel muß eine aufwendige Jahresendverarbeitung gut geplant werden, weil sie zu einem bestimmten Zeitpunkt abgeschlossen sein soll und vielleicht besonders viele Ressourcen benötigt.

Den Swap-Bereich und den physikalischen Speicher zusammen nennt man virtuellen Speicher. Von diesem wird noch ein Teil durch das Betreibssystem belegt, so daß man den Rest zur Verfügung hat. Nun kann man unter Linux diesen virtuellen Speicher tatsächlich überbelegen. Das nennt sich „overcommit“. Dabei wird in den Kernel-Einstellungen ein Parameter entsprechend gesetzt.

Man kann abfragen, welche overcommit-Strategie eingeschaltet ist:

$ cat /proc/sys/vm/overcommit_memory
0

0 bedeutet, daß nur Speicheranforderungen, die nach einer Heuristik plausibel ist, erlaubt wird, 1 bedeutet, daß alle Speicheranforderungen grundsätzlich erlaubt werden.

$ cat /proc/sys/vm/overcommit_ratio
50

bedeutet, daß eine Überbelegung von 50%, also ein Faktor von bis zu 1.5 möglich ist.
Mit sysctl als root oder durch schreiben in diese Pseudo-Dateien kann man das einmalig ändern, mit Einträgen in /etc/sysctl.conf für die Zukunft ab dem nächsten reboot.

Ein paar Fragen, die sich aufdrängen sind:

  • Wie kann das überhaupt funktionieren?
  • Wofür braucht man das?
  • Was sind die Risiken?

Es gibt verschiedene Möglichkeiten, Speicher anzufordern, aber es reduziert sich im wesentlichen auf malloc und Konsorten, fork und exec.

malloc(), calloc(), sbrk(), realloc() und einige andere fordern einen Speicherbereich an, den ein Programm benutzen möchte. Solange da nichts reingeschrieben wurde, brauchen diese Bereiche aber nicht wirklich zur Verfügung gestellt werden. Erst wenn tatsächlich die Zugriffe darauf erfolgen, ist das nötig. Das soll von älteren Fortran-Versionen kommen. Dort hat man Matrizen und Vektoren statisch allozieren müssen, es mußte also im Programm hardcodiert sein, wie groß sie sind. Für kleinere Matrizen hat man einfach nur ein Rechteck in der linken oberen Ecke benutzt. So haben Fortran-Programme früher viel mehr Speicher angefordert, als sie wirklich brauchten und RAM war damals sehr teuer und auch der Plattenplatz für Swap war teuer. So konnte man mit entsprechender Einschätzung der Charakteristik der laufenden Programme mit overcommit das System tunen und mehr damit machen. Auch heute dürften noch viele Programme Speicher allozieren, den sie nicht brauchen, auch wenn das sich heute leichter umgehen ließe als mit Fortran66. Ein spezieller Fall sind Java-Programme, die einen recht großen, konfigurierbaren Speichereich anfordern und den dann weitgehend selber verwalten.

Der andere Fall ist das Starten von Prozessen. Ein neuer Prozess braucht Speicher. Nun wird unter Linux ein Prozess in zwei Schritten gestartet. Mit fork() wird ein laufender Prozess dupliziert. Wenn man also von einem großen Prozess aus ein fork() aufruft, braucht man potentiell sehr viel memory. Das wird aber nicht so heiß gegessen wie gekocht. Denn Linux kennt einen Mechanismus „copy on write“, das heißt, die Speicherbereiche werden von beiden Prozessen gemeinsam benutzt, bis einer der Prozesse dort etwas schreibt. Das darf der andere nicht sehen, weshalb der eine Speicherblock vorher dupliziert werden muß, so daß jeder Prozess seinen eigenen hat. fork() lädt also durchaus dazu ein, mit overcommit verwendet zu werden. Um nun einen anderen Prozess zu starten, wird exec() aufgerufen. Dann wird ein neues Programm im laufenden Prozess ausgeführt und dabei fast der gesamte Speicher des Prozesses neu initialisiert. Das kann Memory freigeben oder zusätzlich in Anspruch nehmen. Erhalten bleiben bei exec() zum Beispiel die Umgebungsvariablen und die offenen Dateien und Verbindungen.

Wenn nun aber das ganze angeforderte Memory den virtuellen Speicher überschreitet und auch tatsächlich verwendet wird? Dann tritt der out-of-memory-Killer auf den Plan und beendet einige Prozesse, bis der Speicher wieder ausreicht. Es ist also wichtig, daß der Systemadministrator seine Systeme kennt und beobachtet, wenn diese nicht krass überdimensioniert sind. So oder so führt es zu Problemen, wenn man den virtuellen Speicher vollständig ausschöpft, weil viele Programme bei scheiternden mallocs(), forks() u.s.w. auch nicht mehr korrekt funktionieren können. Damit meine ich nicht unbedingt, daß man sich jeden Tag auf allen Systemen einloggt, um zu schauen, wie es denen geht, sondern eher, daß man Skripte oder Werkzeuge verwendet, um die Daten zu erfassen und zu aggregieren.

Ich habe einmal für ein paar Monate etwa 1’000 Rechner administriert. Wenn man sich in jeden einloggen muß und dabei pro Rechner etwa 10 min verbringt, sind das 10’000 Minuten, also etwa 167 Stunden. Man wäre allein damit also einen Monat lang voll beschäftigt. Benötigt wurden weitgehende Automatisierungen, die es ermöglichten, die Aufgabe in etwa 20 Stunden pro Woche zu bewältigen. Ja, es waren eine sehr homogene Rechnerlandschaft, sonst wäre der Aufwand sicher größer gewesen.

Share Button

Collections und Multithreading

Am Beispiel von Java soll hier etwas geschrieben werden, was viele Programmiersprachen betrifft, auch wenn die funktionalen Sprachen eine gewisse Immunität gegen derartige Probleme versprechen.

Es geht um Klassen, die sogenannte Collections enthalten. Nun kann man diese mit den sogenannten Gettern herausgeben lassen und vielelicht sogar mittels Settern austauschen oder ändern.

Eine naïve Implemntierung sieht etwa so aus:

import java.util.List;
import java.util.ArrayList;

public class C {
private final List l;

public C() {
this.l = new ArrayList();
}
public C(List l) {
this.l = l;
}

public List getL() {
return l;
}
....
}

Das sieht schön aus, ist aber recht gefährlich. Im Zusammenhang mit Multithreading kann es bsonders schwierig werden.

Das Problem dabei ist, daß der Konstruktor eine Liste übergeben bekommt. Diese wird nicht kopiert, sondern nur referenziert. Nun weiß man nicht genau, was der Progammteil, der den Konstruktor aufgerufen hat, sonst noch mit der Liste macht. So kann sich diese verändern, ohne daß bei C selbst irgendwelche Manipulationen gemacht wurden. Mit den Gettern wird die Liste wiederum an weitere Programmteile verfügbar gemacht, die alle darin etwas ändern können. Vielleicht sogar in mehreren Threads gleichzeitig, was sehr schnell zu merkwürdigen Exceptions führt, wenn man nicht speziell dafür ausgelegte Collections verwendet.

Wie kann man das lösen?

Oft hilft es, im Konstruktor die Liste zu kopieren und beim getter als immutable herauszugeben:

import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

public class C {
private final List l;

public C() {
this.l = new ArrayList();
}
public C(List l) {
this.l = new ArrayList(l);
}

public List getL() {
return Collections.unmodifiableList(l);
}
....
}

Damit sind diese beiden Probleme abgestellt, zumindest in diesem Fall. Wenn es Listen von Listen oder andere kompliziertere Strukturen sind, dann treten die beschriebenen Probleme wegen der Unterlisten wieder auf. Hier sind die Elemente aber vom Typ String und damit selbst unveränderlich. Interessant ist nur die Frage, was passiert, wenn Methoden der Klasse C die darin gespeicherte Liste manipulieren. Die Programmteile, die vorher den getter aufgerufen haben und die dabei erhaltene Liste gespeichert haben, bekommen die Änderungen mit. Das kann erwünscht sein. Man kann es aber auch vermeiden, indem man beim getter noch einmal kopiert. Man kann sich das ewige kopieren auch sparen, indem man die Kopie „casht“ und wegwirft, sobald sich am Original etwas geändert hat.


import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

public class C {
private final List l;
private transient List ll = null;

public C() {
this.l = new ArrayList();
}
public C(List l) {
this.l = new ArrayList(l);
}

public synchronized List getL() {
if (ll == null) {
ll = Collections.unmodifiableList(new ArrayList(l));
}
return ll;
}

public synchronized void changeL() {
ll = null;
l.add("x" + System.currentTimeMillis());
}
}

Der Nachteil ist natürlich, daß dieses viele Kopieren Zeit kostet und dann auch noch den Garbage-Collector beschäftigt.

Wie sieht es nun in funktionalen Sprachen aus? Typischerweise sind dort solche Collections auch immutable, zumindest gibt es diese Variante. Jede Manipulation an der Collection läßt diese selbst unverändert und gibt eine Kopie zurück, die die Änderung enthält. Das ist natürlich auch auf den ersten Blick ineffizient, weil man zum Beispiel von einer Liste immer größere Kopien machen muß, um nur jeweils ein Element hinzuzufügen. Das wird aber nicht so heiß gegessen wie gekocht. Man kann eine Liste implementieren, die sich so verhält, als würde man jedes Mal eine Kopie anlegen. In Wirklichkeit kann man intern einige Optimierungen vornehmen, wenn zum Beispiel ein Zwischenzustand dieser Operationen nie weitervewendet wird. So zeigt es sich, daß funktionale Programmiersprachen trotz der scheinbar ineffizienten Kopiererei sehr effizient sein können.

Share Button

Steckdosen und Stromkabel der Zukunft

Heute haben wir in der Welt etwa 10-15 verschiedene gängige Steckdosentypen für Haushaltgebrauch. Dabei übertragen diese nur Energie. Für Datenübertragung muß man einen zweiten Kanal haben, sei es ein zweites Kabel oder Funk oder so eine „powerline“-Technik, die das Stromkabel mit Hochfrequenzen für die Datenübertragung nutzt. In den Zeiten, wo unsere Haushalte Stromkabel bekommen haben, schien das ausreichend und man hatte völlig getrennt vom Stromkabel die Telefonkabel zum Kommunizieren. Immerhin bekamen die Telefone von dort ihre Energie, so daß man auch bei Stromausfall noch telefonieren konnte.

Nun gibt es aber interessante Anwendungsmöglichkeiten für einen Kommunikationskanal in vielen Haushaltsgeräte. Nicht nur das Display mit der Wbseite für Rezepte auf dem Kühlschrank, an das viele dabei denken, sondern insbesondere auch zur Optimierung des Stromverbrauchs. Heutige Stromtarife haben einen festen Preis pro kWh, vielleicht einen zweiten für die Nacht. Das Stromnetz ist aber dynamisch, es gibt Nachfrage- und Angebotsschwankungen, die sich nicht an feste Zeitraster von Tages- und Nachtstrom halten. Im Sinne einer effizienteren Nutzung wäre es also sinnvoll, wenn man bei verschiedenen Geräten den momentanen Strompreis zur Verfügung hätte und die entsprechenden Aktionen zeitlich etwas versetzt durchführen könnte. Beispielsweise läuft der Kompressor in einem Kühlschränk nur einen Teil der Zeit und er schaltet sich ein, wenn die Temperatur zu hoch wird. Nun könnte man einen Toleranzspanne festlegen und je nach momentanem Strompreis würde er sich am unteren oder am oberen Ende dieses Temperaturintervalls einschalten. Bei Waschmaschinen und Ladegeräten für Mobiltelefone könnte man zwischen einem „Express-Modus“, der die Aufgabe so schnell wie möglich erfüllt, und einem Sparmodus, der vielleicht 1.5 Mal so lange braucht, aber billigeren Strom nutzt, wählen. Das ist natürlich anspruchsvoll, denn man will z.B. die Akku-Lebensdauer nicht durch so ein komisches Laden verkürzen. Aber vielleicht lösbar. Aber es gibt noch sehr viel mehr Möglichkeiten, wenn jedes Elektrogerät prinzipiell Netzwerk zur Verfügung hat.

Die richtige Lösung ist eigentlich, daß Stromkabel und Steckdosen nicht 3-polig, sondern vielleicht 6-polig sind, mit 2-4 zusätzlichen dünnen Drähten für eine einfache Datenübertragung, die man beim einstöpseln des Steckers automatisch mitverbindet. Die Idee stammt aus den frühen 80er Jahren, aber sie ist auch heute noch sinnvoll. Wenn die meisten Geräte so einen Netzwerkanschluß brauchen, ist das die sinnvollste Lösung.

  • separates Ethernetkabel ist zu umständlich und zu viel Kabelsalat
  • Funknetze sind zwar elegant, aber sie brauchen doch unnötig Ressourcen und man könnte dieselbe Bequemlichkeit haben, wenn der Stecker das Netzwerk enthält. Dann wäre die Kapazität der Funkkanäle für die wirklich mobilen Geräte da.
  • Powerline sieht interessant aus, aber man macht doch die Qualität der Sinuswelle im Stromnetz dadurch kaputt. Ich glaube, daß das nicht die sauberste Lösung ist, sondern mehr oder weniger Komprimisse an an verschiedenen Stellen bedingt.
  • Powerline und Funknetz erfordern aufwendigere Technologie und womöglich Konfiguration an allen Endgeräten, was die Sache teurer und unpraktischer macht.

Für Computer, insbesondere Server oder Desktoprechner mit großem Bandbreitenhunger halte ich auch in Zukunft ein separates kabelgebundenes Netzwerk für sinnvoll. Das betrifft aber nur einen kleinen Anteil der Elektrogeräte. Hier ist es aber vorteilhaft, Innovationen bei der Netzwerktechnologie innerhalb weniger Jahre umsetzen zu können, während das Netzwerk in der Steckdose standardisiert und für jahrzehnte festgelegt bleiben muß. Trotzdem könnte es für einfache Anwendungen wie z.B. EMails lesen, ausreichen.

Vielleicht gibt es irgendwann einmal einen ISO-Stecker, der nach und nach in allen Ländern das gute Dutzend verschiedener Stecker verdrängt und der auch gleich noch eine einfache Internetverbindung beinhaltet.

Share Button

Das Richtige entwickeln – richtig gefragt

Wenn eine neue Software entwickelt oder erweitert wird, ist es ja immer eine wichtige Frage, was eigentlich entwickelt werden soll. Die Entwickler wissen es selten selbst, und auch die Kunden oder die Besteller oder die Nutzer der Software muß man gelegentlich erst ein Stück weit begleiten, bis man herausbekommt, was sie wirklich wollen und benötigen. Requirements-Engineering oder Anforderungsanalyse ist eine anspruchsvolle und wichtige Aufgabe. Oft gibt es eine sogenannte „Fachseite“, die in Zusammenarbeit mit Businessanalysten diese Anforderungen definiert.

Im reinen Wasserfallmodell hat man diesen Schritt am Anfang sehr vollständig durchgezogen und dann später auf dieser Basis die Software entwickelt, zu Korrekturzwecken gab es immerhin Änderungsanträge. Bei agileren Prozessen entwickeln sich die Details der Anforderungen oft noch während der Entwicklung weiter und solange der Teil, der gerade entwickelt wird, klar genug spezifiziert werden kann, hat man so die Möglichkeit, aufgrund der Erkenntnisse aus früheren Lieferungen etwas für die später möglichen Anforderungen zu lernen. Man sagt, daß der Appetit mit dem Essen kommt.

Es lohnt sich aber in jedem Fall, noch einen Schritt weiter zu denken. Es kommt auch oft genug vor, daß die Anforderungen durchaus schon gut genug bekannt sind. Nun ist die Erfahrung aber die, daß die Software am Ende nur 80% von dem kann, was man sich eigentlich erhofft hat und so werden die Anforderungen sicherheitshalber etwas höher gestellt, in der Hoffnung, daß von der wirklich gewollten Funktionalität dann am Ende genug da ist. Außerdem kommt es oft vor, daß Anforderungen mit einer bestimmten Implementierung im Hinterkopf gestellt werden, die sich aber später auf Seite von Softwarearechitektur oder -entwicklung als suboptimal erweist.

Hier lohnt es sich, miteinander zu reden, um die wirklichen Anforderungen in Erfahrung zu bringen. Oft stellt sich dann heraus, daß eine wesentlich robustere Implementierung möglich ist, die den eigentlichen Anforderungen, nicht aber den kommunzierten Anforderungen, gleich gut oder sogar besser genügt.

Ein Klassiker sind Tabellen, zum Beispiel für ein Gebührenmodell oder Steuern. Natürlich kann man Tabellen in Software umsetzen, man kann auch eine Möglichkeit anbieten, sie zu ändern. Diese Tabellen können aber recht groß werden und auch wenn das unter Aspekten wie Speicherverbrauch, Verarbeitungsgeschwindigkeit und Durchsatz heute in der Regel keine Rolle spielt, müssen diese Tabellen doch aktuell gehalten werden, was den Prozess zum Betrieb der Software erschwert, vor allem, wenn die Änderung der Tabellen einen Software-Update erfordern, weil sie hardcodiert sind.

Bei genauerem Nachfragen stellt sich heraus, daß die Tabelle eigentlich aufgrund einer Formel gebildet wurde und diese approximiert. Wenn man nun die Formel in die Software einbaut und vielleicht ermöglicht, daß 2-3 Parameter dazu geändert werden kann, bekommt man eine Lösung die „glatter“ ist, weil es keine solchen Sprünge mehr zwischen 4999 und 5000 gibt, was man natürlich als Vorteil erst einmal verkaufen muß, man bekommt eine besser wartbare Software, weil sie viel weniger Code braucht und weniger kaputt gehen kann, aber vor allem bekommt man im Betrieb einen Vorteil, weil man nur noch 2-3 Parameter der Formel aktuell halten muß und nicht mehr eine riesige Tabelle. Daß sich der Softwaretest vereinfacht, ist ein weiterer Vorteil.

Ein konkreter Fall betraf vor vielen Jahren eine Software für die Verarbeitung von Fahrplänen eines Verkehrsbetriebs. Das Zielsystem war für reguläre Schweizer Fahrpläne entwickelt worden, und man konnte so Linien, Routen, das sind verschiedene Fahrtverläufe innerhalb der Linie, mindestens zwei für die beiden Fahrtrichtungen, und ein gutes Dutzend Fahrzeitmuster für Hauptverkehrszeit (langsam), Nacht (schnell) u.s.w. festlegen. Das paßt für mitteleuropäischen Stadtverkehr überall sehr gut. Eine kanadische Stadt weist aber eine viel niedrigere Bevölkerungsdichte auf und der Anteil der öffentlichen Verkehrsmttel an den zurückgelegten Wegen ist auch typischerweise kleiner als in durchschnittlichen mitteleuropäischen Städten. So hat man etwas, was zwischen einem Überlandverkehr und Stadtverkehr in Mitteleuropa liegt. Das Budget für den Verkehrsbetrieb ist begrenzt und man bemüht sich, trotz allem noch eine regelmäßige Bedienung der Stadtregionen zu bewerkstelligen. Dafür war eine Fahrplanerstellungssoftware vorteilhaft, die die Fahrzeiten individuell pro Fahrt wählbar macht. Die Sprachbarriere kam noch hinzu und so meinte der Schweizer Lieferant, die Kanadier sollten nur ihre Fahrplan ein bißchen aufräumen, dann würde es schon passen und die Kanadier meinten, sie wollten sich ihre gute Arbeit nicht wegen einer restriktiven Software verhindern lassen. Als das einige Zeit nach Projektbeginn festgestellt wurde, gab es erst einmal einen Scherbenhaufen.

Die Lösung war schließlich, noch einmal genau die Bedürfnisse des Kunden anzuschauen und konkret die kompliziertesten Linien unter die Lupe zu nehmen. Nun sollte die Software eine bestimmte Zuverlässigkeit bei der Verwendung der Daten erzielen. Es stellte sich am Ende heraus, daß das alles lösbar war. Hierzu mußte man die Fahrzeitmuster der einzelnen Fahrten ermitteln und jeweils die häufigsten Muster in die Zielplattformübernehmen. Bei den meisten Linien reichte das aus, aber für einzelne Fahrten mußte das passendste unter den übernommenen Fahrzeitmustern verwendet werden, was zu einem Fehler führte, der aber noch in die Toleranz für das Gesamtsystem fiel. Die richtige Lösung, das Zielsystem zu erweitern, um die Daten vollständig aufzunehmen, sollte man natürlich nicht aus dem Auge verlieren, was aber ein längerfristiges Projekt war. Mit so einer Kompromißlösung ließ sich dafür etwas Zeit gewinnen und letztlich die unter den gegebenen Umständen bestmögliche Lösung für alle beteiligten umsetzen.

Man sieht also, es lohnt sich auf jeden Fall, genauere Fragen zu stellen, um unter Berücksichtigung der Möglichkeiten, die man von der Softwarearchitektur im gegebenen Zeit- und Budgetrahmen hat, die wirklichen Bedürfnisse möglichst zielgerichtet zu erfüllen.

Share Button

Five Programming Languages you should learn

Larry Wall talking about his suggestion for five programming languages one should know:

Share Button