Durchschnittliche AWS-Spotpreise ermitteln

In der Amazon-Cloud gibt es ein nettes Feature, wonach Amazon brach liegende CPU/RAM-Ressourcen als Spot-Instanzen „versteigert“. Auf dieser Basis lassen sich Cloud-Server oft deutlich unter dem regulären Preis betreiben. Es droht dann zwar die Gefahr, dass eine Instanz von Amazon jederzeit terminiert werden kann, dass passiert aber bei einem vernünftig angesetzten Preis-Limit nur selten.

Die Preise für die Spotinstanzen variieren nicht nur je nach Instanz-Typ (dieser bestimmt RAM und CPU) sondern auch nach Standort des Rechenzentrums (Amazon bietet aktuell 8 Standorte um den Globus verteilt, im Amazon-Jargon „Region“ genannt) und nach der „Verfügbarkeitszone“ (Amazon betreibt an jedem dieser Standort 2-4 autonome Verfügbarkeitszonen).

Daraus ergeben sich über alle Regionen betrachtet aktuell insgesamt 21 Zonen, in welchen man auf eine Spot-Instanz bieten kann. Die Preise variieren hier deutlich.

  • Präferiert man für seinen Server keine Region, wählt man aus diesen 21 Zonen die günstigste aus (Preisunterschied je nach Instanz-Typ um Faktor 2,7 bis 5,2).
  • Ist man (aus Performance- oder Datenschutzgründen) auf eine Region fest gelegt, kann man durch geschickte Wahl aus den dort angebotenen 2-4 Zonen ebenfalls einen besseren Preis erzielen (Preisunterschied je nach Instanz-Typ um Faktor 1,2 bis 2,7).

Amazon bietet in der AWS-Console zwar die Anzeige des aktuellen Spot-Preises und einen Graphen mit dem zeitlichen Preis-Verlauf, es wird jedoch kein Durchschnittspreis für einen Zeitraum angezeigt und der Vergleich mehrerer Regionen artet in eine Klick-Orgie aus (das Tool arbeitet recht träge).

aws-price-history
Preis-History in der EC2-Console für die Region „eu-west“ und den Instanz-Typ „m1.small“ für einen Monat und alle Verfügbarkeits-Zonen in dieser Region

 

Man kann sich die Preise auch über die CLI-Tools von Amazon beschaffen, (via „ec2-describe-spot-price-history“), bekommt hier aber nur eine Liste mit den Zeitpunkten aller Preisänderung und den ab da gültigen Preisen:


ec2-describe-spot-price-history --headers --instance-type=m1.small | head -6
Type Price Timestamp InstanceType ProductDescription AvailabilityZone
SPOTINSTANCEPRICE 0.032000 2013-04-22T22:59:20+0200 m1.small Windows eu-west-1a
SPOTINSTANCEPRICE 0.026000 2013-04-22T20:39:55+0200 m1.small SUSE Linux eu-west-1a
SPOTINSTANCEPRICE 0.032000 2013-04-22T20:35:46+0200 m1.small Windows eu-west-1c
SPOTINSTANCEPRICE 0.016000 2013-04-22T20:07:52+0200 m1.small Linux/UNIX eu-west-1b
SPOTINSTANCEPRICE 0.047000 2013-04-22T20:01:44+0200 m1.small Linux/UNIX eu-west-1b

Will man Durchschnittspreise ermitteln, muss man die Preise mit deren Gültigkeitsdauer über die Zeit integrieren.

Zu diesem Zweck habe ich ein kleines Perl-Tool erstellt, welches durch Aufruf des CLI-Tools „ec2-describe-regions“ die Regionen ermittelt und durch Aufruf von „ec2-describe-spot-price-history“ die Preise für alle Zonen einer Region für einen gegebenen Instanz-Typ und einen Zeitraum ermittelt.

Um das Tool aufrufen zu können sind folgende Voraussetzungen zu schaffen:

1.) Es werden die Perl-Librarys „DateTime“ und „DateTime::Format::ISO8601“ benötigt (das Modul „Getopt::Long“ ist in den meisten Perl-Installationen schon enthalten).
Auf einem Debian-basierten System erhält man diese Librarys aus diesen Paketen:

sudo apt-get install libdatetime-perl libdatetime-format-iso8601-perl

2.) Dann müssen gemäß der Amazon-Anleitung die CLI-Tools installiert werden und einige Umgebungsvariabeln gemäß den eigenen Werten gesetzt werden:


export JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64/jre
export AWS_ACCESS_KEY=ABCABCABCABC
export AWS_SECRET_KEY=def123def123def123def123
export EC2_HOME=/usr/local/ec2-api-tools
export PATH=$PATH:$EC2_HOME/bin

Der Aufruf eines EC2-CLI-Tools (z.B. „ec2-describe-regions“) sollte nun ohne Fehler funktionieren.
Sind diese Vorraussetzungen erfüllt, kann das Tool z.B. so aufgerufen werden


# Zeige die Hilfe:
avg-spot-price -help
# Instanz-Type "m1.large" und festgeleter Zeitraum (maximal 3 Monate in die Vergangenheit)
avg-spot-price --instance-type m1.large --start-time="2013-04-14T00:00:00" --end-time="2013-04-21T00:00:00"
# Default ist der Instanz-Typ "t1.micro" und ein Linux-System:
avg-spot-price

Beispiel-Ausgabe:

avg-spot-price --instance-type t1.micro --start-time="2013-01-20T00:00:00" --end-time="2013-04-21T00:00:00"
ap-northeast-1a: 0.0072 $/hour (5.25 $/month) (max: 1.0000 $/hour)
ap-northeast-1b: 0.0098 $/hour (7.15 $/month) (max: 0.5000 $/hour)
ap-northeast-1c: 0.0070 $/hour (5.15 $/month) (max: 0.0270 $/hour)
ap-southeast-1a: 0.0040 $/hour (2.93 $/month) (max: 0.0040 $/hour)
ap-southeast-1b: 0.0040 $/hour (2.93 $/month) (max: 0.0040 $/hour)
ap-southeast-2a: 0.0054 $/hour (3.93 $/month) (max: 0.0450 $/hour)
ap-southeast-2b: 0.0044 $/hour (3.24 $/month) (max: 0.0400 $/hour)
eu-west-1a: 0.0111 $/hour (8.15 $/month) (max: 1.0000 $/hour)
eu-west-1b: 0.0182 $/hour (13.29 $/month) (max: 0.5000 $/hour)
eu-west-1c: 0.0064 $/hour (4.72 $/month) (max: 2.0000 $/hour)
sa-east-1a: 0.0040 $/hour (2.93 $/month) (max: 0.0050 $/hour)
sa-east-1b: 0.0046 $/hour (3.36 $/month) (max: 0.0100 $/hour)
us-east-1a: 0.0056 $/hour (4.08 $/month) (max: 1.1000 $/hour)
us-east-1b: 0.0034 $/hour (2.51 $/month) (max: 0.1200 $/hour)
us-east-1c: 0.0110 $/hour (8.02 $/month) (max: 1.0000 $/hour)
us-east-1d: 0.0074 $/hour (5.40 $/month) (max: 3.0000 $/hour)
us-west-1a: 0.0041 $/hour (3.00 $/month) (max: 0.0200 $/hour)
us-west-1b: 0.0040 $/hour (2.93 $/month) (max: 0.0040 $/hour)
us-west-2a: 0.0055 $/hour (4.03 $/month) (max: 1.0000 $/hour)
us-west-2b: 0.0054 $/hour (3.92 $/month) (max: 0.1000 $/hour)
us-west-2c: 0.0043 $/hour (3.17 $/month) (max: 0.0400 $/hour)

Hier sieht man, dass der Durchschnittspreis für eine Micro-Instanz in den letzten drei Monaten für die Zone „us-east-1b“ bei 2,51 $/Monat lag, für die Zone „eu-west-1b“ hingegen bei 13,29 $/Monat (das 5,3-fache). In den Zonen „ap-southeast-1x“ (Singapur) hat der Preis nie den Mindestpreis überschritten – dort hätte man mit dem Angebot von 0,004 $/h einen Server durchgehend betreiben können.

Download: avg-spot-price


#!/usr/bin/perl -w

use strict;
use Getopt::Long;
use DateTime;
use DateTime::Format::ISO8601;

my $args = {};
$args->{‚instance-type‘} = „t1.micro“; # defines the default
$args->{‚product-description‘} = „Linux/UNIX“; # defines the default
GetOptions($args, „help|h“, „instance-type|t=s“, „product-description|d=s“, „start-time|s=s“, „end-time|e=s“);

if ($args->{help}){
print while ;
exit 0;
}

my @regions;
@regions = sort map { chop; (split)[2] } `ec2-describe-regions`;

foreach my $region (@regions){

my $query = „ec2-describe-spot-price-history –instance-type „.$args->{‚instance-type‘}.“ –product-description „.$args->{‚product-description‘}.“ –url https://$region“;
$query .= “ –start-time „.$args->{’start-time‘} if defined $args->{’start-time‘};
$query .= “ –end-time „.$args->{‚end-time‘} if defined $args->{‚end-time‘};
#print „$queryn“;
my $points4zone = {};

foreach (`$query`){
chop;
my $point = {};
($point->{type},$point->{price},$point->{timestamp},$point->{instanceType},$point->{productDescription},$point->{availabilityZone}) = split;

# ISO 8601 expects a „+hh:mm“ at the end of a time string – but aws delivers „+hhmm“
$point->{timestamp} =~ s/+(dd)(dd)$/+$1:$2/;

$point->{dt} = DateTime::Format::ISO8601->parse_datetime($point->{timestamp});

push(@{$points4zone->{$point->{availabilityZone}}},$point);
}

foreach my $zone (sort keys %$points4zone){
# sort from new to old:
my @orderd = sort {$b->{dt}->epoch() <=> $a->{dt}->epoch()} @{$points4zone->{$zone}};

# The latest „follower“ is the end-time or „now“ if no end-time is given:
my $follower = (defined $args->{‚end-time‘}) ? DateTime::Format::ISO8601->parse_datetime($args->{‚end-time‘}) : DateTime->now();
my $newest = $follower->epoch();
my $oldest;
my $integralCost = 0;
my $maxPrice = 0;

# Calculate the duration for each point (subtract the epoch of his follower)
foreach my $point (@orderd){
$point->{duration} = $follower->epoch() – $point->{dt}->epoch();
$follower = $point->{dt};
$integralCost += $point->{duration} * $point->{price};
$maxPrice = $point->{price} if $point->{price} > $maxPrice;
$oldest = $point->{dt}->epoch();
}
my $avgDollarPerHour = $integralCost / ($newest – $oldest);
print sprintf(„%s: %.4f $/hour (%.2f $/month) (max: %.4f $/hour)n“, $zone, $avgDollarPerHour, 24*30.5*$avgDollarPerHour, $maxPrice);
}
}

__DATA__
Calculates the average price for one instance type (t1.micro, m1.small, …) and
and one product (‚Linux/UNIX‘, ‚SUSE Linux‘, ‚Windows‘, …) for a given period
– if there no start/end time, delivers the last 2 month
– minimum start time is 3 month in past

-h, –help
This help

-t, –instance-type INSTANCE-TYPE (default=t1.micro)
The instance type for which prices should be returned.

-d, –product-description PRODUCT-DESCRIPTION (default=Linux/UNIX)
The product description for which prices should be returned.

-s, –start-time START-TIME
The date and time after which the price changes should be returned.
The time specified must be in the local timezone.
The format of the date and time must be of the form
‚yyyy-MM-ddTHH:mm:ss‘. For instance ‚2013-04-01T00:00:00‘

-e, –end-time END-TIME
Price changes up until this point in time should be returned.
The time specified must be in the local timezone.
The format of the date and time must be of the form
‚yyyy-MM-ddTHH:mm:ss‘. For instance ‚2013-04-01T00:00:00‘

Perl Modul für die Gallery REST API

Die Websoftware Gallery bietet eine REST-API, über welche alle erforderlichen Aktionen wie „Alben erstellen“, „Fotos/Videos posten“, „Verschlagwortung“ (Tags) usw. gesteuert werden können.

Da meine Fotos/Videos, welche sich aus 13 Jahren gesammelt haben (25.000 Objekte), über die von Gallery für den Massenimport vorgesehene Funktion „serveradd“ hier diverse Probleme bereitete, entschied ich mich, diese API zu nutzen.

Für die API gibt es schon fertige Bibliotheken in PHP, Java oder Python, von welchen ich die PHP-Bibliothek erfolgreich getestet hatte.

Da meine ganzen Fotos und Videos samt Metadaten, Schlagworten und Beschreibungen aktuell mit verschiedenen Perl-Scripten gemanaged werden, Infos aus den EXIF-Feldern, verschiedenen Datenbanken und Textdateien gelesen werden mussten und hier schon diverse Perl-Bibliotheken existieren, wollte ich Gallery ebenfalls mit Perl-Bibliotheken ansprechen.

Da ich nichts fertiges für meinen Bedarf fand, habe ich nun ein eigenes Perl-Modul dafür geschrieben.

Das Modul bietet Methoden, um

  • Alben an zu legen
  • Photos oder Videos in Alben zu posten
  • Infos zu Items (Alben, Fotos, Videos) aus zu lesen
  • Tags zu erzeugen
  • Items (Alben, Fotos, Videos) zu Taggen
  • Items zu löschen
  • URLs zu bestimmten „Pfaden“ zu ermitteln
  • Taglisten zurück zu geben
  • den kompletten Alben-Baum als Array-Baum zurück zu geben

Methoden zum Handeln von Gallery-Kommentaren fehlen noch (benötige ich nicht, liefere ich aber vielleicht noch nach).

Das Perl-Modul kann zusammen mit einem kleinen Beispielprogramm hier geladen werden: galleryApiDemo.tar.gz.

Hier eine kleines Beispiel-Programm zur Veranschaulichung der Nutzung:


#!/usr/bin/perl -w

use strict;
use lib "/usr/local/lib/perl"; # Path to "GalleryApiRest.pm" and "GalleryItem.pm" (if not in @INC).
use GalleryApiRest;
use Config::Simple;

# Provide a config file with the follow two lines (without "#") and write the location in $cfgFile:

######## Content of config file ################################
# Request-Key = 458453e1a665754e7a37514aefa3a0c7
# Gallery-Api-URL = http://my.domain.eu/index.php/rest
######## /Content of config file ###############################

my $cfgFile = "/etc/gallery/demo.cfg";

# Provide here a path for two demo images:
my $imgFile1 = $ENV{'HOME'}."/bin/galleryApiDemo/galleryApiDemo1.jpg";
my $imgFile2 = $ENV{'HOME'}."/bin/galleryApiDemo/galleryApiDemo2.jpg";

# Read the config
my $cfg = Config::Simple->new($cfgFile) or die Config::Simple->error();

# Create the REST-API:
my $requestKey = $cfg->param('Request-Key');
my $galleryApiUrl = $cfg->param('Gallery-Api-URL');
my $gallery = GalleryApiRest->new($requestKey,$galleryApiUrl);

# Now we can start...

# Create an album:

my $album1 = $gallery->newItem();
$album1->setEnt({name=>"Test",title=>"My test album"});
$gallery->createAlbum("$galleryApiUrl/item/1",$album1);
print "Url of the new Album: ",$album1->getUrl(),"n";

# The object $album1 contains now only the given fields "name" and "title".
# The method "createAlbum" adds only an field "url". See "$album1->dump();"
# To get all Infos into this object, we need an additionsl API call:

$gallery->completeItem($album1);

# Call "$album1->dump();" -> then you see all infos.

# Create an sub album "$album2" as child of "$album1":

my $album2 = $gallery->newItem();
$album2->setEnt({name=>"Test2",title=>"sub album"});
$gallery->createAlbum($album1->getUrl(),$album2);
print "Url of the new sub Album: ",$album2->getUrl(),"n";

# Post two images into $album1 and $album2

my $image1 = $gallery->newItem();
$image1->setFilename($imgFile1);
$image1->setEnt({title => "test image", description => "Hello, this is only a test"});
$gallery->postFile($album1->getUrl(),$image1);
print "Url of image1: ",$image1->getUrl(),"n";

my $image2 = $gallery->newItem();
$image2->setFilename($imgFile2);
$gallery->postFile($album2->getUrl(),$image2);
print "Url of image2: ",$image2->getUrl(),"n";

# Tag the images (the tags will be created, if not exists)

my $relationUrl1 = $gallery->tagItem($album1->getUrl(), "Keyword1");
print "Url of relation1: $relationUrl1n";

my $relationUrl2 = $gallery->tagItem($image1->getUrl(), "Keyword2");
print "Url of relation2: $relationUrl2n";

my $relationUrl3 = $gallery->tagItem($image2->getUrl(), "Keyword3");
print "Url of relation3: $relationUrl3n";

# Change title and sorting of the sub album

$album2->setEnt({title=>"changed sub album", sort_order=>'DESC', sort_column=>'captured'});
$gallery->updateItem($album2);

# Get URL for an given path:

my $url = $gallery->getUrlForPath(['Test','Test2']);

# Get Infos about this url

print "Content of $urln";
$gallery->getItem($url)->dump();

# get a list of Tags:

print $gallery->getTags();

# Delete the album with all images an sub albums

$gallery->deleteItem($album1->getUrl());

Synchronisieren virtueller Maschinen auf einen Switchover-Server mit LVM-Snapshots

Ich betreibe zu Hause einen Linux-Server, auf welchem in diversen XEN-VMs ein Fileserver, ein VDR, ein Mailserver, diverse Webserver, und eine Reihe weitere Server laufen.

Da dieser Server mittlerweile so viele Aufgaben übernimmt, ist ein längerer Ausfall (z.B. durch einen Hardware-Schaden) wenig akzeptabel.

Daher habe ich einen älteren ausgemusterten PC als Switchover-Server aufgestellt. Um Strom zu sparen, ist dieser Switchover-Server normalerweise ausgeschaltet. Alle virtuelle Maschinen werden auf diesen Server gespiegelt und beim Ausfall des primären Servers wird durch einen Start des Switchover-Servers so die gesamte Funktionalität weiter verfügbar.

Um im Switchover-Fall dann auch halbwegs aktuelle Daten und Softwarestände bereit zu haben, werden alle 48 Stunden (Nachts) alle virtuellen Maschinen auf den Switchover-Server synchronisiert.

Diese Synchronisation wird im folgenden beschrieben.

Um die Daten synchronisieren zu können, muss der Switchover-Server regelmäßig gestartet werden. Ich realisiere das über eine per USB steuerbare Steckdosenleiste (Silvershield PMS), welche über die Software SiS-PM-Control z.B. per Cronjob oder aus einem Shell-Script heraus gesteuert werden kann.

Setzt man den Switchover-Server damit unter Strom, so fährt er (bei Wahl von geeigneten BIOS-Parameter wie „Power on after AC back“), von selbst hoch und ist nach einer zugestandenen Boot-Zeit per SSH erreichbar.

Nun geht es darum, von allen virtuellen Maschinen eine 1:1-Kopie zu überspielen. Hier bieten sich zwei Ansatzpunkte an:

  • Eine Synchronisation per „rsync„. Vorteil: nur veränderte Daten werden synchronisiert. Nachteil: Spezial-Dateien (z.B. Devices) können so nicht kopiert werden.
  • Eine Synchronisation per „dd“ über eine SSH-Pipe. Vorteil: Kopiert tatsächlich ein 1:1-Abbild – ungeachtet des Dateisystems, auch Device-Dateien usw. Nachteile: es werden jedes mal alle Daten übertragen. Ausnahmen (Excludes) von nicht relevanten Dateien sind nicht möglich. Die Volumes müssen auf dem primären Server und auf dem Switchover-Server identische Größen haben.

Um hier einen brauchbaren Kompromiss zu erzielen, setze ich meine virtuellen Maschinen immer mit einem recht kleinen Root-Volume (je nach Bedarf 1-2 GB) auf und lagere die eigentlichen Daten dann jeweils auf eigenen Volumes. Die Root-Volumes werden dann per „dd“ synchronisiert, die Daten-Volumes per „rsync“.

Für eine konsistente Spiegelung müssten nun die virtuellen Maschinen jeweils gestoppt oder eingefroren werden. Das wäre bei einer nächtlichen Synchronisation zwar für verschiedene VMs akzeptabel, zumindest für die Webserver und ihre Datenbank-Server aber keine glückliche Lösung.

Nun laufen in XEN (und meines Wissens auch bei KVM) virtuelle Maschinen bevorzugt auf LVM-Volumes. Und LVM bietet eine praktische Snapshot-Funktion. Somit kann im Host-System (Bei XEN auch „Dom0“ genannt) nacheinander von jedem LVM-Volume ein Snapshot erstellt werden, dieser auf den Switchover-Server synchronisiert und anschließend wieder gelöscht werden.

Somit erhält man konsistente Kopien aller VMs auf den Switchover-Server ohne je eine VM auf dem primären Server unterbrechen zu müssen oder sonst wie in die VM eingreifen zu müssen. Die VMs „merken“ von der Kopieraktion also nichts.

Hier mein Script (/usr/local/sbin/syncVM), welches zuerst die Daten-LVs, anschliesend die Root-LVs syncronisiert:


#!/bin/sh

# Liste der "Daten-LVs", welche per rsync übertragen werden:
# (die Namen der Volumes zeigt der Befehl "lvs" an)
lv_list_rsync="in-data-home mail-home dmz-cache-data dmz-web-data dmz-gallery-data in-data-data";

# Liste der "Root-LVs", welche per dd übertragen werden:
lv_list_dd="router-disk mail-root dmz-info-disk in-info-disk dmz-cache-disk dmz-web-disk in-data-disk dmz-gallery-disk dmz-varnish-disk";

# Switchover-Server (Name oder IP)
# (Dieser muss über ein Key passwortfrei per ssh erreichbar sein (root -> root)).
switchover_server="r2"

# Ausgabe der Zeit (um in den Logs die gesamte Laufzeit zu sehen)
date;

# Switch-Over-Server starten
# ("sis" ist ein Wrapper-Script um die eigentliche SiS-PM-Control-Software und gibt den Ports Namen).
/usr/local/bin/sis $switchover_server on;

# Gib dem Switch-Over-Server nun eine üppige Boot-Zeit (falls Plattenchecks anstehen)
sleep 300;

# Nun die Daten-Volumes per "rsync" syncen:
for disk in $lv_list_rsync;
do
echo "----- $disk -----";
date;
lvcreate --size 1G --snapshot --name snap-$disk /dev/vg1/$disk;
mkdir -p /mnt/$disk; mount /dev/vg1/snap-$disk /mnt/$disk;
ssh $switchover_server "mkdir -p /mnt/$disk; mount /dev/vg1/$disk /mnt/$disk";
rsync --archive --delete --stats --hard-links /mnt/$disk/* $switchover_server:/mnt/$disk/;
sleep 10;
umount /mnt/$disk;
ssh $switchover_server "umount /mnt/$disk";
lvremove -f /dev/vg1/snap-$disk;
done;

# Jetzt die Root-Volumes per "dd" syncen
for disk in $lv_list_dd;
do
echo "----- $disk -----";
date;
lvcreate --size 1G --snapshot --name snap-$disk /dev/vg1/$disk;
dd if=/dev/vg1/snap-$disk | ssh $switchover_server "dd of=/dev/vg1/$disk";
lvremove -f /dev/vg1/snap-$disk;
done;

# Switch-Over-Server herunter fahren:
ssh $switchover_server "init 0";
date;

# Einige Zeit warten, bis der Shutdown vom Switch-Over-Server abgeschlossen ist:
sleep 120;

# Strom am Switch-Over-Server abschalten:
/usr/local/bin/sis $switchover_server off

Zum Schluss noch Hinweise, wie das Switchover dann vonstatten geht:

Auf dem Switchover-Server liegen ja nun identische Abbilder von allen virtuellen Servern. Da ich für Server immer feste IP-Adressen vergebe, haben diese bei einem Start auf dem Switchover-Server die selbe IP. Das hat den Vorteil, dass die Maschinen ohne weitere Änderungen die Dienste in meinem Netzwerk übernehmen können. Das hat aber auch den Nachteil, dass die virtuellen Server nie auf dem Switchover-Server gestartet werden dürfen, wenn der ursprüngliche Server noch läuft (z.B. während der Synchronisation).

Ich realisiere das bei XEN so, dass die Konfigurationsdateien der virtuellen XEN-Server auf dem Switchover-Server nicht wie üblich in /etc/xen/auto liegen, sondern in einem eigens angelegten Ordner /etc/xen/failover.

Damit werden die virtuellen Maschinen bei einem Serverstart normalerweise nicht gestartet. Um im Switchover-Fall diese nicht alle manuell starten zu müssen, prüft der Server beim Start von XEN, ob die IPs der virtuellen Server auf dem ursprünglichen Server erreichbar sind und verbiegt sonst die Variable XENDOMAINS_AUTO auf diesen Ordner:


ping -c 1 -w 10 192.168.222.11 > /dev/null
|| ping -c 1 -w 10 192.168.222.5 > /dev/null
|| export XENDOMAINS_AUTO=/etc/xen/failover

Somit kann ein sauberes Switchover dadurch realisiert werden, dass der ursprüngliche Server komplett vom Netz getrent wird und dann der Switchover-Server gestartet wird.

Steckdosen unter Linux steuern

Zum automatisierten Schalten von Peripheriegeräten eignen sich Steckdosenleisten, die von einem Computer gesteuert werden.

Ich steuere damit z.B. externe USB-Platten, welche nächtlich für ein Backup gestartet und danach wieder abgeschaltet werden. Weiterhin einen Switchover-Server, auf den nächtlich virtuelle Server synchronisiert werden, einen Testserver und einen Laserdrucker.

Nun gibt es diverse steuerbare Steckdosenleisten. Viele auch, die per Ethernet direkt über das Netzwerk ferngesteuert und z.B. mittels HTTP-Protokoll angesprochen werden.
Leider findet sich wenig, was für den Hobby-Bereich einigermaßen günstig (unter 100 EUR) zu bekommen ist.

Als ein attraktives Angebot fand ich die Shilvershield PMS, welche vier Steckdosen individuell per USB steuern kann und insgesamt 6 Steckdosen gegen Überspannung schützt (kostet etwa 35 EUR).

Für diese Steckdosenleiste gibt es als Open Source die Linux-Software SisPmCtl, welche dann die individuelle Steuerung der einzelnen Steckdosen oder das Abfragen der Zustände erlaubt.

Die Software ist in den mir bekannten Distributionen nicht im Paketmanagement enthalten. Das Compilieren ist aber mit dem üblichen ./configure; make; make install schnell erledigt (keine weiteren Abhängigkeiten, sehr kurze Compilierzeit).

Mit dieser Software kann die Steckdosenleiste nun gemäß folgender Syntax gesteuert werden:


server:~# /usr/local/bin/sispmctl

SiS PM Control for Linux 2.6

(C) 2004, 2005, 2006, 2007, 2008 by Mondrian Nuessle, (C) 2005, 2006 by Andreas Neuper.
This program is free software.
/usr/local/bin/sispmctl comes with ABSOLUTELY NO WARRANTY; for details
see the file INSTALL. This is free software, and you are welcome
to redistribute it under certain conditions; see the file INSTALL
for details.

sispmctl -s
sispmctl [-q] [-n] [-d 1...] -b
sispmctl [-q] [-n] [-d 1...] -[o|f|t|g|m] 1..4|all
'v' - print version & copyright
'h' - print this usage information
's' - scan for supported GEMBIRD devices
'b' - switch buzzer on or off
'o' - switch outlet(s) on
'f' - switch outlet(s) off
't' - toggle outlet(s) on/off
'g' - get status of outlet(s)
'm' - get power supply status outlet(s) on/off
'd' - apply to device
'n' - show result numerically
'q' - quiet mode, no explanations - but errors

Webinterface features:
sispmctl [-q] [-i ] [-p <#port>] [-u ] -l
'l' - start port listener
'i' - bind socket on interface with given IP (dotted decimal, i.e. 192.168.1.1)
'p' - port number for listener (2638)
'u' - repository for web pages (default=/usr/local/share/httpd/sispmctl/doc)

Das funktioniert soweit ganz gut. Ich vermisste jedoch zwei Dinge:

  1. Ich wollte die Steckdosen nicht mit „1“, „2“, „3“ und „4“, sondern mit Namen wie „drucker“, „usb1000“, … ansprechen.
  2. Ich wollte ein Logging haben, da bei mir verschiedene Geräte durch Scripte (z.B. vor und nach einem Backup) geschaltet werden und ich eine Kontrolle über die Laufzeiten haben wollte.

So verrät z.B. eine Status-Abfrage erst dann, welche Geräte gerade an sind, wenn man eine „Doku“ der Zuordnung von Ports zu Geräten hat:


server:~# /usr/local/bin/sispmctl -g all
Accessing Gembird #0 USB device 003
Status of outlet 1: off
Status of outlet 2: off
Status of outlet 3: on
Status of outlet 4: off

Deshalb habe ich folgendes Wrapper-Script (/usr/local/bin/sis) erstellt, welches den Ports Namen gibt und alle Aktivitäten loggt:


#!/usr/bin/perl -w

# Aufruf:
# sis outletAlias [comment]
# sis
#
# Bsp:
# sis usb1000 on "automatisches Backup"
# sis usb1000 off
# sis

use strict;

############## Config ###############

# Alias (Namenszuordnung von Geräten zu Steckdosen):
my $alias = {
'usb1000' => 1,
'switchover' => 2,
'drucker' => 3,
'via1' => 4,
};

my $states = {
'on' => '-o',
'off' => '-f',
};

# Location von "sispmctl"
my $sispmctl = "/usr/local/bin/sispmctl";

# Location vom Logfile
my $log = "/var/log/sis";

#####################################################

my ($outletAlias,$state,$comment) = ($ARGV[0],$ARGV[1],$ARGV[2]);

unless (defined($outletAlias)){
# Keine Argumente -> Status aller Geraete ausgeben:
my $reverseAlias = {};
foreach my $key (keys %$alias){
$reverseAlias->{$alias->{$key}} = sprintf("%-8s",$key);
}

my $out = `$sispmctl -g all`;
$out =~ s/outlet (d)/$reverseAlias->{$1}/ge;
print $out;
exit;
}

die "Unbekannter Alias '$outletAlias'" unless defined $alias->{$outletAlias};
die "Unbekannter Schaltzustand '$state'" unless defined $states->{$state};
$comment = '' unless defined $comment;

my $outlet = $alias->{$outletAlias};
my $stateArg = $states->{$state};

# Bisherigen Status fuers Log erfassen:
my $oldstate = `$sispmctl -n -q -g $outlet`;
die "Fehler beim Aufruf" if ($oldstate eq '');
chomp($oldstate);

# Schalten:
`$sispmctl -n -q $stateArg $outlet`;

# Neuen Status fuers Log erfassen:
my $newstate = `$sispmctl -n -q -g $outlet`;
die "Fehler beim Aufruf" if ($newstate eq '');
chomp($newstate);

my $date = `date`;
chomp($date);

open(LOG, ">>$log") or die "konnte nicht in $log schreiben: $!n";
print LOG "$date $outletAlias $state $oldstate->$newstate ($comment)n";
close LOG;

1; # return

Damit kann dann über den Namen geschaltet werden (der Kommentar als drittes Argument ist optional) bzw. eine Statusabfrage zeigt gleich den Namen an:


server:~# sis usb1000 on "Backup"
server:~# sis
Accessing Gembird #0 USB device 003
Status of usb1000 : on
Status of switchover : off
Status of drucker : off
Status of via1 : off

Und es wird unter /var/log/sis ein Logfile angelegt:


server:~# cat /var/log/sis
[...]
Tue Dec 14 03:30:02 CET 2010 switchover on 0->1 (sync VMs)
Tue Dec 14 05:48:49 CET 2010 switchover off 1->0 (sync VMs)
Wed Dec 15 02:35:01 CET 2010 usb1000 on 0->1 (dirvish)
Wed Dec 15 03:01:27 CET 2010 usb1000 off 1->0 (dirvish)

Mit Caches zusammenarbeiten

Caches (Zwischenspeicher) ermöglichen das beschleunigte Laden von Webseiten und gleichzeitig die Entlastung des Webservers. Mit ein wenig Konfiguration lassen sich diese Effekte optimieren.

Caching an einem Beispiel

Fordert ein Client (Webbrowser) von einem Webserver eine Seite an, dann wird zunächst die Seite und anschließend typischerweise einige zugehörige Grafiken, Stylesheet-Dateien und Javascripts übertragen. Klickt der Nutzer nun zur nächsten Seite des Angebotes, wird idealerweise nur die neue Seite und eventuell noch einige neue Grafiken übertragen.

Gleichbleibende Dateien, die für die folgenden Seite benötigt werden, für die erste Seite aber bereits geladen wurden (Logos, Stylesheets, Javascripts) werden für diese folgende Seite normalerweise nicht neu geladen.

Realisiert wird dies über Caches, welche zwischen Browser und Webserver geschaltet sind und bereits angeforderte Dateien für den erneuten Gebrauch zwischenspeichern.

Das Caching hat somit den Vorteil, dass die folgenden Seiten nun schneller dargestellt werden können, da weniger Dateien übertragen werden müssen. Außerdem entlastet es den Webserver, der weniger Dateien ausliefern muss.

Caching kann bei ungünstiger Konfiguration aber auch den Nachteil offenbaren dass im Browser zwischengespeicherte Daten angezeigt werden, welche mittlerweile veraltet sind. Außerdem sieht der Webmaster nun nicht mehr zweifelsfrei im Logfile, wie oft seine Seiten aufgerufen wurden.

Um die Nachteile zu umgehen und Vorteile optimal zu nutzen, kann ein Webmaster die Cachesteuerung über HTTP-Header, HTML-Header und andere Tricks optimieren.

Arten von Caches

Man kann beim Caching drei Arten von Caches unterscheiden:

  • Browsercaches cachen Daten direkt im Browser. Dazu werden typischerweise Teile des Arbeitsspeichers und für längerfristiges Caching Teile der Festplatte genutzt. Browsercaches sind bei der Kommunikation vom Webserver zum Browser typischerweise immer im Spiel – es sei denn, ein Nutzer deaktiviert den Browsercache explizit.
    Charakteristik: Der Cache speichert für einen Nutzer Dateien von verschiedenen Webservern.
  • Proxycaches arbeiten zentraler als Browsercaches und können somit Daten für mehrere Nutzer cachen. Proxycaches werden typischerweise von Providern oder in Netzen von Organisationen (Firmen, Schulen, Unis) angeboten. Ihr Sinn ist es, den Netzwerktraffic zu reduzieren, da alle Nutzer eines Providers oder einer Organisation beim Aufruf der selben Datei aus dem Proxy-Cache anstatt vom originären Webserver bedient werden. Proxycaches können ggf. casscadiert zum Einsatz kommen: ein Filiale einer Firma betreibt einen Proxy, dieser holt sich die Daten vom zentralen Proxy der Firma, dieser vom Provider der Firma.
    Charakteristik: Der Cache speichert für verschiedene Nutzer die Dateien von verschiedenen Webservern.
  • Reverse Proxycaches sind cachende Proxys, die direkt vor ein Webserver geschaltet werden. Der Proxy dient hier explizit der Entlastung eines Webservers (nicht der Reduktion von Netzwerk-Traffic oder der schnelleren Darstellung im Browser). Dies wird typischerweise bei Webauftritten eingesetzt, bei denen Seiten einerseits für einen größeren Nutzerkreis erzeugt werden, ein sehr hoher Rechenaufwand pro Seitenerzeugung anfällt und die Seiten für eine bestimmte Zeit dann unverändert bleiben.
    Charakteristik: Der Cache speichert für verschiedene Nutzer die Dateien eines Webservers.

Caching ohne vorgegebene Cachezeiten

Damit ein Cache weiß, ob eine gecachte Datei überhaupt noch verwendet werden darf (es könnte ja eine neuere Datei auf dem Webserver liegen), gibt es im HTTP-Protokoll die „if-modified-since„-Anfrage (Aktualisierungsanfrage). Dazu speichert der Cache zusammen mit der zu cachenden Datei den Zeitstempel aus dem HTTP-Feld „Last-Modified„. Wird die Datei nun erneut angefordert, fragt der Cache beim Webserver (oder übergeordneten Cache) nach der Datei und teilt dabei mit, dass er schon eine Version mit dem Zeitstempel x hat. Der Webserver antwortet nun wie folgt:

  • Wenn es keine neuere Version gibt mit dem HTTP-Status 304 („Not Modified“) (ohne weitere Daten).
  • Wenn es eine neuere Version gibt mit dem HTTP-Status 200 und reicht dabei gleich die neue Datei mit durch.

Soweit so gut. Damit lebte man im Internet recht lange, denn typischerweise bestanden die frühen Webseiten nur aus wenig cachebaren Dateien (da gab es noch keine Stylesheet, oft nur ein Logo welches dann auch noch in einem festen Frame stehen blieb, meist keine zugehörigen Javascriptdateien usw.).
Heute übliche Websites binden jedoch an jede einzelne Seite meist dutzende Stylesheets, Grafiken und Javascripts, die sich immer wieder identisch wiederholen und auch oft monatelang überwiegend nicht verändert werden.
Hier schmerzt es, wenn für diese dutzende Dateien bei jedem Klick dutzende „if-modified-since“-Anfrage gemacht werden – die dann fast durchgehend immer mit einem 304 („Not Modified“) beantwortet werden.

Auch wenn bei diesen Aktualisierungsanfragen und Antworten nur wenige Bytes fließen, benötigt diese Kommunikation doch wesentlich mehr Zeit, als wenn die Dateien direkt aus dem Cache ohne Überprüfung verwendet würden.

Bis zur Einführung geeigneter HTTP-Header gab es für Webmaster wenig Möglichkeiten, das Caching zu steuern. Der Browser Netscape hatte Mitte der 90er Jahre daher für das Caching drei Einstell–Optionen (sinngemäß):

  • Jedes mal nach einer neuen Version anfragen
  • Dateien innerhalb einer Session cachen
  • Dateien dauerhaft cachen

Dabei wurde bei der Installation die zweite Option voreingestellt, denn genau damit erreicht man, dass Dateien, welche sich höchstwahrscheinlich nicht verändert haben auch nicht neu angefragt wurden.

Das Browsen fühlte sich damit flotter an als mit der eigentlich korrekten Einstellung „Jedes mal nach einer neuen Version anfragen“ – zeigte in ungünstigen Fällen aber auch Seiten mit veraltetem Inhalt (Wenn z.B. eine Nachrichtenticker in einer Session mehrfach besucht wurde).
Dabei kann ein Browser gar nicht wissen, welche Dateien wie lange zu cachen sind – dies sollte ihm der Webmaster über den Webserver mitteilen. Die oben stehenden Cacheeinstellungen sind aus modernen Browsern daher verschwunden.

Caching mit vorgegebener Cachezeit

In HTTP 1.0 nutzt man dazu den Expires-Header, welchen man dann mit einem sinnvollen Zeitpunkt in der Zukunft besetzt (man kann auch einen Zeitpunkt in der Vergangenheit wählen, wenn man das Caching vermeiden will):

Expires: Sun, 27 May 2007 19:01:00 GMT

In HTTP 1.1 kamen mehrere Header mit verschiedenen möglichen Werten hinzu. Damit ist es nicht nur möglich, ein Cachzeit zu bestimmen sondern auch zu steuern, ob in „privaten Caches“ (Browsercache) oder „öffentlichen Caches“ (Proxy-Cache) gecached werden darf. Das kann sinnvoll sein, wenn z.B. personalisierte Seiten erzeugt werden, welche dann nur in Browsercaches (pro Nutzer) gecached werden dürfen (Darauf wird hier jetzt nicht weiter eingegangen). Im wesentlichen wird bei der Cachesteuerung von HTTP 1.1 die erlaubte Cachezeit (in Sekunden) sowie das Auslieferungsdatum gesetzt, aus welchem der Cache dann das Verfallsdatum errechnen kann:

Date: Sun, 27 May 2010 18:59:38 GMT
Cache-Control: max-age=300

Mit diesen Headern kann man nun eine Datei für eine bestimmte Zeit als „gültig“ erklären und die zwischengeschalteten Caches müssen in dieser Zeit bei der Verwendung dieser Datei keine erneute Anfrage an den Webserver senden.
Noch eine Randbemerkung zu den HTML-Headern, welche man im HTML-Code innerhalb des Bereiches <head></head> notieren kann: auch hier existieren einige Felder, die das Caching steuern können (http-equiv). Allerdings werden diese nur von Browsercaches gelesen, nie von Proxy-Caches (welche typischerweise nur auf der HTTP-Ebene operieren).

Daher sollten Cacheheader immer auf HTTP-Ebene gesetzt werden. Bei widersprüchlichen Cache-Angaben (zwischen HTTP und HTML-Header) werden ohnehin die Angaben aus dem HTTP-Protokoll verwendet, daher sind Cacheangaben im HTML praktisch überflüssig.

Setzen der Header

Bei dynamisch erzeugten Seiten kann man die HTTP-Header normalerweise beliebig selbst setzen. In CGIs einfach durch vorweg senden vor dem HTML-Content (mit Leerzeile getrennt), in PHP mit dem „header“-Befehl.

Zu beachten ist hier, dass für eine bestmögliche Kompatibilität am besten der Expire- und der Cache-Control-Header gleichzeitig gesetzt werden. Man sollte diese auch nicht inkonsistent setzen. Bei einer Inkonsistenz sollten sich Browser, welche HTTP 1.1 nutzen, an die neueren Cache-Control-Angaben halten – der IE6 bevorzugt hier aber z.B. die Angaben vom älteren Expires-Header.

Hier ein Stück Beispielcode in PHP:

<?php
// Seite zehn Minuten cachen:
$maxage = 10 * 60;
header(„Expires: „.gmdate(„D, d M Y H:i:s“, time() + $maxage).“ GMT“);
header(„Cache-Control: max-age=$maxage“);
?>

Zum setzen von Cachezeiten für statische Dateien (gerade die besonders lange und sinnvoll cachebaren Dateien wie Grafiken, Stylesheets und Javascripts sind meist statisch) bietet sich bei Verwendung des Webserver Apache das Modul „mod_expires“ an.

Mit ein paar Konfigurationsangaben wie in folgendem Beispiel setzt der Apache dann automatisch für die passenden Dateien bei der Auslieferung die korrekten Header:

ExpiresActive On
ExpiresDefault „access plus 5 minutes“
ExpiresByType image/jpeg „access plus 2 days“
ExpiresByType image/gif „access plus 2 weeks“
ExpiresByType image/png „access plus 2 weeks“
ExpiresByType image/css „access plus 2 weeks“
<Directory „/var/www/headerimage“>
ExpiresDefault „access plus 4 weeks“
</Directory>
<Directory „/var/www/javascript“>
ExpiresDefault „access plus 4 weeks“
</Directory>

Sinnvolle Cachezeiten vorgeben

Zum Auffinden von Kandidaten, für die sich das setzen von Cachezeiten lohnen könnte, durchsucht man am besten mal das Logfile vom Webserver nach Dateien, welche mit einem Statuscode von 304 („Not Modified“) ausgeliefert wurden. Ein Histogramm aus dem Access-Log des Apache erstellt z.B. folgender Shell-Befehl:

cat /var/log/apache2/default.access.log | cut -d ‚“‚ -f 2-3 | grep ‚ 304 ‚ | cut -d ‚ ‚ -f 2 | sort | uniq -c | sort -n

(Hier ist der Pfad zum Apache-Log anzupassen – und das klappt so auch nur, wenn das Apache-Log im Default-Log-Format ist).

Welche Cache-Zeiten sinnvoll sind, hängt von einer Menge Faktoren ab:

  • Wie häufig ändern sich die Inhalte der Webseite?
  • Wie häufig ändern sich Webdesign, Funktionen, …?
  • Lebt eine Webseite eher von einer regelmäßigen „Stammkundschaft“ oder von zufälligen Besuchern?
  • Kann man Styles und Layoutgrafiken „versionieren“ (s.u.)?
  • Wie tragisch wirkt es sich aus, wenn zu lange gecachte Daten (z.B. Javascript für Formularprüfung) auf veränderten (erneuerten) HTML-Code mit (z.B. umbenannten Formularfeldern) trifft?
  • Ist es wichtiger, dass jeder Nutzer immer den allerletzten Stand sieht oder ist eine Entlastung des Webservers wichtiger?

Nach Beantwortung dieser Fragen kann man für verschiedene Elemente Cachezeiten von etwa zwei Minuten (z.B. für eine Homepage, auf der ein Newsticker eingeblendet ist) bis vier Wochen (z.B. für Navigationsgrafiken, die sich fast nie ändern (oder über Versionierung geändert werden (s.u.)) ausprobieren.

Abbruch des Cachings bei zu langen Cachezeiten

Aus Sicht der Serverentlastung und Geschwindigkeit ist es reizend, für Elemente wie Stylesheets, Layoutgrafiken oder Javascripts lange Cachezeiten (z.B. einige Wochen) zu setzen.

Damit läuft man aber in ein Problem: irgendwann ändert man z.B. in einer HTML-Seite ein Formular, tut dies auch im korrespondierenden Javascript, welches die Formulareingaben prüfen soll – doch dieses kommt wegen der Cachezeiten bei manchen Nutzern erst Wochen später an!
Was tun? Die Cachezeiten auf wenige Minuten verkürzen um das Problem auf wenige Minuten zu beschränken? Bei statischen Seiten in höherer Stückzahl muss man wohl in diesen Apfel beißen – bei dynamisch erzeugten Seiten (welche heute eher die Regel sind) nutzt man eine „virtuelle Versionierung“.
Für eine virtuelle Versionierung baut man in den Pfad zu Grafiken, Stylesheets, Javascripts, Flashs usw. einfach eine Versionsnummer mit ein. Beispiel:

[…]
<script type=“text/javascript“ src=“/js_42/validate_form.js“ ></script>
<link id=“defaultCSS“ rel=“stylesheet“ type=“text/css“ href=“/css_42/default.css“ />
[…]
<img src=“/layout_42/header/logo_main.png“ alt=“Logo“ />
[…]

Wobei man diese Versionsnummer (im Beispiel die „42“) nicht statisch im Code verteilt sondern am besten über eine zentrale Variable einfügt. Bei PHP z.B. so:

[…]
<img src=“/layout_<?=$layoutVersion?>/header/logo_main.png“ alt=“Logo“ />
[…]

Besteht das Angebot aus vielen einzelnen Seiten, zentralisiert man die Variable mit der Version am besten in einer zentral genutzten Include-Datei oder (besonders wenn mehrere Programmiersprachen eingesetzt sind) über die Webserverkonfiguration in einer Umgebungsvariablen.
Und natürlich verschiebt man seine Dateien hier nicht jedes mal korrespondierend zur Versionsnummer in neue Versionsverzeichnisse – die Versionierung soll ja virtuell sein.

Das kann man passend zu oben stehendem Beispiel gut im Apache z.B. mit folgender Konfiguration lösen:

AliasMatch ^/layout_[0-9]+/(.*) /var/www/layout/$1
AliasMatch ^/css_[0-9]+/(.*) /var/www/css/$1
AliasMatch ^/js_[0-9]+/(.*) /var/www/js/$1

Der Apache sendet dann z.B. einen Zugriff auf „http://www.website.org/layout_45/header/logo_main.png“, welcher auf die Datei „/var/www/layout_45/header/logo_main.png“ zugreifen würde, auf die Datei „/var/www/layout/header/logo_main.png“ – auf der Festplatte gibt es also keine Versions-Verzeichnisse.
Nun kann nach jeder Codeänderung die Versionsnummer verändert werden (z.B. um eins inkrementiert). Dadurch wird jeder Webbrowser gezwungen, von der gecachten Version abzusehen und die neue Version (die aus Sicht des Browser in einem neuen Ordner liegt) abzuholen.

Zählbarkeit der Seitenaufrufe trotz fester Cachingzeit

Die meisten Webmaster wollen zur Erfolgskontrolle wissen, wie oft die eigenen Seiten aufgerufen werden. Vergibt man feste Cachezeiten nur für das „Beiwerk“ (Grafiken, Stylesheets, …) verliert man hier noch nicht die Kontrolle. Liefert man jedoch ein HTML-Dokument mit „max-age=3600“ aus („eine Stunde Caching erlauben, da sich auf dem Dokument selten etwas ändert“), dann tauchen alle Zugriffe, welche z.B. aus einer Organisation (Uni, Firma, Provider) über den selben Proxy in dieser Stunde gemacht werden nicht mehr im eigenen Webserver-Log auf.
Die Lösung sollte hier nicht in einem Verzicht von sinnvoll vorgegebenen Cachingzeiten liegen, sondern über ein „Zählpixel“ realisiert werden. Dazu baut man in jede zu zählende Seite eine 1×1 Quadrat-Pixel große transparente (für den Nutzer unsichtbare) Grafik ein.

Diese Grafik liefert man mit entsprechenden HTTP-Headern aus, welche ein Caching des Zählpixel immer vermeiden.

Man tauscht zwar nun eine wegfallende Aktualisierungsanfrage („If-Modified-Since“, 304) gegen einen zusätzlichen HTTP-Request auf eine uncachebare Grafik ein, gewinnt aber die sofortige Darstellung des HTML-Dokumentes und nimmt dafür nur einen nach gelagerten Request auf ein nur wenige Bytes große Grafik-Datei in Kauf.
Um im Webserver-Log zu sehen, welche Datei jeweils wie oft angeklickt wurde, wird natürlich nicht in jedes Dokument der gleiche Aufruf auf das Zählpixel eingebaut. Erzeugt man seine Webseiten dynamisch, dann kann hier wieder mit einer Variabeln gearbeitet werden:

<img src=“/counter/<?=$_SERVER[‚PHP_SELF‘]?>/trans.gif“ alt=““ />

(Wer nicht mit PHP firm ist: in der Variable $_SERVER[‚PHP_SELF‘] steht bei einem Aufruf von http://www.website.org/pfad/zur/seite.php der String „/pfad/zur/seite.php“)
In der Webserverkonfiguration fangen wir die Aufrufe nun wieder mit einem Alias auf eine Grafik mit einem transparenten Pixel ab:

# Beliebige Zugriffe unterhalb /counter auf das Zaehlpixel lenken:
AliasMatch ^/counter/.* /var/www/layout/counter/trans.gif
# Fuer das Zahlpixel ein sofortiges Expire-date setzen:
<Directory „/var/www/layout/counter/“>
ExpiresDefault „access plus 0 seconds“
</Directory>

Ein Zugriff auf „http://www.website.org/pfad/zur/seite.php“ erzeugt im Beispiel dann einen Zugriff auf das Zählpixel „http://www.website.org/counter/pfad/zur/seite.php/trans.gif“, für welches dann die Grafik „/var/www/layout/counter/trans.gif“ ausgeliefert wird und der Eintrag „/counter/pfad/zur/seite.php/trans.gif“ im Webserverlog entsteht.
Bei einer Analyse der Zugriffszahlen werden nun nur noch die Logfileeinträge gezählt, welche mit „/counter/…“ beginnen.