Bash 스크립트 및 대용량 파일(버그): 리디렉션에 내장된 읽기 기능을 사용하여 입력하면 예기치 않은 결과가 발생함

Bash 스크립트 및 대용량 파일(버그): 리디렉션에 내장된 읽기 기능을 사용하여 입력하면 예기치 않은 결과가 발생함

저는 대용량 파일을 작업 중입니다 bash. 내용은 다음과 같습니다.

  • 나는 큰 파일을 가지고 있습니다: 75G와 400,000,000줄이 넘습니다(로그 파일입니다. 안타깝지만 커지게 두었습니다).
  • 각 줄의 처음 10자는 YYYY-MM-DD 형식의 타임스탬프입니다.
  • 파일을 분할하고 싶습니다. 하루에 한 파일씩.

다음 스크립트를 사용해 보았지만 작동하지 않습니다. 내 문제는 대체 솔루션이 아니라 이 스크립트가 작동하지 않는다는 것입니다..

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

디버깅 후 문제가 new_file변수에 있다는 것을 발견했습니다. 이 스크립트는 다음과 같습니다.

while read line; do
  new_file=${line:0:10}_file.log
  echo $new_file
done < file.log | uniq -c

다음 결과를 제공합니다(데이터를 비공개로 유지하기 위해 es를 입력했으며 x다른 문자는 실제입니다). 참고 dh및 짧은 문자열:

...
  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    
...

내 파일 형식이 문제가 아닙니다. 스크립트는 cut -c 1-10 file.log | uniq -c유효한 타임스탬프만 제공합니다. 흥미롭게도 위 출력의 일부는 다음과 같습니다 cut ... | uniq -c.

3227382 2011-xx-x9
4474604 2011-xx-x0
5722027 2011-xx-x1

4474604uniq count 후에 초기 스크립트가 실패했음을 알 수 있습니다 .

bash에서 내가 인식하지 못하는 한계에 도달했습니까? bash에서 버그를 발견했습니까(가능성이 낮음). 아니면 뭔가 잘못하고 있습니까?

고쳐 쓰다:

2G 파일을 읽은 후 문제가 발생했습니다. 이음새 read와 리디렉션은 2G보다 큰 파일을 좋아하지 않습니다. 그러나 더 정확한 설명은 여전히 ​​​​탐색되고 있습니다.

업데이트 2:

버그처럼 보입니다. 다음과 같이 재현할 수 있습니다.

yes "0123456789abcdefghijklmnopqrs" | head -n 100000000 > file
while read line; do file=${line:0:10}; echo $file; done < file | uniq -c

그러나 이것은 해결 방법으로도 잘 작동합니다(내가 찾은 유용한 사용법인 것 같습니다 cat).

cat file | while read line; do file=${line:0:10}; echo $file; done | uniq -c 

GNU와 Debian에 버그가 접수되었습니다. 영향을 받는 버전은 bash6.0.4의 Debian Squeeze 6.0.2 및 4.1.5입니다.

echo ${BASH_VERSINFO[@]}
4 1 5 1 release x86_64-pc-linux-gnu

업데이트 3:

내 버그 보고서에 신속하게 응답한 Andreas Schwab에게 감사드립니다. 이 패치는 이러한 부적절한 동작을 해결합니다. 영향을 받는 파일은 다음과 같습니다.lib/sh/zread.cGiles가 앞서 지적했듯이:

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;

r변수는 반환 값을 저장하는 데 사용됩니다 lseek. as는 lseek파일 시작 부분에서 오프셋을 반환합니다. 오프셋은 2GB를 초과하면 음수이므로 성공해야 할 곳 int에서 테스트가 실패하게 됩니다 .if (r >= 0)

답변1

Bash에서 일종의 버그를 발견했습니다. 이는 알려진 버그이며 수정되었습니다.

프로그램은 파일의 오프셋을 유한한 크기의 정수 유형 변수로 나타냅니다. 예전에는 int거의 모든 사람이 사용했는데, int그 종류가 부호 비트를 포함해 32비트로 제한되어 있어서 -2147483648부터 2147483647까지의 값을 저장할 수 있었다. 이제는 다른다양한 항목의 이름을 입력하세요., off_t파일의 오프셋을 포함합니다.

기본적으로 off_t32비트 플랫폼에서는 32비트 유형(최대 허용 2GB)이고, 64비트 플랫폼에서는 64비트 유형(최대 허용 8EB)입니다. 그러나 유형을 off_t64비트 너비로 전환하고 프로그램이 적절한 함수 구현을 호출하도록 하는 LARGEFILE 옵션을 사용하여 프로그램을 컴파일하는 것이 일반적입니다.lseek.

32비트 플랫폼에서 bash를 실행하고 있고 bash 바이너리가 대용량 파일 지원으로 컴파일되지 않은 것 같습니다. 이제 일반 파일에서 한 줄을 읽을 때 bash는 내부 버퍼를 사용하여 문자를 일괄적으로 읽어 성능을 향상시킵니다. (자세한 내용은 소스 코드 참조)builtins/read.def). 줄이 완료되면 bash 호출은 lseek다른 프로그램이 파일의 위치에 관심을 두는 경우를 대비하여 파일 오프셋을 줄 끝으로 되감습니다. 함수 lseek에서 호출이 발생합니다.zsyncfclib/sh/zread.c.

소스코드를 자세히 읽어보지는 못했지만, 절대 오프셋이 음수일 때 전환점에서 뭔가 원활하게 일어나지 않는 것이 아닌가 추측하고 있습니다. 따라서 bash가 2GB 표시를 통과한 후 버퍼를 다시 채우면 결국 잘못된 오프셋을 읽게 됩니다.

내 결론이 틀렸고 여러분의 bash가 실제로 64비트 플랫폼에서 실행 중이거나 대용량 파일 지원으로 컴파일된 경우 이는 확실히 버그입니다. 이 사실을 배포판에 보고하거나상류.

어쨌든, 쉘은 이러한 대용량 파일을 처리하는 데 적합한 도구가 아닙니다. 매우 느릴 것입니다. 가능하면 sed를 사용하고, 그렇지 않으면 awk를 사용하십시오.

답변2

무엇이 문제인지는 모르겠지만 정말 복잡합니다. 입력 라인이 다음과 같은 경우:

YYYY-MM-DD some text ...

글쎄요, 그럴 이유가 전혀 없습니다:

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

이미 파일에 보이는 것과 똑같이 보이는 결과를 얻기 위해 많은 하위 문자열 작업을 수행하고 있습니다. 이건 어때?

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

이것은 줄의 처음 10자를 가져옵니다. 완전히 포기 bash하고 다음을 사용할 수도 있습니다 awk.

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

$1이는 날짜(각 행에서 공백으로 구분된 첫 번째 열)를 가져와 이를 사용하여 파일 이름을 생성합니다.

파일에 가짜 로그 줄이 있을 수 있습니다. 즉, 문제는 스크립트가 아니라 입력에 있을 수 있습니다. awk다음과 같이 잘못된 줄을 표시하도록 스크립트를 확장할 수 있습니다 .

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
}
'

이는 YYYY-MM-DD로그 파일과 일치하는 행을 작성하고 표준 출력에서 ​​타임스탬프로 시작하지 않는 행을 표시합니다.

답변3

당신이하고 싶은 일은 다음과 같습니다 :

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

이렇게 하면 close열린 파일 테이블이 가득 차는 것을 방지할 수 있습니다.

관련 정보