Skip to main content

Command Palette

Search for a command to run...

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

Published
12 min read
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

ACTION_SCRIPT

Script que se ejecuta para start/stop/check/status

DESCRIPTION

Descripción del recurso

START_DEPENDENCIES

Recurso o servicio del que depende para iniciar

STOP_DEPENDENCIES

Recurso o servicio del que depende para parar

RESTART_ATTEMPTS

Número de reintentos si falla

CHECK_INTERVAL

Intervalo de chequeo en segundos

START_TIMEOUT

Tiempo máximo para iniciar

STOP_TIMEOUT

Tiempo máximo para parar

HOSTING_MEMBERS

Nodos en los que puede correr el recurso

PLACEMENT

Restricciones de colocación (restricted significa que solo se ejecuta en nodos listados)

CARDINALITY

Número de instancias del recurso (1 en nuestro caso)

ACTIONS

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.

More from this blog

Carla Muñoz López

64 posts

Soy DBA senior de bases de datos Oracle y me defino como una persona alegre y creativa. Apasionada por conocer, compartir ideas, divertirme y seguir aprendiendo todo lo relacionado con Oracle.