392 lines
12 KiB
Markdown
392 lines
12 KiB
Markdown
# JTL-Shop MariaDB Optimization
|
||
|
||
Automatisierte Index-Optimierung, FULLTEXT-Suche und MariaDB-Konfiguration für JTL-Shop-Datenbanken auf Multi-Shop-Servern.
|
||
|
||
## 🎯 Problemstellung
|
||
|
||
Bei der Analyse eines Plesk-Servers mit **50 JTL-Shops** wurden massive Performance-Probleme identifiziert:
|
||
|
||
- **24,4% aller Queries** waren Slow Queries (>1 Sekunde)
|
||
- **~250 Slow Queries pro Sekunde**
|
||
- Hauptursache: Fehlende Indizes und deaktivierte FULLTEXT-Suche
|
||
|
||
## 📊 Analyse-Ergebnisse
|
||
|
||
### Systemübersicht (Beispiel-Server)
|
||
|
||
| Ressource | Wert |
|
||
|-----------|------|
|
||
| RAM | 98 GB |
|
||
| CPU | 14 Kerne |
|
||
| Disk | SSD (NVMe) |
|
||
| MariaDB | 10.11.13 |
|
||
| DB Größe | ~20 GB |
|
||
| Shops | 28 Datenbanken |
|
||
| Tabellen | ~14.350 |
|
||
|
||
### Top Slow Queries nach Ursache
|
||
|
||
| Rang | Problem | Anteil | Ursache |
|
||
|------|---------|--------|---------|
|
||
| #1 | Suchcache INSERT | **68,6%** | FULLTEXT deaktiviert, LIKE '%...%' |
|
||
| #2 | tbesucherarchiv | 9,3% | INSTR() verhindert Index-Nutzung |
|
||
| #3-6 | Filter-Queries | ~6% | JTL-interne Query-Struktur |
|
||
| #14 | tlieferscheinposinfo | 0,5% | Fehlender Index |
|
||
| #17 | tkampagnevorgang | 0,4% | Suboptimaler Index |
|
||
| #19 | tlieferscheinpos | 0,3% | Fehlender Index |
|
||
|
||
### Identifizierte fehlende Indizes
|
||
|
||
| Tabelle | Fehlender Index | Betroffene Shops | Rows Examined |
|
||
|---------|-----------------|------------------|---------------|
|
||
| `tartikel` | FULLTEXT | 25 von 28 | bis 468.000 |
|
||
| `tlieferscheinposinfo` | `kLieferscheinPos` | 11 von 28 | ~280.000 |
|
||
| `tlieferscheinpos` | `kLieferschein` | 11 von 28 | ~215.000 |
|
||
| `tkampagnevorgang` | `kKampagneDef, kKey` | 28 von 28 | ~50.000 |
|
||
| `txsellkauf` | `kXSellArtikel` | 28 von 28 | bis 2.000.000 |
|
||
|
||
### Hauptproblem: FULLTEXT-Suche
|
||
|
||
**Befund:** Nur 2 von 28 Shops hatten die FULLTEXT-Suche aktiviert.
|
||
|
||
```sql
|
||
-- JTL mit deaktiviertem FULLTEXT (langsam):
|
||
WHERE tartikel.cName LIKE '%suchbegriff%'
|
||
-- → Full Table Scan über 468.000 Zeilen!
|
||
|
||
-- JTL mit aktiviertem FULLTEXT (schnell):
|
||
WHERE MATCH(cName, cSuchbegriffe) AGAINST('suchbegriff')
|
||
-- → Index-Nutzung, wenige Millisekunden
|
||
```
|
||
|
||
Der FULLTEXT-Index existierte auf den meisten Shops nicht, und selbst wo er existierte, war die JTL-Einstellung `suche_fulltext` auf `N` gesetzt.
|
||
|
||
## 🔧 Lösung
|
||
|
||
Das Skript `mariadb-optimization.py` behebt diese Probleme automatisiert:
|
||
|
||
### Was das Skript macht
|
||
|
||
| Phase | Aktion | Details |
|
||
|-------|--------|---------|
|
||
| **1** | Backup | Sichert alle `suche_fulltext` Einstellungen |
|
||
| **2** | Standard-Indizes | Erstellt 4 fehlende Index-Typen |
|
||
| **3** | FULLTEXT-Index | Erstellt FULLTEXT-Index auf `tartikel` |
|
||
| **4** | Einstellung aktivieren | Setzt `suche_fulltext = 'Y'` |
|
||
| **5** | MariaDB-Config | Erstellt optimierte Konfiguration & Restart |
|
||
|
||
### Erstellte Indizes
|
||
|
||
```sql
|
||
-- 1. Lieferschein-Position Info
|
||
CREATE INDEX idx_kLieferscheinPos ON tlieferscheinposinfo (kLieferscheinPos);
|
||
|
||
-- 2. Lieferschein-Position
|
||
CREATE INDEX idx_kLieferschein ON tlieferscheinpos (kLieferschein);
|
||
|
||
-- 3. Kampagne-Vorgang (Composite)
|
||
CREATE INDEX idx_kKampagneDef_kKey ON tkampagnevorgang (kKampagneDef, kKey);
|
||
|
||
-- 4. Cross-Selling
|
||
CREATE INDEX idx_kXSellArtikel ON txsellkauf (kXSellArtikel);
|
||
|
||
-- 5. FULLTEXT für Artikelsuche
|
||
CREATE FULLTEXT INDEX idx_tartikel_fulltext ON tartikel
|
||
(cName, cSeo, cSuchbegriffe, cArtNr, cKurzBeschreibung, cBeschreibung, cBarcode, cISBN, cHAN);
|
||
```
|
||
|
||
### MariaDB-Konfiguration (Phase 5)
|
||
|
||
Das Skript erstellt automatisch eine optimierte Konfiguration unter:
|
||
```
|
||
/etc/mysql/conf.d/ZZ-ztl-final-override.cnf
|
||
```
|
||
|
||
**Dynamische Buffer Pool Berechnung:**
|
||
- Ermittelt Gesamtgröße aller Datenbanken
|
||
- Berechnet: `DB-Größe × 1.2`, aufgerundet auf ganze GB
|
||
- Beispiel: 18.74 GB × 1.2 = **23 GB** Buffer Pool
|
||
|
||
**Konfigurationsinhalt:**
|
||
|
||
```ini
|
||
[mysqld]
|
||
# InnoDB Buffer Pool (dynamisch berechnet)
|
||
innodb_buffer_pool_size = XXG
|
||
innodb_buffer_pool_instances = 16
|
||
innodb_buffer_pool_dump_at_shutdown = 1
|
||
innodb_buffer_pool_load_at_startup = 1
|
||
innodb_log_file_size = 512M
|
||
innodb_log_buffer_size = 64M
|
||
innodb_flush_log_at_trx_commit = 2
|
||
|
||
# InnoDB Performance
|
||
innodb_flush_method = O_DIRECT
|
||
innodb_file_per_table = 1
|
||
innodb_io_capacity = 2000
|
||
innodb_io_capacity_max = 4000
|
||
innodb_read_io_threads = 16
|
||
innodb_write_io_threads = 16
|
||
|
||
# Query Cache deaktiviert (bei MariaDB 10.1.7+ empfohlen)
|
||
query_cache_type = 0
|
||
query_cache_size = 0
|
||
|
||
# Table Cache für viele Shops
|
||
table_definition_cache = 20000
|
||
table_open_cache = 15000
|
||
open_files_limit = 30000
|
||
|
||
# Performance
|
||
skip_name_resolve = 1
|
||
optimizer_search_depth = 0
|
||
key_buffer_size = 64M
|
||
|
||
# Slow Query Log
|
||
slow_query_log = 1
|
||
slow_query_log_file = /var/log/mysql/slow.log
|
||
long_query_time = 1.0
|
||
log_queries_not_using_indexes = 0
|
||
|
||
# Connections
|
||
max_connections = 400
|
||
thread_cache_size = 100
|
||
wait_timeout = 120
|
||
|
||
# Buffer für JTL-Queries
|
||
join_buffer_size = 4M
|
||
sort_buffer_size = 4M
|
||
read_buffer_size = 2M
|
||
read_rnd_buffer_size = 2M
|
||
```
|
||
|
||
**Wichtige Einstellungen erklärt:**
|
||
|
||
| Parameter | Wert | Funktion |
|
||
|-----------|------|----------|
|
||
| `innodb_buffer_pool_dump/load` | 1 | Buffer Pool bleibt nach Restart "warm" |
|
||
| `skip_name_resolve` | 1 | Schnellere Verbindungen (kein DNS-Lookup) |
|
||
| `optimizer_search_depth` | 0 | Auto-Optimierung für JOINs |
|
||
| `key_buffer_size` | 64M | Reduziert (nur für MyISAM, JTL nutzt InnoDB) |
|
||
| `query_cache` | OFF | Bei write-heavy Workloads kontraproduktiv |
|
||
|
||
## 🚀 Installation & Verwendung
|
||
|
||
### Voraussetzungen
|
||
|
||
- Python 3.6+
|
||
- Root-Zugriff auf den Server
|
||
- MariaDB/MySQL Client
|
||
|
||
### Ausführung
|
||
|
||
```bash
|
||
# Direkt vom Git-Repository ausführen
|
||
wget https://git.jtl-hosting.de/thomasciesla/mariadb-optimization/raw/branch/main/mariadb-optimization.py
|
||
python3 mariadb-optimization.py
|
||
```
|
||
|
||
Oder:
|
||
|
||
```bash
|
||
# Repository klonen
|
||
git clone https://git.jtl-hosting.de/thomasciesla/mariadb-optimization.git
|
||
cd mariadb-optimization
|
||
python3 mariadb-optimization.py
|
||
```
|
||
|
||
### Empfohlene Ausführungszeit
|
||
|
||
⚠️ **Zu Zeiten geringer Last ausführen** (nachts/früh morgens)
|
||
|
||
Der FULLTEXT-Index auf großen Shops (z.B. 468.000 Artikel) kann mehrere Minuten dauern und erhöht kurzzeitig die Server-Last. Am Ende wird MariaDB automatisch neugestartet.
|
||
|
||
## 📁 Ausgabe-Dateien
|
||
|
||
Nach der Ausführung werden folgende Dateien erstellt:
|
||
|
||
| Datei | Zweck |
|
||
|-------|-------|
|
||
| `/root/jtl_search_backup_TIMESTAMP.json` | Backup aller Sucheinstellungen |
|
||
| `/root/jtl_search_rollback_TIMESTAMP.py` | Rollback-Skript für Sucheinstellungen |
|
||
| `/root/mariadb_config_backup_TIMESTAMP.cnf` | Backup der vorherigen MariaDB-Config |
|
||
| `/root/jtl_index_log_TIMESTAMP.txt` | Vollständiges Ausführungs-Log |
|
||
|
||
## 🔄 Rollback bei Problemen
|
||
|
||
Falls nach der Optimierung Probleme auftreten:
|
||
|
||
```bash
|
||
# Rollback der Sucheinstellungen (setzt suche_fulltext auf vorherigen Wert)
|
||
python3 /root/jtl_search_rollback_TIMESTAMP.py
|
||
|
||
# Rollback der MariaDB-Konfiguration
|
||
cp /root/mariadb_config_backup_TIMESTAMP.cnf /etc/mysql/conf.d/ZZ-ztl-final-override.cnf
|
||
systemctl restart mariadb
|
||
```
|
||
|
||
**Hinweis:** Die erstellten Indizes müssen nicht zurückgerollt werden – sie haben keine negative Auswirkung und können bei Bedarf manuell entfernt werden.
|
||
|
||
## ❓ FAQ
|
||
|
||
### Ist das Erstellen von Indizes gefährlich?
|
||
|
||
**Nein.** Ein Index ist eine separate Datenstruktur neben der Tabelle – wie ein Stichwortverzeichnis am Ende eines Buches:
|
||
|
||
- ✅ Ändert keine Daten
|
||
- ✅ Ändert keine Spalten
|
||
- ✅ Ändert kein Verhalten der Anwendung
|
||
- ✅ Kann jederzeit wieder gelöscht werden
|
||
- ✅ MariaDB entscheidet selbst, ob es den Index nutzt
|
||
|
||
### Was ändert sich durch `suche_fulltext = 'Y'`?
|
||
|
||
Diese JTL-Einstellung ändert, **wie** JTL nach Artikeln sucht:
|
||
|
||
| Einstellung | Suchmethode | Performance |
|
||
|-------------|-------------|-------------|
|
||
| `N` | `LIKE '%text%'` | Langsam (Full Table Scan) |
|
||
| `Y` | `MATCH() AGAINST()` | Schnell (FULLTEXT Index) |
|
||
|
||
**Mögliche Unterschiede im Suchverhalten:**
|
||
- FULLTEXT sucht nach ganzen Wörtern
|
||
- LIKE sucht nach Teilstrings
|
||
- Beispiel: Suche nach "gel" findet mit LIKE auch "Nagel", mit FULLTEXT evtl. nicht
|
||
|
||
### Warum fehlen die Indizes überhaupt?
|
||
|
||
Typische Gründe:
|
||
1. JTL hat sie bei bestimmten Versionen nicht erstellt
|
||
2. Updates erstellen keine Indizes nachträglich
|
||
3. Die Tabellen waren ursprünglich klein
|
||
|
||
### Wie wird der Buffer Pool berechnet?
|
||
|
||
```
|
||
Buffer Pool = Gesamtgröße aller Datenbanken × 1.2 (aufgerundet)
|
||
```
|
||
|
||
Beispiel: 18.74 GB × 1.2 = 22.49 GB → **23 GB**
|
||
|
||
### Warum ist Query Cache deaktiviert?
|
||
|
||
Bei Multi-Shop-Servern mit vielen Schreiboperationen (Bestellungen, Lagerbestände, etc.) ist der Query Cache kontraproduktiv:
|
||
|
||
- Jedes INSERT/UPDATE/DELETE invalidiert den Cache
|
||
- Lock Contention bei vielen gleichzeitigen Queries
|
||
- MySQL 8.0 hat den Query Cache komplett entfernt
|
||
|
||
### Wie kann ich den Effekt messen?
|
||
|
||
**Vorher/Nachher Vergleich:**
|
||
|
||
```bash
|
||
# Slow Query Rate prüfen
|
||
mysql -e "SHOW GLOBAL STATUS WHERE Variable_name IN ('Questions','Slow_queries');"
|
||
|
||
# Berechnung: Slow_queries / Questions * 100 = Slow Query Rate (%)
|
||
```
|
||
|
||
## 📈 Erwartete Verbesserungen
|
||
|
||
| Metrik | Vorher | Nachher (erwartet) |
|
||
|--------|--------|-------------------|
|
||
| Slow Query Rate | ~24% | <5% |
|
||
| Suchcache-Queries | 68,6% der Zeit | <5% |
|
||
| Artikelsuche | Sekunden | Millisekunden |
|
||
|
||
## 🔍 Analyse-Befehle
|
||
|
||
Die folgenden Befehle wurden für die Analyse verwendet und können zur Überprüfung genutzt werden:
|
||
|
||
### Slow Query Analyse
|
||
|
||
```bash
|
||
# pt-query-digest installieren (falls nicht vorhanden)
|
||
wget https://repo.percona.com/apt/percona-release_latest.generic_all.deb
|
||
dpkg -i percona-release_latest.generic_all.deb
|
||
apt update && apt install percona-toolkit -y
|
||
|
||
# Slow Query Report erstellen
|
||
pt-query-digest --limit=50 /var/log/mysql/slow.log > /tmp/slow_report.txt
|
||
```
|
||
|
||
### Index-Status prüfen
|
||
|
||
```bash
|
||
# Shops ohne FULLTEXT-Index
|
||
mysql -N -B -e "SELECT t.table_schema, t.table_rows
|
||
FROM information_schema.tables t
|
||
WHERE t.table_name = 'tartikel'
|
||
AND t.table_schema NOT IN (
|
||
SELECT DISTINCT table_schema
|
||
FROM information_schema.statistics
|
||
WHERE table_name = 'tartikel' AND index_type = 'FULLTEXT'
|
||
)
|
||
ORDER BY t.table_rows DESC;"
|
||
|
||
# FULLTEXT-Einstellung pro Shop
|
||
mysql -N -B -e "SELECT table_schema FROM information_schema.tables
|
||
WHERE table_name = 'teinstellungen'" | while read db; do
|
||
val=$(mysql -N -B -e "SELECT cWert FROM \`$db\`.teinstellungen
|
||
WHERE cName = 'suche_fulltext' LIMIT 1" 2>/dev/null)
|
||
echo "$db: ${val:-NICHT GESETZT}"
|
||
done
|
||
```
|
||
|
||
### Datenbankgrößen prüfen
|
||
|
||
```bash
|
||
# Gesamtgröße aller Datenbanken
|
||
mysql -N -B -e "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024 / 1024, 2)
|
||
FROM information_schema.tables
|
||
WHERE table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys');"
|
||
|
||
# Größe pro Datenbank
|
||
mysql -e "SELECT table_schema AS 'Datenbank',
|
||
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Größe (MB)'
|
||
FROM information_schema.tables
|
||
WHERE table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')
|
||
GROUP BY table_schema
|
||
ORDER BY SUM(data_length + index_length) DESC;"
|
||
```
|
||
|
||
### Buffer Pool & Key Metrics
|
||
|
||
```bash
|
||
mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';"
|
||
mysql -e "SHOW GLOBAL STATUS WHERE Variable_name IN
|
||
('Connections','Max_used_connections','Threads_connected',
|
||
'Threads_running','Questions','Slow_queries','Uptime');"
|
||
```
|
||
|
||
## 🐛 Bekannte Einschränkungen
|
||
|
||
### Nicht durch Indizes lösbar
|
||
|
||
| Problem | Ursache | Workaround |
|
||
|---------|---------|------------|
|
||
| tbesucherarchiv (9,3%) | `INSTR(cIP, '...')` in Query | JTL-interner Code, nicht änderbar |
|
||
| Komplexe Filter-Queries | JTL Query-Struktur | Bereits optimale Indizes vorhanden |
|
||
|
||
### FULLTEXT-Einschränkungen
|
||
|
||
- Minimum Wortlänge: 3-4 Zeichen (konfigurierbar in MariaDB)
|
||
- Stoppwörter werden ignoriert
|
||
- Keine Wildcard-Suche am Wortanfang
|
||
|
||
## 📜 Lizenz
|
||
|
||
MIT License - Siehe [LICENSE](LICENSE)
|
||
|
||
## 👤 Autor
|
||
|
||
Thomas Ciesla - JTL-Hosting
|
||
|
||
## 🔗 Links
|
||
|
||
- [Repository](https://git.jtl-hosting.de/thomasciesla/mariadb-optimization)
|
||
- [Skript (Direktlink)](https://git.jtl-hosting.de/thomasciesla/mariadb-optimization/raw/branch/main/mariadb-optimization.py)
|
||
- [JTL-Shop Dokumentation](https://guide.jtl-software.de/jtl-shop/)
|
||
- [MariaDB Index-Dokumentation](https://mariadb.com/kb/en/optimization-and-indexes/)
|
||
- [Percona Toolkit](https://www.percona.com/software/database-tools/percona-toolkit) |