背景介绍

        原业务系统,最大文件限制在64M,不会存在超时的情况。但新业务需求下,有上传500M左右视频的要求,出现了诸如文件过大、访问超时等问题。

解决过程

  1. 调整防火墙限制报文的大小
  2. 调整Nginx对文件大小的限制
  3. 调整SpringBoot对文件大小的限制
  4. 修改GateWay访问超时的设置
  5. 使用分片上传+线程池机制,减少服务器处理时间

代码示例(主要记录分片上传+线程池机制)

官方文档

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;
    }