# MySQL: Error "No space left on device" en /tmp

## Síntomas del problema

Los usuarios reportan errores similares a estos en cPanel, phpMyAdmin y otras aplicaciones:

```
Can't create/write to file '/tmp/#sql_911_0.MAI' (Errcode: 28 "No space left on device")
Se experimentó un error mientras se obtenían los datos: Can't create/write to file '/tmp/#sql_1234_0.MAI'
```

## Diagnóstico paso a paso

### Verificar espacio en disco

```bash
df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        20G  8.5G   10G  46% /
tmpfs           2.0G  144K  2.0G   1% /tmp
```

Salida para indicar que todo es normal

### Verificar inodos (la causa real)
```bash
df -i
Filesystem      Inodes   IUsed   IFree IUse% Mounted on
/dev/sda1      1310720  145230 1165490   11% /
tmpfs           524288  524271      17  100% /tmp
```

**Observación**: La salida indica el agotamiento al 100% de los inodos.


### Identificar los archivos problemáticos

Al intentar listar `/tmp` con `ls`, obtendremos:

```bash
ls /tmp
-bash: /bin/ls: Argument list too long
```

Por eso utilizamos `find`:
```bash
find /tmp -maxdepth 1 -name 'sess*' | wc -l
```
Resultado atípico: `288789` (cientos de miles de archivos)

## Causa raíz

Acumulación masiva de archivos de sesión PHP (`sess_*`) que no se eliminan automáticamente, causando agotamiento de inodos en `/tmp`.

## Solución inmediata

```bash
find /tmp -maxdepth 1 -type f -name 'sess*' -delete
```

**¿Por qué usar find?** Porque `rm sess*` falla con "Argument list too long" cuando hay miles de archivos.

## Prevención

### Configuración automática de limpieza

Añadir al crontab de root:

```bash
# Limpieza diaria de archivos temporales antiguos (>24h)
0 3 * * * find /tmp -type f -name "sess_*" -mtime +1 -delete >/dev/null 2>&1
```

### Script para aplicar cambios PHP en todas las versiones de cPanel

**Contexto**: En cPanel cada versión PHP tiene su propio archivo de configuración:
```
/opt/cpanel/ea-php81/root/etc/php.ini
/opt/cpanel/ea-php82/root/etc/php.ini
/opt/cpanel/ea-php83/root/etc/php.ini
```

**Script automático para todas las versiones:**

```bash
#!/bin/bash
# fix_php_gc_all_versions.sh
# Script para corregir configuración de garbage collector en todas las versiones PHP de cPanel

echo "=== Corrigiendo configuración PHP Garbage Collector ==="
echo "Fecha: $(date)"
echo

# Buscar todas las versiones de PHP instaladas de forma eficiente
PHP_VERSIONS=$(find /opt/cpanel -maxdepth 1 -name "ea-php*" -type d | grep -E "ea-php[0-9]+" | sort -V)

if [ -z "$PHP_VERSIONS" ]; then
    echo "❌ No se encontraron versiones de PHP en cPanel (/opt/cpanel/ea-php*)"
    exit 1
fi

echo "Versiones de PHP encontradas:"
echo "$PHP_VERSIONS" | sed 's/.*ea-php/- PHP /' | sed 's/$//'
echo

for php_dir in $PHP_VERSIONS; do
    php_ini="$php_dir/root/etc/php.ini"
    version=$(basename "$php_dir" | sed 's/ea-php//')
    
    echo "--- Procesando PHP $version ---"
    
    if [ ! -f "$php_ini" ]; then
        echo "⚠️  No existe: $php_ini"
        continue
    fi
    
    # Crear backup
    cp "$php_ini" "$php_ini.backup.$(date +%Y%m%d_%H%M%S)"
    echo "✅ Backup creado"
    
    # Mostrar configuración actual
    current_prob=$(grep -E "^session\.gc_probability\s*=" "$php_ini" | cut -d'=' -f2 | tr -d ' ')
    current_div=$(grep -E "^session\.gc_divisor\s*=" "$php_ini" | cut -d'=' -f2 | tr -d ' ')
    
    echo "   Configuración actual: gc_probability=$current_prob, gc_divisor=$current_div"
    
    # Aplicar cambios
    sed -i 's/^session\.gc_probability\s*=.*/session.gc_probability = 1/' "$php_ini"
    sed -i 's/^session\.gc_divisor\s*=.*/session.gc_divisor = 1000/' "$php_ini"
    
    # Verificar cambios
    new_prob=$(grep -E "^session\.gc_probability\s*=" "$php_ini" | cut -d'=' -f2 | tr -d ' ')
    new_div=$(grep -E "^session\.gc_divisor\s*=" "$php_ini" | cut -d'=' -f2 | tr -d ' ')
    
    echo "   Nueva configuración: gc_probability=$new_prob, gc_divisor=$new_div"
    echo "   ✅ PHP $version actualizado"
    echo
done

echo "=== Resumen ==="
echo "✅ Cambios aplicados a todas las versiones PHP"
echo "⚠️  Los cambios se aplicarán en nuevas sesiones PHP"
echo

# Comando de verificación
echo "Verificar cambios:"
echo "grep -E 'session\.gc_(probability|divisor)' /opt/cpanel/ea-php*/root/etc/php.ini"
```

**Version actualizada** del script se mantiene en [Git Lab Castris - Utilidades](https://gitlab.castris.com/root/utilidades/-/raw/main/cpanel/fix_php_gc_all_versions.sh?ref_type=heads)

**Uso del script:**

```bash
chmod +x fix_php_gc_all_versions.sh
./fix_php_gc_all_versions.sh
```

## Análisis de la configuración PHP problemática

**Configuración actual encontrada en servidores cPanel:**

```ini
session.gc_probability = 0    ; ⚠️ PROBLEMA: Numerador en 0
session.gc_divisor = 0        ; ⚠️ PROBLEMA: Denominador en 0
session.gc_maxlifetime = 1440 ; ✅ Correcto: 24 minutos (1440 segundos)
```

**Explicación del problema:**
El garbage collector de PHP funciona con esta fórmula:

- Probabilidad de limpieza = gc_probability / gc_divisor


Con la configuración actual: `0 / 0 = indefinido` → **Garbage collector completamente desactivado**

**Qué significa cada parámetro:**

- `gc_probability`: Numerador de la probabilidad (cuántas veces de cada X)
- `gc_divisor`: Denominador de la probabilidad (el total X)
- `gc_maxlifetime`: Tiempo en segundos después del cual una sesión se considera "basura"

**Ejemplos de probabilidades:**

- `1/100 = 1%` → 1 de cada 100 peticiones ejecuta limpieza
- `1/1000 = 0.1%` → 1 de cada 1000 peticiones ejecuta limpieza
- `0/0 = desactivado` → Nunca se ejecuta limpieza automática

### Configuración PHP corregida

**Para servidores de producción (recomendado):**

```ini
session.gc_probability = 1    ; Cambiar de 0 a 1
session.gc_divisor = 1000     ; Cambiar de 0 a 1000 (0.1% probabilidad)
session.gc_maxlifetime = 1440 ; Mantener actual
```

**Para servidores de desarrollo/bajo tráfico:**

```ini
session.gc_probability = 1    ; Cambiar de 0 a 1
session.gc_divisor = 100      ; Cambiar de 0 a 100 (1% probabilidad)
session.gc_maxlifetime = 1440 ; Mantener actual
```

**Justificación de los valores:**

- **Producción (1/1000)**: Menor impacto en rendimiento, suficiente con alto tráfico
- **Desarrollo (1/100)**: Mayor frecuencia de limpieza necesaria con poco tráfico

### Monitoreo de inodos

Script para alertas:

```bash
#!/bin/bash
#
# check_tmp_inodes.sh
# Checks the inode usage on /tmp and sends an alert if it exceeds a threshold.
#

# Ensure the script stops if any command fails
set -euo pipefail

# --- CONFIGURATION ---
# Path to the main configuration file
# Check repo for crons/.check_tmp_inodes.cfg file demo
# Not edit this file, copy the crons/.check_tmp_inodes.cfg file to /root/.check_tmp_inodes.cfg
CONFIG_FILE="/root/.check_tmp_inodes.cfg"

# --- FUNCTIONS ---

# Function to log errors and exit
# Usage: error_exit "Error message"
error_exit() {
    echo "ERROR: $1" >&2
    exit 1
}

# --- MAIN SCRIPT ---

# 1. Check if the configuration file exists and load it
if [ ! -f "$CONFIG_FILE" ]; then
    error_exit "Configuration file not found at $CONFIG_FILE"
fi

# Load variables from the configuration file
# shellcheck source=/dev/null
source "$CONFIG_FILE"

# 2. Verify that the necessary variables are defined
if [ -z "${EMAIL_TO-}" ] || [ -z "${THRESHOLD-}" ]; then
    error_exit "EMAIL_TO and THRESHOLD variables must be defined in $CONFIG_FILE"
fi

# 3. Get the current inode usage
# 'tail -n1' is used to be more robust on systems where 'df' might have more than 2 lines
USAGE=$(df -i /tmp | tail -n1 | awk '{print $5}' | sed 's/%//')

if ! [[ "$USAGE" =~ ^[0-9]+$ ]]; then
    error_exit "Could not retrieve a numeric value for inode usage. Got: '$USAGE'"
fi

# 4. Compare usage with the threshold and send an alert if necessary
echo "INFO: Current inode usage on /tmp: ${USAGE}% (Alert threshold: >${THRESHOLD}%)"

if [ "$USAGE" -gt "$THRESHOLD" ]; then
    # Build the message body
    HOSTNAME=$(hostname -f)
    BODY="ALERT on server: $HOSTNAME

Inode usage in the /tmp directory has exceeded the ${THRESHOLD}% threshold.

Current usage: ${USAGE}%

It is recommended to check the contents of /tmp to free up inode space.
"
    # Send the email
    echo "$BODY" | mail -s "$EMAIL_SUBJECT" "$EMAIL_TO"
    
    echo "ALERT: Threshold exceeded. Notification email sent to $EMAIL_TO."
else
    echo "INFO: Inode usage is within normal limits."
fi

exit 0
```

Este script se mantiene en [Gitlab Castris - Utilidades](https://gitlab.castris.com/root/utilidades/-/raw/main/crons/check_tmp_inodes.sh?ref_type=heads)

## Configuración de directorios temporales seguros

### Problema con la configuración actual

- `/home/user/tmp` → enlace simbólico a `/tmp` (cPanel)
- Todos los usuarios comparten el mismo espacio de inodos

### Solución recomendada

Configurar directorios temporales individuales:

```php
// En configuración PHP por usuario
session.save_path = "/home/USERNAME/temporal"
upload_tmp_dir = "/home/USERNAME/temporal"
```

## Monitoreo continuo

### Comando para verificar estado actual

```bash
echo "Espacio: $(df -h /tmp | awk 'NR==2 {print $5}')"
echo "Inodos: $(df -i /tmp | awk 'NR==2 {print $5}')"
echo "Archivos sess: $(find /tmp -maxdepth 1 -name 'sess*' 2>/dev/null | wc -l)"
```

### Señales de alerta temprana
- Uso de inodos > 80% en `/tmp`
- Más de 10,000 archivos `sess_*`
- Quejas de usuarios sobre lentitud en aplicaciones web

---
**Severidad**: Crítica - Afecta disponibilidad de MySQL y aplicaciones web  
**Impacto**: Todos los usuarios del servidor  
**Tiempo de resolución**: 2-5 minutos con la solución inmediata

##### Aviso
Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. 
También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad.
El contenido el contenido se entrega, tal y como está, sin que ello implique ningún obligación ni responsabilidad por parte de [Castris](https://castris.com)

Si necesitas soporte profesional puedes contratar con Castris [soporte profesional](https://intranet.castris.com/store/soporte-profesional).