greylite mit MySQL

18. Januar 2010 von Jonas Pasche

Im Rahmen unserer Arbeiten mit qmail-tauglichen Greylisting-Implementierungen sind wir auf greylite gestoßen, das zu einem der am einfachsten zu installierenden und zu benutzenden Tools gehört. Es unterstützt MySQL als Backend, was insbesondere deshalb für uns wichtig ist, weil wir mehrere Filterserver einsetzen, die sich eine Greylisting-Datenbank teilen sollen.

Auch wenn alles von Anfang an prima klappte, eins haute nicht hin: Das automatische Aufräumen von veralteten Einträgen. Zwar war in der Dokumentation nichts davon erwähnt; aus dem Sourcecode ging aber klar hervor, dass ein derartiges Aufräumen durchaus eingebaut ist.

Mit LOG_DEBUG als Log-Level war schließlich das Problem schnell identifiziert:

2010-01-18 19:51:39.804329500 greylite: Cleaning up stale verified entries.
2010-01-18 19:51:39.804348500 greylite: Query: DELETE FROM verified WHERE NOW() - ts > interval '480 hours'
2010-01-18 19:51:39.805073500 greylite: Query failed: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1

Für die Tabelle pending galt übertragen das gleiche. Das Problem ist also, dass der Autor hier eine Syntax verwendet, die von MySQL nicht unterstützt wird: INTERVAL kann dort nur zusammen mit DATE_ADD/DATE_SUB oder direkter Addition oder Subtraktion mit einem Datum verwendet. „Alleinstehend“ auf einer Seite eines Vergleichs kann es nicht genutzt werden. Damit nicht genug heißt die entsprechende Einheit in MySQL nicht etwa HOURS, sondern immer HOUR im Singular, auch wenn eine Formulierung wie INTERVAL 480 HOUR sicherlich sprachlich zweifelhaft ist. Und schließlich ist HOUR ein Schlüsselwort, das nicht gequotet werden darf. Mit anderen Worten: So kann’s überhaupt nicht gehen.

Ich habe daraufhin die beiden betreffenden SQL-Statements in der db-mysql.c schnell umgeschrieben und getestet. Der Einfachheit halber habe ich das Ergebnis als Patch bereitgestellt (greylite-3.0pre2-db-mysql.patch). Den Autor von greylite habe ich informiert; mit etwas Glück ist der Patch bei der nächsten Version nicht mehr nötig.

MySQL, localhost, 127.0.0.1

18. Januar 2010 von Jonas Pasche

Mein Kollege Christopher hatte sich in seinem Artikel über MySQL-Replikation ja bereits über die etwas gewöhnungsbedürfte Behandlung von localhost vs. 127.0.0.1 bei MySQL ausgelassen. Wir konfigurieren bei uns laufende MySQL-Server in der Regel mit skip-networking oder, wenn z.B. (lokale) Replikation benötigt wird, zumindest mit bind-address = 127.0.0.1.

Auf einem Server, den wir nur während einer Übergangsphase ausnahmsweise betreuen, musste ich nun dennoch erstmal Rätselraten: Die Verbindung über 127.0.0.1 klappte nämlich nicht, obwohl MySQL sich durchaus an das Loopback-Interface gebunden hatte und keinerlei iptables-Regeln vorlagen, die einen Connect auf dieser IP verhindert hätten. Die wenig aussagekräftige Fehlermeldung:

$ mysql --host=127.0.0.1 -p
ERROR 2013 (HY000): Lost connection to MySQL server at 'reading initial communication packet', system error: 0

Insbesondere ein system error: 0 erschien mir ziemlich verwirrend, ist die 0 doch typischerweise der Exitcode für „alles klar, kein Problem“.

Nach einem Suchen war die Lösung überraschend einfach: Auf dem fraglichen System existierte eine /etc/hosts.deny mit ALL : ALL darin, und es war explizites Whitelisting mittels mysqld : 127.0.0.1 in der /etc/hosts.allow vonnöten. Für jemanden wie mich, der eigentlich grundsätzlich IP-basierte Zugriffe per iptables regelt und nicht per /etc/hosts.{allow,deny} durchaus eine Stolperfalle … und MySQL-typisch verwirrend, denn mit localhost funktioniert’s ja. (Wer Christophers Artikel nicht gelesen hat: Es funktioniert deshalb, weil MySQL bei der Angabe von localhost grundsätzlich eine socketbasierte Verbindung benutzt, weshalb nebenbei dann auch sämtliche Portangaben ignoriert werden – und eben auch die /etc/hosts.{allow,deny}.)

Hardware-Lieferzeiten

11. Januar 2010 von Jonas Pasche

Unsere Serverhardware bestellen wir inzwischen fast ausschließlich bei der Thomas-Krenn.AG. Nicht zuletzt, seit wir Ende letzten Jahres dort eine Betriebsführung mitmachen konnten, haben wir noch mehr Argumente dafür: Es geht einfach sauschnell, und das ist nicht zuletzt den hervorragend integrierten Prozessen bei Krenn geschuldet, die den Webshop, das Warenwirtschaftssystem und das RMA-System „aus einem Guss“ realisieren, mit hausintern entwickelter Software. Kurz gesagt: Was im Shop bestellt wird (und dort als „sofort lieferbar“ gekennzeichnet ist), kann typischerweise nach zwei Stunden per Paketdienst das Haus verlassen – also schon am nächsten Tag geliefert sein.

Ehrlich gesagt fehlt mir seitdem zunehmend das Verständnis dafür, wieso wesentlich größere Unternehmen wie Dell, die ebenfalls auf „Build-to-Order“ getrimmt sind, so elend langsam arbeiten.

Letzten Mittwoch hab ich dort was bestellt. Nichts Kompliziertes, zwei Vostro-Desktops ohne Extras. Bezahlt per Kreditkarte. Dann passierte erstmal zwei Tage lang nichts.

Am Freitag bekam ich eine handgeschriebene Mail in eher gebrochenem Deutsch („Hiermit bestätigen wir die Erhaltung ihrer Bestellung“, als ob „Erhalt“ und „Erhaltung“ synonym wären, aber es ist natürlich auch beruhigend, dass meine Bestellung vor dem Verfall bewahrt wird): Meine Kreditkarte konnte nicht belastet werden. Und ich Dummerchen dachte, sowas würde während einer Bestellung live geprüft – aber wohl nicht. Man bat mich um Mitteilung einer alternativen Zahlungsmethode. Hab ich dann auch am gleichen Tag gemacht und für diese Bestellung Bankeinzug genehmigt. Nebenbei kann sich auch meine Bank nicht erklären, was es für ein Problem mit meiner Kreditkarte geben sollte, denn das Limit böte eigentlich ausreichend Spielraum, und ich hatte in den letzten Wochen auch nichts anderes über die Karte gebucht. Sei’s drum.

Dann war natürlich erstmal Wochenende. Heute, am Montag, bekomme ich schließlich die Mitteilung, dass der Auftrag nun gebucht sei und man den Zahlungseingang abwarten würde. Herrje. Per Kreditkarte würde der Betrag vorab nur reserviert und erst nach Auslieferung der Bestellung auch gebucht. Nun will Dell das Geld aber gleich, und ein angehängtes PDF verrät mir den prognostizierten Liefertermin: 28.01.2010. In siebzehn Tagen! Oder ausgehend vom Bestelldatum: Satte drei Wochen, bis das Gerät hier ist.

Für ein Kind der Internet-Generation, das von Amazon & Co schon so „versaut“ ist, dass es eigentlich immer von „heute bestellt, morgen geliefert“ ausgeht, ist das harter Tobak – insbesondere, wenn andere Anbieter wie Krenn kurzfristige Lieferungen scheinbar spielend hinbekommen.

„Sicherheitsmethode“ beim Domaintransfer

06. Januar 2010 von Jonas Pasche

Aus der guten alten Zeit haben wir noch einige wenige Domains bei einem Domainregistrar, von dem wir uns schrittweise trennen, in dem wir die letzten Domains sukzessive kurz vor Ablauf zu InternetX transferieren. Das ist insofern eine ziemliche Arbeit, als dass es mit jenem Domainregistrar keinen Rahmenvertrag gibt, der uns davon befreite, für jeden Vorgang unterschriebene Faxe einzureichen. Als muss es hier Papierkram sein.

Nun sind wir praktischerweise für die restlichen Domains auch immer als admin-c eingetragen, denn bei einem der Domaininhaber handelt es sich um einen Verein mit relativ hoher Vorstands-Fluktuation, dem es lieber ist, wenn wir als „Konstante“ uns um seine Angelegenheiten kümmern; ein anderer Domaininhaber lebt im Ausland und benötigt so oder so einen deutschen Ansprechpartner als admin-c. Dass ich administrativer Kontakt der Domains bin, steht insofern auf rechtlich sicheren Füßen, da ich ausdrücklich dazu bevollmächtigt wurde.

Seit Monaten ziehe ich auf dieser Basis die Domains zu unserem neuen Registrar um und unterschreibe dazu auch die einzureichenden Freigabeerklärungen, wozu ich als admin-c laut DENIC-Richtlinien auch berechtigt bin.

Nun schlägt plötzlich ein Transfer fehl, mit der überraschenden Begründung, die Freigabeerklärung sei nicht vom Domaininhaber unterschrieben. Ich antwortete, das sei zwar richtig; sie sei aber vom admin-c unterschrieben, und der sei schließlich befugt, über den Verbleib der Domain zu entscheiden. Daraufhin passierte nichts. Ich insistierte daher einen Tag später noch einmal und streute bei der Gelegenheit noch ein, dass man ja dem letzten Dutzend Transfers auch schon zugestimmt habe, obwohl die Schreiben nicht vom Domaininhaber, sondern „nur“ vom admin-c unterschrieben worden wären.

Heute wurde dem Transfer dann plötzlich zugestimmt, und ich bekam eine überraschende Erklärung per Mail:

Herr Pasche,

wir wissen die Richtlinien von DeNic, aber Sie kennen nicht die Richtlinien in der $FIRMA. Wir als Registrar muessen sicher sein dass der Transfer in Ordnung ist.
Wir verstehen dass der AdminC normaler Weise dass Recht hat fuer die Transfers zu zustimmen. Aber wir muessen auch sicher sein dass zwischen den AdminC und den Domaininhaber keine Probleme gibt, kein Streit oder so. Darum haben wir diese Sicherheits Methode dass wenn ein Antrag nicht vom Domaininhaber stimmt schicken Wir dem Provider eine Mail, durch welch‘ wir auch die Zustimmung vom Domaininhaber verlangen.
Der Transfer wurde jetzt zugestimmt.
Hoffentlich haben Sie Verstaendniss fuer diese Sicherheits Methode aus unserem System.

Wer mich kennt, weiß, dass ich es nur schwer stehenlassen kann, wenn Leute Unsinn reden. Ich habe mich daher freundlich für die Zustimmung zum Transfer bedankt, aber es ging einfach nicht ohne diese Ergänzung:

Verständnis für diese „Methode“ habe ich allerdings nicht. Denn erstens wenden Sie sie offensichtlich nicht in jedem Fall an (zum Beispiel nicht bei meinen vorherigen 12 Transfers), und zweitens haben Sie den Transfer von $DOMAIN erst abgelehnt und dann aber doch zugestimmt, ohne dass sich an der Sachlage irgendetwas geändert oder sich der Domaininhaber dazu geäußert hätte. Ich habe Sie lediglich mit Nachdruck noch einmal zur Zustimmung aufgefordert, mehr nicht.

Die Sicherheitsmethode hängt also offensichtlich erstens vom Zufall und zweitens von Willkür ab. Damit ist es also weder eine Methode, noch dient es faktisch einer weitergehenden Sicherheit. Es ist nur eine ärgerliche und zeitraubende Verzögerung. Dafür fehlt mir in der Tat das Verständnis.

Jeder weiß, dass „mehr Sicherheit“ in der Regel mit „mehr Arbeit“ einhergeht. Um so weniger habe ich Verständnis dafür, wenn mich jemand ohne Not mehr Arbeit machen lässt, ohne dass dies einem erkennbaren Plus an Sicherheit dient. (Wer sich  über meinen „harten“ Umgangston wundert: Der ist beileibe nicht repräsentativ für meinen Umgang mit Geschäftspartnern und Lieferanten; im Gegenteil. Beim konkreten Registrar reiht sich der aktuelle Vorfall aber in eine ganze Reihe von Vorfällen, die einfach nur von geradezu methodischer Willkür erscheinen – sprich, es gibt durchaus einen Grund, warum wir schon vor langer Zeit zu InternetX gewechselt sind.)

IPv6-Tunnel mit 6in4 unter CentOS

06. Januar 2010 von Christopher Hirschmann

Ich hab mir vor einer Weile bei tunnelbroker.net von Hurricane Electric einen Account geklickt und mir darüber einen IPv6-Tunnel zugelegt. IPv6-Tunnel sind derzeit leider noch ein notwendiges Übel, da bisher bei nahezu keinem DSL-Anbieter in Deutschland IPv6 verfügbar ist. Die wenigen die überhaupt IPv6 anbieten verlangen dafür meines Wissens unabhängig von der normalen Grundgebür eine Extra-Gebühr, außerdem sind es soweit ich es bisher mitbekommen habe ausnahmslos Reseller des ehemaligen Fernmeldeamtes der Bundespost.

Bei Hurricane Electric bekommt man kostenlos einen Account über den man bis zu vier IPv6-Tunnel nutzen kann. Dabei wird einem jeweils ein /64er Netz zugewiesen. Dessen Dimension muß man sich am Anfang erstmal verdeutlichen: Der gesamte IPv4-Adressraum ist 32 Bit lang, man bekommt nun ein Netz der doppelten Länge, hat also nicht rund 4 Milliarden Adressen zur Verfügung, sondern 4 Milliarden zum Quadrat. So astronomisch dies klingen mag: das ist die Standard-Netzgröße bei IPv6. Man kann das Netz verkleinern wenn man unbedingt möchte, es empfiehlt sich aber nicht. Erstens sind 64 Bit für das lokale Netz nur die Hälfte der verfügbaren Bits in einer IPv6-Adresse, d.h. es gibt nochmal 4 Milliarden zum Quadrat solcher Netze (also mehr als genug), zweitens bieten nur Netze mit dieser Standard-Größe das durchaus interessante Feature Autokonfiguration an.

Mit vier Tunneln kann man also vier /64er Netze bei Hurricane nutzen. Wem das noch nicht ausreicht, weil er z.B. hinter dem Tunnel mehrere /64er Subnetze betreiben möchte (z.B. ein WLAN, eine DMZ, ein internes Netz), der kann sich auch ein /48er IPv6-Netze erklicken, die dann ebenfalls über die Tunnel geroutet werden. Mit einem /48er verfügbt man dann über 32.768 /64er Subnetze.

Für den Tunnelaufbau werden bei Hurricane angenehmerweise sehr einfache und sehr direkte Konfigurationsanleitungen für eine Vielzahl von Betriebssystemen angeboten, wobei diese aber allesamt für ein Publikum gedacht sind das die Kommandozeile nicht scheut (dies dürfter allerdings 99% der derzeitigen IPv6-Enthusiasten abdecken).

Für Linux gibt es zwei Methoden. Hier zunächst die traditionelle:


ifconfig sit0 up
ifconfig sit0 inet6 tunnel ::$REMOTEIPV4
ifconfig sit1 up
ifconfig sit1 inet6 add $LOCALIPV4
route -A inet6 add ::/0 dev sit1

Und hier die moderne:


modprobe ipv6
ip tunnel add he-ipv6 mode sit remote $REMOTEIPV4 local $LOCALIPV4 ttl 255
ip link set he-ipv6 up
ip addr add $LOCALIPV6 dev he-ipv6
ip route add ::/0 dev he-ipv6
ip -f inet6 addr

(Hurricane gibt auf ihrer Webseite diese Befehle schon mit den richtigen IPs eingetragen aus, so daß man sie in die Kommandozeile kopieren kann.)

In beiden Fällen wird buchstäblich der Weg des geringsten Widerstands gegangen, anders als die meisten Anbieter von IPv6-Tunneln die ich bisher kennengelernt habe, wird einem nicht die Verwendung abstruser Daemons aufgezwungen, die einen dann gegenüber dem Tunnelserver aufwendig authentifizieren. (Das war früher besonders eklig, weil man die Software ganz oft noch selbst übersetzen mußte, dann init-Skripte schreiben mußte und sie oft nicht ganz einfach zu konfigurieren war.) Sollte man unter den für diese Aufgabe geeigneten Daemons einen brauchbaren finden, kann man diesen natürlich trotzdem verwenden.

Wer darauf verzichten kann, dem wird es leicht gemacht, denn unter https://ipv4.tunnelbroker.net/ipv4_end.php kann man seinen Tunnelendpunkt aktualisieren, wobei man von dort auch eine Anleitung und ggf. Fehlermeldungen bekommt. Die Anleitung ist wieder angenehm einfach:


Please use the format https://ipv4.tunnelbroker.net/ipv4_end.php?ipv4b=$IPV4ADDR&pass=$MD5PASS&user_id=$USERID&tunnel_id=$GTUNID
Where:

$IPV4ADDR = The new IPv4 Endpoint (AUTO to use the requesting client's IP address)
$MD5PASS = The MD5 Hash of your password
$USERID = The UserID from the main page of the tunnelbroker (not your username)
$GTUNID = The Global Tunnel ID from the tunnel_details page

Ich hab mir für diese Aufgabe ein Skript geschrieben, das hier heruntergeladen werden kann.

Nun muß man nur noch dafür sorgen, daß dieses Skript auch ausgeführt wird, wenn sich die lokale IPv4 ändert. Auf einem Laptop kann das durchaus komplizierter werden, aber auf einem „ortsstabilen“ Rechner läßt sich dies wunderbar mit cron oder am besten mit runwhen lösen, das ich an dieser Stelle mal wieder empfehlen möchte.

Am besten kombiniert man die Aktualisierung des Tunnelendpunktes gleich mit dem nächtlichen DSL-Reconnect. Ich habe auf meinem Heimserver ein Skript laufen, das morgens um 5 die DSL-Verbindung resettet um der leidigen Zwangstrennung durch den Provider zuvorzukommen und vor allem um diese Trennung auf eine Zeit zu verlegen, zu der ich mit hoher Wahrscheinlichkeit nicht am Rechner arbeite. Dieses Skript habe ich nun um eine Zeile erweitert, damit der IPv6-Tunnel aktualisiert wird, sobald die DSL-Verbindung wieder steht. (Dies ist natürlich nur nötig, wenn man keine statische IPv4 hat, also bei der Zwangstrennung seine IP wechselt. Ansonsten läuft der Tunnel ganz normal weiter.) Bei runwhen geht das sehr einfach mit einem svc -a /service/tunnelbroker-update. Der Vorteil von runwhen ist hierbei, daß man mit den bei daemontools oder runit üblichen Mitteln bequem ein Log schreiben lassen kann.

Blieb zuletzt nur noch ein Problem zu lösen: So schön einfach und direkt die von Hurricane empfohlenen Befehle für den Tunnelaufbau auch sein mögen, möchte ich trotzdem ungern dafür noch ein Skript schreiben, denn eigentlich bringt CentOS schon alle nötigen Tools mit um IPv6-Tunnel aufzubauen. Ich kann nachvollziehen, daß Hurricane nur den generellsten Weg beschreibt der in jeder Linux-Distribution funktionieren dürfte, aber für mich selbst wünsche ich mir doch was anderes und ich nehme dafür auch gerne in Kauf, daß ich mich erst durch die CentOS-Dokumentation wühlen mußte, um den richtigen Weg zu finden. Wühlen ist der richtige Ausdruck, denn die Online-Dokumentation ist in dem Punkt etwas dürftig. Aber die tatsächlich hilfreiche Dokumentation ist netterweise in jedem CentOS schon vorinstalliert: die Datei /usr/share/doc/initscripts-*/sysconfig.txt.

Um die nötigen Einstellungen zu erläutern möchte ich nochmal daran erinnern, wie laut der Anleitung von Hurricane der Tunnel aufgebaut wird: Zuerst wird das Interface sit0 auf den entfernten Endpunkt des Tunnels konfiguriert, dann das Interface sit1 auf den lokalen Endpunkt und schließlich wird die Route gesetzt.

Unter CentOS 5.4 gestaltet sich das nun leider etwas kontraintuitv. Man kann zwar sit0 auf der Kommandozeile anlegen, aber man sollte nicht versuchen dafür in /etc/sysconfig/network-scripts/ eine ifcfg-sit0 anzulegen. Sie wird im besten Fall ignoriert und führt im ungünstigsten Fall zu Fehlern. Statt dessen sollte man nur eine ifcfg-sit1 anlegen und Ihr in etwa diesen Inhalt geben:


# Hurricane Electric 6in4 tunnel
NAME="tunnelbroker.net"
TYPE=sit
DEVICETYPE=sit
DEVICE=sit1
ONBOOT=yes
BOOTPROTO=none
USERCTL=no
PEERDNS=no
IPV6INIT=yes
IPV6ADDR="$LOCALIPV6/64"
#IPV6_MTU=1280
#IPV6TO4_MTU="1280"
IPV6_ROUTER=yes
IPV6_AUTOCONF=no
IPV6TUNNELIPV4=$REMOTEIPV4

Ich hab wieder wie oben schon die IP-Adressen ggf. ersetzt. Zu beachten ist, daß IPV6ADDR="" die IPv6-Adresse in CIDR-Notation erwartet, also mit Angabe der Netzpräfixlänge.

Wenn das Interface sit1 hochgebracht wird, wird das Interface sit0 automatisch angelegt und korrekt konfiguriert.

Die hier verwendeten Optionen habe ich mir aus /usr/share/doc/initscripts-*/sysconfig.txt zusammengesucht. Mag sein, daß die eine oder andere nicht unbedingt notwendig ist, aber mit diesem Setup funktioniert mein Tunnel. Die Optionen IPV6_MTU und IP6TO4_MTU habe ich als Hinweis drin stehen lassen. Je nach Setup kann es sein, daß man an diesen Einstellung drehen muß, 1280 ist jeweils der niedrigste zulässige Wert.

In die /etc/sysconfig/network sollte man nun folgendes Eintragen:


NETWORKING_IPV6=yes
IPV6_AUTOTUNNEL=yes
IPV6_DEFAULTDEV="sit1"

Damit wird IPv6 generell aktiviert (ist es bei CentOS aber eigentlich sowieso, wenn ich das richtig sehe), das Tunneling aktiviert und schließlich dafür gesorgt, daß eine IPv6-Defaultroute angelegt wird, die über das richtige Interface läuft.

Ich hatte zu Anfang etwas Probleme mit dem Eintrag IPV6_DEFAULTDEV="sit1" und mußte da etwas rumprobieren. Eine Zwischenlösung bot die Datei /etc/sysconfig/static-routes-ipv6, in der ich die korrekte Default-Route zeitweilig eingetragen hatte. Beim Recherchieren zu diesem Problem bin ich über verschiedene Meldungen gestolpert, daß es in CentOS mit der IPv6-Default-Route wohl Probleme gäbe. Vor allem sollte wohl die Route sit1 ::/0 nicht funktionieren, statt dessen solle man sit1 2008::/3 verwenden. Das mag für frühere CentOS Versionen vielleicht richtig gewesen sein, für CentOS 5.4 kann ich das aber nicht bestätigen. Bei mir funktionierte die Route sit1 ::/0 einwandfrei und mit dem Eintrag IPV6_DEFAULTDEV="sit1" in der /etc/sysconfig/network wird diese Route auch vollkommen korrekt vom System erzeugt und eingetragen.

Wenn man nun das Netzwerk neu startet oder das Interface sit1 hochfährt, wird der Tunnel aufgebaut (man sollte vorher natürlich bei Hurricane die richtigen Daten hinterlegt haben, also z.B. einmal das tunnelbroker-update-Skript ausgeführt haben).

Um die Funktionstüchtigkeit des Tunnels zu testen braucht man z.T. andere Tools als bei IPv4, z.B. ping6 oder traceroute6. mtr akzeptiert den Schalter -6;, netstat und route benötigen den Schalter -A inet6.

Ich werde in nächster Zeit noch mehr dazu posten, wie man IPv6 von seinem Router zu seinen Endgeräten bringt, wie man djbdns IPv6 so halbwegs beibiegt (wer mit Daniel J. Bernstein vertraut ist, denn wird es nicht wundern: er mag IPv6 nicht und implementiert es nicht – ich kann seine Gründe nachvollziehen, finde das aber angesichts der mittlerweile geschaffenen Tatsachen etwas albern), was bei ip6tables zu beachten ist und wenn mir Zeit zum experimentieren bleibt, wie man MobileIPv6 nutzt.

MySQL-Replikation

26. Dezember 2009 von Christopher Hirschmann

Dieser Artikel ist der zweite Teil einer kleinen Gruppe von Artikeln über MySQL-Backups. Die weiteren Artikel werden hier verlinkt, sobald sie erscheinen:

Hier ist der erste Artikel: MySQL-Backups, aber wie?

Hier ist der dritte Artikel: Hinter der MySQL-Replikation aufräumen

Hier ist der vierte und letzte Artikel: MySQL mit daemontools

Wie im ersten Teil erläutert, nutzen wir gerne eine zweite MySQL-Instanz mit Replikation (einen sogenannten Slave), um MySQL-Backups zu realisieren. In diesem Teil geht es um die Einrichtung dieser Replikation.

Vorab ein paar Worte zu unserem MySQL-Setup:

  • Wir verwenden fast immer die Einstellungsoption skip-networking mit der die gesamte Erreichbarkeit des MySQL-Datenbanksystems über das Netzwerk abgeschaltet wird, inklusive der Möglichkeit das Datenbanksystem über das Loopback-Interface des Localhost anzusprechen.
  • Wenn die Verbindung über das Loopback-Interface aber benötigt wird, verwenden wir meist bind-address = 127.0.0.1, womit die Netzwerk-Erreichbarkeit auf die Loopback-Verbindung beschränkt wird.
  • Wir betreiben den Slave häufig auf demselben Server auf welchem auch der Master läuft. Die zusätzliche Last hält sich in Grenzen und es gibt weniger potentielle Fehlerquellen. Prinzipiell kann ein Slave aber natürlich auch auf einer anderen Maschine laufen.
  • Egal wo der Slave nun läuft, in jedem Fall muß man leider auf die Option skip-networking verzichten. Replikation kann leider nicht über Sockets erfolgen, sondern muß zwingend durch den TCP/IP Stack gehen. Bei Slaves auf demselben Server wie dem Master nutzen wir daher gezwungenermaßen bind-address = 127.0.0.1.

Nun zur Umsetzung:

Zunächst wird die /etc/my.cnf einfach kopiert und als /etc/my-slave.cnf gespeichert. In dieser Datei müssen dann einige Optionen angepaßt werden, die dafür sorgen daß die neue MySQL-Instanz (im weiteren Slave genannt) auf einen anderen Datenbestand zugreift und ein anderes Socket bereitstellt. Das wesentliche in Auszügen:

[mysqld]
port = 3307
socket = /var/lib/mysql-slave/mysql.sock
datadir = /var/lib/mysql-slave
server-id = 2

Auf den folgenden Teil kann man verzichten, wenn man MySQL nicht per init-Skript startet sondern mit den daemontools (so machen wir es fast immer):


[mysqld_safe]
err-log=/var/log/mysql/mysql-slave.log
pid-file=/var/run/mysql/mysq-slave.pid

Verzichten heißt in diesem Fall ggf. auskommentieren. Die ursprüngliche Form sollte man auf keinen Fall stehen lassen.

In einzelnen Punkten kann das Setup mal etwas abweichen. Wir versuchen uns meist an der Default-my.cnf des System zu orientieren, dementsprechend können sich die Pfade leicht ändern.

Zum Port ist zu sagen, daß er recht beliebig ist. Prinzipiell kann jeder Port genommen werden, den die bereits vorhandene MySQL-Instanz (der Master) nicht nutzt (bzw. den kein anderer Dienst nutzt).

Die Server-ID ist ebenfalls recht beliebig, es muß lediglich eine andere sein als beim Master: „For each server, you should pick a unique positive integer in the range from 1 to 232 – 1, and each ID must be different from every other ID.“ Quelle: MySQL Doku

Die /etc/my.cnf benötigt weniger Anpassungen:


[mysqld]
log-bin=mysql-bin
server-id = 1

Das binary log benötigt man bei Replikation immer, man kommt nicht drum herum. Hier bei sei angemerkt, daß man es unter Umständen später mit Logrotation oder ähnlichen Mechanismen wegräumen kann. Das ist aber nicht ganz einfach, einerseits muß man darauf achten dem Master nicht das aktive Log wegzurotieren, andererseits könnte man die alten binary logs unter Umständen benötigen, falls man in der Zukunft weitere Slaves anlegt. (Diese kann man aber auch von einem späteren Stand des binary logs beginnen.)

Nun muß man dafür sorgen, daß Master und Slave auch beide gestartet werden. Wie man hier vorgeht hängt sehr davon ab, wie man seine MySQL-Instanzen startet, also ob man z.B. selber was skriptet, init verwendet oder lieber auf daemontools oder runit setzt. Wir bevorzugen daemontools.

Bei daemontools muß man lediglich ein run-Skript schreiben, mit dem der Slave mit der /etc/my-slave.cnf gestartet wird. Im einfachsten Fall sieht das so aus:


#!/bin/sh
exec /usr/bin/mysqld_safe --defaults-file=/etc/my-slave.cnf

Wer auf init setzt, muß entweder ein neues init-Skript nur für den Slave schreiben oder das vorhandene so anpassen, daß es beide Instanzen startet. Je nach verwendeter Distribution kann das unterschiedlich aufwendig werden.

An dieser Stelle sollte man den Master schonmal neustarten, damit er die Änderungen in der /etc/my.cnf übernimmt und vor allem anfängt, das binary log zu führen.

Jetzt muß man noch dafür sorgen, daß der Slave auch einen eigenen Datenbestand hat. Wie man dabei vorgeht hängt stark davon ab, wie viel einmalige Downtime man sich beim Einrichten des Slaves erlauben kann und wie groß der Datenbestand ist.

Falls der Datenbestand in der MySQL-Instanz bereits groß ist und eine lange Downtime vermieden werden soll, kann zunächst mit rsync eine Kopie des MySQL-Verzeichnisses eingefertigt werden. Diese Kopie ist zwar nicht gut genug um davon die neue Instanz zu starten, aber da meist ein Großteil des Datenbestands selten oder gar nicht verändert wird, kann man hier schon viel Vorarbeit leisten und so später Zeit sparen.

# mkdir /var/lib/mysql-slave
# rsync -vaH /var/lib/mysql/ /var/lib/mysql-slave/

Wie immer bei rsync ist der Slash am Ende des Pfades jeweils wichtig. Hier wird rsync befohlen, nicht den Ordner /var/lib/mysql/ selbst, sondern seinen Inhalt in den Ordner /var/lib/mysql-slave/ zu kopieren.

Wenn eine spürbare Verlangsamung des Systems vermieden werden soll, kann man das übrigens auch mit nice oder bei neueren Kerneln ionice machen:

# nice -n19 rsync -vaH /var/lib/mysql/ /var/lib/mysql-slave/

bzw.:

# ionice -c3 rsync -vaH /var/lib/mysql/ /var/lib/mysql-slave/

Wie gesagt ist der so duplizierte Datenbestand nicht konsistent! Man kann den Slave noch nicht damit starten. Um dies nun möglich zu machen muß man den Master dazu bringen alle Daten konsistent auf die Platte zu schreiben und ihn kurzzeitig mit einem READ LOCK sperren und dann während die Sperre besteht erneut rsync verwenden. Bei der Gelegenheit kann man gleich die Zugriffsrechte für den Slave einräumen.

Man verbindet sich also mit dem Master und befiehlt:

mysql> GRANT REPLICATION SLAVE ON *.* TO slave@'localhost' IDENTIFIED BY 'PASSWORD';
mysql>FLUSH TABLES WITH READ LOCK;
mysql>SHOW MASTER STATUS;
+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000001 | 2730869 | | |
+------------------+----------+--------------+------------------+
1 row in set (0.00 sec)

(Das Passwort kann man selbstverständlich selbst wählen.)

Die Ausgabe von SHOW MASTER STATUS sollte man sich notieren, wir benötigen sie gleich, genau genommen den Dateinamen und die Position (den Offset) im binary log, in unserem Bespiel ist das die 2730869. (Eventuell benötigt man sie später nochmal, wenn man weitere Slaves einrichten sollte, die auch ab dieser Position die Replikation beginnen sollen. In diesem Fall darf man später auch das binary log nicht wegrotieren. Insgesamt scheint dieser Anwendungsfall aber eher selten zu sein.)

Nun muß man den Master kurz anhalten und die Datenverzeichnisse synchronisieren. Hierbei sollte man ausnahmsweise mysqladmin benutzen, damit die Sperre erhalten bleibt. (Normalerweise werden Sperren beim Beenden aufgehoben.)

Update:

# mysqladmin shutdown
rsync -vaH /var/lib/mysql/ /var/lib/mysql-slave/

Es muß mysqladmin shutdown verwendet werden, mysqladmin stop war ein Tippfehler. /Update.

Sobald das erledigt ist, kann man den Master wieder starten und die Sperre aufheben, letzteres mit: mysql>UNLOCK TABLES;

(Wer später weitere Slaves aufsetzen möchte, die an dieser Stelle im Log mit der Replikation beginnen, sollte sich jetzt ein Backup von /var/lib/mysql-slave ziehen, z.B. einen Tarball.)

Jetzt kann der Slave gefahrlos gestartet werden, denn sein Datenverzeichnis enthält konsistente Daten. Wenn er sauber startet, kann man sich auch gleich mit ihm verbinden.

Achtung: Wenn man Master- und Slave-Instanz auf dem selben Computer am Laufen hat, gilt es bei der Verbindung zur Slave-Instanz folgendes zu beachten: Man kann nicht wie gewohnt einfach mysql auf der Kommandozeile ausführen, da das eine Verbindung zur Master-Instanz öffnen würde. Auch die Angabe des Ports mit mysql -P 3307 hilft nicht weiter, da MySQL in diesem Fall, trotz der expliziten Portangabe eine Verbindung zur Master-Instanz öffnen würde (dafür mal ein verdientes #fail an die Adresse der Programmierer). Das gleiche ärgerliche Verhalten würde auch bei mysql -P 3307 -H localhost auftreten.
Damit das funktioniert muß man mysql -P 3307 -H 127.0.0.1 (IPv4) oder mysql -P -H ::1 (IPv6) benutzen. Eine wie ich finde noch elegantere Lösung ist mysql -S /var/lib/mysql-slave/mysql.sock. Wozu den Networkstack bemühen, wenn man schon eine Shell offen hat?

Nun muß dem Slave noch mitgeteilt werden, daß er Änderungen an seinem Datenbestand vom Master replizieren sollt.

mysql> CHANGE MASTER TO
-> MASTER_HOST='localhost',
-> MASTER_USER='slave',
-> MASTER_PASSWORD='PASSWORD.',
-> MASTER_LOG_FILE='mysql-bin.000001',
-> MASTER_LOG_POS='2730869';
mysql> START SLAVE;

Der Slave beginnt jetzt ausgehend von der angegebenen Position (2730869) im binary log seinen Datenbestand zu synchronisieren. Den Fortschritt kann man mit mysql> SHOW SLAVE STATUS\G am besten einsehen. Die letzte Position ist dabei das entscheidende. Seconds_Behind_Master gibt an, ob der Slave mit dem Master synchron ist oder nicht. Steht hier der Wert NULL, ist der Slave angehalten und synchronisiert sich nicht mit dem Master, steht hier ingegen eine Zahl größer 0, gibt sie an wie viele Sekunden der Slave hinter dem Master zurückliegt (diese Zahl sollte schnell kleiner werden), steht hier genau 0 dann ist der Slave synchron zum Master.

Levenshtein mit DBIx::Class

20. Dezember 2009 von Jonas Pasche

Da ich mittlerweile verstärkt mit Catalyst als Framework arbeite und hier als Model-Integration typischerweise DBIx::Class verwendet wird, komme ich nicht umhin, mich weitergehend mit der entsprechenden Syntax auseinanderzusetzen. Dabei mache ich fast jeden Tag die gleiche Erfahrung: DBIx::Class macht die einfachen Anwendungsfälle von SQL noch einfacher, aber die schwierigeren Anwendungsfälle noch viel schwieriger, denn die Klimmzüge, die hier bisweilen in der kruden SQL::Abstract-Syntax zu vollziehen sind, haben es wirklich in sich.

Als Beispiel erstmal der einfache Fall:

my $contact = $schema->resultset('Contact')->search(
  {
    'n_family' => 'Pasche',
    'n_given' => 'Jonas',
    'addresses.city' => 'Mainz'
  },
  { join => 'addresses' }
)->first;

Das ist ja noch durchaus zu überblicken – insbesondere das Zusammenführen mit der Relation ‚addresses‘ ist einfach und elegant.

Will ich nun allerdings die Levenshtein-Distanz mit einbauen, um schreibfehler-tolerant zu suchen, ergeben sich gleich zwei Problemfelder:

  1. Ich brauche eine MySQL-Funktion auf der linken statt auf der rechten Seite des Vergleichsoperators. Okay, prima dokumentiert im DBIx::Class::Manual::Cookbook.
  2. Sowohl auf der linken als auch auf der rechten Seite des Vergleichsoperators sollen Platzhalter (bind values) zum Einsatz kommen: Links der String, mit dem ich vergleichen will; rechts die maximale Distanz, die ich natürlich gerne flexibel als Variable einsetzen möchte. Auch hier hilft noch das Cookbook, wechselt hierbei aber im Beispiel klammheimlich zu literal SQL – gut daran zu erkennen, dass die Suchkriterien nun plötzlich kein Hashref mehr sind. Okay, habe ich nachvollzogen.
  3. Dummerweise möchte ich aber nicht nur einen Wert vergleichen, sondern gleich mehrere. Dabei kann ich das Cookbook-Beispiel natürlich ausbauen und einen entsprechend langen SQL-String selbst schreiben und dann meine ganzen bind values angeben. Das ist jedoch, gelinde gesagt, unglaublich hässlich und vor allem schwer zu warten, weil die bind values innerhalb von Perl an einer ganz anderen Stelle stehen als das SQL-Statement. Möchte ich zum Beispiel ein Feld mittendrin entfernen, muss ich akribisch meine bind values durchzählen, damit ich dort den zugehörigen Wert entferne. So dann bitte lieber nicht.

Nun kann ich auch beim Hashref für die Suchkriterien bleiben, und das funktioniert auch weiterhin, wenn ich SQL-Strings als Hashkeys angebe. Nur wie ich dann auf der rechten Seite, also bei den Hashwerten, die Suchkriterien angeben kann, das hat sich mir einfach nicht erschlossen. Nach längerer Lektüre bleibt aus meiner Sicht nur die Möglichkeit, auch auf der rechten Seite handgeschriebenes SQL zu verwenden, und dort dann die bind values für beide Seiten des Vergleichs anzugeben.

Und so geht’s: Ich gebe den rechten Teil des Vergleichs als doppeltes Arrayref an – die abstrahierte Schreibweise für „Hier kommt jetzt literales SQL mit bind values„. Dabei ist der erste Wert der literale SQL-String; danach folgen die zwei bind values – erst der für links (Vergleichsstring), dann der für rechts (maximale Levenshtein-Distanz) – und da laut Cookbook DBIx::Class hierbei bindtype=columns in SQL::Abstract setzt, muss ich hierbei die etwas krude Syntax der Wertzuweisung über Dummy-Spaltennamen (in meinem Fall einfach „d“) benutzen, wie die oben verlinkte Dokumentation bereits erklärt.

Und so sieht’s final aus:

my $dist = 2;
my $contact = $schema->resultset('Contact')->search(
  {
    'LEVENSHTEIN(n_family, ?)'       => \[ '<= ?', [ d => 'Paschke' ], [ d => $dist ] ],
    'LEVENSHTEIN(n_given, ?)'        => \[ '<= ?', [ d => 'Ionas' ],   [ d => $dist ] ],
    'LEVENSHTEIN(addresses.city, ?)' => \[ '<= ?', [ d => 'Meins' ],   [ d => $dist ] ]
  },
  { join => 'addresses' }
)->first;

Ich bin nicht ganz sicher, ob das wirklich der optimale Weg ist – es erscheint mir ein wenig unsauber, auf der rechten Seite einen bind value mit hineinzumogeln, der eigentlich nach links gehört. Aber ich habe keine sauberere Möglichkeit dafür gefunden – also nehme ich es jetzt mal so hin.

Und so sieht das dann in SQL aus (Ausgabe gekürzt und der besseren Lesbarkeit wegen mit Umbrüchen und Einrückungen versehen; hinter dem Doppelpunkt folgen dann die bind values):

SELECT me.id, [...]
  FROM contact me
       LEFT JOIN address addresses ON addresses.contact_id = me.id
 WHERE (
         (
           LEVENSHTEIN(n_family, ?) <= ?
           AND
           LEVENSHTEIN(n_given, ?) <= ?
           AND
           LEVENSHTEIN(addresses.city, ?) <= ?
         )
       ): 'Paschke', '2', 'Ionas', '2', 'Meins', '2'

Fraglich ist für mich noch, wieso SQL::Abstract das WHERE-Kriterium gleich in doppelte Klammern setzt, wo doch eigentlich nicht einmal einfache vonnöten wären, aber das tut der korrekten Funktion keinen Abbruch.

Hat mich jetzt auch nur zwei Stunden gekostet – für ein SQL-Statement, das ich per Hand aus dem Kopf innerhalb von zwei Minuten fertiggebracht hätte. Wie gesagt: Kompliziertes wird mit SQL::Abstract noch komplizierter. Aber vielleicht ist das ja auch nur dem Umstand geschuldet, dass sich die für SQL::Abstract-Syntax notwendigen Synapsen bei mir noch nicht gebildet haben. Feedback willkommen!

Levenshtein-Distanz mit MySQL

15. Dezember 2009 von Jonas Pasche

Für ein Projekt benötigte ich eine Möglichkeit, eine Duplikatsprüfung durchzuführen. Anforderung war, nicht nur identische, sondern auch „ähnliche“ Datenbank-Einträge wiederfinden können. Dabei stieß ich auf den Algorithmus der Levenshtein-Distanz. MySQL kennt von Haus aus keine Funktion hierfür. Nach einigem Suchen bin ich allerdings auf ein kleines Plugin gestoßen, dass diesen Algorithmus als user-defined function nachrüstet.

Um es kurz zu machen – für die Installation auf einem CentOS 5 auf einer 64-Bit-Plattform habe ich folgende Schritte benötigt:

mkdir /usr/local/src/mysql_levenshtein_udf-1.0
cd /usr/local/src/mysql_levenshtein_udf-1.0
wget http://joshdrew.com/mysql_levenshtein_udf-1.0.tar.gz
tar -xzvf mysql_levenshtein_udf-1.0.tar.gz
gcc -I/usr/include/mysql -fPIC -O -pipe -o mysqllevenshtein.so -shared \
  -L/usr/lib64/mysql -lmysqlclient mysqllevenshtein.cc
cp mysqllevenshtein.so /usr/lib64/libmysqllevenshtein.so

Das -fPIC steht nicht in der README, wird aber für die 64-Bit-Plattform benötigt. Interessanterweise sucht mysqld die .so-Datei in /usr/lib64 und nicht in /usr/lib64/mysql, wie ich eigentlich erwartet hätte. Aber sei’s drum.

Auf der mysql-Shell schließlich noch als root:

CREATE FUNCTION levenshtein RETURNS INT SONAME 'libmysqllevenshtein.so';

Und das war’s auch schon. MySQL merkt sich solche vom Benutzer eingefügten Funktionen in der Tabelle mysql.func, so dass sie problemlos einen Restart überdauern. Sehr schick!

Und so wird’s benutzt, hier zum Beispiel vor dem Hinzufügen eines neuen Datensatzes, um zu schauen, ob es schon jemanden gibt, der möglicherweise so in etwa ebenfalls „Jonas Pasche“ heißt – nur dass eben auch „Ionas Paschke“ gefunden würde:

SELECT *
  FROM contact
 WHERE LEVENSHTEIN(n_family, "Pasche") < 2
       AND LEVENSHTEIN(n_given, "Jonas") < 2;

Unvorhergesehene Ereignisse

15. Dezember 2009 von Jonas Pasche

Aufgrund eines unvorhergesehenen Ereignisses sind wir momentan telefonisch nicht erreichbar. Bitte versuchen Sie es zu einem späteren Zeitpunkt erneut.

Von wem möchte man diese Ansage nun wirklich nicht hören? Richtig: Von der Störungshotline des eigenen Telefonanbieters. Versatel routet nämlich gerade meine IP-Pakete zwar noch so ein bisschen von meinem DSL-Zugang in sein eigenes Netz, wo sie dann aber schließlich versanden. Rufe ich mein Handy an, verrät mir eine Ansage, die gewählte Nummer sei „unvollständig“. Anrufe zu anderen Festnetzanschlüssen funktionieren manchmal, manchmal auch nicht. Nach vergleichbarem Muster bin ich auch anrufbar: Manchmal, manchmal auch nicht.

Wäre zu verschmerzen, wenn das nicht schon der vierte größere Ausfall (mit „größer“ meine ich: über eine Stunde andauernd …) innerhalb weniger Wochen wäre. Bisher war ich von Versatel immer hohe Stabilität gewohnt, aber, Jungs, das Bild bröckelt.

Ein guter Anlass, mal mein kleines Notfall-Script zu copy-and-pasten, das ich für solche Fälle auf meinem Netbook habe (das ein UMTS-Modul eingebaut hat, in dem dank Multi-SIM auch immer eine aktive SIM-Karte steckt):

#!/bin/sh

IPTABLES=/sbin/iptables
INTIF=wlan0
EXTIF=ppp0

# remove the default route (lan/wlan)
route del default

# add a new default route for umts
route add default gw 10.64.64.64

# enable ip forwarding in general
echo 1 > /proc/sys/net/ipv4/ip_forward

# remove current forwarding and nat rules
$IPTABLES -P FORWARD DROP
$IPTABLES -F FORWARD
$IPTABLES -t nat -F

# provide forwarding for the internal network
$IPTABLES -A FORWARD -i $EXTIF -o $INTIF -m state --state ESTABLISHED,RELATED -j ACCEPT
$IPTABLES -A FORWARD -i $INTIF -o $EXTIF -j ACCEPT
$IPTABLES -A FORWARD -j LOG

# masquerade outgoing connections
$IPTABLES -t nat -A POSTROUTING -o $EXTIF -j MASQUERADE

Das Ändern der Defaultroute liegt darin begründet, dass das Netbook ja gleichzeitig mit zwei Interfaces verbunden ist, die – normalerweise – beide als Gateway taugen, und der NetworkManager bevorzugt dabei immer ein WLAN-Device vor einem UMTS-Device. Nur ist das ja gerade das, was den aktuell nicht funktionierenden Zugang bereitstellt, und wie üblich kann man beim NetworkManager nur extrem wenig manuell einstellen – beispielsweise nicht, welches der aktiven Geräte denn bitte die Defaultroute bereitstellen soll.

Aber wie dem auch sei: Kurz das Script ausgeführt, auf den lokalen Rechnern die lokale IP des Netbooks als Gateway eingestellt, und schon gibt’s wieder Internet. Den DNS-Resolver brauche ich nebenbei nicht zu ändern, da ich lokal auf den Rechnern dnscache laufen habe und ohnehin nicht den DSL-Router dafür benutze.

Update: Die aktuelle Störung ist dann wohl das hier.

„Wir werden nicht eine Plage“

09. Dezember 2009 von Jonas Pasche

Ist man schon je unterhaltsamer von einem – an sich englischsprachigen – Shop, der sich 2010 erstmalig auch über den großen Teich wagt, über den Umgang mit seinen persönlichen Daten informiert worden?

Wir werden niemals verkauft oder vermietet Ihre Informationen. Wir speichern keine Ihrer finanziellen Daten. Wir verkaufen nur Kalender! Wenn Sie bei uns kaufen, können wir uns mit Ihnen im nächsten Jahr wieder sehen, wenn Sie interessiert sind. Wir werden nicht eine Plage.

Schreiben die mit den lustigen Kalendern. Keine Plage zu werden – das sollten ruhig noch mehr Firmen in ihr Selbstverständnis aufnehmen.


Impressum