Pull to refresh

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

Java
В этом топике расскажу о маленьком, но эффективном способе передачи файлов пользователю из сервлета по 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. public static long transfer(File file, long position, long count,
  5. OutputStream out) throws IOException {
  6. FileChannel in = new FileInputStream(file).getChannel();
  7. try {
  8. long writed = in.transferTo(position, count, Channels
  9. .newChannel(out));
  10. return writed;
  11. } finally {
  12. in.close();
  13. }
  14. }
* 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. final boolean sendFileSupport = Boolean.TRUE.equals(request
  3. .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. // using Apache APR and/or NIO to transfer file
  5. response.setBufferSize(1 << 18);
  6. request.setAttribute(TOMCAT_SENDFILE_FILENAME, file.getCanonicalPath());
  7. request.setAttribute(TOMCAT_SENDFILE_START, Long.valueOf(0));
  8. 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).

Для дальнейшего изучения
Tags:javahttpfile downloadniotomcataprnative
Hubs: Java
Rating +15
Views 1.8k Add to bookmarks 18
Comments
Comments 15

Popular right now

Факультет Java-разработки
March 10, 2021180,000 ₽GeekBrains
Java Developer. Professional
March 11, 202160,000 ₽OTUS
Java QA Engineer
March 16, 202160,000 ₽OTUS
Программист Java
March 7, 202150,290 ₽Специалист.ру

Top of the last 24 hours