Java下载S3预签名URL速度过慢,如何提升下载性能?
Java S3预签名URL下载速度优化问题
我有一个Java应用,需要将S3预签名URL的内容下载到磁盘文件中,但遇到了下载速度过慢的问题。为排除磁盘I/O、网络等环境变量影响,我在配置完全相同(内存、区域、临时存储大小一致)的AWS Lambda上进行了对比实验:
- Lambda运行时:Java 17 / NodeJS 18
- Lambda架构:x86_64
- Lambda操作系统:Amazon Linux 2
Java代码
我使用Java非阻塞NIO库进行下载,核心代码如下:
URL url = new URL("MY_S3_PRESIGNED_URL"); URLConnection urlConnection = url.openConnection(); try (final InputStream is = urlConnection.getInputStream(); final FileOutputStream os = new FileOutputStream(file)) { os.getChannel().transferFrom(Channels.newChannel(is), 0, Long.MAX_VALUE); }
NodeJS代码
NodeJS的下载代码如下:
async function getObject() { let url = "MY_S3_PRESIGNED_URL" return new Promise(async (resolve, reject) => { let responseDataChunks = []; https.get(url, response => { response.once('error', err => reject(err)); response.on('data', chunk => responseDataChunks.push(chunk)); response.once('end', () => resolve('done')); }) }) }
实验结果
调用两个Lambda下载同一700MB文件的S3预签名URL,平均速度结果:
- Java:36秒
- NodeJS:8秒
问题
我尝试过按Baeldung文章的建议使用Apache Commons IO或NIO库,但下载速度没有改善。如何优化Java的下载速度?
已尝试方案
我试过多种方法提升Java下载速度,但均无显著效果:
- 使用分段获取(ranged gets)
- 使用Apache Commons IO的
FileUtils.copyURLToFile - 使用BufferedInputStream替代当前InputStream,并尝试不同缓冲区大小
优化建议
- 改用Java 11+ HttpClient(支持HTTP/2)
默认URLConnection对HTTP/2的支持有限,而NodeJS的https.get通常默认启用HTTP/2。Java 11引入的HttpClient原生支持HTTP/2,能大幅提升传输效率:
HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("MY_S3_PRESIGNED_URL")) .build(); client.sendAsync(request, HttpResponse.BodyHandlers.ofFile(file.toPath())) .join();
- 调整Socket缓冲区大小
默认Socket读写缓冲区过小会限制传输速率,可手动设置:
URLConnection conn = url.openConnection(); conn.setReadTimeout(30000); conn.setConnectTimeout(10000); if (conn instanceof HttpURLConnection httpConn) { httpConn.getInputStream().close(); Socket socket = httpConn.getSocket(); // 设置为1MB缓冲区,可根据实际调整 socket.setReceiveBufferSize(1024 * 1024); socket.setSendBufferSize(1024 * 1024); }
- 多线程并发分段下载
单纯的分段获取如果没有结合多线程,无法充分利用带宽。将文件分成多个块,用独立线程并行下载后合并:
int numThreads = 3; long fileSize = urlConnection.getContentLengthLong(); long chunkSize = fileSize / numThreads; ExecutorService executor = Executors.newFixedThreadPool(numThreads); for (int i = 0; i < numThreads; i++) { long start = i * chunkSize; long end = (i == numThreads - 1) ? fileSize - 1 : (i + 1) * chunkSize - 1; executor.submit(() -> { try { URLConnection conn = url.openConnection(); conn.setRequestProperty("Range", "bytes=" + start + "-" + end); try (InputStream is = conn.getInputStream(); RandomAccessFile raf = new RandomAccessFile(file, "rw")) { raf.seek(start); byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = is.read(buffer)) != -1) { raf.write(buffer, 0, bytesRead); } } } catch (IOException e) { e.printStackTrace(); } }); } executor.shutdown(); executor.awaitTermination(1, TimeUnit.MINUTES);
- 替换NIO transferFrom为缓冲流批量读写
transferFrom处理网络输入流通道时,可能会退化为逐字节复制,无法真正利用零拷贝优势。改用大缓冲区的缓冲流反而更高效:
// 用16KB缓冲区,可根据Lambda内存调整更大值 int bufferSize = 16 * 1024; try (BufferedInputStream bis = new BufferedInputStream(urlConnection.getInputStream(), bufferSize); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file), bufferSize)) { byte[] buffer = new byte[bufferSize]; int bytesRead; while ((bytesRead = bis.read(buffer)) != -1) { bos.write(buffer, 0, bytesRead); } }
内容的提问来源于stack exchange,提问作者Luca01
相关产品推荐
相关产品推荐

