En este trabajo intentaremos analizar las diferentes causas que pueden producir el error "ORA-01555:snapshot too old" relacionado con el número o tamaño de los segmentos de rollback de la base de datos.
Dicho error aparece documentado en el manual de errores de Oracle con el siguiente texto:
ORA-01555 snapshot too old: rollback segment number [string] with name "[string]" too small
Cause: Rollback records needed by a reader for consistent read are overwritten by other writers.
Action: Use larger rollback segments.
Los segmentos de rollback son un tipo de objeto que cumplen una serie de funciones dentro la base de datos:
- permiten deshacer transacciones total o parcialmente mediante las sentencias
rollback
; y rollback to <savepoint>;
- se emplean durante la recuperación de transacciones en caso de fallo de instancia: si cae la instancia de BD, durante el siguiente arranque se desharán automáticamente las transacciones no confirmadas. Transaciones no confirmadas son aquellas que pueden haber escrito sus modificaciones a disco durante un checkpoint anterior a la caída de la instancia de BD, pero que realmente no han confirmado los cambios y por tanto no deben perdurar en la BD.
- permite que se cumpla la consistencia en lectura entre transacciones: esta es la parte que puede producir el error de "copia demasiado antigua".
Cada usuario tiene una vista consistente de los datos, incluyendo los cambios realizados por las transacciones del propio usuario y las de los demás usuarios.
Esto significa que hay que mantener mecanismos de bloqueo y almacenamiento de valores antiguos hasta que finalizan las transacciones, asegurando que:
- Cada operación durante su ejecución ve los datos tal y como estaban al comenzar la ejecución.
- Una transacción escritora bloquea las filas que va a modificar y escribe los valores antiguos en el segmento de rollback que le corresponde, de forma que los procesos lectores que vayan a acceder a dichas filas puedan consultar esos valores antiguos del segmento de rollback. Hasta que la transacción no haga commit, las demás no pueden ver los nuevos valores.
Nota: la transacción escribe en rollback los valores antiguos y en el bloque de datos de la tabla los nuevos, creando además en dicho bloque de datos una entrada de transacción que indica dónde ir a leer los valores antiguos de la fila que está siendo modificada (una referencia a su segmento de rollback).
El problema de "imagen demasiado antigua" se debe a que los valores antiguos que se han creado en el segmento de rollback hacen falta para la consistencia de las operaciones lectoras, pero a veces mientras se está ejecutando la operación lectora el valor antiguo desaparece del segmento de rollback, por lo que no se cumple la consistencia de datos dentro de la propia operación lectora (pues no consigue todos los datos con valores referentes al mismo instante de tiempo), y por tanto se produce el error "ORA-01555: snapshot too old".
¿Por qué desaparece el valor antiguo del segmento de rollback? La respuesta está en la estructura y funcionamiento de este tipo de segmentos: la escritura en ellos es cíclica reutilizando el espacio que estaba siendo utilizado por las transacciones que ya hayan hecho commit o rollback.
Por tanto, puede ocurrir que una consulta muy larga precise que los datos de rollback se mantengan durante mucho tiempo para consistencia de datos dentro de la propia consulta, pero la transacción que los estaba actualizando hace commit; a partir de ese instante, cualquier otra transacción que tenga asignado ese mismo segmento de rollback podrá machacar los valores antiguos, por lo que la consulta no podrá obtenerlos de ningún otro sitio y se producirá el error.
Existe otro caso en el que se puede producir error, y es cuando quizá no ha desaparecido la información de rollback, pero sí ha desaparecido la entrada de transacción de la cabecera de rollback que indica dónde ir a leer los datos de rollback para una transacción.
El número de cambio del sistema o SCN es un valor que se incrementa automáticamente y no llega a reutilizarse nunca. El SCN identifica de forma única a una versión de la Base de Datos confirmada.
Cada vez que un usuario confirma una transacción, Oracle memoriza un nuevo SCN. Uno de los lugares donde se pueden consultar los SCN es en el fichero alert.log, y se pueden usar por ejemplo para hacer recuperaciones incompletas hasta un SCN determinado teniendo la base de datos en modo Archivelog.
Oracle utiliza los SCNs en los ficheros de control, cabeceras de ficheros de datos, ficheros de redo log, y en el algoritmo de consistencia en lectura.
El error ORA-01555 significa que ha fallado una lectura consistente (a través de rollback para consistencia en lectura) sobre un bloque de BD determinado.
Cuando comienza una transacción o consulta se memoriza el SCN actual. Este SCN sirve como marca de tiempo para la consulta o transacción, ya que debe ver una imagen consistente de la BD en ese instante de tiempo. A esa marca de tiempo se le denomina snapshot SCN.
Cada bloque involucrado en la consulta o transacción debe reflejar el estado de la BD en ese instante de tiempo (snapshot SCN). Si un bloque tiene que ser modificado, los cambios se aplicarán a la versión actual del bloque. Sin embargo, la consulta de las filas a ser modificadas debe basarse en una versión del bloque consistente con el snapshot SCN. La reconstrucción temporal de una versión del bloque consistente con el snapshot SCN se denomina lectura consistente (consistent get ).
Hay dos tipos de fallos en lecturas consistentes: fallo de rollback (rollback failure), y fallo de limpieza (cleanout failure).
Rollback Failure
Si después de que nuestra transacción o consulta haya comenzado se intenta leer un bloque que ha sido modificado por otra transacción confirmada posterior a nuestro snapshot SCN (es decir, nuestra transacción o consulta tiene un SCN menor al que aparece en dicho bloque), entonces se tiene que obtener una lectura consistente desde rollback para que nuestra operación en conjunto sea consistente, ya que no se pueden ver datos modificados y confirmados a mitad de consulta o a mitad de transacción (depende del nivel de aislamiento, como se explica más tarde en este artículo).
Sin embargo, si los cambios fueron realizados por una transacción discreta (ver Transacciones Discretas al final del artículo), no habrá información de rollback. Por lo que se producirá el error ORA-01555.
De la misma forma, se producirá el error si los bloques requeridos del segmento de rollback no están disponibles debido a que la extensión del segmento que los contenía ha sido desasignada por una operación de compresión (parámetro optimal del segmento de rollback) o reutilizada por otras transacciones ya existentes dentro del segmento o posteriores a la confirmada.
Para que se diera este último caso, información de rollback sobreescrita, se tendrían que haber utilizado al menos una vez desde el snapshot SCN todas las extensiones del segmento de rollback antes de sobreescribir los bloques de rollback necesarios. Por eso el mensaje de error Oracle sugiere que el segmento es demasiado pequeño (esto es como curiosidad).
Cleanout Failure
Cuando una transacción modifica filas de un bloque de datos, crea una entrada en el directorio de transacciones de la cabecera de dicho bloque, escribiendo su ID de transacción. Dicho ID contiene codificado el número de segmento de rollback asignado a la transacción, donde escribirá los valores antiguos. Esto quiere decir que dentro de cada bloque se indica dónde ir a leer los valores antiguos para consistencia en lectura.
Cuando la transacción haga commit, se incrementará el SCN del sistema y el nuevo valor será el commit SCN de esta transacción. Este commit SCN se guardará entonces en la entrada correspondiente de la tabla de transacciones, indicando que ya han sido confirmados los cambios. A partir de este momento, el rollback generado por esta transacción podrá ser sobreescrito por otras transacciones, así como su entrada en la tabla de transacciones del segmento de rollback.
Pero, para no afectar al rendimiento, Oracle no modificará todos los bloques involucrados para reflejar la confirmación. Esto se debe a que si la transacción afectaba a 1.000 bloques tendría que volver a visitarlos para poder modificarlos. Esta labor se deja para la siguiente transacción que visite el bloque, y es a lo que se denomina limpieza de bloque.
Por tanto, el directorio de transacciones que se guarda en la cabecera del bloque de datos, todavía reflejará una entrada para esa transacción como si estuviera actualizando el bloque y aún no se hubiera confirmado, y se mantienen los bloqueos a nivel de fila sobre las cabeceras de las filas afectadas.
Cuando el bloque sea leido por otra consulta o transacción ésta se encargará de hacer la limpieza de bloque, que consiste en comprobar si la transacción anterior se ha confirmado, y si es así se liberan los bloqueos sobre las filas y se guarda el commit SCN de la transacción en la entrada correspondiente del directorio de transacciones.
La nueva transacción cogerá el ID de la antigua que aparece en el directorio de transacciones, para comprobar si dicha transacción se ha confirmado o no. Extraerá el número de segmento de rollback codificado de dicho ID, y accederá a la entrada correspondiente en tabla de transacciones de la cabecera de dicho segmento.
Si en dicha entrada no se ha actualizado el commit SCN, significa que la antigua transacción aún no ha sido confirmada. Si, por el contrario, encuentra un valor para el commit SCN, la nueva transacción se encargará de hacer el block cleanout, liberando los bloqueos de las filas afectadas y eliminando del directorio la entrada de la transacción.
Si la transacción no ha sido confirmada o si fue confirmada después del snapshot SCN de la nueva transacción, hará falta leer de rollback como se explicó anteriormente. Pero si la transacción se confirmó antes del snapshot SCN de la nueva, no hará falta leer de rollback y la nueva transacción operará directamente sobre el bloque de datos.
Esto significa, que siempre se debe poder establecer la relación entre el snapshot SCN de la nueva transacción y el commit SCN de la transacción antigua.
La figura anterior representa el caso de encontrar la entrada de una transacción confirmada en la tabla de transacciones del segmento de rollback.
El problema surge si el bloque de cabecera del segmento de rollback ya no contiene la entrada para esa transacción, y por tanto el commit SCN de la antigua. Esto puede ocurrir ya que si este bloque es modificado con nuevas entradas de transacción y pueden ser sobreescritas las antiguas entradas.
En este caso, Oracle hará una lectura consistente recursiva del bloque de cabecera del segmento de rollback para obtener el commit SCN de la transacción. Es decir, Oracle intenta leer la información de rollback generada para la propia tabla de transacciones, de forma que obtenga la entrada con el commit SCN antes de haber sido sobreescrita.
Dicha información de rollback se almacena dentro del mismo segmento de rollback, y por tanto puede ser reutilizada o desasignada (durante compresiones) de la misma forma que el resto de la información de rollback.
Este caso es más improbable ya que tiene que coincidir que se reutilice la entrada de la cabecera (eso no es muy difícil, ya que las entradas de transacción se reutilizan cíclicamente) y que no esté disponible el rollback correspondiente a esta cabecera (se habrán reutilizado las extensiones del segmento de rollback varias veces, ya que las entradas de la tabla de transacciones se reutilizan cíclicamente y por tanto habrá pasado bastante tiempo). La siguiente figura muestra el caso de no encontrar la entrada de una transacción confirmada en la tabla de transacciones del segmento de rollback.
Los casos más comunes que pueden presentar un rollback failure o cleanout failure:
- Hacer fetch de un cursor abierto después de un commit, Oracle lo permite pero no es una operación ANSI standard. Se puede producir el error debido a que al abrir el cursor, éste adquiere el snapshot SCN de la consulta, y los commits irán generando nuevos commit SCNs.
- Segmento de rollback corrupto: debido a que ciertos bloques no pueden ser leidos, no permitirá realizar la consistencia en lectura.
- Segmentos de rollback muy pequeños: las entradas de rollback de una transacción confirmada se reutilizan demasiado rápido por nuevas transacciones.
- Pocos segmentos de rollback: existen muchas transacciones usando el mismo segmento de rollback, por lo que existe mayor probabilidad de reutilización.
Para reducir la probabilidad de error frente a estos casos se proponen las siguientes recomendaciones:
- No ejecutar transacciones discretas mientras se estén ejecutando consultas o transacciones que puedan verse perjudicadas, a menos que los dos conjuntos de datos sean totalmente independientes.
- Planificar en el tiempo consultas y transacciones largas, de forma que las lecturas consistentes no necesiten consultar datos de rollback desde el snapshot SCN.
- Codificar los procesos largos como módulos que se puedan ejecutar de nuevo independientemente de cualquier proceso anterior.
- Comprimir todos los segmentos de rollback a su tamaño óptimo manualmente antes de ejecutar una consulta o transacción que pueda obtener el error de rollback debido a desasignación de extensiones durante su ejecución. Esto se puede hacer a través del siguiente bloque PL/SQL (se deben tener privilegios de ejecución del paquete sys.dbms_sql):
declare
id_c integer;
sentencia varchar2(100);
cursor nombres_sr is select r.segment_name
from sys.dba_rollback_segs r
where r.status ='ONLINE';
begin
id_c := sys.dbms_sql.open_cursor;
for reg in nombres_sr loop
sentencia := 'alter rollback segment ' ||reg.segment_name|| ' shrink';
sys.dbms_sql.parse(id_c, sentencia, sys.dbms_sql.native);
end loop;
sys.dbms_sql.close_cursor(id_c);
end;
/
Utilizar un tamaño optimo más grande en todos los segmentos de rollback para retrasar la reutilización de extensiones. Se puede saber aproximadamente cuánto tiempo queda antes de que se produzca el error, con el siguiente script que puede indicar el tiempo medio antes de que se reutilice una extensión de segmento de rollback.
Para Oracle 8.0.x ó 8.1.x:
column comentario format a53
column comentario heading "tiempo medio antes de reutilización de extensión ..."
column horas format 9999999 heading "en horas"
column minutos heading "en minutos"
select null comentario,
trunc(24* i.startup_time) / v.ciclos) horas,
trunc(1440*(sysdate- i.startup_time) / v.ciclos) minutos
from sys.v_$instance i,
(select max((r.writes + 24 * r.gets) / -- bytes usados
nvl(least(r.optsize, r.rssize), r.rssize) * -- tamaño de segmento
(r.extents - 1) / r.extents -- reducido en 1 extensión
) ciclos
from sys.v_$rollstat r
where r.status = 'ONLINE' ) v ;
Esta consulta muestra cuántas veces han sido reutilizadas extensiones desde el arranque de la instancia de BD e informan del tiempo medio existente antes de que una extensión sea reutilizada. Da una idea del tiempo que le queda a una transacción o consulta antes de que se produzca el error.
Este resultado no tiene en cuenta las transacciones discretas, ya que no generan rollback, ni el hecho de que los segmentos se pongan online/offline modificando las estadísticas consultadas.
- Utilizar un tamaño de bloque Oracle de BD más grande, para que pueda haber más entradas de transacción en la tabla de transacciones del bloque de cabecera del segmento de rollback, retrasando de esta forma la reutilización de estas entradas.
- Hacer commits menos frecuentes en las tareas que se ejecuten al mismo tiempo que las consultas o transacciones donde se pueden producir problemas, especialmente en procedimientos PL/SQL, de forma que se reduzca la reutilización de entradas de transacción en las tablas de transacciones (cada vez que se hace commit estamos comenzando una nueva transacción, luego incrementamos el número total de transacciones en el sistema).
- Si es necesario, añadir segmentos de rollback extra para tener más entradas de transacción disponibles. Esta recomendación contradice la de usar un tamaño óptimo más grande, asumiendo que el espacio en disco disponible para segmentos de rollback es fijo. La elección de una estrategia u otra depende del riesgo de rollback failures con respecto a cleanout failures.
Representadas gráficamente:
Se observa que se han marcado dos de las soluciones propuestas - aumentar tamaño y número de segmentos de rollback - como complementarias. Vamos a explicar el por qué: · en el caso de un cleanout failure, aumentar el tamaño de los segmentos de rolback, ayudaría a que el rollback de la tabla de transacciones (para poder recuperar el commit SCN en caso de sobreescritura de la entrada) no sea sobreescrito tan rápidamente, ya que hay más espacio para las escrituras del resto de transacciones.
Sin embargo, resulta más adecuado aumentar el número de segmentos de rollback, ya que se consigue tener un número inferior de transacciones dentro de cada segmento de rollback, por lo que las entradas de transacción tardarán más en reutilizarse, resolviendo así el principal problema en el cleanout failure.
Por otro lado, aumentar el número de segmentos permite reducir una posible contención en el bloque de cabecera (tabla de transacciones) debido a que, como se ha indicado, existirán menos transacciones por segmento.
En el caso de un rollback failure, el problema se podría solucionar simplemente aumentando el tamaño de los segmentos de rollback para reducir el riesgo de una sobreescritura de la información requerida para la consistencia en lectura.
Las ventajas de aumentar el número de segmentos de rollback son una reducción de contención en el bloque de cabecera como se ha explicado anteriormente, y menor probabilidad de sobreescritura debido a una mejor distribución de transacciones por segmentos de rollback. Sin embargo, no evitaría el hecho de que una transacción larga reutilice el rollback de una transacción confirmada necesario para la consistencia en lectura, ya que aunque tengamos más segmentos, siguen siendo pequeños y habrá gran cantidad de sobreescrituras.
Como solución de compromiso, se puede hallar un equilibrio entre estas dos soluciones, obteniendo de esta forma las ventajas de ambas.
Para ciertas consultas y transacciones no es necesario hacer todo lo indicado anteriormente. Sólo se necesita prevenir la liberación o reutilización de extensiones necesarias para consistencia en lectura.
Un método sencillo es asegurarse de que haya un único segmento de rollback grande online en el momento del snapshot SCN, y forzar a que la consulta o transacción utilice ese segmento de rollback. Esto asegura que no se liberarán/reutilizarán extensiones hasta que finalice la consulta o transacción, ya que considera que todas las extensiones afectadas por la operación tienen una transacción activa y por tanto no puede liberarlas ni reutilizarlas.
Una variación de esta solución es dejar en cada segmento de rollback online una transacción sin confirmar, aunque aumenta el riesgo de quedarse sin espacio en los tablespaces donde se hayan creado los segmentos de rollback (esto es fácil de controlar).
A continuación se incluyen una serie de scripts para aplicar esta técnica en un sistema Unix:
El primer script a ejecutar crea una tabla en un hash cluster dentro del esquema del usuario System que se usará en los siguientes scripts para implementar y memorizar la protección de los segmentos de rollback frente a la liberación/reutilización de extensiones.
create cluster system.prevent_1555 (segment_name varchar2(30)) hashkeys 29;
create table system.protected_rollback_seg
(
segment_name varchar2(30),
protection_expires date
) cluster system.prevent_1555(segment_name);
Este es el script principal denominado prevent_1555.sql, se invoca para asegurar la protección frente al error ORA-01555 durante un número determinado de segundos. Genera un script unix que al ser ejecutado invoca a protect_rbs.sql, que se comentará posteriormente, para cada segmento de rollback online de la BD.
La forma de ejecutarlo es:
sqlplus -s / @prevent_1555
rbs_protectors=$(sh prevent_1555.sh)
kill $rbs_protectors
y su contenido es:
set define :
set verify off
set recsep off
set heading off
set feedback off
set termout off
set trimspool on
spool prevent_1555.sh
prompt rm -f prevent_1555.sh
select
'sqlplus / @protect_rbs ' || segment_name || ' :1 > /dev/null & echo $!'
from
sys.dba_rollback_segs
where
status = 'ONLINE' and
segment_name != 'SYSTEM' ;
spool off
exit
El script protect_rbs.sql que se incluye a continuación es ejecutado automáticamente para cada segmento de rollback online por el script unix prevent_1555.sh generado en el punto anterior. Se encarga de comprimir el segmento de rollback especificado para reducir el riesgo de quedarnos sin espacio en los tablespaces de rollback. Después memoriza su protección en la tabla de control, antes de dejar la transacción sin confirmar pendiente durante el número de segundos especificado. La invocación a este script tiene la siguiente sintaxis (no hay que ejecutarlo manualmente):
sqlplus -s / @protect_rbs &
alter rollback segment &1 shrink;
delete from system.protected_rollback_seg
where segment_name = '&1' and protection_expires < sysdate;
insert into system.protected_rollback_seg values ('&1', sysdate + &2/86400);
commit;
set transaction use rollback segment &1;
delete from system.protected_rollback_seg
where segment_name = '&1' and protection_expires <= sysdate + &2/86400;
execute sys.dbms_lock.sleep(&2);
commit;
exit
El script protected_rollback_segs.sql que se incluye a continuación monitoriza el estado de protección de los segmentos de rollback cuando han sido protegidos mediante el script prevent_1555.sql:
select substr(d.segment_name, 1, 14) "SEGMENT NAME", v.extents, v.rssize bytes,
v.optsize "OPTIMAL", v.xacts transactions,
substr(to_char(p.protection_expires,'HH24:MI:SS DD/MM/YY'),1,18)"Protegido Hasta"
from sys.dba_rollback_segs d, sys.v_$rollstat v, system.protected_rbs p
where d.segment_id = v.usn and d.segment_name = p.segment_name (+)
and sysdate < p.protection_expires (+) ;