容器基础3:容器镜像
1.容器概念回溯
容器本质是一种特殊的进程
namespace作用是视觉隔离,cgroups作用是限制,给沙箱围了已圈墙
2.容器内看到的文件系统是什么样子?
联想Mount namespace问题
容器里的应用进程,按理应该看到一份完全独立的文件系统,这样就可以在自己容器目录(/tmp)下操作
不受宿主机及其他容器影响
3.拿c的代码去验证一下
伪代码
#define _GNU_SOURCE
#include <sys/mount.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#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运行在了Mount Namespace隔离环境中
编译代码
$ gcc -o ns ns.c
$ ./ns
Parent - start a container!
Container - inside the container!
进入容器中,执行ls命令
ls /tmp
...
发现展示的内容和宿主机的内容是一样的
发现即使开启了MountNamespace,容器进程总看到的文件系统还是和宿主机一毛一样
4.重新认知一下Mount Namespace
Mount Namespace修改的,是容器进程对文件系统“挂载点”视觉的认知。
只有在“挂载”操作后,视觉才会被改变。在挂载之前,新容器会直接继承宿主机的挂载点
如何修复呢?
在容器执行前,先挂载
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;
}
验证
$ gcc -o ns ns.c
$ ./ns
Parent - start a container!
Container - inside the container!
$ ls /tmp
发现变为空目录了,重新挂载生效了,容器内可以用mount -l检查
$ mount -l | grep tmpfs
none on /tmp type tmpfs (rw,relatime)
挂载操作+mount namespace的操作,重新挂载的操作只在容器内的mount namespace中有效。
而在宿主机上执行mount -l 可以发现没有tmpfs的挂载信息
5.更优化的环境,容器中文件系统独立隔离环境,容器镜像
换句话说,就是/分区下是独立的文件系统
chroot帮忙了。change root system 改变进程的根目录到你指定的位置
实际mount namespace就是基于chroot 改良发明的,也是linux 操作系统里的第一个namespace
容器根目录更真实,一版会在根目录下挂载一个完整的文件系统,比如centos的iso,容器启动后,查看ls / 展示的就是centos的所有目录和文件
挂载根目录,用来为容器提供隔离后的执行文件系统,就是容器镜像(rootfs)
一般容器镜像,会包含如下内容
$ ls /
bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var
进入容器后执行的/bin/bash,与宿主机的/bin/bash完全不同
6.docker的核心原理
为待创建的用户进程:
1.启用Linux Namespace配置
2.设置指定的Cgroups参数
3.切换进程的根目录
容器就诞生了。docker项目最后一步切换根目录上,优先使用pivot_root系统调用,如果不支持,才会使用chroot。这2个系统调用功能类似,但有细微区别
7.rootfs的特殊性
rootfs是操作系统包含的文件,配置,目录,不包括系统内核。linux中这2部分是分开的。操作系统只有开机启动才会加载指定版本的内核镜像
同一个宿主机上的n个容器共享宿主机的系统内核
8.镜像带来的强一致性
之前:云端环境与本地服务器环境不同,环境不同,非常痛苦
rootfs打包的不只是应用,而是操作系统层面的打包,应用以及所需要的所有依赖,都被封装一起
应用的依赖,认知不能局限在语言层面,比如golang的godeps.json。实际上操作系统本身才是应用程序最完整的“依赖库”
深入到操作系统层级的环境一致性
9.镜像的分层
用到了什么技术?
Union File System联合文件系统能力,UnionFS
将多个不同位置的目录联合挂载到同一个目录下
$ docker image inspect ubuntu:latest
...
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:f49017d4d5ce9c0f544c...",
"sha256:8f2b771487e9d6354080...",
"sha256:ccd4d61916aaa2159429...",
"sha256:c01d74f99de40e097c73...",
"sha256:268a067217b5fe78e000..."
]
}
rootfs的层次结构
- 只读层
最下面的5层,挂载方式是只读
这些层的内容。增量的方式分别包含了centos操作系统的一部分
$ 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
- 可读写层
最上面一层,rw方式挂载,没有写入文件前,整个目录空的,一旦容器里做了写操作,产生的内容增量出现在这个层里
如果是删除只读层里的一个文件呢?
通过在读写层创建一个whiteout文件,白名单文件,把你要删除的文件遮挡起来,其实并没有真正删除
比如,你要删除只读层里一个名叫 foo 的文件,那么这个删除操作实际上是在可读写层创建了一个名叫.wh.foo 的文件。这样,当这两个层被联合挂载之后,foo 文件就会被.wh.foo 文件“遮挡”起来,“消失”了。这个功能,就是“ro+wh”的挂载方式,即只读 +whiteout 的含义。我喜欢把 whiteout 形象地翻译为:“白障”。 - Init层
只读层和读写层之间,docker的内部曾,专门存放/etc/hosts,/etc/resolv.conf等信息
用户往往需要启动容器时写入一些特定的值hostname,就需要在读写层对他们进行修改
这个修改支队当前容器有效,不希望docker commit 这些也提交
所以单独抽了一个init层出来,用户docker commit提交的是读写层,不包含这些配置文件内容