AWS S3에서 메모리 효율적인 파일 스트리밍

12734 단어 rubys3railsstreams
previous post에서 처리를 위해 데이터베이스 행을 효율적으로 스트리밍하는 방법에 대해 설명했습니다. 이번에는 파일의 라인을 처리하기 위해 동일한 것을 구현하는 방법에 대해 이야기하고 싶습니다. 여기서도 동일한 원칙을 사용할 것이므로 해당 게시물을 읽어 보시기 바랍니다.

대용량 파일을 처리해야 하는 경우 메모리가 부족할 수 있으므로 파일을 메모리에 완전히 로드하지 않는 것이 좋습니다. 파일을 효율적으로 읽는 방법을 아는 것은 모든 프로그래밍 언어에서 필수적인 기술이며 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
    


    두 구현 모두 작동하지만 제어 흐름에 예외를 사용하는 것을 피하는 것을 선호합니다.

    마지막으로 줄 바꿈 문자가 없는 파일과 함께 이 구현을 사용하면 전체 파일을 메모리에 다운로드하게 된다는 점에 주목할 가치가 있습니다(이는 우리가 피하고 싶었던 것입니다). 이러한 경우 이 게시물의 범위를 벗어나는 다른 기술을 고려해야 합니다.

    결론



    이것은 일반 파일에는 적합하지 않을 수 있지만(다운로드를 여러 번 요청하기 때문에) 특정 사용 사례, 예를 들어 디스크 공간이 없거나 매우 적은 경우 또는 가지고 있는 파일이 작업할 파일이 매우 크고 그 동안 처리 작업자가 유휴 상태인 동안 다운로드에 너무 많은 시간이 걸리거나 실패합니다.

    좋은 웹페이지 즐겨찾기