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.