原文地址: 点击直达
我们以 Docker 基础架构来探究Docke底层的核心技术,简单的包括:
传统虚拟机特点:
传统的虚拟机通过在宿主主机中运行 hypervisor 来模拟一整套完整的硬件环境提供给虚拟机的操作系统,虚拟机系统看到的环境是可限制的,也是彼此隔离的,实现了对资源最完整的封装但是意味着系统资源的浪费;
操作系统与资源共享
操作系统中包括内核、文件系统、网络、PID、UID、IPC、内存、硬盘、CPU 等等,所有的资源都是应用进程直接共享的
例如,以宿主机和虚拟机系统都为 Linux 系统为例,虚拟机中运行的应用其实可以利用宿主机系统中的运行环境(docker也是基于此)。
实现虚拟化的要求:
Memory
CPU
使用docker的stress镜像压测实现资源限制:
我的机器是1核的:所以CPU百分比超过100%,Range of CPUs is from 0.01 to 1.00, as there are only 1 CPUs available.
$docker pull lorel/docker-stress-ng
lorel/docker-stress-ng latest 1ae56ccafe55 3 years ago 8.1MB
$docker run --name stress -it --rm lorel/docker-stress-ng:latest stress -help #查看镜像压力测试帮助
Example: stress-ng --cpu 8 --io 4 --vm 2 --vm-bytes 128M --fork 4 --timeout 10s
#演示1.限制虚拟内最大不能藏256MB
[root@izwz9biz2m4sd3bb3k38pgz ~]# docker run --name stress -it -m 256m --rm lorel/docker-stress-ng:latest stress --vm 2
$docker stats
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
36a41704053f stress 98.57% 140MiB / 256MiB (关键点) 54.69% 0B / 0B 0B / 0B 5
#测试cpu的性能:
$docker run --name stress -it -m 256m --rm lorel/docker-stress-ng:latest stress --cpu 8 --io 4 #使用全部CPU占100%
$docker run --name stress --cpus 0.5 -it -m 256m --rm lorel/docker-stress-ng:latest stress --cpu 8 --io 4 #使用半个CPU 不超过50% 上下误差+/-5
$docker run --name stress --cpuset-cpus 0 -it --rm lorel/docker-stress-ng:latest stress --cpu 8 #使用第
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
6d6cc81d6e26 stress 50.99% 15.93MiB / 256MiB 6.22% 0B / 0B 0B / 143kB 13
描述:Linux中I/O设备分为以下两类,两种设备本身没有严格限制,但是基于不同的功能进行分类;
支持按字节、字符来读写数据
,应用程序需要按顺序进行读取,所以通常不支持随机存取; 比如:调制解调器就是典型的字符设备
;Q: 字符设备与块设备的区别?
答:前者顺序读取写入,后者通过寻址磁盘上的任何位置进行读取写入
,注意块设备数据的读写只能以块(通常是512B)的倍数
进行,与字符设备另外一个区别就是块设备并不支持基于字符的寻址
;
Linux 的设备管理和文件系统是紧密相关的(一切皆文件),以文件的格式存放于/dev/目录之下称为设备文件。
应用程序可以打开、关闭、读写这些设备文件,完成对设备的操作就像操作普通数据文件一样,并且为了便于管理设备系统为各个设备进行编号,每个设备号分为主设备号和次设备号;
对于常用设备Linux有约定俗称的编号,如磁盘的主设备号是3;并且一个字符设备或者块设备都有一个主设备号和次设备号(统称为设备号
);
Q: 什么是主设备号和次设备号?
答: 主设备号表示一个特定的驱动程序,用来区分不同种类的设备;
次设备号表示使用该驱动程序的各设备,用来区分同一种类(类型)的多个设备;
描述:容器(Container)利用Linux中内核(>=2.4.19)命名空间来做权限的隔离控制
即将某个特定的全局系统资源通过抽象的方法使得namespace中的进程看起来拥有他们自己的隔离的全局系统资源,并且联合利用 cgroups 来做资源分配限制
。
Docker 容器和 LXC 容器很相似,命名空间提供了最基础也是最直接的隔离,在容器中运行的进程不会被运行在主机上的进程和其它容器发现和作用。随着 Linux 系统对于命名空间功能的完善实现,程序员已经可以实现上面的所有需求,让某些进程在彼此隔离的命名空间中运行。大家虽然都共用一个内核和某些运行时环境(例如一些系统命令和系统库),但是彼此却看不到都以为系统中只有自己的存在
当使用docker run启动一个容器时候,后台Docker会为容器创建一个独立的命名空间和控制组集合;每个容器都有资源独有网络,意味着他们不能访问其他容器的sockets或接口。但能通过–links来连接 2 个容器时或者–net来自定义Docker容器网络(网桥接口相互通信),容器就可以相互通信了(可以根据配置来限制通信的策略);
命名空间(namespace)在Docker容器具体实现
说明如下:
$ls -l /proc/$$/ns
总用量 0
lrwxrwxrwx. 1 root root 0 7月 4 23:27 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 root root 0 7月 4 23:27 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 root root 0 7月 4 23:27 net -> net:[4026531956]
lrwxrwxrwx. 1 root root 0 7月 4 23:27 pid -> pid:[4026531836]
lrwxrwxrwx. 1 root root 0 7月 4 23:27 user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0 7月 4 23:27 uts -> uts:[4026531838]
Namespace | 隔离的全局系统资源 | 容器隔离效果 |
---|---|---|
UTS (UNIX Time-sharing System ) | 主机名与域名 | 每个容器拥有独立的 hostname 和 domain name, 使其在网络上可以被视作一个独立的节点而非主机上的一个进程。 |
User | 用户和用户组 | 每个容器可以有不同的用户uid和组gid, 可以在容器内用容器内部的用户执行程序而非宿主主机上的用户。 |
IPC (Inter-Process Communication) | 信号量、消息队列(POSIX message queues)以及共享内存 | 每个容器有其自己的System V IPC 和 POSIX 消息队列系统,从而只有在同一个IPC namespace的进程之间才能互相通信; 注意:容器的进程间交互实际上还是 host 上具有相同 pid 命名空间中的进程间交互。因此需要在 IPC 资源( 有唯一的32位id )申请时加入命名空间信息。 |
PID | 进程编号 | 每个名称空间中pid中的进程可以有其独立的PID,即不同用户的进程通过 pid 命名空间隔离开的,且不同命名空间中可以有相同 pid号; 每个容器可以有其PID为1的root进程,也使得容器可以在不同的host之间进行迁移(因为namespace中的进程ID和host不是强绑定,使得容器中的每个进程有两个PID即容器中PID和host上的PID); |
Network | 网络设备、网络栈、端口等 | 每个 net 命名空间有独立的网络设备、IP地址、路由表、/proc/net目录、端口号等等,这也使得一个host上多个容器内的同一个应用都绑定到各自的80端口上,Docker 默认采用 veth 的方式,将容器中的虚拟网卡同 host 上的一 个Docker 网桥 docker0 连接在一起。 |
Mount (mnt) | 文件系统挂载点 | 类似 chroot将一个进程放到一个特定的目录执行,与 chroot 不同是每个命名空间中的容器在 /proc/mounts 的信息只包含所在命名空间的 mount point,允许不同命名空间的进程看到的文件结构不同,使得进程间文件目录隔离开来。 |
基础示例1-mnt:
# 1.创建一个文件夹,使用tmpfs这种基于内存的文件系统来模拟挂载
mkdir /tmp/mnt_isolation
# 2.使用unshare(内置命令做作用是不启动一个新进程就可以起到隔离效果,相当于跳出原先的namespace)隔离mnt namespace
unshare --mount /bin/bash
# 3.挂载我们的tmpfs
mount -t tmpfs tmpfs /tmp/mnt_isolation
# 4.然后进入/tmp/mnt_isolation创建文件
cd /tmp/mnt_isolation && touch linux-mnt-{1..10}
# 5.新建立一个shell终端发现并没有创建文件;
[root@localhost mnt_isolation]$ ls #终端1
linux-mnt-1 linux-mnt-2 linux-mnt-4 linux-mnt-6 linux-mnt-8
linux-mnt-10 linux-mnt-3 linux-mnt-5 linux-mnt-7 linux-mnt-9
[root@localhost mnt_isolation]$ ls /tmp/mnt_isolation/ #终端2
#输出为空
基础示例2-ipc:
# 1.创建一个消息队列
$ ipcmk --queue
消息队列 id:0
# 2.查看创建的消息队列
$ ipcs
# --------- 消息队列 -----------
# 键 msqid 拥有者 权限 已用字节数 消息
# 0x651dbb47 0 root 644 0 0
# ------------ 共享内存段 --------------
# 键 shmid 拥有者 权限 字节 nattch 状态
# --------- 信号量数组 -----------
# 键 semid 拥有者 权限 nsems
# 3.使用unshare命令进行隔离IPC namespace
$echo $$
78285
$unshare --ipc /bin/bash
$echo $$ #此时已经切换到新的进程
78468
$ps aux | grep -E "78468|78285"
root 78285 0.0 0.1 117460 3980 pts/1 Ss 00:01 0:00 -bash
root 78468 0.0 0.1 117408 3956 pts/1 S 00:04 0:00 /bin/bash
# 4.再次查看消息队列发现为空,所以证实了在隔离的Namespace中创建的内容外部无法看到
ipcs
# --------- 消息队列 -----------
# 键 msqid 拥有者 权限 已用字节数 消息
# ------------ 共享内存段 --------------
# 键 shmid 拥有者 权限 字节 nattch 状态
# --------- 信号量数组 -----------
# 键 semid 拥有者 权限 nsems
描述:控制组(Control Group)是 Linux 内核(2.6.24)的一个特性也是容器机制的另外一个关键组件,主要用来对共享资源(内存、CPU、磁盘 IO 等资源
)进行隔离、限制、审计等(用于资源控制
)。
简单的说Cgroup是Linux内核的一个功能,它将任意进程进行分组化管理具体资源管理就是通过它来实现的,具体的资源管理功能称为Cgroup子系统或控制器
,它可以控制内存的Memory控制器、控制进程调度的CPU控制器并且运行中的内核可以使用Cgroup子系统利用/proc/cgroup来管理
;
实现原理:将一组进程放在一个控制组里,通过给这个控制组分配指定的可用资源,达到控制这一组进程可用资源的目的。所以只有能控制分配到容器的资源,才能避免当多个容器同时运行时的对系统资源的竞争
。
实现角度:Cgroup实现了一个通用的进程分组的架构,而不同资源的具体管理则是由各个Cgroup子系统实现的。
实现方式: cgroupfs Cgroup Driver: cgroupfs
或者 systemd Cgroup Driver: systemd
;
发展历史:
限制一个进程的文件上限、限大小
等;注意:不同内核版本Cgroup中实现的子系统有些许不同,当用docker run启动一个容器的时候创建一个独立的名称空间和控制组集合;
# 3.10.0-1062.18.1.el7.x86_64
grep cgroup /proc/mounts | awk -F " " '{print $2 " = " $4}'
/sys/fs/cgroup = ro,seclabel,nosuid,nodev,noexec,mode=755
/sys/fs/cgroup/systemd = rw,seclabel,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd
/sys/fs/cgroup/freezer = rw,seclabel,nosuid,nodev,noexec,relatime,freezer
/sys/fs/cgroup/net_cls,net_prio = rw,seclabel,nosuid,nodev,noexec,relatime,net_prio,net_cls
/sys/fs/cgroup/cpu,cpuacct = rw,seclabel,nosuid,nodev,noexec,relatime,cpuacct,cpu
/sys/fs/cgroup/cpuset = rw,seclabel,nosuid,nodev,noexec,relatime,cpuset
/sys/fs/cgroup/perf_event = rw,seclabel,nosuid,nodev,noexec,relatime,perf_event
/sys/fs/cgroup/hugetlb = rw,seclabel,nosuid,nodev,noexec,relatime,hugetlb
/sys/fs/cgroup/pids = rw,seclabel,nosuid,nodev,noexec,relatime,pids
/sys/fs/cgroup/memory = rw,seclabel,nosuid,nodev,noexec,relatime,memory
/sys/fs/cgroup/blkio = rw,seclabel,nosuid,nodev,noexec,relatime,blkio
/sys/fs/cgroup/devices = rw,seclabel,nosuid,nodev,noexec,relatime,devices
# 5.7.0-1.el7.elrepo.x86_64
grep cgroup /proc/mounts | awk -F " " '{print $2 " = " $4}'
/sys/fs/cgroup = ro,nosuid,nodev,noexec,mode=755
/sys/fs/cgroup/systemd = rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd
/sys/fs/cgroup/cpu,cpuacct = rw,nosuid,nodev,noexec,relatime,cpu,cpuacct
/sys/fs/cgroup/memory = rw,nosuid,nodev,noexec,relatime,memory
/sys/fs/cgroup/devices = rw,nosuid,nodev,noexec,relatime,devices #主机设备读写节点创建控制
/sys/fs/cgroup/blkio = rw,nosuid,nodev,noexec,relatime,blkio
/sys/fs/cgroup/net_cls,net_prio = rw,nosuid,nodev,noexec,relatime,net_cls,net_prio
/sys/fs/cgroup/freezer = rw,nosuid,nodev,noexec,relatime,freezer
/sys/fs/cgroup/rdma = rw,nosuid,nodev,noexec,relatime,rdma # 内核新增
/sys/fs/cgroup/hugetlb = rw,nosuid,nodev,noexec,relatime,hugetlb
/sys/fs/cgroup/perf_event = rw,nosuid,nodev,noexec,relatime,perf_event
/sys/fs/cgroup/cpuset = rw,nosuid,nodev,noexec,relatime,cpuset
/sys/fs/cgroup/pids = rw,nosuid,nodev,noexec,relatime,pids
由上面可知cgroups分为多个子系统,每个系统代表一种设施或者说是资源控制器,用来调度某一类的资源使用(cpu,内存,块设备),在实现上cgroups并没有增加新的系统调用,而是表现为一个cgroup文件系统,可以把一个或者多个子系统挂载到某一个目录之中;
# 挂载cpu子系统到 /sys/fs/cgroup/cpu
mount -t cgroup -o cpu cpu /sys/fs/cgroup/cpu
# 查看
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuset)
基础实例:
#示例1.分配和限制内存和CPU使用
docker run -itd --cpus="1.5" --cpuset-cpus="0-1" --cpuset-mems="0" busybox sleep 6000 #常规分配指定cpu节点
docker run -itd --memory 512m --memory-swap 1g busybox sleep 6000 #分配指定内存 -m.--memory 512m
#示例2.为容器添加一个主机设备
docker run -itd --name=demo --device=/dev/sda3 busybox sleep 6000
docker exec -it demo ls /dev/sda3
#实例3.CPU调度与资源限制
docker run -itd calico/cni -c 512 --cpuset-cpus="0" # -c cpu权重
docker run -itd busybox -c 1024 --cpuset-cpus="1,2" # 让一个cgroup使用cpu的1/3,同时另外一个cgroup使用该cpu的2/3(根据cgroup组来划分)
docker run -itd calico/cni --memory 512m --memory-swap 1g --cpu-period=1000000 --cpu-quota=950000 # docker 1.12 及其之前版本
docker run -itd calico/cni --memory 512m --memory-swap 1g --cpu-rt-runtime 950000 # 完全公平调度 CFS scheduler
描述:其作用于控制Cgroup的进程对那些设备具有访问的权限;
接口如下:
# 全局
cat /sys/fs/cgroup/devices/devices.list
# a *:* rwm
# docker 应用全局
cat /sys/fs/cgroup/devices/docker/devices.list
# a *:* rwm
ls -l /sys/fs/cgroup/devices/devices.allow
--w-------. 1 root root 0 6月 15 09:46 /sys/fs/cgroup/devices/devices.allow
ls -l /sys/fs/cgroup/devices/devices.deny
--w-------. 1 root root 0 6月 15 09:46 /sys/fs/cgroup/devices/devices.deny
# docker 容器设置
$docker ps
25d2d645bfc9 test1 "top -b -d 2" 2 weeks ago
cat /sys/fs/cgroup/devices/docker/25d2d645bfc9e6530039d6aac890f69dd9af33f8f966adc2d7287b74964678e3/devices.list
c 1:5 rwm
c 1:3 rwm
c 1:9 rwm
c 1:8 rwm #brw-rw----. 1 root disk 8, 0 6月 15 09:46 /dev/sda
c 5:0 rwm
c 5:1 rwm
c *:* m
b *:* m
c 1:7 rwm #lrwxrwxrwx. 1 root root 7 6月 15 09:46 /dev/mapper/centos-app -> ../dm-2
c 136:* rwm
c 5:2 rwm
c 10:200 rwm
# 手动创建Devices
mkdir /sys/fs/cgroup/devices/testgroup #创建一个控制组
ls /sys/fs/cgroup/devices/testgroup #默认情况下创建的控制组可以访问全部设备
# cgroup.clone_children cgroup.procs devices.deny notify_on_release
# cgroup.event_control devices.allow devices.list tasks
cat devices.list
# a *:* rwm
echo 'c 1:3 rwm' > /sys/fs/cgroup/devices/testgroup/devices.deny #对testgroup组禁止字符设备/dev/null 读写以及创建节点
echo $$ > /sys/fs/cgroup/devices/testgroup/tasks #将当前进程号加入任务中
echo "hello world" > /dev/null #由于组设置了对字符设备/dev/null进行写限制,所以下面写请求失败
# -bash: /dev/null: 不允许的操作
# $$ 表示当前bash进程的PID等价于$BASHPID
cat /sys/fs/cgroup/devices/testgroup/tasks
# 123453
echo $BASHPID
# 123453
接口说明:
#示例演示
a *:* rwm #表示所有设备都可以访问
c 1:3 r #表示字符设备只有一个读权限
描述:其作用于分配指定的CPU和内存节点以此来限定进程可以使用的cpu核心和内存节点,现广泛用于KVM场景之中;
基础实例:
#(1) 创建一个控制组
mkdir /sys/fs/cgroup/cpuset/testgroup
#(2) 限制控制组只能使用内存节点0和cpu0与cpu1(与您cpu核心数有关 `grep "processor" /proc/cpuinfo`)
echo 0 > /sys/fs/cgroup/cpuset/testgroup/cpuset.mems
echo 0,1 > /sys/fs/cgroup/cpuset/testgroup/cpuset.cpus
#(3) 同样将当前进程加入控制组中
echo $$ > /sys/fs/cgroup/cpuset/testgroup/tasks
#(4) 验证配置结果
cat /proc/$$/status | grep "_allowed_list"
Cpus_allowed_list: 0-1
Mems_allowed_list: 0
描述:其作用是限制每个进程能够占用CPU多长时间进行设置;比如在机器上运行多个可能会消耗大量的系统资源的进程时候,我们不希望出现某个程序占据所有的系统资源而导致其它程序进程无法执行,从而导致程序假死的状态,此时使用cgroup对CPU的获取量
便可以有效的进行控制;
我们在学操作系统原理的时候我们知道操作系统中有多种调度策略,各种调度策略适用于不同的应用场景;
常用的调度程序:
cpu子系统接口一览:
- cpu.shares: 资源组的CPU使用权重它不限制进程使用绝对的CPU而是控制各组之间的配额,默认情况下Docker容器中cpu shares权重都为1024;
echo 512 > /sys/fs/cgroup/cpu/testgroup/cpu.shares
echo 1024 > /sys/fs/cgroup/cpu/testgroup1/cpu.shares
- cpu.cfs_period_us: 完全公平调度策略,用于统计CPU使用的时间周期(时间片份数)它可以设定重新分配cgroup的可用CPU资源的时间间隔,单位为微秒us(值范围:1000us~1s);
- cpu.cfs_quota_us: 完全公平调度策略,用于某周期内占用CPU的时间(指单核的时间,多核着需要设置时累加),此参数设定基于时间片份数某个cgroup中所有任务可运行的时间总量,单位为微秒us(默认值为-1);
#比如:cpu.cfs_period_us设置为1000000而cpu.cfs_quota_us设置为200000表示cgroup控制的任务中在每1秒钟的0.2秒可单独对CPU进行存取;
#比如:cpu.cfs_quota_us设置成为-1则表示cgroup不需要遵循任何CPU时间限制,这也是每个cgroup的默认值;
简单来说:在CFS调度下period设置为1s并且quota设置为0.5s,那么在cgroup进程中最多可以运行0.5s然后被强制休眠只能等待下一秒才能继续运行;
#示例:假设CPU>=4核心表示这个组在一个使用周期(1s)内可以跑满4核资源;
echo 1000000 > /sys/fs/cgroup/cpu/testgroup/cpu.cfs_period_us
echo 1000000 > /sys/fs/cgroup/cpu/testgroup/cpu.cfs_quota_us
- cpu.rt_period_us: 实时调度策略,设置某个时间段中每隔多久cgroup对CPU资源的存储就要重新分配,单位为微秒(注意:只可以用于实时调度的程序)
- cpu.rt_runtime_us: 实时调度策略,设置某个时间段内cgroup中任务对CPU资源的最长连续访问时间,单位为微秒(注意:只可以用于实时调度的程序)
#比如:我们宿主机中的内核参数设置如下,表示实时进程在运行时候并不是完全占用CPU的,这样设置的好处是即不会对实时任务响应时间导致大的影响,同时也解决了实时任务卡住时导致整个系统无响应;
sysctl -a | grep "sched_rt"
kernel.sched_rt_period_us = 1000000
kernel.sched_rt_runtime_us = 950000
FAQ补充:
Q:#RT调度与CFS调度区别?
答:两则强制上限类似,但RT只是限制实时任务(对响应时间高要求的进程)对cpu的存取,这类进程需要在限定的时间内处理并完成用户的请求,因此在限定的时间内所占用的CPU资源不能被其它进程打断或者占用; 如果此时实时进程中出现类似死循环之类的情况下就会导致整个系统无响应,因为实时进程中CPU权限最高并且在未完成任务前不会释放CPU资源;
描述:CPU统计(CPU accounting)子系统会自动生成报告来显示cgroup任务所使用的cpu资源,其中包括子群组任务:
ls /sys/fs/cgroup/cpu,cpuacct | grep "cpuacct"
cpuacct.stat # 报告此cgroup中所有任务(包括层级中的低端任务-下同)中用户(用户模式)和系统(kelnel模式)使用的CPU时间
# user 350140
# system 344690
cpuacct.usage # 报告此cgroup中所有任务使用CPU的总时间
# 13713389123644
cpuacct.usage_percpu #报告cgroup中所有任务每个CPU使用时间(单位纳秒)
# 6911595096356 6801848202718
描述:其作用是用来限制Cgroup组所能使用的内存,主要接口:
ls /sys/fs/cgroup/memory | grep "memory."
memory.kmem.limit_in_bytes #设置内存使用上线,单位可以使用k/K,m/M,g/GD等;
memory.memsw.limit_in_bytes #设置内存加上交换分区的使用总量,防止进程把交换分区耗光;
memory.stat #汇报内存使用信息包括当前资源总量,使用量,换页次数,活动页数量等等;
memory.oom_control #用来决定一个进程在申请内存超限时候,是否会被系统kill掉,采用0或1来开启或者关闭cgroup的OOM killer默认开启(oom_kill_disable 0);
# - 0 如果开启任务进程尝试申请内存超过允许,就会被系统OOM Killer终止;
# - 1 如果关闭任务进程尝试申请内存超过允许,那么它就会被暂停,直到额外的内存被释放;
memory.swappiness #设置内存交换条件,对于使用swap空间有很大的重要性;
memory.failcnt
memory.force_empty
memory.kmem.failcnt
memory.kmem.max_usage_in_bytes
memory.kmem.slabinfo
memory.kmem.tcp.failcnt
memory.kmem.tcp.limit_in_bytes
memory.kmem.tcp.max_usage_in_bytes
memory.kmem.tcp.usage_in_bytes
memory.kmem.usage_in_bytes
memory.limit_in_bytes
memory.max_usage_in_bytes
memory.memsw.failcnt
memory.memsw.max_usage_in_bytes
memory.memsw.usage_in_bytes
memory.move_charge_at_immigrate
memory.numa_stat
memory.pressure_level
memory.soft_limit_in_bytes
memory.usage_in_bytes
memory.use_hierarchy
基础示例:
#1.创建控制组
mkdir /sys/fs/cgroup/memory/testcagroup
#2.设置控制组使用的内存上限位100M,且内存与swap的总量为200M
echo 100M > /sys/fs/cgroup/memory/testcagroup/memory.limit_in_bytes
echo 200M > /sys/fs/cgroup/memory/testcagroup/memory.memsw.limit_in_bytes
#3.设置控制组中如果有进程尝试申请内存超过允许,就会被系统OOM killer掉
cat /sys/fs/cgroup/memory/testcagroup/memory.oom_control
oom_kill_disable 1
under_oom 0
#4.查看memory.stat因为控制组内没有添加进程,所以stat的信息为空将当前进程加入cgroup组内
$echo $$ > /sys/fs/cgroup/memory/testcagroup/tasks
$cat memory.stat
cache 0
rss 225280
rss_huge 0
mapped_file 0
swap 0
pgpgin 108
pgpgout 53
pgfault 584
pgmajfault 0
inactive_anon 0
active_anon 200704
inactive_file 0
active_file 0
unevictable 0
hierarchical_memory_limit 104857600
hierarchical_memsw_limit 209715200
total_cache 0
total_rss 225280
total_rss_huge 0
total_mapped_file 0
total_swap 0
total_pgpgin 108
total_pgpgout 53
total_pgfault 584
total_pgmajfault 0
total_inactive_anon 0
total_active_anon 200704
total_inactive_file 0
total_active_file 0
total_unevictable 0
描述:其子系统可以控制并监控cgroup中任务对块设备I/O存取,对一些伪文件写入值可以限制存取次数或者带宽,从伪文件中读取值可以获得关于I/O操作系统信息。
块 I/O blkio子系统给出两种方式来控制对I/O的存取:
基本示例:
# 默认权重为1000
cat /sys/fs/cgroup/blkio/blkio.weight
1000
# 创建2个控制组并设置权重
mkdir/sys/fs/cgroup/blkio/mygroup{1,2}
echo 500 > /sys/fs/cgroup/blkio/mygroup1/blkio.weight
echo 100 > /sys/fs/cgroup/blkio/mygroup2/blkio.weight
# 此时离cgexec命令在清空缓存后采用dd读写(可以以此区别两个控制组的读写比例在5:1左右)
cgexec -g "blkio:mygroup1" dd bs=1M count=4096 if=file1 of=/dev/null
cgexec -g "blkio:mygroup2" dd bs=1M count=4096 if=file1 of=/dev/null
联合文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统也是 Docker 镜像的基础,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)。
Docker 目前支持的联合文件系统包括 OverlayFS, AUFS, Btrfs, VFS, ZFS 和 Device Mapper,在可能的情况下,推荐使用 overlay2 存储驱动
它是目前 Docker 默认的存储驱动;
使用的好处:
镜像可以通过分层来进行继承(对于修改采用分层存储),基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。另外不同 Docker 容器就可以共享一些基础的文件系统层,同时再加上自己独有的改动层,大大提高了存储的效率。
Docker 中使用的 AUFS(AnotherUnionFS)就是一种联合文件系统。
最初,Docker 采用了 LXC 中的容器格式。
从 0.7 版本以后开始去除 LXC,转而使用自行开发的 libcontainer,从 1.11 开始,则进一步演进为使用 runC 和 containerd。
Docker 与 LXC(Linux Container)有何不同?
*答:LXC 利用 Linux 上相关技术实现了容器,Docker 则在如下的几个方面进行了改进:
Docker 与 Vagrant 有何不同?
答:两者的定位完全不同。
简单说:
描述:Docker 的网络实现其实就是利用了 Linux 上的网络命名空间和虚拟网络设备veth(Vritual Enternet Device),veth主要的目的是为了跨NetWork Namespace之间提供一种类似于Linux进程间通信技术,所以veth总数成对出现进行通信其工作在L2数据链路层
,比如veth0与veth1他们分别在不同的Network Namespace
,其中一端Veth设备任意一端上RX到的数据都会在另外一端上以Tx的方式发送出去;
前面我们说过Linux下的Docker容器网络通过Network Namespace机制(/proc/net, IP地址, 网卡, 路由
)实现隔离网络资源,不同的Network Namespace有各自的网络设备,协议栈,路由器以及防火墙,同一个Namepsace下的进程共享同一个网络视图;所以veth-pair设备接口它在本地主机和容器内分别创建一个虚拟接口(即:在不同的网络命名空间中创建通道
),并让它们彼此连通实现网络通信,该设备在转发数据包过程中并不篡改数据包内容;
基本原理:
为了实现Docker网络中各个容器通信,需要veth-pair设备在本地主机和容器内分别创建虚拟接口(Docker 中的网络接口默认都是虚拟的接口
), 然后Linux 通过在内核中进行数据复制来实现虚拟接口之间的数据转发,发送接口的发送缓存中的数据包被直接复制到接收接口的接收缓存中
,并且对于本地系统和容器内系统看来就像是一个正常的以太网卡,只是它不需要真正同外部网络设备通信,所以速度要快很多则转发效率较高;此外如果不同子网之间要进行通信,还需要路由机制。
#1.网桥工具安装brctl
yum install -y bridge-utils
#2.查看网桥连接信息
$brctl show
# bridge name bridge id STP enabled interfaces
# br-fa08aa7db7a6 8000.0242fb5007f1 no veth842b243
# docker0 8000.024245b206ce no veth985cbf0
#3.机器网卡信息
$ip addr | grep "BROADCAST"
# 2: ens192: mtu 1500 qdisc mq state UP group default qlen 1000
# 3: docker0: mtu 1500 qdisc noqueue state UP group default
# 5: br-fa08aa7db7a6: mtu 1500 qdisc noqueue state UP group default
# 57: veth985cbf0@if56: mtu 1500 qdisc noqueue master docker0 state UP group default
# 71: veth842b243@if70: mtu 1500 qdisc noqueue master br-fa08aa7db7a6 state UP group default
当通过docker run创建一个容器的时候便有了单独的网络命名空间
,容器网络初始化流程:
网卡名称@虚拟接口序号
,并配置默认路由到桥接网卡 veth985cbf0@if56(57)# 简单描述
ens192|eth0 <-> docker0 <-> veth985cbf0@56(虚拟网卡) <-> eth0@if57(容器内部)网卡
# 容器内部网卡信息
docker exec -it test1 sh -c ip addr
# 1: lo: mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
# link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# inet 127.0.0.1/8 scope host lo
# valid_lft forever preferred_lft forever
# 56: eth0@if57: mtu 1500 qdisc noqueue state UP
# link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
# inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
# valid_lft forever preferred_lft forever
Docker 中网络提供了五种模式:
# (1) Docker network 查看
docker network ls
# NETWORK ID NAME DRIVER(驱动模式) SCOPE
# 3448511628ee bridge bridge local
# 5baa8b9f47c3 host host local
# afd58da3d80f none null local
# fa08aa7db7a6 opt_default bridge local
# (2) 网络名称信息详细
docker network inspect $(docker network ls -q)
# [
# {
# "Name": "bridge",
# "Id": "3448511628ee3de59985bfa8251a8731148265417530fba645d1f5cca6464ccf",
# "Created": "2020-07-02T11:21:02.785207578+08:00",
# "Scope": "local",
# "Driver": "bridge",
# "EnableIPv6": false,
# "IPAM": {
# "Driver": "default",
# "Options": null,
# "Config": [
# {
# "Subnet": "172.17.0.0/16",
# "Gateway": "172.17.0.1"
# }
# ]
# },
# "Internal": false,
# "Attachable": false,
# "Ingress": false,
# "ConfigFrom": {
# "Network": ""
# },
# "ConfigOnly": false,
# "Containers": {
# "25d2d645bfc9e6530039d6aac890f69dd9af33f8f966adc2d7287b74964678e3": {
# "Name": "test1",
# "EndpointID": "6c825cb11f084e85afddbc993937e6061b2e36ea7dfaa30792b2ea6d0eb414a1",
# "MacAddress": "02:42:ac:11:00:02",
# "IPv4Address": "172.17.0.2/16",
# "IPv6Address": ""
# }
# },
# "Options": {
# "com.docker.network.bridge.default_bridge": "true",
# "com.docker.network.bridge.enable_icc": "true",
# "com.docker.network.bridge.enable_ip_masquerade": "true",
# "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
# "com.docker.network.bridge.name": "docker0",
# "com.docker.network.driver.mtu": "1500"
# },
# "Labels": {}
# }
# ]
Tips:
--net=bridge 这个是默认值,连接到默认的网桥。
--net=host 告诉Docker不要将容器网络放到隔离的命名空间中,即不要容器化容器内的网络
- 此时容器使用本地主机的网络,它拥有完全的本地主机接口访问权(不安全),
- 如果进一步的使用 --privileged=true,容器会被允许直接配置主机的网络堆栈。
--net=container:NAME_or_ID 让Docker将新建容器的进程放到一个已存在容器的网络栈中,新容器进程有自己的文件系统、进程列表和资源限制,但会和已存在的容器共享 IP 地址和端口等网络资源,两者进程可以直接通过 lo 环回接口通信。
--net=none 让 Docker 将新容器放到隔离的网络栈中,但是不进行网络配置。之后用户可以自己进行配置。
- 用户可以使用 ip netns exec 命令来在指定网络命名空间中进行配置,从而配置容器内的网络。
描述: 该模式是Docker默认的一种网络通讯模式;
Bridge 网络模式原理:
答:Docker Daemon 首次启动时候,会在其所在的宿主机上创建一个名为Docker0的虚拟网桥,然后利用
veth pair技术创建一对虚拟网络接口
分别接入到Docker0网桥中和相关容器的Network Namespace之中,即Docker0 <-> veth pair <-> Container[Namespace]
;
容器在建立之初Docker0网桥会为其设置一个IP地址及其网关(即Docker0接口地址), 在通过iptables控制容器与网络通信以及容器间通信的, 实际上Docker0通过iptables中配置与宿主机上的物理网卡链接,并且符合条件的请求将会通过iptables转发到网桥docker0中,之后再分发给对应的机器;
网络图示:
---------- ----------
|Container| |Container|
|eth0@if57| |eth0@if70| #veth pair@ 对应宿主机上虚拟网卡号 $ip addr 可查看
---------- ----------
| |
------------------------------------
veth985cbf0@if56 veth842b243@if69 #veth pair:@ 后对应着容器内部网络号 $ip addr 可查看
| docker0 (bridge) |
------------------------------------
| ipv4_ip_forward (iptales)
(eth0|ens192)
基础实例:
# (1) 容器 test1 的网络信息
$docker exec -it test1 ip addr
56: eth0@if57: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
# (2) 查看iptables中关于test1容器的NAT转发规则
$iptables -t nat -L # 默认四条链+创建的Docker链
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL #网桥
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DOCKER all -- anywhere !loopback/8 ADDRTYPE match dst-type LOCAL #网桥
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.18.0.0/16 anywhere
MASQUERADE tcp -- 172.18.0.2 172.18.0.2 tcp dpt(destination port):http #运行通过的目标IP协议与应用
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- anywhere anywhere
RETURN all -- anywhere anywhere
DNAT tcp -- anywhere anywhere tcp dpt:tproxy to:172.18.0.2:80 #将指定容器的IP和端口进行转发
# 上述规则简要说明:
# 从任意源发送当前机器的80端口的TCP转发到容器地址为 172.18.0.2:80 的内部应用上;
# (3) 具体流程
# 访问宿主机的80端口,然后经过iptables的NAT PREROUTING将IP重定向到172.18.0.2, 之后重定向的数据包通过iptables中的FILTER配置,最终在NAT POSTROUTING阶段将IP地址伪装成为127.0.0.1,此时访问宿主机上映射到容器中端口,所有的请求便会转发到容器中;
127.0.0.1:80 -> NAT PREROUTING -> 172.18.0.2:80 -> FILTER FORWARD -> ACCEPT -> NAT POSTROUTING
总结说明:
宿主机Docker0网桥上与容器的所属的网络命名空间(Network Namespace)上分别创建一个虚拟网络接口
即veth985cbf0@if56 <---> eth0@if57
,它保证了无论哪一个接口接收到网络报文,都会无条件的转发到另外一方之上;一般都会添加本地系统转发支持
)采用是NATP(网络地址端口转换)的方式其包含两种转换方式SNAT(源地址) 和 DNAT(目的地址)
,容器连接外网是通过源NAT地址转换实现的,但是外部网络却无法访问到容器它也需要通过目的NAT转换(数据包的目的地址
)才能与容器进行通信;# (1) 内核转发参数查看
$sysctl -a | grep "ip_forward"
net.ipv4.ip_forward = 1
# PS:手动开启转发(临时生效)
sysctl -w net.ipv4.ip_forward = 1
# 启动docker deamon指定以下参数也可以实现本地转发
dockerd -h | grep "ip_forward"
--ip-forward Enable net.ipv4.ip_forward (default true)
# (2) 宿主机以外的世界需要访问容器时候,利用DNAT修改数据包的目的地址,数据包的流向如下
# 宿主机上的dokcer0网桥采用iptable识别到有请求访问容器IP和端口时,将数据包发送附加到docker0网桥上的veth985cbf0@if56接口中,该接口由于veth-pair特性会将接收到的数据包无条件的发送给容器内部的 eth0@if57 ,容器接收到数据包并做出响应;
|------------------------------------------------|
数据包(外界) -> Eth0 <-<- DNAT ->-> Docker0 —> veth985cbf0@if56 -> eth0@if57(容器内部)
| 宿主机 |
|------------------------------------------------|
# (3) 当容器内部需要访问宿主机以外的世界时候,利用SNAT修改数据包的源地址(此时容器对与外部网络是透明的),数据包流向为下所示
# 流程与上面相似方向相反,只是转发采用SNAT方式;
数据包[eth0@if57](容器) -> veth985cbf0@if56 -> Docker0 <-<- SNAT ->-> Eth0 -> 外界网络
|--------------------宿主机------------------------|
描述:该网络模式与Bridge桥接的网络模式存在一定的差异,最大差异是没有为容器创建一个隔离的网络环境
, 并且因为该host网络模式下的Docker容器与宿主机共享一个网络命名空间(namepace)
,拥有相同的网络设施并且容器的IP即为宿主机IP能直接与外界进行通信;
由于Doker容器的host网络模式在实现过程之中,由于不需要额外的网桥以及虚拟网卡、故而不涉及docker0网桥以及veth-pair虚拟网卡对;
[Docker Container](host 模式) -> 宿主机 (eth0) -> 外界网络
Host网络模式优缺点:
基础实例:
# (1) 创建Host网络模式的同期,可以通过ip addr 命令查看网卡信息,因为直接利用宿主机的网络栈,所有打印出的直接是宿主机的网卡接口信息;
docker run -it --net=host busybox ip addr
# 1: lo: mtu 65536 qdisc noqueue qlen 1000
# 2: ens192: mtu 1500 qdisc mq qlen 1000
# 3: docker0: mtu 1500 qdisc noqueue
# 5: br-fa08aa7db7a6: mtu 1500 qdisc noqueue
# 57: veth985cbf0@if56: mtu 1500 qdisc noqueue master docker0
# 71: veth842b243@if70: mtu 1500 qdisc noqueue master br-fa08aa7db7a6
描述: 该模式通常用于自定义网络栈时候使用,它会重用另外一个容器的网络命名空间(例如Bridge);比如Kubernetes也是使用该模式进行内部分布式应用通信;
实际上,采用Container模式的两个容器共享相同的1个Network Namespace;
Container 网络模式示意图:
Docker Container <-> Docker Container
| eth0@if57 |
-----------------
|
Docker0 (Bridge) <-> [veth985cbf0@if56]
| (ipv4.ip_forward)
eth0
基础实例:
# 1.创建一个bridge网络模式的容器
docker run -d -P --net=bridge --name nginx nginx:latest
# 2.查看其容器的ip 信息
docker inspect nginx | grep "IPAddress"
docker inspect nginx | grep "IPAddress"
"SecondaryIPAddresses": null,
"IPAddress": "172.17.0.3",
# 3.构建一个采用Container网络模式的容器
# 创建容器连接到已键容器的网络之中,比如采用container:容器名称进行制定网络;
docker run -d --name busybox --net=container:nginx busybox:latest top
# 13d274350a2855c1295e3c93fd4e6a166b6e34cfc71a91e1396ca8b4a75f67e2
# 4.此时您会发现其IP地址与链接到容器中的网络IP地址一样均为172.17.0.3
$docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
13d274350a28 busybox:latest "top" 3 seconds ago Up 2 seconds busybox
04074e2e85b4 nginx:latest "/docker-entrypoint.…" 4 minutes ago Up 3 minutes 0.0.0.0:32768->80/tcp nginx
$docker exec -it busybox ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
72: eth0@if73: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
描述: 该模式不为Docker容器提供任何的网络环境可以说该模式只是对容器做了极少的设定,一旦设置none网络模式容器内部就只能使用loopback设备,不会再有其它的网络资源环境;
该模式下方便docker开发者基于此做出其他可能的网络定制开发, 实际上该模式关闭了容器的网络功能,应用场景如下;
基础实例:
# 1.创建一个网络模式为none的容器
docker run -itd --net=none --name=busybox-none busybox top
# 2.查看创建的容器的ip地址,只能看见一个回环地址
docker exec -it busybox-none ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
描述:Overlay模式是Docker原生的跨主机网络方案(其他一些方案:Flannel/Weave和Calico K8s已默认使用
),而Docker 又通过Libnetwork以及CNM将上述各种方案与docker集成在一起;
Q: 什么是Libnetwork库?
答: 他是Docker容器的网络库,其核心内容是其定义的
Container Network Model (CNM)容器网络模型
他对容器网络进行了抽象;
| -> Native Drivers(None, Bridge, Overlay, Macvlan) #原生
Docker -> Libnetwork | ->
| -> Remote Drivers(Flannel, Weave, Calico) #第三方网络插件
CNM 由以下三类组件组成:
Linux Network Namespace
是基于 Sandbox的标准实现,它是容器的网络栈其囊括Interface, 路由器 和 DNS 设置
,也就是说Sandbox将一个容器与另外容器通过Namespace进行隔离,一个容器包含一个Sandbox,它可以包含来自不同的Network的Endpoint,即每个Sandbox可以有多个Endpoint
隶属于不同的网络;总结:
Overlay网络原理
描述:Docker overlay网络需要一个K-V数据库用于保存状态相关信息(包括CNM网络组件Network,Endpoint以及IP),可选方案有Consul/Etc/Zokeeper等,此处以Consul一种K-V数据库为例进行演示,此处我们并不需要写任何代码只需要按照Consul即可;
基础实践:
# 1.在Docker Deamon其中一个节点中拉取并运行Consul镜像
docker run -d -p 8500:8500 -h consul --name consul-net-overlay progrium/consul -server -bootstrap**docker run -d -p 8500:8500 -h consul --name consul-net-overlay progrium/consul -server -bootstrap
# Status: Downloaded newer image for progrium/consul:latest
# 86d9d93384034b19acad56dfdebed754da060125b534b240fd87f79e18a2bfcd
# 2.然后再各个Docker节点的Deamon上进行配置Consul发现: /etc/systemd/system/docker.service 再其后加上如下参数
--cluster-store=consul://10.10.107.245:8500 --cluster-advertise=ens192:2376
$ systemctl cat docker
# /usr/lib/systemd/system/docker.service
# ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --exec-opt native.cgroupdriver=systemd --cluster-store=consul://10.10.107.245:8500 (consul运行节点) --cluster-advertise=ens192:2376(当前节点网卡或者IP即Docker Deamon服务)
# 3.systemd守护进程重启与重启docker
[root@worker-03 ~]$ systemctl daemon-reload
[root@worker-03 ~]$ systemctl restart docker
# 4.创建overlay 网络与创建bridge网络基本一致,只是在-d参数时候有些许不同
docker network create -d overlay overlay-net
docker network create -d overlay overlay-net-sub --subnet 172.25.0.0/24 --gateway 172.25.0.1
# 5.创建运行容器时候指定-network参数,及时在不同的docker主机下创建的容器只要--network为oveylay-net它们之间都能相互访问;
docker run --network overlay-net busybox sleep 6000
docker run --network overlay-net busybox sleep 6000
$docker ps
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 430ec4450df4 busybox "sleep 6000" 6 minutes ago Up 6 minutes cranky_curie
# 0902833f9a63 busybox "sleep 6000" 12 minutes ago Up 11 minutes intelligent_chaplygin
# 6.Docker 网络查看
$docker network ls
NETWORK ID NAME DRIVER SCOPE
5de5f196afda bridge bridge local
23d95a1bd969 docker_gwbridge bridge local
3a700c6af892 host host local
93a5b381ef0d none null local
e437f4650c96 overlay-net overlay global
$docker network inspect docker_gwbridge -f "{{.IPAM.Config}}"
# [{172.18.0.0/16 172.18.0.1 map[]}]
$docker network inspect overlay-net -f "{{.IPAM.Config}}"
# [{10.0.0.0/24 10.0.0.1 map[]}]
# 7.创建容器的IP地址查看
[root@worker-03 ~]$docker exec -it 090 ip addr
22: eth0@if23: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue
link/ether 02:42:0a:00:00:02 brd ff:ff:ff:ff:ff:ff
inet 10.0.0.2/24 brd 10.0.0.255 scope global eth0
valid_lft forever preferred_lft forever
25: eth1@if26: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.2/16 brd 172.18.255.255 scope global eth1
valid_lft forever preferred_lft forever
[root@worker-03 ~]$docker exec -it 430 ip addr
27: eth0@if28: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue
link/ether 02:42:0a:00:00:03 brd ff:ff:ff:ff:ff:ff
inet 10.0.0.3/24 brd 10.0.0.255 scope global eth0
valid_lft forever preferred_lft forever
29: eth1@if30: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.3/16 brd 172.18.255.255 scope global eth1
valid_lft forever preferred_lft forever
[root@worker-03 ~]$docker exec -it 090 sh
/ # ip addr
/ # ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1): 56 data bytes
64 bytes from 10.0.0.1: seq=0 ttl=64 time=0.540 ms
/ # ping 10.0.0.2
PING 10.0.0.2 (10.0.0.2): 56 data bytes
64 bytes from 10.0.0.2: seq=0 ttl=64 time=0.055 ms
/ # ping 10.0.0.3 #可以看到是正常通信的
PING 10.0.0.3 (10.0.0.3): 56 data bytes
64 bytes from 10.0.0.3: seq=0 ttl=64 time=0.110 ms
访问Consul查看究竟存在什么东西:http://10.10.107.245:8500/ui/#/dc1/kv/docker/network/v1.0/
总结:
从上面的实际操作中我们可以看见当创建完overlay网络后,查看docker网络会发现网络中多了个 overlay-net (类型为Overlay,Scope为Global)与 docker_gwbridge (类型为Bridge,Scope为local),为什么会多出来一个 docker_gwbridge网络呢?
答:实际上这就是Overlay工作原理之所在;
可以通过yum install -y bridge-utils && brctl show
进行查看得出结果, 当每创建一个网络类型为Overlay的容器,docker_gwbridge 下便会挂载一个veth***,这说明oveylay容器是通过此网桥来进行对外连接的;
bridge name bridge id STP enabled interfaces
docker0 8000.024274c0c574 no
docker_gwbridge 8000.02422e73bb62 no vethae1f217
vethec87cb3
简单的说overlay网络数据还是从Bridge网络docker_gwbridge网桥出去的,由于Consul的作用记录leoverlay网络的CNM三大组件的信息
,使得其它主机docker知道此网络类型为oveylay,并且可以在该网络下不同主机之间进行相互的通信访问,但是实际上出口还是Docker_gwbridge;
描述:Docker最开始采用AUFS作为文件系统,也得益于AUFS分层的概念,实现了多个Container可以共享同一个image;Docker支持的存储驱动类型有overlay2、AUFS、Btrfs、Device mapper、OverlayFS、ZFS
五种存储驱动,所有驱动都用到写时复制(CoW)的技术。
[2020年6月19日] - 目前最新版本的 docker 默认优先采用 overlay2 的存储驱动,对于已支持该驱动的 Linux 发行版,不需要任何进行任何额外的配置。devicemapper 存储驱动已经在 docker 18.09 版本中被废弃,docker 官方推荐使用 overlay2 替代devicemapper。
docker info
Server Version: 19.03.3
Storage Driver: overlay2
所以无论有多少个容器共享同一个image,所做的写操作都是对从image中复制到自己的文件系统中的复本上进行,并不会修改image的源文件,且多个容器操作同一个文件,会在每个容器的文件系统里生成一个复本,每个容器修改的都是自己的复本,相互隔离,相互不影响。使用CoW可以有效的提高磁盘的利用率。
什么是AUFS?
答:AUFS(AnotherUnionFS)是一种Union FS,是文件级的存储驱动。
AUFS能透明覆盖一或多个现有文件系统的层状文件系统,把多层合并成文件系统的单层表示。文件系统可以一层一层地叠加修改文件,但是无论底下有多少层都是read only,只有最上层的文件系统是可写的;
当需要修改一个文件时,AUFS创建该文件的一个副本,使用CoW将文件从只读层复制到可写层进行修改,结果也保存在可写层;
在Docker中底下的只读层就是image,可写层就是Container;
描述:Overlay是Linux内核3.18后支持的,它也是一种UnionFS与AUFS不同的是Overlay只有两层:一个upper文件系统和一个lower文件系统,分别代表Docker的镜像层和容器层。
当需要修改一个文件时,使用CoW将文件从只读的lower复制到可写的upper进行修改,结果也保存在upper层。
同样在Docker中底下的只读层就是image,可写层就是Container;
采用一个挂载OverlayFS小例子深入理解其存储驱动:
# (1) common.txt分别存放不同的内容
比如lower1下面common.txt内容是lower1,lower2下面common.txt内容是lower2,lower3下面common.txt内容是lower3,upper目录有个和lower2/ower2.sh同名的目录
$tree -L 2
├── lower1
│ ├── common.txt
│ └── ower1.sh
├── lower2
│ ├── common.txt
│ └── ower2.sh
├── lower3
│ ├── common.txt
│ └── ower3.sh
├── merged
├── upper
│ ├── ower2.sh
│ └── up.txt
└── work
└── work
# (2) 我们通过下面的命令进行挂载
* lowerdir: 代表lower层,可以有多个,优先级依次降低,也就是说`lower1 > lower2 > lower3`
* upperdir: 代表upper层,会覆盖lower层
* workdir: 工作目录,用于存放临时文件
* merged: 挂载点,我们看看操作之后的目录
$mount -t overlay overlay -o lowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work merged
# (3) 挂载后的操作目录`merged挂载点`.
├── lower1
│ ├── common.txt
│ └── ower1.sh
├── lower2
│ ├── common.txt
│ └── ower2.sh
├── lower3
│ ├── common.txt
│ └── ower3.sh
├── merged #是不是看出了什么奥秘,很清晰的说明了overlayFS的上面两个特征
│ ├── common.txt
│ ├── ower1.sh
│ ├── ower2.sh
│ ├── ower3.sh
│ └── up.txt
├── upper
│ ├── ower2.sh
│ └── up.txt
└── work
└── work
正如前面介绍的COW(写时复制)在overlayFS中也是只对于只读的lower层的操作,当用到它时候会把它复制到upper层然后再对upper的进行操作,我们演示几个对挂载后的目录的操作,就能很明白这个过程了:
主次设备号(mknod c 0 0)都是0的设备
,当在merge层去找的时候,overlayFS会自动过滤掉和whiteout文件自身以及和他同名的lower层的文件,从而达到隐藏的目的;overlayFS引入了一种Opaque(不透明的
)的属性,通过设置upper层上对应的目录上设置"trusted.overlay.opaque"
为y来实现(前提是upper所在的文件系统支持xattr属性),overlayFS在读取上下层存在同名目录的时候,如果upper层的目录被设置了Opaque的属性,他会忽这个目录下层的所有同名目录项来保证新建的是个空目录。
描述:Device mapper是Linux内核2.6.9后支持的,提供的一种从逻辑设备到物理设备的映射框架机制,在该机制下,用户可以很方便的根据自己的需要制定实现存储资源的管理策略;
注意点:AUFS和OverlayFS都是文件级存储,而Device mapper是块级存储,所有的操作都是直接对块进行操作,而不是文件。
Device mapper驱动会先在块设备上创建一个资源池,然后在资源池上创建一个带有文件系统的基本设备,所有镜像都是这个基本设备的快照,而容器则是镜像的快照
。所以在容器里看到文件系统是资源池上基本设备的文件系统的快照,并不有为容器分配空间。当要写入一个新文件时,在容器的镜像内为其分配新的块并写入数据(写时分配)
Device mapper存储驱动默认会创建一个100G的文件包含镜像和容器(其实就是资源存储池)。每一个容器被限制在10G大小的卷内,可以自己配置调整。它会默认会在/var/lib/docker/devicemapper/devicemapper
目录下生成data和metadata两个稀疏文件,并将两个文件挂为loop设备作为块设备来使用(默认模式),它使用空闲文件来构建存储池,性能非常低。
注意事项:
1.当要修改已有文件时,再使用CoW为容器快照分配块空间,将要修改的数据复制到在容器快照中新的块里再进行修改,注意挂载后的磁盘UUID变化后则需要手动修改deviceset-metadata来指定BaseDeviceUUID;
2.在18.09版本之前在Docker初始化时候可以指定--storage-opt dm.loopdatasize=500G --storage-opt dm.loopmetadatasize=4G
,将回收环境设备大小设置为500G(数据存储),元数据文件大小为4G(稀疏文件),然后分别附加到回环设备/dev/loop0和/dev/loop1,其次再基于回环块设备创建Thin Pool;
描述:Btrfs被称为下一代写时复制文件系统,并入Linux内核也是文件级级存储,但可以像Device mapper一直接操作底层设备。
Btrfs把文件系统的一部分配置为一个完整的子文件系统,称之为subvolume 。那么采用 subvolume,一个大的文件系统可以被划分为多个子文件系统,这些子文件系统共享底层的设备空间,在需要磁盘空间时便从底层设备中分配,类似应用程序调用 malloc()分配内存一样。为了灵活利用设备空间,Btrfs 将磁盘空间划分为多个chunk 。每个chunk可以使用不同的磁盘空间分配策略。比如某些chunk只存放metadata,某些chunk只存放数据
模型优点:Btrfs支持动态添加设备,用户在系统中增加新的磁盘之后,可以使用Btrfs的命令将该设备添加到文件系统中。
Btrfs把一个大的文件系统当成一个资源池,配置成多个完整的子文件系统,还可以往资源池里加新的子文件系统,而基础镜像则是子文件系统的快照,每个子镜像和容器都有自己的快照,这些快照则都是subvolume
的快照。
描述:ZFS 文件系统是一个革命性的全新的文件系统,它从根本上改变了文件系统的管理方式,ZFS 完全抛弃了"卷管理",不再创建虚拟的卷,而是把所有设备集中到一个存储池中来进行管理,用"存储池"的概念来管理物理存储空间。以前文件系统都是构建在物理设备之上的。为了管理这些物理设备,并为数据提供冗余,"卷管理"的概念提供了一个单设备的映像。而ZFS创建在虚拟的,被称为"zpools"的存储池之上。每个存储池由若干虚拟设备(virtual devices,vdevs)组成。这些虚拟设备可以是原始磁盘,也可能是一个RAID1镜像设备,或是非标准RAID等级的多磁盘组。于是zpool上的文件系统可以使用这些虚拟设备的总存储容量。
首先从zpool里分配一个ZFS文件系统给镜像的基础层,而其他镜像层则是这个ZFS文件系统快照的克隆,快照是只读的,而克隆是可写的,当容器启动时则在镜像的最顶层生成一个可写层。如下图所示:
存储驱动的对比及适应场景
AUFS VS Overlay
AUFS和Overlay都是联合文件系统,但AUFS有多层,而Overlay只有两层,所以在做写时复制操作时,如果文件比较大且存在比较低的层,则AUSF可能会慢一些。而且Overlay并入了linux kernel mainline,AUFS没有,所以可能会比AUFS快。但Overlay还太年轻,要谨慎在生产使用。而AUFS做为docker的第一个存储驱动,已经有很长的历史,比较的稳定,且在大量的生产中实践过,有较强的社区支持。目前开源的DC/OS指定使用Overlay。
Overlay VS Device mapper
Overlay是文件级存储,Device mapper是块级存储,当文件特别大而修改的内容很小,Overlay不管修改的内容大小都会复制整个文件,对大文件进行修改显示要比小文件要消耗更多的时间,而块级无论是大文件还是小文件都只复制需要修改的块,并不是整个文件,在这种场景下显然device mapper要快一些。因为块级的是直接访问逻辑盘,适合IO密集的场景。而对于程序内部复杂,大并发但少IO的场景,Overlay的性能相对要强一些。
Device mapper VS Btrfs Driver VS ZFS
Device mapper和Btrfs都是直接对块操作,都不支持共享存储,表示当有多个容器读同一个文件时,需要生活多个复本,所以这种存储驱动不适合在高密度容器的PaaS平台上使用。而且在很多容器启停的情况下可能会导致磁盘溢出,造成主机不能工作。Device mapper不建议在生产使用。Btrfs在docker build可以很高效。
ZFS最初是为拥有大量内存的Salaris服务器设计的,所在在使用时对内存会有影响,适合内存大的环境。ZFS的COW使碎片化问题更加严重,对于顺序写生成的大文件,如果以后随机的对其中的一部分进行了更改,那么这个文件在硬盘上的物理地址就变得不再连续,未来的顺序读会变得性能比较差。ZFS支持多个容器共享一个缓存块,适合PaaS和高密度的用户场景。
IO性能对比
测试工具:IOzone(是一个文件系统的benchmark工具,可以测试不同的操作系统中文件系统的读写性能)
测试场景:从4K到1G文件的顺序和随机IO性能
测试方法:基于不同的存储驱动启动容器,在容器内安装IOzone,执行命令./iozone -a -n 4k -g 1g -i 0 -i 1 -i 2 -f /root/test.rar -Rb ./iozone.xls
测试项的定义和解释
测试数据对比结果:
通过以上的性能数据可以看到:
注意事项:
描述:Docker 数据卷是被用于共享和持久化数据的,而且它的声明周期是独立于容器的,所以当容器宕掉或者删除并不会导致数据卷中的数据丢失;
Q: 数据卷的实现原理?
答: 简单的说卷其实就是文件或者目录,通过挂载的方式,由Docker Daemon挂载到容器内部,不属于联合系统;
Docker 提供的几种数据卷的实现方式:
# (1) 宿主机随机目录,指定的目录如果没有不存在将会被创建
docker run -itd -v /tmp/container/folder --name data-volume busybox top
docker inspect data-volume -f 'Source:{{(index .Mounts 0).Source}}{{println}}Destination:{{(index .Mounts 0).Destination}}'
# 即表示容器中的/tmp/container/folder目标被挂载到宿主机的随机目录;
# Source:/var/lib/docker/volumes/1a035d81e56447cd314faf560754b1808234645785f1b61a2c17199b16a7cf27/_data
# Destination:/tmp/container/folder
$docker exec -it data-volume sh
$ls /tmp/container/folder/ls^C
# (2) 宿主机指定目录
docker run -itd -v /tmp/mnt_isolation:/tmp/tar/tmp/container/folder --name data-volume-1 busybox top
docker inspect data-volume-1 -f 'Source:{{(index .Mounts 0).Source}}{{println}}Destination:{{(index .Mounts 0).Destination}}'
# 指定了宿主机映射的目录(下面查看源、目的地址的目录)
# Source:/tmp/mnt_isolation
# Destination:/tmp/tar/tmp/container/folder
# (3) 连接已经存在的数据卷,通过--volumes-from将上面的数据卷data-volume进行挂载起来
docker run -itd --volumes-from data-volume --name data-volume-2 busybox top
# 91bfdb9c9ca88571faebf6f2409105b1e55d24866fe30a4ea7c2f750031c8baf
docker run -itd --volumes-from data-volume --name data-volume-3 busybox top
# f601713d75277e6255e9a52a30046f1e38c169d7674f3ef2dd287bdb444820d1
# 进入data-volume-3容器中在其数据卷目录中创建文件
$docker exec -it data-volume-3 touch /tmp/container/folder/$$.txt
# 分别查看几个容器共享的数据卷是否一致
$docker exec -it data-volume-3 ls -alh /tmp/container/folder/
-rw-r--r-- 1 root root 0 Jul 6 13:50 3758.txt
$docker exec -it data-volume-2 ls -alh /tmp/container/folder/
-rw-r--r-- 1 root root 0 Jul 6 13:50 3758.txt
$docker exec -it data-volume ls -alh /tmp/container/folder/
-rw-r--r-- 1 root root 0 Jul 6 13:50 3758.txt
# 删除数据卷容器目录时需要加上-v则删除一个停止的容器并且其数据卷中的内容
$docker stop data-volume data-volume-2 data-volume-3
$docker rm -v data-volume data-volume-2 data-volume-3 #需要删除全部的数据卷存储的数据才会全部消失
data-volume
#补充:docker volume prune 清除已停止的数据卷
-v 源宿主机:目标容器
),将卷中保存的数据保存在宿主机上,通过Volume Plugin机制能够很方便的整合第三方存储为Docker提供Volume;Q: 什么是存储插件?
答: 通过使用Docker存储卷插件,为容器提供了持久化卷存储;
Q: 存储插件有哪些?常用的有以下几种插件:
Q: 采用哪些方式进行容器数据备份?
fluentd或filebeat
)将存储于宿主机上的数据进行实时收集从而减少容器销毁导致的数据丢失带来的损失;Docker 服务的运行目前需要 root 权限,因此其安全性十分关键,由于运行一个容器或应用程序的核心是通过 Docker 服务端。
Docker 允许用户在主机和容器间共享文件夹,同时不需要限制容器的访问权限,这就容易让容器突破资源限制;
例如:恶意用户启动容器的时候将主机的根目录/映射到容器的 /host 目录中,那么容器理论上就可以对主机的文件系统进行任意修改了
因此当提供容器创建服务时(例如通过一个 web 服务器),要更加注意进行参数的安全检查,防止恶意的用户用特定参数来创建一些破坏性的容器。
DOCKER的安全特性:
Linux 内核一个强大的特性(从Kernel 2.2版本起),可以提供细粒度的权限访问控制操作能力,既可以作用在进程上,也可以作用在文件上。
使用能力机制对加强 Docker 容器的安全有很多好处;
默认情况下,Docker 启动的容器被严格限制只允许使用内核的一部分能力。并且Docker采用白名单机制,禁用必需功能之外的其它权限
例如,一个 Web 服务进程只需要绑定一个低于 1024 的端口的权限,并不需要 root 权限。那么它只需要被授权 net_bind_service 能力即可
为了加强安全,容器可以禁用一些没必要的权限。
这样就算攻击者在容器中取得了 root 权限,也不能获得本地主机的较高权限,能进行的破坏也有限。
利用一些现有的安全机制来增强使用 Docker 的安全性,例如 TOMOYO, AppArmor, SELinux, GRSEC 等。
Docker 当前默认只启用了能力机制,用户可以采用多种方案来加强 Docker 主机的安全,例如:
docker安全配置
#建议docker账号与组,不采用root运行
#建立 docker 组:
$ sudo groupadd docker
#将当前用户加入 docker 组:
$ sudo usermod -aG docker $USER
#默认情况下,不同容器之间是允许网络互通的。
#如果为了安全考虑,可以在 /etc/docker/daemon.json 文件中配置 {"icc": false} 来禁止它。
Q:在说此项前我们先来了解容器内部应用程序运行用户设置建议采用gosu还是sudo?
答:我们通过实战来确定到底使用哪一个命令较好;
# gosu
docker run --rm gosu/alpine gosu root ps aux
PID USER TIME COMMAND
1 root 0:00 ps aux #通过gosu启动的是符合我们要求的(PID为1)容器内的唯一进程
# sudo
docker run --rm ubuntu:trusty sudo ps aux
#容器内出现了两个进程,sudo命令会创建第一个进程,然后该进程再创建了ps进程,而且ps进程的PID并不等于1,这是达不到我们要求的,此时在宿主机向该容器发送信号量收到信号量的是sudo进程;
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 46012 1772 ? Rs 12:05 0:00 sudo ps aux
root 6 0.0 0.0 15568 1140 ? R 12:05 0:00 ps aux
小结:
正题回归
描述:为了安全容器中不要使用root账号(即最小权限),此时就需要一个能够提升自定账号权限的命令 gosu 便应运而生它与 sudo 类似但它比sudo更安全;
方便学习先来看看一个小例子(在镜像中创建非root账号
):
首先我们以redis官方镜像的Dockerfile为例,来看看如何创建账号
#1.先添加我们的用户和组,以确保他们的id被一致地分配,不管添加了什么依赖项
RUN groupadd -r redis && useradd -r -g redis redis
#2.可见redis官方镜像使用groupadd和useradd创建了名为redis的组合账号,接下来就是用redis账号来启动服务了,理论上应该是以下套路;
* 用USER redis将账号切换到redis;
* 在docker-entrypoint.sh执行的时候已经是redis身份了,如果遇到权限问题,例如一些文件只有root账号有读、写、执行权限,用sudo xxx命令来执行即可;
但事实并非如此!
在Dockerfile脚本中未发现USER redis
命令,意味着执行docker-entrypoint.sh文件的身份是root;
其次在docker-entrypoint.sh中没有发现su - redis命令,也没有sudo命令
;
这是怎么回事呢?难道容器内的redis服务是用root账号启动的?
#动手实践
docker run --name myredis -idt redis
docker exec -it myredis /bin/bash
apt-get update && apt-get install procps #更新软件源以及PS命令安装
root@122c2df16bbb:/data# ps -ef
UID PID PPID C STIME TTY TIME CMD
redis 1 0 0 09:22 ? 00:00:01 redis-server *:6379
root 287 0 0 09:36 ? 00:00:00 /bin/bash
root 293 287 0 09:39 ? 00:00:00 ps -ef
上面的结果展示了两个关键信息:
第一,redis服务是redis账号启动的并非root;
第二,redis服务的PID等于(重要),宿主机执行docker stop命令时,该进程可以收到SIGTERM信号量
,于是redis应用可以做一些退出前的准备工作,例如保存变量、退出循环等,也就是优雅停机(Gracefully Stopping
);
现在我们已经证实了redis服务并非root账号启动,而且该服务进程在容器内还是一号进程,但是我们在Dockerfile和docker-entrypoint.sh脚本中都没有发现切换到redis账号的命令,也没有sudo和su,这是怎么回事呢?
答案:在于redis的docker-entrypoint.sh文件之中
#!/bin/sh
set -e
# first arg is `-f` or `--some-option`
# or first arg is `something.conf`
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
set -- redis-server "$@"
fi
# allow the container to be started with `--user` | 首次运行进入
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
# 当前文件夹下不属于redis用户的文件,全部授权为redis用户
find . \! -user redis -exec chown redis '{}' +
exec gosu redis "$0" "$@"
fi
exec "$@"
注意上图中的代码分析一下:
docker run --name myredis -idt redis redis-server /usr/local/etc/redis/redis.conf
;docker-entrypoint.sh
脚本此时的账号是root;# gosu redis "$0" "@"
docker-entrypoint.sh redis-server /usr/local/etc/redis/redis.conf
gosu redis "$0" "@"
前面加上个exec,表示以gosu redis “$0” "@"这个命令启动的进程替换正在执行的 docker-entrypoint.sh 进程 保证了对应的进程ID为1
;gosu redis "$0" "@"
导致docker-entrypoint.sh再执行一次,但是当前的账号已经不是root了,所以会执行兜底逻辑 exec “$@”;redis-server /usr/local/etc/redis/redis.conf
,因此redis服务会启动并且启动服务的用户为redis;最后我们在 Redis 的Dockerfile 中可以看见安装gosu的一些身影
# grab gosu for easy step-down from root
# https://github.com/tianon/gosu/releases
ENV GOSU_VERSION 1.10
RUN set -ex; \
\
fetchDeps=" \
ca-certificates \
dirmngr \
gnupg \
wget \
"; \
apt-get update; \
apt-get install -y --no-install-recommends $fetchDeps; \
rm -rf /var/lib/apt/lists/*; \
\
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc; \
chmod +x /usr/local/bin/gosu; \
gosu nobody true; \
\
apt-get purge -y --auto-remove $fetchDeps
WeiyiGeek Blog - 为了能到远方,脚下的每一步都不能少。
本文章来源 Blog 站点(友链交换请邮我哟
):
更多学习笔记文章请关注 WeiyiGeek 公众账号
【点击我关注】