Cómo Crear un Recurso de Clúster en Oracle para Ejecutar SQL de forma persistente

En entornos Oracle RAC (Real Application Clusters), a veces necesitamos ejecutar procesos de monitorización o mantenimiento que estén vinculados a la base de datos, que puedan moverse entre nodos, pararse y arrancarse automáticamente. Esto se puede lograr mediante recursos de clúster gestionados por Oracle Clusterware, con scripts que interactúan con la base de datos a través de SQL.
En este artículo veremos un ejemplo práctico, donde implementaremos un recurso llamado monitorizacion_app que lanza un sql que se ejecuta de forma persistente en la BBDD.
Concepto de Recurso de Clúster
Un recurso de clúster en Oracle es cualquier elemento que se puede gestionar mediante Clusterware y tiene:
Un script de acción que Clusterware ejecuta (start, stop, check, status).
Dependencias de otros recursos o servicios.
Posibilidad de moverse entre nodos (relocate).
Parámetros de reinicio automático y timeout.
Control de permisos de usuario y ejecución segura.
En este ejemplo, el recurso ejecutará un script que lanza un procedimiento PL/SQL en la base de datos de forma persistente. La finalidad es que va informando una tabla con datos de consultas muy recurrentes de monitorización (métricas de negocio, sesiones conectadas, etc ) a modo de caché (evitamos llamadas recurrentes mismos usuarios pidiendo la misma información) y guardando un histórico en el tiempo para análisis de tendencias.
Preparación del Script de Monitorización
Nuestro script /scripts/monitorizacion_resource.sh tiene las siguientes características:
Ejecutado por el usuario grid, pero con permisos sudo para ejecutar como oracle.
Controla la inicio, detención, check y status del monitor.
Interactúa con la base de datos a través de SQLPlus.
Guarda logs separados por nodo (monitor_app_.log).
Puede matar sesiones activas si el recurso se detiene.
Ejemplo de invocación manual:
/scripts/monitorizacion_resource.sh start
/scripts/monitorizacion_resource.sh stop
/scripts/monitorizacion_resource.sh status
/scripts/monitorizacion_resource.sh check
Crear el Servicio Oracle
Antes de crear el recurso de clúster, crearemos un servicio asociado a la base de datos, que será la dependencia de nuestro monitor
srvctl add service -db BASEDEDATOS -service MONITORIZACION \
-role primary -preferred node1 -available node2 \
-pdb PDB_APP -stopoption IMMEDIATE
srvctl start service -db BASEDEDATOS -service MONITORIZACION
Este servicio será usado por el monitor para conectarse a la base de datos.
Crear el Recurso de Clúster
Lógica Principal: El script redirige errores al log y luego gestiona la acción solicitada:
Start
Limpia el log.
Verifica si el monitor ya corre en BD.
Lanza el procedimiento PL/SQL USUARIO_MONITORIZACION.pkg_monitor.monitor_main_loop en background con nohup.
Espera a que la sesión aparezca en la BD, confirmando que el monitor está activo.
Stop
Lista todas las sesiones activas.
Mata cada sesión con kill_session_db.
Verifica que no queden sesiones residuales.
Check
Realiza hasta 3 intentos para comprobar que el monitor está corriendo.
Retorna 0 si está activo y 1 si no.
Status
Lista todas las sesiones activas.
Retorna 0 si hay sesiones y 1 si no.
Dentro del paquete de monitorización indentificamos el módulo para ser capaces de ubicar exactamente las sesiones mediante (ejemplo): DBMS_APPLICATION_INFO.SET_MODULE('MODULO_MONITOR', 'capture_metrics');
Además, para que sea persistente, el paquete ejecuta en bucle el sql
LOOP
-- Ejecuta la captura de métricas
.....................
-- Espera de X segundo
DBMS_SESSION.SLEEP(C_SLEEP_CAPTURE);END LOOP;
Código:
#!/bin/bash
# Script para Clusterware monitorizacion_app
# Ejecutado por usuario grid con sudo a oracle
ORACLE_HOME="/u01/app/oracle/product/19.xx"
# Ruta de log local al nodo
HOSTNAME=$(hostname -s)
LOG_FILE="/scripts/logs/monitor_app_${HOSTNAME}.log"
touch "$LOG_FILE" 2>/dev/null || true
chmod 664 "$LOG_FILE" 2>/dev/null || true
# Identificadores únicos para nuestro monitor
MODULE_NAME="MODULO_MONITOR"
# Credenciales
DB_USER="USUARIO_MONITORIZACION"
DB_PASSWORD="xxxxxxxxxxx"
DB_TNS_SERVICE="MONITORIZACION"
ACTION="$1"
# --- FUNCIONES ---
# Ejecutar SQL y devolver resultado
run_sql() {
local sql="$1"
sudo -u oracle /bin/bash -c "
export ORACLE_HOME='$ORACLE_HOME'
export PATH=\"\\(ORACLE_HOME/bin:\\)PATH\"
sqlplus -S \(DB_USER/\)DB_PASSWORD@$DB_TNS_SERVICE <<'EOF'
SET FEEDBACK OFF
SET HEADING OFF
SET PAGESIZE 0
SET VERIFY OFF
SET TRIMSPOOL ON
SET LINESIZE 32767
$sql
EOF
" 2>/dev/null
}
# Verificar si el monitor está corriendo en BD
is_monitor_running_db() {
local sql="
SELECT 'RUNNING:'||s.inst_id||':'||s.sid||':'||s.serial#||':'||s.process
FROM gv\$session s
WHERE s.username = UPPER('$DB_USER')
AND s.module = '$MODULE_NAME'
AND ROWNUM = 1
;
"
local result
result=\((run_sql "\)sql")
if echo "$result" | grep -q "^RUNNING:"; then
echo "$result" | sed 's/^RUNNING://'
return 0
else
return 1
fi
}
# Detener sesión en BD
kill_session_db() {
local inst_id="$1"
local sid="$2"
local serial="$3"
local sql="ALTER SYSTEM KILL SESSION '\(sid,\)serial,@$inst_id' IMMEDIATE;"
run_sql "$sql" >/dev/null
return $?
}
# Obtener todas las sesiones del monitor
get_all_monitor_sessions() {
local sql="
SELECT
s.inst_id||':'||s.sid||':'||s.serial#||':'||s.process
FROM gv\$session s
WHERE s.username = UPPER('$DB_USER')
AND s.module = '$MODULE_NAME'
;
"
run_sql "$sql"
}
# --- LÓGICA PRINCIPAL ---
exec 2>>"$LOG_FILE"
case "$ACTION" in
start)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Iniciando monitor..." >&2
# Limpiar log solo al inicio
echo "=== MONITOR INICIADO \((date '+%Y-%m-%d %H:%M:%S') ===" > "\)LOG_FILE"
# Verificar si ya está corriendo en BD
if session_info=$(is_monitor_running_db); then
IFS=':' read -r inst_id sid serial process <<< "$session_info"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Monitor ya está corriendo en BD:" >&2
echo " Instancia: \(inst_id, SID: \)sid, Serial#: $serial" >&2
exit 0
fi
# Iniciar el proceso
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Ejecutando procedimiento de monitorización..." >&2
# Lanzar proceso en background usando nohup
nohup sudo -u oracle /bin/bash -c "
export ORACLE_HOME='$ORACLE_HOME'
export PATH=\"\\(ORACLE_HOME/bin:\\)PATH\"
export LD_LIBRARY_PATH=\"\\(ORACLE_HOME/lib:\\)LD_LIBRARY_PATH\"
sqlplus -S \(DB_USER/\)DB_PASSWORD@\(DB_TNS_SERVICE <<'SQL_EOF' >> \"\)LOG_FILE\" 2>&1
SET SERVEROUTPUT ON SIZE UNLIMITED
BEGIN
USUARIO_MONITORIZACION.pkg_monitor.monitor_main_loop;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('[' || TO_CHAR(SYSTIMESTAMP, 'YYYY-MM-DD HH24:MI:SS') || '] ERROR: ' || SQLERRM);
RAISE;
END;
/
SQL_EOF
" >/dev/null 2>&1 &
# Esperar y verificar registro en BD
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Esperando registro en BD..." >&2
for i in {1..10}; do
if session_info=$(is_monitor_running_db); then
IFS=':' read -r inst_id sid serial process <<< "$session_info"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Monitor funcionando:" >&2
echo " Instancia: \(inst_id, SID: \)sid, Serial#: $serial" >&2
exit 0
fi
sleep 3
done
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: No se registró en BD después de 30 segundos" >&2
exit 1
;;
stop)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Deteniendo monitor..." >&2
# Obtener todas las sesiones
sessions=$(get_all_monitor_sessions)
if [ -z "$sessions" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] No hay sesiones activas" >&2
exit 0
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Sesiones encontradas:" >&2
while IFS= read -r session_line; do
[ -z "$session_line" ] && continue
IFS=':' read -r inst_id sid serial process <<< "$session_line"
echo " Instancia: \(inst_id, SID: \)sid, Serial#: \(serial, Process: \)process" >&2
done <<< "$sessions"
# Matar cada sesión
while IFS= read -r session_line; do
[ -z "$session_line" ] && continue
IFS=':' read -r inst_id sid serial process <<< "$session_line"
echo " Matando: Instancia \(inst_id, SID \)sid, Serial# \(serial", Process: \)process >&2
kill_session_db "\(inst_id" "\)sid" "$serial"
done <<< "$sessions"
sleep 2
if remaining_sessions=\((get_all_monitor_sessions); [ -n "\)remaining_sessions" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Aún hay sesiones después de KILL SESSION:" >&2
while IFS= read -r line; do
IFS=':' read -r i s se p <<< "$line"
echo " Instancia: \(i, SID: \)s, Serial#: \(se, Process: \)p" >&2
done <<< "$remaining_sessions"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Todas las sesiones eliminadas" >&2
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Comando stop completado" >&2
exit 0
;;
check)
# Clusterware: exit 0 si está corriendo, 1 si no
# Intentar varias veces con pausas cortas
max_attempts=3
wait_seconds=5
for attempt in \((seq 1 \)max_attempts); do
if session_info=$(is_monitor_running_db); then
IFS=':' read -r inst_id sid serial process <<< "$session_info"
echo "[\((date '+%Y-%m-%d %H:%M:%S')] Monitor corriendo (Intento \)attempt/\(max_attempts) - Instancia: \)inst_id, SID: $sid" >&2
exit 0
fi
if [ \(attempt -lt \)max_attempts ]; then
echo "[\((date '+%Y-%m-%d %H:%M:%S')] Check falló (Intento \)attempt/\(max_attempts), reintentando en \){wait_seconds}s..." >&2
sleep $wait_seconds
fi
done
echo "[\((date '+%Y-%m-%d %H:%M:%S')] Monitor no corriendo después de \)max_attempts intentos" >&2
exit 1
;;
status)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Estado del monitor:" >&2
sessions=$(get_all_monitor_sessions)
if [ -n "$sessions" ]; then
echo "Sesiones activas:" >&2
while IFS= read -r line; do
IFS=':' read -r inst_id sid serial process <<< "$line"
echo " Instancia: \(inst_id, SID: \)sid, Serial#: \(serial, Process: \)process" >&2
done <<< "$sessions"
exit 0
else
echo "No hay sesiones activas" >&2
exit 1
fi
;;
*)
echo "Uso: $0 {start|stop|check|status}" >&2
exit 1
;;
esac
Crear el Recurso de Clúster
El recurso se crea mediante crsctl:
crsctl add resource monitorizacion_app \
-type cluster_resource \
-attr "ACTION_SCRIPT='/scripts/monitorizacion_resource.sh', \
DESCRIPTION='Proceso persistente de monitorización', \
START_DEPENDENCIES='hard(ora.BASEDEDATOS.MONITORIZACION.svc)', \
STOP_DEPENDENCIES='hard(ora.BASEDEDATOS.MONITORIZACION.svc)', \
RESTART_ATTEMPTS=5, \
CHECK_INTERVAL=30, \
START_TIMEOUT=60, \
STOP_TIMEOUT=60, \
HOSTING_MEMBERS='node1 node2', \
PLACEMENT='restricted', \
CARDINALITY=1, \
ACTIONS='START,user:oracle STOP,user:oracle CHECK'"
Explicación de los parámetros:
Parámetro | Descripción |
|---|---|
| Script que se ejecuta para start/stop/check/status |
| Descripción del recurso |
| Recurso o servicio del que depende para iniciar |
| Recurso o servicio del que depende para parar |
| Número de reintentos si falla |
| Intervalo de chequeo en segundos |
| Tiempo máximo para iniciar |
| Tiempo máximo para parar |
| Nodos en los que puede correr el recurso |
| Restricciones de colocación ( |
| Número de instancias del recurso (1 en nuestro caso) |
| Qué acciones puede realizar el recurso y con qué usuario |
Asignar Permisos de Ejecución al Recurso
crsctl setperm resource monitorizacion_app -u user:oracle:r-x
Esto permite que Clusterware ejecute el script como el usuario oracle de manera segura.
Gestión del Recurso
Una vez creado, puedes:
# Iniciar
crsctl start resource monitorizacion_app
# Parar
crsctl stop resource monitorizacion_app
# Verificar estado
crsctl status resource monitorizacion_app
Pruebas Reales de Dependencias y Relocalización del Recurso
Una vez creado el recurso monitorizacion_app y vinculado mediante dependencia hard a un servicio de base de datos, realizamos pruebas para validar:
Que el recurso bloquea el stop del servicio si está activo
Que las dependencias se respetan
Que el recurso puede moverse correctamente entre nodos
Que el kill de sesión se ejecuta correctamente
Que el estado final queda consistente
Validación de Dependencia Hard
Intentamos parar el servicio asociado:
srvctl stop service -d BASEDEDATOS -s monitorizacion -i BASEDEDATOS1
PRCD-1316 : fallo al parar los servicios monitorizacion para la base de datos BASEDEDATOS
PRCR-1132 : Fallo al parar los recursos mediante un filtro
CRS-2974: no se ha podido actuar en el recurso 'ora.BASEDEDATOS.MONITORIZACION.svc' en el servidor 'node1' porque sería necesario parar o reubicar el recurso 'monitorizacion_app', pero no se ha especificado el indicador de forzado apropiado
crsctl status resource monitorizacion_app
NAME=monitorizacion_app
TYPE=cluster_resource
TARGET=ONLINE
STATE=ONLINE on node1
Relocate Manual del Recurso
Mueve dependencias:
crsctl relocate resource monitorizacion_app -n nodo2
Attempting to start 'ora.BASEDEDATOS.MONITORIZACION.svc' on 'nodo2'
Start of 'ora.BASEDEDATOS.MONITORIZACION.svc' succeeded
Attempting to stop 'monitorizacion_app' on 'nodo1'
Stop of 'monitorizacion_app' on 'nodo1' succeeded
Attempting to start 'monitorizacion_app' on 'nodo2'
Start of 'monitorizacion_app' on 'nodo2' succeeded
-- Secuencia de eventos
crsctl status resource monitorizacion_app
NAME=monitorizacion_app
TYPE=cluster_resource
TARGET=ONLINE
STATE=ONLINE on nodo1
crsctl status resource monitorizacion_app
NAME=monitorizacion_app
TYPE=cluster_resource
TARGET=ONLINE
STATE=OFFLINE
crsctl status resource monitorizacion_app
NAME=monitorizacion_app
TYPE=cluster_resource
TARGET=ONLINE
STATE=ONLINE on nodo2
-- Veremos en el alertlog
(3):KILL SESSION for sid=(2855, 32280):
(3): Reason = alter system kill session
(3): Mode = KILL HARD SAFE -/-/-
(3): Requestor = USER (orapid = 309, ospid = 810697, inst = 1)
(3): Owner = Process: USER (orapid = 150, ospid = 3169885)
(3): Result = ORA-31
Parada y arranque
crsctl stop resource monitorizacion_app
-- Veremos en el alertlog
(3):KILL SESSION for sid=(2855, 32280):
(3): Reason = alter system kill session
(3): Mode = KILL HARD SAFE -/-/-
(3): Requestor = USER (orapid = 309, ospid = 810697, inst = 1)
(3): Owner = Process: USER (orapid = 150, ospid = 3169885)
(3): Result = ORA-31
crsctl start resource monitorizacion_app
Ejemplo registro log
-- Ejemplo arranque
more /scripts/logs/monitor_app_nodo1.log
=== MONITOR INICIADO 2026-02-03 22:02:08 ===
[2026-02-03 22:02:08] Ejecutando procedimiento de monitorización...
[2026-02-03 22:02:08] Esperando registro en BD...
[2026-02-03 22:02:12] Monitor funcionando:
Instancia: 1, SID: 3320, Serial#: 54019
[2026-02-03 22:02:12] Monitor corriendo (Intento 1/3) - Instancia: 1, SID: 3320
[2026-02-03 22:02:42] Monitor corriendo (Intento 1/3) - Instancia: 1, SID: 3320
[2026-02-03 22:03:12] Monitor corriendo (Intento 1/3) - Instancia: 1, SID: 3320
[2026-02-03 22:03:42] Monitor corriendo (Intento 1/3) - Instancia: 1, SID: 3320
[2026-02-03 22:04:12] Monitor corriendo (Intento 1/3) - Instancia: 1, SID: 3320
[2026-02-03 22:04:42] Monitor corriendo (Intento 1/3) - Instancia: 1, SID: 3320
[2026-02-03 22:05:12] Monitor corriendo (Intento 1/3) - Instancia: 1, SID: 3320
-- Ejemplo parada
[2025-12-10 12:51:19] Deteniendo monitor...
[2025-12-10 12:51:19] Sesiones encontradas:
Instancia: 1, SID: 5197, Serial#: 59722, Process: 3928045
Matando: Instancia 1, SID 5197, Serial# 59722, Process: 3928045
ERROR:
ORA-03114: not connected to ORACLE
BEGIN
*
ERROR at line 1:
ORA-00028: your session has been killed
ORA-06512: at "SYS.DBMS_SESSION", line 435
ORA-06512: at "USUARIO_MONITORIZACION.PKG_MONITOR", line 236
ORA-06512: at line 2
[2025-12-10 12:51:21] Todas las sesiones eliminadas
[2025-12-10 12:51:21] Comando stop completado
[2025-12-10 12:51:21] Check falló (Intento 1/3), reintentando en 5s...
[2025-12-10 12:51:26] Check falló (Intento 2/3), reintentando en 5s...
[2025-12-10 12:51:31] Monitor no corriendo después de 3 intentos
Ventajas de Esta Configuración
Persistencia: El monitor se asegura de que siempre haya una sesión activa.
Alta disponibilidad: Puede moverse a otro nodo si falla el nodo actual.
Integración con servicios: Solo arranca si el servicio de base de datos está disponible.
Control total: Inicio/parada manual, chequeo de estado, reinicio automático.
Conclusión
En nuestro caso necesitábamos que el proceso sobreviviese a relocalizaciones del servicio y que no dependiese de un cron externo. Después de probar varias alternativas ( job scheduler, etc.), la opción de recurso gestionado por Clusterware resultó ser la más limpia operativamente.
Permite:
Ejecutar scripts críticos vinculados a servicios.
Gestionar fallos y reinicios automáticos.
Mantener alta disponibilidad y control operativo.



