docker基础

Docker 技术的基础:

  • namespace,容器隔离的基础,保证A容器看不到B容器. 6个名空间:UserMntNetworkUTSIPCPid   容器各自的网络,PID等信息
  • cgroups,容器资源统计和隔离。主要用到的cgroups子系统:cpublkiodevicefreezermemory      通过它可以划定一个个的分组,然后限制每个分组能够使用的资源,比如内存的上限值、CPU的使用率、硬盘空间总量等等。系统内核会自动检查和限制这些分组中的进程资源使用量。
  • unionfs  或者 chroot  典型:aufs/overlayfs,分层镜像实现的基础 ;挂载操作系统镜像到容器中,共享
  •      Linux 命名空间、控制组和 UnionFS 三大技术支撑了目前 Docker 的实现,也是 Docker 能够出现的最重要原因。

UnionFS

UnionFS 其实是一种为 Linux 操作系统设计的用于把多个文件系统『联合』到同一个挂载点的文件系统服务。而 AUFS Advanced UnionFS 其实就是 UnionFS 的升级版,它能够提供更优秀的性能和效率。
AUFS 作为联合文件系统,它能够将不同文件夹中的层联合(Union)到了同一个文件夹中,这些文件夹在 AUFS 中称作分支,整个『联合』的过程被称为联合挂载(Union Mount):

每一个镜像层或者容器层都是 /var/lib/docker/ 目录下的一个子文件夹;在 Docker 中,所有镜像层和容器层的内容都存储在 /var/lib/docker/aufs/diff/ 目录中:

$ ls /var/lib/docker/aufs/diff/00adcccc1a55a36a610a6ebb3e07cc35577f2f5a3b671be3dbc0e74db9ca691c       93604f232a831b22aeb372d5b11af8c8779feb96590a6dc36a80140e38e764d800adcccc1a55a36a610a6ebb3e07cc35577f2f5a3b671be3dbc0e74db9ca691c-init  93604f232a831b22aeb372d5b11af8c8779feb96590a6dc36a80140e38e764d8-init019a8283e2ff6fca8d0a07884c78b41662979f848190f0658813bb6a9a464a90       93b06191602b7934fafc984fbacae02911b579769d0debd89cf2a032e7f35cfa

...

/var/lib/docker/aufs/layers/ 中存储着镜像层的元数据,每一个文件都保存着镜像层的元数据,最后的 /var/lib/docker/aufs/mnt/ 包含镜像或者容器层的挂载点,最终会被 Docker 通过联合的方式进行组装。

上面的这张图片非常好的展示了组装的过程,每一个镜像层都是建立在另一个镜像层之上的,同时所有的镜像层都是只读的,只有每个容器最顶层的容器层才可以被用户直接读写,所有的容器都建立在一些底层服务(Kernel)上,包括命名空间、控制组、rootfs 等等,这种容器的组装方式提供了非常大的灵活性,只读的镜像层通过共享也能够减少磁盘的占用。

什么是镜像

那么问题来了,没有操作系统,怎么运行程序?

可以在Docker中创建一个ubuntu的镜像文件,这样就能将ubuntu系统集成到Docker中,运行的应用就都是ubuntu的应用。

实际上 Docker 是使用了很多 Linux 的隔离功能,让容器看起来像一个轻量级虚拟机在独立运行,容器的本质是被限制了的 Namespacescgroup,具有逻辑上独立文件系统,网络的一个进程。其底层运用了如下 Linux 的能力:

Namespaces

命名空间(namespaces)是 Linux 为我们提供的用于分离进程树、网络接口、挂载点以及进程间通信等资源的方法。在日常使用 Linux 或者 macOS 时,我们并没有运行多个完全分离的服务器的需要,但是如果我们在服务器上启动了多个服务,这些服务其实会相互影响的,每一个服务都能看到其他服务的进程,也可以访问宿主机器上的任意文件,这是很多时候我们都不愿意看到的,我们更希望运行在同一台机器上的不同服务能做到完全隔离,就像运行在多台不同的机器上一样。

Linux 的命名空间机制提供了以下七种不同的命名空间,包括 CLONE_NEWCGROUPCLONE_NEWIPCCLONE_NEWNETCLONE_NEWNSCLONE_NEWPIDCLONE_NEWUSER CLONE_NEWUTS,通过这七个选项我们能在创建新的进程时设置新进程应该在哪些资源上与宿主机器进行隔离。

Namespaces

这里提出一个问题,在宿主机上启动两个容器,在这两个容器内都各有一个 PID=1的进程,众所周知,Linux PID 是唯一的,既然 Docker 不是跑在宿主机上的两个虚拟机,那么它是如何实现在宿主机上运行两个相同 PID 的进程呢?

这里就用到了 Linux Namespaces,它其实是 Linux 创建新进程时的一个可选参数,在 Linux 系统中创建进程的系统调用是 clone()方法。

通过调用这个方法,这个进程会获得一个独立的进程空间,它的 pid 1,并且看不到宿主机上的其他进程,这也就是在容器内执行 PS 命令的结果。

不仅仅是 PID,当你启动启动容器之后,Docker 会为这个容器创建一系列其他 namespaces

这些 namespaces 提供了不同层面的隔离。容器的运行受到各个层面 namespace 的限制。

Docker Engine 使用了以下 Linux 的隔离技术:

The pid namespace: 管理 PID 命名空间 (PID: Process ID).

The net namespace: 管理网络命名空间(NET: Networking).

The ipc namespace: 管理进程间通信命名空间(IPC: InterProcess Communication).

The mnt namespace: 管理文件系统挂载点命名空间 (MNT: Mount).

The uts namespace: Unix 时间系统隔离. (UTS: Unix Timesharing System).

通过这些技术,运行时的容器得以看到一个和宿主机上其他容器隔离的环境。

进程

进程是 Linux 以及现在操作系统中非常重要的概念,它表示一个正在执行的程序,也是在现代分时系统中的一个任务单元。在每一个 *nix 的操作系统上,我们都能够通过 ps 命令打印出当前操作系统中正在执行的进程,比如在 Ubuntu 上,使用该命令就能得到以下的结果:

当前的 Docker 容器成功将容器内的进程与宿主机器中的进程隔离,如果我们在宿主机器上打印当前的全部进程时,会得到下面三条与 Docker 相关的结果:

UID        PID  PPID  C STIME TTY          TIME CMD

root     29407     1  0 Nov16 ?        00:08:38 /usr/bin/dockerd --raw-logs

root      1554 29407  0 Nov19 ?        00:03:28 docker-containerd -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd --shim docker-containerd-shim --runtime docker-runc

root      5006  1554  0 08:38 ?        00:00:00 docker-containerd-shim b809a2eb3630e64c581561b08ac46154878ff1c61c6519848b4a29d412215e79 /var/run/docker/libcontainerd/b809a2eb3630e64c581561b08ac46154878ff1c61c6519848b4a29d412215e79 docker-runc

在当前的宿主机器上,可能就存在由上述的不同进程构成的进程树:

这就是在使用 clone(2) 创建新进程时传入 CLONE_NEWPID 实现的,也就是使用 Linux 的命名空间实现进程的隔离,Docker 容器内部的任意进程都对宿主机器的进程一无所知。

containerRouter.postContainersStart

└── daemon.ContainerStart

└── daemon.createSpec

    └── setNamespaces

        └── setNamespace

Docker 的容器就是使用上述技术实现与宿主机器的进程隔离,当我们每次运行 docker run 或者 docker start 时,都会在下面的方法中创建一个用于设置进程间隔离的 Spec:

func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {

s := oci.DefaultSpec()

// ...if err := setNamespaces(daemon, &s, c); err != nil {

    return nil, fmt.Errorf("linux spec namespaces: %v", err)

}

return &s, nil

}

setNamespaces 方法中不仅会设置进程相关的命名空间,还会设置与用户、网络、IPC 以及 UTS 相关的命名空间:

func setNamespaces(daemon *Daemon, s *specs.Spec, c *container.Container) error {// user// network// ipc// uts

// pidif c.HostConfig.PidMode.IsContainer() {

    ns := specs.LinuxNamespace{Type: "pid"}

    pc, err := daemon.getPidContainer(c)

    if err != nil {

        return err

    }

    ns.Path = fmt.Sprintf("/proc/%d/ns/pid", pc.State.GetPID())

    setNamespace(s, ns)

} else if c.HostConfig.PidMode.IsHost() {

    oci.RemoveNamespace(s, specs.LinuxNamespaceType("pid"))

} else {

    ns := specs.LinuxNamespace{Type: "pid"}

    setNamespace(s, ns)

}

return nil

}

所有命名空间相关的设置 Spec 最后都会作为 Create 函数的入参在创建新的容器时进行设置:

daemon.containerd.Create(context.Background(), container.ID, spec, createOptions)

所有与命名空间的相关的设置都是在上述的两个函数中完成的,Docker 通过命名空间成功完成了与宿主机进程和网络的隔离。

网络

如果 Docker 的容器通过 Linux 的命名空间完成了与宿主机进程的网络隔离,但是却有没有办法通过宿主机的网络与整个互联网相连,就会产生很多限制,所以 Docker 虽然可以通过命名空间创建一个隔离的网络环境,但是 Docker 中的服务仍然需要与外界相连才能发挥作用。

这个地址其实也是 Docker Redis 服务分配的 IP 地址,如果我们在当前机器上直接 ping 这个 IP 地址就会发现它是可以访问到的:

$ ping 192.168.0.4

PING 192.168.0.4 (192.168.0.4) 56(84) bytes of data.64 bytes from 192.168.0.4: icmp_seq=1 ttl=64 time=0.069 ms64 bytes from 192.168.0.4: icmp_seq=2 ttl=64 time=0.043 ms

^C

--- 192.168.0.4 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 999ms

rtt min/avg/max/mdev = 0.043/0.056/0.069/0.013 ms

从上述的一系列现象,我们就可以推测出 Docker 是如何将容器的内部的端口暴露出来并对数据包进行转发的了;当有 Docker 的容器需要将服务暴露给宿主机器,就会为容器分配一个 IP 地址,同时向 iptables 中追加一条新的规则。

当我们使用 redis-cli 在宿主机器的命令行中访问 127.0.0.1:6379 的地址时,经过 iptables 的 NAT PREROUTING 将 ip 地址定向到了 192.168.0.4,重定向过的数据包就可以通过 iptables 中的 FILTER 配置,最终在 NAT POSTROUTING 阶段将 ip 地址伪装成 127.0.0.1,到这里虽然从外面看起来我们请求的是 127.0.0.1:6379,但是实际上请求的已经是 Docker 容器暴露出的端口了。

$ redis-cli -h 127.0.0.1 -p 6379 pingPONG

Docker 通过 Linux 的命名空间实现了网络的隔离,又通过 iptables 进行数据包转发,让 Docker 容器能够优雅地为宿主机器或者其他容器提供服务。

Libnetwork

整个网络部分的功能都是通过 Docker 拆分出来的 libnetwork 实现的,它提供了一个连接不同容器的实现,同时也能够为应用给出一个能够提供一致的编程接口和网络层抽象的容器网络模型。

The goal of libnetwork is to deliver a robust Container Network Model that provides a consistent programming interface and the required network abstractions for applications.

libnetwork 中最重要的概念,容器网络模型由以下的几个主要组件组成,分别是 Sandbox、Endpoint 和 Network:

想要获得更多与 libnetwork 或者容器网络模型相关的信息,可以阅读 Design · libnetwork 了解更多信息,当然也可以阅读源代码了解不同 OS 对容器网络模型的不同实现。

挂载点

虽然我们已经通过 Linux 的命名空间解决了进程和网络隔离的问题,在 Docker 进程中我们已经没有办法访问宿主机器上的其他进程并且限制了网络的访问,但是 Docker 容器中的进程仍然能够访问或者修改宿主机器上的其他目录,这是我们不希望看到的。
在新的进程中创建隔离的挂载点命名空间需要在 clone 函数中传入 CLONE_NEWNS,这样子进程就能得到父进程挂载点的拷贝,如果不传入这个参数子进程对文件系统的读写都会同步回父进程以及整个主机的文件系统。

这一部分的内容是作者在 libcontainer 中的 SPEC.md 文件中找到的,其中包含了 Docker 使用的文件系统的说明,对于 Docker 是否真的使用 chroot 来确保当前的进程无法访问宿主机器的目录,作者其实也没有确切的答案,一是 Docker 项目的代码太多庞大,不知道该从何入手,作者尝试通过 Google 查找相关的结果,但是既找到了无人回答的问题,也得到了与 SPEC 中的描述有冲突的答案,如果各位读者有明确的答案可以在博客下面留言,非常感谢。

Chroot

在这里不得不简单介绍一下 chroot(change root),在 Linux 系统中,系统默认的目录就都是以 / 也就是根目录开头的,chroot 的使用能够改变当前的系统根目录结构,通过改变当前系统的根目录,我们能够限制用户的权利,在新的根目录下并不能够访问旧系统根目录的结构个文件,也就建立了一个与原系统完全隔离的目录结构。

chroot 的相关内容部分来自《理解 chroot》一文,各位读者可以阅读这篇文章获得更详细的信息。

CGroups

Cgroups

在容器环境里,如果不做限制,运行一个 Java 应用,当应用有 Bug 导致 JVM 进行 Full GC 的时候,会占用大量的内存,也可能导致 CPU 飙升到100%,如何避免由单个容器的问题,导致整个集群不可用?Docker 用到了 Cgroups。Cgroups 是 Control Group 的缩写,由2007年谷歌工程师研发,2008年并入 Linux Kernel 2.6.24,由 C 语言编写。

Docker 底层使用 groups 对进程进行 CPU,Mem,网络等资源的使用限制,从而实现在宿主机上的资源分配,不至于出现一个容器占用所有宿主机的 CPU 或者内存。

具体的实现原理如上图所示,通过使用 cgroups,为不同的进程设定不同的配额,上图中 cgroup1 的进程只能使用60%的 CPU,cgroup2 使用20%的 CPU,同样可以为进程设定容器。所以当你在 Kubernetes 里为某个 pod 声明 CPU 限额时,底层就是调用的 cgroup 的设置。具体实现如下:

进入宿主机 cgroup 目录: cd /sys/fs/cgroup/cpu

为进程创建一个目录,例如 my_container, 然后 cgroup 会自动在这个目录下创建多个默认配置:

cpu.cfs_quota_us里输入对应的数值,即可实现对 CPU 的配额设置:

echo 60000 > /sys/fs/cgroup/cpu/container/cpu.cfs_ q uota_us,这里的单位是毫秒,意思是每一毫秒内,该进程能够使用60%的 CPU 时间。如何将该配置应用到进程里?

echo 22880 > /sys/fs/cgroup/cpu/container/task s,其中22880是宿主机上的 PID,这时候你通过调用 top 命令,能够看到该进程的 CPU 使用率不会超过60%,这样就能实现对进程的限制,避免之前的问题发生。

我们通过 Linux 的命名空间为新创建的进程隔离了文件系统、网络并与宿主机器之间的进程相互隔离,但是命名空间并不能够为我们提供物理资源上的隔离,比如 CPU 或者内存,如果在同一台机器上运行了多个对彼此以及宿主机器一无所知的『容器』,这些容器却共同占用了宿主机器的物理资源。

每一个 CGroup 都是一组被相同的标准和参数限制的进程,不同的 CGroup 之间是有层级关系的,也就是说它们之间可以从父类继承一些用于限制资源使用的标准和参数。

 

Linux 的 CGroup 能够为一组进程分配资源,也就是我们在上面提到的 CPU、内存、网络带宽等资源,通过对资源的分配,CGroup 能够提供以下的几种功能:

 

Linux 使用文件系统来实现 CGroup,我们可以直接使用下面的命令查看当前的 CGroup 中有哪些子系统:

$ lssubsys -m

cpuset /sys/fs/cgroup/cpuset

cpu /sys/fs/cgroup/cpu

cpuacct /sys/fs/cgroup/cpuacct

memory /sys/fs/cgroup/memory

devices /sys/fs/cgroup/devices

freezer /sys/fs/cgroup/freezer

blkio /sys/fs/cgroup/blkio

perf_event /sys/fs/cgroup/perf_event

hugetlb /sys/fs/cgroup/hugetlb

大多数 Linux 的发行版都有着非常相似的子系统,而之所以将上面的 cpuset、cpu 等东西称作子系统,是因为它们能够为对应的控制组分配资源并限制资源的使用。

如果系统管理员想要控制 Docker 某个容器的资源使用率就可以在 docker 这个父控制组下面找到对应的子控制组并且改变它们对应文件的内容,当然我们也可以直接在程序运行时就使用参数,让 Docker 进程去改变相应文件中的内容。

$ docker run -it -d --cpu-quota=50000 busybox53861305258ecdd7f5d2a3240af694aec9adb91cd4c7e210b757f71153cdd274

$ cd 53861305258ecdd7f5d2a3240af694aec9adb91cd4c7e210b757f71153cdd274/

$ ls

cgroup.clone_children  cgroup.event_control  cgroup.procs  cpu.cfs_period_us  cpu.cfs_quota_us  cpu.shares  cpu.stat  notify_on_release  tasks

$ cat cpu.cfs_quota_us50000

当我们使用 Docker 关闭掉正在运行的容器时,Docker 的子控制组对应的文件夹也会被 Docker 进程移除,Docker 在使用 CGroup 时其实也只是做了一些创建文件夹改变文件内容的文件操作,不过 CGroup 的使用也确实解决了我们限制子容器资源占用的问题,系统管理员能够为多个容器合理的分配资源并且不会出现多个容器互相抢占资源的问题。

UnionFS

Linux 的命名空间和控制组分别解决了不同资源隔离的问题,前者解决了进程、网络以及文件系统的隔离,后者实现了 CPU、内存等资源的隔离,但是在 Docker 中还有另一个非常重要的问题需要解决 - 也就是镜像。
镜像到底是什么,它又是如何组成和组织的是作者使用 Docker 以来的一段时间内一直比较让作者感到困惑的问题,我们可以使用 docker run 非常轻松地从远程下载 Docker 的镜像并在本地运行。

Docker 使用了一系列不同的存储驱动管理镜像内的文件系统并运行容器,这些存储驱动与 Docker 卷(volume)有些不同,存储引擎管理着能够在多个容器之间共享的存储。

容器和镜像的区别就在于,所有的镜像都是只读的,而每一个容器其实等于镜像加上一个可读写的层,也就是同一个镜像可以对应多个容器。

 

dockers=LXC+AUFS

docker为LXC+AUFS组合:

  • LXC负责资源管理
  • AUFS负责镜像管理;

LXC包括cgroup,namespace,chroot等组件,并通过cgroup资源管理,那么,从资源管理的角度来看,Docker,Lxc,Cgroup三者的关系是怎样的呢?

cgroup是在底层落实资源管理,LXC在cgroup上面封装了一层,随后,docker有在LXC封装了一层;

Cgroup其实就是linux提供的一种限制,记录,隔离进程组所使用的物理资源管理机制;也就是说,Cgroup是LXC为实现虚拟化所使用资源管理手段,我们可以这样说,底层没有cgroup支持,也就没有lxc,更别说docker的存在了,这是我们需要掌握和理解的,三者之间的关系概念

​ 我们在把重心转移到LXC这个相当于中间件上,上述我们提到LXC是建立在cgroup基础上的,我们可以粗略的认为LXC=Cgroup+namespace+Chroot+veth+用户控制脚本;LXC利用内核的新特性(cgroup)来提供用户空间的对象,用来保证资源的隔离和对应用系统资源的限制;

​ Docker容器的文件系统最早是建立在Aufs基础上的,Aufs是一种Union FS,简单来说就是支持将不同的目录挂载到同一个虚拟文件系统之下

并实现一种laver的概念,

由于Aufs未能加入到linux内核中,考虑到兼容性的问题,便加入了Devicemapper的支持,Docker目前默认是建立在Devicemapper基础上,

devicemapper用户控件相关部分主要负责配置具体的策略和控制逻辑,比如逻辑设备和哪些物理设备建立映射,怎么建立这些映射关系等,而具体过滤和重定向IO请求的工作有内核中相关代码完成,因此整个device mapper机制由两部分组成--内核空间的device mapper驱动,用户控件的device mapper库以及它提供的dmsetup工具;

 

Docker基础2

Linux 内核中,有 cgroups 和 namespaces 可以为进程定义边界,使得进程彼此隔离

unshare命令 用来隔离 进程等其他资源

Linux 中,unshare 命令行程序可以创建一个 namespace,并且根据参数创建在 namespace 中隔离各种资源,在这里我们可以用使用这个工具简单地创建一个 namespace。

#unshare --pid /bin/bash 

子进程与父进程处于不同的命名空间,不会相互影响

在 unshare 命令中,--pid 参数创建 隔离进程的命名空间,此外,还可以隔离多种系统资源:

  • mount :命名空间具有独立的挂载文件系统;
  • ipc:Inter-Process Communication (进程间通讯)命名空间,具有独立的信号量、共享内存等;
  • uts:命名空间具有独立的 hostname 、domainname;
  • net:独立的网络,例如每个 docker 容器都有一个虚拟网卡;
  • pid:独立的进程空间,空间中的进程 Pid 都从 1 开始;
  • user:命名空间中有独立的用户体系,例如 Docker 中的 root 跟主机的用户不一样;
  • cgroup:独立的用户分组;

cgroups 硬件资源隔离

前面提到的 namepace 是逻辑形式使得进程之间相互不可见,形成环境隔离,这跟 Docker 容器的日常使用是一样的,隔离根目录,隔离网络,隔离进程 PID 等。

当然,Docker 处理环境隔离外,还能限制每个容器使用的物理资源,如 CPU 、内存等,这种硬件资源的限制是基于 Linux 内核的 cgroups 的。

在 Docker 中限制容器能够使用的资源量参数示例:

-m 4G --memory-swap 0 --cpu-period=1000000 --cpu-quota=8000000 

cgroups 是 control groups 的缩写,是 Linux 内核提供的一种可以进程所使用的物理资源的机制。

cgroups 可以控制多种资源,在 cgroups 中每种资源限制功能对应一个子系统,可以使用命令查看:

mount | grep cgroup
奋斗为了更好的生活。
原文地址:https://www.cnblogs.com/redman274/p/15495878.html