Это работало нормально до версии Spring-Web 6.0.23.
После обновления до версии 6.1.4 мы постоянно сталкивались со следующей ошибкой OutOfMemoryError:
Код: Выделить всё
10:48:11,632 ERROR org.springframework.scheduling.support.TaskUtils$LoggingErrorHandler %NHET service - ourCloudUnawareScheduler-6 - Unexpected error occurred in scheduled task
java.lang.OutOfMemoryError: Java heap space
at org.springframework.util.FastByteArrayOutputStream.addBuffer(FastByteArrayOutputStream.java:325) ~[spring-core-6.1.4.jar:6.1.4]
at org.springframework.util.FastByteArrayOutputStream.write(FastByteArrayOutputStream.java:126) ~[spring-core-6.1.4.jar:6.1.4]
at org.apache.commons.io.output.ProxyOutputStream.write(ProxyOutputStream.java:92) ~[commons-io-2.11.0.jar:2.11.0]
at java.base/java.security.DigestOutputStream.write(DigestOutputStream.java:143) ~[?:?]
at java.base/java.util.zip.DeflaterOutputStream.deflate(DeflaterOutputStream.java:261) ~[?:?]
at java.base/java.util.zip.DeflaterOutputStream.write(DeflaterOutputStream.java:210) ~[?:?]
at java.base/java.util.zip.GZIPOutputStream.write(GZIPOutputStream.java:148) ~[?:?]
at org.apache.commons.compress.utils.CountingOutputStream.write(CountingOutputStream.java:62) ~[commons-compress-1.23.0.jar:1.23.0]
at org.apache.commons.compress.utils.FixedLengthBlockOutputStream$BufferAtATimeOutputChannel.write(FixedLengthBlockOutputStream.java:91) ~[commons-compress-1.23.0.jar:1.23.0]
at org.apache.commons.compress.utils.FixedLengthBlockOutputStream.writeBlock(FixedLengthBlockOutputStream.java:259) ~[commons-compress-1.23.0.jar:1.23.0]
at org.apache.commons.compress.utils.FixedLengthBlockOutputStream.maybeFlush(FixedLengthBlockOutputStream.java:169) ~[commons-compress-1.23.0.jar:1.23.0]
at org.apache.commons.compress.utils.FixedLengthBlockOutputStream.write(FixedLengthBlockOutputStream.java:206) ~[commons-compress-1.23.0.jar:1.23.0]
at org.apache.commons.compress.archivers.tar.TarArchiveOutputStream.write(TarArchiveOutputStream.java:713) ~[commons-compress-1.23.0.jar:1.23.0]
особенно в коммите 033bebf.
Любая версия Spring-Web, начиная с 6.1.x, использует FastByteArrayOutputStream при обработке запроса по адресу:
Код: Выделить всё
AbstractClientHttpRequest:getBody -> AbstractStreamingClientHttpRequest:getBodyInternal -> new FastByteArrayOutputStreamКод: Выделить всё
AbstractClientHttpRequest:getBody -> SimpleStreamingClientHttpRequest:getBodyInternal -> HttpURLConnection:getOutputStreamOOM в какой-то момент, когда запрос слишком велик.
Вопросы по теме
Был похожий вопрос, но он пошел в совершенно другом направлении:
Spring boot Пространство кучи Java для загрузки больших файлов
Reproducer
Чтобы показать этот OOM вживую, я создал воспроизводитель, чтобы каждый мог его испытать.
Я скопировал некоторые классы 6.0.23, чтобы можно было параллельно показывать как работающие, так и неработающие реализации.
В воспроизводителе я сократил сравниваемые потоки. в нашу собственную реализацию, поэтому он просто использует FileInputStream
для потоковой передачи некоторых ложных данных в OutputStream из тела запроса.
А принимающая конечная точка просто сливает поток.
Подробности можно найти в этом Java-классе.
Не забудьте ограничить доступную память, добавив -Xmx500m при запуске воспроизводитель.
Соответствующий код
Запуск потоковой передачи через RestTemplate работает одинаково для старого и нового.
В воспроизводителе экземпляр RestTemplate создается по-разному, поскольку мы хотим иметь как старый, так и новый шаблон в одном проекте.
Код: Выделить всё
@PostMapping("/startBroken")
public void startBroken() throws IOException {
Path sourcePath = createTestFile();
RequestCallback requestCallback =
request -> {
try (OutputStream os = request.getBody();
FileInputStream fis = new FileInputStream(sourcePath.toFile())) {
doStream(fis, os);
}
};
// The important difference is the version of the restTemplate
restClientBroken()
.execute("http://localhost:8080/putData", HttpMethod.PUT, requestCallback, null);
deleteTestFile(sourcePath);
}
private void doStream(InputStream inputStream, OutputStream outputStream) throws IOException {
byte[] buffer = new byte[65536];
int bytesRead;
long totalBytesProcessed = 0;
while ((bytesRead = inputStream.read(buffer)) != -1) {
totalBytesProcessed += bytesRead;
outputStream.write(buffer, 0, bytesRead);
System.out.println("Processed " + totalBytesProcessed + " bytes");
}
outputStream.flush();
}
Мы не изменили создание двух RestTemplates.
Но в воспроизводителе вы найдете restClientBroken и restClientWorking, где рабочий использует локальные копии старой веб-реализации Spring:
Код: Выделить всё
private RestTemplate restClientBroken() {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(2_000);
requestFactory.setReadTimeout(29_000);
requestFactory.setBufferRequestBody(false);
requestFactory.setChunkSize(4096);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(requestFactory);
return restTemplate;
}
Подойдет любой достаточно большой файл (с -Xmx500m достаточно файла размером 800 МБ):
Код: Выделить всё
private Path createTestFile() throws IOException {
Path path = Files.createTempFile("streaming-test-source-", ".tmp");
try (final BufferedWriter bufferedWriter = Files.newBufferedWriter(path)) {
for (int i = 0; i < 30_000; i++) {
int outerCount = i * 1000;
for (int j = 0; j < 1_000; j++) {
bufferedWriter.write("This is line " + outerCount + i + "\n");
}
bufferedWriter.flush();
}
}
return path;
}
В примере просто сливается поток для имитации потребителя:
Код: Выделить всё
@PutMapping("/putData")
public void receiver(HttpServletRequest request) throws IOException {
StreamUtils.drain(request.getInputStream());
}
Теперь мои вопросы:
- Пропустил ли я очевидную ошибку?
- Произошло ли концептуальное изменение? Ака, есть ли лучший способ потоковой передачи данных, который не использует FastByteArrayOutputStream?
- Или это ошибка, которую мне следует зарегистрировать в Spring-Framework
Подробнее здесь: https://stackoverflow.com/questions/798 ... 4-and-abov
Мобильная версия