#!/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- -> /usr/local/php- # ============================================================================== 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 ""