Bash 스크립팅 및 대용량 파일 (버그) : 리디렉션에서 기본 제공되는 읽기로 입력하면 예기치 않은 결과가 발생합니다.


16

큰 파일과에 이상한 문제가 있습니다 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

결과를 다음과 같이 제공합니다 ( x데이터를 기밀로 유지하기 위해 es를 넣었습니다. 다른 문자는 실제 문자입니다). 통지 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

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

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와 데비안에 버그가 제기되었습니다. 영향을받는 버전은 bashDebian Squeeze 6.0.2 및 6.0.4에서 4.1.5입니다.

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

업데이트 3 :

내 버그 보고서에 빠르게 반응 한 Andreas Schwab 덕분에이 오류는이 오작동에 대한 해결책입니다. 영향을받는 파일은 lib/sh/zread.cGilles가 더 빨리 지적한대로입니다.

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. 으로 lseek되돌아가 파일의 선두로부터의 오프셋 (offset)가 2기가바이트 위에있을 때,의 int값은 시험, 이로 인해 음 if (r >= 0)이 성공해야 어디 실패합니다.


1
더 작은 입력 데이터 세트로 문제를 복제 할 수 있습니까? 이러한 문제를 일으키는 입력 선이 항상 같은가요?
Larsks

@larks : 좋은 질문입니다. 문제는 항상 라인 # 13.520.918에서 시작합니다 (실제로 테스트 한 두 번). 이 줄 이전의 파일 크기는 2.147.487.726입니다. 여기에는 32 비트 제한이 있지만 2 ^ 31 (2.147.483.648)을 약간 넘지 않지만 4K 버퍼 제한 (2 ^ 31 + 4K = 2.147.487.744)만큼 정확합니다. 이전 및 다음 줄은 보통 100 ~ 200 자입니다.
jfg956

두 번째 파일 (약 같은 크기)에서 테스트되었습니다. 문제는 줄 # 13.522.712에서 시작하고 파일은 해당 줄보다 큰 2.147.498.679 바이트입니다. readbash 의 문장 한계 방향을 가리 키도록 솔기가 있습니다.
jfg956

답변:


13

bash에서 일종의 버그를 발견했습니다. 알려진 수정으로 알려진 버그입니다.

프로그램은 파일의 오프셋을 유한 크기의 정수 유형의 변수로 나타냅니다. 예전에는, 모두가 사용하는 int거의 모든 것을, 그리고 int그것이 오늘날이 다른 -2147483648에서 2147483647 값을 저장할 수 있도록, 부호 비트를 포함하여 유형은 32 비트로 제한되었다 다른 것들에 대한 유형 이름이 포함 off_t에 대한 파일에서 오프셋.

기본적 off_t으로 32 비트 플랫폼 (32GB) (최대 2GB 허용), 64 비트 플랫폼 (64MB) (최대 8EB 허용)은 32 비트 유형입니다. 그러나 LARGEFILE 옵션을 사용하여 프로그램을 컴파일하는 것이 일반적입니다.이 옵션은 유형 off_t을 64 비트 폭으로 전환하고 프로그램이와 같은 함수의 적절한 구현을 호출하게합니다 lseek.

32 비트 플랫폼에서 bash를 실행 중이며 bash 바이너리가 큰 파일 지원으로 컴파일되지 않은 것 같습니다. 이제 일반 파일에서 줄을 읽을 때 bash는 내부 버퍼를 사용하여 성능을 위해 문자를 일괄 적으로 읽습니다 (자세한 내용은의 소스 참조 builtins/read.def). 행이 완료되면 bash는 lseek다른 프로그램이 해당 파일의 위치를 ​​염두에 둔 경우 파일 오프셋을 행 끝의 위치로 되감기 위해 다시 호출 합니다. 의 호출 lseek은의 zsyncfc함수에서 발생합니다 lib/sh/zread.c.

소스를 자세히 읽지 않았지만 절대 오프셋이 음수 일 때 전환 지점에서 무언가가 원활하게 일어나지 않을 것이라고 생각합니다. 따라서 bash는 2GB 마크를 통과 한 후 버퍼를 리필 할 때 잘못된 오프셋에서 읽습니다.

내 결론이 잘못되어 bash가 실제로 64 비트 플랫폼에서 실행되거나 큰 파일 지원으로 컴파일 된 경우 분명히 버그입니다. 배포 또는 업스트림에 신고하십시오 .

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


1
Merci Gilles. 훌륭한 답변 : 강력한 CS 배경이없는 사람들 (32 비트 ...)에게도 문제를 이해하기에 충분한 정보를 제공합니다. (larsk는 또한 줄 번호에 대한 질문에 도움이되며 인정해야합니다.) 그 후, 나는 32 비트 문제에도 불구하고 소스를 다운로드했지만 아직이 수준의 분석에는 도달하지 않았습니다. Merci encore, et Bonne journée.
jfg956

4

나는 틀렸다는 것을 알지 못하지만 확실히 복잡합니다. 입력 행이 다음과 같은 경우 :

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로그 파일 과 일치 하는 행을 작성 하고 stdout에서 시간 소인으로 시작하지 않는 행을 플래그합니다.


내 파일에 가짜 줄이 없습니다 cut -c 1-10 file.log | uniq -c. 예상 결과를 얻습니다. ${line:0:4}-${line:5:2}-${line:8:2}파일을 directory에 넣을 때 사용 ${line:0:4}/${line:5:2}/${line:8:2}하고 있으며 문제를 단순화했습니다 (문제 설명을 업데이트합니다). 나는 awk여기에서 나를 도울 수 있지만 그것을 사용하는 다른 문제가 발생했습니다. 내가 원하는 것은의 문제를 이해하고 bash다른 해결책을 찾지 못하는 것입니다.
jfg956

당신이 말했듯이 ... 질문에서 문제를 "단순화"하면 원하는 대답을 얻지 못할 것입니다. 여전히 bash로 이것을 해결하는 것이 실제로 이런 종류의 데이터를 처리하는 올바른 방법은 아니지만 작동하지 않아야 할 이유는 없다고 생각합니다.
Larsks

단순화 된 문제는 질문에 제시 된 예기치 않은 결과를 제공하므로 지나치게 단순화 된 것으로 생각하지 않습니다. 또한 단순화 된 문제는 cut작동 하는 진술 과 비슷한 결과를 제공 합니다. 오렌지가 아닌 사과와 사과를 비교하려면 가능한 한 비슷한 것을 만들어야합니다.
jfg956

1
나는 어디에서 문제가 발생하는지 알아내는 데 도움이되는 질문을 남겼습니다.
larsks

2

원하는 것은 다음과 같습니다.

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채우고에서 열린 파일 테이블을 유지합니다.


awk 솔루션에 감사드립니다. 나는 이미 비슷한 것을 가지고 있습니다. 내 질문은 대체 솔루션을 찾지 않고 bash 제한을 이해하는 것이 었습니다.
jfg956
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.