Как я могу передавать несколько файлов одновременно, используя HTTP::Server?

Я работаю над HTTP-сервисом, который обслуживает большие файлы. Я заметил, что параллельные загрузки невозможны. Процесс обслуживает только один файл за раз, и все остальные загрузки ожидают до завершения предыдущих загрузок. Как я могу передавать несколько файлов одновременно?

    require "http/server"

    server = HTTP::Server.new(3000) do |context|
      context.response.content_type = "application/data"
      f = File.open "bigfile.bin", "r"
      IO.copy f, context.response.output
    end

    puts "Listening on http://127.0.0.1:3000"
    server.listen

Запросить один файл за раз:

    $ ab -n 10 -c 1 127.0.0.1:3000/

    [...]
    Percentage of the requests served within a certain time (ms)
     50%      9
     66%      9
     75%      9
     80%      9
     90%      9
     95%      9
     98%      9
     99%      9
    100%      9 (longest request)

Запросите 10 файлов одновременно:

    $ ab -n 10 -c 10 127.0.0.1:3000/

    [...]
    Percentage of the requests served within a certain time (ms)
     50%     52
     66%     57
     75%     64
     80%     69
     90%     73
     95%     73
     98%     73
     99%     73
    100%     73 (longest request)

1 ответ

Решение

Проблема здесь в том, что оба File#read а также context.response.output никогда не заблокирует Модель параллелизма Crystal основана на совместно запланированных волокнах, где переключение волокон происходит только при блокировке ввода-вывода. Чтение с диска с использованием неблокирующего ввода-вывода невозможно, что означает, что единственная часть, которую можно заблокировать, это запись в context.response.output, Однако дисковый ввод-вывод намного медленнее сетевого ввода-вывода на одном и том же компьютере, а это означает, что запись никогда не будет блокироваться, поскольку ab читает со скоростью, намного превышающей скорость, с которой диск может предоставлять данные, даже из дискового кэша. Этот пример - практически идеальный шторм для разрушения параллелизма кристалла.

В реальном мире гораздо более вероятно, что клиенты службы будут находиться в сети с компьютера, что иногда приводит к блокировке записи ответа. Кроме того, если вы читаете из другого сетевого сервиса или канала / сокета, вы также заблокируете. Другое решение состоит в том, чтобы использовать пул потоков для реализации неблокирующего ввода-вывода файла, что делает libuv. В качестве дополнительного примечания, Crystal перешел на libevent, потому что libuv не допускает многопоточный цикл обработки событий (т.е. любой поток возобновляет любое волокно).

призвание Fiber.yield передать выполнение любому ожидающему волокну - это правильное решение. Вот пример того, как блокировать (и давать) при чтении файлов:

    def copy_in_chunks(input, output, chunk_size = 4096)
      size = 1
      while size > 0
        size = IO.copy(input, output, chunk_size)
        Fiber.yield
      end
    end

    File.open("bigfile.bin", "r") do |file|
      copy_in_chunks(file, context.response)
    end

Это транскрипция обсуждения здесь: https://github.com/crystal-lang/crystal/issues/4628

Реквизиты для пользователей GitHub @cschlack, @RX14 и @ysbaddaden

Другие вопросы по тегам