一篇文章带你吃透 Docker 原理
容器的实现原理
从本质上,容器其实就是一种沙盒技术。就好像把应用隔离在一个盒子内,使其运行。因为有了盒子边界的存在,应用于应用之间不会相互干扰。并且像集装箱一样,拿来就走,随处运行。其实这就是 PaaS 的理想状态。
实现容器的核心,就是要生成限制应用运行时的边界。我们知道,编译后的可执行代码加上数据,叫做程序。而把程序运行起来后,就变成了进程,也就是所谓的应用。如果能在应用启动时,给其加上一个边界,这样不就能实现期待的沙盒吗?
在 Linux 中,实现容器的边界,主要有两种技术 Cgroups
和 Namespace
. Cgroups
用于对运行的容器进行资源的限制,Namespace
则会将容器隔离起来,实现边界。
这样看来,容器只是一种被限制的了特殊进程而已。
容器的隔离:Namespace
在介绍 Namespace
前,先看一个实验:
# 使用 python3.6.8 的官方镜像,建立了一个运行 django 的环境
# 进入该容器后,使用 ps 命令,查看运行的进程
root@8729260f784a:/src# ps -A
PID TTY TIME CMD
1 ? 00:01:22 gunicorn
22 ? 00:01:20 gunicorn
23 ? 00:01:24 gunicorn
25 ? 00:01:30 gunicorn
27 ? 00:01:16 gunicorn
41 pts/0 00:00:00 bash
55 pts/0 00:00:00 ps
可以看到,容器内 PID =1 的进程,是 gunicorn 启动的 django 应用。熟悉 Linux 的同学都知道,PID =1 的进程是系统启动时的第一个进程,也称 init 进程。其他的进程,都是由它管理产生的。而此时,PID=1 确实是 django 进程。
接着,退出容器,在宿主机执行 ps 命令
# 环境为 Centos7
[root@localhost ~]# ps -ef | grep gunicorn
root 9623 8409 0 21:29 pts/0 00:00:00 grep --color=auto gunicorn
root 30828 30804 0 May28 ? 00:01:22 /usr/local/bin/python /usr/local/bin/gunicorn -c gunicorn_config.py ctg.wsgi
root 31171 30828 0 May28 ? 00:01:20 /usr/local/bin/python /usr/local/bin/gunicorn -c gunicorn_config.py ctg.wsgi
root 31172 30828 0 May28 ? 00:01:24 /usr/local/bin/python /usr/local/bin/gunicorn -c gunicorn_config.py ctg.wsgi
root 31174 30828 0 May28 ? 00:01:30 /usr/local/bin/python /usr/local/bin/gunicorn -c gunicorn_config.py ctg.wsgi
root 31176 30828 0 May28 ? 00:01:16 /usr/local/bin/python /usr/local/bin/gunicorn -c gunicorn_config.py ctg.wsgi
如果以宿主机的视角,发现 django 进程 PID 变成了 30828. 这也就不难证明,在容器中,确实做了一些处理。把明明是 30828 的进程,变成了容器内的第一号进程,同时在容器还看不到宿主机的其他进程。这也说明容器内的环境确实是被隔离了。
这种处理,其实就是 Linux 的 Namespace
机制。比如,上述将 PID 变成 1 的方法就是通过PID Namespace
。在 Linux 中创建线程的方法是 clone
, 在其中指定 CLONE_NEWPID
参数,这样新创建的进程,就会看到一个全新的进程空间。而此时这个新的进程,也就变成了 PID=1 的进程。
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
在 Linux 类似于 PID Namespace 的参数还有很多,比如:
容器的限制:Cgroups
通过 Namespace 技术,我们实现了容器和容器间,容器与宿主机之间的隔离。但这还不够,想象这样一种场景,宿主机上运行着两个容器。虽然在容器间相互隔离,但以宿主机的视角来看的话,其实两个容器就是两个特殊的进程,而进程之间自然存在着竞争关系,自然就可以将系统的资源吃光。当然,我们不能允许这么做的。
Cgroups
就是 Linux 内核中用来为进程设置资源的一个技术。
Linux Cgroups 全称是 Linux Control Group,主要的作用就是限制进程组使用的资源上限,包括 CPU,内存,磁盘,网络带宽。
还可以对进程进行优先级设置,审计,挂起和恢复等操作。
在之前的版本中,可通过 libcgroup tools
来管理 cgroup, 在 RedHat7 后,已经改为通过 systemctl
来管理。
我们知道,systemd
在 Linux 中的功能就是管理系统的资源。而为了管理的方便,衍生出了一个叫 Unit
的概念,比如一个 unit
可以有比较宽泛的定义,比如可以表示抽象的服务,网络的资源,设备,挂载的文件系统等。为了更好的区分,Linux 将 Unit
的类型主要分为 12 种。
类型 | 作用 |
---|---|
.automount |
用于自动挂载配置的挂载点 |
.swap |
描述系统的交换区,反映了设备或文件的路径 |
.target |
在系统启动或者改变状态时,为其他 unit 提供同步点 |
.path |
定义的文件路径,用于激活。 |
.service |
一个服务或者一个应用,具体定义在配置文件中。 |
.socket |
一个网络或者 IPC socket,FIFO buffer. |
.device |
描述一个需要被 systemd udev 或 sysfs 文件系统管理的设备 |
.mount |
定义的挂载点 |
.timer |
定时器 |
.snapshot |
被 systemctl snapshot 命令自动创建的单元 |
.slice |
用于关联 Linux Control Group 节点,根据关联的 slice 来限制进程。一个管理单元的组。Slice 并不包含任何进程,仅仅管理由 service 和 scope 组成的层级结构。 |
.scope |
systemd 从 bus 接口收到消息后自动创建。Scope 封装了任意进程通过 fork() 函数开启或停止的进程,并且在 systemd 运行时注册。例如:用户 sessions,容器和虚拟机。 |
在 Cgroup
中,主要使用的是 slice, scope and service 这三种类型。
如创建一个临时 cgroup, 然后对其启动的进程进行资源限制:
# 创建一个叫 toptest 的服务,在名为 test 的 slice 中运行
[root@localhost ~]# systemd-run --unit=toptest --slice=test top -b
Running as unit toptest.service.
现在 toptest 的服务已经运行在后台了
# 通过 systemd-cgls 来查看 Cgroup 的信息
[root@localhost ~]# systemd-cgls
├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
├─test.slice
│ └─toptest.service
│ └─6490 /usr/bin/top -b
# 通过 systemctl status 查看服务的状态
[root@localhost ~]# systemctl status toptest
● toptest.service - /usr/bin/top -b
Loaded: loaded (/run/systemd/system/toptest.service; static; vendor preset: disabled)
Drop-In: /run/systemd/system/toptest.service.d
└─50-Description.conf, 50-ExecStart.conf, 50-Slice.conf
Active: active (running) since Tue 2020-06-02 14:01:01 CST; 3min 50s ago
Main PID: 6490 (top)
CGroup: /test.slice/toptest.service
└─6490 /usr/bin/top -b
现在对运行的 toptest 服务进行资源的限制。
# 先看下,没有被限制前的 Cgroup 的信息, 6490 为进程 PID
[root@localhost ~]# cat /proc/6490/cgroup
11:pids:/test.slice
10:blkio:/test.slice
9:hugetlb:/
8:cpuset:/
7:memory:/test.slice
6:devices:/test.slice
5:net_prio,net_cls:/
4:perf_event:/
3:freezer:/
2:cpuacct,cpu:/test.slice
1:name=systemd:/test.slice/toptest.service
# 对其使用的 CPU 和 内存进行限制
systemctl set-property toptest.service CPUShares=600 MemoryLimit=500M
# 再次查看 Cgroup 的信息,发现在 cpu 和 memory 追加了一些内容。
[root@localhost ~]# cat /proc/6490/cgroup
11:pids:/test.slice
10:blkio:/test.slice
9:hugetlb:/
8:cpuset:/
7:memory:/test.slice/toptest.service
6:devices:/test.slice
5:net_prio,net_cls:/
4:perf_event:/
3:freezer:/
2:cpuacct,cpu:/test.slice/toptest.service
1:name=systemd:/test.slice/toptest.service
这时可以在 /sys/fs/cgroup/memory/test.slice
和 /sys/fs/cgroup/cpu/test.slice
目录下,多出了一个叫 toptest.service 的目录。
在其目录下 cat toptest.service/cpu.shares
可以发现,里面的 CPU 被限制了 600.
回到 Docker,其实 docker 和我们上面做的操作基本一致,具体需要限制哪些资源就是在 docker run
里指定:
$ docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash
关于 docker 具体的限制,可以在 sys/fs/cgroup/cpu/docekr/
等文件夹来查看。
容器的文件系统:容器镜像 - rootfs
现在我们知道,容器技术的核心就是通过 Namespace
限制了容器看到的视野,通过 Cgroup
限制了容器可访问的资源。 但关于 Mount Namespace
还有一些特殊的地方,需要着重关注下。
Mount Namespace
特殊之处在于,除了在修改时需要进程对文件系统挂载点的认证,还需要显式声明需要挂载那些目录。在 Linux 系统中,有一个叫 chroot
的命令,可以改变进程的根目录到指定的位置。而 Mount Namespace
正是基于 chroot 的基础上发展出来的。
在容器内,应该看到完全独立的文件系统,而且不会受到宿主机以及其他容器的影响。这个独立的文件系统,就叫做容器镜像。它还有一个更专业的名字叫 rootfs.
rootfs
中包含了一个操作系统所需要的文件,配置和目录,但并不包含系统内核。 因为在 Linux 中,文件和内核是分开存放的,操作系统只有在开启启动时才会加载指定的内核。这也就意味着,所有的容器都会共享宿主机上操作系统的内核。
在 PaaS 时代,由于云端和本地的环境不同,应用打包的过程,一直是比较痛苦的过程。但有了 rootfs
,这个问题就被很好的解决了。因为在镜像内,打包的不仅仅是应用,还有所需要的依赖,都被封装在一起。这就解决了无论是在哪,应用都可以很好的运行的原因。
不光这样,rootfs
还解决了可重用性的问题,想象这个场景,你通过 rootfs
打包了一个包含 java 环境的 centos 镜像,别人需要在容器内跑一个 apache
的服务,那么他是否需要从头开始搭建 java 环境呢?docker 在解决这个问题时,引入了一个叫层的概念,每次针对 rootfs
的修改,都只保存增量的内容,而不是 fork 一个新镜像。
层级的想法,同样来自于 Linux,一个叫 union file system (联合文件系统)。它最主要的功能就是将不同位置的目录联合挂载到同一个目录下。对应在 Docker 里面,不同的环境则使用了不同的联合文件系统。比如 centos7 下最新的版本使用的是 overlay2
,而 Ubuntu 16.04 和 Docker CE 18.05 使用的是 AuFS.
可以通过 docker info
来查询使用的存储驱动,我这里的是 overlay2
。
[root@localhost ~]# docker info
Client:
Debug Mode: false
Server:
Containers: 4
Running: 4
Paused: 0
Stopped: 0
Images: 4
Server Version: 19.03.8
Storage Driver: overlay2
接着我们来了解下,Overlay2 的文件系统在 docker 中是如何使用的?
Overlay2
在 Linux 的主机上,OverlayFS
一般有两个目录,但在显示时具体会显示为一个目录。这两个目录被称为层,联合在一起的过程称为 union mount
. 在其下层的目录称为 lowerdir
, 上层的目录称为 upperdir
. 两者联合后,暴露出来的视图称为 view
. 听起来有点抽象,先看下整体结构:
可以看到,lowerdir
其实对应的就是镜像层,upperdir
对应的就是容器器。而 merged
对应的就是两者联合挂载之后的内容。而且我们发现,当镜像层和容器层拥有相同的文件时,会以容器层的文件为准(最上层的文件为准)。通常来说,overlay2
支持最多 128 lower 层。
下面实际看下容器层和镜像具体的体现,我这台 linux 主机上,运行着 4 个 container。
Docker 一般的存储位置在 /var/lib/docker
,先看下里面的结构:
[root@localhost docker]# ls -l /var/lib/docker
total 16
drwx------. 2 root root 24 Mar 4 03:39 builder
drwx--x--x. 4 root root 92 Mar 4 03:39 buildkit
drwx------. 7 root root 4096 Jun 1 10:36 containers
drwx------. 3 root root 22 Mar 4 03:39 image
drwxr-x---. 3 root root 19 Mar 4 03:39 network
drwx------. 69 root root 8192 Jun 1 15:01 overlay2
drwx------. 4 root root 32 Mar 4 03:39 plugins
drwx------. 2 root root 6 Jun 1 15:00 runtimes
drwx------. 2 root root 6 Mar 4 03:39 swarm
drwx------. 2 root root 6 Jun 1 15:01 tmp
drwx------. 2 root root 6 Mar 4 03:39 trust
drwx------. 3 root root 45 May 18 10:28 volumes
需要着重关注的是 container
, image
, overlay2
这几个文件夹。
container
:这个不用多说,正在运行或创建的容器会在这个目录下。- image:对应记录的就是镜像。
overlay2
:记录的是每个镜像下包含的lowerrdir
.
之前提到,unionfs 的实现可能有多种,比如 overlay2,aufs,devicemapper
等。那么自然在 image 文件夹下,就会存在多种驱动的文件夹,:
image/
└── overlay2
├── distribution
├── imagedb
│ ├── content
│ └── metadata
├── layerdb
│ ├── mounts
│ ├── sha256
│ └── tmp
└── repositories.json
这里的 imagedb
和 layerdb
, 就是存储元数据的地方。之前我们了解到,容器的文件系统构成就是通过 image 层 和 container 层联合构成的,而每个 image 可能是由多个层构成。这就意味着,每个层可能会被多个 image 引用。那么之间是如何关联的呢?答案就在 imagedb
这个文件下。
这里我以 mysql
镜像为例:
# 查看 mysql 的镜像 id
[root@localhost docker]# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ctg/mysql 5.7.29 84164b03fa2e 3 months ago 456MB
# 进入到 imagedb/content/sha256 目录, 可以找到对应的镜像 id
[root@localhost docker]# ls -l image/overlay2/imagedb/content/sha256/
...
-rw-------. 1 root root 6995 Apr 27 02:45 84164b03fa2ecb33e8b4c1f2636ec3286e90786819faa4d1c103ae147824196a
# 接着看下里面记录的内容, 这里截取有用的部分
cat image/overlay2/imagedb/content/sha256/84164b03fa2ecb33e8b4c1f2636ec3286e90786819faa4d1c103ae147824196a
{
.........
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:f2cb0ecef392f2a630fa1205b874ab2e2aedf96de04d0b8838e4e728e28142da",
"sha256:a9f6b7c7101b86ffaa53dc29638e577dabf5b24150577a59199d8554d7ce2921",
"sha256:0c615b40cc37ed667e9cbaf33b726fe986d23e5b2588b7acbd9288c92b8716b6",
"sha256:ad160f341db9317284bba805a3fe9112d868b272041933552df5ea14647ec54a",
"sha256:1ea6ef84dc3af6506c26753e9e2cf7c0d6c1c743102b85ebd3ee5e357d7e9bc4",
"sha256:6fce4d95d4af3777f3e3452e5d17612b7396a36bf0cb588ba2ae1b71d139bab9",
"sha256:6de3946ea0137e75dcc43a3a081d10dda2fec0d065627a03800a99e4abe2ede4",
"sha256:a35a4bacba4d5402b85ee6e898b95cc71462bc071078941cbe8c77a6ce2fca62",
"sha256:1ff9500bdff4455fa89a808685622b64790c321da101d27c17b710f7be2e0e7e",
"sha256:1cf663d0cb7a52a3a33a7c84ff5290b80966921ee8d3cb11592da332b4a9e016",
"sha256:bcb387cbc5bcbc8b5c33fbfadbce4287522719db43d3e3a286da74492b7d6eca"
]
}
}
可以看到 mysql
镜像由 11 层组成,其中 f2cb
是最低层,bcb3
是最上层。
接着,我们看下 layerdb
的内容:
[root@localhost docker]# ls -l image/overlay2/layerdb/
total 8
drwxr-xr-x. 6 root root 4096 May 13 13:38 mounts
drwxr-xr-x. 39 root root 4096 Apr 27 02:51 sha256
drwxr-xr-x. 2 root root 6 Apr 27 02:51 tmp
# 首先看下 sha256 目录下的内容
[root@localhost docker]# ls -l image/overlay2/layerdb/sha256/
total 0
....
drwx------. 2 root root 71 Apr 27 02:45 bbb9cccab59a16cb6da78f8879e9d07a19e3a8d49010ab9c98a2c348fa116c87
drwx------. 2 root root 71 Apr 27 02:45 f2cb0ecef392f2a630fa1205b874ab2e2aedf96de04d0b8838e4e728e28142da
....
可以发现,在这里仅能找到最底层的层 ID,原因在于层之间的关联是通过 chainID 的方式保存的,简单来说就是通过 sha256 算法后能计算出一层的容器 id.
比如这里,最底层 id 是 f2cb0ecef392f2a630fa1205b874ab2e2aedf96de04d0b8838e4e728e28142da
, 上一层 id 是 a9f6b7c7101b86ffaa53dc29638e577dabf5b24150577a59199d8554d7ce2921
, 那么对应在 sha256 目录下的下一层 id 的计算方法就是:
[root@localhost docker]# echo -n "sha256:f2cb0ecef392f2a630fa1205b874ab2e2aedf96de04d0b8838e4e728e28142da sha256:a9f6b7c7101b86ffaa53dc29638e577dabf5b24150577a59199d8554d7ce2921" | sha256sum
bbb9cccab59a16cb6da78f8879e9d07a19e3a8d49010ab9c98a2c348fa116c87 -
接着我们可以在 sha256
目录下,找到 bbb9..
这层的内容。
OK,现在我们已经把镜像和层关联起来,但之前说过,image 下村的都是元数据。真实的 rootfs 其实在另一个地方 - /docker/overlay2
下。
# 通过查询 cache-id,得到就是真实的 rootfs 层
[root@localhost docker]# cat image/overlay2/layerdb/sha256/f2cb0ecef392f2a630fa1205b874ab2e2aedf96de04d0b8838e4e728e28142da/cache-id
2996b24990e75cbd304093139e665a45d96df8d7e49334527827dcff820dbf16[
进入到 /docker/overlay2
下查看:
[root@localhost docker]# ls -l overlay2/
total 4
...
drwx------. 3 root root 47 Apr 27 02:45 2996b24990e75cbd304093139e665a45d96df8d7e49334527827dcff820dbf16
...
drwx------. 2 root root 4096 May 13 13:38 l
这样真实的 rootfs 层也被找到了。
这里重新梳理下,我们先是在 mage/overlay2/imagedb/content/sha256/
,根据 image id 查看该 image 具有所有的层ID,然后根据最底层ID和上层ID通过 sha256 计算得到,引用的上一层 ID, 依次类推,关联所有的层。最后通过每一层的 cache-id
,将元数据和真实的 rootfs 层数据对应起来了。
最后总结一下,rootfs 的构成。
每个 rootfs 由镜像层(lowerdir)和 容器层(upperdir)构成,其中镜像层只能只读,而容器层则能读写。而且镜像层可有最多128层构成。
其实,rootfs 构成还有另外一层,但由于在进行提交或编译时,不会把这层加进去,所以就没把这层算在rootfs里面,但实际上存在的。
在之前我们查看 ls -l /var/lib/docker/overlay2/
下镜像层,会看到好几个以 -init
结尾的目录,而且数量恰好等于容器的数量。这层夹在镜像层之上,容器层之下。是由 docker 内部单独生成的一层,专门用于存放 etc/hosts、/etc/resolv.conf
等配置信息。存在的目的,是由于用户在容器启动时,需要配置一些特定的值,比如 hostname,dns 等,但这些配置仅对当前容器有效,放到其他环境下自然有别的配置,所以这层被单独拿出来,在提交镜像时,忽略这一层。
容器与虚拟机技术的对比
下面这张图是 docker 官方中,截取下来的,基于上面我们学习的内容,重新分析下 docker 和 传统 VM 的区别:
迁移性和性能:
- 传统 VM: 需要基于 Hypervisor 的硬件虚拟化技术,模拟出 CPU,内存等硬件。然后在其上搭建一套完整的操作系统,自然在性能上会有很大的损失。迁移自然更不用说,传统的 ova 导出后就是一个完整的操作系统。
- Docker:Docker 将 Hypervisor 的位置换成自己的 Docekr Engine. 然后运行的容器仅仅是一个特殊的进程,自然性能不会有太大的损失。并且可以应用和其所需要的系统文件打包成镜像,无论在哪读可以正常运行,而且相对于 ova 来说体积也小了更多。(需要内核支持)
一般来说,运行着 CentOS 的 KVM,启动后,在不做优化的前提下,需要占用 100~200 M 内存。在加上用户对宿主机的调用,需要通过虚拟化软件拦截和处理,又是一层性能损耗,特别是对计算资源,网络和磁盘I/O等。
隔离性:
传统 VM:由于是虚拟化出一套完整的操作系统,所以隔离性非常好。比如微软的 Azure 平台,就是在 Windows 服务器上,虚拟出大量的 Linux 虚拟机。
Docker:在隔离性上相差就很多了,因为本身上容器就是一种进程,而所有的进程都需要共享一个系统内核。
这就意味着,在 Windows 上运行 Linux 容器,或者 Linux 宿主机运行高版本内核的容器就无法实现。
在 Linux 内核中,有许多资源和对象不能 Namespace 化,如时间,比如通过
settimeofday(2) 系统调用
修改时间,整个宿主机的实际都会被修改。安全的问题,共享宿主机内核的事实,容器暴露出的攻击面更大。
资源的限制:
- 传统 VM:非常便于管理,控制资源的使用,依赖于虚拟的操作系统。
- Docker:由于 docker 内资源的限制通过 Cgroup 实现,而 Cgroup 有很多不完善的地方,比如
- 对 /proc 的处理问题。进入容器后,执行
top
命令,看到的信息和宿主机是一样的,而不是配置后的容器的数据。(可以通过 lxcfs 修正)。 - 在运行 java 程序时,给容器内设置的内存为 4g,使用默认的 jvm 配置。而默认的 jvm 读取的内存是宿主机(可能大于 4g),这样就会出现 OOM 的情况。
- 对 /proc 的处理问题。进入容器后,执行
解决的问题
容器是如何进行隔离的?
在创建新进程时,通过 Namespace 技术,如 PID namespaces 等,实现隔离性。让运行后的容器仅能看到本身的内容。
比如,在容器运行时,会默认加上 PID, UTS, network, user, mount, IPC, cgroup 等 Namespace.
容器是如何进行资源限制的?
通过 Linux Cgroup 技术,可为每个进程设定限制的 CPU,Memory 等资源,进而设置进程访问资源的上限。
简述下 docker 的文件系统?
docker 的文件系统称为 rootfs,它的实现的想法来自与 Linux unionFS 。将不同的目录,挂载到一起,形成一个独立的视图。并且 docker 在此基础上引入了层的概念,解决了可重用性的问题。
在具体实现上,rootfs 的存储区分根据 linux 内核和 docker 本身的版本,分为
overlay2
,overlay
,aufs
,devicemapper
等。rootfs(镜像)其实就是多个层的叠加,当多层存在相同的文件时,上层的文件会将下层的文件覆盖掉。容器的启动过程?
- 指定 Linux Namespace 配置
- 设置指定的 Cgroups 参数
- 切换进程的根目录
容器内运行多个应用的问题?
首先更正一个概念,我们都说容器是一个单进程的应用,其实这里的单进程不是指在容器中只允许着一个进程,而是指只有一个进程时可控的。在容器内当然可以使用 ping,ssh 等进程,但这些进程时不受 docker 控制的。
容器内的主进程,也就是 pid =1 的进程,一般是通过 DockerFile 中 ENTRYPOINT 或者 CMD 指定的。如果在一个容器内如果存在着多个服务(进程),就可能出现主进程正常运行,但是子进程退出挂掉的问题,而对于 docker 来说,仅仅控制主进程,无法对这种意外的情况作出处理,也就会出现,容器明明正常运行,但是服务已经挂掉的情况,这时编排系统就变得非常困难。而且多个服务,在也不容易进行排障和管理。
所以如果真的想要在容器内运行多个服务,一般会通过带有
systemd
或者supervisord
这类工具进行管理,或者通过--init
方法。其实这些方法的本质就是让多个服务的进程拥有同一个父进程。但考虑到容器本身的设计,就是希望容器和服务能够同生命周期。所以这样做,有点背道而驰的意味。
控制(回收和生命周期的管理)
参考
一篇文章带你吃透 Docker 原理的更多相关文章
- 一篇文章带你吃透,Java界最神秘技术ClassLoader
ClassLoader 是 Java 届最为神秘的技术之一,无数人被它伤透了脑筋,摸不清门道究竟在哪里.网上的文章也是一篇又一篇,经过本人的亲自鉴定,绝大部分内容都是在误导别人.本文我带读者彻底吃透 ...
- MYSQL(进阶篇)——一篇文章带你深入掌握MYSQL
MYSQL(进阶篇)--一篇文章带你深入掌握MYSQL 我们在上篇文章中已经学习了MYSQL的基本语法和概念 在这篇文章中我们将讲解底层结构和一些新的语法帮助你更好的运用MYSQL 温馨提醒:该文章大 ...
- 一篇文章带你掌握主流服务层框架——SpringMVC
一篇文章带你掌握主流服务层框架--SpringMVC 在之前的文章中我们已经学习了Spring的基本内容,SpringMVC隶属于Spring的一部分内容 但由于SpringMVC完全针对于服务层使用 ...
- MYSQL(基本篇)——一篇文章带你走进MYSQL的奇妙世界
MYSQL(基本篇)--一篇文章带你走进MYSQL的奇妙世界 MYSQL算是我们程序员必不可少的一份求职工具了 无论在什么岗位,我们都可以看到应聘要求上所书写的"精通MYSQL等数据库及优化 ...
- 一篇文章带你掌握主流数据库框架——MyBatis
一篇文章带你掌握主流数据库框架--MyBatis MyBatis 是一款优秀的持久层框架,它支持自定义 SQL.存储过程以及高级映射. 在之前的文章中我们学习了MYSQL和JDBC,但是这些东西远远不 ...
- 一篇文章带你掌握主流基础框架——Spring
一篇文章带你掌握主流基础框架--Spring 这篇文章中我们将会介绍Spring的框架以及本体内容,包括核心容器,注解开发,AOP以及事务等内容 那么简单说明一下Spring的必要性: Spring技 ...
- 一篇文章带你掌握主流办公框架——SpringBoot
一篇文章带你掌握主流办公框架--SpringBoot 在之前的文章中我们已经学习了SSM的全部内容以及相关整合 SSM是Spring的产品,主要用来简化开发,但我们现在所介绍的这款框架--Spring ...
- 一篇文章带你掌握MyBatis简化框架——MyBatisPlus
一篇文章带你掌握MyBatis简化框架--MyBatisPlus 我们在前面的文章中已经学习了目前开发所需的主流框架 类似于我们所学习的SpringBoot框架用于简化Spring开发,我们的国人大大 ...
- 一篇文章带你了解网页框架——Vue简单入门
一篇文章带你了解网页框架--Vue简单入门 这篇文章将会介绍我们前端入门级别的框架--Vue的简单使用 如果你以后想从事后端程序员,又想要稍微了解前端框架知识,那么这篇文章或许可以给你带来帮助 温馨提 ...
随机推荐
- Spring 框架介绍 [Spring 优点][Spring 应用领域][体系结构][目录结构][基础 jar 包]
您的"关注"和"点赞",是信任,是认可,是支持,是动力...... 如意见相佐,可留言. 本人必将竭尽全力试图做到准确和全面,终其一生进行修改补充更新. 目录 ...
- 02_互联网基本原理和HTML入门
上节课的知识复习 互联网的原理:服务器.浏览器.HTTP.知道网页文件是真实的物理存在,用HTTP请求这个文件. 要知道网址的含义:http://www.iqianduan.cn/aaa 请求哪个文件 ...
- C# 基础至集合-数组、List<T>、ArrayList、LinkedList、HashMap的一些区别
1:数组 ]; //赋值 strs[] = "; strs[] = "; //修改 strs[] = "burg"; //删除 没法删除 除非转化为可变数组li ...
- java-> 分包分层
项目分层(分包)的作用 程序为什么要分包分层? 以顾客去饭店吃饭案例分析一下: 小饭店: 一个服务员搞定(接待顾客\点菜\炒菜) 大饭店: 迎宾员(是否有预定\ 询问吃中餐还是西餐或者烧烤等\ 几位用 ...
- GitHub使用SSH连接以及生成修改添加密钥详细过程
目录 1. 先看看本地有没有SSH密钥 2. 生成/修改密钥 3. 把SSH密钥添加到ssh-agent 4. 把SSH密钥添加到GitHub账户里 5. 测试使用ssh地址clone仓库 6. 把远 ...
- windows上docker部署springboot多实例
前提条件: 1.可以运行jar包的环境2.机器上已经安装了docker3.准备部署的springboot的jar包4.Dockerfile文件 准备Dockerfile FROM java:8 VOL ...
- 1.scrapy框架
Scrapy 是一个基于 Twisted 的异步处理框架.异步就是说调用在发出之后,这个调用就直接返回,不管有没有结果.(非阻塞关注的是程序在等待调用结果(消息.返回值)时的状态,指在不能立刻得到结果 ...
- Java并发编程入门(一)
一.为什么要并发? 出现背景:操作系统的出现,使计算机同时运行多个程序成为可能. 1.目的: 资源利用率.某些时候,程序必须等待一些外部操作完成(IO)才能继续运行,在等待时间运行其他程序,可以有效提 ...
- vue2.0+mint-ui资讯类顶导航和内容页联动实例(不是很完美)
<template> <div> <div class="navbox"> <div class="nav"> ...
- .NET Core HttpClient+Consul实现服务发现
简介 随着.NET Core的不断发展与成熟,基于.NET Core实现微服务的解决方案也越来越多.这其中必然需要注册中心,Consul成为了.NET Core实现服务注册与发现的首选.类似的解决方案 ...