Blättern mit MySQL und SQL_CALC_FOUND_ROWS

Verwendet man in einem MySQL-Query die Option SQL_CALC_FOUND_ROWS im Zusammenspiel mit LIMIT, berechnet MySQL die Gesamtzahl der Datensätze unabhängig vom verwendeten Limit.

Beispiel: Will man Daten seitenweise darstellen (-> Pagination), so braucht man z.B. für eine Blätternavigation die Gesamtzahl der vorhandenen Seiten bzw. Zeilen/Datensätze. Die Option SQL_CALC_FOUND_ROWS erspart das doppelte Ausführen des selben Queries, um an die Gesamtzahl zu kommen.

Schlecht

SELECT
    a, b, c
FROM
    table
WHERE  
    /* wahnsinnig kompliziertes Zeug */
LIMIT
    0, 10

--

SELECT
    COUNT(*)
FROM
    table
WHERE
    /* wahnsinnig kompliziertes Zeug */

Besser:

SELECT SQL_CALC_FOUND_ROWS
    a, b, c
FROM
    table
WHERE
    /* wahnsinnig kompliziertes Zeug */
LIMIT
    0, 10

Besonders bei komplexeren Queries schont das den Datenbankserver. Der MySQL-Query-Cache trifft nämlich nicht, wenn das LIMIT (oder sonst irgendwas) verändert wird.

Das Query mit SQL_CALC_FOUND_ROWS und LIMIT gibt wie gewohnt eine eingeschränkte Ergebnismenge zurück. In diesem Beispiel also die ersten 10 Datensätze. Um an die Gesamtzahl der Datensätze zu kommen muss ein zweites Query ausgeführt werden:

SELECT FOUND_ROWS()

Unterm Strich sind das dann trotzdem zwei Queries – aber FOUND_ROWS() gibt lediglich die bereits im ersten Query berechnete Gesamtzahl zurück, zählt also nicht wirklich als komplexes Query.

Eine Sache noch: Unter bestimmten Umständen, z.B. dann wenn nicht sortiert wird, ist das Query mit LIMIT und ohne SQL_CALC_FOUND_ROWS schneller, da das Query beendet ist, sobald das LIMIT erreicht ist. Das ändert aber nichts an der Tatsache, dass unterm Strich die Variante mit SQL_CALC_FOUND_ROWS schneller ist.

SOHO Mailserver unter Debian Lenny mit Getmail

Hier geht’s um die Idee, einen schicken, lokalen SOHO Mailserver unter Debian Lenny einzurichten. SOHO bedeutet Small Office, Home Office – wir reden hier also vom klassischen Serverlein daheim im Schrank, bis hin zum ausgewachsenem Server mit einigen (wenigen) Clients. Das ist kein Copy/Paste-Tutorial. Vielmehr geht es hier um die Möglichkeiten von Debian, einen solchen Mailserver zu realisieren.

Wozu überhaupt ein lokaler Mailserver?

E-Mails direkt am Client per POP abzuholen hat einige Nachteile. Besonders dann, wenn man von verschiedenen Rechner auf seine E-Mails zugreifen will. Außerdem ist die Datensicherung, auch bei nur einem Client, echt anstrengend.

Deswegen wurde IMAP erfunden! Mich stört aber:

  • Die Daten liegen irgendwo (im schlimmsten Fall bei Google!).
  • Begrenzter Speicherplatz: schon allein die Tatsache, dass eine Grenze existiert, stört.
  • Lag! Egal ob xxMbit und lokaler Cache: inet < LAN

Ein lokaler Mailserver löst all diese Probleme: E-Mails werden von diversen Konten per POP3 beim jeweiligen Provider abgeholt, und dann per IMAP im lokalen Netz zur Verfügung gestellt.

Die Anforderungen

  • Abholung von E-Mails verschiedener POP3-Konten bei verschiedenen Providern (GMX, 1und1, usw.)
  • Serverseitige (Spam-)Filter und Weiterleitungen
  • Bereitstellung der Mails mehrerer externer POP-Konten in einem IMAP-Postfach (in Unterordnern)
  • Versand über den SMTP-Server des jeweiligen Providers
  • Internen Mailversand direkt zustellen
  • Shared IMAP Ordner

Die Installation

Glücklicherweise sind alle benötigten Tools als Debian-Pakete verfügbar. Als MTA verwenden wir Postfix, IMAP-Postfächer werden von Dovecot bereitgestellt. Sieve filtert die E-Mails serverseitig. Getmail holt E-Mails von POP3-Konten und Spamassassin kümmert sich um den SPAM. Als Bonus gibt’s den Webmailer Squirrelmail mit Avelsieve, zum Erstellen/Bearbeiten von Sieve-Regeln per Browser.

Benutzer

Die verschiedenen Mail-Benutzer sind “normale” Shell-Benutzer. Die Mail-Verzeichnisse sind jeweils im Home-Verzeichnis des Users. Wir entscheiden uns mal für ~/mail. Das Format ist Maildir und die Verzeichnisse cur, tmp und new müssen wir von Hand mit maildirmake unterhalb von ~/mail anlegen oder einmalig in /etc/skel.

Postfix

Eigentlich könnte man die Mails auch direkt vom Client aus wegschicken. Der “Umweg” über den lokalen Mailserver hat aber den Vorteil, dass wir interne Mails direkt zustellen können (dazu später mehr). Außerdem finde ich es schicker, die SMTP-Daten einmalig am Server zu konfigurieren und dann am Client die einfachen Shell-Logindaten einzutippen. Für serverseitige Weiterleitungen (mit Sieve) brauchen wir sowieso einen MTA.

apt-get install postfix postfix-tls

Wir wählen Internetbetrieb mit Smarthost. Wir wollen ausgehende Mails über den jeweiligen SMTP-Server des Mail-Anbieters senden (also alles von me@gmx.net über smtp.gmx.net, alles von you@1und1.de über smtp.1und1.de usw.). Deshalb müssen wir Postfix so konfigurieren, dass es je nach Absenderadresse verschiedene SMTP-Server mit Authentifizierung verwendet. Das geht mit sender_dependent_relayhost_maps:

/etc/postfix/main.cf:

sender_dependent_relayhost_maps = hash:/etc/postfix/sender_relay
smtp_tls_auth_enable = yes
smtp_tls_password_maps = hash:/etc/postfix/tls_passwd

/etc/postfix/sender_relay:

me@gmx.net     [smtp.gmx.net]
you@1und1.de   [smtp.1und1.de]
...

/etc/postfix/tls_passwd:

me@gmx.net     Benutzer:Passwort
you@1und1.de   you:geheim

Dovecot

Wir installieren Dovecot 1.2 aus den Lenny-Backports. Warum wir die Backports-Version von Dovecot nehmen, und wie man die Backports zur sources.list hinzufügt, habe ich in einem früheren Beitrag schon einmal erwähnt.

apt-get install -t lenny-backports dovecot-common dovecot-imap

Getmail4 /Spamassassin

apt-get install getmail4 spamassassin

Jeder Benutzer hat pro Accoutn eine getmailrc_*-Datei in ~/home/.getmail/. Ein Cronjob führt Getmail regelmäßig aus. Ein Lockfile sorgt dafür, dass E-Mails nicht doppelt abgeholt werden, wenn ein von Cron angestoßener Getmail-Prozess einmal länger dauert.

~$ vi ~/.getmail/me_gmx.net
[options]
delete = true

[retriever]
type = SimplePOP3Retriever
server = pop.gmx.net
port = 110
username = me@gmx.net
password = geheim
use_apop = false
timeout = 180
delete_dup_msgids = false

[destination]
type = MDA_external
path = /usr/sbin/sendmail
arguments = ('-f', me@gmx.net.de', '-oi', 'Shell-Login-Name@Lokaler-Host)

[filter]
type = Filter_external
path = /usr/bin/spamc
arguments = ("-s", "250000", "-u", "Shell-Login-Name", "-p", "783", )

An Postfix wird einmal die lokale Adresse (Shell-User@Hostname) und die externe E-Mail-Adresse übergeben. Das erlaubt die spätere Einsortierung in IMAP-Unterordner anhand des POP-Kontos mittels Envelope-From-Header.

Die Spamassassin-Filterung muss direkt von Getmail über [Filter] konfiguriert werden. Die Weiterleitung an Postfix erfolgt nämlich NICHT per SMTP, und somit ist der Spamassassin in Postfix nutzlos.

Der Cronjob

Der Cronjob wird von root ausgeführt und sucht in Home-Verzeichnissen nach getmailrc_*-Dateien. In diesem Beispiel macht er das jede Minute.

~$ sudo crontab -u root -e
*/1 * * * * /usr/local/bin/getthemail.sh >/dev/null 2>&1

~$ sudo vi /usr/local/bin/getthemail.sh
#!/bin/sh
 LOCK_FILE="/var/lock/getthemail"
 if [ ! -f "${LOCK_FILE}".lock ];
 then
 lockfile-create "${LOCK_FILE}"
 lockfile-touch "${LOCK_FILE}" &

 for getfile in $( find /home/*/.getmail/ -name getmailrc\* -print )
 do
 Y=`ls -l $getfile | awk '{print $3; }'`
 touch $getfile
 sudo -u $Y /usr/bin/getmail --getmaildir /home/$Y/.getmail/ --rcfile $getfile
 done
 lockfile-remove "${LOCK_FILE}"
 fi
exit 0;

Sieve-Filterung

Dovecot 1.2 kommt mit eigener Sieve-Implementierung. Ganz fix:

~$ sudo apt-get install squirrelmail avelsieve
~$ sudo vi /etc/dovecot/dovecot.conf
protocols = imap pop3 pop3s imaps managesieve
protocol lda {
   [...]
   mail_plugins = sieve
}

Per Sieve können wir jetzt auch E-Mails von verschiedenen POP3-Accounts in einem IMAP-Account organisieren. Getmail kann Mails verschiedener Konten in einem IMAP-Ordner zur Verfügung stellen. Mit einer Sieve-Regel lassen sich diese E-Mails in verschiedene Ordner schieben. Der Envelope-To-Header entspricht dabei immer der ursprünglichen Ziel-Adresse.

Interne Mails

Da alle ausgehenden Mails über den Postfix laufen ist das ein Klacks! Wir “mappen” externe Adressen auf lokale User-Account. Das ganze passiert mit Hilfe von alias_maps

~$ postconf -e alias_maps = hash:/etc/postfix/local_delivery
~$ sudo vi /etc/postfix/local_delivery
test@gmx.net    test

Wobei "test" ein lokaler Shell-Benutzer wäre.

Shared IMAP Ordner

Auch hier bietet Dovecot verschiedene Möglichkeiten. Die einfachste: wir symlinken das Shared-Folder einfach in das jeweilige IMAP-Root-Verzeichnis:

ln -s /home/share/mail /home/user/.Shared

In diesem Fall sharen wir die INBOX des users “share”. Das funktioniert natürlich auch für Unterverzeichnisse. Wichtig ist nur, dass dann der user “user” Schreibrechte auf cur, tmp, new und auf die Dovecot-Index-Dateien des “share”-Benutzers besitzt.

Google!

Sommerloch auf neunzehn83? Von wegen!

Es sind ja bekanntlich die kleinen Dinge im Leben, die oft den Unterschied machen. Das ist bei Suchmaschinen nicht anders — schon gar nicht bei Google.

Google ist nicht umsonst die Nr.1 unter den Suchmaschinen. Die Google-Homepage kommt so unscheinbar daher. Und dennoch steckt so unfassbar viel dahinter. Hier mal ein kleines Beispiel:

Google nach “simplexml delete child”

Mysteriöserweise wird auch “remove” als Suchbegriff erkannt und somit fett dargestellt. Wieso? Google benutzt eine ziemlich komplexe Synonym-Erkennung. Das Ganze funktioniert erstaunlich gut. Den meisten Suchenden wird dieses Feature aber gar nicht auffallen.

Was macht die Konkurrenz?

Yahoo

Bing

Enttäuschend. Remove wird offensichtlich nicht berücksichtigt.

Die ganze Thematik  gibt es auch noch einmal in einem lesenswerten Artikel im Google-Blog zum Nachlesen.

Interessanterweise funktioniert das nicht nur für Synonyme, sondern auch für Abkürzungen. Beispiel: “IE6″ bei Google, und Yahoo und Bing.

Was dieses Feature betrifft, kommt nach Google erstmal eine ganze Weile nichts. Bei Bing funktionieren Abkürzungen teilweise. Yahoo scheint keine Abkürzungen zu erkennen.

Runde Ecken mit CSS und ohne Stress

Es könnte alles so einfach sein. Runde Ecken sind mit der CSS3-Eigenschaft “border-radius” leicht zu realisieren. Das funktioniert in allen modernen Browsers (Opera, Firefox, Chrome und Safari). Nicht aber im Internet-Explorer.

Um auch dem IE runde Ecken beizubringen, habe ich bisher immer absolut positionierte Grafiken benutzt. Für einen einfachen Web-Entwickler wie mich stellt die Erstellung solcher “Eck-Grafiken” aber schon eine enorme Herausforderung dar. Gott sei Dank gibt es Generatoren :). Außerdem sind solche Eck-Grafiken unflexibel, da sie bei jeder Farbänderung ausgetauscht werden müssen.

DD_roundies nimmt sich diesem Problem an.  Es ist eine kleine (9KB) Javascript-Datei, die dem IE unter Zuhilfenahme von VML runde Ecken beibringt. Wenn man das Ganze dann per IE-Conditional-Comment einbindet erhöht sich die Ladezeit für die übrigen Browser nicht. Optimal!

<!--[if IE]>
<script type="text/javascript" src="js/dd_roundies.js"></script>

<script type="text/javascript">
    DD_roundies.addRule('.border', 10, false);
    DD_roundies.addRule('.border-top', '10px 10px 0 0', false);
</script>
<![endif]-->

Hier bekommen allen Elementen mit der Klasse “border” rundrum runde Ecken mit 10px Radius. Elemente mit der Klasse “border-top” bekommen nur oben links und oben rechts runde Ecken.

Für alle anderen Browser fügen wir die folgenden CSS3-Eigenschaften hinzu. Da CSS3 in manchen Rendering-Engines nur experimentell umgesetzt ist, müssen wir verschiedene Prefixe verwerden.

.border {
    border: 1px solid #f00;
    border-radius: 10px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
    -khtml-border-radius: 10px;
}
.border-top {
    border: 1px solid #f00;
    border-top-left-radius: 10px;
    -moz-border-radius-topleft: 10px;
    -webkit-border-top-left-radius: 10px;
    -khtml-border-radius-topleft: 10px;
    border-top-right-radius: 10px; 
    -moz-border-radius-topright: 10px;
    -webkit-border-top-right-radius: 10px;
    -khtml-border-radius-topright: 10px;
}

Man beachte die vertauschte Reihenfolge bei -moz und -khtml im Vergleich zu -webkit und dem CSS3-Standard.

Ganz schön viel CSS für ein bisschen runde Ecken – aber immer noch bequemer als Grafiken!

MySQL’s GROUP_CONCAT()

Die GROUP_CONCAT-Funktion von MySQL ist eine tolle Erweiterung zu GROUP BY.  Insbesondere wenn man es mit one-to-many oder many-to-many Beziehungen zu tun hat. GROUP_CONCAT() ist seit der Version 4.1 Bestandteil von MySQL.

Beispiel: Personen haben Hobbys. Die Personen werden in der Personen-Tabelle mit dem Namen (name) und einer eindeutigen ID (person_id) gespeichert. Die zugehörigen Hobbys stehen in der Hobbys-Tabelle. Die Hobbys haben ebenfalls eine fortlaufende ID (hobby_id). Der Name des Hobbys steht in der Spalte hobby, die Verknüpfung zur Person wird über die person_id-Spalte hersgestellt.

mysql> SELECT * FROM personen;
+-----------+-------+
| person_id | name  |
+-----------+-------+
|         1 | Peter |
|         2 | Paul  |
+-----------+-------+

mysql> SELECT * FROM hobbys;
+----------+-----------+-----------+
| hobby_id | person_id | hobby     |
+----------+-----------+-----------+
|        1 |         1 | Schwimmen |
|        2 |         1 | Reiten    |
|        3 |         2 | Radfahren |
+----------+-----------+-----------+

Da eine Person mehrere Hobbys haben, und das selbe Hobby von mehreren Personen ausgeübt werden kann, wäre hier eine many-to-many Beziehung mit extra Join-Tabelle sinnvoller. Der Einfachheit halber belassen wir es aber bei zwei Tabellen. Die Funktionsweise von GROUP_CONCAT() ist in beiden Fällen sowieso die selbe.

Peter hat also die Hobbys “Schwimmen und Reiten“, Paul hat das Hobby Radfahren. Um jetzt mit PHP eine einfache Tabelle auszugeben, in der jeder Name mit den jeweiligen Hobbys aufgelistet wird, könnte man zunächst alle Namen holen, und dann pro Reihe die jeweiligen Hobbys in einem separatem Query. Das würde aber bedeuten, dass wir für n Datensätze n+1 Queries brauchen. Viel zu viel!

Packt man das alles in ein einziges  Query, und joint die Hobbys-Tabelle, taucht im Result der Peter leider doppelt auf. Auch doof…

Abhilfe schafft die GROUP_CONCAT()-Funktion in Verbindung mit einem GROUP BY. GROUP_CONCAT() konkateniert (=verbindet) alle Werte einer Gruppe zu einem String. Null-Werte werden ignoriert. Standardmäßig wird das ganze Komma-getrennt.

SELECT
    p.name
    , GROUP_CONCAT(h.hobby SEPARATOR ', ') AS hobbys
FROM
    personen p
LEFT JOIN
    hobbys h ON h.person_id = p.person_id
GROUP BY
    p.person_id

Mit der Angabe von SEPARATOR innerhalb der GROUP_CONCAT-Funktion trennen wir die einzelnen Hobbys zusätzlich mit einer Leertaste. Ergebnis:

+-------+-------------------+
| name  | hobbys            |
+-------+-------------------+
| Peter | Schwimmen, Reiten |
| Paul  | Radfahren         |
+-------+-------------------+

Perfekt! Für die besonders aufmerksamen Leser gibt’s hier nochmals das komplette Beispiel als SQL-File zum auswendig lernen ;)

SPAM im Griff mit Catch-All E-Mail-Adresse und Subdomain

Die meisten web2.0-Anwendungen verlangen eine Anmeldung, und somit auch eine E-Mail-Adresse, bevor man in den Genuss des kostenlosen Dienstes kommt. Eine Wegwerf-E-Mail-Adresse bietet sich hier nicht immer an. Schließlich will man später eventuell von Updates erfahren. So wird die verwendete E-Mail-Adresse früher oder später zugespammt. Doch welcher der vielen Anbieter, bei denen diese E-Mail-Adresse registriert ist, hat meine Adresse an Spammer verkauft? Oder anders gefragt:

Wer spammt mich hier eigentlich zu?

Wenn ein und dieselbe Adresse für mehrere Dienste benutzt wird, lässt sich das leider nicht herausfinden. Das Anlegen einzelner E-Mail-Adressen für jeden Dienst ist auch mehr als umständlich. Deshalb richten wir eine Catch-All E-Mail-Adresse ein. Damit landen alle E-Mails einer (Sub-)Domain, die an einen nicht existierenden Account gerichtet sind, in einem bestimmten Postfach.

Catch-All auf der Hauptdomain ist generell eine schlechte Idee. Hier ist das Grundrauschen von vornherein viel höher. Deshalb richten wir den Catch-All nur auf einer speziellen Subdomain ein. Zum Beispiel mail.example.org mit catchall@mail.example.org als POP3/IMAP-Account.

Bei Anmeldung an einer Webseite verwenden wir den Namen der selbigen als local-part unser Mailadresse. Beispiel: facebook@mail.example.org oder myspace@mail.example.org. Dank Catch-All laden all diese Mails in unserem catchall@-Account.

Nun lässt sich ganz leicht feststellen, wo der Spam denn herkommt.  Das hilf einem jetzt vielleicht auch nicht unbedingt weiter, aber zumindest lässt sich nun der gesamte Spam anhand der Empfänger-Adresse blockieren. Zum Beispiel mit einer einfachen Sieve-Filterregel. So sollte das Konto auch über längere Zeit sauber bleiben.

PHP’s PDO-Klasse erweitern

PDO ist eine PHP-Erweiterung, die einen konsistenten Zugriff auf Datenbanken ermöglicht. Seit PHP 5.1.0 ist PDO fester Bestandteil von PHP – also eine native (in C geschriebene) PHP-Extension. Dennoch lässt sich PDO mit PHP erweitern. Wozu? Ein Query-Counter, also das simple Mitzählen der ausgeführten SQL-Queries, ist wohl das einfachste Beispiel. Nicht weit davon entfernt sind dann so nette Dinge wie Query-Log oder Query-Zeitmessung.

PDO kommt mit zwei Klassen: PDO und PDOStatement*. Letztere kommt nur bei Prepared Statements ins Spiel. Wir beginnen also mit der PDO-Klasse. Diese muss ihre Eigenschaften an unsere eigene PDO-Klasse vererben. Das geht wie gewöhnlich mit dem Schlüsselwort “extends”. Der Name unserer eigenen PDO-Klasse ist ePDO (e wie enhanced, extended, was auch immer..).

[* und PDOException, welche für dieses Beispiel aber irrelevant ist]

<?php
class ePDO extends pdo {
    private $queryCount;
    public function __construct($dsn, $user = NULL, $pass = NULL, $options = NULL) {
        $this->queryCount = 0;

        parent::__construct($dsn, $user, $pass, $options);

        $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('ePDOStatement', array($this)));
        $this->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    }
}
?>

Wichtig ist hier immer den parent-contructor aufzurufen. Die benötigten Parameter gibt’s im Manual. Außerdem legen wir das private Attribut $queryCount an, in welchem wir die Anzahl der ausgeführten SQL-Queries speichern. Weil wir auch die PDOStatement-Klasse erweitern wollen, teilen wir PDO mittels setAttribute mit, unsere eigene PDOStatement-Klasse zu verwenden. Diese hat sinnigerweise den Namen ePDOStatement (e wie .. ihr wisst schon ..). Der Konstruktor bietet sich übrigens auch dazu an, gewisse Standard-Optionen für PDO zu setzen. In diesem Beispiel die DEFAULT_FETCH_MODE.

Der Beispielcode ist so schon voll funktionsfähig – auch wenn er noch nicht viel macht. Beim Herstellen der Datenbank-Verbindung schreiben wir statt $pdo = new PDO(..) einfach
$pdo = new ePDO(..). Die Parameter, sowie auch alles andere, bleiben die selben.

Um nun die Anzahl der Queries mitzuzählen, müssen wir alle Methoden die Queries ausführen erweitern. Das sind bei der PDO-Klasse: exec() und query():

public function exec($sql) {
    $this->increaseQueryCount();
    return parent::exec($sql);
}
public function query($sql) {
    $this->increaseQueryCount();
    $args = func_get_args();
    return call_user_func_array(array($this, 'parent::query'), $args);
}
public function getQueryCount() {
    return $this->queryCount;
}
public function increaseQueryCount() {
    $this->queryCount++;
}

getQueryCount() ist eine getter-Methode für das privat-Attribut $queryCount. Damit der Query-Count auch von unserer ePDOStatement-Klasse aus erhöht werden kann, packen wir die Logik dazu in die Public-Methode increaseQueryCount(). Da die query()-Methode variable Argumente (siehe PHP-Manual) hat, müssen wir alle übergebenen Argumente mit call_user_func_array an parent::query() übergeben.

Prepared Statements werden von der PDOStatement-Klasse ausgeführt, namentlich von der execute()-Methode. Um auch diese Queries zu zählen, müssen wir uns auch hier der Vererbung bedienen.

class ePDOStatement extends PDOStatement {
    private $pdo;

    protected function __construct(ePDO $pdo) {
        $this->pdo = $pdo
   }

    public function execute($params = array()) {
        $this->pdo->increaseQueryCount();
        return parent::execute($params);
    }
}

Der Konstruktor bekommt das ePDO-Objekt übergeben, in dem unser aktueller Query-Count steht. Wir speichern es als private Eigenschaft ab. In der execute()-Methode rufen wir die increaseQueryCount()-Methode des ePDO-Objektes auf, um unseren Query-Counter zu erhöhen.

Die fertigen Klassen gibt’s es hier nochmal zur Übersicht zum Download: ePDO und ePDOStatement.

So oder so ähnlich wird das dann benutzt:

$pdo = new ePDO("mysql:host=127.0.0.1;dbname=mydb", 'myuser', 'mypass');

$pdo->query("SELECT 1");
$pdo->exec("SELECT 1");

$stmt = $pdo->prepare("SELECT 1");
$stmt->execute();

echo $pdo->getQueryCount(); // 3

Policy based routing auf Debian Lenny/Etch mit Squid Proxyserver

Folgendes Szenario: Ein Debian Lenny Server und zwei Router mit je einer Internetverbindung befinden sich im selben (lokalen) Netzwerk. Wir wollen beide Router (sprich Internetverbindungen) für verschiedene Anwendungen nutzen.

Routing-Tabelle

Routing im privaten Netzwerk ist eigentlich ganz einfach: Liegt die Ziel-IP nicht im selben Subnetz, wird das Paket an den Default-Gateway geschickt. Das nennt sich dann static routing. Verwaltet wird das über sog. Routing-Tabellen. Unter Debian sieht das ungefähr so aus:

~$ sudo route
Kernel-IP-Routentabelle
Ziel            Router          Genmask         Flags Metric Ref    Use Iface
192.168.123.0   *                255.255.255.0   U     0      0        0 eth0
default         192.168.123.99   0.0.0.0         UG    0      0        0 eth0

192.168.123.0 ist das eigene, private Netz, 192.168.123.99 der Router an dem eine DSL oder Kabel-Verbindung hängt. Das sieht bei jedem Client-PC übrigens ähnlich aus. Windows-Benutzer müssen aber “route print” ins cmd tippen.

Policy Based Routing

So weit, so einfach. Was ist aber, wenn wir bestimmte Dienste über einen anderen Gateway leiten wollen? Dann brauchen wir policy based routing. Damit lassen sich unheimlich tolle Sachen machen – wir beschränken uns aber erstmal auf das unterschiedliche Routen anhand der Source-IP-Adresse.

Konkret: Der Lenny-Server bekommt eine zweite (virtuelle) IP, den Squid-Proxyserver binden wir exklusiv auf diese IP, und per PBR schicken wir alle Pakete die vom Squid-Proxy kommen über einen bestimmten Gateway. Der Rest geht nach wie vor zum Default-Gateway.

Mit PBR könnte man natürlich auch nach dem Desination-Port routen, und sich somit die zweite IP-Adresse sparen. Leider bietet  ”ip rule” keine Möglichkeit direkt den TCP destination port als Kriterium auszuwählen. Deshalb müssten die entsprechenden Pakete erst mit iptables markiert, und dann nach dieser Markierung gefiltert werden. Was diesen Post betrifft, blieben wir also bei zwei IPs und somit dem routing anhand der Source-IP.

Ein paar Zahlen zum Verständnis:

- Lenny-Server: 192.168.123.1 & 192.168.123.2
- Router1: 192.168.123.99
- Router2: 192.168.123.98

Die Software

Als Software brauchen wir iproute2 und iptables. Beide Tools sollten selbst bei einer minimalen Debian Installation schon vorhanden sein. Wenn nicht, installieren wir die Tools nach: apt-get install iproute iptables.

Die Konfiguration

Routing-Tabelle anlegen

Zunächst legen wir zwei neue Routing-Tabellen an. Eine für jede ausgehende Leitung. Dazu fügen wir folgenden Zeilen an das Ende von /etc/iproute2/rt_tables hinzu.

#
# reserved values
#
255     local
254     main
253     default
0       unspec
#
# local
#
#1      inr.ruhep
100 arcor
200 kabelbw

Der Name ist jeweils frei wählbar. Ich habe hier die Namen der jeweiligen Provider genommen. Die ID ist auch frei wählbar, muss aber zwischen 1 und 254 liegen.

Routing-Konfiguration

Wir bearbeiten /etc/network/interfaces, fügen ein zweite, virtuelle IP-Adresse hinzu, und teilen Debian mit, mit welchem Adapter welche Routing-Tabellen genutzt werden sollen.

# The loopback network interface
auto lo
iface lo inet loopback
# The primary network interface
#allow-hotplug eth0
auto eth0
iface eth0 inet static
        address 192.168.123.1
        netmask 255.255.255.0
        network 192.168.123.0
        broadcast 192.168.123.255
        post-up ip route add default via 192.168.123.99 dev eth0 table main
        pre-down ip route del default via 192.168.123.99 dev eth0 table main
auto eth0:1
iface eth0:1 inet static
        address 192.168.123.2
        netmask 255.255.255.0
        network 192.168.123.0
        broadcast 192.168.123.255
        post-up ip route add default via 192.168.123.98 dev eth0:1 table arcor
        post-up ip rule add from 192.168.123.2 table arcor
        pre-down ip route del default via 192.168.123.98 dev eth0:1 table arcor
        pre-down ip rule del from 192.168.123.2 table arcor

Die post-up & pre-down Einträge legen automatisch die entsprechenden Regeln in der Routing-Tabelle an, wenn die Netzwerkkarte aktiviert bzw. deaktiviert wird. Von Hand angelegte Einträge verschwinden sonst nämlich nach einem Neustart. Der Traffic über eth0 (192.168.123.1) geht demnach an den Router mit der .99. Traffic über das virtuelle Interface eth0:1 (192.168.123.2) geht an den Router mit der .98.

Squid-Konfiguration

Da nun sämtlicher Traffic der IP 192.168.123.2 über einen speziellen Gateway geht, müssen wir nur noch den Squid-Proxy explizit auf diese IP binden. Dazu öffnen wir die Datei /etc/squid/squid.conf und passen die http_port-Zeile an:

http_port 192.168.99.2:3128

Der Port ist natürlich frei wählbar – wichtig ist hier nur die IP-Adresse. Nach einem Squid-Neustart sollte jetzt sämtlicher Proxy-Traffic über die 192.168.123.98 laufen – der restliche Traffic nach wie vor über die 192.168.123.99.

Sinn?

Gute Frage! PHP-Skripte können zum Beispiel über PHP-Proxy die zwei unterschiedlichen Leitungen benutzen. Selbiges gilt natürlich auch für Clients, die in Ihrem Browser die virtuelle IP des Squid als Proxy eintragen. Somit kann man mit wenigen Klicks mal eben die IP wechseln…

Rdiff-backup unter Debian auf SMB/CIFS-Mount

Für inkrementelle Backups unter Debian Lenny kommt man an rdiff-backup wohl nicht vorbei. Es erstellt Rückwärts-Inkremente — der aktuelle (neuste) Backup-Stand liegt also jederzeit als einfache Datei vor. Vorherige Backup-Stände kann man aus dem aktuellen Stand und den Rückwärts-Deltas wiederherstellen. Das funktioniert entweder über die Kommandozeile oder mit einem Web-Interface wie z.B. rdiff-web.

Das Ganze funktioniert prinzipiell auch auf per SMB oder CIFS gemounteten Shares, zum Beispiel auf eine NAS-Box. rdiff-Backup kümmert sich sogar um das Quoten von Dateinamen, wenn das gemountete Filesystem im Gegensatz zum ext3-Filesystem des Lenny-Servers nicht case-sensitive ist. Allerdings funktioniert das erst so halbwegs reibungslos ab rdiff-backup 1.2.5 – für Etch ist also selber kompilieren angesagt.

In meinem Fall führte ein Backup auf die per SMBFS gemountete NAS aber zu einem komischen Quoting-Schema (“A-Z-”*/:<>?\\|;”). Das resultierte dann in langsamen Backups und rdiff-web wollte auch nicht mehr funktionieren.

Loopback Tricks

Diese Anleitung brachte mich dann auf eine Idee, mit der man die ganzen SMB-Eigenheiten umgehen kann: Man erstellt eine große Datei auf der NAS-Box, erstellt in dieser Datei ein ext3-Dateisystem und mountet es als Loopback-Device. Das Backup erfolgt dann direkt auf diesen ext3-mount. So geht’s:

dd if=/dev/zero of=/mnt/nas/backup.ext3 bs=1M count=100
mkfs.ext3 /mnt/nas/backup.ext3
mount /mnt/nas/backup.ext3 -t ext3 -o,sync,loop,rw,noatime /mnt/backup

Unter /mnt/nas ist die NAS-Box gemountet. Darauf erstellen wir die Datei backup.ext3 (100MB in diesem Beispiel). Das ext3-Dateisystem in dieser Datei mounten wir nach /mnt/backup.

Der Performance hilft das jetzt nicht unbedingt…

Wir messen nach:

Davor:

--------------[ Session statistics ]--------------
 StartTime 1232652246.00 (Thu Jan 22 20:24:06 2009)
 EndTime 1232652365.80 (Thu Jan 22 20:26:05 2009)
 ElapsedTime 119.80 (1 minute 59.80 seconds)
 SourceFiles 4670
 SourceFileSize 16749169 (16.0 MB)
 ...
--------------------------------------------------

Danach:

--------------[ Session statistics ]--------------
 StartTime 1232652246.00 (Thu Jan 22 20:24:06 2009)
 EndTime 1232652365.80 (Thu Jan 22 20:26:05 2009)
 ElapsedTime 48.20 (48.20 seconds)
 SourceFiles 4670
 SourceFileSize 16749169 (16.0 MB)
 ...
 --------------------------------------------------

Das war ein initiales Backup einer Dokuwiki-Installation. Ohne Quoting ist das ganze mehr als doppelt so schnell. Kann natürlich je nach Größe des Backup-Verzeichnisses, inkrementellen Backups oder Restores abweichen :)

Unterm Strich ———

Unterm Strich ist das Loopback-Device für mich die eindeutig bessere Lösung: Das Backup-Repo bleibt schön sauber, da nichts gequotet werden muss und alle SMB-Bugs in rdiff-backup sind ein für alle Mal Geschichte. Hardlink-Support gibts obendrein dazu. Leider kann man den letzten Backup-Stand nicht mehr direkt auf der NAS (von einem Windows-Rechner per Freigabe) einsehen. Hierzu muss man das gemountete ext3-Filesystem erneut mit Samba sharen. Außerdem muss man den Backup-Platz vorher belegen und die Performance ist wohl alles andere als optimal – aber irgendwas ist ja immer!

Kurz notiert: einfache CSS Browser-Hacks für IE6/7

Mit nur einem Zeichen lassen sich CSS-Eigenschaften gezielt für den Internet Explorer 6 und 7 schreiben:

#meinElement {
    background: red;    /* alle Browser */
    *background: green; /* IE 7 und darunter */
    _background: blue;  /* nur IE6 */
}

So sieht das dann aus. Doch sind solche Anpassungen für bestimmte Browser in sparaten CSS-Dateien und per Conditional-Comment eingebunden weitaus besser aufgehoben. Aber zum schnellen Debuggen für zwischendurch kann man diese Hacks schonmal benutzen..