我是如何将 Java 基础 docker 镜像大小从 674Mb 优化到 58Mb的

news2024/12/23 3:41:40

我是如何将 Java 基础 docker 镜像大小从 674Mb 优化到 58Mb的

如果您是 Java 开发人员,并且正在使用 Docker 打包应用程序,您可能已经注意到,即使是“hello world”类型的项目,最终镜像的大小也可能非常大。在本文中,我们将介绍一些优化 Java 应用程序的 Docker 镜像大小的技巧。

我们将使用上一篇文章《使用 RFC-9457 规范在 Spring web 中处理错误》中构建的相同 Spring web 应用程序来演示这些技巧。我们的应用程序仅包含 2 个端点:

  • GET /users/:id :通过 id 获取用户
  • POST /users:创建新用户

Controller.java

@RestController 
@RequestMapping("/api/users") 
@RequiredArgsConstructor 
public  class  UserController { 

    private  final UserService userService; 

    @GetMapping("{id}") 
    public User getUser ( @PathVariable Long id) { 
        return userService.getUserById(id) 
                .orElseThrow(() -> new  UserNotFoundException (id, "/api/users" )); 
    } 

    @PostMapping 
    public User createUser ( @Valid  @RequestBody User user) { 
        return userService.createUser(user); 
    } 

不是太多吧?但正如你所看到的,最简单的docker镜像(没有应用一些优化)的大小可能相当大。

不是太多吧?但正如你所看到的,最简单的docker镜像(没有应用一些优化)的大小可能相当大。

本文的源代码可以在github上找到

为什么我们应该关心镜像大小?

镜像大小会对您的绩效产生重大影响,无论是作为开发人员还是组织。特别是当您在处理包含多项服务的大型项目时,镜像的大小可能会非常大,这可能会花费您大量的金钱和时间。

应避免使用大镜像的原因如下:

  • 磁盘空间:您正在浪费 Docker 注册表和生产服务器中的磁盘空间。
  • 构建速度较慢:镜像越大,构建和推送镜像所需的时间越长。
  • 安全性:镜像越大,依赖关系越大,攻击面就越大。
  • 带宽:镜像越大,从注册表拉取和推送镜像时消耗的带宽就越大。

使用简单的 Dockerfile

基础镜像很重要 ✌🏽 :选择正确的基础镜像

在开始考虑优化之前,你应该始终小心用于打包应用程序的基础镜像。你选择的基础镜像会对最终镜像的大小产生重大影响(如下所示)。

您可以使用多个基础镜像来打包 Java 应用程序,其中一些是:

  • JDK Alpine 基础镜像 :这些镜像体积相当小,但并不适合所有应用程序,因此您可能会遇到与某些库的兼容性问题。
  • JDK Slim 基础镜像 :这些镜像基于 Debian 或 Ubuntu,与完整的 JDK 镜像相比,它们的尺寸非常小,但仍然相当大。
  • JDK 完整基础镜像 :这些镜像尺寸相当大,它们包含运行应用程序所需的所有模块和依赖项。

openjdk:17-jdk-slim为了让您对基础镜像的大小有一个概念,这里是(slim) 和 eclipse-temurin:17-jdk-alpine(alpine) 镜像大小的比较:

知道应用程序工件(jar)的大小为:~20MB

openjdk:17-jdk-slim为了让您对基础镜像的大小有一个概念,这里是(slim) 和 eclipse-temurin:17-jdk-alpine(alpine) 镜像大小的比较:

知道应用程序工件(jar)的大小为:~20MB

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

将我们的工件打包到docker镜像中,我们需要 Dockefile在应用程序根目录中定义如下:

使用 openjdk:17-jdk-slim 作为基础镜像。

Dockerfile.base-openjdk

FROM openjdk: 17 -jdk-slim 

# 设置容器中的工作目录
WORKDIR /app 

# 创建用户
RUN addgroup -- system spring && adduser -- system spring --ingroup spring 

# 更改为用户
USER spring:spring 

COPY target/*.jar app.jar 

EXPOSE 8080

 CMD [ "java" , "-jar" , "app.jar" ]

定义Dockerfile后,我们可以使用以下命令构建镜像:

docker build-t 用户服务。

此后,你应该有一个名为的 docker 镜像 user-service,你可以看到,与应用程序工件的大小相比,镜像的大小相当大,大约为 674MB

在这里插入图片描述

等什么🤯!!然而,这只是一个具有 2 个端点且没有依赖关系的小项目,那么具有十几个依赖项和文件的应用程序怎么办呢?

使用 eclipse-temurin:17-jdk-alpine 作为基础镜像。

Dockerfile.base-temurin

FROM eclipse-temurin:17-jdk-alpine 

ARG APPLICATION_USER=spring 
# 创建用户来运行应用程序,不要以 root 身份运行
RUN addgroup --system $APPLICATION_USER && adduser --system $APPLICATION_USER --ingroup $APPLICATION_USER 

# 创建应用程序目录
RUN mkdir /app && chown -R $APPLICATION_USER /app 

# 设置用户来运行应用程序
USER $APPLICATION_USER 

# 将 jar 文件复制到容器
COPY -- chown = $APPLICATION_USER : $APPLICATION_USER target/*.jar /app/app.jar 

# 设置工作目录
WORKDIR /app 

# 公开端口
EXPOSE 8080 

# 运行应用程序
ENTRYPOINT [ "java" , "-jar" , "/app/app.jar" ]

使用以下命令构建镜像后:

docker build -t 用户服务:alpine -f Dockerfile.base-alpine . --platform=linux/amd64

🚨 附注

重要提示:如果您在 Apple Silicon 上使用 MAC,则在构建映像时可能会遇到以下问题:

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

[internal] 加载 docker.io/library/eclipse-temurin:17-jdk-alpine 的元数据:
— — —
Dockerfile:2
— — — — — — — — — — —
1 | # 第一阶段,构建自定义 JRE
2 | >>> FROM eclipse-temurin:17-jdk-alpine AS jre-builder
3 |
4 | # 安装 jlink 所需的 binutils
— — — — — — — — — — —
错误:无法解决:eclipse-temurin:17-jdk-alpine:清单中的平台不匹配:未找到
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

要解决此问题,您可以将其添加到 docker build命令中:

–平台=linux/amd64

linux/amd64或者您可以通过运行以下命令将默认平台设置为:

导出 DOCKER_DEFAULT_PLATFORM=linux/amd64

linux/amd64或者您可以通过运行以下命令将默认平台设置为:
export DOCKER_DEFAULT_PLATFORM=linux/amd64

eclipse-temurin:17-jdk-alpine使用该镜像作为基础镜像构建镜像后,我们得到了以下结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

查看两个镜像的大小,即使不进行任何调整,用作基础镜像的镜像大小也为 180MB,比用作基础镜像的 eclipse-temurin:17-jdk-alpine镜像(674MB)小 73%openjdk:17-jdk-slim

动手优化

等一下,为什么我们不能使用 JREimage 来代替 JDKimage ?

好问题!这是因为从 Java 11 开始,JRE不再可用

其中需要考虑的最重要的一点是这部分“用户可以使用 jlink 创建更小的自定义运行时。”

JRE使用构建您自己的镜像 jlink

jlink是一种可用于创建自定义运行时映像的工具,该映像仅包含运行应用程序所需的模块;

👉 如果您的应用程序不与数据库交互,则无需 java.sql在映像中包含该模块。 如果您不与桌面 GUI 交互,则无需 java.desktop在映像中包含该模块。 等等。

它有点像 JRE镜像的替代品,但对您想要在镜像中使用的模块有更多的控制权。

因此 jlink我们的 Dockerfile 应该是这样的:

# 第一阶段,构建自定义 JRE
 FROM eclipse-temurin:17-jdk-alpine AS jre-builder 

# 安装 jlink 所需的 binutils
 RUN apk update && \ 
    apk add binutils 

# 构建小型 JRE 映像
RUN $JAVA_HOME /bin/jlink \ 
         --verbose \ 
         --add-modules ALL-MODULE-PATH \ 
         --strip-debug \ 
         --no-man-pages \ 
         --no-header-files \ 
         --compress=2 \ 
         --output /optimized-jdk-17 

# 第二阶段,使用自定义 JRE 并构建应用程序映像
FROM alpine:latest 
ENV JAVA_HOME=/opt/jdk/jdk-17 
ENV PATH= " ${JAVA_HOME} /bin: ${PATH} " 

# 从基础映像复制 JRE
 COPY --from=jre-builder /optimized-jdk-17 $JAVA_HOME 

# 添加应用程序用户
ARG APPLICATION_USER=spring 

# 创建用户来运行应用程序,不要以 root 身份运行
RUN addgroup --system $APPLICATION_USER && adduser --system $APPLICATION_USER --ingroup $APPLICATION_USER 

# 创建应用程序目录
RUN mkdir /app && chown -R $APPLICATION_USER /app 

COPY -- chown = $APPLICATION_USER : $APPLICATION_USER target/*.jar /app/app.jar 

WORKDIR /app 

USER $APPLICATION_USER

 EXPOSE 8080 
ENTRYPOINT [ "java" , "-jar" , "/app/app.jar" ]

那么让我们解释一下我们在这里所做的事情:

  • 我们有两个阶段,第一阶段用于构建自定义 JRE 映像 jlink,第二阶段用于将应用程序打包到 slim alpine 映像中。
  • 在第一阶段,我们使用 eclipse-temurin:17-jdk-alpine来构建自定义 JRE 映像 jlink。然后我们安装 binutils所需的 jlink,然后我们运行 jlink构建一个小型 JRE 映像,该映像包含 --add-modules ALL-MODULE-PATH运行应用程序所需的所有模块(目前)。
  • 在第二阶段,我们使用 alpine镜像(相当小的 3Mb)来打包我们的应用程序)作为基础镜像,然后我们 JRE从第一阶段获取自定义并将其用作我们的 JAVA_HOME
  • Dockerfile 的其余部分与前一个相同,只是复制工件并使用自定义用户(非 root)设置入口点。

然后我们可以使用以下命令构建镜像:

docker build -t 用户服务:jlink-all-modules-temurin -f Dockerfile.jlink-all-modules.temurin 。

如果运行以下命令:

docker 镜像用户服务

您将看到该镜像的新 Docker 镜像大小现在为 85.3MB,比使用 eclipse-temurin 基础镜像的基础镜像小约 95MB 🎉🥳

为了确保镜像按预期工作,您可以运行以下命令:

docker run -p 8080:8080 用户服务:jlink-all-modules-temurin

您应该会看到应用程序按预期运行。

这还不够

作为一名优秀的开发人员,我们总是希望改进我们的工作,所以让我们看看如何进一步改善镜像尺寸。

--add-modules ALL-MODULE-PATH镜像大小仍然很大,这是因为在命令中使用时 jlink,我们包含了运行应用程序所需的所有模块,但我们肯定不需要所有模块。因此,让我们看看如何通过仅包含运行应用程序所需的模块来获得较小的镜像大小。

如何知道运行应用程序需要哪些模块?

我们可以使用 jdepsJDK自带的工具。jdeps该工具可以用来分析jar文件的依赖关系并生成运行应用程序所需的模块列表。

为此,我们可以在项目根目录运行以下命令:

jdeps --ignore-missing-deps -q \ 
      --recursive \ 
      --multi-release 17 \ 
      --print-module-deps \ 
      --class-path BOOT-INF/lib/* \ 
      target/spring-error-handling-rfc-9457-0.0.1-SNAPSHOT.jar

这将打印出运行应用程序所需的模块列表,在我们的例子中是:

java.base、java.compiler、java.desktop、java.instrument、java.management、java.naming、java.net.http、java.prefs、java.rmi、java.scripting、java.security.jgss、java.sql、jdk.jfr、jdk.unsupported

我们可以简单地将其放入命令 ALL-MODULE-PATHjlink

Dockerfile.jlink-已知模块.temurin


# 第一阶段,构建自定义 JRE
 FROM openjdk:17-jdk-slim AS jre-builder 

# 安装 jlink 所需的 binutils
 RUN apt-get update -y && \ 
    apt-get install -y binutils 

# 构建小型 JRE 映像
RUN $JAVA_HOME/bin/jlink \ 
         --verbose \ 
         --add-modules java.base,java.compiler,java.desktop,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.sql,jdk.jfr,jdk.unsupported \ 
         --strip-debug \ 
         --no-man-pages \ 
         --no-header-files \ 
         --compress = 2 \ 
         --output /optimized-jdk-17 

# 第二阶段,使用自定义 JRE 并构建应用程序映像
FROM alpine:latest 
ENV JAVA_HOME =/opt/jdk/jdk- 17
 ENV PATH = "${JAVA_HOME}/bin:${PATH}" 

# 从基础镜像复制 JRE
 COPY --from =jre-builder /optimized-jdk- 17  $JAVA_HOME 

# 添加应用程序用户
ARG APPLICATION_USER =spring 

# 创建用户来运行应用程序,不要以 root 身份运行
RUN addgroup --system $APPLICATION_USER && adduser --system $APPLICATION_USER --ingroup $APPLICATION_USER 

# 创建应用程序目录
RUN mkdir /app && chown -R $APPLICATION_USER /app 

COPY --chown = $APPLICATION_USER : $APPLICATION_USER target/*.jar /app/app.jar 

WORKDIR /app 

USER $APPLICATION_USER 

EXPOSE 8080 
ENTRYPOINT [ "java", "-jar", “/app/app.jar” ]

然后我们可以使用以下命令构建镜像:

docker build - t用户-服务:jlink -已知-模块- temurin - f Dockerfile.jlink -已知-模块.temurin 。

构建后镜像的尺寸如下:

我们得到的镜像尺寸较小,为 57.8MB,而不是 85.3MB。

这很好,但是我们不能自动化这个过程,而是 jdeps手动运行命令,然后将模块复制到 jlink命令中?

dockerfile 内部流程自动化

Dockerfile.jlink-带有-jdeps.temurin

# 第一阶段,构建自定义 JRE
 FROM eclipse-temurin:17-jdk-alpine AS jre-builder 

RUN mkdir /opt/app 
COPY 。 /opt/app 

WORKDIR /opt/app

环境 MAVEN_VERSION 3.5.4
环境 MAVEN_HOME /usr/lib/mvn
环境 PATH $MAVEN_HOME/bin:$PATH

运行 apk 更新 && \ 
    apk 添加 --no-cache tar binutils


运行 wget http://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz && \ 
  tar -zxvf apache-maven-$MAVEN_VERSION-bin.tar.gz && \ 
  rm apache-maven-$MAVEN_VERSION-bin.tar.gz && \ 
  mv apache-maven-$MAVEN_VERSION /usr/lib/mvn

运行 mvn package -DskipTests
运行 jar xvf target/spring-error-handling-rfc-9457-0.0.1-SNAPSHOT.jar 
RUN jdeps --ignore-missing-deps -q \ 
    --recursive \ 
    --multi-release 17 \ 
    --print-module-deps \ 
    --class-path 'BOOT-INF/lib/*' \ 
    target/spring-error-handling-rfc-9457-0.0.1-SNAPSHOT.jar > modules.txt 

# 构建小型 JRE 映像
RUN $JAVA_HOME/bin/jlink \ 
         --verbose \ 
         --add-modules $(cat modules.txt) \ 
         --strip-debug \ 
         --no-man-pages \ 
         --no-header-files \ 
         --compress = 2 \ 
         --output /optimized-jdk-17 

# 第二阶段,使用自定义 JRE 并构建应用程序映像
FROM alpine:latest 
ENV JAVA_HOME =/opt/jdk/jdk- 17
 ENV PATH = "${JAVA_HOME}/bin:${PATH}" 

# 从基础镜像复制 JRE
 COPY --from =jre-builder /optimized-jdk- 17  $JAVA_HOME 

# 添加应用程序用户
ARG APPLICATION_USER =spring 

# 创建用户来运行应用程序,不要以 root 身份运行
RUN addgroup --system $APPLICATION_USER && adduser --system $APPLICATION_USER --ingroup $APPLICATION_USER 

# 创建应用程序目录
RUN mkdir /app && chown -R $APPLICATION_USER /app 

COPY --chown = $APPLICATION_USER : $APPLICATION_USER target/*.jar /app/app.jar 

WORKDIR /app 

USER $APPLICATION_USER 

EXPOSE 8080 
ENTRYPOINT [ "java", "-jar", "/app/app.jar" ]

然后我们可以使用以下命令构建镜像:

docker build -t 用户服务:jlink-with-jdeps.temurin -f Dockerfile.jlink-with-jdeps.temurin . --platform=linux/amd64

奖金

在我们完成之前,请注意,您可以使用一个 .dockerignore文件来排除一些文件和目录被复制到镜像中,这有助于减少中间阶段镜像的大小。

您还应该知道,选择小型的基础镜像是好的,但要确保它具有良好的安全策略并且与您的应用程序兼容。

结论

希望这篇文章对您有所帮助。如果您有任何问题或意见,请随时通过Twitter或LinkedIn 与我联系。请务必访问我的网站https://www.abdelrani.com查看新文章。

参考

  • https://docs.docker.com/develop/develop-images/multistage-build/
  • https://docs.oracle.com/en/java/javase/11/tools/jlink.html
  • https://docs.oracle.com/en/java/javase/11/tools/jdeps.html
  • https://www.oracle.com/java/technologies/javase/11-relnote-issues.html

以上文章翻译自medium,原文链接

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2173049.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DAY16||513.找树左下角的值 |路径总和|从中序与后序遍历序列构造二叉树

513.找树左下角的值 题目:513. 找树左下角的值 - 力扣(LeetCode) 给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至少有一个节点。 示例 1: 输入: root [2,1,3] 输出: 1示例 2: 输入: […

InternVL 微调实践

任务 follow 教学文档和视频使用QLoRA进行微调模型,复现微调效果,并能成功讲出梗图. 复现过程 参考教程部署:https://github.com/InternLM/Tutorial/blob/camp3/docs/L2/InternVL/joke_readme.md 训练 合并权重&&模型转换 pyth…

多旋翼无人机“仿鸟类”精确拦截飞行目标,助力低空安全

摘要: 使用低成本携带捷联式相机的无人机拦截低空入侵目标是一种具有竞争力的选择。然而,非合作目标的恶意机动和摄像头的耦合使得这项任务充满挑战。为了解决这个问题,提出了一种基于比例导引且具有视场保持能力的基于图像的视觉伺服&#x…

【d52】【Java】【力扣】19.删除链表的倒数第N个节点

思路 1.遍历数组,统计个数,记为total 2.计算出需要被删除的节点 是正数第几个,记做order 3.遍历到order-1,,然后执行删除下一个节点的操作 这里遍历到order-1,是因为想要删除一个节点,需要操作的是它前一个节点的next 代码 /…

JAVA使用Scanner类的nextLint()方法无法正确读取中文。

在练习的时候,我发现我使用Scanner类的nextLint()方法无法正确读取到中文了。检查了我的idea编辑器,用的编码格式也是”utf-8“。所以编码格式没有问题。 问题如下棉两张图所示,我输入宝马后,控制台不打印…

助力申报“山东省首台套技术装备”,安畅检测提供第三方检测服务

9月24日,山东省工业和信息化厅发布了《关于组织2024年度山东省首台(套)技术装备认定工作的通知》。 《通知》中对申报范围、申请条件及申报程序做出了明确规定,并在附件中对申请材料做出了要求。 ★检测报告要求 在《通知》附件…

面试前需要准备什么?

面试前的准备是一个细致且全面的过程,它不仅关乎到你能否在面试中展现出最佳状态,还直接影响到你能否成功获得心仪的职位。以下是一个较为详尽的、接近2000字的面试前准备指南: 一.自我评估与定位 1.深入了解自己 在准备面试之前&#xff…

再谈智慧园区

随着AI的兴起,其影响力将渗透到各行各业。产业园区也不例外。特别是江园科技智慧园区在园区运营上,从早期的信息化,到数字化、智能化,智慧园区是一个不可回避的话题。 01 江园科技智慧园区 无论名称或概念怎么办,产…

Halcon实用系列1-识别二维条码

在做项目时,之前使用的是某康的智能读码器,综合考虑成本,可通过相机拍照来读取图片的二维码,我这边用Halcon来实现。 Halcon代码如下: *创建模型 create_data_code_2d_model(Data Matrix ECC 200, [], [], DataCodeH…

微信小程序map组件自定义气泡真机不显示

最近遇到一个需求需要使用uniapp的map自定义气泡 ,做完之后发现在模拟器上好好的,ios真机不显示,安卓页数时好时不好的 一番查询发现是小程序的老问题了,网上的方法都试了也没能解决 后来看到有人说用nvue可以正常显示&#xff0c…

数据结构 ——— 顺序表oj题:编写函数,合并两个有序数组

目录 题目要求 代码实现 题目要求 nums1 和 nums2 是两个升序的整型数组,另外有两个整数 m 和 n 分别代表 nums1 和 nums2 中的元素个数 要求合并 nusm2 到nums1 中,使合并后的 nums1 同样按升序顺序排列 最终,合并后的数组不应由函数返…

基于Hive和Hadoop的招聘分析系统

本项目是一个基于大数据技术的招聘分析系统,旨在为用户提供全面的招聘信息和深入的职位市场分析。系统采用 Hadoop 平台进行大规模数据存储和处理,利用 MapReduce 进行数据分析和处理,通过 Sqoop 实现数据的导入导出,以 Spark 为核…

Text-to-SQL方法研究

1、面临的挑战 自然语言问题往往包含复杂的语言结构,如嵌套语句、倒装句和省略等,很难准确映射到SQL查询上。此外,自然语言本身就存在歧义,一个问题可能有多种解读。消除歧义需要深入的语言理解能力以及融入上下文和领域知识。 要生成正确的SQL查询,文本到SQL系统需要全面理解…

webpack 4 的 30 个步骤构建 react 开发环境

将 react 和 webpack4 进行结合,集 webpack 的优势于一身,从 0 开始构建一个强大的 react 开发环境。 其实很多人都有 一看就会,一做就废 的特点(当然也包括我在内),这个时候,你需要制定一个略微详细的计划&#xff0…

Redis的基础认识与在ubuntu上的安装教程

来自Redis的自我介绍 我是Redis,一个中间件,职责是把数据存储在内存上,因此可以作为数据库、缓存、消息队列等场景使用。由于可以把数据存储在内存上,因此江湖人称快枪手 1.redis的功能特性 (1)数据在内存…

9.3 Linux_I/O_文件I/O相关函数

打开与关闭 1、打开文件 int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode);返回值:成功返回文件描述符,失败返回EOF pathname:文件路径 flags:标志,其中O_RDO…

深入浅出CSS盒子模型

“批判他人总是想的太简单 剖析自己总是想的太困难” 文章目录 前言文章有误敬请斧正 不胜感恩!什么是盒子模型?盒子模型的组成部分详解1. 内容区(Content)2. 内边距(Padding)3. 边框(Border&am…

『功能项目』下载Mongodb【81】

下载网址:Download MongoDB Community Server | MongoDB 点击安装即可 选择Custom 此时安装已经完成 桌面会创建图标 检查是否配置好MongoDB 输入cmd命令行 Windows键 R 打开命令行 输入cmd 复制安装路径 复制data路径 如果输出一大串代码即配置mongdb成功

Mysql高级篇(中)——锁机制

锁机制 一、概述二、分类1、读锁2、写锁★、FOR SHARE / FOR UPDATE(1)NOWAIT(2)SKIP LOCKED(3)NOWAIT 和 SKIP LOCKED 的比较 ★、 脏写3、表级锁之 S锁 / X锁(1)总结(2…

免费视频无损压缩工具+预览视频生成工具

视频无损压缩工具 功能与作用 :视频无损压缩工具是一种能够减少视频文件大小,但同时保持视频质量的工具。它通过先进的编码技术和算法,有效降低视频文件的存储空间,同时保证视频的清晰度和观感。这对于需要分享或存储大量视频内容…