AWS S3에서 메모리 효율적인 파일 스트리밍
대용량 파일을 처리해야 하는 경우 메모리가 부족할 수 있으므로 파일을 메모리에 완전히 로드하지 않는 것이 좋습니다. 파일을 효율적으로 읽는 방법을 아는 것은 모든 프로그래밍 언어에서 필수적인 기술이며 Ruby에서 코드를 보여주겠지만 이 게시물의 아이디어를 좋아하는 프로그래밍 언어로 옮길 수 있기를 바랍니다.
This post은 Ruby에서 파일 스트리밍이라는 주제를 이미 잘 다루고 있으므로 더 흥미롭게 만들기 위해 AWS S3에 저장된 파일을 완전히 다운로드하지 않고 한 줄씩 스트리밍하는 방법을 보여 드리겠습니다. 물론 대부분의 경우 전체 파일을 다운받아서 처리하는 것이 좋겠지만, 사용할 수 있는 디스크 용량에 제한이 있거나, 전체 파일을 다운로드하여 처리합니다.
포스트 제목은 우리가 파일을 다운받을 것이기 때문에 조금 거짓말을 하지만, 아이디어는 그것의 작은 부분을 다운로드하고 처리 후 폐기하는 것입니다. 따라서 가장 먼저 알아야 할 것은 S3에서 파일을 다운로드하는 방법입니다. 다행스럽게도 their documentation에 따라 URI 매개변수를 사용하여 다운로드할 수 있습니다. 직접 요청하지 않고 대신 공식S3 gem을 사용하겠습니다. AWS 설명서에는 파일 범위가 RFC 2616 사양을 준수해야 한다고 지정되어 있으므로 파일의 처음 100바이트를 가져오려면 다음과 같이 해야 합니다.
require 'aws-sdk-s3'
def build_range(range_start, range_end)
"bytes=#{range_start}-#{range_end}"
end
object = Aws::S3::Resource.new.bucket('some-bucket').object('some-file')
object.get(range: build_range(0, 100)).body.read
엄청난! 이것으로 우리는 다운로드된 바이트를 저장하고 그것으로 작업하는 메모리에 버퍼를 가질 수 있습니다. 구현을 시작하기 전에 발생할 수 있는 가능한 상황을 고려하고 무엇을 해야 하는지 생각해 봅시다.
이제 해야 할 일에 대한 아이디어를 얻었으므로 구현을 보여 드리겠습니다.
class RemoteFileIterator
include Enumerable
KILOBYTE = 1024
MEGABYTE = 1024 * KILOBYTE
def initialize(file_name, new_line_character: "\n", batch_size: MEGABYTE, bucket:)
@object = Aws::S3::Resource.new.bucket(bucket).object(file_name)
@content_length = @object.content_length
@new_line_character = new_line_character
@batch_size = batch_size
reset_cursors!
end
def each
if block_given?
while (line = read_buffer_or_fetch!)
yield line
end
reset_cursors!
else
to_enum(:each)
end
end
private
def reset_cursors!
@cursor = 0
@buffer = nil
end
def read_buffer_or_fetch!
if buffer_has_new_line_character?
take_line_from_buffer!
elsif @cursor < @content_length
@buffer = (@buffer || '') + @object.get(range: build_range!).body.read
read_buffer_or_fetch!
end
end
def buffer_has_new_line_character?
!buffers_new_line_character_idx.nil?
end
def buffers_new_line_character_idx
@buffer&.index(@new_line_character)
end
def take_line_from_buffer!
new_line_character_idx = buffers_new_line_character_idx
line = @buffer[0..(new_line_character_idx - @new_line_character.size)]
@buffer = @buffer[(new_line_character_idx + @new_line_character.size)..-1]
@buffer = nil if @buffer.empty? # Doing str[str.size..-1] returns ''
line
end
def build_range!
range_start = @cursor
range_end = @cursor + @batch_size - 1
@cursor = range_end > @content_length ? @content_length : (range_end + 1)
"bytes=#{range_start}-#{range_end}"
end
end
보시다시피 이전 게시물의 클래스와 매우 유사합니다(이 게시물에서는 Enumerable 트릭을 다시 설명하지 않겠습니다).
build_range!
메서드는 이 게시물의 시작 부분에서 보여드린 것과 거의 동일하지만 커서를 추적합니다.take_line_from_buffer!
메서드에서 먼저 첫 줄 바꿈 문자의 위치를 찾은 다음 버퍼를 두 부분으로 분할하여 첫 번째 부분을 반환하고 마지막 부분을 버퍼에 보관합니다.read_buffer_or_fetch!
메서드는 새 줄을 찾거나 모든 파일을 다운로드할 때까지 재귀적으로 데이터를 가져옵니다. ifcursor < content_length
를 확인하지 않고 작성하는 다른 방법이 있습니다. def each
...
while (line = next_line!)
yield line
end
...
end
def next_line!
read_buffer_or_fetch
rescue Aws::S3::Errors::InvalidRange => _e
# Since we don't check if we ask for bytes outside the length
# we can take advantage of the exception thrown by the AWS gem
final_line = @buffer
@buffer = nil
final_line
end
def read_buffer_or_fetch
if buffer_has_new_line_character?
take_line_from_buffer!
else # Removed the condition
...
end
end
두 구현 모두 작동하지만 제어 흐름에 예외를 사용하는 것을 피하는 것을 선호합니다.
마지막으로 줄 바꿈 문자가 없는 파일과 함께 이 구현을 사용하면 전체 파일을 메모리에 다운로드하게 된다는 점에 주목할 가치가 있습니다(이는 우리가 피하고 싶었던 것입니다). 이러한 경우 이 게시물의 범위를 벗어나는 다른 기술을 고려해야 합니다.
결론
이것은 일반 파일에는 적합하지 않을 수 있지만(다운로드를 여러 번 요청하기 때문에) 특정 사용 사례, 예를 들어 디스크 공간이 없거나 매우 적은 경우 또는 가지고 있는 파일이 작업할 파일이 매우 크고 그 동안 처리 작업자가 유휴 상태인 동안 다운로드에 너무 많은 시간이 걸리거나 실패합니다.
Reference
이 문제에 관하여(AWS S3에서 메모리 효율적인 파일 스트리밍), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/alebian/memory-efficient-file-streaming-from-aws-s3-1jnh텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)