Tengo un problema extraño con archivos grandes y bash
. Este es el contexto:
- Tengo un archivo grande: 75G y más de 400,000,000 líneas (es un archivo de registro, lo malo, lo dejé crecer).
- Los primeros 10 caracteres de cada línea son marcas de tiempo en el formato AAAA-MM-DD.
- Quiero dividir ese archivo: un archivo por día.
Intenté con el siguiente script que no funcionó. Mi pregunta es sobre este script que no funciona, no soluciones alternativas .
while read line; do
new_file=${line:0:10}_file.log
echo "$line" >> $new_file
done < file.log
Después de la depuración, encontré el problema en la new_file
variable. Este guión:
while read line; do
new_file=${line:0:10}_file.log
echo $new_file
done < file.log | uniq -c
da el resultado a continuación (pongo los x
es para mantener la confidencialidad de los datos, otros caracteres son los reales). Observe las dh
cadenas más cortas y:
...
27402 2011-xx-x4
27262 2011-xx-x5
22514 2011-xx-x6
17908 2011-xx-x7
...
3227382 2011-xx-x9
4474604 2011-xx-x0
1557680 2011-xx-x1
1 2011-xx-x2
3 2011-xx-x1
...
12 2011-xx-x1
1 2011-xx-dh
1 2011-xx-x1
1 208--
1 2011-xx-x1
1 2011-xx-dh
1 2011-xx-x1
...
No es un problema en el formato de mi archivo . El guión cut -c 1-10 file.log | uniq -c
solo proporciona marcas de tiempo válidas. Curiosamente, una parte de la salida anterior se convierte en cut ... | uniq -c
:
3227382 2011-xx-x9
4474604 2011-xx-x0
5722027 2011-xx-x1
Podemos ver que después del conteo uniq 4474604
, mi script inicial falló.
¿Llegué a un límite en bash que no conozco, encontré un error en bash (parece improbable) o hice algo mal?
Actualización :
El problema ocurre después de leer 2G del archivo. Las costuras read
y la redirección no les gustan los archivos más grandes que 2G. Pero aún buscando una explicación más precisa.
Actualización2 :
Definitivamente parece un error. Se puede reproducir con:
yes "0123456789abcdefghijklmnopqrs" | head -n 100000000 > file
while read line; do file=${line:0:10}; echo $file; done < file | uniq -c
pero esto funciona bien como una solución alternativa (parece que encontré un uso útil cat
):
cat file | while read line; do file=${line:0:10}; echo $file; done | uniq -c
Se ha archivado un error en GNU y Debian. Las versiones afectadas son bash
4.1.5 en Debian Squeeze 6.0.2 y 6.0.4.
echo ${BASH_VERSINFO[@]}
4 1 5 1 release x86_64-pc-linux-gnu
Actualización3:
Gracias a Andreas Schwab, que reaccionó rápidamente a mi informe de error, este es el parche que es la solución a este mal comportamiento. El archivo afectado es lib/sh/zread.c
como Gilles señaló antes:
diff --git a/lib/sh/zread.c b/lib/sh/zread.c index 0fd1199..3731a41 100644
--- a/lib/sh/zread.c
+++ b/lib/sh/zread.c @@ -161,7 +161,7 @@ zsyncfd (fd)
int fd; { off_t off;
- int r;
+ off_t r;
off = lused - lind; r = 0;
La r
variable se utiliza para mantener el valor de retorno de lseek
. Como lseek
devuelve el desplazamiento desde el comienzo del archivo, cuando supera los 2 GB, el int
valor es negativo, lo que hace que la prueba if (r >= 0)
falle donde debería haber tenido éxito.
read
declaración en bash.Respuestas:
Has encontrado un error en bash, de algún tipo. Es un error conocido con una solución conocida.
Los programas representan un desplazamiento en un archivo como una variable en algún tipo de entero con un tamaño finito. En los viejos tiempos, todos usaban
int
para casi todo, y elint
tipo se limitaba a 32 bits, incluido el bit de signo, por lo que podía almacenar valores de -2147483648 a 2147483647. Hoy en día hay diferentes nombres de tipos para diferentes cosas , inclusooff_t
para un desplazamiento en un archivo.De forma predeterminada,
off_t
es un tipo de 32 bits en una plataforma de 32 bits (que permite hasta 2 GB) y un tipo de 64 bits en una plataforma de 64 bits (que permite hasta 8EB). Sin embargo, es común compilar programas con la opción LARGEFILE, que cambia el tipooff_t
a 64 bits de ancho y hace que el programa llame a implementaciones adecuadas de funciones comolseek
.Parece que está ejecutando bash en una plataforma de 32 bits y su binario bash no está compilado con soporte para archivos grandes. Ahora, cuando lee una línea de un archivo normal, bash usa un búfer interno para leer los caracteres en lotes para el rendimiento (para más detalles, consulte la fuente en
builtins/read.def
). Cuando se completa la línea, bash llamalseek
para rebobinar el desplazamiento del archivo a la posición del final de la línea, en caso de que algún otro programa se preocupe por la posición en ese archivo. La llamada alseek
sucede en lazsyncfc
función enlib/sh/zread.c
.No he leído la fuente con mucho detalle, pero supongo que algo no está sucediendo sin problemas en el punto de transición cuando el desplazamiento absoluto es negativo. Entonces bash termina leyendo en los desplazamientos incorrectos cuando rellena su búfer, después de pasar la marca de 2GB.
Si mi conclusión es incorrecta y su bash se está ejecutando en una plataforma de 64 bits o está compilada con soporte para archivos grandes, eso definitivamente es un error. Por favor repórtelo a su distribución o contracorriente .
Un shell no es la herramienta adecuada para procesar archivos tan grandes de todos modos. Va a ser lento Use sed si es posible, de lo contrario awk.
fuente
No sé acerca del error, pero ciertamente es complicado. Si sus líneas de entrada se ven así:
Entonces realmente no hay razón para esto:
Estás haciendo mucho trabajo de subcadenas para terminar con algo que se ve ... exactamente como se ve en el archivo. ¿Qué tal esto?
Eso solo toma los primeros 10 caracteres de la línea. También puede prescindir por
bash
completo y simplemente usarawk
:Esto toma la fecha en
$1
(la primera columna delimitada por espacios en blanco en cada línea) y la usa para generar el nombre de archivo.Tenga en cuenta que es posible que haya algunas líneas de registro falsas en sus archivos. Es decir, el problema puede estar en la entrada, no en el script. Puede extender el
awk
script para marcar líneas falsas como esta:Esto escribe líneas coincidentes
YYYY-MM-DD
con sus archivos de registro y marca líneas que no comienzan con una marca de tiempo en stdout.fuente
cut -c 1-10 file.log | uniq -c
me da el resultado esperado. Estoy usando${line:0:4}-${line:5:2}-${line:8:2}
porque pondré el archivo en un directorio${line:0:4}/${line:5:2}/${line:8:2}
y simplifiqué el problema (actualizaré la declaración del problema). Sé queawk
puede ayudarme aquí, pero encontré otros problemas al usarlo. Lo que quiero es entender el problemabash
, no encontrar soluciones alternativas.cut
declaración que funciona. Como quiero comparar manzanas con manzanas, no con naranjas, necesito hacer las cosas lo más similares posible.Parece que lo que quieres hacer es:
Esto
close
evita que la tabla de archivos abiertos se llene.fuente