Benchmarking MariaDB mit (& ohne) ProxySQL mit (& ohne) Docker, Teil 1 von 2

Zugegeben, eigentlich wollte ich gar keine Benchmark-Tests durchführen. Zumindest war dies nicht der erste Gedanke, als ich mich seit längerer Zeit einmal wieder mit MySQL bzw. MariaDB und Docker, insbesondere dessen neuen Features, beschäftigt habe.

Bewegte Ziele: MySQL/MariaDB mit Docker

Auch das Ziel sollte ein anderes sein als der aktuelle Stand widerspiegelt, aber ich wollte mich zunächst langsam darauf zu bewegen. Ein Zwischenstadium sieht somit wie folgt aus: Mehrere Hosts, darauf Docker CE 17.09 im swam mode, und irgendwie sollte darauf eine MariaDB-Datenbank eingerichtet werden. Irgendwie? Genau, eigentlich irgendwann auf mehreren Hosts zwecks Ausfallsicherheit, so dass die Daten mindestens auf drei unterschiedlichen Maschinen lagern. Dazu bot sich geradezu die Verwendung des Galera Clusters an, sofern eine Master-Slave-Konfiguration nicht ausreichen sollte. Jedoch wollte ich MariaDB möglichst mit Docker-Bordmitteln einrichten und noch dazu möglichst wenig im Nachhinein konfigurieren müssen. Mit Galera Cluster hatte ich bereits einige Erfahrungen machen dürfen, die übliche Installation sieht grob beschrieben so aus, dass ein Node als Bootstrap-Node dient, d.h. ein Node wird initial eingerichtet, woraufhin sich Schritt für Schritt die weiteren Nodes mit dem ersten Node verbinden und die Daten untereinander austauschen. Nach dieser Bootstrap-Phase stehen auf allen Nodes identische Daten zur Verfügung, und angesichts des synchronen Replikation ist zu jeder Zeit auf allen Nodes dieser Multi-Master-Konfiguration derselbe Datenbestand verfügbar. Somit können sich Clients mit jedem Node verbinden, z.B. über einen Load-Balancer, teilweise bieten auch die Client-Libraries entsprechende Funktionen an, um mehrere Nodes in der Konfiguration zu hinterlegen und bei einem Ausfall eines Nodes automatisch auf einen anderen zu wechseln.

Das klingt vom ersten Eindruck her durchaus positiv, aber das Konzept vom Docker swarm mode geht beinahe schon in die andere Richtung: Ein Verbund von Containern kann Stack definiert werden, dazu gehören einerseits die einzelnen Services, hinter denen sich letztlich Container verbergen. Diese Services beinhalten jedoch nicht nur einzelne Container, sondern auch deren Verhalten im Swarm, so kann z.B. die Anzahl laufender  Container (“Tasks”) erhöht oder verringert werden. Die Docker-Engine sorgt dabei für die Verteilung im Swarm, so dass nicht alle Container auf demselben Node platziert werden. Bei der Einrichtung der Services werden neben gewünschter Anzahl von Tasks auch Angaben wie das zu verwendende Docker-Netzwerk oder die freizugebenden Ports angegeben. Services können entweder direkt per Kommandozeilenparameter aufgebaut werden oder liegen als Definition in einem Docker-Compose-yml-File vor. Letzteres ist auf jeden Fall komfortabler, auch weil die Datei versioniert werden kann.

Die Docker-Container bzw. Tasks sind im besten Fall “ephemeral”, die beste Übersetzung dafür ist wohl “kurzlebig”, können beliebig gestartet und beendet werden, ohne dass es zu Zustandsänderungen kommt, d.h. Nutzdaten sollten sich nicht darin befinden, da diese schlicht und einfach nicht mehr vorhanden wären. Insofern müssen die Daten der Datenbank irgendwo gespeichert werden, dazu dienen Verzeichnisse auf dem Host, die in die Container hinein gemountet werden können, oder aber Volume-Container, d.h. Container, die rein zur Datenhaltung existieren. Jedoch sind beide Konzepte wiederum an einen Host gebunden, so dass die Docker-Engine die MariaDB-Container eben nicht beliebig verschieben können soll.

Um dies zu realisieren, lassen sich Constraints, also Bedingungen bzw. Einschränkungen definieren, so dass z.B. ein Datenbank-Container immer auf demselben Host gestartet wird. Insofern habe ich mich zunächst auf die Suche nach Anleitungen bzw. Hinweisen begeben, wie eine derartige Installation eingerichtet werden kann, d.h. MariaDB Galera Cluster in einem Docker Swarm mit swarm mode. Tatsächlich findet sich dazu so gut wie nichts. Vielfach beschäftigen sich entsprechende Blog-Einträge noch mit dem alten Docker Swarm (vor 1.12), oder nur mit Galera Cluster und dessen Einrichtung oder aber mit speziellen Voraussetzungen des Hostings, z.B. AWS oder SoftLayer. Bisher habe ich nur einen Beitrag gefunden, der den Swarm mode und Galera Cluster thematisiert. Darüber hinaus gibt es von der MariaDB-“M|17”-Konferenz einen Vortrag, der die Details erklärt. Dabei wird jedoch die Speicherung von Zuständen des Clusters bzw. einzelner Nodes in einem weiteren Container bzw. einem weiteren verteilten Speichersystem, und zwar etcd vorgenommen. Dessen Notwendigkeit ergibt sich daraus, dass es zwar mittels neuer HEALTHCHECK-Anweisung innerhalb des Dockerfile möglich ist, festzustellen, ob ein Dienst läuft oder nicht, aber weitere Zustände sind dabei noch nicht möglich. Es ist insofern einiger Aufwand notwendig, damit sich einzelne MariaDB-Galera-Container “automatisch” mit dem Cluster verbinden, Daten abgleichen usw.. Oder um es kurz zu machen – es ist kompliziert.

Als ersten Zwischenschritt wollte ich daher zunächst einen Docker-Container bzw. den dazu gehörigen Service auf einem Node mit MariaDB zum Laufen bringen. Alles andere sollte später realisiert werden.

Docker Secrets

Ein relativ neues Feature von Docker sind die so genannten Secrets. Dabei können z.B. Passwörter unter einer Kennung (Key) hinterlegt werden, die dann von Docker verschlüsselt gespeichert und auf allen Nodes verteilt werden. Innerhalb der Swarm-Container stehen diese Daten (lt. Docker bis zu 500 kb) dann zur Verfügung, so dass es nicht mehr notwendig ist, Passwörter oder ähnliches (SSH-Keys, Zertifikate) manuell auf die Nodes zu verteilen oder als Umgebungsvariable zu übermitteln. Innerhalb der laufenden Container können diese Secrets dann als Datei an einem bestimmten Platz zur Verfügung stehen.

Ein ähnliches Feature sind die Docker Configs. Dabei können Konfigurationen hinterlegt werden, dies kann eine Zeichenkette in der Form Key-Value sein, aber auch Dateien, die zur Konfiguration der Container verwendet werden. Auch diese werden von der Docker-Engine verwaltet, so dass sie auf allen Nodes zur Verfügung stehen und in die jeweiligen Container gemountet werden können.

Ich habe zunächst das gewünschte MariaDB-Root-Passwort als Secret angelegt:

geschke@connewitz:~$ echo "GANZ TOLLES PASSWORT" | docker secret create mysql_root_password -
kymul4l70d3x9qqkkoql74r6t

geschke@tondi:~$ docker secret ls
ID                          NAME                  CREATED             UPDATED
kymul4l70d3x9qqkkoql74r6t   mysql_root_password   7 days ago          7 days ago
geschke@tondi:~$ docker secret inspect mysql_root_password
[
    {
        "ID": "kymul4l70d3x9qqkkoql74r6t",
        "Version": {
            "Index": 113149
        },
        "CreatedAt": "2017-10-10T14:28:01.740799954Z",
        "UpdatedAt": "2017-10-10T14:28:01.740799954Z",
        "Spec": {
            "Name": "mysql_root_password",
            "Labels": {}
        }
    }
]

Wie man sieht, wurde das Secret auf der VM “connewitz” angelegt, daraufhin auf “tondi” geprüft, wo es ebenfalls verfügbar war. Dieser Schritt war insofern schon einmal erfolgreich.

Der nächste leider weniger:

docker run -d --name mariadb-1 -e MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password mariadb:10.3

geschke@connewitz:~$ docker logs mariadb-1
/usr/local/bin/docker-entrypoint.sh: line 37: /run/secrets/mysql_root_password: No such file or directory

Interessant, das Secret war als Datei einfach nicht da. Ich hatte zugegebenermaßen zunächst einen Blog-Eintrag über die Secrets gelesen, in der ein Run-Kommando in genau dieser Form skizziert wurde. Aber entweder der Beitrag war veraltet oder Docker hatte bereits wieder etwas geändert, denn laut Docker kann dies so gar nicht funktionieren, da die Secrets nur im Swam mode zur Verfügung stehen. Manchmal hilft doch ein Blick in die offizielle Dokumentation…

Ein erster Docker Service

Daraufhin wollte ich einen Docker Service bauen, jedoch direkt per docker-compose-File und nicht erst auf der Kommandozeile. Die Zwischenschritte außen vor gelassen, sah das Ergebnis wie folgt aus:

version: '3.1'

services:
   mariadb:
     image: mariadb:latest
     deploy:
       replicas: 1
       placement:
         constraints:
           - node.hostname == connewitz
     volumes:
       - mysql-data:/var/lib/mysql
     environment:
       MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
     secrets:
       - mysql_root_password
     networks:
       - mariadb_net
     ports:
       - "3306:3306" 

secrets:
   mysql_root_password:
     external: true

volumes:
    mysql-data:
      external: true

networks:
  mariadb_net:
    driver: overlay
    #external: true

Da ich das MySQL-Root-Passwort zuvor angelegt hatte, ist es mit “external: true” definiert. Ebenfalls wurde das Volume bereits angelegt:

geschke@connewitz:~/mysql$ docker volume create mysql-data
mysql-data

Das Overlay-Network wurde zwar ebenfalls zuvor angelegt, in der Fassung jedoch nicht benutzt, was beim Starten des Stacks auffällt:

geschke@connewitz:~/mysql$ docker stack deploy -c connewitz.yml db
Creating network db_mariadb_net
Creating service db_mariadb

Da sowohl Netzwerk als auch Service beim Löschen per Docker stack-Kommando wieder entfernt werden, ist dies kein Problem, andernfalls kann das Netzwerk ebenfalls mittels “external: true” definiert werden.

Der erste Schritt war somit erledigt – MariaDB läuft, ist auf Port 3306 von außen erreichbar, User, Datenbanken etc. können angelegt werden.

MySQL-/MariaDB-Proxy-Server

Daraufhin bin ich irgendwie auf die verschiedenen MySQL-Proxy-Server gestoßen. Zwar waren diese teilweise bereits bekannt, aber insbesondere die Features von ProxySQL stießen auf mein Interesse. Vielleicht ließe sich damit ein einfaches Failover realisieren, ohne auf eine recht komplexe Galera-Cluster-Konfiguration zurückgreifen zu müssen… Noch dazu steht ProxySQL unter GPL, während die Lizenzbedingungen von MariaDB MaxScale seit Version 2.0 restriktiver geworden sind. Einen schönen Überblick gibt eine als “The Proxy Wars” betitelte Präsentation.

Der Gedanke lag nahe, auch ProxySQL in einem Docker-Service zu betreiben und sich dann mit den weiteren Features, insbesondere Cluster-Fähigkeiten zu beschäftigen.

Docker-Image für ProxySQL

Da mir die vorhandenen Docker-Images von ProxySQL nicht hundertprozentig gefielen, habe ich mir aus den Extrakten ein eigenes erstellt. Der Grund ist, dass manche Images auf älteren ProxySQL-Versionen basieren, andere nutzen den Quellcode anstatt eines von ProxySQL bereit gestellten Binaries, wiederum andere bringen noch weitere Programme in das ProxySQL-Image mit ein. Das Image sollte jedoch so einfach wie möglich sein, zumindest für ein paar erste Tests. Daher ist es bislang auch nicht optimiert, die vielen RUN-Anweisungen würde ich letztlich in eine Zeile packen, damit nicht für jeden RUN-Befehl ein eigener Layer aufgebaut wird. Die aktuelle Fassung des Dockerfiles sieht so aus:

FROM phusion/baseimage

MAINTAINER Ralf Geschke <ralf@kuerbis.org>

RUN apt update && apt install -y wget

RUN cd /tmp/ && wget https://github.com/sysown/proxysql/releases/download/v1.4.3/proxysql_1.4.3-ubuntu16_amd64.deb

RUN cd /tmp/ && dpkg -i /tmp/proxysql_1.4.3-ubuntu16_amd64.deb
RUN rm -rf /var/lib/apt/lists/*

RUN rm /tmp/proxysql*
#COPY proxysql.cnf /etc/proxysql.cnf 


COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

EXPOSE 6032 6033

CMD [""]

Dazu gehört das entrypoint.sh-Skript:

#!/bin/bash

/usr/bin/proxysql --initial -f -c /etc/proxysql.cnf 

Letztlich benötigt das Image auch eine Konfigurationsdatei /etc/proxysql.cnf. Genau diese wollte ich per Docker config bereit stellen. Dazu habe ich die beim Ubuntu- bzw. Debian-Paket mitgelieferte proxysql.cnf kopiert und editiert. Die Änderungen beschränkten sich jedoch auf die Einstellung mysql_users und mysql_servers. D.h. je nach dem, welcher Server gerade angesprochen werden sollte, wurde die Einstellung geändert, im Beispiel:

mysql_servers=
(
 	{
 		#address="mariadb"
 		#address="connewitz"
		address="moeckern"
 		port=3306
 		hostgroup=0
 		max_connections=200
 	}
)

[...]

mysql_users=
(

	{
		username="sbtest"
		password="TOLLES PASSWORT"
		default_hostgroup=0
	}

)
[...]

In dem Fall kann also die Verbindung zum MySQL-Host “moeckern” hergestellt werden, dabei wird die Datenbank “sbtest” verwendet, das Passwort muss ebenfalls angegeben werden. ProxySQL nutzt diese Daten und verbindet sich anschließend zu besagtem Server.

Die Konfigurationsoptionen sind sehr umfangreich, insbesondere wenn die richtig interessanten ProxySQL-Features verwendet werden. Ich wollte jedoch zunächst ProxySQL tatsächlich nur testen, bzw. es sollte überhaupt einmal laufen.

Docker Configs

Als nächstes habe ich die Datei proxysql.cnf als Docker-Config importiert:

geschke@connewitz:~/docker-proxysql$ docker config create proxysql.cnf proxysql.cnf
5c7ay3pxuokkzub3e978xa14b

Ein Hinweis vorweg – bei einer Änderung des Inhalts der Datei proxysql.cnf muss dies Docker mitgeteilt werden. Ein direktes Update ist leider nicht möglich, insofern muss die config-Einstellung gelöscht und neu erstellt werden:

geschke@connewitz:~/docker-proxysql$ docker config rm proxysql.cnf
proxysql.cnf
geschke@connewitz:~/docker-proxysql$ docker config create proxysql.cnf proxysql.cnf
jdgte6wfrpbvjltcyovhjmoqw

Docker Stack via Compose-File

Die endgültige Docker-Compose-Datei für beide Services sieht dabei wie folgt aus:

version: '3.3'

services:
  mariadb:
    image: mariadb:latest
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.hostname == connewitz
    volumes:
      - mysql-data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
    secrets:
      - mysql_root_password
    networks:
      - mariadb_net
    ports:
      - "3306:3306"

  proxysql:
    image: hub.kuerbis.org:81/proxysql
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.hostname == connewitz
    configs:
      - source: proxysql.cnf
        target: /etc/proxysql.cnf
    networks:
      - mariadb_net
    ports:
      - "6032:6032"
      - "6033:6033"
    
secrets:
  mysql_root_password:
    external: true

volumes:
  mysql-data:
    external: true

networks:
  mariadb_net:
    driver: overlay
    external: true

configs:
  proxysql.cnf:
    external: true

Dazu einige Anmerkungen:

  • Zwingend notwendig für die Verwendung der “configs”-Option ist die Deklarierung als “version: ‘3.3’”. Andernfalls wird eine Fehlermeldung ausgegeben.
  • Das Secret, Network und die Configs sind extern definiert. Das Netzwerk wie üblich, das Secret und die Config wie oben beschrieben.
  • Nach außen sind der MariaDB-Port als auch die ProxySQL-Ports freigegeben. Im Produktivbetrieb von ProxySQL wäre es zumindest nicht notwendig, den MariaDB-Port freizugeben, da sämtlicher Traffic über ProxySQL läuft.
  • Die Constraints zwingen Docker dazu, beide Services auf demselben Host zu starten. Bei ProxySQL wäre es zwar recht einfach möglich, den Container auf einem anderen laufen zu lassen, jedoch ergeben sich für einen Benchmark dann wiederum Seiteneffekte (Leistung des Hosts bzw. der virtuellen Maschine, Netzwerk usw.).
  • Das ProxySQL-Image wurde lokal erzeugt (docker build -t proxysql .) mit dem o.g. Dockerfile und dann in die private Registry kopiert. Daher lautet der Image-Name “hub.kuerbis.org:81/proxysql” anstatt nur “proxysql”.
  • Es soll jeweils nur eine Instanz der beiden Container erstellt werden. Während eine Erhöhung bei ProxySQL eigentlich kein Problem sein sollte, würde es bei MariaDB zu einigem Chaos bei der Datenspeicherung führen.

Der Stack kann gestartet werden mit:

geschke@connewitz:~/docker-proxysql$ docker stack deploy -c docker-compose.yml --with-registry-auth db
Creating service db_mariadb
Creating service db_proxysql

Die Option --with-registry-auth ist nur dann notwendig, wenn das Docker-Image nicht bereits lokal vorliegt und die Credentials für die Registry auf anderen Nodes eingesetzt werden sollen. Nach kurzer Zeit sollten beide Services laufen:

geschke@connewitz:~/docker-proxysql$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE                                PORTS
t4wdnsm6wu3l        db_mariadb          replicated          1/1                 mariadb:latest                       *:3306->3306/tcp
9z0n76s1g2b3        db_proxysql         replicated          1/1                 hub.kuerbis.org:81/proxysql:latest   *:6032->6032/tcp,*:6033->6033/tcp

Das Ergebnis ist nicht weiter überraschend – man kann sich sowohl direkt mit MariaDB als auch über ProxySQL verbinden:

geschke@moeckern:~$ mysql -u sbtest -p -P 3306 -h moeckern sbtest
Enter password:
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 2703
Server version: 10.0.24-MariaDB-1~wily mariadb.org binary distribution

Copyright (c) 2000, 2016, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [sbtest]> show tables;
+------------------+
| Tables_in_sbtest |
+------------------+
| sbtest1          |




geschke@moeckern:~$ mysql -u sbtest -p -P 6033 -h connewitz sbtest
Enter password:
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 5.5.30 (ProxySQL)

Copyright (c) 2000, 2016, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [sbtest]> show tables;
+------------------+
| Tables_in_sbtest |
+------------------+
| sbtest1          |

Das entspricht zunächst genau den Erwartungen, aber daraufhin war ich neugierig, welche Auswirkungen ProxySQL hat. Ein erster, sehr kurzer Test mit sysbench hatte mich dann doch ein wenig überrascht.

Fazit und Ausblick

Immerhin konnten bereits die neuen Features von Docker eingesetzt werden, d.h. secrets sowie configs. Beides verspricht einen wesentlichen Fortschritt gegenüber den bisherigen Verfahren, bedarf aber auch des Einsatzes der neuesten Docker-Engine und insofern einigen Updates, auch bzgl. der bisherigen Infrastruktur, Docker-Images, Deploy-Methoden usw..

Tatsächlich sind die bisherigen Ausführungen ein wenig umfangreicher geworden. Daher – Spannung – widme ich mich den Benchmark-Ergebnissen in einem nächsten Teil.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Tags: