Files
php/config-php-fpm.sh
T

439 lines
15 KiB
Bash
Executable File

#!/usr/bin/env bash
# ==============================================================================
# tune-php-fpm.sh
# Setzt optimierte Werte in php.ini und fpm.conf fuer mehrere PHP-Versionen
# und startet den jeweiligen PHP-FPM-Service neu.
#
# Betroffene PHP-Hauptversionen: 8.3, 8.4, 8.5
# Symlink-Basis: /usr/local/php-<MAIN-VERSION> -> /usr/local/php-<VOLLVERSION>
# ==============================================================================
set -euo pipefail
# ------------------------------------------------------------------------------
# Konfiguration
# ------------------------------------------------------------------------------
PHP_MAIN_VERSIONS=("8.2" "8.3" "8.4" "8.5")
APC_SHM_SIZE="128M"
PM_MAX_REQUESTS=5000
PM_MAX_CHILDREN=60
# Wird durch interaktive Abfrage befuellt:
CONFIGURE_LOGLEVEL=false
FPM_LOG_LEVEL=""
# ------------------------------------------------------------------------------
# Farben & Ausgabe
# ------------------------------------------------------------------------------
C_RESET="\e[0m"
C_BOLD="\e[1m"
C_BLUE="\e[34m"
C_GREEN="\e[32m"
C_YELLOW="\e[33m"
C_RED="\e[31m"
C_CYAN="\e[36m"
C_GRAY="\e[90m"
info() { echo -e "${C_BLUE}[INFO]${C_RESET} $*"; }
ok() { echo -e "${C_GREEN}[OK]${C_RESET} $*"; }
skip() { echo -e "${C_GRAY}[SKIP]${C_RESET} $*"; }
warn() { echo -e "${C_YELLOW}[WARN]${C_RESET} $*"; }
error() { echo -e "${C_RED}[ERROR]${C_RESET} $*" >&2; }
changed() { echo -e "${C_GREEN}[SET]${C_RESET} $*"; }
hr() {
echo -e "${C_GRAY}--------------------------------------------------------------${C_RESET}"
}
section() {
echo ""
echo -e "${C_BOLD}${C_CYAN}+- PHP $1 -----------------------------------------------------${C_RESET}"
}
# ------------------------------------------------------------------------------
# INI-Hilfsfunktionen
# ------------------------------------------------------------------------------
# Liest den aktuell aktiven (nicht auskommentierten) Wert eines Keys aus einer INI-Datei.
# Gibt leeren String zurueck wenn nicht aktiv gesetzt.
get_ini_value() {
local file="$1"
local key="$2"
# Nur aktive Zeilen (nicht auskommentiert), letzten Treffer nehmen
local val
val=$(grep -E "^\s*${key}\s*=" "$file" 2>/dev/null | tail -1 \
| sed -E "s/^\s*${key}\s*=\s*//" | sed 's/[[:space:]]*$//' || true)
echo "$val"
}
# Setzt einen Wert in einer INI-Datei.
# Ueberspringt wenn der Wert bereits korrekt gesetzt ist.
# Ersetzt aktive oder auskommentierte Zeilen; fuegt ans Ende an falls nicht vorhanden.
#
# Gibt zurueck: 0 = geaendert, 1 = uebersprungen, 2 = Fehler
set_ini_value() {
local file="$1"
local key="$2"
local desired="$3"
if [[ ! -f "$file" ]]; then
error "Datei nicht gefunden: $file"
return 2
fi
# Aktuellen Wert lesen
local current
current=$(get_ini_value "$file" "$key")
# Bereits korrekt gesetzt?
if [[ "$current" == "$desired" ]]; then
skip "${key} = ${desired} (bereits korrekt, keine Aenderung)"
return 1
fi
# Backup anlegen (einmal pro Datei und Skriptlauf)
local backup="${file}.bak-${BACKUP_TS}"
if [[ ! -f "$backup" ]]; then
cp "$file" "$backup"
fi
if grep -qE "^\s*;?\s*${key}\s*=" "$file"; then
# Zeile vorhanden (aktiv oder auskommentiert) -> ersetzen
sed -i -E "0,/^\s*;?\s*${key}\s*=/{s|^\s*;?\s*${key}\s*=.*|${key} = ${desired}|}" "$file"
if [[ -n "$current" ]]; then
changed "${key} = ${desired} (war: ${current})"
else
changed "${key} = ${desired} (war auskommentiert)"
fi
else
# Key fehlt komplett -> anhaengen
echo "" >> "$file"
echo "${key} = ${desired}" >> "$file"
changed "${key} = ${desired} (neu eingefuegt)"
fi
return 0
}
# Setzt einen Wert gezielt im [global]-Abschnitt einer php-fpm.conf.
# log_level und andere globale FPM-Direktiven sind NUR dort gueltig --
# am Dateiende anhaengen wuerde den Wert in den Pool-Kontext setzen -> Fehler.
#
# Strategie:
# 1. Key bereits aktiv in [global] -> ersetzen (wie set_ini_value)
# 2. Key auskommentiert in [global] -> aktivieren und Wert setzen
# 3. Key fehlt -> nach der "[global]"-Zeile einfuegen
# 4. Kein [global]-Abschnitt -> Fehler
#
# Gibt zurueck: 0 = geaendert, 1 = uebersprungen, 2 = Fehler
set_global_fpm_value() {
local file="$1"
local key="$2"
local desired="$3"
if [[ ! -f "$file" ]]; then
error "Datei nicht gefunden: $file"
return 2
fi
if ! grep -qE "^\s*\[global\]" "$file"; then
error "Kein [global]-Abschnitt in ${file} gefunden -- Abbruch."
return 2
fi
# Aktuellen aktiven Wert lesen
local current
current=$(get_ini_value "$file" "$key")
if [[ "$current" == "$desired" ]]; then
skip "${key} = ${desired} (bereits korrekt, keine Aenderung)"
return 1
fi
# Backup anlegen
local backup="${file}.bak-${BACKUP_TS}"
if [[ ! -f "$backup" ]]; then
cp "$file" "$backup"
fi
# Pruefen ob Key irgendwo in der Datei vorkommt (aktiv oder auskommentiert)
if grep -qE "^\s*;?\s*${key}\s*=" "$file"; then
# Zeile vorhanden -> einfach ersetzen (erste Fundstelle, egal in welchem Abschnitt)
sed -i -E "0,/^\s*;?\s*${key}\s*=/{s|^\s*;?\s*${key}\s*=.*|${key} = ${desired}|}" "$file"
if [[ -n "$current" ]]; then
changed "${key} = ${desired} (war: ${current})"
else
changed "${key} = ${desired} (war auskommentiert)"
fi
else
# Key fehlt komplett -> nach der [global]-Zeile einfuegen,
# mit Kommentar davor und Leerzeile danach.
sed -i "/^\s*\[global\]/a \\\n; Minimaler Log-Level fuer FPM-eigene Meldungen (Standard: notice).\n; Gueltige Werte: alert, error, warning, notice, debug\n${key} = ${desired}\n" "$file"
changed "${key} = ${desired} (in [global] eingefuegt)"
fi
return 0
}
# Entfernt eine faelschlicherweise ausserhalb von [global] eingefuegte Direktive.
# Wird als Reparaturschritt verwendet wenn das Skript den Wert zuvor ans Ende
# der Datei angehaengt hat.
repair_misplaced_key() {
local file="$1"
local key="$2"
if [[ ! -f "$file" ]]; then
return
fi
# Zeile zaehlen die den Key enthalten
local count
count=$(grep -cE "^\s*${key}\s*=" "$file" || true)
if [[ "$count" -gt 1 ]]; then
warn "Reparatur: ${count}x '${key}' in ${file} gefunden -- entferne Duplikate."
# Backup falls noch nicht vorhanden
local backup="${file}.bak-${BACKUP_TS}"
if [[ ! -f "$backup" ]]; then
cp "$file" "$backup"
fi
# Alle Vorkommen loeschen ausser dem ersten
awk -v key="^[[:space:]]*${key}[[:space:]]*=" '
$0 ~ key { if (!seen++) print; next }
{ print }
' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"
ok "Duplikate entfernt."
fi
}
# ------------------------------------------------------------------------------
# Interaktive Log-Level-Abfrage
# ------------------------------------------------------------------------------
ask_loglevel() {
echo ""
echo -e "${C_BOLD}+--------------------------------------------------------------+${C_RESET}"
echo -e "${C_BOLD}| PHP-FPM Log-Level konfigurieren |${C_RESET}"
echo -e "${C_BOLD}+--------------------------------------------------------------+${C_RESET}"
echo ""
echo -e " Der Standard-Log-Level ${C_BOLD}notice${C_RESET} erzeugt die bekannten"
echo -e " Massen-Eintraege (child started / exited) im FPM-Error-Log."
echo ""
echo -e " Soll der Log-Level in ${C_BOLD}php-fpm.conf${C_RESET} (globale Sektion)"
echo -e " fuer alle Versionen konfiguriert werden?"
echo ""
echo -e " ${C_BOLD}[j]${C_RESET} Ja, Log-Level setzen"
echo -e " ${C_BOLD}[n]${C_RESET} Nein, Log-Level unveraendert lassen"
echo ""
while true; do
read -r -p " Auswahl [j/n]: " answer
case "${answer,,}" in
j|ja|y|yes)
CONFIGURE_LOGLEVEL=true
break
;;
n|nein|no)
CONFIGURE_LOGLEVEL=false
info "Log-Level wird nicht veraendert."
return
;;
*)
warn "Bitte 'j' oder 'n' eingeben."
;;
esac
done
echo ""
echo -e " ${C_BOLD}Verfuegbare Log-Level${C_RESET} (von ruhig -> ausfuehrlich):"
echo ""
echo -e " ${C_BOLD}[1]${C_RESET} ${C_GREEN}warning${C_RESET} Nur Warnungen und schlimmer"
echo -e " ${C_GRAY}-> Empfohlen: unterdrueckt child-started/exited Meldungen${C_RESET}"
echo -e " ${C_BOLD}[2]${C_RESET} ${C_YELLOW}error${C_RESET} Nur Fehler (alert + error)"
echo -e " ${C_BOLD}[3]${C_RESET} ${C_RED}alert${C_RESET} Nur kritische Meldungen"
echo -e " ${C_BOLD}[4]${C_RESET} notice Standard - inkl. child started/exited"
echo -e " ${C_BOLD}[5]${C_RESET} debug Sehr ausfuehrlich (nur zur Fehlersuche)"
echo ""
while true; do
read -r -p " Level waehlen [1-5, Standard: 1]: " choice
choice="${choice:-1}"
case "$choice" in
1) FPM_LOG_LEVEL="warning"; break ;;
2) FPM_LOG_LEVEL="error"; break ;;
3) FPM_LOG_LEVEL="alert"; break ;;
4) FPM_LOG_LEVEL="notice"; break ;;
5) FPM_LOG_LEVEL="debug"; break ;;
*) warn "Bitte eine Zahl zwischen 1 und 5 eingeben." ;;
esac
done
echo ""
ok "Log-Level wird gesetzt auf: ${C_BOLD}${FPM_LOG_LEVEL}${C_RESET}"
}
# ------------------------------------------------------------------------------
# Root-Check
# ------------------------------------------------------------------------------
if [[ $EUID -ne 0 ]]; then
error "Dieses Skript muss als root ausgefuehrt werden."
exit 1
fi
# Einheitlicher Backup-Zeitstempel fuer diesen Skriptlauf
BACKUP_TS="$(date +%Y%m%d%H%M%S)"
# ------------------------------------------------------------------------------
# Interaktive Abfragen (vor der eigentlichen Verarbeitung)
# ------------------------------------------------------------------------------
ask_loglevel
echo ""
hr
echo -e " ${C_BOLD}Zusammenfassung der geplanten Aenderungen:${C_RESET}"
hr
echo -e " php.ini apc.shm_size = ${C_BOLD}${APC_SHM_SIZE}${C_RESET}"
echo -e " fpm pool pm.max_requests = ${C_BOLD}${PM_MAX_REQUESTS}${C_RESET}"
echo -e " fpm pool pm.max_children = ${C_BOLD}${PM_MAX_CHILDREN}${C_RESET}"
if [[ "$CONFIGURE_LOGLEVEL" == true ]]; then
echo -e " php-fpm.conf log_level = ${C_BOLD}${FPM_LOG_LEVEL}${C_RESET}"
fi
echo -e " Versionen: ${C_BOLD}${PHP_MAIN_VERSIONS[*]}${C_RESET}"
hr
echo ""
read -r -p " Fortfahren? [j/n]: " confirm
case "${confirm,,}" in
j|ja|y|yes) echo "" ;;
*)
info "Abgebrochen, keine Aenderungen vorgenommen."
exit 0
;;
esac
# ------------------------------------------------------------------------------
# Hauptverarbeitung
# ------------------------------------------------------------------------------
ERRORS=0
CHANGED=0
SKIPPED=0
NEEDS_RESTART=false
for MAIN_VER in "${PHP_MAIN_VERSIONS[@]}"; do
section "$MAIN_VER"
SYMLINK_DIR="/usr/local/php-${MAIN_VER}"
if [[ ! -d "$SYMLINK_DIR" ]]; then
warn "Verzeichnis/Symlink nicht gefunden: ${SYMLINK_DIR} - ueberspringe Version."
continue
fi
REAL_DIR="$(realpath "$SYMLINK_DIR")"
info "Echter Pfad: ${REAL_DIR}"
PHP_INI="${SYMLINK_DIR}/etc/php.ini"
FPM_CONF="${SYMLINK_DIR}/etc/fpm.d/www-${MAIN_VER}.php-fpm.conf"
FPM_GLOBAL_CONF="${SYMLINK_DIR}/etc/php-fpm.conf"
SERVICE="php-${MAIN_VER}-fpm"
SERVICE_FILE="/etc/systemd/system/${SERVICE}.service"
VERSION_CHANGED=false
# --- php.ini ---
echo ""
info "php.ini: ${PHP_INI}"
if [[ ! -f "$PHP_INI" ]]; then
error "Datei nicht gefunden: ${PHP_INI}"
((ERRORS++)) || true
else
if set_ini_value "$PHP_INI" "apc.shm_size" "$APC_SHM_SIZE"; then
VERSION_CHANGED=true; ((CHANGED++)) || true
fi
fi
# --- Pool fpm.conf ---
echo ""
info "Pool-Conf: ${FPM_CONF}"
if [[ ! -f "$FPM_CONF" ]]; then
error "Datei nicht gefunden: ${FPM_CONF}"
((ERRORS++)) || true
else
local_changed=false
if set_ini_value "$FPM_CONF" "pm.max_requests" "$PM_MAX_REQUESTS"; then
local_changed=true
fi
if set_ini_value "$FPM_CONF" "pm.max_children" "$PM_MAX_CHILDREN"; then
local_changed=true
fi
if $local_changed; then
VERSION_CHANGED=true; ((CHANGED++)) || true
fi
fi
# --- Log-Level in globaler php-fpm.conf ---
if [[ "$CONFIGURE_LOGLEVEL" == true ]]; then
echo ""
info "FPM-Global: ${FPM_GLOBAL_CONF}"
if [[ ! -f "$FPM_GLOBAL_CONF" ]]; then
warn "Globale php-fpm.conf nicht gefunden: ${FPM_GLOBAL_CONF}"
warn "Log-Level konnte fuer PHP ${MAIN_VER} nicht gesetzt werden."
((ERRORS++)) || true
else
# Reparatur: frueherer Skript-Lauf hat log_level evtl. ans Ende
# der Datei (ausserhalb [global]) angehaengt -> Duplikate entfernen
repair_misplaced_key "$FPM_GLOBAL_CONF" "log_level"
# log_level MUSS im [global]-Abschnitt stehen
if set_global_fpm_value "$FPM_GLOBAL_CONF" "log_level" "$FPM_LOG_LEVEL"; then
VERSION_CHANGED=true; ((CHANGED++)) || true
fi
fi
fi
# --- Service-Aktion ---
echo ""
if [[ ! -f "$SERVICE_FILE" ]]; then
warn "Service-Datei nicht gefunden: ${SERVICE_FILE} - kein Neustart."
((ERRORS++)) || true
continue
fi
if ! $VERSION_CHANGED; then
skip "Keine Aenderungen fuer PHP ${MAIN_VER} - Service-Neustart nicht noetig."
((SKIPPED++)) || true
continue
fi
# apc.shm_size erfordert immer restart (Shared Memory)
info "Starte Service neu: ${SERVICE}"
if systemctl restart "${SERVICE}"; then
ok "Service ${SERVICE} neu gestartet."
NEEDS_RESTART=false
else
error "Neustart fehlgeschlagen: ${SERVICE}"
((ERRORS++)) || true
fi
done
# ------------------------------------------------------------------------------
# Abschluss
# ------------------------------------------------------------------------------
echo ""
echo -e "${C_BOLD}+--------------------------------------------------------------+${C_RESET}"
echo -e "${C_BOLD}| Abschlussbericht |${C_RESET}"
echo -e "${C_BOLD}+--------------------------------------------------------------+${C_RESET}"
echo ""
echo -e " Geaenderte Werte: ${C_BOLD}${CHANGED}${C_RESET}"
echo -e " Uebersprungen: ${C_BOLD}${SKIPPED}${C_RESET} (bereits korrekt gesetzt)"
if [[ $ERRORS -gt 0 ]]; then
echo -e " Fehler/Warnungen: ${C_RED}${C_BOLD}${ERRORS}${C_RESET}"
else
echo -e " Fehler/Warnungen: ${C_GREEN}${C_BOLD}0${C_RESET}"
fi
echo ""
if [[ $ERRORS -eq 0 ]]; then
ok "Fertig - alle Versionen erfolgreich verarbeitet."
else
warn "Fertig mit ${ERRORS} Warnung(en)/Fehler(n) - bitte Ausgabe pruefen."
fi
echo ""
echo -e " ${C_GRAY}Backups mit Suffix .bak-${BACKUP_TS} angelegt.${C_RESET}"
echo ""