Posts Tagged ‘MySQL’

Montag, 16. Mai 2016

MariaDB (MySQL) meldet “ERROR 1436 (HY000) at line 574: Thread stack overrun”

Damit die Migration von MySQL nach MariaDB sauber abläuft, ist es wichtig, nach der De-Installation von MySQL und der erfolgreichen Installation von MariaDB folgendes Kommando auszuführen (Anstoss war eine Fehlermeldung in mysql.log):

# mysql_upgrade
...
ERROR 1436 (HY000) at line 574: Thread stack overrun:  5904 bytes used of a 131072 byte stack, and 0 bytes needed.  Use 'mysqld --thread_stack=#' to specify a bigger stack.

Blöd nur, wenn diese Fehlermeldung erscheint. Nach einer kurzen Google-Suche stellte sich heraus, dass mein Konfigurationstuning in /etc/mysql/my.cnf einen Kollateralschaden verursacht hatte. In der Konfigurationsdatei hatte ich nämlich eingestellt:

...
[mysqld]
...
thread_stack		= 128K

Dieser Wert berechnet sich für jedes System basierend auf dessen Eigenschaften und es macht deshalb keinen Sinn, den Wert in my.cnf hartzukodieren, wie der Artikel MySQL error 1436: Thread stack overrun, with simple query aufzeigt.

Das Problem löste sich in Luft auf, indem ich den Eintrag auskommentierte …

#thread_stack		= 128K

… und den Datenbankserver neu startete:

# systemctl restart mysql

Tags: , , , , , , ,
Labels: Linux

Keine Kommentare | neuen Kommentar verfassen

Montag, 16. Mai 2016

MariaDB (MySQL) meldet Errcode: 24 “Too many open files”

Vorgestern habe ich auf Grund von Performance-Problemen beim Umstieg auf cacti spine die Datenbank auf meinem Linux-Server ausgetauscht: Statt MySQL kommt nun MariaDB zum Einsatz.

Heute nun wurde ich auf einen Kollateralschaden aufmerksam. Jede Nacht lasse ich ein Script laufen, welches meine MySQL-Datenbanken sperrt (LOCK-ed) und die Datenbankdateien in ein Sicherungsverzeichnis kopiert. Mit tarsnap schreibe ich die Datenbankdateien dann weg in die Cloud und habe so versionierte Datenbankbackups.

Seit dem Wechsel und einigen Anpassungen an der MySQL-Konfiguration (my.cnf) traten bei der Verwendung des Scripts Probleme auf:

...
site_wedding
     /home/mysql/site_wedding.sql
mysqldump: Error: 'Out of resources when opening file '/tmp/#sql_4fde_2.MAD' (Errcode: 24 "Too many open files")' when trying to dump tablespaces
mysqldump: Got error: 23: "Out of resources when opening file './site_wedding/log.MYD' (Errcode: 24 "Too many open files")" when using LOCK TABLES
...

Und auch im mysql.log las ich:

...
May 16 06:30:02 ALPHA mysqld: 160516  6:30:02 [ERROR] Error in accept: Too many open files

Den Fehler behob ich mit folgenden Massnahmen:

Im Zuge des Performance-Tunings der Datenbank (für cacti spine) hatte ich folgende Zeilen in my.cnf eingefügt:

...
[mysqld]
...
open_files_limit	= 80000
...

Beim Starten der Datenbank mittels

# systemctl start mysql

fand sich im MySQL-Log folgender Eintrag:

...
May 16 12:55:45 ALPHA mysqld: 160516 12:55:45 [Warning] Could not increase number of max_open_files to more than 1024 (request: 80162)
...

Offenbar waren 1024 gleichzeitig geöffnete File Handles nicht ausreichend.

Ich nahm deshalb folgende Anpassung an /etc/security/limits.conf vor:

...
mysql hard nofile 102400
mysql soft nofile 102400

Quelle: How to permanently raise ulimit ‘open files’ and MySQL ‘open_files_limit’

Ein Neustart von MySQL löste das Problem aber nicht; die Fehlermeldung im Log blieb bestehen (eventuell muss man den Server neu starten, damit diese Limits aktiv werden?).

Ein wenig Recherche fügte dann zu Tage, dass man dem MySQL-Server via der systemd Service-Datei selber eine Maximalzahl an File Handles zuweisen kann.

Unter Debian findet sich die Service-Datei unter /etc/systemd/system/mysql.service, wo ich folgende Zeile einfügte:

...
[Service]
...
LimitNOFILE=infinity

Anschliessend musste ich folgenden Befehl ausführen, damit die Anpassungen von systemd zur Kenntnis genommen werden:

# systemctl stop mysql
Warning: mysql.service changed on disk. Run 'systemctl daemon-reload' to reload units.
# systemctl daemon-reload
# systemctl start mysql

Beim Neustart von MySQL erschien die Fehlermeldung im mysql.log erneut, dieses Mal aber mit einer deutlichen höheren Zahl an gleichzeitig geöffneten Dateien:

...
May 16 13:02:57 ALPHA mysqld: 160516 13:02:57 [Warning] Could not increase number of max_open_files to more than 65536 (request: 80162)
...

Good enough, dachte ich mir. Deshalb entschied ich mich, erneut my.cnf anzupassen:

...
[mysqld]
...
open_files_limit	= 60000
...

Noch einmal den Server neu starten, und gut war es:

# systemctl stop mysql
# systemctl start mysql

Bei der manuellen Ausführung des Scripts lief die Sause anschliessend durch, ohne zu stocken.

Tags: , , ,
Labels: Uncategorized

Keine Kommentare | neuen Kommentar verfassen

Sonntag, 17. Januar 2016

MySQL meldet Fehler 2049

Vor einigen Wochen habe ich im Zuge eines apt-get upgrade MySQL auf meinem Linux-Server zu Hause aktualisiert. Wie gelegentlich der Fall resultierte das Update in einem Kollateralschaden. In meinem PHP Error-Log las ich nach dem Update für einige meiner Web-Applikationen folgendes:

MySQL returned error #2049: Connection using old (pre-4.1.1) authentication protocol refused (client option 'secure_auth' enabled)

Nach etwas Googeln stellte sich heraus, dass einige meiner MySQL-Benutzer ihr Passwort noch mit einem uralten MySQL-Hash in der Datenbank liegen hatten:

Support for pre-4.1 password hashes is removed in MySQL 5.7.5. This includes removal of the mysql_old_password authentication plugin and the OLD_PASSWORD() function.

Quelle: 6.1.2.4 Password Hashing in MySQL

Nachdem ich die Klartext-Passwörter im PHP-Code der Applikationen ausfindig gemacht hatte, änderte ich diese mittels phpMyAdmin in den MySQL-Benutzertabelle und verwendete dafür die PASSWORD()-Funktion.

Noch ein FLUSH PRIVILEGES, und die Web-Applikationen funktionierten wieder ohne Murren.

Tags: , , , , ,
Labels: Uncategorized

Keine Kommentare | neuen Kommentar verfassen

Sonntag, 14. Dezember 2014

MySQL-Query auf Dev blitzschnell, in Prod extrem langsam

Am Freitag habe ich mich entschieden, eine bisher nur auf meinem lokalen Mac mini in einem Vagrant-Container laufende Web-Applikation auf meinen “produktiven” Web-Server bei der Cyon GmbH zu verschieben.

Es handelt sich um eine Applikation, mit welcher Fragen einer Zertifizierungsprüfung mittels Multiple Choice beantwortet werden können. Eine Unterseite der Applikation berechnet aus dem Log der bisher getätigten Antworten Statistiken.

Nach einigen Minuten lief die Web-Applikation und der aktuellste MySQL-Dump war ebenfalls auf dem Server eingespielt. Doch dann kam das böse Erwachen: Beim Aufruf einer neuen Frage wartete ich unzählige Sekunden auf eine Antwort des Servers, bis die Ausführung des PHP-Scripts dann mit einem Timeout abbrach.

Was zum Teufel? Rasch war klar, dass ein SQL-Query zur Berechnung von Statistiken der Übeltäter war.

Auf Dev lief das MySQL-Query folgendermassen rasch durch:

[12-Dec-2014 13:22:18 Europe/Zurich] I - mysql->query() took 0.1694 secs in /var/www/apps/%APP%/inc/%APP%.class.php:343 for URI (Referer: unknown) from IP 192.168.1.1 with User Agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/600.1.17 (KHTML, like Gecko) Version/7.1 Safari/537.85.10"

Auf Prod hingegen loggte ich diesen Zeitwert:

[12-Dec-2014 12:40:18 Europe/Zurich] I - mysql->query() took 47.8917 secs in %PATH%%APP%.class.php:343 for URI <%URI%> (Referer: %REFERER%) from IP %IP% with User Agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/600.1.17 (KHTML, like Gecko) Version/7.1 Safari/537.85.10"

Eine Verlangsamung von mehr als dem 200-fachen!

Als erstes prüfte ich die Versionen der Datenbank-Software:

Dev

-- Server version	5.6.21-1~dotdeb.1

Prod

-- Server version	5.5.40-cll

Ich verwendete also eine Zehntelsversion bessere Datenbank.

Anschliessend liess ich mir das Query sowohl in Dev als auch in der Produktion “erklären” (EXPLAIN):

Dev

EXPLAIN-localhost
image-6013

Prod

EXPLAIN-cyon
image-6014

Der Unterschied: Auf Prod führte MySQL ein “Dependent Subquery” aus, auf Dev “nur” (?) ein “Subquery”. War das das Problem?

Da mir EXPLAIN hier nun wirklich nicht weiterhalf, wendete ich mich Google zu. Folgende zwei Stackexchange-Artikel erheischten meine Aufmerksamkeit:

Beim Lesen der Antworten kam mir plötzlich die Idee: Vielleicht sind INDEXe deine Freunde? Ich wählte die Spalte Questions-id (`Questions-id` varchar(64) NOT NULL, CLSIDs enthaltend) aus und versah sie mit einem Index.

Und siehe da, beim nächstem Aufruf der Web-Seite wurde folgender Zeitwert registriert:

[12-Dec-2014 23:33:09 Europe/Zurich] I - mysql->query() took 0.0412 secs in %PATH%%APP%.class.php:343 for URI <%URI%> (Referer: %REFERER%) from IP %IP% with User Agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/600.1.17 (KHTML, like Gecko) Version/7.1 Safari/537.85.10"

Der zusätzliche Index auf einer Spalte auf hat mein Performance-Problem zu meiner vollsten Zufriedenheit gelöst. Wieso der Index aber in Version 5.6 von MySQL nicht mehr benötigt wird, ist mir derzeit noch ein Rätsel. Wahrscheinlich haben die Entwickler die Subquery-Performance in dieser Version stark verbessert?

Tags: , , , , , , , , , , ,
Labels: Web

Keine Kommentare | neuen Kommentar verfassen

Donnerstag, 10. Juli 2014

MySQL meldet “Using unique option prefix X instead of Y is deprecated”

Warning: Using unique option prefix database instead of databases is deprecated and will be removed in a future release.
Please use the full name instead.

MySQL reklamiert dies, weil die Namen einiger Argumente und Konfigurationsparameter in den kommenden Releases umbenannt werden.

Im Script zur Sicherung meiner Datenbanken war die Anpassung simpel:

...
$MYSQLDUMP --user=$USER --password=$PW --database $DATABASE > "$DUMPFILE"
...

… wurde zu …

...
$MYSQLDUMP --user=$USER --password=$PW --databases $DATABASE > "$DUMPFILE"
...

Man beachte, dass in der ursprünglichen Version der Parameter databases noch im Singular (database) aufgeführt war.

Denkanstoss: Thread: Using unique option prefix pass instead of password is deprecated

Tags: ,
Labels: Linux

Keine Kommentare | neuen Kommentar verfassen

Sonntag, 29. Juni 2014

PHP kann unter Mac OS X nicht mit MySQL kommunizieren

Da war ich am Donnerstag in den beeindruckenden neuen Räumlichkeiten meines ehemaligen Arbeitgebers Liip und nahm am Hackday teil, wo ich mich anfänglich mit der Konfiguration von Docker befasste — und war dann auf meinem MacBook Air in einem anderen Zusammenhang mit Verbindungsproblemen zwischen PHP und der MySQL-Datenbank konfrontiert.

PHPs mysqli meldete:

Error: 2002 - No such file or directory

Das Problem lag gemäss dieser Diskussion auf Stack Overflow darin begründet, dass ich nach dem Upgrade auf Mavericks die mitgelieferte php.ini unter /etc/php.ini verwendete, welche für Mac OS X nicht anwendbare Standardwerte für die Verbindung zu MySQL enthielt.

Nachdem ich die Einträge

...
pdo_mysql.default_socket=/var/mysql/mysql.sock
mysql.default_socket = /var/mysql/mysql.sock
mysqli.default_socket = /var/mysql/mysql.sock
...

in

...
pdo_mysql.default_socket=/tmp/mysql.sock
mysql.default_socket = /tmp/mysql.sock
mysqli.default_socket = /tmp/mysql.sock
...

geändert hatte und Apache mittels

# apachectl graceful

neugestartet hatte, sprach PHP problemlos mit MySQL.

Solche Handstände sind künftig nicht mehr nötig, da ich nun endlich meine Vagrant-Installation vom Mac mini hier auf das MacBook transferiert habe.

Tags: , , , ,
Labels: Uncategorized

Keine Kommentare | neuen Kommentar verfassen

Montag, 2. Juni 2014

MySQL will nach dem Upgrade auf Version 5.5 unter Debian nicht mehr starten

Aus irgendeinem Grund setze ich auf meinem Entwicklungsserver seit Jahren MySQL 5.0 ein. Heute sah ich den Tag gekommen, die Datenbank auf Version 5.5 zu “lüpfen”.

Leider lief dieses Unterfangen nicht ohne Komplikationen ab:

# /etc/init.d/mysql start
Starting MySQL database server: mysqld . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . failed!

Konfigurationsdatei anpassen

Na toll, sehr aussagekräftig das Ganze! Doch wenn man anstelle des Debian Startup-Scripts mysqld direkt aufruft, wird man von Fehlermeldungen nur so erschlagen:

# /usr/sbin/mysqld 
140602 21:39:51 [Warning] Using unique option prefix key_buffer instead of key_buffer_size is deprecated and will be removed in a future release. Please use the full name instead.
140602 21:39:51 [Warning] The syntax '--log-slow-queries' is deprecated and will be removed in a future release. Please use '--slow-query-log'/'--slow-query-log-file' instead.
140602 21:39:51 [ERROR] An old style --language value with language specific part detected: /usr/share/mysql/english/
140602 21:39:51 [ERROR] Use --lc-messages-dir without language specific part instead.
140602 21:39:51 [Warning] Using unique option prefix myisam-recover instead of myisam-recover-options is deprecated and will be removed in a future release. Please use the full name instead.
140602 21:39:51 [Note] Plugin 'FEDERATED' is disabled.
/usr/sbin/mysqld: Table 'mysql.plugin' doesn't exist
140602 21:39:51 [ERROR] Can't open the mysql.plugin table. Please run mysql_upgrade to create it.
140602 21:39:51 InnoDB: The InnoDB memory heap is disabled
140602 21:39:51 InnoDB: Mutexes and rw_locks use GCC atomic builtins
140602 21:39:51 InnoDB: Compressed tables use zlib 1.2.8
140602 21:39:51 InnoDB: Using Linux native AIO
140602 21:39:51 InnoDB: Initializing buffer pool, size = 128.0M
140602 21:39:51 InnoDB: Completed initialization of buffer pool
140602 21:39:51 InnoDB: highest supported file format is Barracuda.
140602 21:39:51  InnoDB: Waiting for the background threads to start
140602 21:39:52 InnoDB: 5.5.37 started; log sequence number 3243771
140602 21:39:52 [ERROR] /usr/sbin/mysqld: unknown option '--skip-bdb'
140602 21:39:52 [ERROR] Aborting

140602 21:39:52  InnoDB: Starting shutdown...
140602 21:39:53  InnoDB: Shutdown completed; log sequence number 3243771
140602 21:39:53 [Note] /usr/sbin/mysqld: Shutdown complete

Soso. Zuerst einmal musste ich die Konfigurationsdatei /etc/mysql/my.cnf säubern — auch wenn obige Fehlermeldungen aussehen als würden sie von Kommandozeilenoptionen stammen, waren sie bei mir alle in der my.cnf spezifiziert.

  • key_buffer muss in key_buffer_size umbenannt werden
  • log_slow_queries muss in slow-query-log-file umbenannt werden
  • language = /usr/share/mysql/english muss vollständig auskommentiert werden
  • myisam-recover muss in myisam-recover-options umbenannt werden
  • skip-bdb muss vollständig auskommentiert werden

Tabellenstruktur aktualisieren

Nach dieser Aktion kam MySQL zwar nicht mehr hoch, aber spuckte immerhin nur noch eine Fehlermeldung aus:

...
mysqld: Table 'mysql.plugin' doesn't exist
140602 21:52:02 [ERROR] Can't open the mysql.plugin table. Please run mysql_upgrade to create it.
...

Wenn ich diesen Befehl aber auf der Kommandozeile ausführte, erhielt ich folgendes zu Gesicht:

# mysql_upgrade
Looking for 'mysql' as: mysql
Looking for 'mysqlcheck' as: mysqlcheck
FATAL ERROR: Upgrade failed

Eine Antwort auf Serverfault half mir dann aber auf die Sprünge: Bevor man diesen Befehl ausführt, muss mysql im Hintergrund laufen. Doch wie starte ich MySQL, wenn es zuerst die Ausführung von mysql_upgrade verlangt? Mit folgendem Befehl ignoriert MySQL Probleme mit fehlenden Tabellen:

# mysqld --skip-grant-tables

Anschliessend lässt man mysql_upgrade in einem anderen Terminal laufen. Und voila, nach dieser Aktion kommt MySQL nun auch wieder mit dem Debian-Startscript hoch:

# # /etc/init.d/mysql start
Starting MySQL database server: mysqld ..
Checking for tables which need an upgrade, are corrupt or were 
not closed cleanly..

Tags:
Labels: Linux

Keine Kommentare | neuen Kommentar verfassen

Montag, 4. November 2013

MySQL-Parameter mit Perconas Monitoring Plugins in Cacti überwachen

Wer seine MySQL-Installation mit Cacti überwachen möchte, sei Perconas Monitoring Plugins empfohlen. Die Datei (zum Zeitpunkt des Verfassens dieses Artikels war 1.0.5 aktuell) lädt man sich im Download-Bereich des Anbieters herunter.

$ cd /tmp
$ wget http://www.percona.com/redir/downloads/percona-monitoring-plugins/LATEST/percona-monitoring-plugins-1.0.5.tar.gz

Anschliessend entpackt man das Archiv:

$ tar xvzf percona-monitoring-plugins-1.0.5.tar.gz

Danach wechselt man in das cacti-Unterverzeichnis …

$ cd /tmp/percona-monitoring-plugins-1.0.5/cacti

… wo man folgenden Befehl ausführt:

cp -R . /var/www/cacti/

Dies kopiert alle nötigen Dateien (sprich: das ss_get_mysql_stats.php-Script) in das cacti-Root.

Anschliessend erstellt man sich unter MySQL einen Benutzer, welchen das Percona-Script verwenden wird, um die Parameter auszulesen:

GRANT SUPER, PROCESS ON *.* TO 'mysqlmon'@'localhost' IDENTIFIED BY "sikrit";
FLUSH PRIVILEGES;

Die Zugangsdaten erfasst man nun auch noch im Script ss_get_mysql_stats.php.

Anschliessend importiert man das XML-Template mit dem Namen cacti_host_template_percona_mysql_server_ht_0.8.6i-sver1.0.5.xml in cacti — dies geschieht über den Menupunkt Import Templates.

Zu guter letzt richte man sich einen neuen Host ein, gibt in das Adressfeld localhost ein und weist dem Host den Typ Percona MySQL Server HT zu. Anschliessend kann man die Graphen generieren.

Debugging

Bei Problemen hilft es, im Script folgende Zeile anzupassen …

...
$debug = TRUE;
...

… und die Kommandozeile, wie sie im cacti-Log auftaucht, in einer interaktiven Shell auszuführen.

ERROR 2013 (HY000): Lost connection to MySQL server at ‘reading initial communication packet’, system error: 0

Diesem Fehler stand ich zu Beginn gegenüber. Folgende drei Massnahmen lösten das Problem schlussendlich:

  • In /etc/mysql/my.cnf muss die Zeile mit bind-address = ... auskommentiert und der MySQL-Server neu gestartet werden.
  • Dem MySQL-Benutzer mysqlmon muss in der Tabelle user in der Datenbank im Feld Host localhost eingetragen werden.
  • MySQL muss über localhost und nicht über 127.0.0.1 angesprochen werden.

RRD-Dateien werden nicht aktualisiert …

… obwohl das cacti-Log aufzeigt, dass das PHP-Script Werte zurückliefert? Nach einem Debug-Spagat hatte ich die Lösung gefunden. Im cacti-Log las ich etwas in der Form:

...
/usr/bin/rrdtool update /var/www/cacti/rra/mysql_sort_rows_999.rrd --template Sort_merge_passes:Sort_range:Sort_rows:Sort_scan 1383591127:0:522:13426:951
...

Ich führte den Befehl auf der Kommandozeile aus und sah mich mit folgender Fehlermeldung konfrontiert:

ERROR: /var/www/cacti/rra/mysql_sort_rows_999.rrd: illegal attempt to update using time 1383591127 when last update time is 1383594548 (minimum one second step)

Den Unix-Timestamp 1383590086 konvertierte ich mit Bordmitteln in ein lesbares Kalenderdatum und stellte fest, dass die Uhrzeit eine Stunde zurück lag. Ich hatte es also mit einem ganz klassischen Zeitzonen-Problem zu tun!

Um sicher zu gehen, startete ich in eine interaktive PHP-Session:

$ php -a
Interactive mode enabled

php > date_default_timezone_get();
PHP Warning:  date_default_timezone_get(): It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected the timezone 'UTC' for now, but please set date.timezone to select your timezone. in php shell code on line 1
PHP Stack trace:
PHP   1. {main}() php shell code:0
PHP   2. date_default_timezone_get() php shell code:1
php > 

Voila! Nachdem ich in der Datei /etc/php5/cli/php.ini folgende Information eingefügt hatte …

...
date.timezone = Europe/Zurich
...

… klappte es plötzlich mit den Updates.

Tags: , , , , ,
Labels: Linux

Keine Kommentare | neuen Kommentar verfassen

Freitag, 16. August 2013

MySQL-Passwörter nicht in cron-Job Kommandozeilen hinterlegen

Seit längerem habe ich mich gestört, dass in den von cron versendeten E-Mails mit dem Status des Datenbank Backup-Scripts der Benutzername und das Passwort des verwendeten Datenbankbenutzers im Klartext stehen.

Dank einer Frage auf Superuser.com konnte ich diese “Unschönheit” beheben.

Im Home-Verzeichnis meines Hosting-Benutzers habe ich eine Datei namens .my.cnf angelegt. Deren Inhalt:

[client]
user=username
password=password

username und password müssen selbstverständlich mit gültigen Zugangsdaten ersetzt werden.

Ganz wichtig: Die Datei ist mittels folgendem Befehl nur für den Owner lesbar zu machen:

$ chmod 600 ~/.my.cnf

Anschliessend habe ich mein Backup-Script umgebaut. Dort steht nun neu:

...
OPTS=""
OPTS="$OPTS --defaults-file=/home/sitename/.my.cnf"

echo ""
echo "Running $MYSQLDUMP $OPTS $DATABASE > $DUMPFILE"

$MYSQLDUMP $OPTS $DATABASE > $DUMPFILE

Der Vorteil dieser Lösung: Selbst wenn der Sysadmin des Servers mittels ps ax alle auf dem Web-Server laufenden Prozesse (und so je nach Zeitpunkt auch meinen cron-Job) angezeigt erhält, steht auch dort nichts von einem Passwort oder gar Benutzernamen.

Tags: , , , , ,
Labels: Linux

Keine Kommentare | neuen Kommentar verfassen

Donnerstag, 14. Februar 2013

Data Mining mit Ubuntu, MySQL, PHP und Python

Beruflich setze ich mich derzeit mit der Analyse von Inhalten von Web-Sites auseinander. Nachfolgend habe ich einige Erfahrungen aufgelistet, welche ich dabei gemacht habe.

Wir verarbeiten in diesem Projekt Web-Sites, welche wir mit entsprechenden Tools aus dem Web auf den lokalen Rechner gespiegelt haben. Die Web-Site Assets liegen im Dateisystem. Zur Weiterverarbeitung der Daten wurden die HTML-, JS-, CSS- und XML-Rohdaten in eine MySQL-Tabelle gespitzt (12GB) und anschliessend mit Meta-Daten ergänzt.

Ich persönlich bin nicht sicher, ob die Ablage von HTML-Code in der Datenbank die sinnvollste und performanteste Lösung ist, aber dies war nunmal der Stand des Projektes als ich dazu gestossen bin — und daran liess sich nichts mehr rütteln.

Da wir unter anderem auch Volltextsuchen auf die Grunddaten anwenden, hätte ich mir Apache Solr genauer angeschaut und darauf mittels PHP und JSON zugegriffen.

Kommandozeile

Da der Ubuntu-Server aus mir unerfindlichen Gründen mit LAMPP aufgesetzt wurde, befinden sich die Binaries wie php nicht in den Standardpfaden und werden von bash ohne absolute Pfadangabe nicht gefunden. Da man den Interpreter täglich dutzende, wenn nicht gar hunderte Male aufruft, ist es ratsam, das Verzeichnis sofort in die $PATH-Variable des Shells aufzunehmen.

Hier findet man sich in der Shell Startup File Hell wieder. Je nachdem, ob man lokal arbeitet oder sich per SSH einloggt muss der Befehl an einem anderen Ort stehen. Schlussendlich habe ich die nachfolgende Zeile …

...
PATH="$PATH:/opt/lampp/bin"

… ans Ende folgender zwei Dateien angefügt:

  • ~/.profile
  • ~/.bashrc

Datenbank (MySQL)

Cache aktivieren

Wir führen PHP-Scripts über die Linux-Shell aus. Gerade bei der Entwicklung neuer Scripts sind verschiedene Anläufe nötig, bis alle Bugs und nicht beabsichtigten Funktionen ausgemerzt sind. Da wir oftmals an einem Datenset von 60’000+ Seiten arbeiten, ist es unabdingbar, dass wir eine Cacheing-Lösung anwenden, um Datenbankabfragen im Kurzspeicher zwischenzulagern.

Folgende Parameter in my.cnf aktivieren den in MySQL vorhandenen Cache:

...
[mysqld]
...
query_cache_type = 1
query_cache_size = 512M
query_cache_limit = 32M

Der Geschwindigkeitsgewinn ist immens — nachdem ein Script mit SELECT-Statements zum ersten Mal ausgeführt wurde und dafür mehrere Minuten benötigte, rauscht es in den folgenden Malen innert Sekunden durch.

Programmierung (PHP und Python)

Helper-Funktionen und -Klassen

Wie aus der Web-Entwicklung gewohnt sollte man bei jedem der mit der Zeit entstehenden Scripts zu Beginn Klassen, allgemeine Einstellungen und Funktionen einbinden.

In der Funktionsbibliothek setze ich beispielsweise folgende wichtigen Parameter in globaler Form:

...
error_reporting(E_ALL);
date_default_timezone_set('Europe/Zurich');

Auch die Datenbankverbindung wird mittels meiner MySQL-Klasse hier erstellt und an alle Scripts weitergegeben, welche die Library einbinden.

Aufbau der Scripts

Als ich zum Projekt stiess herrschten Spaghetti-Code in teils monolithischen Scripts vor. Ich habe meine eigenen Scripts dann aber so entwickelt, dass sie dem Unix-Gedanken folgend normalerweise nur eine bestimmte Funktion ausführen, diese dafür aber ausgezeichnet und in sich abgeschlossen. So steht im Normalfall in jedem Script, welches Daten manipuliert, zuoberst ein SQL-Query, welches die zu verändernden Datenbankdaten auswählt.

Bei der Entwicklung wählt man hierbei ein Query, das einen oder nur wenige Werte aus der Datenbank ausliest und verarbeitet — in dieser Phase hat man keine Zeit, möglicherweise fehlerhafte Manipulationen an 60’000+ Seiten durchzuführen.

HTML mit regulären Ausdrücken parsen?

Nein, besonders nicht dann, wenn man konkret an Eigenschaften des SGML-Markup interessiert ist (bspw. Wohin zeigen Links?). Hierzu verwendet man die in PHP standardmässig enthaltene DOMDocument-Bibliothek.

Wichtig ist, dass man bei der Verwendung von E_ALL die loadHTML()-Funktion mittels @ stummschaltet, weil sonst das Terminal mit Warnungen über fehlerhaften HTML-Code (leider an der Tagesordnung) vollgespamt wird:

...
$dom = new DOMDocument();
@$dom->loadHTML($html);
$elements = $dom->getElementsByTagName('a');

HTML manipulieren — und Fallstricke

Für jede HTML-Seite erstellen wir eine Nur-Text-Version. Hierzu verwenden wir html2text.py von Aaron Swartz selig. Bevor das HTML aber umgewandelt wird, säubern wir die HTML-Datei auf eigene Faust. Auch hier kommt DOMDocument zum Zug.

Wir suchen dabei zuerst einmal Elemente jeglicher Art, deren ID oder Klasse den String nav, menu und breadcrumb enthält. Die Navigation interessiert uns nämlich nicht, und noch schlimmer: Sie verfälscht teilweise die Resultate, weil in der Navigation gesuchte Begriffe vorkommen.

Hierzu lade ich den HTML-Code wieder in eine DOMDocument und iteriere danach über alle Elemente auf der Suche nach den besagten IDs und Klassennamen:

function cleanNavMenuElements($html = null) {
		$dom = new DOMDocument();
		@$dom->loadHTML($html);
		
		$changesMade = false;
		
		$elements = $dom->getElementsByTagName('*');
		foreach($elements as $element) {
			if(preg_match('/^(html|body)$/',$element->nodeName)) {
				// Otherwise we might delete the whole DOM!
				continue;
			}
			
	        if($element->hasAttribute('class') && preg_match('/(nav|menu|breadcrumb)/i',$element->getAttribute('class')) > 0) {
				status('Found element with class ' . $element->getAttribute('class'));
				$element->parentNode->removeChild($element);
				
				$changesMade = true;
				
				// Don't go further if we removed this node already
				continue;
			}
			
			if($element->hasAttribute('id') && preg_match('/(nav|menu|breadcrumb)/i',$element->getAttribute('id')) > 0) {
				status('Found element with id ' . $element->getAttribute('id'));
				$element->parentNode->removeChild($element);
				
				$changesMade = true;
			}
	    }
		
		if(!$changesMade) {
			return $html;
		}
		
		return $dom->saveHTML();
	}

Ein Problem manifestiert sich bei der Manipulation aber: Jegliche Anpassungen erfolgen live, was verwirrende Folgen für Schleifen haben kann.

Da die Suche nach obigen ID- und Klassennamen nicht alle Navigationselemente eliminiert, suche ich in einem zweiten Anlauf Tabellen und Listen, deren Elemente ausschliesslich Links enthalten. Dies ist ein guter Indikator, ein Navigationsblock gefunden zu haben.

Hier ist das Problem des sich bei jeder Iteration veränderndem DOM aber sehr ausgeprägt. Wenn ich deshalb durch td-Elemente und li-Elemente iteriere, verwende ich nicht foreach() sondern eine for()-Schleife, deren Counter $i ich immer dann zurücksetze, wenn ich ein Element entferne. Ansonsten wird aus Erfahrung in der Folge eines (oder mehrere Elemente) übersprungen. Damit dies klappt, arbeite ich den DOM Tabellen- respektive Listenweise ab:

$containers = $dom->getElementsByTagName($containerTag);
	
foreach($containers as $container) {
	$items = $container->getElementsByTagName($itemTag);
	
	for($i = 0; $i < $items->length; $i++) {
		$item = $items->item($i);
		
		if($item === null) {
			continue;
		}
		
		$otherTagsPresent = false;
		foreach($item->childNodes as $child) {
			$tag = $child->nodeName;

			if($tag == '#text') {
				$text = trim($child->nodeValue);
				$len = strlen($text);
				if($len < 1) {
					//#text is empty, thus not relevant
					continue;
				}
			}

			if($tag != 'a') {
				$otherTagsPresent = true;
				continue;
			}
		}

		if(!$otherTagsPresent) {
			$item->parentNode->removeChild($item);
			$i = $i-1;
		}
	}

Weiterverarbeitung der Daten durch Nicht-IT-Profis

Zur Weiterverarbeitung der Auswertungen durch andere, in IT nicht versierte Mitarbeiter habe ich eine Funktion geschrieben, welche eine Pfadangabe sowie CSV-Daten als Argumente übertragen erhält. Die Datei wird geschrieben und gleich anschliessend mittels eines kleinen Python-Scripts (Stichwort: openpyxl) in das bei uns hauptsächlich verwendete XLSX-Format konvertiert.

Tags: , , , , , , , , ,
Labels: Programmierung

Keine Kommentare | neuen Kommentar verfassen