Too Cool for Internet Explorer

nginx tips: adding arbitrary output to a response


A while ago i wrote about how to use nginx as a proxy to do cookie based redirects. We use this functionality at work to provide easy access and view the progress of development of each developer.

I thought it would be nice to have the information, of which developers machine you currently have access to, right on the website. But i always disliked to put functionality to accomplish this inside the framework or even the app itself or have to install / modify some special configuration on each developers machine. I always wanted my proxy to do this kind of work. ... And nginx can.

It's the substitution module of nginx, that can replace arbitrary text in a http response. Nginx must be compiled with the option --with-http_sub_module configured.

The following rows show how to fill a variable $name with the name of the developer we are accessing the machine of. The statement sub_filter defines the search pattern as first parameter and the replace string as second parameter -- very easy, isn't it?

set $name ""
if ($http_cookie ~* "(; )?devredirect=([^;]+)") {
    set $name $2;
}
sub_filter      "</body>"       "<div style='position: fixed; 
    left: 0; top: 0; font-weight: bold; padding: 5px; color: #000; 
    background-color: rgb(235,58,0);'>devredirect: ${name}</div>
    </body>";

The following rows show the part of the proxy configuration with the inserted substitution filter:

server {
    listen          80;
    server_name     .clipdealer.devcenter.int;

    proxy_redirect          off;
    proxy_set_header        Host            $host;
    proxy_set_header        X-Real-IP       $remote_addr;
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;

    location / {
        set $name ""
        if ($http_cookie ~* "(; )?devredirect=([^;]+)") {
            set $name $2;
        }
        sub_filter      "</body>"       "<div style='position: fixed; 
            left: 0; top: 0; font-weight: bold; padding: 5px; 
            color: #000; background-color: rgb(235,58,0);'>
            devredirect: ${name}</div></body>";

        if ($http_cookie ~* "(; )?devredirect=harald") {
            proxy_pass              http://10.0.0.20;
            break;
        }
        ...
    }
    ....
}


OSX 10.5.x + PHP + pecl_ncurses


After several days playing around with newt i am quite disappointed to come to a point where i have to say, that newt has not only a very limited documentation (i think there's only documentation of about 50% of the newt functionality) -- which would not be that of a problem, though. The bigger problem is, that things are very unstable. "Segmentation faults" reproducable on different operating systems and platforms -- not sure though, if the problem is newt or pecl_newt. However: this is definitly not what i want. So, let's move on to something better.

I had a look at the apple's opensource repository the other day and stumbled over a ncurses package. I had problems with the normal gnu ncurses package before, it would not install but hang when building the terminfo database. With the ncurses package i found at apple's opensource repository, i get it to install. Here is the howto:

Note: The first three steps below are for osx 10.5.x. For osx 10.6.6 simply download ncurses 5.7 and build it using ./configure --with-shared.

  1. Download ncurses from apple's opensource repository. You can find it by clicking on the link of the Mac OSX version you are running, eg. 10.5.8.
  2. Unpack the package und you get a directory called something like "ncurses-21" or so. Next cd into this direktory and run make and sudo make install. If make complains about a missing directory /tmp/ncurses/Build, just create it mkdir -p /tmp/ncurses/Build and start again.
  3. Now an intermediate build should have been generated. Next cd /tmp/ncurses/Build, make and sudo make install. Now ncurses should have been successfully installed.
  4. Ready to install the ncurses php extension by either executing sudo pecl install ncurses or downloading ncurses from pecl.php.net and building manually.
  5. Don't forget to add ncurses.so to your php.ini

Now everything should be ready to start writing ncurses apps -- however: for me it sill did not work. The simplest ncurses app resultet in the following error message:

Error opening terminal: vt100.

What's going on? dtruss might be your friend to figure this out. Execute the following command, where example.php is your ncurses app you want to run: sudo dtruss php example.php. You should see output like:

[...]
lstat("/Users/harald\0", 0xBFFFE41C, 0xFFFFFFFFBFFFEFC8)                 = 0 0
lstat("/Users\0", 0xBFFFE2EC, 0xFFFFFFFFBFFFEFC8)                = 0 0
ioctl(0x3, 0x4004667A, 0xBFFFF17C)               = -1 Err#25
ioctl(0x3, 0x402C7413, 0xBFFFF150)               = -1 Err#25
fstat(0x3, 0xBFFFF1B0, 0xBFFFF150)               = 0 0
mmap(0x0, 0x226, 0x1, 0x2, 0x3, 0x100000000)             = 0x17FA000 0
lseek(0x3, 0x0, 0x1)             = 518 0
munmap(0x17FA000, 0x1F3)                 = 0 0
close_nocancel(0x3)              = 0 0
ioctl(0x1, 0x4004667A, 0xBFFFECEC)               = 0 0
stat("/Users/harald/.terminfo\0", 0xBFFFEC00, 0xBFFFECEC)                = 0 0
access("/Users/harald/.terminfo/73/vt100\0", 0x4, 0xBFFFECEC)              = -1 Err#2
stat("/usr/local/share/terminfo\0", 0xBFFFEC00, 0xBFFFECEC)              = -1 Err#2
write_nocancel(0x2, "Error opening terminal: vt100.\n\0", 0x24)            = 36 0

You can see, that ncurses is looking for a file vt100 in a terminfo directory and fails looking at /usr/local/share/terminfo. I indeed had no directory /usr/local/share/terminfo but a directory /usr/share/terminfo. I was not able to figure out, where i could specify the correct directory configuration for this, so i just created a symlink: sudo ln -snf /usr/share/terminfo /usr/local/share/terminfo. After creating the symlink, i was able to execute my example ncurses application.



OSX 10.5.x + PHP + pecl_newt


I always wanted to provide nice user interfaces for some of my commandline tools written in PHP, but was not able to solve one problem until recently. The big problem when writing user interfaces for commandline utilities written in PHP is: ... what library to use for actually building the user interface?

  • You could try to write your own library using ANSI escape sequences -- but your user interfaces would either be very limited or it would be a hell of work to write an extensive library providing more than just the basics.

  • You could try to get the ncurses extension to work -- i failed.

  • You could try to get the newt extension to work -- i failed ...

... until recently. Every once in a while i tried to dig up more information of how to get newt to work, because this is the library i would prefer over the other solutions. However, it did not compile on my system. Recently i searched again and was able to dig up a patch for newt-0.5.22.11. So, here are the steps to get things work:

  1. Download newt 0.52.11 and extract it.

  2. Download the patch for newt from the page above and apply it as described in the short tutorial provided on that page.

  3. Download slang, which is required by newt. I decided to download the latest snapshot (pre2.2.3-60) and things worked just fine with it. Extract and build it -- i did not use any special flags for this.

  4. Download popt, which is required for newt. I decided to download popt-1.16, which seems to be the latest release and can be found at the bottom of the download page. Extract and build it -- i did not use any special flags for this.

  5. After installing slang and popt and patching newt, you should now be able to build and install newt.

  6. Download pecl_newt, or install it using pecl install .... Don't forget to add newt.so to your php.ini.

pecl_newt provides some examples which should be executed to verify, that installation succeeded.



Setting up Master-Slave replication using xtrabackup


In a previous blog entry i described a method of how to setup master-slave replication with mysql. In steps #4 and #5 i used mysqldump and mysql-client for creating a database dump on the master and importing it on the slave. The problem with this approach is, that the database tables are locked, as long as the dump is running. For small databases this might not be a problem, but as data grows, the time to create a dump takes longer and longer. @work we apparently reached some critical level -- mysqldump ran hours and hours and would probably still run, if i had not stopped it already.

Luckily there are more suitable tools for large databases available. Innodb hot backup and xtrabackup. I've decided to go with xtrabackup, because it's open-source, free and actively developed. Innodb hot backup is closed-source and not for free ;-).

The following steps are ment to replace steps #4 and #5 of my previous blog post.

1. building xtrabackup

For Linux i had to build xtrabackup from the source package, because there was no binary package available for my architecture -- it's very easy, though:

harald@master:~/xtrabackup-0.9.5rc$ automake -a -c
...
harald@master:~/xtrabackup-0.9.5rc$ ./configure
...
harald@master:~/xtrabackup-0.9.5rc$ make
...
harald@master:~/xtrabackup-0.9.5rc$ cd innobase/xtrabackup
harald@master:~/xtrabackup-0.9.5rc/innobase/xtrabackup$ make
...
harald@master:~/xtrabackup-0.9.5rc/innobase/xtrabackup$ sudo cp \
    innobackupex-1.5.1 /usr/local/bin
harald@master:~/xtrabackup-0.9.5rc/innobase/xtrabackup$ sudo cp \ 
    xtrabackup /usr/local/bin

Needless to say, that xtrabackup needs to be deployed on every database server.

2. creating a database dump

After successfully building and installing xtrabackup, taking a database dump is very easy:

root@master:~# innobackupex-1.5.1 --user=... --password=... \
    --defaults-file=... --databases="..." .

The command innobackupex-1.5.1 takes the following parameters:

--user
username to use for database connection
--password
password to use for database connection
--defaults-file
this parameter is required, if the my.cnf configuration file is not located at /etc/my.cnf
--dabases
space-separated list of databases to backup
.
destination directory to save dump to

Dumping the database with xtrabackup is incredible fast compared to mysqldump. With xtrabackup it's just a matter of minutes:

real    4m15.614s
user    0m11.710s
sys     0m14.960s

If xtrabackup was successful, it should have created a subdirectory which name is the current date/time, with all required files in it. The directory can now be copied to the slave:

root@master:~# scp -r 2010-03-02_15-02-24 root@xx.xx.xx.xx:~

3. Setting up the slave

The first thing to do on the slave is applying the binary log files to the database dump:

root@dbslave1:~# innobackupex-1.5.1 --apply-log 2010-03-02_15-02-24

...

100302 14:29:56  innobackupex: innobackup completed OK!

Innobackupex will show the message above, if everything was OK. Next task is to copy the database dump to it's new location on the slave. innobackupex is doing everything for you:

root@dbslave1:~# innobackupex-1.5.1 --copy-back 2010-03-02_15-02-24

...

100302 14:29:56  innobackupex: innobackup completed OK!

xtrabackup should now have copied the dump to the mysql data directory. It's a good idea to check the user and owner of the copied files and adjust them, when needed.

Last step is to start the replication. All information required to do so ist stored in the file xtrabackup_binlog_info:

root@dbslave1:~# cat 2010-03-02_15-02-24/xtrabackup_binlog_info
mysql-bin.000331	54249842

With this information available the replication can be set up as described in step #6 of my previous blog post.



MySQL master / slave replication


There are tons of tutorials about setting up master / slave replication for MySQL. Here are my own quick notes:

1. Master: /etc/mysql/my.cnf

[mysqld]
server-id = 1
log_bin   = /var/log/mysql/mysql-bin.log
expire_logs_days = 1
max_binlog_size  = 100M

2. Slave: /etc/mysql/my.cnf

[mysqld]
server-id = 2
log_bin   = /var/log/mysql/mysql-bin.log
expire_logs_days = 10
max_binlog_size  = 100M

3. Master: granting privileges for slave user on database master

GRANT REPLICATION SLAVE ON . 
TO '<slave_username>'@'<slave_ip>' 
IDENTIFIED BY '<slave_password>';

4. Master: creating database dump

Start mysql console as database root and enter the following command:

FLUSH TABLES WITH READ LOCK;

DON'T shut down the mysql client, otherwise the table lock is lost. Open a second shell to the database master and enter the following command on commandline:

mysqldump -u root -p... --databases ... --opt > masterdump.sql

Next, switch back to your mysql console and enter the following command:

SHOW MASTER STATUS;

The output will look something like:

mysql> show master status;
+------------------+----------+--------------+------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000004 | 40140874 |              |                  | 
+------------------+----------+--------------+------------------+
1 row in set (0.00 sec)

mysql> 

Write down "File" and "Position" ... you will need it later for starting replication.

Now you can unlock the tables:

UNLOCK TABLES;

5. Slave: import database dump

Copy masterdump.sql to the slave server and import the database:

mysql -u root -p... < masterdump.sql

This may take quite some time ...

6. Slave: start replication

Start mysql client on slave and enter the following commands:

CHANGE MASTER TO 
MASTER_HOST='<master_host>', 
MASTER_USER='<slave_username>', 
MASTER_PASSWORD='<slave_password>', 
MASTER_LOG_FILE='<mysql-bin file name you've written down in step 4>', 
MASTER_LOG_POS=<master position you've written down in step 4>;

START SLAVE;


cookie based redirect mit nginx


Vor einiger Zeit habe ich einen Tip beschrieben, wie man einen cookie-based redirect mit dem LightTPD konfigurieren kann. Das Problem an der Sache ist für mich, dass das für diesen Zweck verwendete Modul mod_proxy von LightTPD in dem von mir eingesetzen Entwicklungszweig (1.4.x) nicht SSL kompatibel ist und dementsprechend HTTPS Verbindungen fehlschlagen.

Seit einiger Zeit schon habe ich den Web- und (Reverse-) Proxy-Server nginx im Auge. Auch mit diesem Server ist es möglich einen Redirect einzurichten, so wie ich ihn brauche. Und: nginx unterstützt an dieser Stelle SSL!


server {
    listen          80;
    server_name     *.devcenter.int;

    proxy_redirect      off;
    proxy_set_header    Host            $host;
    proxy_set_header    X-Real-IP       $remote_addr;
    proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;

    location / {
        if ($http_cookie ~ "(; )?devredirect=harald") {
            proxy_pass              http://10.0.0.20;
            break;
        }
        if ($http_cookie ~ "(; )?devredirect=markus") {
            proxy_pass              http://10.0.0.22;
            break;
        }

        ...
    }
}


server {
    listen          443;
    server_name     ....devcenter.int;

    ssl                     on;
    ssl_certificate         /etc/nginx/....crt;
    ssl_certificate_key     /etc/nginx/....key;

    proxy_redirect      off;
    proxy_set_header    Host            $host;
    proxy_set_header    X-Real-IP       $remote_addr;
    proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;

    location / {
        if ($http_cookie ~ "(; )?devredirect=harald") {
            proxy_pass              https://10.0.0.20;
            break;
        }
        if ($http_cookie ~ "(; )?devredirect=markus") {
            proxy_pass              https://10.0.0.22;
            break;
        }

        ...
    }
}



ORMs sind doof


ORM

Wikipedia sagt dazu:

Objektrelationale Abbildung (englisch object-relational mapping, ORM) ist eine Technik der Softwareentwicklung, mit der ein in einer objektorientierten Programmiersprache geschriebenes Anwendungsprogramm seine Objekte in einer relationalen Datenbank ablegen kann. Dem Programm erscheint die Datenbank dann als objektorientierte Datenbank, was die Programmierung erleichtert. [...]

Inzwischen bringt ja so ziemlich jedes PHP Framework seine eigene ORM Implementierung mit, es gibt aber auch einige Framework-unabhängige ORM Implementierungen. Ich habe mir in den letzten Jahren immer mal wieder verschiedenste ORM Implementierungen angesehen -- immer dann, wenn in mir der Wunsch nach einer objektorientierten Zugriffsweise auf meine Datenbanken aufkam. Leider jedoch konnte mich bisher keine ORM Implementierung überzeugen.

ORMs sind doof

Auch in mir kommt immer mal wieder der Wunsch auf objektorientiert auf meine Datenbanken zuzugreifen, da dies den Zugriff auf einzelne Datensätze -- Objekte -- erheblich vereinfacht. Jedoch -- zu welchem Preis wird diese Vereinfachung erkauft?

Modellierung

Ich modelliere meine Datenbanken schon seit Jahren mit dem ER Modeller dbWrench. Das ist meiner Meinung nach super komfortabel. Ich sehe auf einen Blick all meine Tabellen und die Abhängigkeiten bzw. Verknüpfungen zwischen einzelnen Tabellen. Über die Funktion "Forward Engineering" kann dbWrench mein Datenbankschema in der Datenbank immer aktualisieren. Da ich bei MySQL den Tabellentyp InnoDB verwende, sind auch in der Datenbank sämtliche Verknüpfungen festgehalten und liessen sich z.b. über die INFORMATION_SCHEMA Tabelle leicht auslesen.

Nun ist es leider so, dass offenbar so ziemlich jede ORM Implementierung die Datenbankdefinition auf Ihre Weise bekommen möchte. Da muss man entweder seitenweise XML oder YAML Konfiguration, oder gar ellenlangen PHP Code schreiben -- nur um der Anwendung eine Information bekannt zu geben, die eigentlich exakt so schon in der Datenbank vorhanden ist?

Abstraktion

Wie weit muss man die Datenbankzugriffe abstrahieren? Nun, es gibt da sicherlich die verschiedensten Anforderungen. Ich denke bei der Entwicklung von Unternehmenssoftware kann man die Anforderungen ziemlich genau spezifizieren. Man entscheidet sich zu einem gewissen Zeitpunkt für ein bestimmtes Datenbankprodukt. Normalerweise wird diese Entscheidung nicht nach wenigen Monaten oder Jahren über den Haufen geworfen -- es sei denn es gibt sehr triftige Gründe dafür.

Deshalb bin ich der Meinung, dass die Abstraktion nicht so weit gehen muss, dass sämtliche Datenbankzugriffe abstrahiert werden und für beliebige Datenbanksysteme geeignet sind. Im Gegenteil: ich entscheide mich ja nicht für eine bestimmte Datenbank nur aus Kostengründen, sondern auch, weil diese vielleicht Features mitbringt, die ein anderes Datenbanksystem nicht unterstützt.

So erweitert z.b. MySQL den SQL Standard um eigene spezifische Befehle, die es in anderen Datenbanken nicht gibt, die aber sehr praktisch sind. Das ist kein Alleinstellungsmerkmal von MySQL. Beispiel: Hätte ich mich für Oracle entschieden, wäre ich doch dumm, würde ich zum Abbilden / Abfragen von Hierarchischen Strukturen nicht CONNECT BY verwenden -- nur weil dies nicht Teil des SQL Standards ist und dies so mit keiner anderen Datenbank funktioniert.

Nur: keine ORM Implementierung kann auf diese einzelnen Datenbankfeatures eingehen -- womit ich beim nächsten Punkt angelangt wäre.

Abfrage

Das grösste Manko aller (PHP) ORM Implementierungen ist meiner Meinung nach die Abfrage einer Datenbank. Ich gebe zu: ich mag SQL -- es gibt mir das passende Werkzeug zum Abfragen einer relationalen Datenbank in die Hand -- es wurde zu diesem Zweck entwickelt! Ich schreibe gern SQL, da es strukturiert und übersichtlich ausschaut und mich schnell zum Ziel führt. Ich gebe weiterhin zu: Ich nutze auch gern MySQL spezifische SQL Features -- aus den oben genannten Gründen.

Nun ist es jedoch so, dass die ORM Implementierungen in der Regel den Zugriff soweit abstrahieren, dass -- normalerweise -- kein SQL mehr geschrieben wird. CONNECT BY und ähnliche Dinge wären damit Unmöglich. Heutzutage hat sich folgende Schreibweise zum Erstellen von Datenbankabfragen etabliert:

$dbo
    ->select(array(
        'media.media_id', 'media.media_name', 'member.member_name', ...
    ))
    ->from('media')
    ->join('member', 'member.member_id = media.member_id')
    ->where('media.category_id = ?')
    ->order('media.media_id')
...

Ich bin kein Fan einer solchen Schreibweise:

  1. Es ist kein SQL ;-)
  2. Es ist wesentlich mehr Aufwand als beim Schreiben von SQL erforderlich
  3. Ich kann keine Datenbankspezifischen SQL Erweiterungen verwenden
  4. Ich habe keine Kontrolle darüber, welchen SQL Code die ORM Implementierung daraus generiert
  5. Es liegt in der Natur der Sache, dass ein derartiges Konstrukt niemals auch nur annähernd so performant sein kann wie ein simples SQL Statement übergeben an die Datenbank
  6. Ich kann das Statement nicht per Copy / Paste zwischen meinem Datenbank Client und der Anwendung hin und her kopieren -- praktisch, wenn man das ganze erstmal testen will
  7. Wenn ich den Datenbanktreiber einer nicht-relationalen Datenbank hinterlege, weil ich mich z.b. entscheide statt MySQL MongoDB anzusprechen, wird diese Schreibweise ohnehin ad Absurdum geführt. (Nur als beispiel -- ich weiss nicht, ob irgendeine PHP ORM Implementation überhaupt nicht-relationale Datenbanken unterstützt)

Natürlich bietet so ziemlich jede ORM Implementierung einen Fallback zur Herkömmlichen Absetzung von SQL Anfragen ohne ein Objekt-Mapping. Nur, wenn ich damit an einer Stelle in meiner Anwendung anfange: warum dann überhaupt eine derartige Abstraktion nutzen?

Fazit

Meiner Ansicht nach erkauft man sich den konsequenten Einsatz eines ORM zu einem zu hohen Preis. Deshalb habe ich den Einsatz eines solchen für mich immer wieder verworfen. Mein Wunsch wäre ein SQL -> Objektmapper. D.h.: Ich schreibe SQL, zurück bekomme ich Objekte, mit denen ich weiterarbeiten kann ...



MongoDB + PHP + OSX


Ich spiele gerade ein wenig mit Mongo DB. Da ich das die Tage sicherlich noch mal gebrauchen kann, mache ich mir hier mal ein paar wichtige Notizen zur Installation:

  • Der Aktuelle PHP Treiber ist nicht kompatibel zum aktuellen stable Release (0.8.0) auf der Mongo DB Seite. Deshalb muss ein daily-build von Mongo DB heruntergeladen werden. Am besten lädt man hier das Paket inkl. aller Tools und Treiber (beinhaltet auch den Source der PHP Erweiterung). Mongo DB also herunterladen und irgendwohin installieren (z.b. /opt/mongo).
  • Zum Compilieren des PHP Treibers benötigt man die Boost C++ Libraries. Beim Compilieren der Boost Libraries wurden die Namen der Bibliotheken derart angelegt, dass sie beim Compilieren des PHP Treibers von Mongo DB nicht gefunden werden konnten. Deshalb habe ich nach dem ./configure --prefix=/opt/boost von Boost die Datei Makefile editiert und die Zeile BJAM_CONFIG= nach BJAM_CONFIG='--layout=system' geändert.
  • Nachdem Boost Installiert wurde, lässt sich nun der Mongo DB Treiber compilieren. Bei mir befindet sich der PHP Treiber unterhalb des Verzeichnisses /opt/mongo/drivers_and_tools/mongo-php-driver. Ein phpize, ./configure --with-mongodb=/opt/mongo/ --with-boost=/opt/boost/, make, make install und der Treiber ist gebaut und installiert.

Eigentlich ganz einfach ... aber ich vergesse es sonst garantiert wieder :-)



cookie based redirect mit lighttpd


Alle unsere Entwickler haben eine komplett eigene Entwicklungsumgebung inkl. WebServer installiert. Manchmal ist es praktisch den aktuellen Entwicklungsstand am Checkout eines Entwicklers zu sehen. Anstatt für jedes Projekt und jeden Entwickler eine interne Subdomain einzurichten, kann man viel praktischer einen zentralen Reverse Proxy nutzen, der anhand einer Cookie Einstellung den Host des jeweiligen Entwicklers auswählt:

$SERVER["socket"] == "10.0.0.20:80" {
    ...
    $HTTP["host"] =~ "^.*\.devcenter.int$" {
        $HTTP["cookie"] =~ "(; )?devredirect=harald" {
            proxy.server = (
                "" => (("host" => "10.0.0.20", "port" => 8080))
            )
        }
        $HTTP["cookie"] =~ "(; )?devredirect=markus" {
            proxy.server = (
                "" => (("host" => "10.0.0.22", "port" => 8080))
            )
        }
    }
}    

Die oben gezeigte Konfiguration installiert einen Reverse Proxy für Subdomains von 'devcenter.int' -- z.B. 'clipdealer.devcenter.int'. Über das Cookie 'devredirect' kann spezifiert werden, welcher Host angesprochen werden soll.



gearman job queue server


Die letzten Wochen habe ich für die Arbeit einen Workflow Prozessor entwickelt, über den wir zukünftig unsere Medien Uploads verarbeiten werden. Unter anderem gehört zu seinen Aufgaben die Erzeugung von Still Images aus Videos, Wave- und Spektrum-Analyse aus Audiodateien, Erzeugung von Thumbnails und Previews für die Darstellung auf der Web Seite. Der Workflow Prozessor arbeitet von der Console bzw. als Hintergrundprozess (Dämon) und ist in PHP entwickelt. Wird er auf einem Server gestartet, kann der Hauptprozess beliebig viele Worker-Prozesse forken.

Ein Problem bei der Entwicklung war die Verteilung der Prozesse an die Worker. Hier habe ich zunächst auch auf eine reine PHP basierte Lösung gesetzt und die Verteilung der Aufgaben vom Hauptprozess übernehmen lassen, der über Socket-Pairs mit den Worker-Prozessen kommunizieren kann. Bei Tests während der Entwicklung im kleinen Rahmen und mit wenigen Jobs hat das auch wunderbar funktioniert. Bei umfangreicheren Tests traten dann allerdings die Probleme zu Tage.

Einmal abgesehen davon, dass die ganze Sache nicht wirklich der Performance-bringer war, war sie dank der direkten Kommunikation Client->Worker auch fehleranfällig und schlecht bzw. gar nicht skalierbar, denn wie soll man mit dieser Methode mehrere bzw. beliebige Worker-Server transparent ansprechen?

Zum Glück habe ich schon seit einiger Zeit gearman in meinen Lesezeichen. Gearman ist eine Serversoftware, die -- einfach ausgedrückt -- den Zweck erfüllt Jobs zu verteilen. Mögliche Einsatzgebiete sind z.b. die Verteilung von Jobs an Systeme, die besser für die jeweilige Aufgabe geeignet sind, die Lastverteilung auf beliebige Server oder um Jobs parallel abarbeiten zu können. Hierfür wird seitens gearman eine (persistente) Warteschlange implementiert, an die ein Client einen Job senden kann. Ein Worker kann sich einen Job aus der Warteschlange holen und diesen abarbeiten.

Gearman wurde ursprünglich von Brian Fitzpatrick (livejournal, memcached, ...) in Perl entwickelt und vor einiger Zeit von Brian Aker und Eric Day nach C portiert. Der Entwicklungsstand des C Zweiges von gearman ist seiner Version (0.3) zu schliessen noch relativ jung. Insgesamt aber scheint sich das ganze schon derart bewährt zu haben, dass gearman produktiv und unter hoher Last z.b. von digg und yahoo eingesetzt wird.

Zum erfolgreichen Kompilieren von gearman unter OSX muss sichergestellt sein, dass man eine aktuelle Version der libevent Bibliothek installiert hat. Danach lässt sich auch gearman problemlos unter OSX bauen und installieren. Sowohl für libevent wie auch für gearman funktioniert das wie üblich:

$ ./configure
$ make
$ make install

Anschliessend kann man gearman starten. Der Parameter "-d" bewirkt dabei, dass gearman als Hintergrundprozess losgelöst vom Terminal gestartet wird:

/usr/local/bin/gearman -d

Per default lauscht gearman auf Port 4370. Dies ist der offizielle Port, der von der IANA für gearman reserviert wurde.

Client/Worker APIs zur Kommunikation mit gearman gibt es bereits für C (enthalten im gearman Download), Perl, MySQL (als UDF) und PHP. Zum Ansprechen von gearman unter PHP gibt es ein pear package, net_gearman, sowie seit kurzem auch eine native Erweiterung für PHP, die allerdings noch experimentell ist und dementsprechend mit Vorsicht zu geniessen ist. Unter OSX konnte ich sie z.b. zwar einwandfrei kompilieren und installieren, die ersten Versuche scheiterten dann aber an segfaults. Nicht weiter tragisch, da das pear paket bisher einwandfrei seine Arbeit macht.

Eine Client Anwendung, die Jobs an den gearman Server übergibt, ist mit dem pear Paket schnell entwickelt:

#!/usr/bin/env php
<?php

require_once('Net/Gearman/Client.php');

$set = new Net_Gearman_Set();

function result($resp) {
    print_r($resp);
}

$files = array(
    '/Users/harald/Movies/00001.mov'
);

foreach ($files as $file) {
    print "adding task $file\n";
    
    $task = new Net_Gearman_Task('test', array(
        'filename'  => $file
    ));

//    $task->attachCallback('result');
    $set->addTask($task);
}

print "running set ...\n";

$client = new Net_Gearman_Client(array('127.0.0.1:4730'));
$client->runSet($set);

?>

Der erste Parameter beim Instanzieren der Klasse Net_Gearman_Task spezifiziert übrigens den Namen des Jobs, der vom Worker registriert werden muss, damit dieser entsprechende Jobs annehmen und verarbeiten kann. Normalerweise wartet der Client, bis alle Jobs abgearbeitet sind und kann wenn eine Callback Methode registriert wurde, mögliche Ergebnisse verarbeiten, die die Worker zurückliefern. Dieses Verhalten kann möglicherweise unerwünscht sein, z.b. wenn viele Jobs eingereicht werden und der Client während der Abarbeitung nicht blockiert sein soll. Hierfür implementiert die Net_Gearman_Task Klasse ein öffentliches Property:

public $type = self::JOB_NORMAL;

Um das Verhalten zu ändern, sind folgende Typen definiert:

  • Net_Gearman_Task::JOB_NORMAL -- Standardverhalten: Client blockiert während der Abarbeitung der Jobs
  • Net_Gearman_Task::JOB_BACKGROUND -- Client reicht Jobs an gearman weiter und blockiert nicht
  • Net_Gearman_Task::JOB_HIGH -- selbes Verhalten wie JOB_NORMAL, allerdings wird die queue übersprungen und ein Job kann sofort von einem Worker verarbeitet werden. Sinnvoll z.b. für höher priorisierte Jobs

Eine einfacher Klasse, die die Aufgabe 'test' implementiert, könnte folgendermassen aussehen:

<?php

class test extends Net_Gearman_Job_Common {
    function run($args) {
        print_r($args);
        
        print "sleeping 5 seconds ...\n";
        sleep(5);
        print "done!\n";
        
        return "OK!";
    }
}

?>

Der Worker selbst wird folgendermassen aufgebaut:

#!/usr/bin/env php
<?php

define('NET_GEARMAN_JOB_CLASS_PREFIX', '');
define('NET_GEARMAN_JOB_PATH', '.');

require_once('Net/Gearman/Worker.php');

$worker = new Net_Gearman_Worker(array('127.0.0.1:4730'));
$worker->addAbility('test');
$worker->beginWork();

?>

Normalerweise erwartet Net_Gearman Klassen, die Aufgaben implementieren unterhalb eines Verzeichnisses, das in der PEAR Verzeichnisstruktur angesiedelt ist. Weil man dies normalerweise nicht möchte, gibt es folgende Konstanten, die man mit eigenen Werten füllen kann um so alternative Verzeichnisse zu setzen:

// spezifiziert ein Prefix für den Namen der Klasse
define('NET_GEARMAN_JOB_CLASS_PREFIX', '');

// spezifiziert das Verzeichnis, in dem Klassen gesucht werden
define('NET_GEARMAN_JOB_PATH', '');

Unschön ist, dass für einzubindende Klassen Dateien mit folgendem Namensschema erwartet werden, z.b: test.php. Da ich meine Klassen immer z.B. -- test.class.php -- benenne, ist dies also leider eine kleine Unschönheit, mit der man offenbar leben muss.

Zum Abschluss sei noch zu sagen, dass gearman in den ersten Tests mit mehreren 1000 Jobs in der Queue seine Arbeit tadellos und sehr zufriedenstellend erledigt hat.

Empfohlene weiterführende Links:



JavaScript: The World's Most Misunderstood Programming Language


Eigentlich sollte ich stolz sein: es gibt tatsächlich Leser meines Blogs, die meine Artikel aufgreifen und sich darüber kritische(?) Gedanken machen. Gestern bin ich zufälligerweise auf einen Artikel in einem Blog gestossen, in dem sich der Autor über meine Einstellung zu JavaScript lustig macht. Bei all seinem Sarkasmus vergisst der Autor leider Argumente vorzubringen warum er meine Einstellung so lächerlich findet. Die im Text eingestreuten Klischees, die man ähnlich so eigentlich nur aus dem Heise-Forum kennt, können es kaum sein.

Dabei kann ich die Einstellung tatsächlich sogar verstehen: In JavaScript lässt es sich ähnlich schmutzig und disziplinlos programmieren, wie mit PHP. Nur: dort wo PHP einem Steine in den Weg legt, weil der Parser viele dinge einfach nicht zulässt, ist JavaScript in der Regel offen und man kann einen sehr sauberen und eleganten Programmierstil fahren. Nun ist es bei JavaScript so wie mit jeder Programmiersprache: man muss sich damit beschäftigen. Was PHP und JavaScript darüberhinaus gemein haben ist, dass es für beide Sprachen wenig gute Literatur, dafür aber umso mehr schlechte Literatur gibt.

Bei ernsthaftem Interesse an JavaScript möchte ich an dieser Stelle auf einen Artikel von Douglas Crockford verweisen -- JavaScript: The World's Most Misunderstood Programming Language. Dieser Artikel räumt mit einigen Vorurteilen gegenüber JavaScript auf und macht vielleicht auch verständlich, warum JavaScript einen derart schlechten Ruf hat.



multiple ssh


Heute wollte ich mal den "slave status" unserer 5 Datenbank Server bei Pixelio ansehen. Natürlich ist es schrecklich umständlich sich auf allen Servern einzeln anzumelden um immer und immer wieder den gleichen Befehl auszuführen. Hierfür gibt es Tools, die ein Kommando auf mehreren Servern ausführen können (sollen). Ausprobiert habe ich pssh und dsh. Jedoch, mit beiden bin ich nicht klar gekommen. Da ich wie gesagt nur eben schnell mal einen Befehl auf den Slaves ausführen wollte, habe ich auch keine Lust gehabt, mich intensiv mit den Tools zu beschäftigen:

pssh ist python basiert, schon der erste Minuspunkt -- aber gut, da OSX eh python mitbringt, hab ich's halt installiert und konfiguriert. Allerdings wollte pssh keine Ausgabe zurück liefern und nach dem Ausführen hatte ich plötzlich jede menge SSH prozesse laufen, die ich manuell killen musste.

dsh hat imo eine mehr als rudimentäre Dokumentation. Nach Ausgiebigem googeln wusste ich dann auch, dass man offenbar eine Liste der Server benötigt, mit denen man sich Verbinden will und ausserdem noch Servergruppen anlegen kann. Klingt gut. Allerdings kann dsh offenbar meine "~/.ssh/config" nicht lesen und kennt daher auch meine hosts nicht. Da ich auf die Schnelle nicht in Erfahrung bringen konnte, wie ich in der Serverliste den Port spezifiziere (mit ":" ging es jedenfalls nicht), habe ich das also auch sein lassen.

In der Zwischenzeit hätte ich sicherlich auch 5x manuell den slave status ausführen können -- aber gut, man ist ja faul. Nachdem ich also pssh und dsh von meiner festplatte verbannt hatte, habe ich mir dann in ca. 1 Minute selbst ein kleines Shell-Script zusammengezimmert, das zwar bei weitem nicht die Features von pssh und dsh bietet, aber dafür für meine Zwecke problemlos funktioniert:

#!/usr/bin/env sh
SERVERS=`cat ~/.dsh/$1`

for i in $SERVERS
do
        echo $i
        ssh $i $2
done

Das Script habe ich unter "/usr/local/bin/dsh" abgelegt und ist damit in meinem Pfad erreichbar. In der Datei "~/.dsh/dbslaves" habe ich mir eine Liste meiner Slaves angelegt:

slave-1
slave-2
slave-3
slave-4
slave-5

Diese Hosts habe ich ohnehin schon in meiner "~/.ssh/config" drin. Ausserdem sind auf den Servern meine Keys für passwortloses Login installiert. Nun kann ich über:

dsh dbslaves "mysql -uroot -p... -e 'SHOW SLAVE STATUS\G'"

bequem meinen Slave Status abfragen oder andere Kommandos ausführen.



Neue Serverlandschaft bei pixelio.de


In den vergangenen fünf Jahren ist pixelio.de aus einem Hobbyprojekt eine kleine Firma geworden und aus ehemals einem Server ist eine Serverlandschaft mit 10 Servern und einem Loadbalancer entstanden. Die letzten Wochen haben wir endlich unser Netzwerk umstrukturiert und auf eine solidere Basis gestellt, sodass wir es zukünftig leichter haben weitere Server aufzunehmen. Dabei muss man mal ein Lob an HostEurope aussprechen, die im Service und Support eine wirklich gute Arbeit leisten -- es hat schon einen Vorteil, wenn man direkt mit Technikern am Telefon reden kann im Vergleich zu anderen Providern, wo das nur über einen Call-Center möglich ist und das ganze einem Stille-Post ähnelt (und die Ergebnisse entsprechend ausfallen).

Wir nutzen nachwievor eine alte Software, die eigentlich nicht für eine derart grosse Bilddatenbank ausgelegt ist und ausgesprochen schlecht skaliert. Um das Datenbankproblem in den Griff zu kriegen, setzen wir seit einiger Zeit schon den mysql-proxy von Jan Kneschke ein. Dieser Proxy kann transparent zwischen die Datenbank und eine Web-Anwendung gehängt werden und lässt sich über lua voll steuern. Über ein lua Script können wir so alle Queries parsen, der Proxy entscheidet dann welche Queries an den Master gehen müssen und welche an die Slaves. Das funktioniert sehr gut -- viel besser als eine reine PHP basierte Lösung, die wir ursprünglich mal in die Datenbankschicht der Bilddatenbank-Software gestrickt hatten.

Für nächstes Jahr ist endlich die Einführung einer von Grund auf neu entwickelten Software angedacht, die wir derzeit schon bei clipdealer.de im Einsatz haben. In diesem Zuge werden wir dann auch einen memcache-Cluster aufbauen, der die Datenbank massiv entlasten soll.

Ich bin gespannt, wie das Server-Diagramm in weiteren fünf Jahren aussehen wird ...



Ich liebe JavaScript


Ich gestehe: ich liebe JavaScript -- ich habe noch keine Programmiersprache kennengelernt, in der es sich ähnlich intuitiv entwickeln lässt. Die Sprache ist absolut logisch aufgebaut, es gibt eigentlich nie aha-Effekte, dass etwas unerwarteterweise nicht möglich ist. Ähnlichen Spass am Programmieren hatte ich zuletzt mit Turbo Pascal vor vielen Jahren. Sollte es irgendwann eine ernstzunehmende serverseitige JavaScript Implementation geben, die sich über fastCGI in meinen bevorzugten Webserver einhängen lässt, werde ich (wahrscheinlich :-)) nicht zögern PHP hinter mir zu lassen um auch serverseitig JavaScript einzusetzen.

Mit der neuen JavaScript Engine von Google -- v8 -- die erstmals im Google Browser Chrome zum Einsatz kam, ist der Traum vielleicht irgendwann keiner mehr. Da die JavaScript Engine im Quellcode erhältlich ist und die Bibliothek sich ohne Probleme unter den wichtigsten Betriebssystemen compilieren lässt, sind schon zahlreiche Projekte entstanden, die v8 auch serverseitig einsetzbar machen sollen.

Im Moment spiele ich hier ein wenig mit llv8call -- eine Bibliothek, die v8 um Zahlreiche Funktionen erweitert, wie z.B.:

  • File und Dir
  • System Variablen
  • STDIN, STDOUT und STDERR
  • Process Management / Prozess Kontroll Funktionen
  • Dynamic Library Loader
  • Sockets
  • libxml Support

... und ausserdem noch folgende Erweiterungen mitbringt:

  • memcached
  • fastCGI Support
  • sqlite 3 Support
  • Curl

Auch eine Verbesserte Shell (im Vergleich zur mit v8 Verfügbaren Shell) ist vorhanden, die nun Dank libreadline mehr Komfort mitbringt (z.b. die Befehlshistorie).

Zur Installation unter OSX sind evtl. folgende Tools und Bibliotheken notwendig:

Sofern man die Entwicklungstools von OSX installiert hat, lassen sich die genannten Bibliotheken ohne weiteres installieren. Lediglich bei ClearSilver musste ich den Ruby-Support deaktivieren:

./configure --disable-ruby

... und nach der Installation einen Symlink erstellen, damit llv8call beim Compilieren die Header-Dateien von ClearSilver einbinden konnte:

ln -snf /usr/local/include/ClearSilver /usr/include/ClearSilver

Wenn scons ohne Fehler durchgelaufen ist, kann man die erzeugten Binaries unter ./out/ im llv8call Quellverzeichnis finden.



php + ncurses + osx


Benötige ich demnächst bestimmt noch 1-2x, deshalb hier auf alle Ewigkeit festgehalten. Zunächst ncurses saugen und über ...

./configure --prefix=/usr/local --with-shared --disable-rpath --without-debug --without-ada --enable-safe-sprintf --enable-sigwinch --without-progs
make
make install

... konfigurieren und installieren. Anschliessend kann man ncurses über pecl installieren:

pecl install ncurses

... und mit der ncurses Entwicklung in Verbindung mit php loslegen.

p.s.: ich bin kein Fan von "(darwin)ports" oder "fink", aber manchmal sind sie ganz nützlich um sich die richtigen Konfigurationsschalter herauszusuchen: http://ncurses.darwinports.com/