Deprecated: imwpcache\f884414bce24ee67f\f73723ec7b1919fa5::__construct(): Implicitly marking parameter $YECBGYFECGEAFWHA as nullable is deprecated, the explicit nullable type must be used instead in /www/wwwroot/www.chuangxiangniao.com/wp-content/plugins/imwpcache-dist/build/f884414bce24ee67ff73723ec7b1919fa5.php on line 2

Deprecated: imwpcache\f884414bce24ee67f\f73723ec7b1919fa5::__construct(): Implicitly marking parameter $BBWFDDBHHYHDXXAB as nullable is deprecated, the explicit nullable type must be used instead in /www/wwwroot/www.chuangxiangniao.com/wp-content/plugins/imwpcache-dist/build/f884414bce24ee67ff73723ec7b1919fa5.php on line 2
白话容器基础(三):深入理解容器镜像_创想鸟

白话容器基础(三):深入理解容器镜像

白话容器基础(三):深入理解容器镜像

在前两次的分享中,我讲解了 linux 容器最基础的两种技术:namespace 和 cgroups。希望此时,你已经彻底理解了“容器的本质是一种特殊的进程”这个最重要

的概念。

而正如我前面所说的,Namespace 的作用是“隔离”,它让应用进程只能看到该 Namespace 内的“世界”;而 Cgroups 的作用是“限制”,它给这个“世界”围上了一圈

看不见的墙。这么一折腾,进程就真的被“装”在了一个与世隔绝的房间里,而这些房间就是 PaaS 项目赖以生存的应用“沙盒”。

可是,还有一个问题不知道你有没有仔细思考过:这个房间四周虽然有了墙,但是如果容器进程低头一看地面,又是怎样一副景象呢?

换句话说,容器里的进程看到的文件系统又是什么样子的呢?

可能你立刻就能想到,这一定是一个关于 Mount Namespace 的问题:容器里的应用进程,理应看到一份完全独立的文件系统。这样,它就可以在自己的容器目

录(比如 /tmp)下进行操作,而完全不会受宿主机以及其他容器的影响。

那么,真实情况是这样吗?

“左耳朵耗子”叔在多年前写的一篇关于 Docker 基础知识的博客里,曾经介绍过一段小程序。这段小程序的作用是,在创建子进程时开启指定的 Namespace。

下面,我们不妨使用它来验证一下刚刚提到的问题。

代码语言:javascript代码运行次数:0运行复制

#define _GNU_SOURCE#include  #include #include #include #include #include #include #define STACK_SIZE (1024 * 1024)static char container_stack[STACK_SIZE];char* const container_args[] = {  "/bin/bash",  NULL}; int container_main(void* arg){    printf("Container - inside the container!n");  execv(container_args[0], container_args);  printf("Something's wrong!n");  return 1;} int main(){  printf("Parent - start a container!n");  int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS | SIGCHLD , NULL);  waitpid(container_pid, NULL, 0);  printf("Parent - container stopped!n");  return 0;}

这段代码的功能非常简单:在 main 函数里,我们通过 clone() 系统调用创建了一个新的子进程 container_main,并且声明要为它启用 Mount Namespace(即:CLONE_NEWNS 标志)。

而这个子进程执行的,是一个“/bin/bash”程序,也就是一个 shell。所以这个 shell 就运行在了 Mount Namespace 的隔离环境中。

我们来一起编译一下这个程序:

代码语言:javascript代码运行次数:0运行复制

$ gcc -o ns ns.c$ ./nsParent - start a container!Container - inside the container!

这样,我们就进入了这个“容器”当中。可是,如果在“容器”里执行一下 ls 指令的话,我们就会发现一个有趣的现象: /tmp 目录下的内容跟宿主机的内容是一样的。

代码语言:javascript代码运行次数:0运行复制

$ ls /tmp# 你会看到好多宿主机的文件

也就是说:

这是怎么回事呢?

仔细思考一下,你会发现这其实并不难理解:Mount Namespace 修改的,是容器进程对文件系统“挂载点”的认知。但是,这也就意味着,只有在“挂载”这个操作

发生之后,进程的视图才会被改变。而在此之前,新创建的容器会直接继承宿主机的各个挂载点。

这时,你可能已经想到了一个解决办法:创建新进程时,除了声明要启用 Mount Namespace 之外,我们还可以告诉容器进程,有哪些目录需要重新挂载,就比

如这个 /tmp 目录。于是,我们在容器进程执行前可以添加一步重新挂载 /tmp 目录的操作:

代码语言:javascript代码运行次数:0运行复制

int container_main(void* arg){  printf("Container - inside the container!n");  // 如果你的机器的根目录的挂载类型是 shared,那必须先重新挂载根目录  // mount("", "/", NULL, MS_PRIVATE, "");  mount("none", "/tmp", "tmpfs", 0, "");  execv(container_args[0], container_args);  printf("Something's wrong!n");  return 1;}

可以看到,在修改后的代码里,我在容器进程启动之前,加上了一句 mount(“none”, “/tmp”, “tmpfs”, 0, “”) 语句。就这样,我告诉了容器以 tmpfs(内存盘)格

式,重新挂载了 /tmp 目录。

这段修改后的代码,编译执行后的结果又如何呢?我们可以试验一下:

代码语言:javascript代码运行次数:0运行复制

$ gcc -o ns ns.c$ ./nsParent - start a container!Container - inside the container!$ ls /tmp

可以看到,这次 /tmp 变成了一个空目录,这意味着重新挂载生效了。我们可以用 mount -l 检查一下:

代码语言:javascript代码运行次数:0运行复制

$ mount -l | grep tmpfsnone on /tmp type tmpfs (rw,relatime)

可以看到,容器里的 /tmp 目录是以 tmpfs 方式单独挂载的。

更重要的是,因为我们创建的新进程启用了 Mount Namespace,所以这次重新挂载的操作,只在容器进程的 Mount Namespace 中有效。如果在宿主机上用

mount -l 来检查一下这个挂载,你会发现它是不存在的:

代码语言:javascript代码运行次数:0运行复制

# 在宿主机上$ mount -l | grep tmpfs

这就是 Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效。

可是,作为一个普通用户,我们希望的是一个更友好的情况:每当创建一个新容器时,我希望容器进程看到的文件系统就是一个独立的隔离环境,而不是继承自宿

主机的文件系统。怎么才能做到这一点呢?

不难想到,我们可以在容器进程启动之前重新挂载它的整个根目录“/”。而由于 Mount Namespace 的存在,这个挂载对宿主机不可见,所以容器进程就可以在里

面随便折腾了。

在 Linux 操作系统里,有一个名为 chroot 的命令可以帮助你在 shell 中方便地完成这个工作。顾名思义,它的作用就是帮你“change root file system”,即改变进

程的根目录到你指定的位置。它的用法也非常简单。

假设,我们现在有一个 $HOME/test 目录,想要把它作为一个 /bin/bash 进程的根目录。

首先,创建一个 test 目录和几个 lib 文件夹:

代码语言:javascript代码运行次数:0运行复制

$ mkdir -p $HOME/test$ mkdir -p $HOME/test/{bin,lib64,lib}$ cd $T

然后,把 bash 命令拷贝到 test 目录对应的 bin 路径下:

代码语言:javascript代码运行次数:0运行复制

$ cp -v /bin/{bash,ls} $HOME/test/bin

接下来,把 bash 命令需要的所有 so 文件,也拷贝到 test 目录对应的 lib 路径下。找到 so 文件可以用 ldd 命令:

代码语言:javascript代码运行次数:0运行复制

$ T=$HOME/test$ list="$(ldd /bin/ls | egrep -o '/lib.*.[0-9]')"$ for i in $list; do cp -v "$i" "${T}${i}"; done

最后,执行 chroot 命令,告诉操作系统,我们将使用 $HOME/test 目录作为 /bin/bash 进程的根目录:

代码语言:javascript代码运行次数:0运行复制

$ chroot $HOME/test /bin/bash

这时,你如果执行 “ls /”,就会看到,它返回的都是 $HOME/test 目录下面的内容,而不是宿主机的内容。

更重要的是,对于被 chroot 的进程来说,它并不会感受到自己的根目录已经被“修改”成 $HOME/test 了。

这种视图被修改的原理,是不是跟我之前介绍的 Linux Namespace 很类似呢?

没错!

实际上,Mount Namespace 正是基于对 chroot 的不断改良才被发明出来的,它也是 Linux 操作系统里的第一个 Namespace。

当然,为了能够让容器的这个根目录看起来更“真实”,我们一般会在这个容器的根目录下挂载一个完整操作系统的文件系统,比如 Ubuntu16.04 的 ISO。这样,在

容器启动之后,我们在容器里通过执行 “ls /” 查看根目录下的内容,就是 Ubuntu 16.04 的所有目录和文件。

而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系

统)。

所以,一个最常见的 rootfs,或者说容器镜像,会包括如下所示的一些目录和文件,比如 /bin,/etc,/proc 等等:

代码语言:javascript代码运行次数:0运行复制

$ ls /bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var

而你进入容器之后执行的 /bin/bash,就是 /bin 目录下的可执行文件,与宿主机的 /bin/bash 完全不同。

现在,你应该可以理解,对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:

启用 Linux Namespace 配置;设置指定的 Cgroups 参数;切换进程的根目录(Change Root)。

这样,一个完整的容器就诞生了。不过,Docker 项目在最后一步的切换上会优先使用 pivot_root 系统调用,如果系统不支持,才会使用 chroot。这两个系统调用

虽然功能类似,但是也有细微的区别,这一部分小知识就交给你课后去探索了。

另外,需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统

只有在开机启动时才会加载指定版本的内核镜像。

所以说,rootfs 只包括了操作系统的“躯壳”,并没有包括操作系统的“灵魂”。

那么,对于容器来说,这个操作系统的“灵魂”又在哪里呢?

实际上,同一台机器上的所有容器,都共享宿主机操作系统的内核。

这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操

作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身。

这也是容器相比于虚拟机的主要缺陷之一:毕竟后者不仅有模拟出来的硬件机器充当沙盒,而且每个沙盒里还运行着一个完整的 Guest OS 给应用随便折腾。

不过,正是由于 rootfs 的存在,容器才有了一个被反复宣传至今的重要特性:一致性。

什么是容器的“一致性”呢?

我在专栏的第一篇文章《小鲸鱼大事记(一):初出茅庐》中曾经提到过:由于云端与本地服务器环境不同,应用的打包过程,一直是使用 PaaS 时最“痛苦”的一

个步骤。

但有了容器之后,更准确地说,有了容器镜像(即 rootfs)之后,这个问题被非常优雅地解决了。

由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。

事实上,对于大多数开发者而言,他们对应用依赖的理解,一直局限在编程语言层面。比如 Golang 的 Godeps.json。但实际上,一个一直以来很容易被忽视的事

实是,对一个应用来说,操作系统本身才是它运行所需要的最完整的“依赖库”。

有了容器镜像“打包操作系统”的能力,这个最基础的依赖环境也终于变成了应用沙盒的一部分。这就赋予了容器所谓的一致性:无论在本地、云端,还是在一台任

何地方的机器上,用户只需要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就被重现出来了。

Calliper 文档对比神器 Calliper 文档对比神器

文档内容对比神器

Calliper 文档对比神器 28 查看详情 Calliper 文档对比神器

这种深入到操作系统级别的运行环境一致性,打通了应用在本地开发和远端执行环境之间难以逾越的鸿沟。

不过,这时你可能已经发现了另一个非常棘手的问题:难道我每开发一个应用,或者升级一下现有的应用,都要重复制作一次 rootfs 吗?

比如,我现在用 Ubuntu 操作系统的 ISO 做了一个 rootfs,然后又在里面安装了 Java 环境,用来部署我的 Java 应用。那么,我的另一个同事在发布他的 Java 应

用时,显然希望能够直接使用我安装过 Java 环境的 rootfs,而不是重复这个流程。

一种比较直观的解决办法是,我在制作 rootfs 的时候,每做一步“有意义”的操作,就保存一个 rootfs 出来,这样其他同事就可以按需求去用他需要的 rootfs 了。

但是,这个解决办法并不具备推广性。原因在于,一旦你的同事们修改了这个 rootfs,新旧两个 rootfs 之间就没有任何关系了。这样做的结果就是极度的碎片化。

那么,既然这些修改都基于一个旧的 rootfs,我们能不能以增量的方式去做这些修改呢?这样做的好处是,所有人都只需要维护相对于 base rootfs 修改的增量内

容,而不是每次修改都制造一个“fork”。

答案当然是肯定的。

这也正是为何,Docker 公司在实现 Docker 镜像时并没有沿用以前制作 rootfs 的标准流程,而是做了一个小小的创新:

当然,这个想法不是凭空臆造出来的,而是用到了一种叫作联合文件系统(Union File System)的能力。

Union File System 也叫 UnionFS,最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。比如,我现在有两个目录 A 和 B,它们分

别有两个文件:

代码语言:javascript代码运行次数:0运行复制

$ tree.├── A│  ├── a│  └── x└── B  ├── b  └── x

然后,我使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C 上:

代码语言:javascript代码运行次数:0运行复制

$ mkdir C$ mount -t aufs -o dirs=./A:./B none ./C

这时,我再查看目录 C 的内容,就能看到目录 A 和 B 下的文件被合并到了一起:

代码语言:javascript代码运行次数:0运行复制

$ tree ./C./C├── a├── b└── x

可以看到,在这个合并后的目录 C 里,有 a、b、x 三个文件,并且 x 文件只有一份。这,就是“合并”的含义。此外,如果你在目录 C 里对 a、b、x 文件做修改,

这些修改也会在对应的目录 A、B 中生效。

那么,在 Docker 项目中,又是如何使用这种 Union File System 的呢?

我的环境是 Ubuntu 16.04 和 Docker CE 18.05,这对组合默认使用的是 AuFS 这个联合文件系统的实现。你可以通过 docker info 命令,查看到这个信息。

AuFS 的全称是 Another UnionFS,后改名为 Alternative UnionFS,再后来干脆改名叫作 Advance UnionFS,从这些名字中你应该能看出这样两个事实:

它是对 Linux 原生 UnionFS 的重写和改进;它的作者怨气好像很大。我猜是 Linus Torvalds(Linux 之父)一直不让 AuFS 进入 Linux 内核主干的缘故,所以我们只能在 Ubuntu 和 Debian 这些发行版上使用它。

对于 AuFS 来说,它最关键的目录结构在 /var/lib/docker 路径下的 diff 目录:

代码语言:javascript代码运行次数:0运行复制

/var/lib/docker/aufs/diff/

而这个目录的作用,我们不妨通过一个具体例子来看一下。

现在,我们启动一个容器,比如:

代码语言:javascript代码运行次数:0运行复制

$ docker run -d ubuntu:latest sleep 3600

这时候,Docker 就会从 Docker Hub 上拉取一个 Ubuntu 镜像到本地。

这个所谓的“镜像”,实际上就是一个 Ubuntu 操作系统的 rootfs,它的内容是 Ubuntu 操作系统的所有文件和目录。不过,与之前我们讲述的 rootfs 稍微不同的

是,Docker 镜像使用的 rootfs,往往由多个“层”组成:

代码语言:javascript代码运行次数:0运行复制

$ docker image inspect ubuntu:latest...     "RootFS": {      "Type": "layers",      "Layers": [        "sha256:f49017d4d5ce9c0f544c...",        "sha256:8f2b771487e9d6354080...",        "sha256:ccd4d61916aaa2159429...",        "sha256:c01d74f99de40e097c73...",        "sha256:268a067217b5fe78e000..."      ]    }

可以看到,这个 Ubuntu 镜像,实际上由五个层组成。这五个层就是五个增量 rootfs,每一层都是 Ubuntu 操作系统文件与目录的一部分;而在使用镜像时,

Docker 会把这些增量联合挂载在一个统一的挂载点上(等价于前面例子里的“/C”目录)。

这个挂载点就是 /var/lib/docker/aufs/mnt/,比如:

代码语言:javascript代码运行次数:0运行复制

/var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e

不出意外的,这个目录里面正是一个完整的 Ubuntu 操作系统:

代码语言:javascript代码运行次数:0运行复制

$ ls /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3ebin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

那么,前面提到的五个镜像层,又是如何被联合挂载成这样一个完整的 Ubuntu 文件系统的呢?

这个信息记录在 AuFS 的系统目录 /sys/fs/aufs 下面。

首先,通过查看 AuFS 的挂载信息,我们可以找到这个目录对应的 AuFS 的内部 ID(也叫:si):

代码语言:javascript代码运行次数:0运行复制

$ cat /proc/mounts| grep aufsnone /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fc... aufs rw,relatime,si=972c6d361e6b32ba,dio,dirperm1 0 0

即,si=972c6d361e6b32ba。

然后使用这个 ID,你就可以在 /sys/fs/aufs 下查看被联合挂载在一起的各个层的信息:

代码语言:javascript代码运行次数:0运行复制

$ cat /sys/fs/aufs/si_972c6d361e6b32ba/br[0-9]*/var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...=rw/var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...-init=ro+wh/var/lib/docker/aufs/diff/32e8e20064858c0f2...=ro+wh/var/lib/docker/aufs/diff/2b8858809bce62e62...=ro+wh/var/lib/docker/aufs/diff/20707dce8efc0d267...=ro+wh/var/lib/docker/aufs/diff/72b0744e06247c7d0...=ro+wh/var/lib/docker/aufs/diff/a524a729adadedb90...=ro+wh

从这些信息里,我们可以看到,镜像的层都放置在 /var/lib/docker/aufs/diff 目录下,然后被联合挂载在 /var/lib/docker/aufs/mnt 里面。

而且,从这个结构可以看出来,这个容器的 rootfs 由如下图所示的三部分组成:

白话容器基础(三):深入理解容器镜像

第一部分,只读层。

它是这个容器的 rootfs 最下面的五层,对应的正是 ubuntu:latest 镜像的五层。可以看到,它们的挂载方式都是只读的(ro+wh,即 readonly+whiteout,至于什

么是 whiteout,我下面马上会讲到)。

这时,我们可以分别查看一下这些层的内容:

代码语言:javascript代码运行次数:0运行复制

$ ls /var/lib/docker/aufs/diff/72b0744e06247c7d0...etc sbin usr var$ ls /var/lib/docker/aufs/diff/32e8e20064858c0f2...run$ ls /var/lib/docker/aufs/diff/a524a729adadedb900...bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

可以看到,这些层,都以增量的方式分别包含了 Ubuntu 操作系统的一部分。

第二部分,可读写层。

它是这个容器的 rootfs 最上面的一层(6e3be5d2ecccae7cc),它的挂载方式为:rw,即 read write。在没有写入文件之前,这个目录是空的。而一旦在容器里

做了写操作,你修改产生的内容就会以增量的方式出现在这个层中。

可是,你有没有想到这样一个问题:如果我现在要做的,是删除只读层里的一个文件呢?

为了实现这样的删除操作,AuFS 会在可读写层创建一个 whiteout 文件,把只读层里的文件“遮挡”起来。

比如,你要删除只读层里一个名叫 foo 的文件,那么这个删除操作实际上是在可读写层创建了一个名叫.wh.foo 的文件。这样,当这两个层被联合挂载之后,foo

文件就会被.wh.foo 文件“遮挡”起来,“消失”了。这个功能,就是“ro+wh”的挂载方式,即只读 +whiteout 的含义。我喜欢把 whiteout 形象地翻译为:“白障”。

所以,最上面这个可读写层的作用,就是专门用来存放你修改 rootfs 后产生的增量,无论是增、删、改,都发生在这里。而当我们使用完了这个被修改过的容器

之后,还可以使用 docker commit 和 push 指令,保存这个被修改过的可读写层,并上传到 Docker Hub 上,供其他人使用;而与此同时,原先的只读层里的内

容则不会有任何变化。这,就是增量 rootfs 的好处。

第三部分,Init 层。

它是一个以“-init”结尾的层,夹在只读层和读写层之间。Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。

需要这样一层的原因是,这些文件本来属于只读的 Ubuntu 镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如 hostname,所以就需要在可读

写层对它们进行修改。

可是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 时,把这些信息连同可读写层一起提交掉。

所以,Docker 做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行 docker commit 只会提交可读写层,所以是不包含这些内容的。

最终,这 7 个层都被联合挂载到 /var/lib/docker/aufs/mnt 目录下,表现为一个完整的 Ubuntu 操作系统供容器使用。

总结

在今天的分享中,我着重介绍了 Linux 容器文件系统的实现方式。而这种机制,正是我们经常提到的容器镜像,也叫作:rootfs。它只是一个操作系统的所有文件

和目录,并不包含内核,最多也就几百兆。而相比之下,传统虚拟机的镜像大多是一个磁盘的“快照”,磁盘有多大,镜像就至少有多大。

通过结合使用 Mount Namespace 和 rootfs,容器就能够为进程构建出一个完善的文件系统隔离环境。当然,这个功能的实现还必须感谢 chroot 和 pivot_root 这

两个系统调用切换进程根目录的能力。

而在 rootfs 的基础上,Docker 公司创新性地提出了使用多个增量 rootfs 联合挂载一个完整 rootfs 的方案,这就是容器镜像中“层”的概念。

通过“分层镜像”的设计,以 Docker 镜像为核心,来自不同公司、不同团队的技术人员被紧密地联系在了一起。而且,由于容器镜像的操作是增量式的,这样每次

镜像拉取、推送的内容,比原本多个完整的操作系统的大小要小得多;而共享层的存在,可以使得所有这些容器镜像需要的总空间,也比每个镜像的总和要小。这

样就使得基于容器镜像的团队协作,要比基于动则几个 GB 的虚拟机磁盘镜像的协作要敏捷得多。

更重要的是,一旦这个镜像被发布,那么你在全世界的任何一个地方下载这个镜像,得到的内容都完全一致,可以完全复现这个镜像制作者当初的完整环境。这,

就是容器技术“强一致性”的重要体现。

而这种价值正是支撑 Docker 公司在 2014~2016 年间迅猛发展的核心动力。容器镜像的发明,不仅打通了“开发 – 测试 – 部署”流程的每一个环节,更重要的是:

以上就是白话容器基础(三):深入理解容器镜像的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/459906.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
如何开发生鲜蔬菜app,开发一个生鲜蔬菜app需要多少钱?
上一篇 2025年11月8日 03:05:31
Win11修改文件关联 Win11更改默认打开方式技巧
下一篇 2025年11月8日 03:05:46

相关推荐

  • mac怎么使用iMovie剪辑视频_mac使用iMovie剪辑视频教程

    首先打开iMovie并导入视频素材,然后将视频拖入时间线进行裁剪与分割,接着为片段间添加转场效果,再插入背景音乐并调节音量,最后设置参数导出视频。 如果您想在Mac上对视频进行剪辑和编辑,但不知道如何使用系统自带的iMovie应用完成操作,可以按照以下步骤进行。iMovie提供了直观的界面和基础剪辑…

    2026年9月22日
    500
  • MySQL自动化备份如何实现_适合企业级部署吗?

    MySQL自动化备份如何实现_适合企业级部署吗?MySQL自动化备份如何实现_适合企业级部署吗?MySQL自动化备份如何实现_适合企业级部署吗?MySQL自动化备份如何实现_适合企业级部署吗?

    mysql的自动化备份对企业级部署是必要的,且可通过多种方式实现。1. 使用mysqldump+定时任务(crontab)是最基础的方式,操作简单适合中小规模数据库,但备份时可能锁表影响业务;2. 增量备份结合二进制日志(binary log)更高效,适用于频繁变更的数据,支持精确恢复到某时间点;3…

    2026年9月21日 用户投稿
    000
  • TensorFlow的AI混合工具怎么操作?构建机器学习模型的详细步骤

    TensorFlow的混合编程核心在于结合Keras的高级抽象与TensorFlow底层API的灵活性,实现高效模型开发。首先使用tf.data构建高性能数据管道,通过map、batch、shuffle和prefetch等操作优化数据预处理;接着利用Keras快速搭建模型结构,同时通过继承tf.ke…

    2026年9月21日
    300
  • Intel前CEO:公司过去15年连锁犯错、18A是重要里程碑

    10月14日,曾担任intel首席执行官的帕特·基辛格(pat gelsinger)在近期一次采访中分享了他对自身在intel职业生涯的反思,并就当下ai产业的发展态势表达了个人见解。 他坦言,Intel“在过去十五年间接连做出多项错误的战略选择”, 这使得公司进入了漫长的重建期,同时也失去了曾经在…

    2026年9月21日
    600
  • VSCode如何自定义文件图标 VSCode资源管理器视觉优化的技巧

    自定义vscode文件图标需安装图标主题扩展,如material icon theme;2. 通过扩展市场安装后,在文件图标主题设置中启用;3. 选择主题时应考虑视觉风格、图标覆盖率、辨识度和更新频率;4. 可结合文件嵌套、隐藏文件夹、缩进指南等设置优化资源管理器视觉体验;5. 自定义图标对性能影响…

    2026年9月21日
    000
  • 如何使用Scikit-learn训练AI大模型?传统机器学习与深度结合

    如何使用Scikit-learn训练AI大模型?传统机器学习与深度结合如何使用Scikit-learn训练AI大模型?传统机器学习与深度结合如何使用Scikit-learn训练AI大模型?传统机器学习与深度结合如何使用Scikit-learn训练AI大模型?传统机器学习与深度结合

    Scikit-learn在大型模型预处理中的核心作用是提供数据清洗、特征缩放、编码和降维等工具,确保输入数据高质量且规范化,为深度学习模型奠定坚实基础。 ☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 免费无限量使用 DeepSeek R1 模型☜☜☜ 说实话,如果你的目标是纯粹地“训练AI大…

    2026年9月21日 用户投稿
    700
  • Online Config VS Code

    Online Config VS CodeOnline Config VS CodeOnline Config VS CodeOnline Config VS Code

    run vs view Install Code Server Update Code Server Database:It is recommended to create a Docker container for the database. Code Language: JavaScript…

    2026年9月21日 用户投稿
    000
  • Java Collections.sort与Collections.reverse的使用区别

    Collections.sort用于排序,基于元素值比较,结果有序,默认升序,可自定义规则;2. Collections.reverse仅反转列表顺序,不比较元素,时间复杂度O(n);3. 两者功能不同,不可替代,按需选择使用。 Java 中 Collections.sort 和 Collectio…

    2026年9月21日
    200
  • 一键PHP环境怎么解决网页空白问题_空白页故障诊断

    答案是开启错误提示并检查文件路径与代码逻辑。先启用PHP错误显示,确认配置正确;再核对网站根目录和入口文件是否存在;接着排查代码致命错误及输出缓冲问题,确保无BOM头且session前无输出。 遇到一键PHP环境安装后出现网页空白或空白页问题,通常不是环境完全失效,而是某些关键环节出了错。这类问题在…

    2026年9月21日
    000
  • MediBangPaint中AI生成图片如何导出?快速保存图像的详细方法

    答案:导出AI生成图片应优先选择PNG格式以保留细节和色彩。通过“文件”菜单中的“导出(单层)”功能,可将图像保存为PNG或JPG等格式,其中PNG为无损压缩,适合高质量输出;若需透明背景或后续编辑,更应选用PNG。清晰度不足常因原始分辨率低、过度放大或JPG压缩过度所致,建议导出时设置高质量(80…

    2026年9月21日
    300
  • Bun 1.3 正式发布

    2025年10月10日,高性能 javascript 运行时 bun 发布了 1.3 版本。这是 bun 项目迄今为止最重大的版本更新,标志着 bun 从单纯的运行时工具演变为一个功能完备的全栈 javascript 开发平台。 从运行时到全栈平台的跨越 Bun 1.3 的核心突破在于将前端开发能力…

    2026年9月21日
    000
  • Java中多态的基本实现方法

    多态允许同一接口调用不同实现,通过继承与方法重写实现。1. 子类重写父类方法,如Animal的makeSound被Dog和Cat重写;2. 父类引用指向子类对象,运行时动态绑定,如Animal myPet = new Dog()调用Woof;3. 方法参数使用父类类型,提升代码复用,如playWit…

    2026年9月21日
    000
  • MySQL字段注释快速补全方法_Sublime脚本自动生成标准文档结构

    MySQL字段注释快速补全方法_Sublime脚本自动生成标准文档结构MySQL字段注释快速补全方法_Sublime脚本自动生成标准文档结构MySQL字段注释快速补全方法_Sublime脚本自动生成标准文档结构MySQL字段注释快速补全方法_Sublime脚本自动生成标准文档结构

    要快速补全mysql字段注释,可通过sublime text编写python脚本实现自动化;1. 脚本获取表名,可手动输入或从当前sql文件解析;2. 通过subprocess调用mysql命令行获取show full columns信息;3. 解析输出内容,提取字段名和现有注释;4. 生成alte…

    2026年9月21日 用户投稿
    100
  • 更偏向移动端?Steam新版商店页引国外玩家批评

    今日,v社正式上线全新版本的steam商店界面,标志着此前长期测试的新设计终于全面启用。新版首页在视觉上更加开阔、简洁,将原先位于左侧的游戏分类菜单与顶部的蓝色导航栏整合为统一的顶部导航条,支持用户直接浏览竞速、潜行等具体游戏类型,并结合用户偏好实现个性化内容推荐。整体布局更贴近移动端操作逻辑,页面…

    2026年9月21日
    000
  • safari浏览器如何设置链接在新窗口而不是新标签页打开_safari浏览器链接新窗口打开设置

    通过快捷键或第三方扩展可实现Safari中链接在新窗口打开:1. 按住Command键点击链接可临时在新窗口打开;2. 使用AppleScript脚本通过“自动操作”创建快速操作以新建Safari窗口;3. 网站自身代码如window.open()会强制新窗口打开;4. 安装可信扩展如“Link i…

    2026年9月21日
    000
  • Hibernate Search嵌入式对象索引策略与常见问题解决

    本文探讨了在使用Hibernate Search对关联或嵌入式对象进行索引时遇到的常见问题,特别是@IndexedEmbedded与includePaths属性的结合使用。通过分析HSEARCH000216错误,揭示了嵌入式对象属性需要显式@Field注解才能被主实体索引的机制,并提供了具体的代码示…

    2026年9月21日
    100
  • Ubuntu安装Python3.6并切换到3.6版本「建议收藏」

    大家好,很高兴再次与你们见面,我是你们的好朋友全栈君。 文章目录 前言==补充==1 了解系统中已安装的Python版本2 安装Python 3.63 从Python 2.7切换到Python 3.64 中间遇到的问题4.1 问题一4.2 问题二总结参考文献 前言本文记录我在Ubuntu 16.04…

    2026年9月21日
    000
  • 陈震透露劳斯莱斯事故原因:辅助驾驶是“元凶”

    知名汽车博主陈震,就其于10月3日驾驶劳斯莱斯闪灵发生交通事故一事,作出了最新的回应。在北京交警通报其负事故全部责任后,陈震于昨晚发文,透露了事故发生的核心原因,他坦言,是由于自己使用了车辆的辅助驾驶功能,并将其归咎于自己“对闪灵的辅助驾驶能力边界认知不够清晰”。 辅助驾驶“甩锅”,驾驶员永远是第一…

    2026年9月21日
    100
  • 巧文书AI官网首页官方入口 巧文书AI在线文档编辑官网链接直达

    巧文书AI官网首页官方入口是https://qiaowenshu.cn,该平台提供AI驱动的文档生成、智能解析、语义扩展、在线编辑与保存等功能,支持多场景文案创作和跨设备同步,具备一键润色、风格推荐及模板管理等智能化写作辅助体验。 ☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 免费无限量使用…

    2026年9月21日
    100
  • Linux文件系统mv命令使用详解

    mv命令用于移动或重命名文件和目录,若目标不存在则重命名,存在且为目录则移动,为文件则覆盖;常用选项包括-i(交互确认)、-f(强制覆盖)、-u(更新移动)、-v(显示过程);可重命名如mv oldname.txt newname.txt,或移动如mv file.txt /dir/;建议使用alia…

    2026年9月21日
    000

发表回复

登录后才能评论
关注微信