Journaling Deep Dive

Journal receivers, journal-based audit, recovery point-in-time, replicacion para alta disponibilidad y gestion avanzada de journals en IBM i.

Conceptos de journaling

El journaling es el mecanismo de registro transaccional nativo de IBM i. Captura cada modificacion realizada sobre los datos (INSERT, UPDATE, DELETE) en un log secuencial e inmutable que permite recovery, auditoria y replicacion.

x86 -- Write-Ahead Log (WAL)

  • PostgreSQL: WAL logs en pg_wal/
  • MySQL: Binary logs (binlog)
  • SQL Server: Transaction Log (.ldf)
  • Configuracion separada del motor
  • Rotacion por tamano o tiempo
  • Replicacion basada en WAL shipping

IBM i -- Journaling

  • Journal (*JRN) + Receivers (*JRNRCV)
  • Integrado en el sistema operativo
  • Se activa por tabla o por biblioteca
  • Receivers rotan por swap o automatico
  • Remote journaling para HA/DR nativo
  • Recovery con APYJRNCHG / RMVJRNCHG

Journal (*JRN)

Objeto central que recibe y gestiona las entradas. Las tablas se asocian a un journal. Administra la cadena de receivers.

Journal Receiver (*JRNRCV)

Almacena fisicamente las entradas de journal. Se crean en cadena secuencial. Cuando uno se llena, se hace swap a uno nuevo.

Journal Entry

Cada registro individual de cambio. Contiene el tipo de operacion, datos antes/despues, usuario, programa, job y timestamp.

Analogia: Pensa en el journal como una camara de seguridad que graba todo lo que pasa en tu base de datos. El journal receiver es la cinta donde se graba. Cuando la cinta se llena, la cambias por una nueva (swap) y guardas la anterior para consultar si es necesario.

Crear journals y receivers

El setup de journaling requiere tres pasos: crear el receiver inicial, crear el journal y luego asociar las tablas al journal con STRJRNPF.

CL -- Paso 1: Crear el journal receiver
CRTJRNRCV JRNRCV(MILIB/RCVR0001) +
          THRESHOLD(500000) +
          TEXT('Receiver inicial para MILIB')

/* THRESHOLD: tamano en KB a partir del cual el sistema
   genera un mensaje de warning. Con MNGRCV(*SYSTEM) en
   el journal, se hace swap automatico al llegar.       */
CL -- Paso 2: Crear el journal
CRTJRN JRN(MILIB/QSQJRN) +
       JRNRCV(MILIB/RCVR0001) +
       MNGRCV(*SYSTEM) +
       DLTRCV(*YES) +
       RCVSIZOPT(*MAXOPT2) +
       MINENTDTA(*NONE) +
       TEXT('Journal principal de MILIB')

/* Parametros clave:
   MNGRCV(*SYSTEM) - swap automatico de receivers
   DLTRCV(*YES)    - elimina receivers viejos automaticamente
   RCVSIZOPT(*MAXOPT2) - optimiza I/O de receivers grandes
   MINENTDTA(*NONE)    - datos completos en cada entrada   */
CL -- Paso 3: Journalear tablas (STRJRNPF)
/* Asociar tablas criticas al journal */
STRJRNPF FILE(MILIB/CLIENTES) +
         JRN(MILIB/QSQJRN) +
         IMAGES(*BOTH) +
         OMTJRNE(*OPNCLO)

STRJRNPF FILE(MILIB/PEDIDOS) +
         JRN(MILIB/QSQJRN) +
         IMAGES(*BOTH) +
         OMTJRNE(*OPNCLO)

STRJRNPF FILE(MILIB/FACTURAS) +
         JRN(MILIB/QSQJRN) +
         IMAGES(*BOTH) +
         OMTJRNE(*OPNCLO)

/* Detener journaling de una tabla */
ENDJRNPF FILE(MILIB/TABLA_TEMP) +
         JRN(MILIB/QSQJRN)
Parametro STRJRNPFDescripcion
IMAGES(*BOTH)Graba imagen antes y despues del cambio. Necesario para RMVJRNCHG (reverse recovery).
IMAGES(*AFTER)Solo imagen posterior. Menos espacio pero no permite reverse recovery.
OMTJRNE(*OPNCLO)Omitir entradas de open/close. Reduce volumen significativamente.
JRN(*FILE)Usar el journal por defecto de la tabla (si esta definido).
Tablas SQL: Las tablas creadas con CREATE TABLE en una biblioteca que tiene un journal QSQJRN se journalean automaticamente. No es necesario ejecutar STRJRNPF para tablas creadas via DDL si el journal por defecto esta configurado.

Journal entries y tipos

Cada operacion sobre una tabla journaleada genera una o mas entradas en el journal. Los tipos mas importantes se identifican por un codigo de journal y un tipo de entrada.

CodigoTipoDescripcion
R PTRecord Put (INSERT)Se inserto un registro nuevo en la tabla
R UBUpdate Before ImageImagen ANTES del UPDATE (datos originales)
R UPUpdate After ImageImagen DESPUES del UPDATE (datos nuevos)
R DLRecord DeleteSe elimino un registro de la tabla
R BRRead Before UpdateLectura antes de update (record-level access)
F OPFile OpenSe abrio el archivo/tabla
F CLFile CloseSe cerro el archivo/tabla
C SCStart Commit CycleInicio de un ciclo de commitment control
C CMCommitSe confirmo (commit) la transaccion
C RBRollbackSe deshizo (rollback) la transaccion
J PRReceiver SwapSe realizo un swap de journal receiver
*CHG vs *BOTH: Con IMAGES(*BOTH), cada UPDATE genera dos entradas: UB (before image) y UP (after image). Con IMAGES(*AFTER), solo se genera UP. La before image es esencial para reverse recovery y para auditoria detallada de cambios.
CL -- Ver entradas con DSPJRN
/* Forma clasica: volcar entradas a un outfile */
DSPJRN JRN(MILIB/QSQJRN) +
       FILE(MILIB/CLIENTES) +
       RCVRNG(*CURCHAIN) +
       JRNCDE(R) +
       ENTTYP(PT UB UP DL) +
       OUTPUT(*OUTFILE) +
       OUTFILFMT(*TYPE5) +
       OUTFILE(QTEMP/JRNDATA)

/* Luego consultar el outfile */
SELECT * FROM QTEMP.JRNDATA
ORDER BY JOENTT DESC
FETCH FIRST 50 ROWS ONLY

Journaling para auditoria

El journal es la herramienta definitiva de auditoria en IBM i. Cada entrada registra no solo los datos modificados, sino el contexto completo: usuario, programa, job, direccion IP y timestamp exacto.

Quien hizo el cambio

USER_NAME identifica al perfil de usuario que ejecuto la operacion.

Desde donde

JOB_NAME muestra el job. En combinacion con la IP del job se puede rastrear el origen.

Con que programa

PROGRAM_NAME registra el *PGM o *SRVPGM que ejecuto la operacion.

Cuando exactamente

ENTRY_TIMESTAMP con precision de microsegundos para cada operacion.

SQL -- Auditoria: quien modifico datos de un cliente
-- Rastrear todos los cambios sobre el cliente 1001
SELECT
  ENTRY_TIMESTAMP AS CUANDO,
  USER_NAME AS QUIEN,
  JOB_NAME AS DESDE_JOB,
  PROGRAM_NAME AS PROGRAMA,
  JOURNAL_ENTRY_TYPE AS OPERACION,
  CASE JOURNAL_ENTRY_TYPE
    WHEN 'PT' THEN 'INSERT'
    WHEN 'UP' THEN 'UPDATE (after)'
    WHEN 'UB' THEN 'UPDATE (before)'
    WHEN 'DL' THEN 'DELETE'
    ELSE JOURNAL_ENTRY_TYPE
  END AS TIPO_LEGIBLE,
  ENTRY_DATA
FROM TABLE(
  QSYS2.DISPLAY_JOURNAL(
    'MILIB', 'QSQJRN',
    JOURNAL_CODES       => 'R',
    JOURNAL_ENTRY_TYPES => 'PT,UB,UP,DL',
    OBJECT_NAME         => 'CLIENTES',
    OBJECT_LIBRARY      => 'MILIB',
    STARTING_TIMESTAMP  => CURRENT_TIMESTAMP - 30 DAYS
  )
) AS jrn
ORDER BY ENTRY_TIMESTAMP DESC;
SQL -- Resumen de actividad por usuario y tabla
-- Dashboard de actividad: operaciones por usuario en la ultima semana
SELECT
  USER_NAME AS USUARIO,
  OBJECT_NAME AS TABLA,
  SUM(CASE WHEN JOURNAL_ENTRY_TYPE = 'PT' THEN 1 ELSE 0 END) AS INSERTS,
  SUM(CASE WHEN JOURNAL_ENTRY_TYPE = 'UP' THEN 1 ELSE 0 END) AS UPDATES,
  SUM(CASE WHEN JOURNAL_ENTRY_TYPE = 'DL' THEN 1 ELSE 0 END) AS DELETES,
  COUNT(*) AS TOTAL_OPS,
  MIN(ENTRY_TIMESTAMP) AS PRIMERA_OP,
  MAX(ENTRY_TIMESTAMP) AS ULTIMA_OP
FROM TABLE(
  QSYS2.DISPLAY_JOURNAL(
    'MILIB', 'QSQJRN',
    JOURNAL_CODES      => 'R',
    STARTING_TIMESTAMP => CURRENT_DATE - 7 DAYS
  )
) AS jrn
GROUP BY USER_NAME, OBJECT_NAME
ORDER BY TOTAL_OPS DESC;

Recovery con journals

Los journals permiten dos estrategias fundamentales de recovery: forward recovery (aplicar cambios sobre un backup) y reverse recovery (deshacer cambios erroneos).

Forward Recovery (APYJRNCHG)

  • Aplica cambios del journal a una tabla restaurada
  • Escenario: restore de backup + aplicar cambios del dia
  • Lleva la tabla al estado mas reciente posible
  • Funciona con IMAGES(*BOTH) y con IMAGES(*AFTER)
  • Equivalente a pg_restore + WAL replay en PostgreSQL

Reverse Recovery (RMVJRNCHG)

  • Deshace cambios aplicados a la tabla
  • Escenario: DELETE sin WHERE ejecutado por error
  • Requiere obligatoriamente IMAGES(*BOTH)
  • Opera en orden inverso (del ultimo al primero)
  • No hay equivalente directo en PostgreSQL/MySQL
CL -- Forward Recovery: restaurar + aplicar journals
/* Escenario: la tabla CLIENTES se corrompio.
   Tenes un backup de anoche y los journals del dia. */

/* Paso 1: Restaurar la tabla desde el backup */
RSTOBJ OBJ(CLIENTES) SAVLIB(MILIB) DEV(*SAVF) +
       SAVF(MILIB/SAVNOCHE)

/* Paso 2: Aplicar los cambios del journal desde el ultimo save */
APYJRNCHG JRN(MILIB/QSQJRN) +
          FILE(MILIB/CLIENTES) +
          FROMENT(*LASTSAVE) +
          TOENT(*LAST)

/* Resultado: la tabla queda con todos los datos
   hasta la ultima entrada del journal */
CL -- Reverse Recovery: deshacer cambios
/* Escenario: a las 14:00 alguien ejecuto
   DELETE FROM MILIB.CLIENTES (sin WHERE!) */

/* Deshacer todos los cambios desde las 14:00 */
RMVJRNCHG JRN(MILIB/QSQJRN) +
          FILE(MILIB/CLIENTES) +
          FROMENT(*LAST) +
          TOTIME('2025-01-15-14.00.00')

/* Point-in-time recovery: aplicar hasta un momento especifico */
APYJRNCHG JRN(MILIB/QSQJRN) +
          FILE(MILIB/CLIENTES) +
          FROMENT(*LASTSAVE) +
          TOTIME('2025-01-15-13.59.59')
Critico: RMVJRNCHG solo funciona si el journaling se configuro con IMAGES(*BOTH). Con IMAGES(*AFTER) solamente se puede hacer forward recovery. Para tablas de produccion criticas, siempre usa *BOTH.

Replicacion basada en journals

El remote journaling permite enviar journal entries a otro sistema IBM i en tiempo real. Es la base de las soluciones de alta disponibilidad (HA) como Precisely MIMIX, iTERA, Quick-EDD y Robot HA.

Replicacion sincrona

La aplicacion espera hasta que la entrada se grabe en ambos sistemas. Maxima proteccion, mayor latencia. RPO = 0.

Replicacion asincrona

La entrada se graba localmente y se envia al remoto en background. Menor impacto en performance. RPO > 0.

CL -- Configurar remote journaling
/* Agregar remote journal al journal existente */
ADDRMTJRN JRN(MILIB/QSQJRN) +
          RMTJRN((MILIB/QSQJRN *RMTSYSNAME SYS_DR)) +
          DLTRCV(*YES) +
          JRNSTATE(*ACTIVE) +
          DELIVERY(*ASYNC)

/* Verificar estado del remote journal */
WRKRMTJRN JRN(MILIB/QSQJRN)
SQL -- Monitorear estado de replicacion
-- Consultar estado de remote journals
SELECT
  JOURNAL_NAME,
  JOURNAL_LIBRARY,
  REMOTE_JOURNAL_TYPE,
  REMOTE_JOURNAL_DELIVERY,
  RELATIONAL_DATABASE_NAME AS SISTEMA_REMOTO,
  REMOTE_JOURNAL_STATE AS ESTADO,
  CURRENT_RECEIVER_NAME,
  CURRENT_RECEIVER_LIBRARY
FROM QSYS2.REMOTE_JOURNAL_INFO
WHERE JOURNAL_NAME = 'QSQJRN'
  AND JOURNAL_LIBRARY = 'MILIB';

PostgreSQL -- Streaming Replication

  • WAL shipping via pg_receivewal
  • Synchronous / asynchronous commit
  • pg_stat_replication para monitoreo
  • Hot standby para lecturas
  • Logical replication con publicaciones

IBM i -- Remote Journaling

  • Journal entries via comunicacion DDM/DRDA
  • Sync / async delivery configurable
  • QSYS2.REMOTE_JOURNAL_INFO para monitoreo
  • Sistema target puede ser read-only o activo
  • Productos HA manejan apply automatico

Gestion de journal receivers

Los journal receivers crecen continuamente con cada operacion registrada. Una buena gestion de receivers es esencial para evitar problemas de espacio en disco y mantener la cadena de recovery integra.

CL -- Swap manual y gestion de receivers
/* Swap manual: crear nuevo receiver y cambiar */
CRTJRNRCV JRNRCV(MILIB/RCVR0002) +
          THRESHOLD(500000) +
          TEXT('Receiver 2 para MILIB')

CHGJRN JRN(MILIB/QSQJRN) +
       JRNRCV(MILIB/RCVR0002)
/* El receiver anterior queda "detached" */

/* Eliminar un receiver viejo (verificar que no sea necesario) */
DLTJRNRCV JRNRCV(MILIB/RCVR0001)
/* Solo se puede eliminar si esta detached y
   no es necesario para recovery */

/* Ver informacion del journal */
WRKJRN JRN(MILIB/QSQJRN)
WRKJRNA JRN(MILIB/QSQJRN)
SQL -- Consultar receivers y su estado
-- Estado de todos los receivers de un journal
SELECT
  JOURNAL_RECEIVER_NAME AS RECEIVER,
  JOURNAL_RECEIVER_LIBRARY AS BIBLIOTECA,
  CASE ATTACHED
    WHEN 'YES' THEN 'ATTACHED (activo)'
    ELSE 'DETACHED'
  END AS ESTADO,
  SIZE AS TAMANO_KB,
  NUMBER_JOURNAL_ENTRIES AS ENTRADAS,
  FIRST_ENTRY_TIMESTAMP AS PRIMERA_ENTRADA,
  LAST_ENTRY_TIMESTAMP AS ULTIMA_ENTRADA
FROM QSYS2.JOURNAL_RECEIVER_INFO
WHERE JOURNAL_NAME = 'QSQJRN'
  AND JOURNAL_LIBRARY = 'MILIB'
ORDER BY FIRST_ENTRY_TIMESTAMP;

-- Tamano total de receivers activos
SELECT
  SUM(SIZE) / 1024 AS TOTAL_MB,
  COUNT(*) AS CANTIDAD_RECEIVERS
FROM QSYS2.JOURNAL_RECEIVER_INFO
WHERE JOURNAL_NAME = 'QSQJRN'
  AND JOURNAL_LIBRARY = 'MILIB';
Retencion: Con DLTRCV(*YES) en el journal, los receivers detached se eliminan automaticamente cuando ya no son necesarios para recovery. Si necesitas mantener receivers para auditoria a largo plazo, usa DLTRCV(*NO) y gestiona la eliminacion manualmente, idealmente despues de salvarlos a cinta o medio externo.

Impacto en performance

El journaling tiene un costo de I/O adicional. Cada operacion de escritura genera al menos una entrada en el receiver. Sin embargo, este overhead es generalmente bajo (2-5%) y se puede optimizar con las configuraciones adecuadas.

Journal caching

El sistema agrupa multiples entradas en un solo write fisico al receiver. RCVSIZOPT(*MAXOPT2) mejora esto aun mas, reduciendo operaciones de I/O.

IMAGES(*AFTER) vs *BOTH

*AFTER usa la mitad del espacio en el receiver pero no permite reverse recovery. Para tablas de alto volumen no criticas, puede ser una opcion.

OMTJRNE(*OPNCLO)

Omitir entradas de open/close reduce significativamente el volumen. Las entradas F OP y F CL rara vez se necesitan para recovery.

MINENTDTA(*NONE)

No minimizar datos de entrada. *FILE reduce tamano pero complica la lectura del journal. Solo para escenarios extremos de volumen.

Receivers en ASP separado

Colocar los receivers en un ASP (disk pool) diferente al de los datos mejora el throughput al distribuir la carga de I/O.

MNGRCV(*SYSTEM)

El sistema maneja swaps automaticos. Evita la situacion de receiver full que bloquea operaciones de escritura.

MySQL -- Binary Log overhead

  • binlog_format: ROW vs STATEMENT vs MIXED
  • sync_binlog=1 para mayor durabilidad
  • Overhead tipico: 1-5% segun workload
  • binlog_row_image: full / minimal / noblob

IBM i -- Journal overhead

  • IMAGES(*BOTH) vs *AFTER para controlar volumen
  • Journal caching reduce I/O fisico
  • Overhead tipico: 2-5% segun configuracion
  • OMTJRNE(*OPNCLO) reduce entradas innecesarias

Consultar journals con SQL

A partir de IBM i 7.3, la table function QSYS2.DISPLAY_JOURNAL permite consultar journal entries directamente con SQL, eliminando la necesidad de usar DSPJRN con outfiles.

SQL -- DISPLAY_JOURNAL: sintaxis basica
-- Consulta basica de journal entries
SELECT
  ENTRY_TIMESTAMP,
  JOURNAL_ENTRY_TYPE,
  JOURNAL_CODE,
  JOB_NAME,
  USER_NAME,
  PROGRAM_NAME,
  OBJECT_NAME,
  OBJECT_LIBRARY,
  ENTRY_DATA
FROM TABLE(
  QSYS2.DISPLAY_JOURNAL(
    'MILIB', 'QSQJRN',
    JOURNAL_ENTRY_TYPES   => 'PT,UB,UP,DL',
    JOURNAL_CODES         => 'R',
    OBJECT_NAME           => 'CLIENTES',
    OBJECT_LIBRARY        => 'MILIB',
    STARTING_TIMESTAMP    => '2025-01-15 08:00:00',
    ENDING_TIMESTAMP      => '2025-01-15 17:00:00'
  )
) AS jrn
ORDER BY ENTRY_TIMESTAMP DESC
FETCH FIRST 100 ROWS ONLY;
SQL -- Analisis avanzado con CTEs
-- Dashboard completo de actividad semanal
WITH actividad AS (
  SELECT
    DATE(ENTRY_TIMESTAMP) AS FECHA,
    HOUR(ENTRY_TIMESTAMP) AS HORA,
    USER_NAME,
    OBJECT_NAME AS TABLA,
    JOURNAL_ENTRY_TYPE AS TIPO,
    COUNT(*) AS CANTIDAD
  FROM TABLE(
    QSYS2.DISPLAY_JOURNAL(
      'MILIB', 'QSQJRN',
      JOURNAL_CODES       => 'R',
      JOURNAL_ENTRY_TYPES => 'PT,UP,DL',
      STARTING_TIMESTAMP  => CURRENT_DATE - 7 DAYS
    )
  ) AS jrn
  GROUP BY DATE(ENTRY_TIMESTAMP), HOUR(ENTRY_TIMESTAMP),
           USER_NAME, OBJECT_NAME, JOURNAL_ENTRY_TYPE
)
SELECT FECHA, HORA, USER_NAME, TABLA,
  SUM(CASE WHEN TIPO = 'PT' THEN CANTIDAD ELSE 0 END) AS INSERTS,
  SUM(CASE WHEN TIPO = 'UP' THEN CANTIDAD ELSE 0 END) AS UPDATES,
  SUM(CASE WHEN TIPO = 'DL' THEN CANTIDAD ELSE 0 END) AS DELETES,
  SUM(CANTIDAD) AS TOTAL
FROM actividad
GROUP BY FECHA, HORA, USER_NAME, TABLA
ORDER BY FECHA DESC, HORA, TOTAL DESC;

Buenas practicas

1Siempre usar MNGRCV(*SYSTEM)

Permite que el sistema gestione automaticamente el swap de receivers. Evita la situacion critica de receiver lleno que bloquea las escrituras.

2IMAGES(*BOTH) para tablas criticas

Garantiza la posibilidad de reverse recovery. El costo extra en espacio se justifica con la capacidad de deshacer cambios erroneos.

3Un journal por biblioteca

Es la practica estandar. Usar el nombre QSQJRN para que las tablas DDL se journaleen automaticamente.

4Salvar receivers antes de eliminarlos

Si necesitas mantener historial para auditoria, salva los receivers detached a cinta o SAVF antes de hacer DLTJRNRCV.

5Monitorear tamano de receivers

Usa QSYS2.JOURNAL_RECEIVER_INFO para verificar que los receivers no crezcan sin control. Configura alertas de tamano.

6Probar recovery periodicamente

Valida que APYJRNCHG y RMVJRNCHG funcionan correctamente en un entorno de test. No esperes a una emergencia para probarlo.

CL -- Setup recomendado de produccion
/* Setup optimo de journaling para produccion */
CRTJRNRCV JRNRCV(PRODLIB/RCVR0001) +
          THRESHOLD(1000000) +
          TEXT('Receiver inicial produccion')

CRTJRN JRN(PRODLIB/QSQJRN) +
       JRNRCV(PRODLIB/RCVR0001) +
       MNGRCV(*SYSTEM) +
       DLTRCV(*NO) +
       RCVSIZOPT(*MAXOPT2) +
       MINENTDTA(*NONE) +
       TEXT('Journal produccion')

/* Journalear TODAS las tablas de la biblioteca */
STRJRNPF FILE(PRODLIB/*ALL) +
         JRN(PRODLIB/QSQJRN) +
         IMAGES(*BOTH) +
         OMTJRNE(*OPNCLO)