Использование apr_socket_sendfile() из сервлетов под Tomcat

    В этом топике расскажу о маленьком, но эффективном способе передачи файлов пользователю из сервлета по HTTP протоколу. Используется:
    • Apache Tomcat
    • Apache Portable Runtime Library
    • Apache Tomcat Native Library
    • Ваш сервлет, которому нужно отдавать файлы пользователю

    Конечно, отдавать файлы сервлетом не очень хорошо с точки зрения производительности. Во-первых, отдавать статичный контент лучше всего вообще без всяких скриптов. Но иногда без этого не обойтись. Во-вторых, отдача данных сводится, чаще всего, к чему-то подобному:
    1.     long writed = 0;
    2.     byte[] buffer = new byte[BUFFER_LENGTH];
    3.     int readed = in.read(buffer, 0, BUFFER_LENGTH);
    4.     while (readed != -1) {
    5.       out.write(buffer, 0, readed);
    6.       writed += readed;
    7.       readed = in.read(buffer, 0, BUFFER_LENGTH);
    8.     }
    * This source code was highlighted with Source Code Highlighter.

    После прочтения книжек по NIO и воспользовавшись микроскопом можно это переделать в чуть более эффективное средство:
    1.   public static long transfer(File file, OutputStream out) throws IOException {
    2.     return transfer(file, 0, file.length(), out);
    3.   }
    4.  
    5.   public static long transfer(File file, long position, long count,
    6.       OutputStream out) throws IOException {
    7.     FileChannel in = new FileInputStream(file).getChannel();
    8.     try {
    9.       long writed = in.transferTo(position, count, Channels
    10.           .newChannel(out));
    11.       return writed;
    12.     } finally {
    13.       in.close();
    14.     }
    15.   }
    * This source code was highlighted with Source Code Highlighter.

    Однако те, кто внимательно изучал stack trace'ы своего сервера, знает, что OutputStream у Tomcat не поддерживает передачу с помощью каналов, и всё сводится к первому примеру, но уже в недрах JVM.

    Очевидные недостатки данного подхода:
    • Производительность. Код, написанный на Java в данном случае будет, очевидно, медленее, чем native-код, если бы он умел копировать напрямую из файла в OutputStream
    • Использование памяти. Разработчикам трудно удержаться и не обернуть каждый из стримов в пару-другую Buffered(Input|Output)Stream. Получается, что каждый кусок файла поочерёдно побывает в трёх-четырёх местах нашего ОЗУ (вспомните ещё дисковый кеш операционной системы и, скорее всего, некоторый кеш TCP/IP)
    • Код активно использует ресурсы процессора по копированию кусочков данных туда-сюда

    Однако Apache Tomcat позволяет сервлетам использовать (через удобный интерфейс) функцию apr_socket_sendfile из библиотеки Apache Portable Runtime. Эта функция принимает на вход указатель на сокет, на файл, а также параменты старта и длины передаваемых данных (передавать можно не только файл целиком). Доступ к данной функциональности делается через использование атрибутов запроса (HttpServletRequest). Проверить наличие данной функциональности:
    1.   private static final String TOMCAT_SENDFILE_SUPPORT = "org.apache.tomcat.sendfile.support";
    2.  
    3.   final boolean sendFileSupport = Boolean.TRUE.equals(request
    4.         .getAttribute(TOMCAT_SENDFILE_SUPPORT));
    * This source code was highlighted with Source Code Highlighter.


    Теперь, если:
    1. sendFileSupport == true
    2. Файл не будет удалён сразу после выполнения кода
    3. Размер файла меньше 2 Гб

    То можно вместо самостоятельной передачи файла поручить это Apache Tomcat:
    1. private static final String TOMCAT_SENDFILE_FILENAME = "org.apache.tomcat.sendfile.filename";
    2. private static final String TOMCAT_SENDFILE_START = "org.apache.tomcat.sendfile.start";
    3. private static final String TOMCAT_SENDFILE_END = "org.apache.tomcat.sendfile.end";
    4.  
    5. // using Apache APR and/or NIO to transfer file
    6. response.setBufferSize(1 << 18);
    7. request.setAttribute(TOMCAT_SENDFILE_FILENAME, file.getCanonicalPath());
    8. request.setAttribute(TOMCAT_SENDFILE_START, Long.valueOf(0));
    9. request.setAttribute(TOMCAT_SENDFILE_END, Long.valueOf(fileLength));
    * This source code was highlighted with Source Code Highlighter.

    Второе ограничение связано с тем, что процесс передачи файла будет начат уже после того, как мы закончим работу в сервлете. Третье — с чем не ясно, но, возможно, с тем, что у меня 32-битная JVM и 32-битная Gentoo на тестовой машине (не захотел Tomcat отдавать файл больше 2 Гб сам).

    В результате:
    • Количество рабочих Java-потоков сервера снизилось в два-три раза, так как файлы теперь передаются в отдельных native-тредах
    • Загрузка процессора уменьшилась, так как APR использует функции операционной системы, чтобы оптимизировать передачу файлов
    • В «куче» остаётся меньше «мусора», что улучшает работу Garbage Collector'а

    Разумеется, для production системы нужно не только уметь отдавать файл целиком, но и по частям, а также учитывать возможность того, что файл уже есть у пользователя (обрабатывать NotModifiedSince).

    Для дальнейшего изучения
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 15

      +1
      стандартный томкат комплектуется по умолчанию APR? или нужно допиливать?
        0
        Забыл написать, действительно.

        В систему нужно дополнительно установить:
        — Apache Portable Runtime Library
        — Apache Tomcat Native Library

        При этом линуксоиды должны обратить внимание, что APR для инициализации во время запуска пытается набрать энтропии из /dev/random, поэтому либо его надо переключить на /dev/urandom, либо настроить /dev/random так, чтобы энтропии хватало для запуска сервера.
          0
          спасибо, очень пригодится, не всегда имеет смысл ставить nginx
        0
        Если честно, не очень понимаю необходимость отдавать статику томкатом, если есть nginx и X-Accel-Redirect.
        wiki.nginx.org/NginxXSendfile
          0
          ИМХО, это тоже самое :)

          Статика будет отдана функцией операционной системы sendfile. APR просто предоставляет оболочку для этой функции, которую можно вызвать из сервлета (либо реализует её сам, если такой функции в ОС нет).
            0
            да, но в случае с томкатом, это будет нативный вызов через кучу врапперов, что не есть гуд
              0
              Мне кажется, обработкой займётся не Tomcat-Java, а Tomcat Native Library, то есть будет без лишних wrapper'ов.

              Так как если TNL есть в ClassPath, то обработка идёт HTTP -> TNL -> Java -> TNL -> HTTP
              (TNL создаёт свой Thread Pool для Java)
                0
                Nginx пользует предварительно выделенную память, пользует epoll/kqueue, в конце концов да, также может использовать sendfile, является FSM заточенным под отдачу статики… Зачем же занимать Thread Pool томката на отдачу статики, если с этим и так прекрасно справляется nginx?.. Мне кажется вполне логичным четко распределять отдачу static и dynamic контента в системе…
            0
            Что насчет ограничения доступа к файлу уровнем приложения (файлы из закрытой авторизацией зоны)?..
            0
            А ещё можно воспользоваться библиотекой UploadFile от того же Apache. Получится очень неплохой сервлет, сам недавно кодил.
              0
              pardon, FileUpload.
                0
                One more pardon, перепутал upload с download.
                0
                А с NIO-коннектором к tomcat transferTo тоже не через sendfile работает?
                  0
                  Хм, забавно никто не обратил внимания на самую вопиющую проблему кода, описанного в начале поста — файл будет отдаваться без поддержки докачки.
                    0
                    В данном случае это не принципиально. Ну изменятся стартовые и конечные значения, но основная нагрузка всё равно будет на цикл.

                    В докачке самое сложное — это распарсить range'ы и корректно их обработать. На это уходит больше строчек кода, чем на саму отдачу :)

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

                  Самое читаемое