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—:

  1. Medir el estado actual y anotarlo. Sin línea base no hay «después».
  2. Formular una hipótesis concreta: qué parámetro, por qué crees que importa, y qué esperas que cambie.
  3. Cambiar una sola cosa. Dos cambios simultáneos hacen imposible atribuir el resultado.
  4. Medir de nuevo con el mismo método.
  5. 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

  1. sysctl: la interfaz de parámetros del kernel
  2. Memoria virtual
  3. Red y pila TCP
  4. Límites de ficheros y procesos
  5. Planificador de E/S
  6. Páginas enormes transparentes
  7. Límites por proceso y quién gana
  8. Gobernadores de frecuencia y tuned
  9. Módulos del kernel
  10. Compilar el kernel: cuándo tiene sentido y cuándo no
  11. 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
10

sysctl 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\.'
684

Casi 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 = 10

Los 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.

$ sysctl vm.swappiness
vm.swappiness = 60
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 0

Cero 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 pausa

Los valores más pequeños producen volcados más frecuentes y pequeños, con latencia más uniforme:

$ sudo sysctl -w vm.dirty_background_ratio=5
$ sudo sysctl -w vm.dirty_ratio=10

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.0

Ese 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	4194304

Los 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                  receiver

De 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 = bbr

Fí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 = 65536

fs.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=524288

Ese 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 bfq

Como /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 bfq

Y otros dos parámetros de la cola que a veces importan:

$ cat /sys/block/sda/queue/read_ahead_kb /sys/block/sda/queue/nr_requests
128
64

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.

$ echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled

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] never

El 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.

$ ulimit -n          # limite blando de la sesion actual
1024
$ ulimit -Hn         # limite duro
1048576
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               processes

Con 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
214

214 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 all

Dos 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: installed

Con 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.cfg

make 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.txt

Paso 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 -20

Paso 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 = 10

La 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 sysctl de 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_recycle es 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 swappiness reduce 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 somaxconn sin mirar el backlog de la aplicación. El límite efectivo es el mínimo de los dos. ss -ltn muestra el real en Send-Q.
  • Confiar en limits.conf para un servicio de systemd. No se aplica: systemd no pasa por PAM. Se usa LimitNOFILE= en la unidad, y se verifica en /proc/<pid>/limits.
  • Dejar THP en always con una base de datos. Provoca pausas síncronas de compactación impredecibles. madvise es el compromiso correcto, y hay que aplicarlo antes de que la base de datos arranque.
  • Poner mq-deadline en un NVMe. Reordenar peticiones no tiene sentido sin cabezal: solo añade latencia. none para NVMe.
  • Olvidar update-initramfs tras 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-virtual o 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 = 134217728

Evalú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

somaxconn es un techo. La cola real es el mínimo entre este valor y el backlog que la aplicación pide en listen().

$ 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. Y ListenOverflows = 0 significa que nunca se ha descartado una conexión por cola llena. Para que este parámetro sirviera habría que cambiar el backlog en 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 directory

Se 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 es tcp_tw_reuse, y ya está en 2, 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.0

Es 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_syncookies activo 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  9223372036854775807

En 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.conf no se aplica a los servicios, que es un detalle que confunde a mucha gente.

5. vm.swappiness = 0 — Contraproducente

Este es el que me preocupa. swappiness = 0 no 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,4Gi

Hoy da igual porque no hay actividad de swap, pero el día que la haya, 0 es peor que el valor por defecto. He puesto 10, 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 memoria

Los 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 ms

Con 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 en tcp_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  receiver

De 77 a 185 Mbit/s hacia clientes remotos. Ese sí es un ajuste con efecto medido, y está en /etc/sysctl.d/70-rendimiento.conf con 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 -w y 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
214

Uso 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/null

80 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;"
1000

El 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: 412

Hoy 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 = 9223372036854775807

2.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:524288

Aquí 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'
0

Cero 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.tsv

Y 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:

  1. El planificador que importa es el del dispositivo físico, no el de la pila. /srv/tramontana/backups es dm-1 (LUKS) sobre lv-backups (LVM) sobre sda. Los dispositivos dm-* no tienen planificador propio: pasan las peticiones hacia abajo. Se comprueba:

    $ cat /sys/block/dm-1/queue/scheduler
    none
    

    Un none fijo, sin alternativas entre corchetes, indica que ese dispositivo no planifica. Así que el experimento solo tiene sentido sobre sda.

  2. 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-guest es el perfil de tuned activo.

  3. El cuello podría ser el cifrado, no la E/S. Si perf stat da CPUs utilized cerca de 1,00 y offcputime señ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.

  4. 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.

  5. restic deduplica 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

Módulo 2: Comandos Básicos de Linux

Módulo 3: Habilidades Avanzadas en la Línea de Comandos

Módulo 4: Scripting en Shell

Módulo 5: Administración del Sistema

Módulo 6: Redes y Seguridad

Módulo 7: Temas Avanzados

Módulo 8: Proyectos Prácticos

© Copyright 2026. Todos los derechos reservados