Bash scripting y archivos grandes (error): la entrada con la lectura incorporada desde una redirección da un resultado inesperado

16

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_filevariable. 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 xes para mantener la confidencialidad de los datos, otros caracteres son los reales). Observe las dhcadenas 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 -csolo 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 ready 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 bash4.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.ccomo 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 rvariable se utiliza para mantener el valor de retorno de lseek. Como lseekdevuelve el desplazamiento desde el comienzo del archivo, cuando supera los 2 GB, el intvalor es negativo, lo que hace que la prueba if (r >= 0)falle donde debería haber tenido éxito.

jfg956
fuente
1
¿Puedes replicar el problema con conjuntos más pequeños de datos de entrada? ¿Son siempre las mismas líneas de entrada las que provocan estos problemas?
Larsks
@larks: buena pregunta. El problema siempre comienza en la línea 13.520.918 (dos veces para las pruebas que hice). El tamaño del archivo antes de esta línea es 2.147.487.726. Parece que hay un límite de 32 bits aquí, pero no exactamente ya que estamos un poco por encima de 2 ^ 31 (2.147.483.648), pero justo en un límite de búfer 4K (2 ^ 31 + 4K = 2.147.487.744). Las líneas anteriores y siguientes son líneas normales de 100 a 200 caracteres.
jfg956
Probado en un segundo archivo (aproximadamente del mismo tamaño): el problema comienza en la línea n. ° 13.522.712 y el archivo tiene 2.147.498.679 bytes antes de esa línea. Parece apuntar en la dirección de un límite de la readdeclaración en bash.
jfg956

Respuestas:

13

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 intpara casi todo, y el inttipo 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 , incluso off_tpara un desplazamiento en un archivo.

De forma predeterminada, off_tes 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 tipo off_ta 64 bits de ancho y hace que el programa llame a implementaciones adecuadas de funciones como lseek.

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 llama lseekpara 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 a lseeksucede en la zsyncfcfunción en lib/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.

Gilles 'SO- deja de ser malvado'
fuente
1
Merci Gilles. Gran respuesta: completa, con suficiente información para comprender el problema incluso para personas sin experiencia en CS (32 bits ...). (las larsks también ayudan a preguntar sobre el número de línea, y debería reconocerse). Después de eso, también pensé en un problema de 32 bits y descargué la fuente, pero aún no estaba en este nivel de análisis. Merci encore, et bonne journée.
jfg956
4

No sé acerca del error, pero ciertamente es complicado. Si sus líneas de entrada se ven así:

YYYY-MM-DD some text ...

Entonces realmente no hay razón para esto:

new_file=${line:0:4}-${line:5:2}-${line:8:2}_file.log

Estás haciendo mucho trabajo de subcadenas para terminar con algo que se ve ... exactamente como se ve en el archivo. ¿Qué tal esto?

while read line; do
  new_file="${line:0:10}_file.log"
  echo "$line" >> $new_file
done

Eso solo toma los primeros 10 caracteres de la línea. También puede prescindir por bashcompleto y simplemente usar awk:

awk '{print > ($1 "_file.log")}' < file.log

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 awkscript para marcar líneas falsas como esta:

awk '
$1 ~ /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/ {
    print > ($1 "_file.log")
    next
}

{
    print "INVALID:", $0
}
'

Esto escribe líneas coincidentes YYYY-MM-DDcon sus archivos de registro y marca líneas que no comienzan con una marca de tiempo en stdout.

larsks
fuente
No hay líneas falsas en mi archivo: cut -c 1-10 file.log | uniq -cme 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é que awkpuede ayudarme aquí, pero encontré otros problemas al usarlo. Lo que quiero es entender el problema bash, no encontrar soluciones alternativas.
jfg956
Como dijiste ... si "simplificas" el problema en la pregunta, probablemente no obtendrás las respuestas que deseas. Todavía creo que resolver esto con bash no es realmente la forma correcta de procesar este tipo de datos, pero no hay razón para que no funcione.
Larsks
El problema simplificado da el resultado inesperado que presenté en la pregunta, por lo que no creo que sea una simplificación excesiva. Además, el problema simplificado da un resultado similar al de la cutdeclaración que funciona. Como quiero comparar manzanas con manzanas, no con naranjas, necesito hacer las cosas lo más similares posible.
jfg956
1
Te dejé una pregunta que podría ayudar a determinar dónde van las cosas mal ...
larsks
2

Parece que lo que quieres hacer es:

awk '
{  filename = substr($0, 0, 10) "_file.log";  # input format same as output format
   if (filename != lastfile) {
       close(lastfile);
       print 'finished writing to', lastfile;
   }
   print >> filename;
   lastfile=filename;
}' file.log

Esto closeevita que la tabla de archivos abiertos se llene.

Arcege
fuente
Gracias por la solución awk. Ya vengo con algo similar. Mi pregunta era entender la limitación de bash, no encontrar una solución alternativa.
jfg956