APC “Cache Flow” überwachen

Das APC (Alternative PHP Cache) Modul für PHP sollte ja bekannt sein. Es handelt sich, wie auch bei bereits veralteten Projekt von Zend (dem Zend Optimizer), um einen Opcode Cache für PHP. Opcode Cache bedeutet, das der kompilierte Source von PHP Scripten im Memory gecached wird. Ab dem zweiten Aufruf des Scripts verschnellert sich dadurch die Script Execution Time um ein vielfaches.

Mittlerweile nutze ich APC aktiv bei ziemlich jedem Webprojekt. Die Performance Schübe, die dadurch zu möglich sind, sind einfach enorm. Dies betrifft inbesondere rechen- und speicherintensive Scripte wie WordPress, Typo3 oder phpBB3. Meist genügen hier schon die Standardeinstellungen mit einer Cachegröße von 30 MB.

Was ich aber noch nicht kannte, ist das APC Info Script.

APC Opcode Cache

Dort lässt sich herauslesen, welche Seiten sich aktuell im Cache befinden und in wie weit der Cache größenmäßig genutzt wird. Optimal für die Performance ist es natürlich, wenn der Cache niemals auf 100% läuft und somit einmal kompilierte Scripts nicht wieder überschrieben werden.

Das Script befindet sich direkt mit im PECL APC Paket und nennt sich apc.php. Bei den Debian Paketen (php-apc) ist es zu finden unter /usr/share/doc/php-apc/apc.php.gz.

Mit PHP6 läuft APC fest in den PHP Core, somit macht es definitiv Sinn sich damit zu beschäftigen.

DynDNS für PowerDNS

Durch meinen Umzug habe ich wieder das gute alte Problem: einen Telekom Anschluss mit einer dynamischen IP. Auf die Hosts dahinter würde gerne von überall unterwegs mittels VPN/IPSec zugreifen. Problem ist natürlich die eventuell wechselnde IP des Anschlusses selbst.
Stichwort DDNS. Lange Zeit hatte ich das mit no-ip.com realisiert. Da ich für linux-dev.de mittlerweile aber sowieso eigene DNS-Server betreibe, kann ich nun mit “hauseigenen” Mitteln lösen.

Anbei ein kleines PHP-Script mit dem ich mir meinen eigenen kleinen DynDNS-Service auf PowerDNS-Basis hoste. Ratsam ist natürlich TTL für den betreffenden Record extrem niedrig zu setzen.

<?php
/**
 * DynDNS-Service für PowerDNS
  *
 * @author Simon "cmon2k" Lauger <simon@lauger.name
 * @date   06.09.2012
 */


$dsn     = 'mysql:dbname=pdns;host=127.0.0.1'; // Datenbank DSN
$user    = 'pdns'; // Name der Datenbank
$pass    = 'password'; // Datenbank Passwort

// Auth-String der als GET-Parameter übermittelt werden muss
$auth    = 'hiddenstring'; 

// Für alle im Array enthaltenen Records dürfen Updates gefahren werden
$allowed = array('somehost.linux-dev.de'); 

$domain = (isset($_GET['domain'])) ? $_GET['domain'] : null;
$ip     = (isset($_GET['ipaddr'])) ? $_GET['ipaddr'] : null;

if ((empty($domain) || is_null($domain)) || (empty($ip) || is_null($ip))) {
        die('missing parameter');
        exit;
}

if (!in_array($domain, $allowed)) {
        die('forbidden domain name');
        exit;
}

if (!isset($_GET['passwd']) || $_GET['passwd'] != $auth) {
        die('authentification failed');
        exit;
}

try {
    $dbh = new PDO($dsn, $user, $pass);
} catch (PDOException $e) {
    echo 'Connection failed: ' . $e->getMessage();
}

$check = $dbh->prepare('SELECT id FROM records WHERE name = :name AND type = :type');
$check->bindParam(':name', $domain);
$check->bindValue(':type', 'A');
$check->execute();
$result = $check->fetch(PDO::FETCH_ASSOC);

if (empty($result)) {
        die('record not found');
        exit;
} else {
        $update = $dbh->prepare('UPDATE records SET content = :content WHERE id = :id LIMIT 1');
        $update->bindParam(':content', $ip);
        $update->bindParam(':id',      $result['id']);
        if ($update->execute()) {
                die('update successful (' . htmlentities($ip, ENT_QUOTES) . ')');
                exit;
        }
        die('update returned false');
        exit;
}

?>

Bleibt noch die Konfiguration des DynDNS-Clients in der Fritzbox (in meinem Fall eine 7390). Diese sieht in meinem Fall so aus:

  • Dynamic-DNS-Anbieter: Benutzerdefiniert
  • Update-URL: http://foo.linux-dev.de/update.php?domain=<domain>&ipaddr=<ipaddr>&passwd=<pass>
  • Domainname: somehost.linux-dev.de
  • Benutzername: beliebig (in diesem Script nicht benutzt)
  • Kennwort: hiddenstring

Und schon funktioniert das ganze – vollkommen unabhängig von Diensten wie DynDNS.com oder No-IP.com.

find: Dateien älter als 5 Minuten löschen

Das Tool find bietet mit den Parameteren -ctime/-mtime nur die Möglichkeit nach Dateien mit Alter +/- N*24h zu suchen. Möchte man etwas spezieller werden und nach Minuten- oder Sekundenwerten suchen, um etwa Cache oder Temp-Files löschen zu können, kann man einen kleinen Umweg gehen. Hier ein kleines Bash-Snippet, das in diesem Fall dazu dient alte Konfigurationen für pxelinux (Format der Dateien: pxelinux.cfg/01-aa-bb-cc-dd-ee-ff) zu löschen.

#!/usr/bin/env bash
cd /srv/tftp/pxelinux.cfg
touch -t `date -d "-5 minutes" +"%Y%m%d%H%M"` /tmp/find.helper
find . ! -newer /tmp/find.helper -iname '01-*' -exec rm {} \;

Zunächst legt man sich mit touch ein File mit dem gewünschten Mindesalter der zu löschenden Dateien an und übergibt dieses File bei find als Parameter -newer. Ein Parameter -older existiert nicht, somit muss das ganze noch mit einem Ausrufezeichen negiert werden.

Zone-Transfer von PowerDNS zu BIND9

DNS-Verwaltung ist immer so eine Sache. Ich baue meine Setups mittlerweile mit PowerDNS und BIND9 zusammen. PowerDNS dient als “versteckter” Master, der an sich nicht auf externe Anfragen antwortet. Die BIND9 Server beziehen ihre Zonen via Zonetransfer vom PowerDNS. Dazu kommt dann noch PowerAdmin. Somit hat man ein DNS-Setup mit Webinterface (und MySQL-Backend) und alle Vorteile in Sachen Geschwindigkeit von BIND9 bleiben erhalten.

Irgendwie muss die Slave-Config auf die BIND9-Slave Maschinen kommen. Hierzu ein kleines, simples PHP-Script.

#!/usr/bin/php
<?php
/**
 * Generiert eine BIND9 Slave Konfiguration aus
 * der PowerDNS-Master Datenbank.
 * 
 * @author Simon Lauger <simon@lauger.name>
 * @date   11.06.2011
 */

$dsn    = 'mysql:dbname=pdns;host=127.0.0.1';
$user   = 'root';
$pass   = 'password';
$master = '127.0.0.1';

try {
    $dbh = new PDO($dsn, $user, $pass);
} catch (PDOException $e) {
    echo 'Connection failed: ' . $e->getMessage();
    exit;
}

$result = $dbh->query('SELECT name FROM domains;');

foreach ($result as $row):
echo 'zone "'.$row['name'].'" {
   type slave;
   file "'.$row['name'].'.hosts";
   masters {
      '.$master.';
   };
};';
echo "\n";
endforeach;

?>

Diese Konfiguration wird komplett via Puppet auf unsere BIND9 Slaves verteilt.

class bind9 {
...
        file { "/etc/bind/named.conf.local":
                mode => 640,
                owner => root,
                group => bind,
                ensure => file,
                source => "puppet:///modules/bind9/named.conf.local",
        }
...
}

Um auch Konfigurationsänderungen zu berücksichtigen sollte zudem noch ein Reload des BIND9s via Cronjob eingerichtet werden.

MySQL Datenbank revisonieren

Ich bin derzeit dabei eine einfache Entwicklungsumgebung aufzubauen. Dabei habe ich nach einer vernünftigen Möglichkeit gesucht um Datenbankänderungen zu revisionieren, sprich alle Änderungen in der Struktur festzuhalten.

Niemand möchte alle SQL-Befehle für Strukturänderungen manuell dokumentieren. Ein kleiner Lebensretter ist hier das Perl Script “mysqldiff”. Das Tool kann mehrere Datenbanken miteinander vergleichen und gibt die Änderungen als SQL-Befehle zurück. Das bedeutet natürlich nicht, das man dadurch eine saubere Softwaredokumentation ersetzen kann.

wget http://adamspiers.org/computing/mysqldiff/MySQL-Diff-0.30.tar.gz
tar -xf MySQL-Diff-0.30.tar.gz
cd MySQL-Diff-0.30
perl ./Makefile.PL
make
make install

Zudem wird noch das Paket Class::MakeMethods::Template::Hash aus dem CPAN benötigt.

cpan Class::MakeMethods::Template::Hash

Gehen wir von der folgenden Situation aus: eine Datenbank “live” mit den Live-Daten sowie eine Datenbank “staging” mit Test-Daten. In der Tabelle “test_1″ in der Staging-Datenbank wurde das zusätzliche Feld “id” hinzugefügt.

CREATE DATABASE live;
CREATE TABLE live.test_1 (
  id int (11)
);

CREATE DATABASE staging;
CREATE TABLE staging.test_1 (
  id int (11),
  id2 int(11)
);

Nun können wir mittels mysqldiff die beiden Datenbanken miteinander vergleichen lassen. Zurück bekommen wir eine Liste mit SQL-Queries, um die Live-Datenbank auf den aktuellen Stand zu bringen.

root@dev01:~# mysqldiff -h localhost -u root -p gX8AmLuO8hWpqau3 live staging
## mysqldiff 0.30
##
## run on Tue Feb  7 22:48:32 2012
##
## ---   db: live (host=localhost user=root)
## +++   db: staging (host=localhost user=root)

ALTER TABLE `test_1` ADD COLUMN `id2` int(11) DEFAULT NULL;

Bestenfalls lässt man das ganze nun automatisiert in ein GIT/SVN Repository laufen. Zum überspielen der Änderungen bietet das Tool auch die Möglichkeit die Änderungen direkt (mittels –apply) auf die Live-Datenbank zu überspielen, jeweils mit einer Abfrage vor dem Absetzen jedes einzelnen SQL-Querys.