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

Minio-java 8.3.1 长时断连后首次连接报流异常求助

问题解决:Minio客户端长时间闲置后首次操作报unexpected end of stream异常

问题背景

  • minio-java版本:8.3.1
  • Minio服务端:未特殊配置连接相关参数
  • 现象:服务器部署后,长时间未操作Minio的首次请求会抛出java.io.IOException: unexpected end of stream on,本地运行无此问题;已启用OkHttpClient的retryOnConnectionFailure(true),但该版本SDK不支持PUT/POST请求自动重试。

解决方案

1. 配置OkHttpClient连接池,主动回收空闲失效连接

长时间闲置的连接可能被服务器或中间件(如Nginx)主动断开,但客户端仍认为连接可用,导致首次请求失败。给连接池配置空闲连接回收策略,避免复用失效连接。

修改Minio配置中的newDefaultHttpClient方法:

private OkHttpClient newDefaultHttpClient() {
    // 配置连接池,设置最大空闲连接数和空闲连接存活时间
    ConnectionPool connectionPool = new ConnectionPool(
        5, // 最大空闲连接数
        5, TimeUnit.MINUTES // 空闲连接存活时间,建议小于服务器/代理的超时时间
    );

    return new OkHttpClient()
            .newBuilder()
            .connectTimeout(10000, TimeUnit.MILLISECONDS)
            .readTimeout(30000, TimeUnit.MILLISECONDS) // 增加读取超时,避免超时断开
            .protocols(Arrays.asList(Protocol.HTTP_1_1))
            .retryOnConnectionFailure(true)
            .connectionPool(connectionPool)
            // 拦截失效连接,触发连接池回收
            .addInterceptor(chain -> {
                Response response = chain.proceed(chain.request());
                // 检测连接是否已失效
                if (response.code() == 0 || response.message().isEmpty()) {
                    response.close();
                    throw new IOException("Connection closed by server");
                }
                return response;
            })
            .build();
}

2. 手动捕获异常,针对PUT/POST请求实现重试逻辑

由于SDK不自动重试PUT/POST,在业务代码中捕获特定异常,手动重试有限次数。

修改上传方法,添加重试逻辑:

// 定义最大重试次数
private static final int MAX_RETRY_TIMES = 2;

public String uploadFile(InputStream inputStream, String bucketName, String path, String suffix) {
    int retryCount = 0;
    // 若InputStream不可重置,先转成ByteArrayInputStream
    ByteArrayInputStream reusableStream = null;
    try {
        byte[] data = IOUtils.toByteArray(inputStream);
        reusableStream = new ByteArrayInputStream(data);
    } catch (IOException e) {
        throw new RuntimeException("读取文件流失败", e);
    }

    while (retryCount <= MAX_RETRY_TIMES) {
        try {
            reusableStream.reset(); // 重置流,确保每次重试都能从头读取
            boolean exists = minioClient.bucketExists(BucketExistsArgs.builder()
                    .bucket(bucketName)
                    .build());
            if (!exists) {
                minioClient.makeBucket(MakeBucketArgs.builder()
                        .bucket(bucketName)
                        .build());
                minioClient.setBucketPolicy(SetBucketPolicyArgs.builder()
                        .bucket(bucketName)
                        .config(config)
                        .build());
            }
            String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
            String filePath = new StringBuilder()
                    .append(path)
                    .append(dateTime)
                    .append("/")
                    .append(IdUtil.fastSimpleUUID())
                    .append(suffix)
                    .toString();
            
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(filePath)
                    .stream(reusableStream, reusableStream.available(), -1)
                    .build());
            return filePath;
        } catch (IOException e) {
            if (e.getMessage().contains("unexpected end of stream") && retryCount < MAX_RETRY_TIMES) {
                retryCount++;
                try {
                    Thread.sleep(500); // 重试前短暂休眠,避免频繁请求
                } catch (InterruptedException ignored) {
                    Thread.currentThread().interrupt();
                }
                continue;
            }
            throw new RuntimeException("上传文件失败", e);
        } catch (Exception e) {
            throw new RuntimeException("上传文件失败", e);
        }
    }
    throw new RuntimeException("上传文件重试多次失败");
}

3. 添加定时预热任务,定期发送轻量请求保持连接活跃

通过定时任务定期调用Minio的轻量接口(如bucketExists),避免连接长时间闲置被断开。

使用Spring的@Scheduled实现定时任务:

@Component
@Slf4j
public class MinioConnectionWarmUp {

    @Autowired
    private MinioClient minioClient;
    // 配置一个已存在的bucket名称
    private static final String WARM_UP_BUCKET = "your-default-bucket";

    @Scheduled(fixedRate = 4 * 60 * 1000) // 每4分钟执行一次,小于服务器/代理的空闲超时时间
    public void warmUpConnection() {
        try {
            minioClient.bucketExists(BucketExistsArgs.builder()
                    .bucket(WARM_UP_BUCKET)
                    .build());
        } catch (Exception e) {
            log.warn("Minio连接预热失败", e);
        }
    }
}

注意:需要在Spring启动类添加@EnableScheduling注解开启定时任务。

4. 检查服务器/中间件的连接超时配置

如果服务器或前端有Nginx等反向代理,检查其keepalive_timeout等连接超时设置,确保客户端配置的空闲连接存活时间小于代理的超时时间,避免代理提前断开连接。


内容的提问来源于stack exchange,提问作者jie Old

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 02:47:02