J'ai un problème étrange avec les gros fichiers et bash
. Voici le contexte:
- J'ai un gros fichier: 75G et 400 000 000+ lignes (c'est un fichier journal, mon mauvais, je l'ai laissé grandir).
- Les 10 premiers caractères de chaque ligne sont des horodatages au format AAAA-MM-JJ.
- Je veux diviser ce fichier: un fichier par jour.
J'ai essayé avec le script suivant qui n'a pas fonctionné. Ma question concerne ce script qui ne fonctionne pas, pas les solutions alternatives .
while read line; do
new_file=${line:0:10}_file.log
echo "$line" >> $new_file
done < file.log
Après le débogage, j'ai trouvé le problème dans la new_file
variable. Ce script:
while read line; do
new_file=${line:0:10}_file.log
echo $new_file
done < file.log | uniq -c
donne le résultat ci-dessous (je mets les x
es pour garder les données confidentielles, les autres caractères sont les vrais). Remarquez la dh
et les chaînes plus courtes:
...
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
...
Ce n'est pas un problème dans le format de mon fichier . Le script cut -c 1-10 file.log | uniq -c
ne donne que des horodatages valides. Fait intéressant, une partie de la sortie ci-dessus devient avec cut ... | uniq -c
:
3227382 2011-xx-x9
4474604 2011-xx-x0
5722027 2011-xx-x1
Nous pouvons voir qu'après le décompte uniq 4474604
, mon script initial a échoué.
Ai-je atteint une limite en bash que je ne connais pas, ai-je trouvé un bug dans bash (cela semble improbable), ou ai-je fait quelque chose de mal?
Mise à jour :
Le problème se produit après la lecture de 2G du fichier. Les coutures read
et la redirection n'aiment pas les fichiers plus gros que 2G. Mais toujours à la recherche d'une explication plus précise.
Update2 :
Cela ressemble définitivement à un bug. Il peut être reproduit avec:
yes "0123456789abcdefghijklmnopqrs" | head -n 100000000 > file
while read line; do file=${line:0:10}; echo $file; done < file | uniq -c
mais cela fonctionne bien comme solution de contournement (il semble que j'ai trouvé une utilisation utile de cat
):
cat file | while read line; do file=${line:0:10}; echo $file; done | uniq -c
Un bogue a été déposé sur GNU et Debian. Les versions concernées sont bash
4.1.5 sur Debian Squeeze 6.0.2 et 6.0.4.
echo ${BASH_VERSINFO[@]}
4 1 5 1 release x86_64-pc-linux-gnu
Update3:
Grâce à Andreas Schwab qui a réagi rapidement à mon rapport de bug, c'est le patch qui est la solution à ce mauvais comportement. Le fichier impacté est lib/sh/zread.c
comme Gilles l'a souligné plus tôt:
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 est utilisée pour conserver la valeur de retour de lseek
. Comme lseek
renvoie le décalage depuis le début du fichier, lorsqu'il est supérieur à 2 Go, la int
valeur est négative, ce qui entraîne l' if (r >= 0)
échec du test là où il aurait dû réussir.
read
instruction en bash.Réponses:
Vous avez trouvé un bug dans bash, en quelque sorte. C'est un bug connu avec un correctif connu.
Les programmes représentent un décalage dans un fichier sous forme de variable dans un type entier avec une taille finie. Autrefois, tout le monde utilisait
int
à peu près tout, et leint
type était limité à 32 bits, y compris le bit de signe, afin qu'il puisse stocker des valeurs de -2147483648 à 2147483647. De nos jours, il existe différents noms de type pour différentes choses , y comprisoff_t
pour un décalage dans un fichier.Par défaut,
off_t
est un type 32 bits sur une plate-forme 32 bits (autorisant jusqu'à 2 Go) et un type 64 bits sur une plate-forme 64 bits (autorisant jusqu'à 8EB). Cependant, il est courant de compiler des programmes avec l'option LARGEFILE, qui commute le typeoff_t
sur une largeur de 64 bits et rend le programme appelant des implémentations appropriées de fonctions telles quelseek
.Il semble que vous exécutiez bash sur une plate-forme 32 bits et que votre binaire bash ne soit pas compilé avec un support de fichiers volumineux. Maintenant, lorsque vous lisez une ligne à partir d'un fichier normal, bash utilise un tampon interne pour lire les caractères par lots pour des performances (pour plus de détails, voir la source dans
builtins/read.def
). Lorsque la ligne est terminée, bash appellelseek
pour rembobiner le décalage du fichier à la position de fin de ligne, au cas où un autre programme se soucierait de la position dans ce fichier. L'appel àlseek
se produit dans lazsyncfc
fonction danslib/sh/zread.c
.Je n'ai pas lu la source en détail, mais je présume que quelque chose ne se passe pas bien au point de transition lorsque le décalage absolu est négatif. Donc bash finit par lire les mauvais décalages lorsqu'il remplit son tampon, après avoir franchi la barre des 2 Go.
Si ma conclusion est fausse et que votre bash s'exécute en fait sur une plate-forme 64 bits ou est compilé avec un support de gros fichiers, c'est certainement un bogue. Veuillez le signaler à votre distribution ou en amont .
Un shell n'est de toute façon pas le bon outil pour traiter des fichiers aussi volumineux. Ça va être lent. Utilisez sed si possible, sinon awk.
la source
Je ne sais pas mal, mais c'est certainement compliqué. Si vos lignes d'entrée ressemblent à ceci:
Ensuite, il n'y a vraiment aucune raison à cela:
Vous faites beaucoup de travail de sous-chaîne pour vous retrouver avec quelque chose qui ressemble ... exactement à ce qu'il ressemble déjà dans le fichier. Que dis-tu de ça?
Cela prend juste les 10 premiers caractères de la ligne. Vous pouvez également vous dispenser de
bash
entièrement et simplement utiliserawk
:Cela saisit la date
$1
(la première colonne délimitée par des espaces dans chaque ligne) et l'utilise pour générer le nom de fichier.Notez qu'il est possible qu'il y ait de fausses lignes de journal dans vos fichiers. Autrement dit, le problème peut être lié à l'entrée, pas à votre script. Vous pouvez étendre la
awk
script pour signaler les fausses lignes comme ceci:Cela écrit les lignes correspondant
YYYY-MM-DD
à vos fichiers journaux et signale les lignes qui ne commencent pas par un horodatage sur stdout.la source
cut -c 1-10 file.log | uniq -c
me donne le résultat attendu. J'utilise${line:0:4}-${line:5:2}-${line:8:2}
parce que je mettrai le fichier dans un répertoire${line:0:4}/${line:5:2}/${line:8:2}
et j'ai simplifié le problème (je mettrai à jour l'énoncé du problème). Je saisawk
peut m'aider ici, mais j'ai rencontré d'autres problèmes en l'utilisant. Ce que je veux, c'est comprendre le problème etbash
ne pas trouver de solutions alternatives.cut
déclaration qui fonctionne. Comme je veux comparer des pommes avec des pommes, pas avec des oranges, je dois rendre les choses aussi similaires que possible.Cela ressemble à ce que vous voulez faire:
Le
close
empêche la table de fichiers ouverte de se remplir.la source