En la lección anterior encontraste un problema de rendimiento cuya causa era un valor de configuración mal ajustado: max_conexiones=200 contra un max_connections=100. Lo encontraste midiendo, y lo corregiste comprobando el efecto. Esta lección hace lo mismo un nivel más abajo: los valores que vas a ajustar son los del kernel.
Y empieza con una advertencia, porque el ajuste del kernel es el área de la administración de sistemas con más folclore por metro cuadrado. Internet está lleno de listas de parámetros «para optimizar Linux» que se copian sin entender, que contradicen los valores por defecto sin motivo, y que en el mejor de los casos no hacen nada. La regla que separa el trabajo del ritual es una sola:
No se ajusta lo que no se ha medido.
Y el procedimiento que la aplica, que es el mismo que ya has usado dos veces —en el incidente de la web lenta de 05-07 y en el de la latencia de 07-02—:
- Medir el estado actual y anotarlo. Sin línea base no hay «después».
- Formular una hipótesis concreta: qué parámetro, por qué crees que importa, y qué esperas que cambie.
- Cambiar una sola cosa. Dos cambios simultáneos hacen imposible atribuir el resultado.
- Medir de nuevo con el mismo método.
- Documentar el cambio con su motivo, o revertirlo. Un parámetro sin justificación escrita es un parámetro que nadie podrá quitar dentro de un año.
Los valores por defecto de Ubuntu 24.04 son razonables para la mayoría de las cargas. Cambiarlos sin una medición que lo respalde es, con bastante probabilidad, empeorar el sistema.
Contenido
- sysctl: la interfaz de parámetros del kernel
- Memoria virtual
- Red y pila TCP
- Límites de ficheros y procesos
- Planificador de E/S
- Páginas enormes transparentes
- Límites por proceso y quién gana
- Gobernadores de frecuencia y tuned
- Módulos del kernel
- Compilar el kernel: cuándo tiene sentido y cuándo no
- Caso Tramontana: ajustar la pila de red con medición
sysctl: la interfaz de parámetros del kernel
El kernel expone sus parámetros ajustables como ficheros bajo /proc/sys/. Es «todo es un archivo» del Módulo 1 aplicado a la configuración del núcleo: leer el parámetro es leer un fichero, cambiarlo es escribir en él.
$ cat /proc/sys/vm/swappiness
60
$ sudo sh -c 'echo 10 > /proc/sys/vm/swappiness'
$ cat /proc/sys/vm/swappiness
10sysctl es la herramienta que hace lo mismo con una sintaxis de puntos, donde cada punto es una barra de la ruta:
$ sysctl vm.swappiness # equivale a /proc/sys/vm/swappiness
vm.swappiness = 10
$ sysctl -a 2>/dev/null | wc -l
1247
$ sysctl -a 2>/dev/null | grep -c '^net\.'
684Casi mil trescientos parámetros, de los cuales dos tercios son de red. La inmensa mayoría no se toca nunca.
Los tres modos de operación, y cuál usar cuándo:
| Operación | Comando | Persiste al reiniciar |
|---|---|---|
| Consultar | sysctl <parametro> |
— |
| Cambiar ahora, para probar | sudo sysctl -w <parametro>=<valor> |
No |
| Cambiar permanentemente | Fichero en /etc/sysctl.d/ + sysctl --system |
Sí |
Ese -w efímero es la herramienta del paso 3 del procedimiento: permite probar un cambio, medir, y si empeora las cosas basta con reiniciar —o volver a escribir el valor anterior— para deshacerlo. Nunca se pone un parámetro directamente en /etc/sysctl.d/ sin haberlo probado antes en caliente.
La persistencia y su precedencia:
$ ls /etc/sysctl.d/
10-console-messages.conf 10-network-security.conf 60-endurecimiento.conf
10-ipv6-privacy.conf 10-ptrace.conf 99-sysctl.conf
$ sudo tee /etc/sysctl.d/70-rendimiento.conf >/dev/null <<'EOF'
# Ajustes de rendimiento. Cada linea con su motivo y su medicion.
vm.swappiness = 10
EOF
$ sudo sysctl --system 2>&1 | grep -A1 70-rendimiento
* Applying /etc/sysctl.d/70-rendimiento.conf ...
vm.swappiness = 10Los ficheros se aplican en orden alfabético, y el último en escribir un parámetro gana. De ahí la convención de numerarlos: 10- para lo que viene de la distribución, 60- a 90- para lo propio, y 99- para lo que debe imponerse a todo. Y una nota importante: /etc/sysctl.conf sigue funcionando, pero está en desuso; usa /etc/sysctl.d/ con ficheros separados por propósito.
Fíjate en que ya tienes 60-endurecimiento.conf, de 06-06, con los parámetros de seguridad. Los de rendimiento van en un fichero aparte, y esa separación no es estética: cuando alguien tenga que revisar la postura de seguridad, o cuando haya que revertir un ajuste de rendimiento, se toca un solo fichero y no se mezcla lo que se puede quitar con lo que no.
Memoria virtual
El subsistema de memoria virtual es donde los ajustes tienen más impacto y donde hay más mitología. Los parámetros que importan:
vm.swappiness
El más malentendido de todos. No es «cuánto swap usar», ni un porcentaje de memoria. Es la preferencia relativa del kernel entre dos formas de liberar memoria cuando le hace falta: descartar páginas de caché de fichero, o mover páginas anónimas (memoria de proceso) a swap.
| Valor | Comportamiento | Cuándo |
|---|---|---|
0 |
Solo usa swap para evitar el OOM killer | Casi nunca; puede provocar OOM antes de tiempo |
1 |
Mínimo posible sin desactivarlo | Bases de datos con toda la RAM asignada |
10 |
Prefiere descartar caché | Servidores con RAM suficiente |
60 |
Por defecto | Escritorio y uso general |
100 |
Trata caché y anónimas por igual | Cargas donde la caché es más valiosa |
Y el dato que hay que interiorizar antes de tocarlo: si el sistema no está usando swap, swappiness no hace nada. Es un parámetro que solo se manifiesta bajo presión de memoria. Antes de cambiarlo, mide si hay presión:
# La medicion previa: hay actividad de swap? (columnas si/so)
$ vmstat 5 4
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 1284412 104882 1841204 0 0 0 12 412 882 3 2 95 0 0
0 0 0 1284188 104882 1841204 0 0 0 8 388 841 2 2 96 0 0
# Y el acumulado desde el arranque
$ grep -E 'pswpin|pswpout' /proc/vmstat
pswpin 0
pswpout 0Cero entradas y cero salidas de swap desde el arranque. En srv-tramontana, con 3,8 GB y 2,4 GB disponibles, swappiness es irrelevante hoy. Bajarlo no mejoraría nada.
¿Merece la pena cambiarlo entonces? Sí, pero por un motivo distinto del rendimiento medio: como seguro contra la degradación bajo presión. Si un día la aplicación crece y empieza a haber presión, con swappiness=10 el kernel preferirá descartar caché antes que mandar a swap las páginas activas del proceso, lo que evita el escenario de latencias de cientos de milisegundos. Es una decisión defendible, y así hay que documentarla: no «esto acelera el servidor», sino «esto limita el daño si algún día falta memoria».
vm.dirty_ratio y vm.dirty_background_ratio
Estos sí tienen efecto medible, y son la causa de un patrón de latencia muy característico.
Cuando un proceso escribe en un fichero, los datos van primero a la caché de página y se marcan como sucios (dirty); el kernel los vuelca a disco más tarde. Estos dos parámetros controlan cuándo:
$ sysctl vm.dirty_background_ratio vm.dirty_ratio vm.dirty_expire_centisecs
vm.dirty_background_ratio = 10
vm.dirty_ratio = 20
vm.dirty_expire_centisecs = 3000| Parámetro | Qué ocurre al alcanzarlo |
|---|---|
dirty_background_ratio |
El kernel empieza a volcar en segundo plano, sin bloquear a nadie |
dirty_ratio |
El proceso que escribe se bloquea hasta que se vuelca lo suficiente |
dirty_expire_centisecs |
Antigüedad máxima de una página sucia antes de volcarla (30 s) |
El patrón que producen: escrituras rápidas mientras hay margen, y de golpe una pausa larga cuando se alcanza dirty_ratio y el proceso queda bloqueado. Con 3,8 GB de RAM, el 20 % son unos 760 MB de datos sucios que pueden tener que irse a disco de golpe.
Eso es exactamente el perfil del incidente de 05-07: la copia de seguridad escribiendo en ráfagas, con w_await de 22,85 ms y la aplicación bloqueada. ionice mitigó el síntoma; estos parámetros atacan el mecanismo.
# Medicion previa: cuantos datos sucios hay en un momento dado
$ grep -E '^Dirty|^Writeback' /proc/meminfo
Dirty: 1024 kB
Writeback: 0 kB
# Y durante la copia, con el volumen cifrado como destino
$ while true; do grep '^Dirty:' /proc/meminfo; sleep 2; done
Dirty: 412844 kB
Dirty: 688204 kB # se acerca al 20% de 3,8 GB
Dirty: 12408 kB # volcado de golpe: aqui esta la pausaLos valores más pequeños producen volcados más frecuentes y pequeños, con latencia más uniforme:
Y la advertencia honesta: esto no hace el sistema «más rápido» en caudal total —a veces lo hace algo más lento— sino más predecible. Para un servicio interactivo, la latencia uniforme vale más que el caudal máximo. Para una copia de seguridad aislada, lo contrario. Es un compromiso, no una mejora.
El resto de la memoria virtual
| Parámetro | Por defecto | Qué hace | Cuándo tocarlo |
|---|---|---|---|
vm.vfs_cache_pressure |
100 | Agresividad al recuperar caché de inodos y dentries | Bajar a 50 si hay muchos ficheros pequeños y se relee mucho |
vm.overcommit_memory |
0 | 0 heurístico, 1 permitir todo, 2 estricto | 1 para Redis; 2 solo con overcommit_ratio bien calculado |
vm.min_free_kbytes |
~45000 | Reserva mínima libre | Subir si hay fallos de asignación en ráfagas de red |
vm.max_map_count |
65530 | Regiones de memoria por proceso | Subir para Elasticsearch y bases de datos grandes |
vm.overcommit_memory=2 merece un aviso: parece prudente («no prometas memoria que no tienes») y en la práctica hace que aplicaciones perfectamente normales fallen al reservar, porque muchas reservan mucho más de lo que usan. No se toca sin una razón muy concreta.
Red y pila TCP
Aquí es donde srv-tramontana tiene margen real, y donde el incidente de 07-02 dejó una pista sin explorar.
Colas de conexiones entrantes
Cuando llega una conexión TCP, pasa por dos colas antes de que la aplicación la acepte:
$ sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 1024| Parámetro | Qué cola controla |
|---|---|
tcp_max_syn_backlog |
Conexiones a medio establecer: llegó el SYN, falta el ACK final |
somaxconn |
Conexiones establecidas esperando que la aplicación las acepte |
Y el detalle crucial que hace que este parámetro se ajuste mal la mitad de las veces: somaxconn es un techo, no un valor efectivo. La cola real es el mínimo entre somaxconn y el parámetro backlog que la aplicación pasa a la llamada listen(). Subir somaxconn a 65535 no sirve de nada si la aplicación pide 128.
La medición que dice si la cola se está llenando:
# La columna Recv-Q en un socket LISTEN es la cola de aceptacion pendiente;
# Send-Q es el maximo efectivo (el minimo entre somaxconn y el backlog de la app)
$ ss -ltn
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 127.0.0.1:8080 0.0.0.0:*
LISTEN 0 4096 10.0.2.15:5432 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
# El contador de conexiones descartadas por cola llena: LA metrica que importa
$ nstat -az | grep -iE 'ListenOverflows|ListenDrops'
TcpExtListenOverflows 0 0.0
TcpExtListenDrops 0 0.0Ese Send-Q 128 del puerto 8080 es la información valiosa: la aplicación pide un backlog de 128, así que el somaxconn de 4096 no la afecta en absoluto. Y ListenOverflows a cero significa que ninguna conexión se ha descartado por cola llena. Conclusión: subir somaxconn en esta máquina no haría nada, y quien lo pusiera en su sysctl.d estaría añadiendo folclore.
Esto es el procedimiento funcionando: la hipótesis razonable («las colas de conexión son el cuello») queda refutada por la medición antes de tocar nada. Refutar hipótesis es tan valioso como confirmarlas, y bastante más barato.
Puertos efímeros y conexiones en espera
Aquí sí hay algo que el incidente de 07-02 dejó a la vista. Cuando la aplicación abría 300 conexiones cada diez segundos, cada una consumía un puerto efímero que quedaba en estado TIME_WAIT durante 60 segundos:
$ sysctl net.ipv4.ip_local_port_range net.ipv4.tcp_fin_timeout net.ipv4.tcp_tw_reuse
net.ipv4.ip_local_port_range = 32768 60999
net.ipv4.tcp_fin_timeout = 60
net.ipv4.tcp_tw_reuse = 2
$ ss -s
Total: 284
TCP: 112 (estab 24, closed 74, orphaned 0, timewait 71)Setenta y un sockets en TIME_WAIT. Con 28.231 puertos disponibles no es un problema, pero durante el incidente, con 30 conexiones por segundo, llegaron a acumularse unos 1.800 — y una aplicación que abriera diez veces más los agotaría.
TIME_WAIT no es un defecto: garantiza que paquetes retrasados de una conexión cerrada no se confundan con una nueva que reutilice el mismo par de puertos. Los ajustes posibles:
| Ajuste | Efecto | Riesgo |
|---|---|---|
Ampliar ip_local_port_range a 1024 65535 |
Más puertos disponibles | Puede chocar con servicios que escuchen en puertos altos |
tcp_tw_reuse = 1 |
Reutilizar sockets en TIME_WAIT para conexiones salientes |
Ninguno relevante hoy; requiere marcas de tiempo TCP |
Bajar tcp_fin_timeout |
Menos tiempo en TIME_WAIT |
Reintroduce el riesgo que TIME_WAIT evita |
tcp_tw_recycle |
— | Eliminado del kernel en 4.12. No existe. |
Ese último es el ejemplo perfecto de folclore: tcp_tw_recycle aparece en cientos de guías «para optimizar TCP», rompía las conexiones desde redes con NAT de forma sutil e intermitente, y fue eliminado del kernel hace años. Si encuentras una guía que lo recomienda, sabes que su autor no la ha probado en esta década.
En Ubuntu 24.04, tcp_tw_reuse ya viene en 2 (habilitado solo para direcciones de bucle y enlace local), que es un valor prudente. La corrección real del problema de 07-02 no era ningún sysctl: era arreglar el grupo de conexiones, que es lo que hiciste.
Búferes de socket
$ sysctl net.core.rmem_max net.core.wmem_max net.ipv4.tcp_rmem net.ipv4.tcp_wmem
net.core.rmem_max = 212992
net.core.wmem_max = 212992
net.ipv4.tcp_rmem = 4096 131072 6291456
net.ipv4.tcp_wmem = 4096 16384 4194304Los tres valores de tcp_rmem son mínimo, por defecto y máximo, y el kernel los ajusta automáticamente dentro de ese rango. Ampliar el máximo importa cuando el producto ancho de banda × latencia es grande: un enlace de 1 Gbit/s con 100 ms de ida y vuelta necesita unos 12 MB de ventana para saturarse, y con 6 MB de máximo se queda a la mitad.
En una red local con 0,4 ms de latencia, como la de Tramontana, esto es completamente irrelevante. Es un ajuste para transferencias de larga distancia, y ponerlo «por si acaso» solo consume memoria.
BBR: el ajuste de red que sí merece la pena
El control de congestión decide a qué velocidad envía TCP cuando detecta problemas en la red. El algoritmo clásico, CUBIC, interpreta la pérdida de paquetes como señal de congestión. BBR, desarrollado por Google, modela en cambio el ancho de banda y la latencia reales del camino.
La diferencia práctica es grande en enlaces con pérdida o con búferes grandes (el problema del bufferbloat), que es el caso de casi cualquier conexión a través de Internet:
$ sysctl net.ipv4.tcp_available_congestion_control net.ipv4.tcp_congestion_control
net.ipv4.tcp_available_congestion_control = reno cubic
net.ipv4.tcp_congestion_control = cubic
# Cargar el modulo de BBR
$ sudo modprobe tcp_bbr
$ sysctl net.ipv4.tcp_available_congestion_control
net.ipv4.tcp_available_congestion_control = reno cubic bbr
# Medir ANTES, con el cliente al otro lado de la red
$ iperf3 -c 198.51.100.20 -t 20 -R | tail -3
[ 5] 0.00-20.00 sec 184 MBytes 77.2 Mbits/sec receiver
# Cambiar (efimero) y medir DESPUES
$ sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
$ sudo sysctl -w net.core.default_qdisc=fq
$ iperf3 -c 198.51.100.20 -t 20 -R | tail -3
[ 5] 0.00-20.00 sec 441 MBytes 185 Mbits/sec receiverDe 77 a 185 Mbit/s en el mismo enlace, y con latencia menor bajo carga. default_qdisc=fq no es opcional: BBR necesita el encolado fair queue para funcionar como está diseñado, y sin él el resultado es peor.
Con la medición hecha, el cambio se hace persistente con su motivo escrito:
$ sudo tee -a /etc/sysctl.d/70-rendimiento.conf >/dev/null <<'EOF'
# Control de congestion BBR. Medido con iperf3 contra un cliente remoto:
# 77 -> 185 Mbit/s de bajada, y menor latencia bajo carga. Requiere fq.
# Medicion: 2026-08-18. Revertir con cubic si aparecen anomalias.
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
EOF
$ echo 'tcp_bbr' | sudo tee /etc/modules-load.d/bbr.conf
$ sudo sysctl --system >/dev/null && sysctl net.ipv4.tcp_congestion_control
net.ipv4.tcp_congestion_control = bbrFíjate en el comentario: qué se midió, con qué, el resultado, la fecha y cómo revertirlo. Eso es la diferencia entre un ajuste y una línea copiada.
Límites de ficheros y procesos
$ sysctl fs.file-max fs.file-nr kernel.pid_max fs.inotify.max_user_watches
fs.file-max = 9223372036854775807
fs.file-nr = 2848 0 9223372036854775807
kernel.pid_max = 4194304
fs.inotify.max_user_watches = 65536fs.file-max en kernels modernos es prácticamente ilimitado, así que subirlo —otro clásico de las guías— no hace nada. El límite real que se alcanza en la práctica es el por proceso, que se ve en el apartado siguiente.
fs.inotify.max_user_watches sí se agota de verdad, y con un error desconcertante. Cada inotify watch consume una entrada, y herramientas de desarrollo, sincronizadores de ficheros y logrotate los usan intensamente:
# El sintoma: "No space left on device" sin que falte espacio en disco
$ df -h / | tail -1
/dev/mapper/vg-root 23G 7,1G 15G 33% /
# La causa real
$ find /proc/*/fd -lname anon_inode:inotify 2>/dev/null | wc -l
1284
$ sudo sysctl -w fs.inotify.max_user_watches=524288Ese ENOSPC que no es falta de disco es una de las confusiones más frecuentes de Linux, y merece estar en el runbook.
Planificador de E/S
El planificador decide en qué orden se sirven las peticiones al disco. Ubuntu 24.04 usa la infraestructura de multicola (blk-mq):
$ cat /sys/block/sda/queue/scheduler
[none] mq-deadline kyber bfq
$ lsblk -d -o NAME,ROTA,SCHED
NAME ROTA SCHED
sda 1 none| Planificador | Cómo funciona | Para qué dispositivo |
|---|---|---|
none |
Sin reordenación; FIFO por cola | NVMe y SSD rápidos |
mq-deadline |
Plazos por petición, evita inanición | SATA SSD y discos mecánicos de servidor |
kyber |
Objetivos de latencia adaptativos | Cargas mixtas con muchas colas |
bfq |
Reparto equitativo por proceso | Escritorio; interactividad sobre caudal |
La regla de none para NVMe tiene un porqué concreto que hay que entender: un planificador reordena peticiones para minimizar el movimiento del cabezal de un disco mecánico. Un NVMe no tiene cabezal, atiende decenas de miles de operaciones por segundo y tiene sus propias colas en hardware. Reordenar solo añade latencia y consumo de CPU. En un disco mecánico, en cambio, reordenar puede multiplicar el caudal.
# Cambio efimero, para probar
$ echo mq-deadline | sudo tee /sys/block/sda/queue/scheduler
$ cat /sys/block/sda/queue/scheduler
none [mq-deadline] kyber bfqComo /sys no es persistente, la forma correcta de fijarlo es una regla de udev que decida según el tipo de dispositivo:
$ sudo tee /etc/udev/rules.d/60-planificador-io.rules >/dev/null <<'EOF'
# NVMe: sin planificador. El dispositivo tiene sus propias colas.
ACTION=="add|change", KERNEL=="nvme[0-9]*n[0-9]*", ATTR{queue/scheduler}="none"
# Discos rotacionales (ROTA=1): mq-deadline reordena y aprovecha la localidad.
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", \
ATTR{queue/scheduler}="mq-deadline"
# SSD SATA (ROTA=0): mq-deadline tambien, por sus plazos.
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", \
ATTR{queue/scheduler}="mq-deadline"
EOF
$ sudo udevadm control --reload && sudo udevadm trigger --subsystem-match=block
$ cat /sys/block/sda/queue/scheduler
none [mq-deadline] kyber bfqY otros dos parámetros de la cola que a veces importan:
read_ahead_kb es cuánto lee el kernel por adelantado. Subirlo (a 512 o más) ayuda en lecturas secuenciales grandes —una copia de seguridad, un volcado de base de datos—; bajarlo ayuda en accesos aleatorios pequeños, porque evita leer datos que no se van a usar. Es exactamente el compromiso que hay que medir antes de tocar.
Una observación sobre la pila de Tramontana: el volumen de copias es LUKS sobre LVM sobre sda, y cada capa presenta su propio dispositivo de bloque con su propia cola. El planificador que importa es el del dispositivo físico (sda); los dispositivos dm-* pasan las peticiones hacia abajo. Es la misma pila que había que descomponer en el primer ejercicio de 07-02.
Páginas enormes transparentes
El procesador traduce direcciones virtuales a físicas en páginas de 4 KB, usando una caché llamada TLB. Con mucha memoria, la TLB falla a menudo y cada fallo cuesta accesos extra a memoria. Las páginas enormes de 2 MB reducen drásticamente el número de entradas necesarias.
THP (Transparent Huge Pages) hace eso automáticamente. Y es el parámetro que todas las bases de datos piden desactivar:
$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
$ cat /sys/kernel/mm/transparent_hugepage/defrag
always defer [defer+madvise] madvise never| Valor | Comportamiento |
|---|---|
always |
THP para todo. Por defecto en Ubuntu |
madvise |
Solo para procesos que lo piden con madvise(MADV_HUGEPAGE) |
never |
Desactivado |
Por qué las bases de datos lo rechazan, que es la parte interesante: para dar una página de 2 MB, el kernel necesita 2 MB físicamente contiguos. Cuando la memoria está fragmentada, tiene que compactarla, y esa compactación ocurre de forma síncrona en el contexto del proceso que pide memoria. El resultado son pausas impredecibles de decenas o cientos de milisegundos justo en el proceso que menos las tolera. PostgreSQL, MongoDB, Redis, Oracle y Elasticsearch documentan todos la recomendación de madvise o never.
madvise es el mejor compromiso: los procesos que se benefician lo piden explícitamente, y los demás no pagan el coste.
Como /sys no persiste, se fija con una unidad de systemd —aplicando 05-05—, que es más limpio que un rc.local:
$ sudo tee /etc/systemd/system/thp-madvise.service >/dev/null <<'EOF'
[Unit]
Description=Poner transparent hugepages en madvise (recomendado por PostgreSQL)
Documentation=https://www.postgresql.org/docs/16/kernel-resources.html
DefaultDependencies=no
After=sysinit.target local-fs.target
Before=postgresql.service tramontana.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c 'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled'
[Install]
WantedBy=basic.target
EOF
$ sudo systemctl daemon-reload && sudo systemctl enable --now thp-madvise.service
$ cat /sys/kernel/mm/transparent_hugepage/enabled
always [madvise] neverEl Before=postgresql.service importa: el cambio tiene que estar aplicado antes de que la base de datos reserve su memoria compartida.
Límites por proceso y quién gana
Un límite del kernel es global; los que se alcanzan en la práctica son los por proceso, y aquí hay tres mecanismos que se pisan entre sí. Saber cuál gana ahorra horas de desconcierto.
| Mecanismo | Dónde | A quién se aplica |
|---|---|---|
ulimit |
Comando del shell | El proceso actual y sus hijos |
/etc/security/limits.conf |
PAM (pam_limits) |
Solo sesiones que pasan por PAM (login, SSH, su) |
LimitNOFILE= en la unidad |
systemd | Los servicios de systemd |
DefaultLimitNOFILE= |
/etc/systemd/system.conf |
Todos los servicios sin límite propio |
Y la respuesta a «cuál gana», que es la razón de este apartado: para un servicio de systemd, gana la unidad, y limits.conf no se aplica en absoluto. systemd arranca los servicios directamente, sin pasar por PAM. Poner svc-tramontana hard nofile 16384 en limits.conf y esperar que afecte a tramontana.service es un error clásico que no da ningún aviso: simplemente no funciona.
# Comprobar el limite REAL de un servicio en marcha: la fuente de verdad
$ pid=$(systemctl show tramontana.service -p MainPID --value)
$ grep -E 'Max open files|Max processes' /proc/$pid/limits
Max open files 1024 524288 files
Max processes 15370 15370 processesCon MemoryMax=512M y TasksMax=64 ya puestos en 05-05, el límite de ficheros abiertos se ajusta igual, por drop-in:
$ sudo tee /etc/systemd/system/tramontana.service.d/limites.conf >/dev/null <<'EOF'
[Service]
# 80 conexiones al grupo de PostgreSQL + sockets de cliente + logs + margen.
# Medido tras el ajuste de 07-02: pico de 214 descriptores.
LimitNOFILE=8192
EOF
$ sudo systemctl daemon-reload && sudo systemctl restart tramontana.service
$ pid=$(systemctl show tramontana.service -p MainPID --value)
$ grep 'Max open files' /proc/$pid/limits
Max open files 8192 8192 files
# Y verificar el uso real, para saber si 8192 es razonable o excesivo
$ ls /proc/$pid/fd | wc -l
214214 descriptores en uso con un límite de 8192: margen de sobra sin ser absurdo. Ese es el criterio — un límite se dimensiona a partir del uso medido, no de un número redondo copiado.
Gobernadores de frecuencia y tuned
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null \
|| echo "sin cpufreq (habitual en una VM)"
sin cpufreq (habitual en una VM)En una máquina virtual la gestión de frecuencia la hace el anfitrión, así que este apartado no aplica a srv-tramontana. En hardware físico:
| Gobernador | Comportamiento | Cuándo |
|---|---|---|
powersave |
Frecuencia mínima, sube según demanda | Por defecto; con intel_pstate es razonable |
performance |
Frecuencia máxima siempre | Servidores sensibles a la latencia |
schedutil |
Guiado por el planificador | El moderno; buen equilibrio |
El caso de uso real de performance es la latencia de cola: subir de frecuencia lleva microsegundos, y en un servicio con objetivos de latencia estrictos esos microsegundos aparecen en el percentil 99. A cambio de bastante más consumo eléctrico.
tuned es la forma sensata de aplicar conjuntos coherentes de ajustes en lugar de parámetros sueltos:
$ sudo apt install tuned
$ sudo tuned-adm list
Available profiles:
- balanced - General non-specialized tuned profile
- latency-performance - Optimize for deterministic performance
- network-latency - Optimize for deterministic performance, low latency
- network-throughput - Optimize for streaming network throughput
- throughput-performance - Broadly applicable tuning for throughput
- virtual-guest - Optimize for running inside a virtual guest
$ sudo tuned-adm active
Current active profile: virtual-guest
# Ver EXACTAMENTE que cambia un perfil antes de aplicarlo
$ cat /usr/lib/tuned/throughput-performance/tuned.conf | grep -A12 '\[sysctl\]'Ese último comando es el consejo importante: tuned es útil porque aplica conjuntos probados y consistentes, pero leer qué hace un perfil antes de activarlo es obligatorio. Un perfil puede cambiar veinte parámetros, y si uno de ellos entra en conflicto con tu sysctl.d, el resultado depende del orden de aplicación y es difícil de depurar.
Módulos del kernel
Un módulo es un trozo de kernel que se carga y descarga en caliente: drivers, sistemas de ficheros, protocolos.
$ lsmod | head -5
Module Size Used by
tcp_bbr 24576 20
dm_crypt 65536 1
raid1 49152 0
vboxguest 57344 2
$ modinfo tcp_bbr | head -5
filename: /lib/modules/6.8.0-41-generic/kernel/net/ipv4/tcp_bbr.ko.zst
license: Dual BSD/GPL
description: TCP BBR (Bottleneck Bandwidth and RTT)
depends:
intree: Y| Operación | Comando |
|---|---|
| Listar cargados | lsmod |
| Información | modinfo <modulo> |
| Cargar (con dependencias) | sudo modprobe <modulo> |
| Descargar | sudo modprobe -r <modulo> |
| Ver parámetros actuales | systool -v -m <modulo> |
La configuración persistente:
# Cargar un modulo en el arranque (lo usaste para BBR)
$ echo 'tcp_bbr' | sudo tee /etc/modules-load.d/bbr.conf
# Pasar parametros a un modulo
$ sudo tee /etc/modprobe.d/tramontana.conf >/dev/null <<'EOF'
# Reduce el uso de memoria del modulo de cifrado en una VM pequena
options dm_crypt max_read_size=131072
EOF
# Impedir que un modulo se cargue: reduccion de superficie de ataque (06-06)
$ sudo tee /etc/modprobe.d/blacklist-tramontana.conf >/dev/null <<'EOF'
# Sistemas de ficheros y protocolos que este servidor no usa nunca.
# Cada linea reduce superficie de ataque: son modulos con CVE historicos.
install cramfs /bin/true
install freevxfs /bin/true
install jffs2 /bin/true
install hfs /bin/true
install hfsplus /bin/true
install udf /bin/true
install dccp /bin/true
install sctp /bin/true
install rds /bin/true
install tipc /bin/true
EOF
$ sudo update-initramfs -u -k allDos detalles importantes. El primero: install <modulo> /bin/true es más eficaz que blacklist <modulo>, porque blacklist solo evita la carga automática y no impide que otro módulo lo cargue como dependencia. El segundo: tras tocar modprobe.d, hay que regenerar el initramfs, por lo que aprendiste en 07-01 — el initramfs lleva su propia copia de esa configuración.
Esta lista de módulos bloqueados es, de hecho, una recomendación de los CIS Benchmarks que quedó pendiente en la checklist de 06-06. Es un buen momento para cerrarla.
DKMS (Dynamic Kernel Module Support) resuelve el problema de los módulos de terceros: un módulo compilado para el kernel 6.8.0-41 no funciona en el 6.8.0-45. DKMS lo recompila automáticamente en cada actualización de kernel:
$ dkms status
virtualbox-guest/7.0.16, 6.8.0-41-generic, x86_64: installed
virtualbox-guest/7.0.16, 6.8.0-39-generic, x86_64: installedCon Secure Boot activo, un módulo recompilado por DKMS necesita firma, y ahí entra el mmx64.efi (MokManager) que viste en 07-01. Es la causa habitual de que un driver de terceros deje de funcionar tras actualizar el kernel en una máquina con Secure Boot.
Compilar el kernel: cuándo tiene sentido y cuándo no
Compilar el kernel es un rito de paso en el aprendizaje de Linux, y conviene ser honesto sobre su utilidad en un servidor de producción: casi nunca la tiene.
| Motivo alegado | ¿Compensa? |
|---|---|
| «Un kernel más pequeño y rápido» | No. Los módulos no cargados no consumen nada. La ganancia es indetectable |
| «Necesito una opción no habilitada» | A veces. Primero comprueba si existe un paquete de Ubuntu que ya la trae |
| «Necesito un parche que no está en ninguna versión» | Sí. Es el caso legítimo |
| «Necesito una versión muy reciente para un driver» | No. Usa el kernel HWE o linux-generic-hwe-24.04 |
| «Para aprender» | Sí, y en el laboratorio, no en producción |
Y el coste que se subestima: un kernel compilado a mano queda fuera del sistema de paquetes. No recibe actualizaciones de seguridad automáticas —adiós al unattended-upgrades de 05-03—, no está firmado para Secure Boot, y cada CVE del kernel exige recompilar a mano. En una máquina que trata datos personales, eso es un problema de cumplimiento, no solo de comodidad.
Las variantes empaquetadas cubren casi todo:
| Paquete | Para qué |
|---|---|
linux-image-generic |
Uso general. Es lo que tienes |
linux-image-virtual |
Máquinas virtuales: sin drivers de hardware físico, más pequeño |
linux-image-generic-hwe-24.04 |
Kernel más reciente sobre la LTS, para hardware nuevo |
linux-image-lowlatency |
Menor latencia de planificación, menor caudal |
El procedimiento, para el laboratorio:
$ sudo apt install build-essential libncurses-dev bison flex libssl-dev \
libelf-dev dwarves zstd
$ apt source linux-image-unsigned-$(uname -r)
$ cd linux-6.8.0
# Partir de la configuracion ACTUAL, nunca de cero
$ cp /boot/config-$(uname -r) .config
$ make olddefconfig # ajusta la config antigua a las opciones nuevas
$ make menuconfig # solo el cambio que necesitas
$ make -j"$(nproc)" 2>&1 | tail -3
$ sudo make modules_install
$ sudo make install # copia vmlinuz y ejecuta update-initramfs
$ sudo update-grub
# Y la red de seguridad de 07-01: el kernel anterior sigue en el menu
$ awk -F"'" '/menuentry .Ubuntu, with Linux/ {print $2}' /boot/grub/grub.cfgmake olddefconfig partiendo de /boot/config-$(uname -r) es lo que evita el error del principiante: make defconfig genera una configuración genérica que probablemente no incluya el driver de tu controlador de disco, y el resultado es el prompt (initramfs) de 07-01.
Caso Tramontana: ajustar la pila de red con medición
El ejercicio completo, aplicando el procedimiento de cinco pasos. Punto de partida: la línea base de 05-07, el incidente de los db_timeout con conexiones_activas=200, y el ajuste del grupo de conexiones de 07-02.
Paso 1: medir el estado actual
$ cat ~/scripts/linea_base_kernel.sh
#!/usr/bin/env bash
# linea_base_kernel.sh - Registra los parametros y contadores del kernel
# relevantes para el rendimiento, para poder comparar antes y despues.
# Uso: linea_base_kernel.sh [etiqueta]
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/comunes.sh
source "${SCRIPT_DIR}/lib/comunes.sh"
readonly ETIQUETA_LOG="linea-base-kernel"
readonly DESTINO="${TRAMONTANA_BASE_DIR:-/home/operador/datos/lineas-base}"
main() {
local etiqueta="${1:-manual}"
local fecha; fecha="$(date +%Y%m%d-%H%M%S)"
local fichero="${DESTINO}/kernel-${etiqueta}-${fecha}.txt"
install -d -m 750 "$DESTINO"
{
printf '# Linea base de kernel — %s — %s\n\n' "$etiqueta" "$(date -Is)"
printf '## Parametros\n'
sysctl vm.swappiness vm.dirty_ratio vm.dirty_background_ratio \
net.core.somaxconn net.ipv4.tcp_max_syn_backlog \
net.ipv4.tcp_congestion_control net.core.default_qdisc \
net.ipv4.ip_local_port_range 2>/dev/null
printf '\n## Planificador de E/S\n'
for d in /sys/block/sd*/queue/scheduler; do
printf '%s: %s\n' "$d" "$(cat "$d")"
done
printf '\n## Contadores de red (los que importan)\n'
nstat -az 2>/dev/null | grep -iE 'ListenOverflows|ListenDrops|TCPSynRetrans|RetransSegs'
printf '\n## Sockets\n'
ss -s
printf '\n## Colas de escucha\n'
ss -ltn
printf '\n## Swap\n'
grep -E 'pswpin|pswpout' /proc/vmstat
printf '\n## Latencia de la aplicacion (5 muestras)\n'
for _ in {1..5}; do
curl -s -o /dev/null -w '%{time_total}\n' \
"http://127.0.0.1:${TRAMONTANA_PUERTO:-8080}/casas" || true
done
} >"$fichero"
chmod 640 "$fichero"
log "linea base escrita en $fichero"
printf '%s\n' "$fichero"
}
main "$@"$ chmod +x ~/scripts/linea_base_kernel.sh
$ shellcheck ~/scripts/linea_base_kernel.sh && ~/scripts/linea_base_kernel.sh antes
[2026-08-18 17:12:04] linea-base-kernel: linea base escrita en /home/operador/datos/lineas-base/kernel-antes-20260818-171204.txtPaso 2: hipótesis, y la que se refuta
Tres hipótesis razonables a partir del historial, y qué dice la medición de cada una:
| Hipótesis | Medición | Veredicto |
|---|---|---|
| Las colas de conexión se desbordan | ListenOverflows = 0, Send-Q = 128 (backlog de la app) |
Refutada. somaxconn no interviene |
| Hay presión de memoria y swap | pswpin = 0, pswpout = 0, 2,4 GB disponibles |
Refutada hoy; se ajusta como seguro |
| El control de congestión limita el tráfico remoto | cubic, y 77 Mbit/s medidos con iperf3 |
Confirmada. BBR da 185 Mbit/s |
Dos de tres refutadas por la medición, y eso es el resultado normal. Si hubieras copiado una guía de «optimización de red para Linux», habrías puesto somaxconn = 65535 —que aquí no hace nada— y probablemente tcp_tw_recycle, que ya no existe.
Paso 3 y 4: un cambio, medir
# El unico cambio confirmado por medicion, primero en efimero
$ sudo modprobe tcp_bbr
$ sudo sysctl -w net.core.default_qdisc=fq
$ sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
$ iperf3 -c 198.51.100.20 -t 20 -R | tail -2
[ 5] 0.00-20.00 sec 441 MBytes 185 Mbits/sec receiver
# Y verificar que no ha empeorado nada mas
$ ~/scripts/revision_salud.sh; echo "estado: $?"
estado: 0
$ ~/scripts/linea_base_kernel.sh despues
$ diff -u ~/datos/lineas-base/kernel-antes-*.txt \
~/datos/lineas-base/kernel-despues-*.txt | head -20Paso 5: documentar el fichero final
$ sudo tee /etc/sysctl.d/70-rendimiento.conf >/dev/null <<'EOF'
# Ajustes de rendimiento de srv-tramontana.
# REGLA: cada parametro lleva su motivo, su medicion y su fecha.
# Los parametros de SEGURIDAD estan en 60-endurecimiento.conf, aparte
# a proposito: estos se pueden revertir, aquellos no.
# --- Red ---------------------------------------------------------------
# BBR modela ancho de banda y latencia en vez de reaccionar a la perdida.
# Medido con iperf3 contra 198.51.100.20 el 2026-08-18:
# cubic: 77 Mbit/s -> bbr: 185 Mbit/s, y menor latencia bajo carga.
# Requiere el encolado fq: sin el, BBR rinde PEOR que cubic.
# Revertir: net.ipv4.tcp_congestion_control = cubic
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
# --- Memoria virtual ---------------------------------------------------
# NO es una mejora de rendimiento medio: hoy no hay actividad de swap
# (pswpin=0, pswpout=0). Es un SEGURO: si algun dia hay presion de memoria,
# el kernel preferira descartar cache antes que enviar a swap las paginas
# activas del proceso, evitando latencias de cientos de ms.
vm.swappiness = 10
# Umbrales de paginas sucias mas bajos que el 10/20 por defecto.
# Motivo: con 3,8 GB, el 20% son ~760 MB que pueden volcarse de golpe y
# bloquear al proceso que escribe. Es el mecanismo del incidente del
# 2026-08-18 (copia nocturna saturando E/S, w_await 22,85 ms).
# Efecto: menos caudal maximo, latencia mas uniforme. Compromiso aceptado.
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10
# --- Ficheros ----------------------------------------------------------
# Se agota de verdad y el error es enganoso ("No space left on device"
# sin que falte disco). Medido: 1284 watches en uso con 65536 de limite.
fs.inotify.max_user_watches = 524288
# NO TOCAMOS, y consta por que:
# net.core.somaxconn: la app pide backlog=128, asi que somaxconn no
# interviene (ss -ltn: Send-Q=128). ListenOverflows=0. Sin efecto.
# net.core.rmem_max/wmem_max: red local con 0,4 ms de RTT. El producto
# ancho de banda x latencia no justifica buferes mayores.
# net.ipv4.tcp_tw_recycle: NO EXISTE. Eliminado del kernel en 4.12.
EOF
$ sudo sysctl --system >/dev/null
$ sysctl net.ipv4.tcp_congestion_control vm.swappiness
net.ipv4.tcp_congestion_control = bbr
vm.swappiness = 10La sección final —«NO TOCAMOS, y consta por qué»— es la parte del fichero que más valor tiene a largo plazo. Documenta las hipótesis refutadas, y eso impide que dentro de seis meses alguien —tú incluido— añada somaxconn = 65535 porque lo ha visto en una guía. Un ajuste descartado con su motivo escrito es conocimiento; un ajuste ausente es solo una omisión.
Errores Comunes y Consejos
- Copiar listas de
sysctlde Internet. La mayoría son de hace diez años, muchas contradicen valores por defecto que ya son correctos, y algunas recomiendan parámetros que ya no existen (tcp_tw_recyclees la señal inequívoca de una guía sin probar). - Cambiar varias cosas a la vez. Si mejora, no sabes cuál; si empeora, tampoco. Un cambio, una medición.
- Poner un parámetro en
/etc/sysctl.d/sin probarlo con-w. El cambio efímero es reversible con un reinicio; el persistente puede dejarte un sistema que arranca mal. - Creer que
swappinessreduce el uso de memoria. Solo actúa cuando hay presión y el sistema recurre a swap. Sin swap en uso, no hace nada. - Subir
somaxconnsin mirar elbacklogde la aplicación. El límite efectivo es el mínimo de los dos.ss -ltnmuestra el real enSend-Q. - Confiar en
limits.confpara un servicio de systemd. No se aplica: systemd no pasa por PAM. Se usaLimitNOFILE=en la unidad, y se verifica en/proc/<pid>/limits. - Dejar THP en
alwayscon una base de datos. Provoca pausas síncronas de compactación impredecibles.madvisees el compromiso correcto, y hay que aplicarlo antes de que la base de datos arranque. - Poner
mq-deadlineen un NVMe. Reordenar peticiones no tiene sentido sin cabezal: solo añade latencia.nonepara NVMe. - Olvidar
update-initramfstras tocar/etc/modprobe.d/. El initramfs lleva su propia copia. Es la lección de 07-01. - Compilar el kernel en producción. Queda fuera del sistema de paquetes: sin actualizaciones de seguridad automáticas, sin firma para Secure Boot, y con cada CVE a mano. Prueba primero
linux-image-virtualo el kernel HWE. - Ajustar sin línea base. Sin un «antes» registrado, el «después» no significa nada, y la sensación de mejora es notoriamente poco fiable.
- Consejo de método. El comentario de cada parámetro debe responder a cuatro preguntas: qué mediste, con qué comando, qué resultado, y cómo revertirlo. Y documenta también lo que decidiste no tocar: es lo que evita que el folclore vuelva a entrar.
Ejercicios
Ejercicio 1
Luis te manda esta lista «para optimizar el servidor», sacada de un blog:
net.core.somaxconn = 65535
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_max_syn_backlog = 65535
fs.file-max = 2097152
vm.swappiness = 0
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728Evalúa cada línea: qué haría, si tiene sentido en srv-tramontana, y con qué comando lo comprobarías. Redacta la respuesta que le enviarías.
Ejercicio 2
Tras el ajuste de 07-02, la aplicación mantiene un grupo de 80 conexiones a PostgreSQL. Comprueba si los límites de descriptores de fichero son adecuados en los tres niveles implicados (el servicio de la aplicación, el de PostgreSQL, y el global), y ajusta lo que haga falta con su medición.
Ejercicio 3
/srv/tramontana/backups es LUKS sobre LVM sobre sda, y quieres saber si el planificador de E/S influye en la duración de la copia nocturna. Diseña el experimento: qué medirías, cómo aislarías la variable, y por qué el resultado podría no ser concluyente.
Soluciones
Solución 1
Re: lista de optimización del kernel
Gracias, Luis. La he revisado línea por línea contra el estado real del servidor. Resumen: una tiene sentido, una es contraproducente, una no existe, y cuatro no harían nada. Te paso el detalle con los comandos, para que lo puedas comprobar tú mismo.
1.
net.core.somaxconn = 65535— Sin efecto
somaxconnes un techo. La cola real es el mínimo entre este valor y elbacklogque la aplicación pide enlisten().$ ss -ltn | grep 8080 LISTEN 0 128 127.0.0.1:8080 0.0.0.0:* $ nstat -az | grep ListenOverflows TcpExtListenOverflows 0 0.0
Send-Q = 128: la aplicación pide 128, así que el valor actual (4096) ya es irrelevante, y 65535 lo sería igual. YListenOverflows = 0significa que nunca se ha descartado una conexión por cola llena. Para que este parámetro sirviera habría que cambiar elbacklogen el código de la aplicación, y no hay ninguna evidencia de que haga falta.2.
net.ipv4.tcp_tw_recycle = 1— No existe$ sysctl net.ipv4.tcp_tw_recycle sysctl: cannot stat /proc/sys/net/ipv4/tcp_tw_recycle: No such file or directorySe eliminó del kernel en la versión 4.12 (2017) porque rompía las conexiones desde redes con NAT de forma intermitente y muy difícil de diagnosticar. Que aparezca en la guía nos dice algo útil: el autor no la ha probado en esta década, así que conviene desconfiar del resto. Si el objetivo era el reciclado de
TIME_WAIT, el parámetro vigente estcp_tw_reuse, y ya está en2, que es el valor prudente por defecto en Ubuntu 24.04.3.
net.ipv4.tcp_max_syn_backlog = 65535— Sin efecto medible$ nstat -az | grep -iE 'TCPReqQFullDrop|ListenDrops' TcpExtListenDrops 0 0.0Es la cola de conexiones a medio establecer, y su utilidad real es resistir una inundación de SYN. Cero descartes con el valor actual de 1024. Además, contra ese ataque ya tenemos
tcp_syncookiesactivo desde 06-03, que es la defensa correcta. Subirlo reservaría memoria del kernel sin beneficio.4.
fs.file-max = 2097152— Sin efecto (y sería una bajada)$ sysctl fs.file-max fs.file-max = 9223372036854775807 $ sysctl fs.file-nr fs.file-nr 2848 0 9223372036854775807En kernels modernos ya es prácticamente ilimitado: fijar 2.097.152 sería reducirlo. Y usamos 2.848. El límite que sí se alcanza en la práctica es el de por proceso, que en un servicio de systemd se pone con
LimitNOFILE=en la unidad —limits.confno se aplica a los servicios, que es un detalle que confunde a mucha gente.5.
vm.swappiness = 0— ContraproducenteEste es el que me preocupa.
swappiness = 0no significa «no uses swap», significa «usa swap solo para evitar el OOM killer». El efecto real es que el kernel agota la caché por completo antes de considerar swap, y en un pico de memoria puede invocar al OOM killer antes de tiempo — matando el proceso de la aplicación o de PostgreSQL.$ grep -E 'pswpin|pswpout' /proc/vmstat pswpin 0 pswpout 0 $ free -h | awk 'NR==2 {print "disponible:", $7}' disponible: 2,4GiHoy da igual porque no hay actividad de swap, pero el día que la haya,
0es peor que el valor por defecto. He puesto10, que prefiere descartar caché sin renunciar a swap como red de seguridad, y lo he documentado como seguro contra la degradación, no como mejora de rendimiento.6 y 7.
rmem_max/wmem_max= 128 MB — Sin efecto, y consume memoriaLos búferes grandes importan cuando el producto ancho de banda × latencia es grande.
$ ping -c3 10.0.2.15 | tail -1 rtt min/avg/max/mdev = 0.312/0.398/0.482/0.071 msCon 0,4 ms de ida y vuelta en red local, la ventana necesaria para saturar 1 Gbit/s son unos 50 KB. El máximo actual (208 KB de
rmem_max, y hasta 6 MB entcp_rmem) sobra con holgura. Reservar 128 MB por socket en una máquina con 3,8 GB de RAM es un riesgo real si se abren muchas conexiones.
Lo que sí he hecho, y por qué
Siguiendo el mismo criterio —medir primero—, el único cambio de red que ha resultado justificado es el control de congestión BBR, que no estaba en tu lista:
# cubic (por defecto) $ iperf3 -c 198.51.100.20 -t 20 -R | tail -2 [ 5] 0.00-20.00 sec 184 MBytes 77.2 Mbits/sec receiver # bbr + fq $ iperf3 -c 198.51.100.20 -t 20 -R | tail -2 [ 5] 0.00-20.00 sec 441 MBytes 185 Mbits/sec receiverDe 77 a 185 Mbit/s hacia clientes remotos. Ese sí es un ajuste con efecto medido, y está en
/etc/sysctl.d/70-rendimiento.confcon la medición, la fecha y la forma de revertirlo.Lo que propongo para la próxima vez. Antes de aplicar un parámetro, tres preguntas: ¿qué contador me dice que este recurso es el cuello? ¿lo he probado con
sysctl -wy medido antes y después? ¿está escrito el motivo? Si alguna respuesta es no, el parámetro no entra. He añadido al fichero una sección de «NO TOCAMOS, y consta por qué» con estas siete líneas y su razón, precisamente para que no volvamos a discutirlo dentro de seis meses.
Solución 2
Los tres niveles, medidos de dentro hacia fuera.
# --- Nivel 1: el servicio de la aplicacion ---
$ pid_app=$(systemctl show tramontana.service -p MainPID --value)
$ grep 'Max open files' /proc/$pid_app/limits
Max open files 8192 8192 files
$ ls /proc/$pid_app/fd | wc -l
214Uso real 214 sobre un límite de 8192: holgado y correcto. El desglose confirma que el número tiene sentido:
$ ls -l /proc/$pid_app/fd | awk '{print $NF}' | sed 's/[0-9]*$//' \
| sort | uniq -c | sort -rn | head -5
80 socket:[
94 /opt/tramontana/releases/3.2.1/plantillas/
14 /var/log/tramontana/
3 pipe:[
2 /dev/null80 sockets = el grupo de conexiones a PostgreSQL, exactamente el valor de max_conexiones=80 que se ajustó en 07-02. Los descriptores están donde deben.
# --- Nivel 2: PostgreSQL. Aqui esta el problema. ---
$ pid_pg=$(systemctl show [email protected] -p MainPID --value)
$ grep 'Max open files' /proc/$pid_pg/limits
Max open files 1024 524288 files
$ sudo -u postgres psql -tAc "SHOW max_connections;"
100
$ sudo -u postgres psql -tAc "SHOW max_files_per_process;"
1000El límite blando es 1024, y max_files_per_process es 1000. Y aquí está el cálculo que revela el riesgo: cada proceso de backend de PostgreSQL abre descriptores para los ficheros de tabla e índice que toca. Con 100 conexiones posibles y hasta 1000 ficheros por proceso, el límite de 1024 del proceso principal es estrecho.
# La comprobacion directa: uso real agregado
$ sudo ls /proc/$pid_pg/fd | wc -l
88
$ for p in $(pgrep -P $pid_pg); do sudo ls /proc/$p/fd 2>/dev/null | wc -l; done \
| awk '{s+=$1} END {print "descriptores en los backends:", s}'
descriptores en los backends: 412Hoy no se está alcanzando, pero el margen es escaso y el fallo, cuando llega, se manifiesta como errores de conexión intermitentes bajo carga — difíciles de diagnosticar.
# Corregir por drop-in, nunca editando la unidad del paquete
$ sudo mkdir -p /etc/systemd/system/[email protected]
$ sudo tee /etc/systemd/system/[email protected]/limites.conf >/dev/null <<'EOF'
[Service]
# max_connections=100 x max_files_per_process=1000 en el peor caso.
# 65536 da margen amplio sin ser absurdo. Medido el 2026-08-18:
# proceso principal 88 fd, backends 412 fd agregados.
LimitNOFILE=65536
EOF
$ sudo systemctl daemon-reload && sudo systemctl restart [email protected]
$ pid_pg=$(systemctl show [email protected] -p MainPID --value)
$ grep 'Max open files' /proc/$pid_pg/limits
Max open files 65536 65536 files# --- Nivel 3: el limite global ---
$ sysctl fs.file-nr fs.file-max
fs.file-nr 2848 0 9223372036854775807
fs.file-max = 92233720368547758072.848 descriptores en todo el sistema contra un límite prácticamente infinito: no hay nada que ajustar, y esto confirma lo que se le respondió a Luis en el ejercicio anterior sobre fs.file-max.
# Y el valor por defecto para servicios que no declaren limite propio
$ grep -i defaultlimitnofile /etc/systemd/system.conf
#DefaultLimitNOFILE=1024:524288Aquí hay una decisión de criterio: se podría subir el valor por defecto global, pero es preferible no hacerlo. Un límite por defecto alto oculta las fugas de descriptores: un servicio que no cierra lo que abre falla pronto y de forma visible con un límite razonable, y consume memoria del kernel silenciosamente durante meses con uno alto. Mejor límites explícitos y dimensionados por servicio.
Verificación final:
$ ~/scripts/revision_salud.sh; echo "estado: $?"
estado: 0
$ sudo -u postgres psql -tAc "SELECT count(*) FROM pg_stat_activity;"
82
$ sudo timeout 10 strace -f -c -p $pid_app 2>&1 | grep -cE 'EMFILE|ENFILE'
0Cero errores EMFILE/ENFILE, que son los que aparecerían si se agotaran los descriptores. Y la lección de método: el problema estaba en PostgreSQL, no en la aplicación, aunque el síntoma anterior se manifestara en la aplicación. Igual que en 07-02, la causa estaba al otro lado de la relación cliente-servidor. Conviene añadir al runbook la regla: al ajustar un límite de un lado, comprobar el del otro.
Solución 3
Qué mediría, y por qué esas métricas. La duración total de la copia es la variable de interés, pero es demasiado gruesa para atribuirla al planificador. Hay que medir en tres niveles:
# 1. Duracion total (lo que le importa a Marta)
$ systemd-analyze --no-pager verify tramontana-respaldo.service
$ sudo systemctl show tramontana-respaldo.service \
-p ExecMainStartTimestamp -p ExecMainExitTimestamp
# 2. Latencia por DISPOSITIVO. -D separa las capas de la pila, que es
# la razon de usar biolatency en vez de la media de iostat.
$ sudo biolatency-bpfcc -D 300 1 > /tmp/lat-planificador.txt
# 3. Donde se bloquea el proceso: cifrado (CPU) o E/S (espera)
$ sudo timeout 60 offcputime-bpfcc -p $(pgrep -f 'restic backup') -f \
| sort -k2 -rn | head -5
$ sudo perf stat -p $(pgrep -f 'restic backup') -- sleep 30 2>&1 \
| grep -E 'CPUs utilized'Cómo aislaría la variable. Este es el núcleo del ejercicio, y hay cuatro fuentes de contaminación que hay que neutralizar:
# a) Datos identicos en ambas ejecuciones. Una copia incremental de restic
# depende de lo que haya cambiado: dos noches distintas NO son comparables.
# Solucion: partir del mismo snapshot en las dos pruebas.
$ sudo restic -r "$REPO" snapshots --latest 1
$ sudo lvcreate -L 4G -s -n prueba-io /dev/vg-datos/lv-backups
# b) Estado de la cache de pagina identico. Una segunda ejecucion es mas
# rapida solo porque los datos ya estan en cache.
$ sync && echo 3 | sudo tee /proc/sys/vm/drop_caches
# c) Sin carga concurrente. La copia real corre a las 02:30 con ionice;
# para el experimento hay que quitar tanto la carga como el ionice,
# porque ionice interactua con el planificador y confundiria el resultado.
$ sudo systemctl stop tramontana.service # solo en la VM de pruebas
$ sudo ss -tn state established | wc -l
# d) Varias repeticiones, no una. La variabilidad de E/S en una VM es alta.
$ for planificador in none mq-deadline bfq; do
for repeticion in 1 2 3; do
echo "$planificador" | sudo tee /sys/block/sda/queue/scheduler >/dev/null
sync && echo 3 | sudo tee /proc/sys/vm/drop_caches >/dev/null
inicio=$(date +%s)
sudo restic -r /srv/tramontana/backups/restic backup \
--quiet /opt/tramontana/releases/3.2.1
printf '%s\t%s\t%s\n' "$planificador" "$repeticion" "$(( $(date +%s) - inicio ))"
done
done | tee /tmp/experimento-planificador.tsvY una decisión de diseño importante: el experimento se hace en srv-tramontana-pruebas, no en producción. Es la VM que crearás en la lección siguiente, y este es exactamente el caso de uso que la justifica: cambiar el planificador de E/S en caliente en el servidor real, con drop_caches y sin ionice, es pedir una degradación del servicio.
Por qué el resultado podría no ser concluyente. Cinco motivos, y son los que hacen de este un buen ejercicio:
-
El planificador que importa es el del dispositivo físico, no el de la pila.
/srv/tramontana/backupsesdm-1(LUKS) sobrelv-backups(LVM) sobresda. Los dispositivosdm-*no tienen planificador propio: pasan las peticiones hacia abajo. Se comprueba:$ cat /sys/block/dm-1/queue/scheduler noneUn
nonefijo, sin alternativas entre corchetes, indica que ese dispositivo no planifica. Así que el experimento solo tiene sentido sobresda. -
Estamos en una máquina virtual. El planificador del huésped opera sobre un disco que en realidad es un fichero en el anfitrión, que tiene su propio planificador y su propia caché. Las decisiones del huésped pueden quedar completamente anuladas por el anfitrión. Esto es lo que probablemente hará el resultado no concluyente, y es la razón por la que
virtual-guestes el perfil detunedactivo. -
El cuello podría ser el cifrado, no la E/S. Si
perf statdaCPUs utilizedcerca de 1,00 yoffcputimeseñala funciones de AES, el proceso está limitado por CPU y el planificador de E/S es irrelevante por definición. Este control hay que hacerlo antes del experimento: si el cuello es el cifrado, el experimento no procede. -
La copia es en su mayoría escritura secuencial, y ahí las diferencias entre planificadores son pequeñas. Donde se separan de verdad es en la mezcla de lectura aleatoria y escritura con varios procesos compitiendo.
-
resticdeduplica y comprime, así que la relación entre datos leídos y bloques escritos no es fija. Aunque los datos de origen sean idénticos, la carga de E/S puede variar entre ejecuciones.
Conclusión del diseño. El experimento hay que hacerlo, pero con la expectativa correcta: la hipótesis más probable es que el planificador no influya de forma medible en esta máquina, y el paso 3 —medir si el cuello es CPU o E/S— probablemente lo demostrará antes de empezar. Eso no es un fracaso: es refutar una hipótesis por 30 minutos de trabajo, y evita añadir una regla de udev que no hace nada.
Y si el paso 3 muestra que el cuello es el cifrado, la línea de investigación correcta es otra: comprobar si la CPU expone AES-NI al huésped (grep -m1 aes /proc/cpuinfo) y, si no, activar host-passthrough en la definición de la VM. Eso puede multiplicar por cinco o diez la velocidad del cifrado, y es un cambio de virtualización, no de kernel. Justo el terreno de la lección siguiente.
Conclusión
Sabes ajustar el kernel, y —más importante— sabes cuándo no ajustarlo. Tienes el mapa de los parámetros que de verdad importan: la memoria virtual con swappiness y los umbrales de páginas sucias, la pila de red con sus dos colas de conexión y el control de congestión, los límites de ficheros con la trampa de limits.conf que no se aplica a los servicios de systemd, el planificador de E/S con la regla de none para NVMe y su porqué, y las páginas enormes transparentes que toda base de datos pide en madvise. Has visto los módulos, modprobe.d con install ... /bin/true —cerrando de paso una fila pendiente de la checklist de 06-06—, DKMS, y una respuesta honesta a compilar el kernel: casi nunca en producción, porque saca la máquina del sistema de paquetes y con él de las actualizaciones de seguridad.
Pero lo que te llevas de esta lección no es una lista de parámetros. Es el procedimiento: medir, formular una hipótesis, cambiar una cosa con sysctl -w, medir otra vez, y documentar con la medición y la fecha o revertir. Aplicándolo, de tres hipótesis razonables sobre la red de srv-tramontana dos quedaron refutadas —somaxconn no interviene porque la aplicación pide backlog=128, y swappiness no hace nada sin actividad de swap— y solo una, BBR, resultó tener efecto medible: de 77 a 185 Mbit/s. Ese ratio es lo normal, y la sección «NO TOCAMOS, y consta por qué» de tu 70-rendimiento.conf es lo que impedirá que el folclore vuelva a entrar dentro de seis meses.
Fíjate en dónde te ha dejado el último ejercicio. Para probar el planificador de E/S sin degradar el servicio hacía falta una máquina donde vaciar la caché, quitar el ionice y parar la aplicación sin que nadie se enterase. Para medir si el cifrado de LUKS va por software hacía falta cambiar la configuración de CPU de la VM. Y el despliegue 3.3.0 que no arrancó y provocó un rollback en el Módulo 4 nunca se llegó a diagnosticar, porque no había dónde reproducirlo. Todo apunta a lo mismo: falta un entorno de pruebas, y el curso lleva cuatro módulos echándolo en falta.
En la lección 07-04: Virtualización con Linux lo construyes. Entenderás qué es virtualizar de verdad —los tres modelos, el papel exacto de KVM como módulo que convierte Linux en hipervisor, y su relación con QEMU y libvirt—, manejarás máquinas con virsh y virt-install, aprovisionarás sin intervención con cloud-init, elegirás entre raw y qcow2 sabiendo por qué, harás snapshots recordando que un snapshot no es una copia de seguridad, y conectarás la red por NAT o por puente según convenga —incluida la explicación de aquel virbr0 sin enlace que costaba 35 segundos de arranque en 07-01—. Al final tendrás srv-tramontana-pruebas, clonado del servidor real: el sitio donde probar los cambios de kernel de esta lección, reproducir el despliegue que falló, y ensayar la recuperación de arranque de 07-01 sin arriesgar nada. Y de paso verás la pila de virtualización desde dentro, que es lo que hace que la lección siguiente —contenedores— se entienda por contraste y no por analogía.
Curso de Linux: De Principiante a Administrador de Sistemas
Módulo 1: Introducción a Linux
- ¿Qué es Linux?
- Historia de Linux
- Distribuciones de Linux
- Instalando Linux
- Primer Contacto con el Sistema
- Estructura del Sistema de Archivos de Linux
Módulo 2: Comandos Básicos de Linux
- Introducción a la Línea de Comandos
- Obtener Ayuda y Documentación del Sistema
- Navegando el Sistema de Archivos
- Operaciones con Archivos y Directorios
- Visualización y Edición de Archivos
- Enlaces Duros y Simbólicos
- Permisos y Propiedad de Archivos
Módulo 3: Habilidades Avanzadas en la Línea de Comandos
- El Entorno del Shell: Variables, Alias e Historial
- Uso de Comodines y Expresiones Regulares
- Búsqueda de Archivos y Contenido: find, locate y grep
- Tuberías y Redirección
- Procesamiento de Texto: cut, sort, uniq, sed y awk
- Gestión de Procesos
- Programación de Tareas con Cron
- Comandos de Redes
Módulo 4: Scripting en Shell
- Introducción al Scripting en Shell
- Variables y Tipos de Datos
- Entrada, Salida y Argumentos de un Script
- Estructuras de Control
- Funciones y Librerías
- Depuración y Manejo de Errores
- Scripts de Producción: Buenas Prácticas
Módulo 5: Administración del Sistema
- Gestión de Usuarios y Grupos
- sudo y Permisos Especiales
- Gestión de Paquetes
- Gestión de Discos
- systemd y la Gestión de Servicios
- Registros del Sistema: journald y syslog
- Monitoreo del Sistema y Optimización del Rendimiento
- Respaldo y Restauración
Módulo 6: Redes y Seguridad
- Configuración de Redes
- SSH y Acceso Remoto
- Firewall y Seguridad Perimetral
- Sistemas de Detección de Intrusos
- Gestión de Secretos y Certificados TLS
- Asegurando Sistemas Linux
Módulo 7: Temas Avanzados
- El Proceso de Arranque y la Recuperación del Sistema
- Diagnóstico Avanzado: strace, perf y eBPF
- Optimización del Kernel de Linux
- Virtualización con Linux
- Contenedores de Linux y Docker
- Automatización con Ansible
- Alta Disponibilidad y Balanceo de Carga
