如何在 Spring Boot 中高效流式转发大型文件(避免内存溢出)

本文介绍在 spring boot 构建的 ingress 服务中,不落盘、不缓存、直接流式转发 storage 服务响应给客户端的最佳实践,彻底规避 outofmemoryerror 并显著提升大文件传输性能。

在典型的微服务架构中,Ingress(网关)服务常需作为代理,将客户端对大文件(如视频、备份包、日志归档等)的请求,透明地转发至后端 Storage 服务,并将响应流式透传回客户端。若采用“先下载保存为临时文件 → 再读取响应”的方式(如问题中所述),不仅 I/O 开销巨大、延迟高,还极易因并发请求导致磁盘空间耗尽或内存堆积(尤其当 InputStream 未及时关闭或缓冲区过大时)。

推荐方案:使用 WebClient 实现非阻塞、响应式流式代理

Spring Boot 2.0+ 原生支持响应式编程,org.springframework.web.reactive.function.client.WebClient 是最佳选择——它基于 Netty,天然支持异步流式处理,可将 Storage 的响应体(Flux)直接映射为客户端响应体,全程零内存缓冲、零临时文件:

@RestController
public class FileProxyController {

    private final WebClient storageClient;

    public FileProxyController(@Value("${storage.base-url}") String storageBaseUrl) {
        this.storageClient = WebClient.builder()
                .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(-1)) // 禁用内存缓冲限制(由 DataBufferUtils 控制流)
                .build();
    }

    @GetMapping("/files/{id}")
    public ResponseEntity> proxyFile(
            @PathVariable String id,
            ServerHttpRequest request,
            ServerHttpResponse response) {

        String storageUrl = storageBaseUrl + "/files/" + id;

        // 复制关键请求头(如 Authorization、Range 等)
        HttpHeaders headers = new HttpHeaders();
        request.getHeaders().entrySet().stream()
                .filter(entry -> !entry.getKey().toLowerCase().startsWith("host"))
                .forEach(entry -> headers.put(entry.getKey(), entry.getValue()));

        return storageClient.get()
                .uri(storageUrl)
                .headers(h -> h.addAll(headers))
                .exchangeToMono(clientResponse -> {
                    // 复制 Storage 响应头(Content-Type, Content-Length, Accept-Ranges 等)
                    response.getHeaders().putAll(clientResponse.headers().asHttpHeaders());
                    // 设置状态码
                    response.setStatusCode(clientResponse.statusCode());

                    // 直接返回响应体流(自动处理背压、分块传输)
                    return Mono.just(ResponseEntity.ok()
                            .headers(response.getHeaders())
                            .body(clientResponse.body(BodyExtractors.toDataBuffers())));
                })
                .block(); // ⚠️ 注意:此处仅作示意;生产环境应保持完全响应式链路!
    }
}

但更优写法(全响应式、无阻塞):

@GetMapping(value = "/files/{id}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public Mono>> proxyFileReactive(
        @PathVariable String id,
        ServerHttpRequest request) {

    String storageUrl = storageBaseUrl + "/files/" + id;

    return storageClient.get()
            .uri(storageUrl)
            .headers(h -> copyRelevantHeaders(request.getHeaders(), h))
            .exchangeToMono(clientResponse -> {
                HttpHeaders respHeaders = clientResponse.headers().asHttpHeaders();
                // 关键:显式设置 Content-Transfer-Encoding 或确保 Transfer-Encoding: chunked 自动生效
                respHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                return Mono.just(ResponseEntity.status(clientResponse.statusCode())
                        .headers(respHeade

rs) .body(clientResponse.body(BodyExtractors.toDataBuffers()))); }); } private void copyRelevantHeaders(HttpHeaders src, HttpHeaders dest) { src.entrySet().stream() .filter(e -> !e.getKey().equalsIgnoreCase("host")) .forEach(e -> dest.put(e.getKey(), e.getValue())); }

关键要点与注意事项:

  • 零内存缓冲:BodyExtractors.toDataBuffers() 返回 Flux,配合 Netty 的 PooledDataBuffer,数据从网络套接字直通客户端 Socket,不经过 JVM 堆内存缓冲;
  • 自动背压支持:Reactor 的 Flux 天然支持下游消费速率控制(如客户端网络慢时自动降速),避免 OOM;
  • Range 请求支持(断点续传):需确保 Storage 服务正确响应 206 Partial Content,并在代理中透传 Accept-Ranges, Content-Range 等头部;
  • ⚠️ 禁用 @EnableWebMvc:确保应用运行在 WebFlux 模式(而非 Spring MVC),否则 WebClient 响应式流会被强制阻塞转换;
  • ⚠️ 超时配置:务必为 WebClient 设置合理的连接/读取超时,防止 Storage 响应延迟拖垮整个网关:
    .clientConnector(new ReactorClientHttpConnector(
        HttpClient.create()
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5_000)
            .responseTimeout(Duration.ofSeconds(30))))

替代方案对比:

方案 内存安全 性能 实现复杂度 支持 Range
临时文件中转 ❌(磁盘 I/O + 文件句柄泄漏风险) 需手动解析 Range 头并切片读取
RestTemplate + StreamingResponseBody ⚠️(易因 InputStream 缓冲失控导致 OOM) 需手动处理
WebClient 响应式流代理 最优 中(需理解响应式编程) ✅(透传即可)
Spring Cloud Gateway(嵌入式) 低(声明式配置) ✅(开箱支持)
? 小结:对于 Spring Boot 项目,优先采用 WebClient 实现纯响应式流式代理;若网关职责较重且未来需扩展路由、限流、熔断等功能,可考虑将 Spring Cloud Gateway 以库方式嵌入 Ingress 服务(无需独立部署),通过 RouteLocatorBuilder 动态配置转发规则,兼顾灵活性与工程效率。