写在前面:容器云是推动微服务发展的主要推手
一、Docker概览
1、描绘Docker
- 基于 Linux 内核的
Cgroup
,Namespace
,以及Union FS
等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术,由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。 - 最初实现是基于
LXC
(LinuX Container),从 0.7 以后开始去除 LXC,转而使用自行开发的Libcontainer
,从 1.11 开始,则进一步演进为使用runC
和Containerd
。 - Docker 在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护,使得 Docker技术比虚拟机技术更为轻便、快捷。
2、为什么要用Docker
• 更高效地利用系统资源
• 更快速的启动时间
• 一致的运行环境
• 持续交付和部署
• 更轻松地迁移
• 更轻松地维护和扩展
……
3、Docker引擎架构
过程解析:docker run -it … 执行此命令运行一个容器,首先会通过restful api向docker daemon发送一个post请求。dockerd接收到请求之后,向containerd发送GRPC调用创建容器。containerd(实际是runC)接收到调用之后,会fork出一个shim进程,并fork一个以shim进程为父进程的container进程,shim的父进程是init进程。
为什runC执行fork调用,创建的shim进程的父进程却是init进程
,而且container进程的父进程是shim进程?,这个是用了Setppid函数
,代码示例如下:
package main
import (
"fmt"
"os"
)
func main() {
// 创建子进程
pid, err := os.Fork()
if err != nil {
fmt.Println("fork failed:", err)
os.Exit(-1)
}
if pid == 0 {
// 子进程
fmt.Println("I am the child process, my parent is:", os.Getppid())
} else {
// 父进程
fmt.Println("I am the parent process, my pid is:", pid)
// 修改子进程的父进程
newPid := os.Getpid()
err = os.Setppid(pid, newPid)
if err != nil {
fmt.Println("setppid failed:", err)
os.Exit(-1)
}
fmt.Println("child's new parent is:", newPid)
}
}
早期其实是没有这个shim进程的,这样就会存在一个问题,所有runc进程都挂到containerd进程下,containerd如果挂了,其他runc进程就变成孤儿进程或僵尸进程
了。
疑问:runc是用来创建容器的,那么为什么一个runc进程对应一个被创建的容器呢?
答:因为runc其实是一个二进制程序, shim要创建容器, 就传要创建的容器的namespace、image id、containerd.sock file location 这些参数给runc程序,这样他就会创建出一个容器出来, 运行一次程序, 就是一个runc进程。
参考资料:docker、containerd、runc、shim… 容器技术名词全解析
4、OCI(开放容器标准)
OCI全称开放容器标准,OCI 主要定义两个规范:
-
Runtime Specification(
运行时标准
)
答:具体实现:Runc,runtime 规范中介绍了如何将拉下来的镜像解压缩到磁盘上的 Filesystem Bundle并运行
(文件系统包)。在 OCI 标准下,运行一个容器的过程就是下载一个 OCI 的镜像,将其解压到某个 Filesystem Bundle 中,然后某个 OCI Runtime 就会运行这个 Bundle -
Image Specification(
镜像标准
)答:如何通过构建系统打包,生成镜像清单(Manifest)、文件系统序列化文件、镜像配置。
-
Distribution Specification(
分发标准
)
答:定义了如何分发容器镜像
下面这个图很好的解释了两个标准负责的部分:
容器主要特性:(安全性、隔离性、便携性)namespace、Union FS
、(可配额)cgroups
二、Namespace隔离
Linux Namespace 是一种 Linux Kernel 提供的资源隔离方案
:
- 系统可以为进程分配不同的 Namespace;
- 并保证不同的 Namespace 资源独立分配、进程彼此隔离,即不同的 Namespace 下的进程互不干扰 。
1、内核代码中namespace实现
task_struct完整代码地址,搜索struct task_struct {
- 进程数据结构
struct task_struct {
...
/* namespaces */
struct nsproxy *nsproxy;
...
}
- Namespace 数据结构
/*
* A structure to contain pointers to all per-process
* namespaces - fs (mount), uts, network, sysvipc, etc.
*
* The pid namespace is an exception -- it's accessed using
* task_active_pid_ns. The pid namespace here is the
* namespace that children will use.
*
* 'count' is the number of tasks holding a reference.
* The count for each namespace, then, will be the number
* of nsproxies pointing to it, not the number of tasks.
*
* The nsproxy is shared by tasks which share all namespaces.
* As soon as a single namespace is cloned or unshared, the
* nsproxy is copied.
*/
struct nsproxy {
atomic_t count;
struct uts_namespace *uts_ns;
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns;
struct pid_namespace *pid_ns_for_children;
struct net *net_ns;
// 新加的三个namespace
struct time_namespace *time_ns;
struct time_namespace *time_ns_for_children;
struct cgroup_namespace *cgroup_ns;
};
2、Linux 对 Namespace 操作方法
• clone
在创建新进程的系统调用时,可以通过 flags 参数指定需要新建的 Namespace 类型:
// CLONE_NEWCGROUP / CLONE_NEWIPC / CLONE_NEWNET / CLONE_NEWNS / CLONE_NEWPID /
CLONE_NEWUSER / CLONE_NEWUTS
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg)
• setns
该系统调用可以让调用进程加入某个已经存在的 Namespace 中:
Int setns(int fd, int nstype)
• unshare
该系统调用可以将调用进程移动到新的 Namespace 下:
int unshare(int flags)
3、Namespace隔离性
图比较老,就不加time、cgroup namespace了。
4、Namespace含义解析
- pid
不同用户的进程
就是通过 pid namespace 隔离开的,且不同 namespace 中可以有相同 pid。 在容器中的实现效果就是,一个容器中有从1开始独立于宿主机的进程,但容器其实也是一个进程,它在主机namespace中也会有个pid
。 - net
网络隔离是通过 net namespace 实现的, 每个 net namespace 有独立的 network devices, IP addresses, IP routing tables, /proc/net 目录。 Docker 默认采用 veth pair 的方式将 container 中的虚拟网卡同 host 上的一个 docker bridge: docker0 连接在一起。 - ipc
Container 中进程交互还是采用 linux 常见的进程间交互方法
(interprocess communication – IPC), 包括常见的信号量、消息队列和共享内存(不支持管道)
。container 的进程间交互实际上还是host上 具有相同 Pid namespace 中的进程间交互
,因此需要在 IPC通信申请时需要加入 namespace 信息 - 每个 IPC 资源有一个唯一的 32 位 ID。 - mnt
mnt namespace 允许不同 namespace 的进程看到的文件结构不同,这样每个 namespace 中的进程所看到的文件目录就被隔离开了。
- uts
UTS(“UNIX Time-sharing System”) namespace允许每个 container 拥有独立的 hostname 和domain name, 使其在网络上可以被视作一个独立的节点而非 Host 上的一个进程
。 - user
每个 container 可以有不同的 user 和 group id, 也就是说可以在 container 内部用 container 内部的用户执行程序而非 Host 上的用户。注意container内部的用户id会映射到主机上的一个id range
,你可以在security context
中去设置这个range。 - cgroup
隔离cgroup根目录,如果不隔离,容器内可以看到整个cgroup根目录,容器拥有者就可以操作整个宿主机的cgroup,这是不安全的(本人认为)。 - time
隔离系统时间,主机和容器可以看到不一样的时间。
5、关于namespace常用操作
(1)查看当前系统的 namespace:
lsns –t <NS_TYPE>
(2)查看某进程的 namespace:
ls -la /proc/<pid>/ns/
(3)进入某进程的拥有的namespace环境运行命令:
// 相当于docker exec -it bash进入了容器
nsenter -t <pid> -n ip addr
三、Cgroups资源限制
- Cgroups (Control Groups)是 Linux 下用于
对一个或一组进程进行资源控制和监控的机制
; - 可以对诸如 CPU 使用时间、内存、磁盘 I/O 等进程所需的资源进行限制;
不同资源的具体管理工作由相应的 Cgroup 子系统(Subsystem)来实现
;- 针对不同类型的资源限制,只要将
限制策略在不同的的子系统上进行关联
即可 ; - Cgroups 在不同的系统资源管理子系统中以层级树(Hierarchy)的方式来组织管理:每个 Cgroup 都可以包含其他的子Cgroup(理解应用场景应该是
容器套容器
的场景?),因此子 Cgroup 能使用的资源除了受本 Cgroup 配置的资源参数限制,还受到父Cgroup 设置的资源限制
。
1、Linux 内核代码中 Cgroups 的实现
task_struct完整代码地址,搜索struct task_struct {
内核cgroups实现解读,点进来直接搜索css_set就找到了
- 进程数据结构
struct task_struct {
#ifdef CONFIG_CGROUPS
struct css_set __rcu *cgroups;
struct list_head cg_list;
#endif
}
- css_set 是 cgroup_subsys_state 对象的集合数据结构
struct css_set {
/*
* Set of subsystem states, one for each subsystem. This array is
* immutable after creation apart from the init_css_set during
* subsystem registration (at boot time).
*/
struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
};
subsys 是一组指向 struct cgroup_subsys_state 指针的指针数组;CGROUP_SUBSYS_COUNT 是内核中支持子系统的最大值;每一个 cgroup_subsys_state 存储的是进程的某一个子系统的相关信息。
struct cgroup_subsys_state {
/* PI: the cgroup that this css is attached to */
struct cgroup *cgroup;
/* PI: the cgroup subsystem that this css is attached to */
struct cgroup_subsys *ss;
/* reference count - access via css_[try]get() and css_put() */
struct percpu_ref refcnt;
/* siblings list anchored at the parent's ->children */
struct list_head sibling;
struct list_head children;
/*
* PI: the parent css.>-Placed here for cache proximity to following
* fields of the containing structure.
*/
struct cgroup_subsys_state *parent;
}
- refcnt 是该 cgroup_subsys_state 引用的次数,只有当这个参数为 0 时,该数据结构才能被释放;
- sibling children 和 parent 将同一层级的 cgroup 连接层一颗树;
cgroup 是指向 struct cgroup 的一个指针,也就是进程属于的 cgroup
。对于用户来说,即cgroups中的目录
。一个 struct cgroup 存储了一个目录的相关信息;- ss 是指向 struct cgroup_subsys 的一个指针,里面主要是一些钩子函数的定义,涉及 cgroups
用户态地一系列操作,对应的具体实现在对应的子系统中。
2、可配额/可度量 - Control Groups (cgroups)
cgroups实现了对资源的配额和度量
• blkio: 这个子系统设置限制每个块设备的输入输出控制。例如:磁盘,光盘以及 USB 等等。
• CPU: 这个子系统使用调度程序为 cgroup 任务提供 CPU 的访问。
• cpuacct: 产生 cgroup 任务的 CPU 资源报告。
• cpuset: 如果是多核心的 CPU,这个子系统会为 cgroup 任务分配单独的 CPU 和内存。
• devices: 允许或拒绝 cgroup 任务对设备的访问。
• freezer: 暂停和恢复 cgroup 任务。
• memory: 设置每个 cgroup 的内存限制以及产生内存资源报告。
• net_cls: 标记每个网络包以供 cgroup 方便使用。
• ns: 名称空间子系统。
• pid: 进程标识子系统。
3、几种常见子系统解释
(1)cpu子系统
- cpu.shares: 可出让的能获得 CPU 使用时间的相对值。
-
cpu.cfs_period_us:cfs_period_us 用来配置时间周期长度,单位为 us(微秒)。
-
cpu.cfs_quota_us:cfs_quota_us 用来配置当前 Cgroup 在 cfs_period_us 时间内最多能使用的
CPU 时间数,单 位为 us(微秒)。
-
cpu.stat : Cgroup 内的进程使用的 CPU 时间统计。
-
nr_periods : 经过 cpu.cfs_period_us 的时间周期数量。
-
nr_throttled : 在经过的周期内,有多少次因为进程在指定的时间周期内用光了配额时间而受到限制。
-
throttled_time : Cgroup 中的进程被限制使用 CPU 的总用时,单位是 ns(纳秒)。
Kubernetes中的cpu.shares、cpu.cfs_period_us、cpu.cfs_quota_us其实大有用处,详见深入理解 Kubernetes 资源限制:CPU
简要说下:cpu.shares是用来实现container request cpu, cpu.cfs_period_us、cpu.cfs_quota_us是用来实现container request cpu。
[1] container request cpu实现
那 它是怎么保证一定有50m cpu使用的呢? ,96c的服务器,其实就有96*1000个时间片,最多可以分这么多,也就是分母最多是96000。这样就保证了至少有50m的cpu可以使用,而且由于shares,在服务器空闲的时候还可以得到更多的资源。
[2] container limit cpu实现
# 1.限制只能使用1个CPU(每250ms能使用250ms的CPU时间)
$ echo 250000 > cpu.cfs_quota_us /* quota = 250ms */
$ echo 250000 > cpu.cfs_period_us /* period = 250ms */
# 2.限制使用2个CPU(内核)(每500ms能使用1000ms的CPU时间,即使用两个内核)
$ echo 1000000 > cpu.cfs_quota_us /* quota = 1000ms */
$ echo 500000 > cpu.cfs_period_us /* period = 500ms */
# 3.限制使用1个CPU的20%(每50ms能使用10ms的CPU时间,即使用一个CPU核心的20%)
$ echo 10000 > cpu.cfs_quota_us /* quota = 10ms */
$ echo 50000 > cpu.cfs_period_us /* period = 50ms */
你要是在一个周期想用超limit cpu,那么就会throttle限制,throttled_time就会增加
,而且必须等到下个周期你才能用。
(2)cpuacct 子系统
用于统计 Cgroup 及其子 Cgroup 下进程的 CPU 的使用情况。
- cpuacct.usage
包含该 Cgroup 及其子 Cgroup 下进程使用CPU 的时间
,单位是 ns(纳秒)。 - cpuacct.stat
包含该 Cgroup 及其子 Cgroup 下进程使用的CPU 时间,以及用户态和内核态的时间
。
(3)Memory 子系统
- memory.usage_in_bytes
cgroup 下进程使用的内存
,包含 cgroup及其子 cgroup
下的进程使用的内存 - memory.max_usage_in_bytes
cgroup 下进程使用内存的最大值
,包含子 cgroup 的内存使用量。 - memory.limit_in_bytes
设置 Cgroup 下进程最多能使用的内存
。如果设置为 -1,表示对该 cgroup 的内存使用不做限制。 - memory.soft_limit_in_bytes
这个限制并不会阻止进程使用超过限额的内存,只是在系统内存足够时,软限制会优先回收超过限额的内存,使之向限定值靠拢
。 - memory.oom_control
设置是否在 Cgroup 中使用 OOM(Out of Memory)Killer,默认为使用
。当属于该 cgroup 的进程使用的内存超过最大的限定值时,会立刻被 OOM Killer 处理。
4、Linux 进程调度器
内核默认提供了5个调度器,Linux 内核使用 struct sched_class 来对调度器进行抽象:
• Stop 调度器,stop_sched_class:优先级最高的调度类,可以抢占其他所有进程,不能被其他进程抢占;
• Deadline 调度器,dl_sched_class:使用红黑树,把进程按照绝对截止期限进行排序,选择最小进程进行调度运行;
• RT 调度器, rt_sched_class:实时调度器,为每个优先级维护一个队列;
• CFS 调度器, cfs_sched_class:完全公平调度器,采用完全公平调度算法,引入虚拟运行时间概念;
• IDLE-Task 调度器, idle_sched_class:空闲调度器,每个 CPU 都会有一个 idle 线程,当没有其他进程
可以调度时,调度运行 idle 线程。
(1)CFS 调度器(vruntime 红黑树)
CFS 调度器解决问题:此时那个进程应该对cpu进行持有,持有多久?
• CFS 是 Completely Fair Scheduler 简称,即完全公平调度器。
• CFS 实现的主要思想是维护为任务提供处理器时间方面的平衡,这意味着应给进程分配相当数量的处理器。
• 分给某个任务的时间失去平衡时,应给失去平衡的任务分配时间,让其执行。
• CFS 通过虚拟运行时间(vruntime)来实现平衡,维护提供给某个任务的时间量。
• vruntime = 实际运行时间*1024 / 进程权重
• 进程按照各自不同的速率在物理时钟节拍内前进,优先级高则权重大,其虚拟时钟比真实时钟跑得慢,但获得比较多的运行时间。
CFS 调度器没有将进程维护在运行队列中,而是维护了一个以虚拟运行时间为顺序的红黑树
。 红黑树的主要特点有:
1、自平衡
,树上没有一条路径会比其他路径长出俩倍。
2、O(log n) 时间复杂度
,能够在树上进行快速高效地插入或删除进程。
(2)CFS调度原理
• 在时钟周期开始时,调度器调用 __schedule() 函数来开始调度的运行。
• __schedule() 函数调用 pick_next_task() 让进程调度器从就绪队列中选择一个最合适的进程 next,即红黑树最左边的节点。
• 通过 context_switch() 切换到新的地址空间,从而保证 next 进程运行。
• 在时钟周期结束时,调度器调用 entity_tick() 函数来更新进程负载、进程状态以及 vruntime(当前vruntime + 该时钟周期内运行的时间)。
• 最后,将该进程的虚拟时间与就绪队列红黑树中最左边的调度实体的虚拟时间做比较,如果小于坐左边的时间,则不用触发调度,继续调度当前调度实体。
5、Cgroup driver
systemd:
- 当
操作系统使用 systemd 作为 init system
时,初始化进程生成一个根 cgroup 目录结构并作为cgroup管理器。 - systemd 与 cgroup 紧密结合,并且为每个 systemd unit 分配 cgroup。
cgroupfs:
docker 默认用 cgroupfs 作为 cgroup 驱动
。
存在问题:
- 在 systemd 作为 init system 的系统中,默认并存着两套 Cgroupdriver。
- Docker 默认采用 cgroupfs 驱动管理,而 systemd 拉起的kubelet服务由systemd驱动管理,让 cgroup 管理混乱且容易在资源紧张时引发问题。
因此 kubelet 会默认–cgroup-driver=systemd,若运行时 cgroup 不一致时,kubelet 会报错,解决方法:修改docker驱动为systemd
。
6、资源限制实操
操练须知:
1、docker
答:会创建一个/sys/fs/cgroup/cpu/docker,cpu子系统下有个docker控制组,下面有许多容器id目录,这样实现的资源限制。
2、kubernetes
答:会创建一个/sys/fs/cgroup/cpu/kubepods.slice, cpu子系统下有个kubepods控制组,此目录下又通过QOS对pod做了进一步拆分,创建了kubepods-besteffort.slice、kubepods-burstable.slice,这些目录下,才是区分各个pod的podid目录,podid目录下才是各个container的目录,各个容器的资源限制便是在container目录下做的。注意没有kubepods-guaranteed.slice目录,guaranteed qosclass pod会直接放在相应子系统的kubepods.slice目录下,如下图:3、cgroup组织方式
答:是以层级树的方式组织的,其中/sys/fs/cgroup是层级树的根节点,根节点下面是各个子系统,也是第一层级。每个子系统下一层级就是相应子系统的相关参数设置,其中k8s就是在这一层级创建了kubepods.slice目录,再下一层级就是kubepods-besteffort.slice…, 每一个层级下有很多对应子系统的相关参数,当然其实大部分参数都是没用的,只有最终的container cgroup组下的参数是实际被容器使用的,其他都是组织container的逻辑。
那么为什么会根据QOS对pod去做目录拆分?
答:这样应该是为了做服务质量保证如下图:
我们可以看到三种服务的oom-score
不一样,1000的在机器内存不足时,更容易被kill,0最不容易被kill。那么是根据什么去设置的这个值呢,我猜测应该是根据/proc/4133724/cgroup
这个文件内容来判断是那个qos,设置那个值。
- 在 cgroup cpu 子系统目录中创建目录结构
// 限制内存就进memory创建目录,其他操作都一样。
// 唯一区别是cpu是可压缩资源,内存是不可压缩。申请内存如果超出memory.limit_in_bytes,会相应进程被OOM KILL
cd /sys/fs/cgroup/cpu
mkdir cpudemo
cd cpudemo
- 运行 busyloop
- 执行 top 查看 CPU 使用情况,CPU 占用 200%
- 通过 cgroup 限制 cpu
cd /sys/fs/cgroup/cpu/cpudemo
- 把进程添加到 cgroup 进程配置组
echo ps -ef|grep busyloop|grep -v grep|awk '{print $2}' > cgroup.procs
- 设置 cpuquota
echo 10000 > cpu.cfs_quota_us
- 执行 top 查看 CPU 使用情况,CPU 占用变为10%
四、Union FS(联合文件系统)
- 将不同目录挂载到同一个虚拟文件系统下的文件系统
- 支持为每一个成员目录(类似Git Branch)设定 readonly、readwrite 和 whiteout-able 权限
- 文件系统分层, 对 readonly 权限的 branch 可以逻辑上进行修改(增量地, 不影响 readonly 部分的)。
- 通常 Union FS 有两个用途, 一方面可以将多个 disk 挂到同一个目录下, 另一个更常用的就是将一个readonly 的branch 和一个 writeable 的 branch 联合在一起。
构建文件系统还得从构建镜像说起:
如图所示,上面镜像构建出来,其实就是一个镜像文件系统,若运行成容器,后续再对image fs中的文件有修改,就会有cow机制到容器文件系统,image fs是read only,container fs却是read write.也即是readonly 的branch 和一个 writeable 的 branch 联合在一起
这句话的含义所在
1、Docker文件系统
典型的 Linux 文件系统组成:
- Bootfs(boot file system)
(1) Bootloader - 引导加载 kernel,
(2) Kernel - 当 kernel 被加载到内存中后 umount bootfs。
- rootfs (root file system)
(1) /dev,/proc,/bin,/etc 等标准目录和文件。
(2) 对于不同的 linux 发行版, bootfs 基本是一致的,但 rootfs 会有差别。
2、Docker启动中rootfs加载流程
docker中没有bootfs只有rootfs,容器比较轻量级,像虚拟机才有bootfs。
Docker 启动
- 初始化时也是将 rootfs 以 readonly (image fs)方式加载并检查,然而接下来利用 union mount 的方式将一个readwrite 文件系统(container fs)挂载在 readonly 的 rootfs 之上;
- 并且允许再次将下层的 FS(file system) 设定为 readonly 并且向上叠加。
- 这样一组 readonly 和一个 writeable 的结构构成一个 container 的运行时态, 每一个 FS 被称作一个FS层。
3、Docker中对容器的写操作
由于镜像具有共享特性,所以对容器可写层的操作需要依赖存储驱动提供的写时复制和用时分配机制,以此来支持对容器可写层的修改,进而提高对存储和内存资源的利用率。
(1)写时复制
- 写时复制,即 Copy-on-Write。
- 一个镜像可以被多个容器使用,但是不需要在内存和磁盘上做多个拷贝。
- 在需要对镜像提供的文件进行修改时,该文件会从镜像的文件系统被复制到容器的可写层的文件系统进行修改,而镜像里面的文件不会改变。
- 不同容器对文件的修改都相互独立、互不影响。
(2)用时分配
按需分配空间,而非提前分配,即当一个文件被创建出来后,才会分配空间
。
4、容器存储驱动
docker早期使用的是device mapper,现在都使用OverlayFS。
(1)OverlayFS简介
OverlayFS 也是一种与 AUFS 类似的联合文件系统
,同样属于文件级的存储驱动,包含了最初的 Overlay 和更新更稳定的 overlay2。
Overlay 只有两层:upper 层
和 lower 层
,Lower 层代表镜像层
,upper 层代表容器可写层
。
(2)Overlay FS实战
$ mkdir upper lower merged work
$ echo "from lower" > lower/in_lower.txt
$ echo "from upper" > upper/in_upper.txt
$ echo "from lower" > lower/in_both.txt
$ echo "from upper" > upper/in_both.txt
// 指定lowerdir、upperdir、workdir以overlay fs方式挂载到`pwd`/merged目录下
// 这个目录其实就是文件mount结果,即mergeddir(合并层)
// 把lowerdir看成镜像层,upperdir看成容器层,合并层即是容器终端展现的效果。
$ sudo mount -t overlay overlay -o lowerdir=`pwd`/lower,upperdir=`pwd`/upper,workdir=`pwd`/work `pwd`/merged
$ cat merged/in_both.txt
$ delete merged/in_both.txt
$ delete merged/in_lower.txt
$ delete merged/in_upper.txt
docker inspect 容器id
五、网络
1、Docker中四种网络模式(同一主机容器网络互联)
上面网络只能支持同一主机上各个容器的网络互通。
上面两个是支持不同主机的网络互通的。
(1)网桥详解
在自己的主机上面有eth0网卡,上面有主机的ip,docker本身会启动docker0的bridge,默认以172,17.0.1作为网关地址配置在bridge上面,启动任意一个容器docker会默认的从这个IP段选一个ip给到这个容器,同时创建虚拟链路veth pair,一端插在docker0上面,一端插在容器网络namespace里面,并且配给里面的eth0,bridge设备可以将上面所有的网口做连接的容器都互通起来,所以所有的容器都是互通的。
如果要将容器内的服务发布到主机外面,通过-p来将主机的端口映射到容器内部,最底层的实现逻辑是通过主机上面的iptable来做一个端口转发。
(2)Null模式详解
此模式需要自己给容器配置网络:
- Null 模式是一个空实现;
- 可以通过 Null 模式启动容器并在宿主机上通过命令为容器配置网络。
mkdir -p /var/run/netns
find -L /var/run/netns -type l -delete
ln -s /proc/$pid/ns/net /var/run/netns/$pid
ip link add A type veth peer name B
brctl addif br0 A
ip link set A up
ip link set B netns $pid
ip netns exec $pid ip link set dev B name eth0
ip netns exec $pid ip link set eth0 up
ip netns exec $pid ip addr add
$SETIP/$SETMASK dev eth0
ip netns exec $pid ip route add default via
$GATEWAY
2、两种网络模式(不同主机容器网络互联)
(1)underlay(物理IT基础设施网络)
- 采用 Linux 网桥设备(sbrctl),通过物理网络连通容器;
- 创建新的网桥设备 mydr0;
- 将主机网卡加入网桥;
- 把主机网卡的地址配置到网桥,并把默认路由规则转移到网桥 mydr0;
- 启动容器;
- 创建 veth 对,并且把一个 peer 添加到网桥 mydr0;
- 配置容器把 veth 的另一个 peer 分配给容器网卡;
(2)overlay覆盖网
- Docker overlay 网络驱动原生支持多主机网络;
- Libnetwork 是一个内置的基于 VXLAN 的网络驱动。
Flannel覆盖网:
Flannel packet分析:
六、Dockerfile
1、回顾12 Factor之进程
- 运行环境中,应用程序通常是以一个和多个进程运行的。
- 12-Factor 应用的进程必须无状态(Stateless)且无共享(Share nothing)。
- 任何需要持久化的数据都要存储在后端服务内,比如数据库。
- 应在构建阶段将源代码编译成待执行应用。
- Session Sticky 是 12-Factor 极力反对的。
- Session 中的数据应该保存在诸如 Memcached 或 Redis 这样的带有过期时间的缓存中。
Docker 遵循以上原则管理和构建应用。
2、理解构建上下文
- 当运行 docker build 命令时,当前工作目录被称为构建上下文。
- docker build 默认查找当前目录的 Dockerfile 作为构建输入,也可以通过 –f 指定 Dockerfile。
docker build –f ./Dockerfile
- 当 docker build 运行时,首先会把构建上下文传输给 docker daemon,把没用的文件包含在构建上下文时,会导致传输时间长,构建需要的资源多,构建出的镜像大等问题。
试着到一个包含文件很多的目录运行下面的命令,会感受到差异;
docker build -f $GOPATH/src/github.com/cncamp/golang/httpserver/Dockerfile ;
docker build $GOPATH/src/github.com/cncamp/golang/httpserver/;
可以通过.dockerignore文件从编译上下文排除某些文件。
因此需要确保构建上下文清晰,比如创建一个专门的目录放置 Dockerfile,并在目录中运行 docker build。
3、Build Cache
docker build $GOPATH/src/github.com/cncamp/golang/httpserver/
Sending build context to Docker daemon 14.57MB
Step 1/4 : FROM ubuntu
---> cf0f3ca922e0
Step 2/4 : ENV MY_SERVICE_PORT=80
---> Using cache
---> a7d824f74410
Step 3/4 : ADD bin/amd64/httpserver /httpserver
---> Using cache
---> 00bb47fce704
Step 4/4 : ENTRYPOINT /httpserver
---> Using cache
---> f77ee3366d08
Successfully built f77ee3366d08
上面是镜像构建日志。
构建容器镜像时,Docker 依次读取 Dockerfile 中的指令,并按顺序依次执行构建指令。
Docker 读取指令后,会先判断缓存中是否有可用的已存镜像,只有已存镜像不存在时才会重新构建。
- 通常 Docker 简单判断 Dockerfile 中的指令与镜像。
- 针对 ADD 和 COPY 指令,Docker 判断该镜像层每一个文件的内容并生成一个 checksum,与现存镜像比较时,
Docker 比较的是文件的 checksum
。 - 其他指令,比如 RUN apt-get -y update,Docker 简单比较与现存镜像中的
指令字串是否一致
。 - 当某一层 cache 失效以后,后面所有层级的 cache 均一并失效,后续指令都重新构建镜像。
4、多段构建(Multi-stage build)
有效减少镜像层级的方式之一:
FROM golang:1.16-alpine AS build
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
RUN dep ensure -vendor-only
COPY . /go/src/project/
RUN go build -o /bin/project(只有这个二进制文件是产线需要的,其他都是waste)
# 生产镜像构建从现在才开始,上面build镜像只是为了生产环境镜像做铺垫,生成二进制文件
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"] # 使用上面得到的二进制文件直接启动服务
CMD ["--help"]
5、Dockerfile常见指令
- FROM:选择基础镜像,推荐 alpine
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
- LABELS:按标签组织项目
LABEL multi.label1="value1" multi.label2="value2" other="value3”
- 配合 label filter 可过滤镜像查询结果
docker images -f label=multi.label1="value1"
- RUN:执行命令
最常见的用法是 RUN apt-get update && apt-get install,这两条命令应该永远用&&连接,如果分开执行,RUN apt-get update 构建层被缓存,可能会导致新 package 无法安装 - COPY:从源地址(文件,目录或者URL)复制文件到目标路径
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"] // 路径中有空格时使用
COPY 的使用与 ADD 类似,但有如下区别
COPY 只支持本地文件的复制,不支持 URL
COPY 不解压文件
COPY 可以用于多阶段编译场景,可以用前一个临时镜像中拷贝文件
COPY --from=build /bin/project /bin/project
COPY 语义上更直白,复制本地文件时,优先使用 COPY
- VOLUME: 将指定目录定义为外挂存储卷,Dockerfile 中在该指令之后所有对同一目录的修改都无效
VOLUME ["/data"]
等价于 docker run –v /data,可通过 docker inspect 查看主机的 mount point,
/var/lib/docker/volumes/<containerid>/_data
- USER:切换运行镜像的用户和用户组,因安全性要求,越来越多的场景要求容器应用要以 non-root 身份运行
USER <user>[:<group>]
6、Dockerfile 最佳实践
- 不要安装无效软件包。安装软件包也应该清理缓存
- 最小化层级数
【1】最新的 docker 只有 RUN, COPY,ADD 创建新层,其他指令创建临时层,不会增加镜像大小。多条 RUN 命令可通过连接符连接成一条指令集以减少层数。
【2】通过多段构建减少镜像层数。
- 编写 dockerfile 的时候,应该把变更频率低的编译指令优先构建以便放在镜像底层以有效利用 build cache。
- 复制文件时,每个文件应独立复制,这确保某个文件变更时,只影响该文件对应的缓存。
Dockerfile编写目标:易管理、少漏洞、镜像小、层级少、利用缓存。
7、多进程的容器镜像
选择适当的 init 进程
【1】需要捕获 SIGTERM 信号并完成子进程的优雅终止
【2】负责清理退出的子进程以避免僵尸进程
开源项目: https://github.com/krallin/tini
8、Docker镜像与github版本管理
以 Kubernetes 为例
- 开发分支
git checkout master
- Release 分支
git checkout –b release-1.21
- 在并行期,所有的变更同时放进 master 和 release branch
- 版本发布
以 release branch 为基础构建镜像,并为镜像标记版本信息:docker tag
0e5574283393 k8s.io/kubernetes/apiserver:v1.21
- 在 github 中保存 release 代码快照
git tag v1.21
核心点
:github代码版本与docker tag版本一致便于维护