写点什么

万字长文:直击关于 Dorcker 所必须了解的知识

  • 2022 年 4 月 13 日
  • 本文字数:8975 字

    阅读完需:约 29 分钟

万字长文:直击关于Dorcker所必须了解的知识

云原生的核心技术之一就是容器,很多人会以为 Docker 等于容器,其实 Docker 并不等于容器。其实容器可以理解为:cgroups(资源控制)+namespaces(访问隔离)+rootfs(文件系统)+engine(容器生命周期管理)。

容器与虚拟机的区别

系统虚拟化是将一台物理计算机虚拟成一台或多台虚拟计算机系统,每个计算机系统都有自己的虚拟硬件,其上的操作系统认为自己运行在一台独立的主机上,计算机软件在一个虚拟平台上,而不是真实的硬件平台上运行。


容器和虚拟机都是虚拟化技术。容器是在 Linux 上本机运行,并与其他容器共享主机的内核,无须模拟操作系统指令,它是运行在宿主机上的一个独立的进程。在操作系统(Operating System,OS)的基础上进行虚拟化以及进程资源隔离,占用的 CPU/内存资源不比其他任何可执行文件多,非常轻量。


虚拟机运行的是一个完整的访客操作系统,每个虚拟机中都有一个独立的操作系统内核,通过软件模拟宿主机的操作系统指令,虚拟出多个 OS,然后在 OS 的基础上构建相对独立的程序运行环境,因此隔离效果要比容器好一些。

Docker 的构成

Docker 是 C/S 架构的程序,Docker 客户端向 Docker 守护进程(Dockerdaemon)发起请求,守护进程负责构建、运行和分发 Docker 容器,处理完成后返回结果。

Docker 客户端和守护进程既可以在同一个系统上运行,也可以将 Docker 客户端连接到远程 Docker 守护进程,CLI 使用 REST API 通过脚本或直接通过 CLI 命令来控制 Docker 守护进程或与之交互。守护进程创建并管理 Docker 对象,如镜像、容器、网络和数据卷。

Docker 三大组件

要在 Docker 宿主机上拉起并运行 Docker 容器,与 3 个组件密切相关,分别是 Docker 镜像、Docker 镜像仓库、Docker 容器。


  • Docker 镜像(image):镜像是容器的基石,容器基于镜像启动,镜像就像是容器的源代码,保存了用于容器启动的各种条件(应用代码,二方库、环境变量和配置文件等)。


Docker 运行(run)一个容器前,在本地需要存在对应的镜像。如果本地不存在,则从默认的镜像仓库下载对应的镜像(默认的镜像仓库是 Docker Hub 公共服务器中的仓库,也可以改为国内或者公司自己搭建的镜像仓库)。如果没有在仓库名称后指定具体的镜像标签(tag),则 Docker 会默认拉取标签为 latest 的镜像。


Dockerfile 是一个文本格式的配置文件,利用给定的指令描述基于某一个父镜像(from image×××)创建新镜像的过程。编辑好 Dockerfile 文件后,可以通过 docker build 命令创建本地镜像。在使用 docker build 命令通过 Dockerfile 创建镜像时,会产生一个 build 上下文(context)。


  • Docker 镜像仓库(registry):Docker 镜像仓库用于保存用户创建的镜像,仓库分为公有和私有两种。Docker 公司自己提供了仓库 Docker Hub,可以在 Docker Hub 上创建账户,保存并分享自己创建的镜像,当然也可以架设私有镜像仓库。


由于网络原因,从 Docker Hub(该仓库的服务器在国外)上下载镜像的速度太慢,或者 Docker 镜像拉取不下来,需要配置镜像加速器(要求 Docker 版本 1.10.0 以上)。我们一般会选择国内的某一家云服务商提供的镜像仓库服务。


  • Docker 容器(container):容器是 Docker 的执行单元(运行时),通过镜像启动,容器中可以运行客户端的多个进程。如果说镜像是 Docker 生命周期的构建和打包阶段,那么容器则是启动和执行阶段。


容器是镜像的一个运行实例,不同的是它带有额外的可写层。是独立运行的一个或一组应用,以及它们所运行的必需环境。一个容器实例就是宿主机上的一个独立进程。其拥有独立的文件系统、网络和进程树。


新建容器的方式有两个:一个是使用 dockercontainer run 命令,另一个是使用 docker container create 命令。这两个命令的不同之处在于,create 命令新建的容器处于停止状态,还需要使用 dockercontainer start 命令来启动它。


Docker 内存控制内存异常(Out Of Memory Exception,OOME)在 Linux 系统上,如果内核探测到当前宿主机已经没有可用内存,那么会抛出一个 OOME,并且会开启 killing 去终止一些进程。


容器最多能使用的 CPU 时间有两种限制方式:一是有多个 CPU 密集型的容器竞争 CPU 时,设置各个容器能使用 CPU 时间的相对比例;二是以绝对的方式设置容器在每个调度周期内最多能使用的 CPU 时间。


Docker 提供的内存限制功能包括:容器能使用的内存和交换分区大小、容器的核心内存大小、容器虚拟内存的交换行为、容器内存的软性限制、是否终止占用过多内存的容器、容器被终止的优先级。


Docker 还提供了来满足服务访问的基本需求:一个是允许映射容器内应用的服务端口到本地宿主机;另一个是通过互联机制实现多个容器间通过容器名来快速访问。

Docker 处理流程

假如我们要启动一个新的 Docker 应用“app1”,整个工作的处理流程如下:


  1. Docker 客户端向守护进程发送启动 app1 指令。

  2. Docker 守护进程发请求给 Docker 镜像仓库,在仓库中检索 app1 的软件镜像。

  3. 如果找到 app1 应用,就把它下载到我们的服务器上。

  4. Docker 守护进程启动 app1 应用。

  5. 把启动 app1 应用是否成功的结果返回给 Docker 客户端。

Docker 的优势

Docker 支持将应用打包进一个可移植的容器中,重新定义了应用开发、测试、部署上线的过程,核心理念是“一次构建,到处运行”,其典型应用场景是在开发和运维上提供持续集成和持续部署的服务。

  • 标准化和版本控制:Docker 是软件工程领域的“标准化”交付组件。还可以像 Git 仓库一样,可以让你提交变更到 Docker 镜像中,并通过不同的版本来管理它们。

  • 一次构建,多次交付:Docker 具有可移植性。基于 Docker 容器镜像能够很容易地移植到其他云厂商的平台上,应用不用做任何改动。

  • 应用隔离:Docker 能够确保每个容器都拥有自己的资源,并且和其他容器是隔离的。你可以用不同的容器来运行使用不同堆栈的应用程序。

Docker 分层设计

为了实现“一次构建,到处运行”的目标,我们需要把这些依赖全部打包到一起,以屏蔽环境的差异性。解决部署包过大使分发下载慢的问题,Docker 引入了分层的概念。把一个应用分为任意多个层,比如操作系统是第一层,依赖的库和第三方软件是第二层,应用的软件包和配置文件是第三层。如果两个应用有相同的底层,就可以共享这些层。


为了避免冲突问题,Docker 参考了 Java 的子类继承机制,设计了有优先级的层次,上层和下层有相同的文件和配置时,上层覆盖下层,以上层的数据为准。


在 Docker 的官方仓库里,只需有完整的文件系统和程序包,没有动态生成新文件的需求。当把它下载到宿主机上运行以对外提供服务时,有可能修改文件(比如应用启动参数、日志输出),需要有空白层用于写时拷贝。Docker 把这两种不同的状态做了区分,分别叫作镜像(image)和容器(container)。


镜像是指分层的、可被 LXC/Libcontainer 理解的文件存储格式。仓库中的应用都是以镜像的形式存在的,把镜像从 Docker 镜像仓库中下载到本地宿主机,以这个镜像为模板启动应用,就叫作容器。镜像是只读的,容器是可读写的。


Docker 中的镜像分为基础镜像(base image)和扩展镜像。基础镜像为上层应用提供操作系统内核,通常是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu、Debian、CentOS 等。Linux 系统包含内核空间 kernel 和用户空间 rootfs 两部分,容器只使用各自的 rootfs,但共用宿主机的 kernel。一台宿主机上的所有容器都共用宿主机的 kernel,在容器中无法对 kernel 升级。


若多个镜像从相同的基础镜像构建而来,那么 Docker 宿主机只需在磁盘上保存一份基础镜像,同时内存中也只需加载一份基础镜像,就可以为所有容器服务了,而且镜像的每一层都可以被共享。创建镜像时,分层可以让 Docker 只保存我们添加和修改的部分内容。


当一个容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。容器层可以读写,容器所有文件变更都发生在这一层,而镜像层只允许读取。

Docker 数据管理

Docker 中的数据主要分为两类:非持久化数据和持久化数据。


非持久化数据是不需要保存的运行过程临时数据,每个 Docker 容器都有自己的非持久化存储。非持久化存储自动创建,从属于容器,生命周期与容器一致,这意味着删除容器也会删除全部的非持久化数据。


Docker 提供了多种存储驱动(storage-driver)来实现不同方式的数据存储,下面是常用的几种存储驱动。

  • AUFS:AUFS 代表 AnotherUnionFS,是一种联合文件系统(UnionFilesystem,Union FS),是文件级的存储驱动。AUFS 能透明覆盖一个或多个现有文件系统的层状文件系统,把多层合并成文件系统的单层表示。简单来说,就是支持将不同目录挂载到同一个虚拟文件系统下的文件系统。

  • OverlayFS:Overlay 是 Linux 内核 3.18 后支持的,也是一种 Union FS。和 AUFS 的多层不同的是,Overlay FS 只有两层——一个 upper 文件系统和一个 lower 文件系统,分别代表 Docker 的镜像层和容器层。

  • Device mapper:是 Linux 内核 2.6.9 后支持的,提供了一种从逻辑设备到物理设备的映射框架机制。在该机制下,用户可以很方便地根据自己的需要制定实现存储资源的管理策略。AUFS 和 OverlayFS 都是文件级存储,而 Device mapper 是块级存储。

  • Btrfs:Btrfs 称为下一代写时拷贝文件系统,并入 Linux 内核,也是文件级存储,但可以像 Device mapper 一样直接操作底层设备。Btrfs 把文件系统的一部分配置为一个完整的子文件系统,称为 subvolume。

  • ZFS:ZFS 文件系统是一个革命性的、全新的文件系统,它从根本上改变了文件系统的管理方式。ZFS 创建在虚拟的、称为“zpool”的存储池上。每个存储池由若干虚拟设备(virtual devices、vdevs)组成。这些虚拟设备既可能是原始磁盘,也可能是一个 RAID1 镜像设备,或是非标准独立冗余磁盘阵列(Redundant Arrays of Independent Disks,RAID)等级的多磁盘组。zpool 上的文件系统可以使用这些虚拟设备的总存储容量。


默认容器的数据保存在容器的可读写层,当容器被删除时,其上的数据将会丢失。为了实现数据的持久性,需要选择一种数据持久化技术来保存数据,当前有以下几种方式。无论选择哪种挂载类型,从容器内部看都没有区别,它们都是目录或者文件。数据都寄存在宿主机上,只不过具体位置有所区别。

  • 数据卷(volume):也叫 Docker 容器管理数据卷(Docker managedvolume),在 Docker 启动时用-v 或--volume 参数跟宿主机目录做绑定。如果是 Docker 17.06 或更高的版本,推荐使用--mount(同绑定挂载)。

  • 绑定挂载(bind mount):将宿主机中的文件、目录挂载到容器上,在 Docker 启动时用 mount 参数与宿主机目录做绑定。此方式与 Linux 系统的挂载方式很相似。

  • tmpfs 挂载:tmpfs 挂载类型文件与普通文件的区别是只存在于宿主机内存中,不会持久化。

  • 数据卷容器:如果用户需要在多个容器之间共享一些持续更新的数据,最简单的方式是使用数据卷容器。数据卷容器也是一个容器,但是它是专门用来提供数据卷供其他容器挂载的。


Docker 能够集成外部存储系统,使集群的多个节点间共享外部存储数据变得可行。例如网络文件系统(Network File System,NFS)或 Amazon S3 可以共享应用到多个 Docker 宿主机,因此无论容器或服务副本运行在哪个节点上,都可以共享该存储。

Docker 网络

为了支持网络协议栈的多个实例,Linux 在网络协议栈中引入了网络命名空间。这些独立的协议栈被隔离到不同的命名空间中。Docker 正是利用了网络的命名空间特性,实现不同容器之间的网络隔离。


Docker 的本地网络实现其实利用了 Linux 上的网络命名空间和虚拟网络设备。Linux 的网络虚拟化是 LXC 项目中的一个子项目,LXC 包括文件系统虚拟化、进程空间虚拟化、用户虚拟化、网络虚拟化等,Docker 就是使用 LXC 的网络虚拟化来模拟多个网络环境。


Linux 网络虚拟化的类型如下:

  • 桥接:创建一个虚拟桥设备(网桥),网桥可以理解为一个软件交换机,负责挂载其上的接口之间进行包转发。

  • 隔离:仅将需要互相通信的虚拟机的后半段网卡添加到同一个虚拟的桥设备上,即可完成虚拟机之间的通信,且与外网仍旧是物理机隔离。

  • 路由:将虚拟机关联至虚拟桥设备上,再给桥设备配置一个与虚拟机同段的 IP 地址(内网地址)作为虚拟机的网关(物理网卡是连接外网的,所以应该与内网 IP 地址不是一个段),最后打开物理主机的核心转达功能,即可让虚拟机 ping 外部主机,但是外部主机无法发送相应包,因为外部主机没有到达虚拟机的路由。

  • NAT:在路由模型的基础上,为其配置源地址转换(Source NAT,SNAT)规则,即可完成真正的虚拟机与外网通信,且自己使用的是内网地址。


Docker 中的网络接口默认是虚拟接口,虚拟接口的最大优势是转发效率高。这是因为 Linux 通过在内核中进行数据复制来实现虚拟接口之间的数据转发,即发送接口的发送缓存中的数据包将直接复制到接收接口的接收缓存中,无须通过外部物理网络设备进行交换。


Linux 虚拟化网络都是基于网络命名空间 netns 实现的,netns 可以创建一个完全隔离的新网络环境,这个环境包括一个独立的网卡空间、路由表、ARP 表、IP 地址表、iptables 等。总之,与网络有关的组件都是独立的。


Docker 网络架构由 3 个主要部分构成:CNM、Libnetwork 和驱动。Docker 守护进程通过调用 Libnetwork 对外提供的 API 完成网络的创建和管理等功能;Libnetwork 中则使用了 CNM 来完成网络功能的提供;而 CNM 中主要有沙盒(sandbox)、接入点(endpoint)和网络(network)3 种组件。


CNM 模型包括 3 种基本组件。

  • 沙盒:代表一个容器所在独立的网络栈(准确地说,是其网络命名空间),包括以太网接口、端口、路由表以及 DNS 配置。

  • 接入点:代表网络上可以挂载容器的虚拟接口,会分配 IP 地址。就像普通网络接入点一样,接入点主要负责创建连接;在 CNM 模型中,接入点负责将沙盒连接到网络。

  • 网络:可以连通多个接入点的一个虚拟子网,是 IEEE 802.1d 网桥(虚拟交换机)的软件实现。


目前 CNM 支持 4 种网络驱动类型:null、bridge、overlay、remote。不同网络驱动的特性简单说明如下:

  • null:不提供网络服务,容器启动后无网络连接。

  • bridge:即 Docker 传统上默认用 Linux 网桥和 iptables 实现的单机网络。

  • overlay:即用虚拟扩展局域网(Virtual eXtensible Local Area Network,VXLAN)隧道技术实现的跨宿主机容器网络。

  • remote:扩展类型,预留给其他外部实现的方案,由第三方编写网络驱动,如 Calico、Contiv、Kuryr、Weave 等。


覆盖网络是理想的容器间通信方式,具备良好的网络伸缩性。Docker 为覆盖网络提供了本地驱动,其背后是基于 Libnetwork 以及相应的 overlay 驱动来构建的,使得创建覆盖网络非常简单,只需在 docker networkcreate 命令中添加 --d overlay 参数。


overlay 驱动默认采用 VXLAN 协议,在 IP 地址可以互相访问的多个宿主机之间搭建隧道,让容器可以互相访问。Docker 使用 VXLAN 隧道技术创建了虚拟二层覆盖网络。在 VXLAN 的设计中,允许用户基于已经存在的三层网络架构创建虚拟的二层网络。


Docker 容器的网络访问控制主要通过 Linux 上的 iptables 防火墙软件来进行管理和实现。iptables 是 Linux 系统流行的防火墙软件,大部分发行版中自带 iptables。


Docker 的 forward 规则默认允许所有的外部 IP 访问容器时,可以通过在 filter 的 Docker 链上添加规则对外部的 IP 访问做出限制。不仅是与外部通信,Docker 容器之间互相通信也受到 iptables 规则限制。

Docker 三剑客

Docker Machine、Docker Compose 和 Docker Swarm 是 Docker 原生提供的三大编排工具,用来部署管理多个宿主机的 Docker 容器集群,号称“Docker 三剑客”。随着 Docker 对 K8S 的支持,以及 K8S 的普及,这里的三剑客重在了解。


Docker Machine 是 Docker 官方提供的一个命令行工具,它可以帮助我们在远程的机器上安装 Docker,或在虚拟机 host 上直接安装虚拟机并在虚拟机中安装 Docker。用于配置和管理 Docker 化的主机(带有 Docker 引擎的主机),运维人员可以使用一台 Docker Machine 主机在一个或多个虚拟机上安装 Docker 引擎。


Docker Machine 是一个框架,比较开放。对于任何提供虚拟机服务的平台,只要在这个框架下开发针对该平台的驱动,Docker Machine 就可以集成到该平台,在该平台上执行创建、删除、启动、停止 Docker 等行为。


Docker 的最佳实践是一个容器只运行一个进程,因此运行多个微服务就要运行多个容器。多个容器协同工作需要一个有效的工具来管理它们,定义这些容器如何相互关联,这就需要容器编排工具。


作为 Docker 官方的编排工具,Docker Compose 的重要性不言而喻,它可以让用户编写一个简单的模板文件。模板文件是 Docker Compose 的核心,涉及的指令关键字比较多,但是大部分的指令与 docker run 相关参数的含义是类似的,默认的模板名是 docker-compose.yml(YAML 文件格式)。利用模板文件,用户可以快速地创建和管理基于 Docker 容器的应用集群,并定义多容器之间的关系。一个 docker-compose up 命令就可以运行完整的应用。


Docker Compose 是在单个服务器或主机上创建多个容器的工具,而 DockerSwarm 可以在多个服务器或主机上创建容器集群服务,将一群 Docker 宿主机抽象成一个单一的虚拟主机。


DockerSwarm 的优势之一是原生支持 Docker API,给用户的使用带来极大的便利。Swarm 使用标准的 Docker API 作为其前端的访问入口,因此各种形式的基于标准 API 的 Docker 客户端工具(Docker Compose、Docker SDK、各种管理软件等)均可以直接与 Swarm 通信,甚至 Docker 本身都可以很容易地与 Swarm 集成,这大大方便了用户将原本基于单节点的系统移植到 Swarm 上。

Swarm 的具体工作流程:Docker 客户端发送请求给 Swarm,Swarm 守护进程是一个调度器(scheduler)加路由器(router);Swarm 处理请求并根据调度策略发送至相应的 Docker 节点;Docker 节点执行相应的操作并返回响应。


节点是 Swarm 集群的最小资源单位,每个节点实际上都是一台 Docker 主机(物理机或虚拟机)。Swarm 集群中的节点分为两种。

  • 管理节点(manager nodes):负责响应外部对集群的操作请求,并维护集群中的资源,监控集群状态,分发任务给工作节点。一般推荐每个集群设置 5~7 个管理节点。

  • 工作节点(worker nodes):负责执行管理节点安排的具体任务,为了提高资源利用率,默认情况下,管理节点自身也是工作节点。每个工作节点上运行代理(agent)来汇报任务完成情况。


Swarm 集群是典型的主从(master-slave)架构,通过发现服务来选举中心管理节点,各个节点上运行代理接受中心管理节点的统一管理,集群会自动通过 Raft 协议分布式选举出中心管理节点,无须额外的发现服务支持,避免了单点的瓶颈问题,同时也内置了 DNS 的负载均衡和对外部负载均衡机制的集成支持。


Swarm 的配置和状态信息保存在一套位于所有管理节点上的分布式 etcd 数据库中。该数据库运行于内存中,并保持数据的最新状态,并且它几乎不需要任何配置,只作为 Swarm 的一部分被安装,无须管理。


任务是 Swarm 集群中最小的调度单位,即一个指定的应用容器。当用户通过创建或更新服务声明一个期望状态的服务时,调度器通过调度任务来实现期望的状态。例如,指定一个服务始终保持运行 3 个 HTTP 实例,调度器就创建 3 个任务,每个任务运行一个容器。容器是任务的实例化。如果一个 HTTP 容器之后出现故障停止,此任务被标志为失败,调度器就会创建一个新的任务来生成一个新容器。任务是一个单向机制,单向地执行一个系统状态,如 assigned、prepared、running 等。如果一个任务失败了,调度器会删除这个任务和它的容器,然后创建一个新的任务来替换它。

Docker 常用命令

Docker 基础命令

docker info     检查当前容器的安装情况(包括镜像数、容器数、多少个物理机节点等)docker version  查看当前安装的Docker版本
复制代码

Docker 生命周期管理命令

docker run -d apache -p 8080:80 <docker镜像ID/镜像名称>   用某一个镜像在后台运行一个容器,run命令加上-d参数可以在后台运行,-name是指定容器名字。-p将宿主机的8080端口映射到容器里的80端口。docker create -name mynginx nginx:latest   创建一个新的容器,但不启动它。docker start/stop/restart <docker容器ID>    启动/停止/重启某个容器。docker kill <docker容器ID>  终止一个运行中的容器,kill不管容器是否同意,直接执行kill -9 强行终止。docker rm -vf <docker容器ID> 删除一个或多个(空格分隔)容器。docker exec -it <docker容器ID> bash 在运行的容器中执行命令,进入某一个容器。
复制代码

Docker 容器操作命令

docker ps -a |grep xxx 显示某一个组件XXXX的容器列表。docker inspect <docker容器ID> |grep -i host 查看容器所在宿主机IP地址。docker top mynginx  查看容器中运行的进程信息,支持ps命令参数。docker stats <docker容器ID> 实时显示容器自由(cpu、内存)使用统计,在容器里用free、cat/proc/meminfo等指令看到的是物理机内存,并非容器的。docker events -sine="14673202400" 从服务器获取实时事件。docker logs <docker容器ID> 查看容器内的标准日志输出。docker port mynginx 列出指定的容器的端口映射。docker cp ./aa.txt 4c9328e:/tmp/ 用于容器与宿主机之间的数据复制。docker diff 11dfd1f54c1b 从创建容器以来,列出容器文件系统中已更改的文件和目录。docker update -memmory=16g -memory-swap=20g{cid}  修改运行中的容器配置,即时生效,无须重启。
复制代码

Docker 镜像管理命令

docker images  列出本地宿主机上的镜像。docker history runoob/ubuntu:v3 查看指定镜像的分层结构以及创建历史。docker image inspect a1235938  获取镜像的元数据信息(如镜像分层信息)。docker rmi -f <image镜像ID>  删除本地一个或多个镜像(空格分开)。docker tag ubuntu:15:10 runoob/ubuntu:v3 标记本地镜像,将ubuntu:15:10标记为runoob/ubuntu:v3。docker build -t repos_local/centos-jdk7-tomcat7 使用Dockefile文件构建Docker镜像,-t是设置tag名称。docker export -o my.tar a123457 将一个容器导出为文件,用于以后用import命令将容器导入为一个新的镜像。docker import my.tar runoob/ubuntu:v3 从归档文件中创建镜像。docker save -o my.tar runoob/ubuntu:v3 将指定镜像保存成tar归档文件。docker load -input my.tar  加载使用docker save命令导出的镜像docker login -u <用户名> -p <密码> 登录一个Docker镜像仓库,如果未指定镜像仓库地址,则默认为官方仓库Docker Hub。docker logout 退出一个Docker镜像仓库,如果未指定镜像仓库地址,则默认为官方仓库Docker Hub。docker pull registry.XXX.com/apache-php5:latest  从镜像仓库中拉取或者更新指定镜像到本地docker push nginx:v1 将本地镜像的上传到镜像仓库,执行该命令前要先登录镜像仓库docker search nginx  从镜像仓库查找镜像
复制代码

发布于: 2022 年 04 月 13 日阅读数: 38
用户头像

InfoQ签约作者 2018.11.30 加入

热爱生活,收藏美好,专注技术,持续成长

评论

发布
暂无评论
万字长文:直击关于Dorcker所必须了解的知识_Docker_穿过生命散发芬芳_InfoQ写作平台