# 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: ```text 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: ```text 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` | sí | sí | | Ruta DMU/ARC/ABD que necesita páginas | sí | sí | | `zio_wait` evidente | no | no | En `o6hsmartos10`, `::memstat` mostró: ```text 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: ```text 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: ```text 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: ```text Server MPM: event mpm_event_module (shared) ``` Configuración activa: ```text 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`: ```text Procesos Apache: 5 RSS total: 139,4 MiB RSS medio: 27,9 MiB ``` El conteo fiable de threads desde `/proc//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: ```text /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: ```text 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: ```text zona con mayor RSS -> matar su proceso mayor ``` ni: ```text 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: ```text 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`: ```text 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: ```text 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: ```text 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`: ```text 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: ```text ``` no era válido para el DTD de SMF usado. La forma correcta fue: ```text ``` ### 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: ```text 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: ```text state=NORMAL action_pressure=0 floor=0 ``` ### 10.3 Ventanas de tiempo demostradas A las 17:19:06 se observó: ```text 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: ```text zone ``` 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: ```text ~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 ```text 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 ```text 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 ```text 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: ```text 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 ```text 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 ```bash 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 ```bash /opt/custom/bin/o6h-memory-guard --probe --force-dry-run ``` ### Logs ```bash 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 ```bash 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 ```bash pagesize kstat -p unix:0:system_pages kstat -p | egrep -i 'anon.*pageout|pageout.*anon|anonpgout' vmstat 1 20 ``` Vista compacta: ```bash 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 ```bash 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 ```bash zoneadm list -cp vmadm get | json alias zonename state zoneadm list -cp | grep '' ``` No usar `zone_id` como identidad persistente. ### Apache event y threads en LX ```bash 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//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: ```bash grep 'event=top_process' \ /opt/custom/var/log/o6h-memory-guard/guard.log | tail -100 ``` Después debe correlacionarlo con: ```bash 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.