You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,并尝试不同缓冲区大小

优化建议

  1. 改用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();
  1. 调整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);
}
  1. 多线程并发分段下载
    单纯的分段获取如果没有结合多线程,无法充分利用带宽。将文件分成多个块,用独立线程并行下载后合并:
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);
  1. 替换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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 23:50:23