Conservative SmartOS global-zone memory pressure guard with dry-run-first policy and SMF integration.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

33 KiB

Checkpoint del proyecto o6h-memory-guard

Fecha de corte: 18 de septiembre de 2026
Nodo de validación principal: o6hsmartos10
Versión validada en ejecución: v1.0.2
Modo operativo: dry-run
Estado de activación: armed no activado y no autorizado todavía

1. Propósito y conclusión ejecutiva

o6h-memory-guard es una protección específica para la global zone de SmartOS. Su objetivo es detectar con antelación una trayectoria de agotamiento de páginas físicas que pueda terminar en un panic pageout_deadman, identificar de forma conservadora qué zona y qué proceso están contribuyendo a la presión y, sólo después de una validación suficiente, permitir una acción correctiva limitada.

El problema que motivó el proyecto no es una alarma genérica de porcentaje de RAM. En dos hosts SmartOS de unos 32 GiB se observaron panics donde pageout quedó 90 segundos intentando evacuar la misma página con freemem prácticamente a cero. En ambos dumps, el camino de escritura de swap atravesaba un zvol de ZFS y necesitaba asignar memoria adicional dentro de DMU, ARC y ABD. Esa dependencia puede impedir que pageout progrese precisamente cuando el host ya no dispone de páginas.

La conclusión operativa hasta este corte es:

  • La telemetría base y continua de v1.0.2 está validada en SmartOS real.
  • El daemon compila, funciona bajo SMF, clasifica correctamente el estado normal y mantiene los deltas de 5, 15 y 60 segundos.
  • La política de elegibilidad por UUID funciona y las zonas no autorizadas quedan fuera.
  • La atribución por zona ya produce datos útiles.
  • La selección final de un PID por tamaño y crecimiento, y el evento WOULD_ACTION bajo presión real, todavía no están validados de extremo a extremo.
  • No se debe activar armed todavía.

2. Problema raíz observado en SmartOS

2.1 Qué significa el panic

El mensaje común fue:

pageout_deadman: stuck pushing the same page for 90 seconds

El panic indica que el hilo pageout no consiguió avanzar durante 90 segundos al intentar expulsar una página. No identifica por sí solo al proceso que creó la presión ni demuestra por sí solo que el disco estuviera bloqueado.

2.2 Cadena técnica demostrada en los dumps

En los dos casos analizados apareció esencialmente esta ruta:

pageout
  -> swap_putpage
  -> swap_putapage
  -> zvol_strategy
  -> dmu_write
  -> arc_alloc_buf / arc_hdr_alloc
  -> arc_get_data_abd
  -> abd_alloc
  -> kmem_cache_alloc / vmem_alloc
  -> segkmem_zio_alloc
  -> page_create_va

Interpretación:

  1. El host se queda casi sin páginas físicas libres.
  2. pageout intenta liberar RAM escribiendo memoria anónima a swap.
  3. El swap reside en un zvol.
  4. La escritura atraviesa ZFS, DMU, ARC y ABD.
  5. Ese camino necesita páginas físicas o memoria de kernel para completar la operación.
  6. Ya no existen páginas suficientes.
  7. pageout no progresa y finalmente dispara pageout_deadman.

También se observó un camino de swapout que alcanzaba zvol_strategy, dmu_tx_assign, dmu_tx_wait y txg_wait_synced. No se encontró un stack real detenido en zio_wait; por ello, la evidencia encaja mejor con inanición de páginas y dependencia de memoria que con una simple escritura de disco bloqueada durante 90 segundos.

3. Evidencia de los dos panics

Evidencia o6hsmartos20 o6hsmartos10
RAM física ~32 GiB ~32 GiB
Panic pageout_deadman pageout_deadman
freemem en el panic 22 páginas 57 páginas
Equivalente con páginas de 4 KiB ~88 KiB ~228 KiB
Anon en ::memstat ~23,6 GiB, 72 % 21.486 MiB, 66 %
Kernel en ::memstat ~6,8 GiB, 21 % 8.899 MiB, 27 %
ZFS File Data ~1,8 GiB 1.896 MiB, 6 %
Ruta pageout -> swap -> zvol
Ruta DMU/ARC/ABD que necesita páginas
zio_wait evidente no no

En o6hsmartos10, ::memstat mostró:

Page Summary                Pages                MB  %Tot
Kernel                    2278204              8899   27%
Boot pages                  87855               343    1%
ZFS File Data              485621              1896    6%
Anon                      5500542             21486   66%
Exec and libs                9849                38    0%
Page cache                  13864                54    0%
Free (cachelist)               66                 0    0%
Free (freelist)                 2                 0    0%
Total                     8376003             32718
Physical                  8376002             32718

La diferencia entre las 57 páginas del mensaje del panic y las 68 páginas libres contabilizadas después por ::memstat no cambia la conclusión: el sistema estaba prácticamente a cero.

4. Causa de la presión y mecanismo del panic no son lo mismo

Es una decisión fundamental del proyecto mantener separadas estas dos preguntas.

4.1 Causa de la presión

Es aquello que consume o compromete la memoria hasta atravesar lotsfree, desfree y minfree. En los dumps, la mayor categoría era memoria Anon; los consumidores probables incluían Apache, PHP-FPM, PHP-CGI, MySQL y otros procesos de zonas. También había una cantidad importante de memoria de kernel, que debe estudiarse por separado y no atribuirse automáticamente a una zona.

4.2 Mecanismo que convierte el agotamiento en panic

Es la dependencia circular del camino de pageout con swap sobre ZFS:

presión extrema de RAM
  -> pageout intenta liberar memoria
  -> escritura a swap en zvol
  -> ZFS/DMU/ARC/ABD necesita memoria
  -> no quedan páginas
  -> pageout no avanza
  -> pageout_deadman

Apache prefork puede explicar parte de por qué se llegó al agotamiento. No explica por sí solo por qué el agotamiento terminó en pageout_deadman. Del mismo modo, mejorar el zvol de swap reduce el riesgo del mecanismo final, pero no elimina la necesidad de controlar a los consumidores de memoria.

5. Hallazgos de Apache y migración de prefork a event

En el momento de los panics, los dos servidores relevantes usaban mpm_prefork. En un dump se contaron 301 procesos apache2:

  • 152 en la zona que entonces tenía zone_id=7.
  • 124 en la zona que entonces tenía zone_id=9.
  • El resto distribuido entre otras zonas.

Los zone_id eran válidos sólo en el instante del crash y cambiaron después del reinicio. El análisis resolvió la antigua zona 9 al UUID estable bc806d9e-fca0-e333-aee6-93883689efe5.

Un Apache de ejemplo, PID 47998 en el dump, tenía:

a_size      ~= 514,9 MiB
a_resvsize  ~= 523,8 MiB
segments    = 664

Estos valores describen el espacio de direcciones y la reserva contable; no son RSS y no deben multiplicarse por el número de workers como si fueran memoria física. Aun así, una población de cientos de procesos prefork es compatible con muchos GiB agregados de memoria anónima privada.

Después se migraron ambos servidores a mpm_event. En web5 quedó demostrado:

Server MPM: event
mpm_event_module (shared)

Configuración activa:

ThreadLimit         64
ThreadsPerChild     25
MaxRequestWorkers  150

Esto permite aproximadamente seis procesos worker para 150 workers lógicos, en lugar de hasta 150 procesos independientes con prefork.

Medición posterior en web5:

Procesos Apache: 5
RSS total:       139,4 MiB
RSS medio:        27,9 MiB

El conteo fiable de threads desde /proc/<pid>/task mostró tres hijos con 27 threads y procesos padre o transitorios con un thread. ps -o nlwp dentro de la LX zone mostraba incorrectamente 1, por lo que no debe usarse allí como prueba definitiva.

Conclusión: la migración prefork -> event reduce de forma convincente una fuente importante de presión de memoria, pero no demuestra por sí sola qué fracción exacta de Anon pertenecía a Apache. Al evaluar el consumo web actual hay que sumar Apache y PHP-FPM, porque con event PHP reside en los pools FPM.

6. Diseño de o6h-memory-guard

6.1 Arquitectura

  • Daemon nativo en C.
  • Lecturas con libkstat y /proc.
  • Servicio persistente SMF en la global zone.
  • Instalación bajo /opt/custom.
  • Muestreo del estado de memoria cada segundo.
  • Escaneo periódico de procesos y mantenimiento anticipado del candidato.
  • Sin fork() ni comandos externos en la ruta crítica.

Rutas instaladas observadas:

/opt/custom/bin/o6h-memory-guard
/opt/custom/etc/o6h-memory-guard.conf
/opt/custom/smf/o6h-memory-guard.xml
/opt/custom/var/log/o6h-memory-guard/guard.log

La razón para evitar shell y procesos externos durante la emergencia es que, cerca de minfree, incluso ps, awk, sort, prstat, vmadm o mdb podrían no poder hacer fork() o reservar memoria.

6.2 Modos de seguridad

  • dry-run: modo actual y predeterminado. Puede observar, puntuar candidatos y registrar lo que haría, pero no envía señales.
  • --force-dry-run: salvaguarda de línea de comandos usada en --probe.
  • armed: implementado, pero no activado ni aceptado para producción.
  • La política exige dos compuertas de armado; una zona allow sólo podría ser señalizada si ambas están abiertas.
  • protect, observe, una zona ausente de la configuración y unmapped nunca son elegibles, ni siquiera al alcanzar el floor.

6.3 Máquina de estados

Estados implementados o definidos:

NORMAL
PRE-PRESSURE
PRESSURE
CRITICAL
EMERGENCY
RECOVERY

La lógica se apoya en los umbrales que ofrece el propio kernel:

  • NORMAL: memoria por encima de lotsfree y sin señales de presión.
  • PRE-PRESSURE y PRESSURE: cruce persistente de lotsfree; se intensifica diagnóstico y se prepara candidato, sin concluir por una sola muestra.
  • CRITICAL: entorno de desfree, persistente y corroborado por tendencia, scanner o pageout.
  • EMERGENCY: entorno de minfree, con acción mucho más urgente si el modo estuviera armado.
  • RECOVERY: histéresis y cooldown; no se considera recuperación por un rebote momentáneo.

El floor absoluto es minfree / 2. Es un último umbral de protección, no un permiso para ignorar la política: una zona no autorizada sigue sin ser elegible.

6.4 Histéresis y ventanas

El guard evita reaccionar a una sola caída rápida de freemem. Mantiene deltas de 5, 15 y 60 segundos para distinguir:

  • un pico breve ya terminado;
  • una tendencia sostenida;
  • un evento aún visible en la ventana larga pero inactivo en la corta.

La recuperación requiere estabilidad por encima del umbral, no un único sample. Al reiniciar o releer la configuración, el histórico se reinicia y aparece temporalmente como NA; esto es esperado.

6.5 Selección de zona y PID

El identificador persistente es el UUID de la zona, nunca zone_id. Los números de zona cambian tras un reinicio.

La selección no debe ser:

zona con mayor RSS -> matar su proceso mayor

ni:

PID con mayor RSS global -> matar

Debe combinar:

  1. Presión real del host.
  2. Zona explícitamente allow.
  3. RSS y crecimiento de la zona.
  4. RSS actual y crecimiento d5/d15/d60 del PID.
  5. Capacidad probable de liberar memoria rápidamente.
  6. Exclusión de procesos y zonas protegidos.

Procesos o clases que no deben tocarse por defecto incluyen la global zone, PID 0/1, pageout, fsflush, zsched, zoneadmd, svc.startd, svc.configd, procesos zpool-*, vmadmd y VMs como qemu-system-x86 o bhyve salvo autorización explícita extraordinaria.

7. Métricas validadas y su semántica

Métrica Fuente/uso Interpretación correcta
freemem / free_pages unix:0:system_pages páginas físicas libres; señal principal de margen inmediato
availrmem system_pages memoria disponible para ciertos compromisos; no equivale a RAM libre
lotsfree system_pages frontera de reclamación activa y comienzo del diagnóstico intensivo
desfree system_pages presión seria
minfree system_pages situación extrema
pp_kernel system_pages páginas atribuidas al kernel; útil como serie y delta, no como trigger directo de kill
pageslocked system_pages páginas bloqueadas; se registra y sigue su delta
nscan system_pages actividad del page scanner; corrobora presión real
ARC kstat ARC tamaño/variación del ARC; no fue el consumidor dominante en los dumps
swap_committed_pages equivalente a swap -s usado compromiso virtual: allocated + reserved; no significa bytes escritos al zvol
swap_physical_used_bytes equivalente a swap -l ocupación física: (blocks - free) * 512
anon_pageout_pages suma de cpu:*:vm:anonpgout páginas anónimas realmente expulsadas; no duplicar con cpu_stat:*

El swap físico y anonpgout corroboran presión sólo junto con el estado de memoria; no disparan acciones por sí solos.

8. Valores reales de o6hsmartos10

Página del sistema: 4096 bytes.

Valores estables observados de system_pages:

Métrica Páginas Aproximación
physmem 8.376.003 32.718,8 MiB
lotsfree 130.875 511,2 MiB
desfree 65.437 255,6 MiB
minfree 49.077 191,7 MiB
floor minfree/2 ~24.538 ~95,8 MiB

Ejemplos de situación normal:

freemem=255449 pages ~= 997,8 MiB
availrmem=5099973 pages ~= 19.921,8 MiB
pp_kernel=3251495 pages ~= 12.701,2 MiB

Otro sample del probe v1.0.1:

free=274591 pages ~= 1.072,6 MiB
nscan=0
pp_kernel=3108718 pages ~= 11,86 GiB
pageslocked=3133407 pages ~= 11,95 GiB
arc_bytes=7001795280 ~= 6,52 GiB
swap_committed_pages=7621435 ~= 29,07 GiB
swap_physical_used_bytes=4061863936 ~= 3,78 GiB
anon_pageout_pages=6553284

Interpretaciones que deben conservarse:

  • availrmem de unos 20 GiB no significa que haya 20 GiB físicamente libres.
  • pp_kernel puede ser mayor que la categoría Kernel de un ::memstat; no se deben tratar como cifras equivalentes ni usar pp_kernel > X como trigger sin más análisis.
  • Un crecimiento rápido de swap_committed_pages puede ocurrir sin crecimiento de swap físico, sin anonpgout y sin nscan; ese caso no es presión de pageout.

8.1 Validaciones cruzadas exactas

En una lectura casi simultánea:

swap_committed_pages = 7.632.011
7.632.011 * 4 KiB = 30.528.044 KiB
swap -s: 30.525.928 KiB used
diferencia ~= 2,07 MiB

Swap físico:

swap -l blocks = 67.024.888
swap -l free   = 59.100.824
used_blocks    = 7.924.064
physical_used  = 7.924.064 * 512 = 4.057.120.768 bytes

El probe cercano dio 4.061.863.936 bytes; la diferencia de unos 4,5 MiB era compatible con actividad entre lecturas.

anonpgout:

cpu:0..7:vm:anonpgout sumados = 6.553.284
guard anon_pageout_pages       = 6.553.284

La coincidencia fue exacta. Las entradas cpu_stat:*:anonpgout duplican los mismos contadores y no deben sumarse otra vez.

9. Evolución de versiones

9.1 v1.0.0

Primera entrega con daemon C, configuración, Makefile, SMF, instalador, pruebas, checksums, dry-run y modo armed implementado. Se validó inicialmente en Linux, pero no estaba aún certificada en SmartOS.

Primer defecto descubierto al instalar en SmartOS:

<stability value="Uncommitted"/>

no era válido para el DTD de SMF usado. La forma correcta fue:

<stability value="Unstable"/>

9.2 v1.0.1

Cambios principales:

  • Corrección SMF Uncommitted -> Unstable.
  • Sustitución semántica de swap_used_pages por swap_committed_pages.
  • Incorporación de swap_physical_used_bytes.
  • Incorporación de anon_pageout_pages.
  • Deltas 5/15/60 para compromiso virtual, swap físico y pageout anónimo.

La lectura puntual mediante --probe quedó validada en SmartOS. Sin embargo, el daemon continuo seguía registrando la nomenclatura antigua swap_used_pages y no exponía de forma continua el swap físico ni anon_pageout. Esto fue un FAIL del contrato de logging, no de la lectura puntual.

9.3 v1.0.2

Corrigió el runtime continuo:

  • event=sample incluye swap_committed_pages, swap_physical_used_bytes, anon_pageout_pages y sus flags *_ok.
  • event=delta incluye d5/d15/d60 para las tres.
  • swap_used_pages desaparece del runtime nuevo.
  • swap_alloc_pages, swap_reserved_pages y swap_available_pages permanecen sólo como auxiliares de semántica explícita.
  • La corroboración por swap físico/pageout sólo se usa bajo presión de memoria.

En el paquete se ejecutaron 113 pruebas de política y 50 de runtime, ASan, UBSan, -fanalyzer, C99 estricto y validaciones shell/XML. Después se compiló y probó nativamente en SmartOS.

El README de v1.0.2 no traía una sección de upgrade. Se realizó una actualización controlada sustituyendo sólo el binario, preservando configuración, manifiesto y logs, con copia de rollback de v1.0.1.

10. Validación real de v1.0.2 en SmartOS

10.1 Resultado aceptado

Componente Estado
Compilación nativa SmartOS PASS
--probe --force-dry-run PASS
system_pages PASS
ARC PASS
swap comprometido PASS
swap físico PASS
anonpgout PASS
SMF online PASS
muestreo 1 s PASS
logging continuo v1.0.2 PASS
delta 5 s PASS
delta 15 s PASS
delta 60 s PASS
clasificación NORMAL PASS
proc_scan PASS
dry-run PASS
política UUID/eligible inicial PASS
selección final zona/PID bajo presión PENDIENTE
WOULD_ACTION real y razonable PENDIENTE
armed NO GO

10.2 Comportamiento conservador demostrado

En un intervalo real:

free_pages d5               = -110448 pages ~= -431 MiB
swap_committed_pages d5     = +113850 pages ~= +445 MiB
swap_physical_used_bytes d5 = -204800 bytes
anon_pageout_pages d5       = 0
nscan_pages d5              = 0

Aunque el compromiso virtual creció con rapidez, no aumentaron el swap físico, anonpgout ni nscan, y freemem seguía por encima de lotsfree. El guard mantuvo correctamente:

state=NORMAL action_pressure=0 floor=0

10.3 Ventanas de tiempo demostradas

A las 17:19:06 se observó:

swap_committed_pages:     d5=+45584  d15=-187569  d60=-155145 pages
swap_physical_used_bytes: d5=-40960  d15=-30179328 d60=+75329536 bytes
anon_pageout_pages:       d5=0       d15=0         d60=+27274 pages

La ventana de 60 s conservaba un episodio de unos 106,5 MiB de pageout anónimo y +75,3 MiB de swap físico, pero las ventanas de 5 y 15 s mostraban que ya no seguía activo. Es exactamente el contexto temporal que se quería obtener.

11. Política de zonas

Gramática confirmada:

zone <lowercase-UUID> <alias-sin-espacios> <allow|protect|observe>

Semántica:

  • allow: elegible para simulación; en armed, sólo con las dos compuertas abiertas.
  • protect: visible y nunca señalizable.
  • observe: visible y nunca señalizable.
  • ausente o unmapped: observada pero nunca elegible.
  • global: nunca elegible.

11.1 Inventario conocido a este corte

UUID Alias Estado que puede afirmarse
c82f1d64-fc20-4a80-9b72-b0940af83249 web5.open6hosting.com / alias configurado web5 allow confirmado explícitamente
fac1f403-e42c-ee4f-c0f6-cbaf7a6d7def webscorp.unita.es aparentemente autorizada/visible con alias; no consta aquí la línea exacta de política
344df936-1577-e8d7-b6d0-d9159fb15c33 servidor.correccionencatala.cat aparentemente autorizada/visible con alias; no consta aquí la línea exacta de política
54e0531f-da35-c877-9fc8-92454565c9c5 03.base0.es aparentemente autorizada/visible con alias; no consta aquí la línea exacta de política
81790093-33cb-45b5-d91e-8d38e9b4902a servidor.consultaerte.com aparentemente autorizada/visible con alias; no consta aquí la línea exacta de política
be9a7015-33ae-461b-d4e7-dab81ffc3c26 sailandfun.bz2 añadida y visible con alias; aparentemente autorizada, pero la línea exacta allow no está reproducida
bc806d9e-fca0-e333-aee6-93883689efe5 sin alias en la política actual deliberadamente no elegible, unmapped
d0a34514-aa7f-c6ad-fee5-b6c7489787a6 sin alias en la política actual deliberadamente no elegible, unmapped
global unmapped deliberadamente no elegible y siempre protegida

Nota histórica: antes de añadir sailandfun.bz2, las tres entradas deliberadamente fuera eran bc806d9e-..., d0a34514-... y be9a7015-..., además de global. Tras añadir be9a7015-... con alias sailandfun.bz2, las que siguen inequívocamente fuera son bc806d9e-..., d0a34514-... y global.

Que un UUID aparezca con alias demuestra que el daemon ha leído inventario/política para él, pero no demuestra por sí solo si la regla exacta es allow, protect u observe. Antes de armed debe revisarse el fichero de configuración literal.

12. Comportamiento observado por zona y proceso

12.1 web5 es grande y volátil, pero no siempre el mejor candidato

web5.open6hosting.com se ha observado aproximadamente entre 7 y 9 GiB de suma de RSS. En distintas ventanas pasó por:

~8,58 GiB con d60 ~= +1,23 GiB
~7,49 GiB con d60 ~= -734 MiB
~7,73 GiB con d60 ~= +241 MiB

Por tanto mueve mucha memoria y es volátil, pero puede estar liberándola. Su tamaño total no demuestra que sea el culpable de cada episodio.

Además, distribuye el consumo entre muchos procesos. Los primeros top_process autorizados mostraron PHP-FPM individuales de aproximadamente 185 y 212 MiB. Un proceso único de otra zona con 800 MiB o 1 GiB puede ser un mejor objetivo de rescate aunque su zona sea menor.

12.2 Otras zonas

  • webscorp.unita.es: aproximadamente 2,0-2,5 GiB; ventanas observadas desde crecimiento modesto hasta descenso.
  • servidor.consultaerte.com: aproximadamente 1,8-2,0 GiB; en general estable, con algún intervalo cercano a +85 MiB/min.
  • sailandfun.bz2: aproximadamente 2,9-3,6 GiB en las primeras muestras; candidata importante para comparar procesos individuales pesados.
  • Las zonas unmapped se siguen midiendo, pero eligible=0 evita que entren en cualquier acción.

13. Ejemplos de logs reales

Los backslashes que aparecían al copiar algunos logs eran escapes del chat; en el fichero real los nombres son free_pages, swap_committed_pages, etc.

13.1 event=sample

2026-09-18T17:16:49Z mono=296136.349 event=sample state=NORMAL action_pressure=0 floor=0 free_pages=348087 lotsfree=130875 desfree=65437 minfree=49077 nscan=0 pp_kernel=2993989 pageslocked=3018668 arc_ok=1 arc_bytes=6152011944 swap_committed_ok=1 swap_committed_pages=7752388 swap_physical_used_ok=1 swap_physical_used_bytes=4136677376 anon_pageout_ok=1 anon_pageout_pages=6576887 swap_alloc_pages=5889225 swap_reserved_pages=1863163 swap_available_pages=5851985

13.2 event=delta

2026-09-18T17:19:06Z mono=296273.346 event=delta metric=swap_committed_pages d5=+45584@5.00s d15=-187569@15.00s d60=-155145@60.00s
2026-09-18T17:19:06Z mono=296273.346 event=delta metric=swap_physical_used_bytes d5=-40960@5.00s d15=-30179328@15.00s d60=+75329536@60.00s
2026-09-18T17:19:06Z mono=296273.346 event=delta metric=anon_pageout_pages d5=+0@5.00s d15=+0@15.00s d60=+27274@60.00s

13.3 event=zone

2026-09-18T17:48:43Z mono=298050.579 event=zone UUID=c82f1d64-fc20-4a80-9b72-b0940af83249 alias=web5.open6hosting.com generation=6 rss_sum_kib=8377060 complete=1 d5_kib=+52884@6.00s d15_kib=NA d60_kib=+649088@61.00s
2026-09-18T17:53:33Z mono=298340.974 event=zone UUID=be9a7015-33ae-461b-d4e7-dab81ffc3c26 alias=sailandfun.bz2 generation=1 rss_sum_kib=3380020 complete=1 d5_kib=-11692@6.00s d15_kib=NA d60_kib=+548@61.00s

13.4 event=top_process

En el primer escaneo después de autorizar web5 se observaron, entre otros, PHP-FPM de aproximadamente 212 MiB y 185 MiB con:

event=top_process UUID=c82f1d64-fc20-4a80-9b72-b0940af83249 alias=web5.open6hosting.com zone_id=6 pid=57890 comm=php-fpm rss_kib~=217000 eligible=1 d5_kib=NA d15_kib=NA d60_kib=NA
event=top_process UUID=c82f1d64-fc20-4a80-9b72-b0940af83249 alias=web5.open6hosting.com zone_id=6 pid=60832 comm=php-fpm rss_kib~=189000 eligible=1 d5_kib=NA d15_kib=NA d60_kib=NA

Estas dos líneas son una normalización de los campos y valores descritos en el hilo, no una copia byte a byte del fichero original. En la misma salida, procesos de be9a7015-... y bc806d9e-... aparecían como alias=unmapped eligible=0.

13.5 proc_scan

2026-09-18T17:16:50Z mono=296137.359 event=proc_scan complete=1 duration_ms=12.7 capacity=4096

También se observó otro scan completo en unos 14,0 ms. El coste en estado normal parece aceptable.

14. Estado de aceptación y pendientes

PASS

  • Diagnóstico reproducido en dos dumps independientes.
  • Distinción conceptual entre consumidor y mecanismo final del panic.
  • Compilación nativa de v1.0.2.
  • Probe y salvaguarda --force-dry-run.
  • Lectura de system_pages, ARC, swap comprometido, swap físico y anonpgout.
  • Correlación exacta o prácticamente exacta con kstat, swap -s y swap -l.
  • Servicio SMF y logging continuo.
  • Clasificación NORMAL coherente.
  • Deltas 5/15/60 del host.
  • proc_scan completo.
  • Resolución UUID/alias y elegibilidad básica.
  • Deltas por zona.
  • Exclusión de zonas deliberadamente no autorizadas.

PENDIENTE

  • Confirmar d5/d15/d60 por PID de forma sostenida en event=top_process.
  • Comparar el ranking de procesos entre todas las zonas autorizadas.
  • Confirmar que tamaño y crecimiento producen un candidato razonable, no sólo el mayor RSS.
  • Observar una presión real con WOULD_ACTION, todavía en dry-run.
  • Validar cooldown, reelección y límites de víctimas en un episodio representativo.
  • Revisar literalmente cada regla allow/protect/observe antes de armado.
  • Mantener seguimiento separado de pp_kernel/pageslocked; si crecen mientras las zonas se mantienen estables, no acusar automáticamente a una zona.
  • No se ha aceptado el envío real de SIGTERM/SIGKILL.

15. Próximos pasos recomendados

  1. Dejar el servicio en dry-run y acumular varios minutos o episodios reales.
  2. Extraer event=top_process para todas las zonas con alias y comprobar que los PID persistentes adquieren d5, d15 y d60.
  3. Correlacionar cada event=zone con los top_process del mismo intervalo.
  4. Comparar especialmente web5 y sailandfun.bz2: una zona enorme con procesos moderados frente a una zona menor que puede contener uno o dos procesos pesados.
  5. Esperar un WOULD_ACTION provocado por presión real; no provocar artificialmente un estado peligroso en producción sólo para probarlo.
  6. Revisar que el candidato:
    • pertenece a una zona allow confirmada;
    • no coincide con ningún proceso protegido;
    • está realmente creciendo o es capaz de liberar una cantidad relevante;
    • explica una parte significativa del crecimiento de su zona;
    • se selecciona sólo cuando el host presenta presión real.
  7. Repetir la observación en más de un episodio antes de considerar armed.

Evidencia mínima exigible antes de armed

  • Varias transiciones de estado observadas y explicables.
  • Al menos un WOULD_ACTION coherente en presión real.
  • Deltas por proceso estables y sin confundir reinicios/reutilización de PID.
  • Política completa revisada por UUID, con zonas críticas como protect o fuera del inventario de acción.
  • Confirmación de que global y las UUID no autorizadas siempre producen eligible=0.
  • Límites de víctimas, cooldown e inhibición inicial probados.
  • Procedimiento de rollback ensayado.
  • Aceptación explícita del operador para pasar de simulación a señales reales.

16. Comandos operativos útiles

Estado y configuración del servicio

svcs svc:/site/o6h-memory-guard:default
svcs -xv svc:/site/o6h-memory-guard:default
svcs -l svc:/site/o6h-memory-guard:default
pgrep -fl o6h-memory-guard
grep -Ei 'mode|armed|dry' /opt/custom/etc/o6h-memory-guard.conf
grep '^zone ' /opt/custom/etc/o6h-memory-guard.conf

Probe seguro

/opt/custom/bin/o6h-memory-guard --probe --force-dry-run

Logs

tail -f /opt/custom/var/log/o6h-memory-guard/guard.log

grep 'event=sample' /opt/custom/var/log/o6h-memory-guard/guard.log | tail -20
grep 'event=delta' /opt/custom/var/log/o6h-memory-guard/guard.log | tail -50
grep 'event=zone ' /opt/custom/var/log/o6h-memory-guard/guard.log | tail -50
grep 'event=top_process' /opt/custom/var/log/o6h-memory-guard/guard.log | tail -100
grep 'event=proc_scan' /opt/custom/var/log/o6h-memory-guard/guard.log | tail -20

tail -1000 /opt/custom/var/log/o6h-memory-guard/guard.log |
  egrep 'metric=(swap_committed_pages|swap_physical_used_bytes|anon_pageout_pages)' |
  tail -30

Comprobar que la nomenclatura antigua no reaparece

grep 'event=delta metric=swap_used_pages' \
  /opt/custom/var/log/o6h-memory-guard/guard.log | tail

Las coincidencias históricas de v1.0.1 son posibles; no debe haber ninguna nueva después del arranque de v1.0.2.

Memoria y kstat

pagesize
kstat -p unix:0:system_pages
kstat -p | egrep -i 'anon.*pageout|pageout.*anon|anonpgout'
vmstat 1 20

Vista compacta:

PAGESIZE=$(pagesize)
kstat -p unix:0:system_pages |
nawk -v p="$PAGESIZE" '
$1 ~ /:(freemem|availrmem|lotsfree|desfree|minfree|physmem|pp_kernel|pageslocked|nscan)$/ {
    split($1,a,":")
    printf "%-12s %12d pages  %10.1f MiB\n", a[4], $2, ($2*p)/(1024*1024)
}'

Swap virtual y físico

swap -s
swap -l

swap -l | nawk '
NR > 1 { used_blocks += ($4 - $5) }
END {
    printf "used_blocks=%d\n", used_blocks
    printf "physical_used_bytes=%.0f\n", used_blocks * 512
    printf "physical_used_MiB=%.1f\n", used_blocks * 512 / 1024 / 1024
}'

Zonas y UUID

zoneadm list -cp
vmadm get <UUID> | json alias zonename state
zoneadm list -cp | grep '<UUID>'

No usar zone_id como identidad persistente.

Apache event y threads en LX

apache2ctl -V | grep -i 'Server MPM'
apache2ctl -M | egrep 'mpm|php|proxy_fcgi'

for p in $(pgrep apache2); do
    printf "%-7s threads=%s\n" "$p" "$(ls /proc/$p/task 2>/dev/null | wc -l)"
done

ps -C apache2 -o rss= | awk '{sum+=$1; n++} END {
    printf "Procesos: %d\nRSS total: %.1f MB\nRSS medio: %.1f MB\n",
           n, sum/1024, n ? sum/n/1024 : 0
}'

17. Reglas para continuar en un hilo nuevo

  1. Empezar desde este checkpoint y no rehacer como hipótesis lo que ya está demostrado por dos dumps.
  2. Mantener siempre la distinción entre causa de presión y mecanismo del panic.
  3. Tratar freemem como métrica física crítica; no sustituirla por availrmem ni por porcentaje genérico de RAM.
  4. Usar los umbrales dinámicos lotsfree, desfree y minfree del host; no codificar porcentajes arbitrarios.
  5. Conservar el floor minfree/2, pero nunca usarlo para saltarse la política de elegibilidad.
  6. No confundir swap comprometido con swap escrito físicamente. Mantener las métricas separadas.
  7. No sumar dos veces anonpgout desde cpu:* y cpu_stat:*.
  8. Identificar zonas por UUID estable y alias. zone_id sólo sirve para el instante observado.
  9. Una zona visible con alias no se considera allow sin revisar la regla exacta.
  10. global, las zonas unmapped, observe, protect o ausentes nunca son sacrificables.
  11. No elegir por RSS absoluto solamente; combinar tamaño, crecimiento y presión real.
  12. No asumir que web5 es siempre culpable porque sea la zona mayor. Es grande y volátil, y sus procesos individuales observados son moderados.
  13. Seguir comparando procesos pesados de otras zonas, especialmente sailandfun.bz2.
  14. No interpretar a_size o a_resvsize de un dump como RSS físico.
  15. No atribuir la categoría Kernel de ::memstat a una zona sin evidencia. Seguir pp_kernel como diagnóstico, no como trigger directo.
  16. En LX, contar threads Apache mediante /proc/<pid>/task; ps -o nlwp resultó engañoso.
  17. No ejecutar comandos externos en la ruta crítica del guard.
  18. Mantener dry-run hasta observar y revisar WOULD_ACTION bajo presión real.
  19. No activar armed por el mero hecho de que compilación, probe y logging estén en PASS.
  20. Antes de cualquier señal real, exigir revisión humana explícita de configuración, candidatos, cooldown, límites y rollback.

18. Punto exacto para reanudar

La próxima sesión debería comenzar obteniendo histórico por proceso:

grep 'event=top_process' \
  /opt/custom/var/log/o6h-memory-guard/guard.log |
  tail -100

Después debe correlacionarlo con:

grep 'event=zone ' \
  /opt/custom/var/log/o6h-memory-guard/guard.log |
  tail -50

La pregunta inmediata no es si el daemon mide memoria correctamente; eso ya está aceptado. La pregunta pendiente es si el ranking por PID identifica de manera repetible al proceso que mejor explica el crecimiento y que más memoria podría liberar, respetando la política de zonas, antes de que se autorice cualquier acción real.