Files
mariadb-optimization/mariadb-optimization.py

556 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
JTL Shop MariaDB Optimierung
============================
Erstellt fehlende Indizes, aktiviert FULLTEXT-Suche und optimiert MariaDB-Konfiguration.
Features:
- Backup aller Sucheinstellungen vor Änderungen
- Erstellt 5 Index-Typen auf allen betroffenen Shops
- Aktiviert FULLTEXT-Suche
- Generiert optimierte MariaDB-Konfiguration
- Generiert Rollback-Skript für Notfälle
Autor: Claude
Datum: 2025-12-12
"""
import subprocess
import json
import sys
import math
import os
from datetime import datetime
from pathlib import Path
# Konfiguration
TIMESTAMP = datetime.now().strftime("%Y%m%d_%H%M%S")
BACKUP_FILE = f"/root/jtl_search_backup_{TIMESTAMP}.json"
ROLLBACK_FILE = f"/root/jtl_search_rollback_{TIMESTAMP}.py"
LOG_FILE = f"/root/jtl_index_log_{TIMESTAMP}.txt"
MARIADB_CONFIG_FILE = "/etc/mysql/conf.d/ZZ-ztl-final-override.cnf"
MARIADB_CONFIG_BACKUP = f"/root/mariadb_config_backup_{TIMESTAMP}.cnf"
# Index-Definitionen
INDEXES = [
{
"name": "idx_kLieferscheinPos",
"table": "tlieferscheinposinfo",
"columns": "kLieferscheinPos",
"check_column": "kLieferscheinPos"
},
{
"name": "idx_kLieferschein",
"table": "tlieferscheinpos",
"columns": "kLieferschein",
"check_column": "kLieferschein"
},
{
"name": "idx_kKampagneDef_kKey",
"table": "tkampagnevorgang",
"columns": "kKampagneDef, kKey",
"check_column": "kKampagneDef"
},
{
"name": "idx_kXSellArtikel",
"table": "txsellkauf",
"columns": "kXSellArtikel",
"check_column": "kXSellArtikel"
}
]
FULLTEXT_INDEX = {
"name": "idx_tartikel_fulltext",
"table": "tartikel",
"columns": "cName, cSeo, cSuchbegriffe, cArtNr, cKurzBeschreibung, cBeschreibung, cBarcode, cISBN, cHAN"
}
class Logger:
def __init__(self, log_file):
self.log_file = log_file
self.messages = []
def log(self, message, level="INFO"):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
formatted = f"[{timestamp}] [{level}] {message}"
print(formatted)
self.messages.append(formatted)
def save(self):
with open(self.log_file, 'w') as f:
f.write("\n".join(self.messages))
print(f"\nLog gespeichert: {self.log_file}")
def run_mysql(query, database=None):
"""Führt MySQL-Query aus und gibt Ergebnis zurück."""
cmd = ["mysql", "-N", "-B", "-e", query]
if database:
cmd.extend(["-D", database])
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
return None, result.stderr
return result.stdout.strip(), None
def get_all_shops():
"""Ermittelt alle JTL-Shop-Datenbanken."""
query = """
SELECT DISTINCT table_schema
FROM information_schema.tables
WHERE table_name = 'tartikel'
AND table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')
ORDER BY table_schema;
"""
result, err = run_mysql(query)
if err:
return []
return [line.strip() for line in result.split('\n') if line.strip()]
def check_index_exists(shop, table, check_column):
"""Prüft ob ein Index für die Spalte bereits existiert."""
query = f"""
SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = '{shop}'
AND table_name = '{table}'
AND column_name = '{check_column}'
AND index_name != 'PRIMARY';
"""
result, _ = run_mysql(query)
return result and int(result) > 0
def check_fulltext_exists(shop):
"""Prüft ob FULLTEXT-Index bereits existiert."""
query = f"""
SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = '{shop}'
AND table_name = 'tartikel'
AND index_type = 'FULLTEXT';
"""
result, _ = run_mysql(query)
return result and int(result) > 0
def check_table_exists(shop, table):
"""Prüft ob Tabelle existiert."""
query = f"""
SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = '{shop}'
AND table_name = '{table}';
"""
result, _ = run_mysql(query)
return result and int(result) > 0
def get_search_setting(shop):
"""Liest aktuelle suche_fulltext Einstellung."""
query = f"SELECT cWert FROM `{shop}`.teinstellungen WHERE cName = 'suche_fulltext' LIMIT 1;"
result, _ = run_mysql(query)
return result if result else "NICHT_GESETZT"
def set_search_setting(shop, value):
"""Setzt suche_fulltext Einstellung."""
query = f"UPDATE `{shop}`.teinstellungen SET cWert = '{value}' WHERE cName = 'suche_fulltext';"
_, err = run_mysql(query)
return err is None
def create_index(shop, index_name, table, columns, is_fulltext=False):
"""Erstellt einen Index."""
if is_fulltext:
query = f"CREATE FULLTEXT INDEX `{index_name}` ON `{shop}`.`{table}` ({columns});"
else:
query = f"CREATE INDEX `{index_name}` ON `{shop}`.`{table}` ({columns});"
_, err = run_mysql(query)
return err
def get_total_database_size_gb():
"""Ermittelt die Gesamtgröße aller Datenbanken in GB."""
query = """
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');
"""
result, err = run_mysql(query)
if err or not result:
return None
try:
return float(result)
except ValueError:
return None
def calculate_buffer_pool_size(db_size_gb):
"""Berechnet Buffer Pool Size: DB-Größe × 1.2, aufgerundet auf ganze GB."""
recommended = db_size_gb * 1.2
return math.ceil(recommended)
def generate_mariadb_config(buffer_pool_gb):
"""Generiert die MariaDB-Konfiguration mit dynamischem Buffer Pool."""
config = f"""# JTL-Shop MariaDB Optimierung
# Generiert am: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
# Buffer Pool berechnet: DB-Größe × 1.2 = {buffer_pool_gb}G
[mysqld]
# InnoDB Buffer Pool (dynamisch berechnet)
innodb_buffer_pool_size = {buffer_pool_gb}G
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
"""
return config
def backup_existing_config():
"""Erstellt ein Backup der existierenden Config, falls vorhanden."""
if os.path.exists(MARIADB_CONFIG_FILE):
with open(MARIADB_CONFIG_FILE, 'r') as f:
content = f.read()
with open(MARIADB_CONFIG_BACKUP, 'w') as f:
f.write(content)
return True
return False
def write_mariadb_config(config_content):
"""Schreibt die MariaDB-Konfiguration."""
with open(MARIADB_CONFIG_FILE, 'w') as f:
f.write(config_content)
def restart_mariadb():
"""Startet MariaDB neu."""
result = subprocess.run(
["systemctl", "restart", "mariadb"],
capture_output=True,
text=True
)
return result.returncode == 0, result.stderr
def generate_rollback_script(backup_data):
"""Generiert ein Rollback-Skript."""
script = f'''#!/usr/bin/env python3
"""
JTL Search Settings Rollback Script
====================================
Generiert am: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
Dieses Skript stellt die ursprünglichen suche_fulltext Einstellungen wieder her.
"""
import subprocess
BACKUP_DATA = {json.dumps(backup_data, indent=4)}
def run_mysql(query):
result = subprocess.run(["mysql", "-N", "-e", query], capture_output=True, text=True)
return result.returncode == 0
def main():
print("=== JTL Search Settings Rollback ===")
print(f"Stelle {{len(BACKUP_DATA)}} Einstellungen wieder her...\\n")
success = 0
failed = 0
for shop, value in BACKUP_DATA.items():
if value == "NICHT_GESETZT":
print(f" [SKIP] {{shop}}: Keine Einstellung vorhanden")
continue
query = f"UPDATE `{{shop}}`.teinstellungen SET cWert = '{{value}}' WHERE cName = 'suche_fulltext';"
if run_mysql(query):
print(f" [OK] {{shop}}: suche_fulltext = '{{value}}'")
success += 1
else:
print(f" [FEHLER] {{shop}}")
failed += 1
print(f"\\n=== Rollback abgeschlossen ===")
print(f"Erfolgreich: {{success}}")
print(f"Fehlgeschlagen: {{failed}}")
if __name__ == "__main__":
main()
'''
with open(ROLLBACK_FILE, 'w') as f:
f.write(script)
# Ausführbar machen
subprocess.run(["chmod", "+x", ROLLBACK_FILE])
def main():
logger = Logger(LOG_FILE)
print("=" * 60)
print("JTL Shop MariaDB Optimierung")
print("=" * 60)
print()
# Phase 0: Shops ermitteln
logger.log("Ermittle alle JTL-Shop-Datenbanken...")
shops = get_all_shops()
logger.log(f"Gefunden: {len(shops)} Shops")
print()
if not shops:
logger.log("Keine Shops gefunden. Abbruch.", "ERROR")
return
# Phase 1: Backup der Sucheinstellungen
print("=" * 60)
print("PHASE 1: Backup der Sucheinstellungen")
print("=" * 60)
backup_data = {}
for shop in shops:
setting = get_search_setting(shop)
backup_data[shop] = setting
logger.log(f" {shop}: suche_fulltext = '{setting}'")
# Backup speichern
with open(BACKUP_FILE, 'w') as f:
json.dump(backup_data, f, indent=2)
logger.log(f"Backup gespeichert: {BACKUP_FILE}")
# Rollback-Skript generieren
generate_rollback_script(backup_data)
logger.log(f"Rollback-Skript generiert: {ROLLBACK_FILE}")
print()
# Phase 2: Standard-Indizes erstellen
print("=" * 60)
print("PHASE 2: Standard-Indizes erstellen")
print("=" * 60)
index_stats = {"created": 0, "skipped": 0, "failed": 0}
for idx in INDEXES:
logger.log(f"\n--- Index: {idx['name']} auf {idx['table']} ---")
for shop in shops:
# Prüfen ob Tabelle existiert
if not check_table_exists(shop, idx['table']):
continue
# Prüfen ob Index existiert
if check_index_exists(shop, idx['table'], idx['check_column']):
logger.log(f" [SKIP] {shop}: Index existiert bereits")
index_stats["skipped"] += 1
continue
# Index erstellen
err = create_index(shop, idx['name'], idx['table'], idx['columns'])
if err:
logger.log(f" [FEHLER] {shop}: {err}", "ERROR")
index_stats["failed"] += 1
else:
logger.log(f" [OK] {shop}: Index erstellt")
index_stats["created"] += 1
print()
# Phase 3: FULLTEXT-Index erstellen
print("=" * 60)
print("PHASE 3: FULLTEXT-Index erstellen")
print("=" * 60)
fulltext_stats = {"created": 0, "skipped": 0, "failed": 0}
for shop in shops:
# Prüfen ob FULLTEXT bereits existiert
if check_fulltext_exists(shop):
logger.log(f" [SKIP] {shop}: FULLTEXT existiert bereits")
fulltext_stats["skipped"] += 1
continue
# FULLTEXT-Index erstellen
logger.log(f" [WORK] {shop}: Erstelle FULLTEXT-Index (kann dauern)...")
err = create_index(
shop,
FULLTEXT_INDEX['name'],
FULLTEXT_INDEX['table'],
FULLTEXT_INDEX['columns'],
is_fulltext=True
)
if err:
logger.log(f" [FEHLER] {shop}: {err}", "ERROR")
fulltext_stats["failed"] += 1
else:
logger.log(f" [OK] {shop}: FULLTEXT-Index erstellt")
fulltext_stats["created"] += 1
print()
# Phase 4: FULLTEXT-Suche aktivieren
print("=" * 60)
print("PHASE 4: FULLTEXT-Suche aktivieren")
print("=" * 60)
search_stats = {"updated": 0, "skipped": 0, "failed": 0}
for shop in shops:
current = backup_data.get(shop, "NICHT_GESETZT")
if current == "Y":
logger.log(f" [SKIP] {shop}: Bereits aktiviert")
search_stats["skipped"] += 1
continue
if current == "NICHT_GESETZT":
logger.log(f" [SKIP] {shop}: Keine Einstellung vorhanden")
search_stats["skipped"] += 1
continue
# Einstellung ändern
if set_search_setting(shop, "Y"):
logger.log(f" [OK] {shop}: suche_fulltext = 'Y' (war: '{current}')")
search_stats["updated"] += 1
else:
logger.log(f" [FEHLER] {shop}", "ERROR")
search_stats["failed"] += 1
print()
# Phase 5: MariaDB-Konfiguration optimieren
print("=" * 60)
print("PHASE 5: MariaDB-Konfiguration optimieren")
print("=" * 60)
config_stats = {"success": False, "restart": False}
# Datenbankgröße ermitteln
logger.log("Ermittle Gesamtgröße aller Datenbanken...")
db_size_gb = get_total_database_size_gb()
if db_size_gb is None:
logger.log("Konnte Datenbankgröße nicht ermitteln. Überspringe Config-Optimierung.", "ERROR")
else:
logger.log(f" Gesamtgröße: {db_size_gb} GB")
# Buffer Pool berechnen
buffer_pool_gb = calculate_buffer_pool_size(db_size_gb)
logger.log(f" Berechneter Buffer Pool: {db_size_gb} GB × 1.2 = {buffer_pool_gb} GB")
# Backup der existierenden Config
if backup_existing_config():
logger.log(f" Bestehendes Config-Backup erstellt: {MARIADB_CONFIG_BACKUP}")
else:
logger.log(f" Keine bestehende Config gefunden, erstelle neu")
# Config generieren und schreiben
config_content = generate_mariadb_config(buffer_pool_gb)
write_mariadb_config(config_content)
logger.log(f" Config geschrieben: {MARIADB_CONFIG_FILE}")
config_stats["success"] = True
# MariaDB neustarten
logger.log(" Starte MariaDB neu...")
success, err = restart_mariadb()
if success:
logger.log(" [OK] MariaDB erfolgreich neugestartet")
config_stats["restart"] = True
else:
logger.log(f" [FEHLER] MariaDB Neustart fehlgeschlagen: {err}", "ERROR")
print()
# Zusammenfassung
print("=" * 60)
print("ZUSAMMENFASSUNG")
print("=" * 60)
print()
print("Standard-Indizes:")
print(f" Erstellt: {index_stats['created']}")
print(f" Übersprungen: {index_stats['skipped']}")
print(f" Fehlgeschlagen: {index_stats['failed']}")
print()
print("FULLTEXT-Index:")
print(f" Erstellt: {fulltext_stats['created']}")
print(f" Übersprungen: {fulltext_stats['skipped']}")
print(f" Fehlgeschlagen: {fulltext_stats['failed']}")
print()
print("Sucheinstellung (suche_fulltext = 'Y'):")
print(f" Aktualisiert: {search_stats['updated']}")
print(f" Übersprungen: {search_stats['skipped']}")
print(f" Fehlgeschlagen: {search_stats['failed']}")
print()
print("MariaDB-Konfiguration:")
print(f" Config erstellt: {'Ja' if config_stats['success'] else 'Nein'}")
print(f" MariaDB Restart: {'Erfolgreich' if config_stats['restart'] else 'Fehlgeschlagen/Übersprungen'}")
print()
print("=" * 60)
print("WICHTIGE DATEIEN")
print("=" * 60)
print(f" Search Backup: {BACKUP_FILE}")
print(f" Rollback-Skript: {ROLLBACK_FILE}")
print(f" Config Backup: {MARIADB_CONFIG_BACKUP}")
print(f" MariaDB Config: {MARIADB_CONFIG_FILE}")
print(f" Log: {LOG_FILE}")
print()
print("Bei Problemen:")
print(f" Rollback Search: python3 {ROLLBACK_FILE}")
print(f" Rollback Config: cp {MARIADB_CONFIG_BACKUP} {MARIADB_CONFIG_FILE} && systemctl restart mariadb")
print()
# Log speichern
logger.save()
if __name__ == "__main__":
main()