背景介绍
原业务系统,最大文件限制在64M,不会存在超时的情况。但新业务需求下,有上传500M左右视频的要求,出现了诸如文件过大、访问超时等问题。
解决过程
- 调整防火墙限制报文的大小
- 调整Nginx对文件大小的限制
- 调整SpringBoot对文件大小的限制
- 修改GateWay访问超时的设置
- 使用分片上传+线程池机制,减少服务器处理时间
代码示例(主要记录分片上传+线程池机制)
官方文档
https://help.aliyun.com/document_detail/84786.html
/**
* 设置每个分片的大小(单位bytes)
*/
private final static long partSize = 10 * 1024 * 1024;
/**
* 获取CPU核心线程数
*/
private final static int cores = Runtime.getRuntime().availableProcessors();
/**
* 设置执行分片上传任务的线程池
*/
public static ExecutorService threadPoolTaskExecutor = null;
static{
/**
* 自定义一个线程池
* 1.线程等待时间:
* 根据初步测试 单片文件小于10M时,线程数充足的情况下,文件上传最短耗时、cpu时间累计耗时
* 0.5M 747ms
* 1M 1112ms
* 2M 1881ms
* 3M 4125ms
* 5M 4213ms
* 10M 4196ms
* 2.核心线程数 = ((线程等待时间+线程CPU时间)/线程CPU时间)* CPU数目
* 3.线程等待时间较长,因此应保证足够的线程
* 4.每一个分片的失败都会导致文件上传的失败,因此分片不宜过小
*
*/
threadPoolTaskExecutor = new ThreadPoolExecutor(
cores*4, // coreSize
cores*8, // maxSize
60, // 60s
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(cores*4), // 有界队列
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
}
/**
* 分片上传
*
* @param bucketName bucket完整名称
* @param objectName Object完整路径
* @param multipartFile 上传的文件
* @return
*/
public String multipartUpload(String bucketName, MultipartFile multipartFile, String objectName) {
//long start = System.currentTimeMillis();
//初始化返回值
String url = null;
// 创建InitiateMultipartUploadRequest对象。
InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, objectName);
// 初始化分片。
InitiateMultipartUploadResult upresult = ossClient.initiateMultipartUpload(request);
// 返回uploadId,它是分片上传事件的唯一标识。您可以根据该uploadId发起相关的操作,例如取消分片上传、查询分片上传等。
String uploadId = upresult.getUploadId();
//获取文件大小(单位bytes)
long fileLength = multipartFile.getSize();
// partETags是PartETag的集合。PartETag由分片的ETag和分片号组成。
List<PartETag> partETags = Collections.synchronizedList(new ArrayList<>());
//根据文件大小、分片大小,计算分片的个数
int partCount = (int) (fileLength / partSize);
if (fileLength % partSize != 0) {
partCount++;
}
/**
* 创建多线程上传的线程池和同步计数器
*/
CountDownLatch partCountDownLatch = new CountDownLatch(partCount);
// 遍历分片上传。
InputStream instream = null;
try {
for (int i = 0; i < partCount; i++) {
long startPos = i * partSize;
long curPartSize = (i + 1 == partCount) ? (fileLength - startPos) : partSize;
instream = multipartFile.getInputStream();
// 跳过已经上传的分片。
instream.skip(startPos);
UploadPartRequest uploadPartRequest = new UploadPartRequest();
uploadPartRequest.setBucketName(bucketName);
uploadPartRequest.setKey(objectName);
uploadPartRequest.setUploadId(uploadId);
uploadPartRequest.setInputStream(instream);
// 设置分片大小。除了最后一个分片没有大小限制,其他的分片最小为100 KB。
uploadPartRequest.setPartSize(curPartSize);
// 设置分片号。每一个上传的分片都有一个分片号,取值范围是1~10000,如果超出此范围,OSS将返回InvalidArgument错误码。
uploadPartRequest.setPartNumber(i + 1);
/**
* 单线程上传
// 每个分片不需要按顺序上传,甚至可以在不同客户端上传,OSS会按照分片号排序组成完整的文件。
UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
// 每次上传分片之后,OSS的返回结果包含PartETag。PartETag将被保存在partETags中。
partETags.add(uploadPartResult.getPartETag());
*/
/**
* 创建线程池上传分片
*/
threadPoolTaskExecutor.execute(new Runnable() {
@Override
public void run() {
// long start = System.currentTimeMillis();
// 每个分片不需要按顺序上传,甚至可以在不同客户端上传,OSS会按照分片号排序组成完整的文件。
UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
// 每次上传分片之后,OSS的返回结果包含PartETag。PartETag将被保存在partETags中。
partETags.add(uploadPartResult.getPartETag());
//上传完毕,修改同步计数器
partCountDownLatch.countDown();
// long end = System.currentTimeMillis();
// System.err.println("分片大小:"+partSize/(1024*1024)+"M\t上传用时:"+(end-start)+"毫秒");
}
});
}
// 创建CompleteMultipartUploadRequest对象。
/**
* 同步计数器,保证所有分片上传成功后,才执行验证及后续操作(未完成)
*/
partCountDownLatch.await();
// 在执行完成分片上传操作时,需要提供所有有效的partETags。OSS收到提交的partETags后,会逐一验证每个分片的有效性。当所有的数据分片验证通过后,OSS将把这些分片组合成一个完整的文件。
CompleteMultipartUploadRequest completeMultipartUploadRequest =
new CompleteMultipartUploadRequest(bucketName, objectName, uploadId, partETags);
// 完成上传
CompleteMultipartUploadResult completeMultipartUploadResult = ossClient.completeMultipartUpload(completeMultipartUploadRequest);
url = "https://" + bucketName + "." + oss.getEndpoint() + "/" + objectName + "?versionId=" + completeMultipartUploadResult.getVersionId();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 关闭OSSClient。
//ossClient.shutdown();
try {
instream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// long end = System.currentTimeMillis();
// System.err.println("文件大小:"+multipartFile.getSize()/1048576 +"M\t上传用时:"+(end-start)/1000+"秒");
return url;
}