или как избежать нехватки памяти

ВСТУПЛЕНИЕ

На этой неделе мы с моей командой столкнулись с проблемой, которую я впервые прочитал, когда учился в колледже. Я совершенно забыл с этой среды октября: переносить очень большой файл по HTTP.

Требования и дизайн

Наш заказчик заменил свою CRM облачной, и мы попросили нас интегрировать ее со всей картой программного обеспечения.

Один поток интеграции отправляет документы в CRM из локального хранилища и связывает их с сохраненными учетными записями клиентов; размер файла не имеет верхней границы, и мы приняли 1 ГБ как среднее значение.

Все интеграции CRM основаны на REST, без общих папок, без промежуточной БД, разрешены только защищенные REST API OAUTH1.

Я набросал для вас упрощенную архитектурную модель на картинке ниже.

Наше приложение, как и другие части карты приложений, работает в локальной среде, в то время как CRM размещается в облачном клиенте.

Открытый API принимает тело Multipart с двумя частями: одна с документом JSON, содержащим метаданные, такие как имя файла, идентификатор учетной записи клиента и т. Д., А другая - двоичное содержимое файла.

Стандартное решение

Приложение состоит из двух частей: первая - опросчик файлов, который создает поток каждый раз, когда новый файл появляется в промежуточной папке, и вторая, которая связывает его с учетной записью клиента и отправляет в CRM.

Если вы заинтересованы в создании опросчика файлов, я связываю вас с Apache Camel Polling Consumer, это отличное решение, чтобы сделать это легко.

Я хочу поговорить с вами о том, как мы отправляем файлы в CRM.

Приступим к кодированию. вот выдержка из out pom.xml:

<dependency>
   <groupId>org.apache.httpcomponents</groupId>      <artifactId>httpclient</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-core</artifactId>
</dependency>

Вот наш стандартный код решения

RestTemplate remoteService = new RestTemplate();
//HTTP has two parts: Header and Body
//Here is the header:
HttpHeader header = new HttpHeader();
//Here is the body
MultiValueMap<String,Object>  bodyMap =  new MultiValueMap<String,Object>();
bodyMap.add(“customer_file”,new FileSystemResource(fileName));
bodyMap.add(“customer_name”, customerJSON);
HttpEntity<MultiValueMap> request = new HttpEntity(bodyMap, header);
ResponseEntity<String> restResponse = remoteService .exchange(remoteServiceURL, HttpMethod.POST, request, String);

переменная customerJson - это javax.json.JsonObject; таким образом составной запрос автоматически выбирает правильный тип контента, и такое же поведение ожидается при использовании экземпляра org.springframework.core.io.FileSystemResource.

Мы сделали эти тесты:

  1. отправить небольшой файл для поиска некорректных запросов
  2. отправить огромный файл, подтверждающий надежность нашего приложения

Мы не встретили ничего важного в этом документе с тестом 1, отсутствием некоторого значения заголовка, неправильным форматированием ввода URL и т. Д.
Тест 2 заставил нас ждать пару минут, после чего весь кошмар Java-разработчиков явился

java.lang.OutOfMemoryError: Java heap space

Проблема возникла не только потому, что мы запускали код в среде разработки, но и потому, что приложение пыталось загрузить все содержимое файла в ОЗУ, что сделало его более требовательным к памяти, чем J. Веллингтон Вимпи .

Это было ясно при анализе объема памяти, используемого приложением, sic et simpliciter.

Напомним, что это было не лучшим решением и с архитектурной точки зрения, потому что:

  • мы не можем предполагать максимальный размер для входящих файлов
  • мы не можем последовательно работать с файлами

Нам нужно было его улучшить.

Разделенное решение

Нам нужно было научить наш код не загружать все содержимое файла в память, а использовать функцию, которую HTTP1.1 поддерживает до тех пор, пока я учился в колледже: кодирование фрагментированной передачи.

Эта функция сообщает серверу, что входящий запрос состоит из нескольких HTTP-сообщений, и ему необходимо получить их все, чтобы начать обработку.

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

Если вы хотите узнать больше о том, как HTTP реализует кодирование фрагментированной передачи, следуйте инструкциям WIKI, W3C.

Мы улучшили наш класс, правильно настроив RestTemplate:

RestTemplate remoteService = new RestTemplate();
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setBufferRequestBody(false); remoteService.setRequestFactory(requestFactory);

Мы повторили тест 2, и на этот раз все прошло хорошо.

Мы наблюдали, что объем памяти составляет менее 300 МБ для полной передачи файла размером 1,5 ГБ! Успех!

Выводы и пожелания

В этой статье я описал найденное нами решение для передачи файла большого размера; вы можете найти несколько других, используя разные библиотеки.

Я хотел бы добавить, что эта функция поставляется только с HTTP1.1 и что HTTP 2 больше не поддерживает кодирование передачи по частям; Я думаю, вам нужно искать какой-то потоковый API.

Здесь мы решили использовать хорошо известный класс RestTemplate вместо более нового WebClient: я не могу сказать вам, можете ли вы адаптироваться к нему.

В самом конце я хочу поблагодарить Луку и Давиде за время, потраченное на работу над полным решением, которое послужило вдохновением для этой статьи, и, конечно же, за весь наш смех каждый день.