MariaDB, Master/Slave – Master/Slave-Replikation – Synchronisation der Binary Logs mit dem Storage ? – II

Im letzten Beitrag hatte ich eine row-basierte Master/Slave-Replikation zwischen zwei MariaDB-Servern konfiguriert und mit kontinuierlichen INSERTs getestet. Ein Ergebnis war, dass es bei einer Dauerlast auf dem Master und einer hochfrequenten Synchronisation der Log-Files auf dem Slave-System mit Festplatten durchaus zu einem systematischen Anwachsen der “Catch Up”-Zeit zwischen Master- und Slave-System kommen kann. Als Ursache erschien plausibel, dass der Slave mehr mit Updates der Logfiles und deren Synchronisation [SYNC] in Richtung Storage-System beschäftigt ist als mit der Durchführung der SQL-Änderungen in der Datenbank selbst.

Ein Heilmittel bestand darin, die Sychronisation der Log-Files im Bedarfsfall nicht, wie üblicherweise empfohlen, nach jeder Änderung sondern erst nach mehreren Änderungen vornehmen zu lassen. Das RDBMS kann dann wieder Commits in hinreichender Anzahl ausführen und der Änderungsfrequenz auf dem Master-RDBMS nahtlos folgen.

Im vorliegenden Beitrag überprüfen wir diesen Befund nun mittels eines etwas komplexeren Szenarios – nämlich einer parallel stattfindenden “Master/Slave – Master/Slave”-Replikation zwischen zwei Servern für zwei unterschiedliche Datenbanken (Schemata).

Konfiguration des ersten Servers

Die Skizze aus dem letzten Beitrag zeigt die Konfiguration schematisch:

Wir haben zwei RDBMS-Serversysteme “histux” und “hertux“. “hertux” fungiert als Master bzgl. der Datenbank “Kirk”, “histux” dagegen als Master bzgl. der Datenbank “Spock”. Repliziert wird zum jeweils anderen Server.

Die für die Replikation relevanten Einträge in der Konfigurationsdatei “/etc/my.cnf” auf dem Server “hertux” sehen dann etwa wie folgt aus:

Server “hertux”:

[mysqld]
# Enforce GTID consistency
#gtid-mode=ON
#enforce_gtid_consistency=1

# Binary Log 
log_bin=/var/lib/mysql/mysql-bin
binlog_format=row 
binlog_do_db='Kirk'

# Slave with respect to "Spock"
relay_log=/var/lib/mysql/relay-bin
skip_slave_start
log_slave_updates=1

slave-parallel-threads=10
replicate_do_db='Spock' 

# SYNC statements
sync_binlog=1
sync_master_info=2
sync_relay_log=2
sync_relay_log_info=2

# Server ID
server-id	= 2

# These are commonly set, remove the # and set as required.
# port = 3306
...
...

 
Die meisten Einträge kennen wir bereits aus dem letzten Artikel. Die Statements, die sich auf die Slave-Rolle beziehen, wurden dort für das System “histux” erläutert. Sie sind nun in gleicher Weise auf dem System “hertux” für die Datenbank “Spock” zur Anwendung zu bringen.

Neu ist dagegen das Statement

binlog_do_db=’Kirk’

Dieses Statement schränkt die Aufzeichnung der zu replizierenden Master-Änderungen(SQL-Statements oder Row-Änderungen) im Binary Log File auf die Datenbank “Kirk” ein! Das ist im vorliegenden Fall harmlos. Anders sähe die Situation dagegen im Fall einer statement-basierten Replikation aus: Die Einschränkung bezieht sich nämlich immer nur auf die aktive Bank! Das hat Auswirkungen bei bank-übergreifenden SQL-Statements! Im Fall statement-basierter Replikaton sollte man deshalb die Warnungen unter “https://dev.mysql.com/doc/refman/5.7/en/replication-options-binary-log.html” beachten:

Zitat bzgl. der Variable “binlog-do-db=db_name”:
…. Statement-based logging: Only those statements are written to the binary log where the default database (that is, the one selected by USE) is db_name. To specify more than one database, use this option multiple times, once for each database; however, doing so does not cause cross-database statements such as UPDATE some_db.some_table SET foo=’bar’ to be logged while a different database (or no database) is selected. …” (Hervorhebung durch mich).

Für die im aktuellen Szenario verwendete “row-basierte” Replikation ist die Situation jedoch einfacher.

Zitat:
“… Row-based logging: Logging is restricted to database db_name. Only changes to tables belonging to db_name are logged; the default database has no effect on this. Suppose that the server is started with –binlog-do-db=sales and row-based logging is in effect, and then the following statements are executed:

USE prices;
UPDATE sales.february SET amount=amount+100;

The changes to the february table in the sales database are logged in accordance with the UPDATE statement; this occurs whether or not the USE statement was issued. However, when using the row-based logging format and –binlog-do-db=sales, changes made by the following UPDATE are not logged:

USE prices;
UPDATE prices.march SET amount=amount-25;

Even if the USE prices statement were changed to USE sales, the UPDATE statement’s effects would still not be written to the binary log. ….”

Das entspricht aus meiner Sicht doch recht gut der Erwartungshaltung eines Anwenders.

Bzgl. seiner zweiten Rolle als Slave-Systems schränkt das Statement

replicate_do_db=’Spock’

die Replikation auf die Bank “Spock” ein; “replicate_do_db” ist das slave-bezogene Gegenstück zum Statement “binlog_do_db” für die Master-Rolle.

Im Falle einer Replikation mehrerer Datenbanken ist zudem folgender Punkt zu beachten:

To specify multiple databases you must use multiple instances of this option. Because database names can contain commas, the list will be treated as the name of a single database if you supply a comma-separated list.

Konfiguration des zweiten Servers

Für den Server “histux” nehmen wir eine gespiegelte Konfiguration vor:

Server “histux”

[mysqld]
# Enforce GTID consistency
#gtid_mode=ON
#enforce_gtid_consistency=1

# Binary Log File 
log_bin=/var/lib/mysql/mysql-bin
binlog_format=row 
binlog_do_db='Spock'

# Slave with respect to database "Kirk"
relay_log=/var/lib/mysql/relay-bin
skip_slave_start
log_slave_updates=1
slave-parallel-threads=10
replicate_do_db='Kirk' 

# SYNC statements  
sync_binlog=1
sync_master_info=2
sync_relay_log=2
sync_relay_log_info=2

#Server ID 
server-id	= 3

# These are commonly set, remove the # and set as required.
port = 3386
...
...

 
Wir haben hier zusätzlich den MySQL-Port geändert! Natürlich müssen festgelegte Ports für die Replikation in evtl. zwischengeschalteten Firewalls geöffnet werden.

Test 1: 10000 INSERTs auf beiden Servern / SYNC-Werte ≤ 2 / Replikationsstart zeitlich versetzt

Auf beiden Servern sehen wir – wie im letzten Artikel beschrieben – einen manuellen Start der Replikation vor (skip_slave_start !). Den priviligierten Datenbank-User für die Durchführung von Replikationen hatten wir bereits im letzten Artikel auf beiden RDBMS angelegt.

Wie im letzten Artikel erzeugen wir auf beiden Servern für eine begrenzte Zeit von ca. 80 Sek. eine
Dauerlast durch permanente Durchführung von 10000 Einzel-INSERTs in Tabellen der Bank “Kirk” auf “hertux” und nun such in Tabellen der Bank “Spock” auf “histux”. Nach dem Start des Testprograms starten wir auf beiden Servern etwas zeitversetzt die jeweilige Slave-Replikation. Vor dem Replikationsstart auf “hertux” müssen wir allerdings noch (einmalig) die Daten für den Master-Server “histux” (DB Spock) am MySQL-Prompt setzen; dies geschieht in Anlehnung an das Vorgehen im letzten Artikel durch folgende Statements:

MariaDB [Spock]> CHANGE MASTER TO MASTER_HOST='histux.mynet.de',  MASTER_USER='repl', MASTER_PASSWORD='MyReplPasswd', MASTER_PORT=3386;
MariaDB [Spock]> CHANGE MASTER TO master_use_gtid=slave_pos;
MariaDB [Spock]> START SLAVE;

Für SYNC-Werte

sync_binlog=1
sync_master_info=2
sync_relay_log=2
sync_relay_log_info=2

auf beiden Servern erhielt ich für meine HW-Konfiguration ein systematisch anwachsendes Nachlaufen eines der beiden RDBMS:

30 < Catch Up-Zeit < 60 Sekunden

Welcher Server hinterherhinkte und welchen genauen Wert die Catch Up-Zeit annahm, hing u.a. davon ab

  • auf welchem Server die INSERTs zuerst gestartet wurden,
  • und wie groß der zeitliche Versatz des Starts der Slave-Replikationen gewählt wurde.

Festzuhalten bleibt aber auch diesmal, dass ein systematisches Anwachsen der Catch Up-Zeit durchaus möglich ist, wenn die SYNC-Parameter zu klein gewählt werden. Die absoluten Werte für die Catch Up-Zeit wirken klein; angesichts der gesamten INSERT-Zeit von ca. 80 Sekunden pro Server sind sie aber signifikant groß.

Test 2: 10000 INSERTs auf beiden Servern / SYNC-Werte ≥ 2 / Replikationsstart zeitlich versetzt

Wählt man hingegen SYNC-Werte von

# SYNC-Werte
sync_binlog=1
sync_master_info=3
sync_relay_log=3
sync_relay_log_info=3

und führt wieder zeitlich etwas versetzte INSERTS auf beiden Servern aus, so ergibt sich keine nenneswerte Catch Up-Zeit:

Server “histux”:

MariaDB [(none)]> SHOW SLAVE STATUS\G
....              
                  Master_Host: hertux.mynet.de
                  Master_User: repl
                  Master_Port: 3306
....
              Replicate_Do_DB: Kirk
....
                   Last_Errno: 0
                   Last_Error: 
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 1660492
              Relay_Log_Space: 943
....
        Seconds_Behind_Master: 0
....
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 2
                   Using_Gtid: Slave_Pos
                  Gtid_IO_Pos: 0-2-537280
1 row in set (0.00 sec)

 
Server “hertux”:

MariaDB [(none)]> SHOW SLAVE STATUS\G

                  Master_Host: histux.mynet.de
                  Master_User: repl
                  Master_Port: 3386
...
              Replicate_Do_DB: Spock
...                   Last_Errno: 0
                   Last_Error: 
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 342
              Relay_Log_Space: 1671507
...
        Seconds_Behind_Master: 0
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 3
               Master_SSL_Crl: 
           Master_SSL_Crlpath: 
                   Using_Gtid: Slave_Pos
                  Gtid_IO_Pos: 0-3-535624
1 row in set (0.00 sec)

 
Beide
Server arbeiten also synchron; auf beiden RDBMS gilt:

Seconds_Behind_Master: 0

Bewertung und Fazit

Steht der bzw. stehen die Master-Server in einer Replikation vom Typ

  • “Master-Slave”>
  • oder “Master/Slave – Master/Slave”

unter dauerhafter Last, sollte man ein Auge auf die SYNC-Parametrierung für die Log-Files werfen. Ggf. sollten die slave-bezogenen SYNC-Variablen auf Werte > 2 gesetzt werden, um ein systematisches Anwachsen von Catch Up-Zeiten zwischen den RDBMS-Systemen zu vermeiden.

sync_binlog=1
sync_master_info=3
sync_relay_log=3
sync_relay_log_info=3

Der zu wählende Wert ist natürlich abhängig von der HW-Konfiguration.

Wir groß ist der potentielle Schaden im Fall eines Crashes, in dem Änderungen verloren gehen, die nicht rechtzeitig auf das Plattensystem geschrieben wurden? Aus meiner Sicht nicht groß. Relevant nach einem Systemcrash ist ja der zu wählende richtige Aufsetzpunkt für die jeweilige lave-Replikation. (Bzgl. der Änderungen in einer Master-Datenbank müssen sowieso Mechanismen der jeweiligen Applikation greifen.) Werden die SYNC-Parameter klein gewählt, sind nur einige wenige letzte Transaktionen vor dem Crash zu prüfen, um den richtigen Aufsetzpunkt (bzw. die zugehörige GTID) zu bestimmen. In einer Abwägung

zwischen einem nicht ganz nahtlosen Wiederaufsetzen der Replikation nach einem Crash
undeiner systematisch anwachsenden Catch UP-Zeit während der Replikation

erscheint mir der begrenzte Aufwand für die Identifikation der richtigen GTID das kleinere Übel zu sein.

Links

https://mariadb.com/kb/en/library/gtid/
https://www.lexiconn.com/blog/2014/04/how-to-set-up-selective-master-slave-replication-in-mysql/
https://docs.jelastic.com/mariadb-master-slave-replication

MariaDB, Master/Slave – Master/Slave-Replikation – Synchronisation der Binary Logs mit dem Storage ? – I

Datenbank-Replikationen sind nützlich und können in vielen Anwendungen einen Gleichstand von Daten über System- und Netzgrenzen hinweg ermöglichen. Es ist relativ einfach, eine Master-Slave-Replikation mit MariaDB oder MySQL zu implementieren. Ich verwende solche Replikationen inzwischen häufig, um Ergebnisdaten komplexer Berechnungen zwischen RDBMS-Serversystemen zu übertragen.

Je nachdem, wie groß

  • die Übertragungsrate des Netzwerkes zwischen den RDBMS-Servern ist,
  • wie groß die Transfergeschwindigkeit zum Storage-Sub-System auf jedem der Server ist
  • und wie die Parametrierung der Replikation erfolgt

kann man dabei Überraschungen mit den “CatchUp“-Zeiten erleben. Unter der CatchUp-Zeit verstehe ich hier die Zeit, die das Slave-System hinter dem Status seines Master-Systems hinterherhinkt.

Relevant werden diese Zeiten natürlich vor allem dann, wenn auf dem Master über einen längeren Zeitraum hinweg die Änderungsrate durch INSERTs und UPDATEs kontinuierlich hoch bleibt. Sprich: Wenn das RDBMS des Masters richtig unter Last steht und neben den eigentlichen SQL-Statements permanent Log-Einträge zum Slave-System geschaufelt werden müssen.

In einem Lastszenario mit Perioden dauerhafter Last muss man natürlich auch an die Fähigkeit zur Restaurierung des gesamten RDBMS-Verbundes denken. Dass man regelmäßige Backups braucht, ist sowieso klar. Es stellt sich aber auch die Frage, wie man die Replikation nach Crashes eines der beteiligten Systeme wieder aufsetzen kann.

Folgt man dem Standardwerk “High Performance MySQL” von Schwartz, Zaitsev, Tkachenko (O’Reilly Verlag, 3rd Ed.), so erhält man den Eindruck, dass man vor Crashes gehörigen Respekt haben sollte. Die Autoren empfehlen deshalb in mehreren Abschnitten (sehr von Vorsicht geprägte) Einstellungen zur Synchronisation von “Binary Log Files” und “Information Log Files” mit den Storage-Systemen – sowohl auf dem Master- wie auch auf dem Slave-System.

Es ist grundsätzlich interessant, sich das mal anzusehen. Ferner gilt: Replizierende Systeme haben nicht immer ein identisches Leistungsvermögen. Wie wirken sich SYNC-Einstellungen dann auf CatchUp-Zeiten aus? Bei einem Kunden ergab sich vor einiger Zeit zudem die Frage, wie es eigentlich mit einer “Master/Slave-Master/Slave”-Replikation aussieht. Gemeint waren hier gegenseitige Replikationen mit vertauschten Master/Slave-Rollen für zwei unterschiedliche Datenbanken; beide RDBMS-Systeme spielen dabei sowohl Master als auch Slave, nur eben für verschiedene Datenbanken (Schemata).

Zeit, mal einen Blick auf das grundsätzliche Replikationsverhalten für 2 Server in unserem LAN zu werfen. Im vorliegenden Artikel bespreche ich, wie man eine einfache Master/Slave-Replikation aufsetzt. Der Text ist also auch für Leute gedacht, die MySQL/MariaDB zwar nutzen, sich dem Thema Replikation aber noch nie genähert haben. Die angegebenen Kommandos sind einfach nachzustellen. Ein zweiter Artikel greift dann die komplexere “Master/Slave-Master/Slave”-Konstellation auf.

Eigentliches Ziel beider Artikel ist es aber, herauszuarbeiten, ob und wie man ggf. an SYNC-Einstellungen drehen kann, um die CatchUp-Zeit möglichst gering zu halten.

Testsituation Master/Slave – Master/Slave

Die nachfolgende Skizze zeigt den Testaufbau schematisch:

Wir haben zwei RDBMS-Serversysteme “histux” und “hertux“. Wir betrachten nachfolgend zunächst nur den oberen Teil der Skizze – also ein reines Master/Slave-Szenario bzgl. der
Datenbank “Kirk”. “hertux” fungiert dabei als Master, “histux” als Slave.

Im zweiten Artikel dieser Serie ergänzen wir dann das Master/Slave-Szenario mit umgekehrtem Vorzeichen für eine weitere Datenbank “Spock” (unterer Teil der Skizze). “histux” wird dann für die Datenbank “Spock” die Rolle des Masters übernehmen, “hertux” die des Slaves. Es entsteht so das dargestellte “Master/Slave – Master/Slave” Szenario. Beide Server spielen darin gleichzeitig sowohl Master, als auch Slave für jeweils eine der Datenbanken [DBen].

Ich verwende in meinem Setup übrigens Systeme mit einer MariaDB – und nicht mit einem originalen MySQL-RDBMS. In meinem Fall ist das “histux”-System etwas schwächer ausgelegt als das “hertux”-System; das betrifft den Prozessor, aber auch das Storage-System (einfache HD vs. einfache SSD; für Tests mit schnellen SSD-Raid-Verbänden hatte ich noch keine Zeit …).

Ich betrachte nachfolgend nur InnoDB-Tabellen, deren Änderungen (Inserts) mit einer sog. row-basierten Replikation (s.u.) auf die jeweiligen Slave-Server übertragen werden sollen. Um der Replikation eine Chance zu geben, werden die Änderungen (Inserts) durch echte Einzelstatements für jeden Record erzeugt. Inserts, Updates für mehrere Datensätze werden also nicht in einem SQL-Statement gebündelt.

Master/Slave: Konfiguration des Master-Servers

Die DB “Kirk” existiere zunächst als einzige DB in den RDBMS der Server “histux” und “hertux“. Die notwendigen Konfigurationsstatements für die RDBMS-Replikation sind dann recht einfach; wir tragen folgende Zeilen in die Datei “/etc/my.cnf” des Master-Systems “hertux” ein:

# Binary Log file / Log Method 
log_bin=/var/lib/mysql/mysql-bin
binlog_format=row
# binlog_format=mixed

# Syn des Logfiles auf die Platte 
sync_binlog=1

#ID des Servers 
server-id=2

Wir gehen kurz die drei Einstellungsgruppen durch:

Binary-Log-File und Log- bzw. Replikations-Verfahren
Transaktionen auf einem System werden in sog. “Binary Log”-Dateien protokolliert. Die ersten zwei Statements legen fest, wo das Binary-Log-File gelagert wird und welches Format des Loggings verwendet wird:

Eine Replikation kann (SQL-) statement-bezogen oder row-bezogen durchgeführt werden.
Im ersteren Fall werden SQL-Statements vom Master auf den Slave übertragen und dort nachgefahren. Im zweiten Fall werden Daten-Änderungen betroffener Tabellen-Rows übertragen. Je nach Art von (komplexen) SQL-Statements kann das Nachfahren solcher Statements auf dem Slave deutlich aufwändiger sein, als die Änderungen der betroffenen Datensätze selbst nachzuziehen. Andererseits gilt evtl. genau das Umgekehrte für Massenänderungen über viele Rows hinweg. Man sollte die Art des Loggings also an der Charakteristik seiner SQL-Anwendungen ausrichten. Das gilt im Grunde aber auch für den Einsatz von MyISAM vs. InnoDB-Tabllen. Im Kontext von InnoDB-Tabellen und Anwendungen mit Einzeländerungen verwende ich selbst oft eine row-basierte Replikation. Stehen dagegen Massenänderungen vieler numerischer Daten im Fokus einer Anwendung weiche ich oft auf MyISAM-Tabellen und statement-basierte Replikation aus. Pauschalisieren lässt sich das aber nicht.

Synchronisation mit dem Storage-Subsystem
Das Statement “sync_binlog=1” legt fest, dass das Binary Log File mit dem Storage-Systems des Servers (z.B. einer lokalen Harddisk) nach jedem Commit synchronisiert werden soll. Das sichert gegenüber Datenverlust während Systemcrashes ab; aber eine solche Synchronisation ist eine durchaus aufwändige Operation, die die Performance bei vielen Änderungen pro Sekunde
beeinträchtigen kann. In kleineren Firmen sind hier batteriegepufferte Caches von Raid-Controllern ggf. ein Ausweg.

Server-ID
Sind in einem Netzwerkverbund mehrere Server vorhanden, so sollte jedes RDBMS mit einer netzwerkweit eindeutigen Server-ID versehen werden. Die Festlegung einer eindeutigen ID der MySQL/MariaDB-RDBMS ist notwendig, um sog. “Globale Transaktions IDs” [GTIDs] zu unterstützen. In die Transaktions-IDs geht u.a. die eindeutige ID eines Servers mit ein.
Ich weise daraufhin, dass Format/Implementierung von GTIDs einer MariaDB (> Version 10.0.3) anders aussehen als bei einer herkömmlichen MySQL-Datenbank. Siehe hierzu:
https://mariadb.com/kb/en/library/gtid/#setting-up-a-new-slave-server-with-global-transaction-id.

Mit der bisherigen Einstellung werden Änderungen aller Datenbanken [DBen] des MariaDB RDBMS “hertux” im “Binary Log File” des Masters aufgezeichnet. Wir werden das im nächsten Artikel durch einen einfachen Filter für DBen abändern.

Alle anderen Einstellungen (u.a. bzgl. der IO-capacity und Anzahl IO Threads für InnoDB, ..) bleiben in meinem Test auf Standardwerten.

Master/Slave: Konfiguration des Slave-Servers

Die “/etc/my.cnf” des Slave-Systems “histux” sieht etwas erwartungsgemäß anders aus:

# Binary Log file / Log Method 
log_bin=/var/lib/mysql/mysql-bin
# binlog_format=mixed
binlog_format=row

# Relay Log File, automat. Starten der Replikation?   
relay_log=/var/lib/mysql/relay-bin
skip_slave_start

# Log der Updates
log_slave_updates=1

# Threads 
slave-parallel-threads=4

#ID des Servers 
server-id=3
# read-only=1

Hinweise zu den Einstellungen:

Relay-Log-File
Auf dem Slave ist die Festlegung des Relay-Log-Files erforderlich. Hierhin werden die durchzuführenden Transaktionen des Masters übertragen; in unserem einfachen Fall, in dem keine Änderungen auf dem Slave durchgeführt werden, ergibt sich letztlich eine Kopie des Binary-Logs des Masters.

Kein automatischer Start der Replikation
Wir schalten über “skip_slave_start” ein automatisches Starten der Replikation nach dem Starten des RDBMS ab. Das ist für Testsituationen OK; auf Produktiv-Systemen mag der Bedarf anders sein. Die Replikation lässt sich manuell immer zu einem geeigneten Zeitpunkt starten. Z.B., wenn man sicher ist, dass der Master auch aktionsbereit ist; s.u.. Über weitere Parameter (s. die MariaDB-Dokumentation) kann man steuern, wie oft und in welchem zeitlichen Abstand der Slave einen Verbindungsversuch zum Master unternehmen soll.)

Eintrag der Slave-Aktionen im eigenen Binary-Log?
“log_slave_updates=1” sorgt dafür, dass die Replikationstransaktionen im eigenen Binary Log des Slaves vermerkt werden. (Das kann wichtig sein, wenn der Slave selbst wieder Master gegenüber dritten Systemen spielen soll).

Mehrere Threads zum Nachfahren der übermittelten Änderungen
Wir stellen zudem mehrere Threads zur Verfügung, um die einlaufenden row-basierten Änderungen ggf. parallel in die Bank zu übertragen Dies betrifft ausschließlich die SQL-Ebene; s. https://mariadb.com/kb/en/library/parallel-replication/. Welche Anzahl von Threads sinnvoll ist, hängt von der Änderungsrate auf dem Master UND der Leistungsfähigkeit des Slave-Systems ab. Man muss testen, bis zu welcher Thread-Anzahl sich noch eine Performanceverbesserung ergibt.

Auf dem
Slave “histux” könnte die Nutzung global auch auf “read-only” gesetzt werden, wenn die Datenbankinhalte wirklich nur replizierte Inhalte beinhalten sollen. In unserem Fall ist dies wenig sinnvoll, da “histux” ja später Master für die zusätzliche DB “Spock” werden soll. Das entsprechende Statement wurde daher auskommentiert.

Was brauchen wir noch für eine funktionsgerechte Master-Slave-Replikation?

Auf dem Master müssen wir einem Datenbank-User (hier: “repl”) Replikationsrechte einräumen; das zugehörige SQL-Statement, das wir nach Einloggen in das Master-RDBMS am MySQL-Prompt absetzen, ist:

GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO repl@’192.168.0.%’ IDENTIFIED BY ‘MyReplPasswd’;

“repl” ist ein in priviligierter Account, der alle transaktionalen Änderungen auf dem RDBMS einsehen kann. Auch, wenn dieser Account an Datenbankeinträgen nicht direkt etwas ändern kann, können sich aus seinen Rechten dennoch sekundäre Sicherheitsprobleme ergeben. Also: Vorsicht! Der gleiche User sollte natürlich auch auf dem Slave angelegt werden.

Füllen des Masters

Nun führen wir auf dem Master-Server und seiner/n Tabelle/n einen Schwung von SQL-Statements aus. Ich benutze dazu immer PHP-Test-Files, die hunderttausende von Inserts vornehmen.

Wir prüfen dann auf dem Master Server den Status und damit gleichzeitig den genauen Namen des Binary-Log-Files ab:


MariaDB [Kirk]> SHOW MASTER STATUS;
+------------------+----------+--------------+------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000007 | 16601394 |              |                  |
+------------------+----------+--------------+------------------+
1 row in set (0.00 sec)

 
Man sieht hier, dass auf dem Master schon Einiges passiert ist.

Starten der Replikation auf dem Slave

Wir starten nun unsere erste Replikation. Wir melden uns dazu als Datenbank-User “root” am Slave RDBMS von “histux” an (“mysql -u root -p”). Am MySQL-Prompt geben wir ein:

MariaDB [Kirk]> CHANGE MASTER TO MASTER_HOST='hertux.mynet.de',  MASTER_USER='repl', MASTER_PASSWORD='MyReplPasswd', MASTER_LOG_FILE='mysql-bin.000007;
MariaDB [Kirk]> CHANGE MASTER TO master_use_gtid=slave_pos;
MariaDB [Kirk]> START SLAVE;

 
Das erste Statement definiert den Master und seine Eigenschaften (dauerhaft) für das Slave-System! das zweite Statement setzt den Ausgangspunkt der Replikation auf die richtige Position im Binary File. Das ist im vorliegenden Fall natürlich die 0-Position. Das angegebene Statement

CHANGE MASTER TO master_use_gtid=slave_pos;

lässt sich im regulären Betrieb nach einem evtl. erfolgten Restart des Slaves oder einem Stop und Neustart der Replikation aber immer wieder einsetzen: Die Replikation soll ab der Position im Log-File ansetzen, zu der der Slave bislang gekommen war.

Die Master-Log-Files ändern im Lauf des Betriebs ihre Bezeichnung. Wir müssen uns im weiteren Normalbetrieb allerdings nicht mehr um die Angabe des richtigen Master-Log-Files kümmern. Der Slave erkennt dies in der Regel über GTIDs automatisch. Im Gegensatz zu früheren MariaDB-Varianten, in denen GTIDs nicht automatisch benutzt wurden, muss man sich nun im Normalbetrieb also keine Gedanken mehr zu Byte-bezogenen Positionen im binären Master-Log machen. Das Statement “CHANGE MASTER TO master_use_gtid=slave_pos;” sorgt dafür, dass der richtige Aufsetzpunkt für die Replikation im Verhältnis zum Status des Slaves gefunden wird.

Man kann den
Ablauf der Replikation auf dem Slave nun mit

MariaDB [Kirk]> SHOW SLAVE STATUS\G

verfolgen. Am Schluss des Nachfahrens aller bisherigen Einträge in unsere Test-DB “Kirk” erhält man dann etwas in der Art :

MariaDB [(none)]> SHOW SLAVE STATUS\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: hertux.mynet.de
                  Master_User: repl
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000007
          Read_Master_Log_Pos: 16601394
               Relay_Log_File: relay-bin.000002
                Relay_Log_Pos: 16601681
        Relay_Master_Log_File: mysql-bin.000007
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 0
                   Last_Error: 
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 16601394
              Relay_Log_Space: 16601972
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File: 
           Master_SSL_CA_Path: 
              Master_SSL_Cert: 
            Master_SSL_Cipher: 
               Master_SSL_Key: 
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 0
               Last_SQL_Error: 
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 2
               Master_SSL_Crl: 
           Master_SSL_Crlpath: 
                   Using_Gtid: Slave_Pos
                  Gtid_IO_Pos: 0-2-311024
1 row in set (0.00 sec)

 
Wer mag kann den korrekten Tabellenstatus ja mit PhpMyAdmin kontrollieren. Nebenbei: Es lohnt sich, bei Gelegenheit die Features von PhpMyAdmin für Replikationseinstellungen und die Einsichtnahme in zugehörige Parameter des RDBMS zu studieren.

Interessant ist der Unterschied zwischen der GTID-Position und der Byte-Position:

Gtid_IO_Pos: 0-2-311024
Exec_Master_Log_Pos: 16601394

Verschiedene Infos; gleiche Positionierung in den Binary-Logs!

Die aktuellen Positionen lassen sich auf Master und Slave übrigens jederzeit wie folgt abfragen:

Master

MariaDB [massa]> SELECT @@GLOBAL.gtid_binlog_pos;
+--------------------------+
| @@GLOBAL.gtid_binlog_pos |
+--------------------------+
| 0-2-331028               |
+--------------------------+
1 row in set (0.00 sec)
  
<strong>Slave</strong> 
MariaDB [massa]> SELECT @@GLOBAL.gtid_slave_pos;
+-------------------------+
| @@GLOBAL.gtid_slave_pos |
+-------------------------+
| 0-2-331028              |
+-------------------------+
1 row in set (0.00 sec)

Master/Slave: Test mit 10000 neuen Inserts am Master – ohne SYNC-Einstellungen am Slave

Für meinen Test zur CatchUP-Zeit verwende ich ein Skript, das fortlaufende Inserts in einer InnoDB-Tabelle vornimmt – und zwar Record/Row für Record/Row – inkl. paralleler Indexgenerierung. Ohne Prepared Statements. Es werden dabei mehrere Stringfelder eines neuen Datensatzes gefüllt.

Ich begrenze die Anzahl auf 10000 Inserts. Die Zeitdauer für meine Inserts beträgt 80 Sek..

Die Rate ist also nicht besonders hoch; das liegt u.a. an einer parallel stattfindenden Index-
Generierung und fehlender Optimierung. Die Insert-Rate lässt sich viel höher schrauben; allein eine Bündelung von Inserts in einem Statement und/oder die Nutzung von “prepared statements” würde Faktoren an Geschwindigkeit bringen. Aber es geht mir hier nicht um Optimierung; es kommt mir nur auf relative Anteile der CatchUp-Zeiten an.

Welche CatchUp-Zeit zieht eine solche Insert-Folge während der 80 Sekunden Laufzeit nach sich?

Wir greifen hierzu – während das Testprogramm mit den Inserts läuft – den Slave-Status am Slave ab:

 
MariaDB [massa]> SHOW SLAVE STATUS\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: histux.mynet.de
                  Master_User: repl
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000008
          Read_Master_Log_Pos: 1878830
               Relay_Log_File: relay-bin.000002
                Relay_Log_Pos: 1879117
        Relay_Master_Log_File: mysql-bin.000008
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 0
                   Last_Error: 
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 1878830
              Relay_Log_Space: 1879408
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File: 
           Master_SSL_CA_Path: 
              Master_SSL_Cert: 
            Master_SSL_Cipher: 
               Master_SSL_Key: 
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 0
               Last_SQL_Error: 
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 2
               Master_SSL_Crl: 
           Master_SSL_Crlpath: 
                   Using_Gtid: Slave_Pos
                  Gtid_IO_Pos: 0-2-332342
1 row in set (0.00 sec)

 
Relevant ist die Zeile:

Seconds_Behind_Master: 0

Wunderbar! Offenbar kann der Slave unter den herrschenden Bedingungen [LAN, keine SYNC-Prozesse] locker mithalten – obwohl er schwächer ausgelegt ist als der Master!

Auf den Master hat der Datenaustausch für die Replikation übrigens kaum Auswirkungen: Die benötigte Gesamtzeit steigt dort von etwa 80 Sekunden auf 84 Sekunden an.

Master/Slave: Test mit 10000 neuen Inserts am Master – mit SYNCs auf die HD am Slave

Wir verändern die Einstellung des Slave-RDBMS bzgl. der Synchronisation von dortigen Log-Files zum Storage-System. Es gibt mehrere Dateien, die im Replikationsvorgang auf dem Slave-System fortgeschrieben werden (Relay-Log=> nachzufahrende Transaktionen / master.info-File: Infos über die Position des Masters / Relay_log.info-File => eigene Position im Relay-Log.) Wir folgen zunächst 1:1 den Empfehlungen des Buchs “High Performance MySQL”:

#sync_binlog=1
sync_master_info=1
sync_relay_log=1
sync_relay_log_info=1

Damit starten wir das MariaDB-RDBMS auf dem Slave-Server “histux” neu und loggen uns wieder mittels “mysql -u root -p” in das RDBMS ein. Am MySQL-Prompt verfolgen wir dann einen neuen Testlauf und fragen sukzessive den Slave-Status ab. Es zeigte für meine Systeme leider, dass die CatchUp-Zeit relativ zum Master-Status nun permanent und systematisch zunahm!:

 
MariaDB [(none)]> SHOW SLAVE STATUS\G
*************************** 1. row ***************************
               Slave_IO_State: Queueing master event to the relay log
                  Master_Host: histux.mynet.de
                  Master_User: repl
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000008
          Read_Master_Log_Pos: 6766074
               Relay_Log_File: relay-bin.000002
                Relay_Log_Pos: 5466
        Relay_Master_Log_File: mysql-bin.000008
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 0
                   Last_Error: 
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 5318184
              Relay_Log_Space: 1453647
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File: 
           Master_SSL_CA_Path: 
              Master_SSL_Cert: 
            Master_SSL_Cipher: 
               Master_SSL_Key: 
        Seconds_Behind_Master: 131
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 0
               Last_SQL_Error: 
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 2
               Master_SSL_Crl: 
           Master_SSL_Crlpath: 
                   Using_Gtid: Slave_Pos
                  Gtid_IO_Pos: 0-2-361783
1 row in set (0.12 sec)

 

Am Schluss erhielt ich als maximale Differenz:

Seconds_Behind_Master: 158 (Sekunden)

Es sieht also so aus, dass eine Synchronisation von Relay-Log-Dateien auf dem Slave sehr problematische Folgen haben kann: Immerhin ist der Zeitverlust gegenüber dem Master auf fast das Doppelte der für die DB-Transaktionen benötigten Zeit angewachsen!

In anderen Tests mit über 100000 Inserts wuchs der Zeitverlust weiter extrem an – auf über 3 Stunden!

Master/Slave: SYNC in größeren Transaktions-Abständen am Slave?

Kann man das obige verheerende Ergebnis irgendwie beseitigen? Ein wenig Experimentierfreude zeigt schnell, dass eine Änderung der Parameter auf

  
log_slave_updates=0
sync_master_info=0
sync_relay_log=1
sync_relay_log_info=0

leider gar nichts bringt. Der systematisch größer werdenden Abstand zwischen Slave und Master legt den Schluss nahe, dass der Slave vor lauter SYNC-Prozessen gar nicht dazu kommt, seine SQL-Arbeit im RDBMS durchzuführen. Daten müssen über Netz vom Master geholt werden, Informationen über die Positionen des Masters und des Slaves auf dem Slave in Dateien geschrieben werden. Jede Datenbank-Transaktion am Slave führt dann zu zusätzlichen SYNC-Vorgängen, die unabhängig von den anwachsenden Master-Transaktionen durchzuführen sind. Zudem werden die Files bei einmal wachsender CatchUp-Zeit automatisch immer größer! Das riecht förmlich danach, dass unser Slave bei hoher Taktung der Änderungen auf dem Master in einen Teufelskreis gerät …

Wenn dies Theorie stimmt, dann sollten wir den Datenbanktransaktionen mehr Zeit gegenüber Synchronisationsprozessen einräumen. Die “1” als Parameter bei den 3 Slave-SYNC-Einstellungen bedeutet ja nicht ein Einschalten im Sinne eines boolschen Flags. Vielmehr kann hier ein numerischer Parameter angegeben werden: Er legt fest, nach wieviel Änderungen das jeweilige File auf dem jeweiligen Status
auf die Platten geschrieben werden soll. Wir sollten also versuchen, die SYNC-Parameterwerte zu erhöhen.

Tatsächlich ergibt sich bei einer Einstellung

  
relay_log=/var/lib/mysql/relay-bin
skip_slave_start
log_slave_updates=1
slave-parallel-threads=4
sync_master_info=4
sync_relay_log=4
sync_relay_log_info=4

das Ergebnis :

sync_relay_log=4, sync_master_info=4, sync_relay_log_info=4 => CatchUp-Zeit: 0 Sekunden

Geht man mit den sync-Parametern danach weiter runter, so erhalten wir wieder ein Anwachsen des Delays, wenn auch moderater als beim Wert “1”:

  
sync_master_info=2
sync_relay_log=2
sync_relay_log_info=2

sync_relay_log=4 => CatchUp-Zeit: 54 Sekunden

Bei einem Wert von “3”:

sync_relay_log=3 => CatchUp-Zeit: 3 Sekunden => 0 Sekunden

Master/Slave: SYNC und Abhängigkeit von der Anzahl der Slave-Threads?

Wir können parallel auch mal die Anzahl der Slave-Threads erhöhen. So ergibt

slave-parallel-threads=10
sync_master_info=2
sync_relay_log=2
sync_relay_log_info=2

sync_relay_log=2 / slave-parallel-threads=10 => CatchUp-Zeit: max. 3 Sekunden => 0 Sekunden

Fazit

Schon in einer normalen Master-Slave-Replikation gilt: Durch Synchronisation von Replikations-Log-Files mit dem HD- oder SSD-System auf dem Slave kann sich die CatchUp-Zeit systematisch erhöhen,

  • der Takt der Änderungen auf dem Master konstant hoch ist
  • wenn die Frequenz der SYNC-Prozesse zu hoch gewählt wird,
  • und eine row-basierte Replikation für InnoDB-Tabellen konfiguriert wurde.

Im vorliegenden Fall erwies sich die Vorgabe, nach jeder Änderung alle Log-Files direkt auf die Platte zu schreiben einfach als zu ambitiös: Das Slave-System ist mehr mit SYNC-Prozessen beschäftigt als mit der eigentlichen Durchführung der Änderungen in der Datenbank. Gerät das System erst einmal außer Takt, wächst die CatchUp-Zeit des Slaves gegenüber dem Master bei einer kontinuierlichen Last kontinuierlich an. Relevant ist das vor allem für gekoppelte Systeme, die permanente Änderungsprozesse hoher Frequenz verkraften müssen.

Für replizierende MariaDB-Systeme ist es deshalb wichtig zu testen, ob nicht mehrere Änderungen (Commits) in der Bank abgewartet werden sollten, bevor ein Schreiben der Binary-Log- und Info-Files auf das Storage-System erfolgen soll. U.U. genügt hier schon die Wahl eines nur etwas erhöhten Wertes

sync_master_info=3 (oder 4)
sync_relay_log=3 (oder 4)
sync_relay_log_info=3 (oder 4)

um schon eine signifikante Verbesserung zu erreichen. Die Arbeit, nach einem Crash des Slaves, den richtigen Aufsetzpunkt für die Replikation zu finden, hält sich dann immer noch in relativ engen Grenzen.

Im kommenden Artikel betrachten wir die Parametrierung und die gleiche Situation für eine “Master/Slave – Master/Slave”-Konfiguration.

Links

https://mariadb.com/kb/en/library/setting-up-replication/

Zeichensatzvorgaben für MySQL und PHP – SET NAMES – und leere Strings durch htmlentities()

Gestern ist mir ein dummer Fehler passiert, dessen Analyse mich Zeit gekostet hat. Dabei erwies sich die Sache am Ende als trivial. Es ging letztlich um konsistente Zeichensatzeinstellungen für PHP und MySQL – allerdings mit einem mir bislang unbekannten Nebeneffekt.

Zeichensätze im Kontext von PHP und MySQL sind ein Thema vieler Foren- und Q&A-Artikel im Internet – und nicht immer trifft man auf zufriedenstellende Antworten. Ich hoffe, dieser Artikel trägt anhand eines Beispiels zu etwas mehr Klarheit bei.

Voraussetzungen und aufgetretener Fehler

Unsere hausinternen Apache- und Datenbank-Server laufen normalerweise vollständig unter UTF-8. Inklusive der eigenen Datenbank- und Tabellen-Kollationen unter MySQL- oder MariaDB-Systemen. Aber für Tests müssen wir immer mal wieder gezielt eine Kompatibilität zu den festgelegten Kollationen für MySQL-Banken/Tabellen auf Kundenservern herstellen. Meist kommt dann Latin1 (iso-8859-1) ins Spiel.

Gestern mussten wir für einen solchen Test Datensätze eines von uns entwickelten, php-basierten CMS von einem gehosteten MySQL-Kundenserver in unsere lokale Test-Datenbank übernehmen. Diese Datensätze beinhalteten viele Text-Strings mit deutschen Umlauten. Da es sich nur um wenige Records handelte, haben wir die Daten in diesem Fall mit Copy/Paste und mit Hilfe von Eingabefeldern unserer CMS-Verwaltungsoberfläche übernommen. Das CMS lief dabei unter einer lokalen UTF8-Standarddomäne. Unsere aktuellen CMS-Programme zeigten die fraglichen Textstrings denn auch korrekt in der Web-Oberfläche des CMS an.

Es gab aber andere zu untersuchende Punkte im Layout. Um die Unstimmigkeiten zu testen, haben wir zusätzlich eine separate Testdomäne angelegt und diverse PHP-Klassen vom Kundenserver (auf dem dortigen Versionsstand) in bestimmte Verzeichnisse des zugehörigen Web-Spaces auf unserem lokalen Server geladen. Ergebnis: 2 Domainen mit z.T. unterschiedlichen Versionsständen von PHP-Klassen.

Und dann passierte es:

Bei einem Aufruf der Webseiten in der Testdomäne verschwanden plötzlich fast alle Text-Strings aus der Web-Oberfläche.

Bilder und grundsätzlicher Aufbau der Webseiten bleiben jedoch erhalten. Ich war zunächst völlig verblüfft über diesen Effekt. Zumal der Einsatz unserer lokalen Versionen der gleichen PHP-Klassen kein Verschwinden der Textstrings zeitigte.

Die Analyse war nicht ohne, da ich zunächst nicht wusste, wonach ich zu suchen hatte. Man denkt da zunächst natürlich an Unterschiede im Programmcode selbst. Am Schluss entpuppten sich aber eine im wesentlichen unmodifizierte Klasse, die die Datenbankverbindung steuert, sowie eine Klasse, die die Strings aus Sicherheitsgründen nach unerlaubten Sequenzen filtert, als Kerne des Übels. Ausschlaggebend waren allerdings nicht Programmunterschiede sondern bestimmte Parameter-Setzungen sowie Server- und Zeichensatzeinstellungen. Aber der Reihe nach.

Zeichensatzeinstellungen in der Kommunikationskette zwischen einem PHP-Server und einer MySQL-Datenbank

In der Datenaustausch-Kette zwischen einer MySQL-Datenbank und PHP-Modulen auf einem Web-Server (und natürlich auch bei der Übersendung eines HTML-/XML- oder JSON-Outputs an Web-Clients) spielen verschiedene Zeichensatzeinstellungen eine Rolle. Einige davon können vom Entwickler beeinflusst werden. Andere wiederum nicht immer.

Für unseren Fall waren vor allem die nachfolgenden Punkte relevant; sie betreffen den Web-Server (mit PHP-Modul) und die MySQL-Datenbank:

  1. Zeichensätze (“Kollationen”) der MySQL-Datenbank und zugehöriger Datenbanktabellen.
  2. Zeichensatz-Vorgaben zur MySQL-Verbindung und zum Datentransfer von und zu (PHP-)Programmen, die mittels der SQL-Direktive “SET NAMES” vorgenommen wurden.
  3. Einstellungen für den Default-Character-Set des PHP-Apache-Moduls.
  4. Zeichensatzeinstellungen für die PHP-Funktion “htmlentities()”.

Zeichensätze in der Datenbank und die Direktive “SET NAMES”

Für die Kollation der relevanten MySQL-Datenbank und ihrer Tabellen war auf dem Kundenserver “latin1_german2_ci” gewählt worden. Unsere Einstellungen im lokalen Testsystem waren dazu auf Tabellenebene kompatibel. Die Kollation einer Datenbanktabelle bestimmt letztlich aber nur die interne Ablage der Daten in der Tabelle und nicht den Zeichensatz, unter dem z.B. per SQL ermittelte Resultsets an weiterverarbeitende Programme übermittelt werden.

Für letzteres sind andere Parameter verantwortlich, die man als Entwickler für eine spezifische Verbindung zur MySQL-Datenbank einstellen kann. Unter MySQL und der MariaDB nutzt man dafür etwa die SQL-Direktive “SET NAMES” (oder aber die Funktion mysqli_set_charset(); s.u.).

“SET NAMES” führt zur gleichzeitigen Festlegung dreier RDBMS-Parameter für die Behandlung einer spezifischen Datenbankverbindung. Diese Parameter sind: character_set_client, character_set_connection, character_set_results.

Siehe hierzu etwa
https://dev.mysql.com/doc/refman/5.7/en/set-names.html
und
https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_character_set_results.

Die Datenbankverbindung wird unter dem PHP/mysqli-Interface dabei über das Connection-Objekt

$dbi = mysqli_connect(….)

identifiziert. Im Wesentlichen legen die genannten Parameter Folgendes fest:

  • character_set_client: Zeichensatz, unter dem Daten, die vom Datenbank-Client zum RDBMS-Server transferiert werden, interpretiert werden.
  • character_set_connection: Handhabung bestimmterErsetzungen und Konversionen, u.a. von Numbers zu Strings.
  • character_set_results: Zeichensatz, unter dem Daten, die vom RDBMS zu Client-Programmen übermittelt werden interpretiert werden.

Der “Client” ist in unserem Fall natürlich ein PHP-Programm auf einem Apache-Server. Die Anwendung des “SET NAMES”-Statements durch ein PHP-Programm, z.B.

$sql_unames = “SET NAMES ‘utf8′”;
$this->dbi->query($sql_unames);
//dbi->Datenbank-Connection Object
//$this->dbi = mysqli_connect(….)

ist somit von zentraler Bedeutung für die Kommunikation zwischen einem PHP-Programm und einer MySQL/MariaDB! Solange eine hinreichende Konvertierung verwendeter Zeichen gewährleistet ist, muss der Zeichensatz, unter dem Resultsets zum PHP-Programm übermittelt werden, nicht zwingend mit dem der Datenbanktabellen selbst identisch sein.

Es sei darauf hingewiesen, dass es zu “SET NAMES” auch Alternativen gibt, die man im Rahmen des mysqli-Interfaces einsetzen kann und sollte (s.u.). Dass wir in einigen Programmen noch SET NAMES verwenden, hat lediglich historische Gründe. Die hier beschriebene Problematik gilt aber unabhängig vom genauen Werkzeug zur Einstellung der Zeichensatzparameter.

Konsistenz zu Zeichensatzvorgaben für PHP – Einstellungen in der php.ini

Das PHP-Programm muss in jedem Fall mit dem gewählten Zeichensatz für Resultsets adäquat umgehen können; s.u.. Der rechte Bereich der folgenden Skizze, die ich aus einem anderen Artikel dieses Blogs entliehen habe, verdeutlicht das für den Fall “SET NAMES ‘latin1′” :

Nun könnte man in seinen PHP-Programmen natürlich spezifische Umwandlungsfunktionen (u.a. iconv(), mb_convert_encoding(), utf8-encode(), utf8-decode) für die Zeichensatzkonvertierung aus- und eingehender Strings bemühen. Die linke Seite der Skizze liefert hierfür Beispiele (s. den dazu gehörigen Abschnitt weiter unten).

Wenn möglich, kann man aber auch einen Standardzeichensatz für die PHP-Verarbeitung von Strings vorgeben und sich darauf bzgl. der Datenbankinteraktion verlassen.

Entsprechende Einstellungen nimmt man in der Konfigurationsdatei
/etc/php7/apache2/php.ini
vor. Die relevanten Parameter sind dort:

; PHP's default character set is set to UTF-8.
; http://php.net/default-charset
default_charset = "UTF-8"

; PHP internal character encoding is set to empty.
; If empty, default_charset is used.
; http://php.net/internal-encoding
;internal_encoding =

; PHP input character encoding is set to empty.
; If empty, default_charset is used.
; http://php.net/input-encoding
;input_encoding =

; PHP output character encoding is set to empty.
; If empty, default_charset is used.
; mbstring or iconv output handler is used.
; See also output_buffer.
; http://php.net/output-encoding
;output_encoding =

Die hierfür gesetzten Werten sollte natürlich mit den Einstellungen für die Datenbankverbindung zusammenpassen.

Kundenvorgaben

Wir parametrieren in Kundenprojekten die PHP-Methoden für den Verbindungsaufbau zum Datenbankserver meist gemäß expliziter Kundenvorgaben. In unserem Fall hatten wir “SET NAMES ‘latin1′” gewählt. (Hinweis: Die Zeichensatz-Namen auf einem MySQL-Server enthalten grundsätzlich keine Trennzeichen; daher ist statt iso8859-1 “latin1” zu verwenden).

Der Grund dafür war, dass das Apache-PHP-Modul des (gehosteten) Kunden-Web-Servers auf “iso-8859-1” eingestellt war. Das bestätigte eine Überprüfung mit phpinfo(). Diese Einstellungen sollten wir gem. Kundenvorgabe nicht ändern, da auf dem Web-Server auch andere PHP-Programme als unsere eigenen laufen müssen.

Auf dem Kundenserver waren die Zeichensatzeinstellungen also konsistent.

Ursache unseres Problems und die Rolle von htmlentities()

Man ahnt es bereits: Durch das Kopieren der PHP-Klasse für die Steuerung von Datenbankverbindungen vom Kundenserver auf die Testdomäne unseres lokalen Web-Servers entstand dort u.a. eine Inkonsistenz zwischen der Zeichensatzbehandlung unter PHP (utf8!) und dem Zeichensatz für den Transfer von Resultsets aus der MySQL-Datenbank (latin1).

Diese Inkonsistenz kam jedoch noch nicht zum Tragen, als wir die Daten per Copy/Paste über lokale Web-Interfaces einer UTF8-Standard-Domäne in die Bank einbrachten.

Für unsere Testdomäne dagegen muss man jedoch u.a. eine fehlerhafte Konvertierung von deutschen Umlauten erwarten! Warum aber verschwanden die Text-Strings in Gänze aus den Web-Oberflächen?

Die Antwort lieferte schließlich eine von mir bislang zu wenig beachtete Eigenschaft der PHP-Funktion “htmlentities()”.

Unsere Web-Generatoren jagen Strings vor einer Web-Darstellung durch eine Reihe von Prüfroutinen, Transformatoren für erlaubte Zeichenfolgen und durch Filter (u.a. HTMLPurifier, aber auch eigene Filter). Dabei wird in einem Zwischenschritt (nach einer vorhergehenden Konvertierung erlaubter HTML-Zeichenfolgen) auch “htmlentities()” eingesetzt.

htmlentities() erlaubt selbst eine Vorgabe des “Character Sets” über einen Parameter. Ein Check zeigte: Dieser Parameter stand in der lokalen Testdomäne explizit auf “UTF-8”. Diese Einstellung betrifft jedoch eine Konvertierung in den gewünschten Ziel-Zeichensatz für den HTML-Output. Hier hatten wir kein Problem, da die
HTML-Header der Webseiten bzw. die vom Apache-Server generierten HTTP-Header tatsächlich auf UTF-8 ausgerichtet waren.

Allerdings sorgten schon die vorhergehenden Widersprüche zwischen dem Zeichensatz der Datenbank-Resultsets und dem Zeichensatz für die anschließende Behandlung von Strings durch PHP. Das hatte gravierende Folgen. Unter http://php.net/manual/de/function.htmlentities.php findet man nämlich folgenden Hinweis:

Rückgabewerte
Gibt die kodierte Zeichenkette zurück. Enthält der string eine in dem übergebenen encoding ungültige Code Unit Sequenz, wird eine leere Zeichenkette zurückgegeben, sofern weder das ENT_IGNORE noch das ENT_SUBSITUTE Flag gesetzt sind.

(Hervorhebung durch mich).

Das war des Rätsels Lösung:

Eine Inkonsistenz in der Zeichensatzbehandlung im Datenaustausch zwischen unserer MySQL-Datenbank und PHP führte zu nicht behandelbaren Zeichen bei der Anwendung von htmlentities() auf Strings – und diese Funktion produzierte dann gemäß ihrer Default-Einstellungen leere Strings.

Trivial – man muss es halt nur wissen! Ein Test mit

“SET NAMES ‘utf8′”

ließ denn alle verschwundenen Strings auch in unserer Testdomäne prompt wieder erscheinen!

Notwendige Checks vor der Anwendung von SET NAMES (oder von mysqli_set_charset())

Hat man die Kette der Zeichensatzsetzungen bzw. Zeichensatzbehandlung im Austausch zwischen PHP und einer MySQL-Datenbank erst einmal verstanden, so ist auch klar, wo man mit vorbeugenden Maßnahmen ansetzen kann. Solche Vorkehrungen sind – wie das Beispiel zeigt – vor allem dann notwendig, wenn man die Zeichensatz-Einstellungen für PHP nicht auf allen involvierten Web-Servern beeinflussen kann oder darf.

Bevor man “SET NAMES” (oder mysqli_set_charset(); s.u.) in einem PHP-Programm tatsächlich anwendet, sollte man die Setzung des “Default Character Sets” in der php.ini für den aktuellen Server explizit abfragen – mittels

ini_get(‘default_charset’);

– und dann mit der ebenfalls abfragbaren Kollation der Datenbanktabellen vergleichen. Das Ergebnis dieses Vergleichs kann man dann nach bestimmten Regeln behandeln:

Z.B. Warnhinweise bei (ernsthafter) Inkompatibilität ausgeben. Oder wenn man wirklich sicher ist, dass nur deutsche Umlaute zu potentiellen Problemen führen können: Wahl des zu den PHP-Einstellungen konsistenten Zeichensatzes für den Transfer von Resultsets aus der Datenbank. In unserem Fall also “utf8”.

Alternative: Ändern der php.ini-Vorgaben für den Zeichensatz durch und für das laufende PHP-Programm

Auf festgestellte Inkonsistenzen in den Zeichensatzeinstellungen kann man u.U. – nämlich wenn man die dafür nötigen Rechte besitzt – auch mit einer Modifikation der php.ini-Vorgaben für das laufende Programm reagieren. Dazu muss man natürlich die Funktion ini_set() bemühen; Bsp.:

ini_set( string ‘default_charset’, ‘ISO-8859-1’)

bemühen.

Empfohlener Weg zum Setzen des “Character Sets” für eine MySQL-Verbindung

Ich möchte explizit darauf hinweisen, dass es andere Möglichkeiten als die SQL-Direktive “SET NAMES” gibt, Character Sets für die Datenbankverbindung zu setzen. Das PHP-Manual empfiehlt explizit, die Funktion

mysqli_set_charset(mysqli $link , string $charset)

anstelle von SQL und “Set Names” zu verwenden. Siehe: http://php.net/manual/de/mysqli.set-charset.php

Zeichensätze und die Web-Client-Seite

Obwohl nicht Kernthema dieses Artikels werfen wir noch einen ergänzenden Blick auf den
Datenaustausch des PHP/Web-Servers mit Web-Clients (z.B. einem Browser). Die Zeichensatzthematik setzt sich natürlich auch auf dieser Kommunikationsstrecke fort; der linke Teil der obigen Skizze verdeutlicht das am Beispiel von Ajax/Ajaj-Programmen. Diese erfordern i.d.R. UTF-8 für einen ordnungsgemäßen Datenaustausch mit dem Web-Server.

Ist der Parameter “default_charset” in der php.ini-Datei aber auf “iso-8859-1” gesetzt, so muss man ein- und ausgehende Daten entsprechend konvertieren. Dafür eignen sich die Funktionen utf8_decode() für einlaufende POST-/GET-Daten aus Ajax-Programmen und utf8_encode() bei der Erzeugung des Ajax/Ajaj-Outputs in Richtung Web-Client.

Für reinen HTML-Output gilt analoges; dabei sind aber auch HTTP- und HTML-Header-Anweisungen für Character Sets zu setzen. Siehe hierzu:
http://www.html-info.eu/php/php-als-script-sprache/item/zeichensatz-latin1-oder-unicode-utf-8.html

Fazit

In unbeabsichtigter Weise kann htmlentities() plötzlich zu einer Testfunktion für die Konsistenz zwischen

  • der Zeichensatz-Einstellungen durch “SET NAMES” bzw. mysqli_set_charset() für die MySQL/MariaDB-Datenbankanbindung an ein PHP-Programm
  • und den Zeichensatzeinstellungen für das PHP-Modul selbst auf dem Webserver

werden – und in letzter Konsequenz zu leeren Strings auf Webseiten führen.

Es lohnt sich vor einem Einsatz von “SET NAMES” – oder besser mysqli_set_charset() – eigentlich immer, die Einstellungen in der “php.ini” bzgl. des “default_charset” und verwandter Parameter abzufragen und daraus angemessene Konsequenzen für den Aufbau der Datenbankverbindung zu ziehen.

Dies gilt vor allem dann, wenn man im Rahmen von Tests und Produktivierungen auf verschiedenen Servern arbeiten will oder muss – und dabei nicht alle Konfigurationsparameter der Server selbst beeinflussen kann und darf.

Neben dem Setzen von Server- und Verbindungsparametern kann man auf festgestellte oder vorgegebene Zeichensatzanforderungen oder Zeichensatzdiskrepanzen aber auch gezielt mit verschiedenen Funktionen reagieren, die PHP für eine Zeichensatz-Detektion und eine explizite Zeichensatzkonvertierung von Strings anbietet.