или как избежать нехватки памяти
ВСТУПЛЕНИЕ
На этой неделе мы с моей командой столкнулись с проблемой, которую я впервые прочитал, когда учился в колледже. Я совершенно забыл с этой среды октября: переносить очень большой файл по 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, отсутствием некоторого значения заголовка, неправильным форматированием ввода 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: я не могу сказать вам, можете ли вы адаптироваться к нему.
В самом конце я хочу поблагодарить Луку и Давиде за время, потраченное на работу над полным решением, которое послужило вдохновением для этой статьи, и, конечно же, за весь наш смех каждый день.