Большие файлы и Sinatra

    Недавно столкнулся с интересной проблемой, когда попытка отдать большой файл через Sinatra::Helpers.send_file приводила к отжиранию всей оперативной памяти (типичный размер файла — 14Gb).

    Исследование показало, что Sinatra сама читает и отдает файл кусками по 512 байт, но web-сервер thin (а также WEBrick) буферизует вывод в оперативной памяти на своем уровне, что и приводит к таким печальным последствиям.

    Для решения проблемы достаточно оказалось перейти на web-сервер Rainbows (web-сервер, базирующийся на коде unicorn, но предназначенный для работы без проксирования, для медленных клиентов и/или сервисов). Но при отдаче больших файлов процесс кушал порядка 30% CPU на одном ядре.

    Rainbows позволяет оптимизировать отдачу файлов, используя, к примеру, гем sendfile, который предоставляет соответствующие API операционной системы. Но для этого необходимо, чтобы отдача файла шла через Rack::File API.

    В текущей master-ветке Sinatra метод send_file переписали, используя API Rack::File, поэтому мы можем просто бэкпортировать соответствующий функционал в существующие версии гема Sinatra:

    if Sinatra::VERSION < '1.3.0' && Rack.release >= '1.3'
      # Monkey patch old Sinatra to use Rack::File to serve files.
    
      Sinatra::Helpers.class_eval do
        # Got from Sinatra 1.3.0 sources
        def send_file(path, opts={})
          if opts[:type] or not response['Content-Type']
            content_type opts[:type] || File.extname(path), :default => 'application/octet-stream'
          end
    
          if opts[:disposition] == 'attachment' || opts[:filename]
            attachment opts[:filename] || path
          elsif opts[:disposition] == 'inline'
            response['Content-Disposition'] = 'inline'
          end
    
          last_modified opts[:last_modified] if opts[:last_modified]
    
          file      = Rack::File.new nil
          file.path = path
          result    = file.serving env
          result[1].each { |k,v| headers[k] ||= v }
          halt result[0], result[2]
        rescue Errno::ENOENT
          not_found
        end
      end
    end
    


    При этом файл конфигурации rainbows будет выглядеть примерно так:
    # try to use sendfile when available
    begin
      require 'sendfile'
    rescue LoadError
    end
    
    Rainbows! do
      use :ThreadSpawn
    end
    


    Теперь мы используем эффективную технику отдачи файлов, если в системе установлен гем rack версии 1.3 или выше, и установлен гем sendfile. Кстати, при использовании ruby 1.9 гем sendfile, скорее всего, не потребуется.

    P.S.: Если ваш сервис находится за прокси-сервером, то более оптимально использовать возможности, предоставляемые прокси-серверами, к примеру, API X-Accel-Redirect (nginx) или X-Sendfile (Lighttpd, Apache).
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 13
    • 0
      Не уверен, что когда-нибудь придется загружать 14Гб файлы, но спасибо.
      • 0
        Я тоже изначально такого не предполагал, но тут потребовалось давать ссылки на скачивание файлов логов, и кое-где у нас ротируемые файлы логов имеют размеры по 12-14Gb.
        • 0
          Логи, тесты, — это от лукавого, только время зря терять :)
        • 0
          Кстати, проблемы со скачиванием файлов начинаются где-то от 512Mb. Да и CPU грузится лишний раз.
        • +15
          Неделя Ruby?) Ну пожалуйста пусть это будет так, пусть это будет так, пусть это будет так.
          • +1
            non-stop!
            • 0
              Всеми конечностями присоединяюсь!
            • 0
              Советую чуть-чуть дальше пойти и в production просто выдавать HTTP заголовок X-Sendfile ( Apache2 ) или
              x-accel-redirect — для nginx. Прирост скорости ( и уменьние загрузки ), по моему опыту, просто сногсшибательные.
              • 0
                О подобном решении написано в постскриптуме. Для бэкэнд-решений часто не подходит.

                Во-первых, некоторые сервисы не должны зависеть от внешних пакетов, включая nginx. Ибо ставятся на самые различные машины.

                Во-вторых, если сервис использует sendfile, то уже не имеет значения, чью работу он делает, ибо работа перекладывается на операционную систему;

                В-третьих, если надо отдавать файлы из различных локаций, то nginx тут уже не спасет, ибо для него пришлось бы динамически генерировать и перечитывать конфигурацию (читаем документацию по X-Accel-Redirect).
              • 0
                А зачем выдавать такие большие файлы через Ruby? Чем ngnix не нравится, он как раз для раздачи статики разрабатывался.
                • +2
                  Ни чем не не не нравиться, просто всплыла вот такая лажа синатры в связке с webrick и отписали что-да как.
                  Неужели неинтересно? Не ngnix«ом же единым, какая бы няшка он не был.
                  • 0
                    Заметка интересная. Но я бы тоже не стал отдавать файл прямо из приложения. Лучше это поручить прокси-серверу. Не обязательно nginx. Но альтернатив я, честно говоря, не вижу.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.