> For the complete documentation index, see [llms.txt](https://nixum.gitbook.io/note/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://nixum.gitbook.io/note/rong-qi.md).

# 容器

容器底层原理以及docker相关

\[TOC]

## 底层原理

容器技术的核心功能，就是通过约束和修改进程的动态表现，从而为其创造出一个边界。

### Namespace - 隔离

进程只能看到被规定的视图，即 隔离，比如通过docker启动一个/bin/sh，再在容器里通过ps命令查看该/bin/sh进程的pid，会发现它的pid是1，但是实际上它在外部的宿主机里的pid是10，使得让在容器里运行的进程以为自己就在一个独立的空间里，实际上只是进行了逻辑的划分，本质还是依赖宿主机。

作用：在同一台宿主机上运行多个用户的容器，充分利用系统资源；不同用户之间不能访问对方的资源，保证安全。

常见的Namespace类型有：

* PID Namespace：隔离不同容器的进程
* Network Namespace：隔离不同容器间的网络
* Mount Namespace：隔离不同容器间的文件系统

**与虚拟化的区别**：虚拟化是在操作系统和硬件上进行隔离，虚拟机上的应用需要经过虚拟机再经过宿主机，有两个内核，本身就有消耗，而容器化后的应用仅仅只是宿主机上的进程而已，只用到宿主机一个内核；

因为namespace隔离的并不彻底，由于内核共享，容器化应用仍然可以把宿主机的所有资源都吃掉，有些资源不能通过namespace隔离，比如修改了容器上的时间，宿主机上的时间也会被改变，因此需要Cgroups；

### Cgroups - 资源限制

是用来制造约束的主要手段，即控制进程组的优先级，设置进程能够使用的资源上限，如CPU、内存、IO设备的流量等

比如，限定容器只能使用宿主机20%的CPU

```shell
docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash
```

> Cgroups 通过不同的子系统限制了不同的资源，每个子系统限制一种资源。每个子系统限制资源的方式都是类似的，就是把相关的一组进程分配到一个控制组里，然后通过树结构进行管理，每个控制组都设有自己的资源控制参数。

**相互关系**：

每个子系统是一个控制组，每个控制组可以被看作是一个树的节点，每个控制组下可以有多个子节点，比如我们在在CPU子系统中，创建一个DB控制组，然后把所有运行的数据库服务放在其中，然后再在该组下再创建 MySQL和MongoDB两个子组来，分别划分不同的使用资源，所以形成了一颗树，大致就如下图

![](https://github.com/Nixum/Java-Note/raw/master/picture/Cgroup%E7%9B%B8%E4%BA%92%E5%85%B3%E7%B3%BB.png)

`/sys/fs/cgroup/cpu/{task}/`目录表示task这个任务挂在了CPU Cgroup下，在这个目录下有很多的配置文件，比如`cpu.cfd_quota_us、cgroup.procs`等，文件内容是该task所属进程的PID；

`/proc/{PID号}/cgroup`文件表示这个进程涉及到的所有cgroup子系统的信息

常见的Cgroups子系统

* CPU 子系统，用来限制一个控制组（一组进程，你可以理解为一个容器里所有的进程）可使用的最大 CPU，配合cfs（完全公平调度算法）实现CPU的分配和管理。

  cpu share：用于cfs中调度的权重，条件相同的情况下，cpushare值越高，分得的时间片越多。

  cpu set：主要用于设置CPU的亲和性，可以限制cgroup中的进程只能在指定的CPU上运行，或者不能在指定的CPU上运行，同时cpuset还能设置内存的亲和性。
* memory 子系统，用来限制一个控制组最大的内存使用量。
* pids 子系统，用来限制一个控制组里最多可以运行多少个进程。
* cpuset 子系统， 这个子系统来限制一个控制组里的进程可以在哪几个物理 CPU 上运行。

Cgroups 有 v1 和 v2 两个版本，v1中每个进程在各个Cgroups子系统中独立配置，可以属于不同的group，比较灵活但因为每个子系统都是独立的，会导致对同一进程的资源协调困难，比如同一容器配置了Memory Cgroup和Blkio Cgroup，但是它们间无法相互协作。

v2针对此做了改进，使各个子系统可以协调统一管理资源。

### Mount Namespace与rootfs(根文件系统)

挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统，即容器镜像，也是容器的根文件系统。Mount Namespace保证每个容器都有自己独立的文件目录结构。

镜像可以理解为是容器的文件系统（一个操作系统的所有文件和目录），它是只读的，挂载在宿主机的一个目录上。同一台机器上的所有容器，都共享宿主机操作系统的内核，如果容器内应用修改了内核参数，会影响到所有依赖的应用。而虚拟机则都是独立的内核和文件系统，共享宿主机的硬件资源。

> 上面的读写层通常也称为容器层，下面的只读层称为镜像层，所有的增删查改操作都只会作用在容器层，相同的文件上层会覆盖掉下层。比如修改一个文件的时候，首先会从上到下查找有没有这个文件，找到，就复制到容器层中，进行修改，修改的结果就会作用到下层的文件，这种方式也被称为copy-on-write。

### 注意点

容器是“单进程模型”，单进程模型并不是指容器只能运行一个进程，而是指容器没有管理多个进程的能力，它只能管理一个进程，即如果在容器里启动了一个Web 应用和一个nginx，如果nginx挂了，你是不知道的。

另外，直到JDK 8u131以后，java应用才能很好的运用在docker中，在此之前可能因为docker隔离出的配置和环境，导致JVM初始化默认数值出错，因此如果使用以前的版本，需要显示设置默认配置，比如直接规定堆的最大值和最小值、线程数之类的

## 进程

Linux中的进程状态

![](https://github.com/Nixum/Java-Note/raw/master/picture/Linux%E8%BF%9B%E7%A8%8B%E7%8A%B6%E6%80%81.jpeg)

活着的进程有两种状态：

* 运行态(TASK\_RUNNING)：进程正在运行，或 处于run queue队列里等待
* 睡眠态(TASK\_INTERRUPTIBLE、TASK\_UNINTERRUPTIBLE)：因为需要等待某些资源而被放在了wait queue队列，该状态包括两个子状态：
  * 可被打断状态(TASK\_INTERRUPTIBLE)：此时ps查看的Stat的值为 S
  * 不可被打断状态(TASK\_UNINTERRUPTIBLE)：此时ps查看的Stat的值为 D

进程退出时会有两个状态：

* EXIT\_ZOMBIE状态：僵尸状态；之所以有这个状态是为了给父进程可以查看子进程PID、终止状态、资源使用信息的机会，如果子进程直接消失，父进程则没有机会掌握子进程具体的终止情况。
* EXIT\_DEAD状态：真正结束退出时一瞬间的状态

### init进程

init进程也称1号进程，是第一个用户态进程，由它直接或间接创建了Namespace中的其他进程。Linux系统本身在启动后也是这么干的，会先执行内核态代码，然后根据缺省路径尝试执行1号进程的代码，从内核态切换到用户态，比如Systemd。

在容器中，无法使用SIGKILL(-9)和SIGTOP(19)这两个信号杀死1号进程，但对于其他kill信号(比如默认的kill信号是SIGTERM)，如果init进程注册了自己处理该信号的handler，则1号进程可以做出响应。但是，如果SIGKILL和SIGTOP信号是从Host Namespace里发出的，则可以被响应，因为此时容器里的1号进程在宿主机上只是一个普通进程。

**有时无法被kill掉的原因：**

Linux执行kill命令，实际上只是发送了一个信号给到Linux进程，**可被调度的进程**在收到信号后，一般会从 \*\*默认行为(每个信号都有)、忽略、捕获后处理（需要用户进程自己针对这个信号做handler）\*\*中进行操作，但SIGKILL和SIGTOP这两个信号是特权信号，不允许被自行捕获处理也无法忽略，只能执行系统的默认行为。

执行kill命令时，会调用一系列内核函数进行处理，其中有一个函数`sig_task_ignored`会判断是否要忽略这个信号。Linux内核里的每个Namespace里的init进程，会忽略只有默认handler的信号，即如果我们的进程有处理相关信号的handler，就可以响应。可以通过`cat /proc/{进程pid}/status | grep -i SigCgt`查看该进程注册了哪些handler。

### ZOMBIE进程

`ps aux`查看Linux进程，STAT里的状态是Z，ps后有defunct标记，表示该进程为**僵尸进程，此时该进程不可被调度。僵尸进程是Linux进程退出状态的一种，进程处于此状态下，无法响应kill命令，虽然资源被释放，但是仍然占用着进程号**。

**形成的原因：**

父进程在创建完子进程后，没有对子进程进行后续的管理。

**影响：**

Linux内核在初始化系统时，会根据CPU数目限制进程最大数量，通过`/proc/sys/kernel/pid_max`查看，对Linux系统而言，容器是一组进程的集合，如果容器中的应用创建过多，就会出现fork bomb行为，不断建立新进程消耗系统资源，如果达到了Linux最大进程数，导致系统不可用。

**解决方案：**

因此对于容器，也要限制容器内的进程数量，通过pids Cgroup来完成，限制进程数目大小。在一个容器建立后，创建容器的服务会在`/sys/fs/cgroup/pids`下建立一个子目录作为控制组，里面的pids.max文件表示容器允许的最大进程数目。当容器内进程达到最大限制后再起新进程，会报错`Resource temporarily unavailable`

当出现僵尸进程后，父进程可以调用wait函数（该函数是同步阻塞）或者调用waitpid函数（该函数仅在调用时检查僵尸进程，如果没有则返回，不会阻塞等待）回收子进程资源，避免僵尸进程 的产生。或者kill掉僵尸进程的父进程，此时僵尸进程会归附到init进程下，利用init进程的能力回收僵尸进程的资源。

init进程是所有进程的父进程，init进程具备回收僵尸进程的能力。

或者把容器内的init进程替换为tini进程，该进程具备自动回收收子进程的能力。

### 进程的退出

当我们停止一个容器时，比如`docker stop`，容器内的init进程会收到SIGTERM信号，而其他进程会收到SIGKILL信号，这意味着只有init进程才能注册handler处理信号实现graceful shotdown，而其他进程不行，直接就退出了。

如果想要容器内的其他进程能收到SIGTERM信号，只能在init进程中注册一个Handler，将收到的信号转发到子进程中，在init进程退出之前把子进程都停掉，子进程就不会收到SIGKILL信号了。

## CPU

![](https://github.com/Nixum/Java-Note/raw/master/picture/Linux_CPU%E4%BD%BF%E7%94%A8%E5%88%86%E7%B1%BB.jpeg)

### CPU Cgroup

容器会使用CPU Cgroup来控制CPU的资源使用。CPU Cgroup只会对用户态us和ni、内核态sys做限制，不对wa、hi、si这些I/O或者中断相关做限制。

CPU Cgroup一般会通过一个虚拟文件系统挂载点的方式，挂载在`/sys/fs/cgroup/cpu`目录下，每个子目录为一个控制组，各个目录间是一个树状的层级关系。

对于普通调度类型，每个目录下有三个文件对应三个参数：

* `cpu.cfs_period_us`：CFS的调度周期，单位微秒
* `cpu.cfs_quota_us`：一个调度周期内该控制组被允许运行的时间，单位微秒，`CPU最大配额 = cpu.cfs_quota_us / cpu.cfs_period_us`
* `cpu.shares`：CPU Cgroup对控制组之间的CPU分配比例，只有当控制组间的CPU配额超过了CPU可以资源的最大值，则会启用该参数进行配额分配。

Linux中 `cpu.cfs_period_us` 是个固定值，Kubernetes的pod中，限制容器CPU使用率的requestCPU和limitCPU是通过调整其余两个参数来实现。

**在容器内使用top命令查看CPU使用率，显示的是宿主机的CPU使用率以及单个进程的CPU使用率，无法查到该容器的CPU使用率**。

因为top命令对于单个进程读取`/proc/[pid]/stat`里面包含进程用户态和内核态的ticks数目，对于整个节点读取的是`/proc/stat`里各个不同CPU类型的ticks数目，因为这些文件不属于任何一个Namespace，因此无法读取单个容器CPU的使用率，只能通过CPU Cgroup的控制组内的`cpuacct.stat`参数文件计算得到，该参数文件包含了这个控制组里所有进程的内核态ticks和用户态ticks的值，带入公式即可计算得到。

> ticks是Linux操作系统中的一个时间单位，Linux通过自己的时钟周期性产生中断，每次中断会触发内核做一次进程调度，一次中断就是一个ticks
>
> utime：表示进程在用户态部分在Linux调度中获得CPU的ticks，这个值会一直累加
>
> stime：表示进程在内核态部分在Linux调度中获得CPU的ticks，这个值会一直累加
>
> HZ：时钟频率
>
> et：utime\_1和utime\_2这两个值的时间间隔
>
> 进程的 CPU 使用率 =((utime\_2 – utime\_1) + (stime\_2 – stime\_1)) \* 100.0 / (HZ \* et \* CPU个数 )

### Load Average平均负载

Load Average是指**Linux进程调度器中一段时间内，可运行队列里的进程平均数 + 休眠队列中不可打断的进程平均数**。

所以，有可能在使用top命令时观察到 明明CPU空闲率很高，但是Load Average的数值也很高，CPU性能下降，因为此时休眠队列中有很多在等待的进程，这些进程的stat是D，这种状态可能是IO或者信号量锁的访问导致。

## 内存

**Linux进程的内存申请策略**：Linux允许进程在申请内存时overcommit，即允许进程申请超过实际物理内存上限的内存。因为申请只是申请内存的虚拟地址，只是一个地址范围，只有真正写入数据时，才能得到真实的物理内存，当物理内存不够时，Linux就会根据一定的策略杀死某个正在运行的进程，当容器内存使用率超过限制值时，容器就会出现OOM Killed。

OOM Killed的标准是通过`oom_badness函数`决定，通过以下两个值的乘积决定：

* 进程已经使用的物理内存页面数
* 每个进程的OOM校准值oom\_score\_adj，在`/proc/[pid]/oom_score_adj`文件中，该值用于调整进程被OOM Kill的几率

**Linux的内存类型**：有两类，一类是内核使用的内存，如页表、内核栈、slab等各种cache pool（匿名页）；另一类是用户态使用的内存，如堆内存、栈内存、共享库内存、文件读写的Page Cache（文件页）。

**RSS（Resident Set Size）**：进程真正申请到的物理页面的内存大小，RSS内存包括了进程的代码段内存，堆内存、栈内存、共享库内存，每一部分RSS内存的大小可查看`/proc/[pid]/smaps`

**Page Cache**：为了提高磁盘文件的读写性能，Linux在有空闲的内存时，默认会把读写过的页面放在Page Cache里，一旦进程需要更多的物理内存，但剩余的内存不够，则会使用(page frame reclaim)这种内存页面回收机制，根据系统里空闲物理内存是否低于某个阈值，决定是否回收Page Cache占用的内存。

### Memory Cgroup

Memory Cgroup一般会通过一个虚拟文件系统挂载点的方式，挂载在`/sys/fs/cgroup/memory`目录下，每个子目录为一个控制组，各个目录间是一个树状的层级关系。每个目录有很多参数文件，跟OOM相关的有3个

* `memory.limit_in_bytes`：控制控制组里进程可使用的内存最大值，父节点的控制组内该值可以限制它的子节点所有进程的内存使用，Kubernetes的limit改的就是该值，而request仅在调度时计算节点是否满足。
* `memory.oom_control`：当控制组中的进程内存使用达到上限时，该参数决定是否触发OOM Killed，默认是会触发OOM Killed，只能杀死控制组内的进程，无法杀死节点上的其他进程。当值设置为1时，表示控制组内进程即使达到limit\_in\_bytes设置的值，也不会触发OOM Killed，但是这可能会影响控制组中正在申请物理内存页面的进程，造成该进程处于停止状态，无法继续运行。
* `memory.usage_in_bytes`：表示当前控制组里所有进程实际使用的内存总和，与limit\_in\_bytes的值越接近，越容易触发OOM Killed。

当发送OOM killed时，可以查看内核日志 `journalctl -k 或者 查看日志文件 /var/log/message`

Memory Cgroup只统计了RSS和Page Cache这两部分内存，两者的和等于 `memory.usage_in_bytes`，当控制组里的进程要申请新的物理内存，但`memory.usage_in_bytes`已超过`memory.limit_in_bytes`，此时会启用`page frame reclaim内存页面`回收机制，回收Page Cache的内存。

判断容器真实的内存使用量，不能仅靠Memory Cgroup里的`memory.usage_in_bytes`，而需要`memory.stat`里的rss值，该值才真正反应了容器使用的真实物理内存的大小。

### Swap

容器可以使用Swap空间，但会导致Memory Cgroup失效，比如在Swap空间足够的情况下，设置容器limit为512MB，然后容器申请了1G内存，是可以申请成功的，此时容器的RSS为512MB，Swap只剩下512MB空闲。

为了解决这个问题，可以使用swapiness，参数文件在`/proc/sys/vm/swapiness`，通过它来设置Swap使用的权重，用于定义Page Cache内存和匿名内存释放的比例值，取值范围是0到100，默认是60。100表示匿名内存和Page Cache内存的释放比例是100：100；60表示匿名内存和PageCache内存的释放比例是60：140，此时PageCache内存的释放优先于匿名内存；0不会完全禁止Swap的使用，在内存特别紧张的时候才会启用Swap来回收匿名内存。

swapiness也存在于Memory Cgroup控制组中，参数文件是`memory.swappiness`，该值的优先级会大于全局的swappiness，但有一点跟全局的swappiness不太一样，当`memory.swappiness=0`时，是完全不使用Swap空间的。

通过`memory.swappiness`参数可以使得需要使用Swap容器和不需要使用Swap的容器，同时运行在同一个节点上。

## 存储

Linux两种文件 I / O模式：

Direct I/O：用户进程写磁盘文件，通过Linux内核文件系统 -> 块设备层 -> 磁盘驱动 -> 磁盘硬件，直到落盘。

Buffer I/O：先把文件数据写入内存就返回，Linux内核有现成会把内存中的数据再写入磁盘，性能更佳。

当文件数据先写入内存时，存在内存的数据叫 dirty pages

> 当 dirty pages 数量超过 dirty\_background\_ratio（百分比，默认是10%） 对应的内存量的时候，内核 flush 线程就会开始把 dirty pages 写入磁盘 ;
>
> 当 dirty pages 数量超过 dirty\_ratio （百分比，默认是20%）对应的内存量，这时候程序写文件的函数调用 write() 就会被阻塞住，直到这次调用的 dirty pages 全部写入到磁盘；
>
> dirty\_writeback\_centisecs，时间值，单位是百分之一秒，默认是5秒，即每5s会唤醒内核的flush线程来处理dirty pages；
>
> dirty\_expire\_centisecs，时间值，单位是百分之一秒，默认是30s，定义dirty pages在内存的存放的最长时间，如果超过该时间，就会唤醒内核的flush线程处理dirty pages；

### UnionFS

**将多个目录(处于不同的分区)一起挂载在一个目录下，实现多目录挂载**。这种方式可以使得同一节点上，多个容器使用同一份基础镜像，减少磁盘冗余的镜像数据。OverlayFS是其中的一种实现。

比如容器A和容器B都使用了ubuntu作为基础镜像，放在目录/ubuntu上，容器A使用的额外应用程序放在appA目录，容器B的放在appB目录，将/ubuntu目录和appA目录同时挂载在containA目录下，作为容器A看到的文件系统，同理容器B也是，此时节点就只需要保存一份ubuntu镜像文件即可。

### OverlayFS

OverlayFS是一种堆叠文件系统，依赖并建立在其他文件系统之上，如EXT4FS、XFS等，并不直接参与磁盘空间结构的划分，仅将原来底层文件系统中不同目录进行合并，用户见到的Overlay文件系统根目录下的内容就来自挂载时指定的不同目录的合集，OverlayFS会将挂载的文件进行分层。

* lower：文件的最底层，不允许被修改，只读，OverlayFS支持多个lowerdir，最多500个；
* upper：可读写层，文件的创建、修改、删除都会在这一层反应，只有一个；
* merged：挂载点目录，用户实际对文件的操作都在这里进行；
* work：存放临时文件的目录，如果OverlayFS中有文件修改，中间过程中会临时存放文件到这里；

上下层同名目录合并，上下层同名文件覆盖，lower层文件写时会拷贝(copy\_up)到upper层进行修改，不影响lower层的文件。

比如，在merged目录里新建文件，这个文件会出现在upper目录中；删除文件，这个文件会在upper目录中消失，但在lower目录里不会有变化，只是upper目录中会增加一个特殊文件告诉OverlayFS该文件不能出现在merged目录里，表示它被删除；修改文件，会在upper目录中新建一个修改后的文件，而lower目录中原来的文件不会改变。

在Docker中，容器镜像文件可以分成多个层，每层对应lower dir的一个目录，容器启动后，对镜像文件中的修改会保存在upper dir里。Docker会将镜像层作为lower dir，容器层作为upper dir，最后挂载到容器的merged挂载点，即容器的根目录下。

OverlayFS本身没有限制文件写入量的功能，需要依赖底层文件系统，比如XFS文件系统的quota，限制upper目录的写入大小。

### Blkio Cgroup

IOPS：每秒钟磁盘读写的次数

吞吐量：每秒钟磁盘读取的数据量，有时也称为带宽

两者的关系：吞吐量 = 数据块大小 \* IOPS

Blkio Cgroup一般会通过一个虚拟文件系统挂载点的方式，挂载在`/sys/fs/cgroup/blkio`目录下，每个子目录为一个控制组，各个目录间是一个树状的层级关系。每个目录有很多参数文件，有4个主要参数来限制磁盘 I/O性能。

* `blkio.throttle.read_iops_device`：磁盘读取IOPS限制
* `blkio.throttle.read_bps_device`：磁盘读取吞吐量限制
* `blkio.throttle.write_iops_device`：磁盘写入IOPS限制
* `blkio.throttle.write_bps_device`：磁盘写入吞吐量限制

但在Cgroup V1，只有Direct I/O才能通过Blkio Cgroup限制，Buffered I/O不能，因为Buffered I/O会用到Page Cache，但V1版本各个Cgroup子系统相互独立，所以没办法做限制。

Cgroup V2才可以，通过配置Blkio Cgroup和Memory Cgroup即可解决。有个问题是，如果Memory Cgroup的`memory.limit_in_bytes`设置得比较小，而容器中进程有大量的IO、这样申请新的Page Cache内存时，又会不断释放老的内存页面，带来了额外的开销，可能会产生写入波动。

## 网络

容器网络一个Network Namespace网络栈包括：网卡、回环设备、路由表、iptables规则。

### Network Namespace

Network Namespace主要用来隔离网络资源，如

* 网络设备，比如 lo(回环设备)、eth0等，可以通过`ip link`命令查看
* IPv4和IPv6协议栈，IP层以及上面的TCP和UDP协议栈都是每个Namespace独立工作，这些参数大都在`/proc/sys/net`目录下，包括TCP和UDP的port资源。
* IP路由表，也是每个Namespace独立工作，使用`ip route`命令查看
* 防火墙规则，即iptables，每个Namespace可独立配置
* 网络状态信息，可以从`/proc/net`和`/sys/class/net`里查看，包括了上面几种资源的状态信息

在宿主机上，可以使用`lsns -t net`命令查看系统已有的Network Namespace，使用`nsenter或ip netns`这个命令进入某个Network Namespace里查看具体的网络配置。

网络相关参数很大一部分放在了`/proc/sys/net`目录下，比如tcp相关的参数，可以直接修改，也可以使用sysctl命令修改。

出于安全考虑，对于非privileged容器，`/proc/sys`是read-only挂载的，容器启动后无法内部修改该目录下相关的网络参数，只能通过runC sysctl相关接口，在容器启动时对容器内的网络参数进行修改，比如

如果使用Docker，可以加上 `--sysctl 参数名=参数值`来修改，如果是K8s，则需要用到 `allowed unsaft sysctl`这个特性了。

可以使用系统函数clone()或unshare()来创建Namespace，比如Docker或者containerd启动容器时，是通过runC间接调用unshare函数启动容器的。

### 网络通信

容器与外界通信，总共分成两步：

1. 数据包从容器的Network Namespace发送到Host Network Namespace
2. 数据包从Host Network Namespace，从宿主机的eth0上发送出去

可以使用 tcpdump抓包工具 查看数据包在各个设备接口的日志

比如查看容器内eth0接收数据包的情况 `ip netns exec [pid] tcpdump -i eth0 host [目标ip地址] -nn`，查看veth\_host或其他设备使用 `tcpdump -i [设备名如veth_host、docker0、eth0] host [目标ip地址] -nn`

#### 同一节点下容器间的通信

在 Linux 中能够起到虚拟交换机作用的网络设备，是网桥。它是一个工作在数据链路层的设备，主要功能是根据 MAC 地址来将数据包转发到网桥的不同端口（Port）上，因此Docker 项目会默认在宿主机上创建一个名叫 docker0 的网桥，凡是连接在 docker0 网桥上的容器，就可以通过它来进行通信。

容器里会有一个eth0网卡，作为默认的路由设备，连接到宿主机上一个叫vethxxx的虚拟网卡，而vethxxx网卡又插在了docker0网桥上，这一套虚拟设备就叫做Veth Pair。每个容器对应一套VethPair设备，多个容器会将其Veth Pair注册到宿主机的docker0网桥上，即**Veth Pair相当于是连接不同Network Namespace的网线，一端在容器，一端在宿主机**。此时，数据包就能从容器的Network Namespace发送到Host Network Namespace上了。

docker0和容器会组成一个子网，docker0上的ip就是这个子网的网关ip。

网络请求实际上就是在这些虚拟设备上进行映射（经过路由表，IP转MAC，MAC转IP）和转发，到达目的地。

**同一节点内，容器间通信一般流程**：容器A往容器B的IP发出请求，请求先经过容器A的eth0网卡，发送一个ARP广播，找到容器B IP对应的MAC地址，宿主机上的docker0网桥，把广播转发到注册到其身上的其他容器的eth0，容器B收到该广播后把MAC地址发给docker0，docker0回传给容器A，容器A发送数据包给docker0，docker0接收到数据包后，根据数据包的目的MAC地址，将其转发到容器B的eth0，

**同一节点内，宿主机与容器通信一般流程**：宿主机往容器的IP发出请求，这个请求的数据包先根据路由规则到达docker0网桥，转发到对应的Veth Pair设备上，由Veth Pair转发给容器内的应用。

关于容器缺省使用的peer veth方案，由于从容器的veth0到宿主机的veth0会有一次软中断，带来了额外的开销，时延会高一些，可以使用 ipvlan/macvlan的网络接口替代，ipvlan/macvlan 直接在物理网络接口上虚拟出接口，容器发送网络数据包时直接从容器的eth0发送给宿主机的eth0，减少了转发次数，实延就降低了。但是该方案无法使用iptables规则，Kubernetes的service就无法使用该方案。

![](https://github.com/Nixum/Java-Note/raw/master/picture/%E5%90%8C%E4%B8%80%E8%8A%82%E7%82%B9%E4%B8%8B%E7%9A%84%E5%AE%B9%E5%99%A8%E9%80%9A%E4%BF%A1.png)

#### 容器访问另一节点

一个节点内的容器访问另一个节点一般流程：先经过docker0网桥，出现在宿主机上，根据路由表或者nat的方式，知道目标节点是其他机器，则将数据转发到宿主机的eth0网卡上，再发往目标节点。

其实跟同一节点内，宿主机与容器通信类似，最终转化为节点间的通信。

所以当容器无法访问外网时，就可以检查docker0网桥是否能ping通，查看docker0和Veth Pair设备的iptables规则是否有异常。

![](https://github.com/Nixum/Java-Note/raw/master/picture/%E5%AE%B9%E5%99%A8%E8%AE%BF%E9%97%AE%E5%8F%A6%E4%B8%80%E8%8A%82%E7%82%B9.png)

#### 不同节点下容器间的通信

默认配置下，不同节点间的容器、docker0网桥，是不知道彼此的，没有任何关联，想要跨主机容器通信，就需要在多主机间在建立一个公共网桥，所有节点的容器都往这个网桥注册，才能进行通信。通过每台宿主机上有一个特殊网桥来构成这个公用网桥，这个技术被称为overlay network（覆盖网络）。

常见的解决方案是Flannel、Calico等。

![](https://github.com/Nixum/Java-Note/raw/master/picture/%E5%AE%B9%E5%99%A8%E7%BD%91%E7%BB%9C.png)

#### Docker的网络模型

1. bridge模式（默认）

   Docker进程启动时，会在主机上创建一个名为docker0的虚拟网桥，此主机上启动的Docker容器会分配一个Network Namespace，通过eth0-veth虚拟设备连接到宿主机的docker0网桥上。
2. host模式

   此模式下容器不会获得独立的Network Namespace，和宿主机共用一个Network Namespace，容器也不会虚拟自己的网卡和ip，而是用宿主机的ip和端口。
3. container模式

   指定新创建的容器和一个已存在的容器共享一个Network Namespace，新创建的容器不会创建自己的网卡和IP，而是和指定的容器共享IP、端口范围
4. none模式

   Docker容器拥有自己的Network Namespace，但并不为Docker容器进行任何网络配置，需要手动添加。

## 安全

进程的权限分为两类，特权用户进程(进程有效用户ID是0，root用户的进程)、非特权用户进程(进程有效用户ID非0，非root用户的进程)，特权用户可以执行Linux系统上所有操作，而非特权用户执行这些操作会被内核限制。

在kernel2.2开始，Linux把特权用户的特权做了划分，每个被划分出来的单元称为capability，比如运行iptables命令，对应的进程需要有CAP\_NET\_ADMIN这个capability。非root用户启动的进程默认没有任何capabilities，root用户则包含了所有。子父进程的capabilities有继承属性。查看进程拥有的capability命令：`cat /proc/[pid]/status | grep Cap`。

对于privileged容器，实际上就是拥有了所有capability，允许执行所有特权操作，比如docker容器启动时增加参数 `--privileged`。容器缺省启动时，是root用户，但只允许了15个capabilities。

一般只给容器所需操作的最小capabilities，而不是直接给privileged，因为privileged的权限太大，容器可以轻易获取宿主机上的所有资源，比如直接访问磁盘设备，修改宿主机上的文件。

为了保证容器运行不对宿主机造成安全影响，可以在容器中指定用户。

在docker中，启动容器命令后面加`-u [uid]/[gid]`，或者在写Dockerfile时指定(这样启动容器就不用加-u)，**容器里的用户与宿主机上的用户共享，即容器上的uid实际上是宿主机上的uid(root用户也是，只是容器里capabilities只有15个，而宿主机上的root有全部)**，这样会产生限制，因为在Linux上每个用户的资源是有限的，比如打开文件数目、最大进程数等，如果有多个容器共享同一个uid，就会互相影响。

为了解决容器uid共享问题，可以使用User Namespace。

**User Namespace本质上是将容器中的uid/gid与宿主机上的uid/gid建立映射**，同时也支持嵌套映射。比如划分宿主机上的uid的范围1000-1999，对应容器uid的0到999，容器里uid也可以继续嵌套映射。

使用User Namespace，可以把容器中root用户映射称宿主机上的普通用户，解决uid冲突共享问题，**目前Kebernetes还不支持User Namespace(当前时间20210920，不过github上有pr了)**

## runC 与 OCI

* OCI：Open Container Initiatives，围绕容器格式和运行时制定一个开放的工业化标准，包含容器运行时标准 （runtime spec）和 容器镜像标准（image spec）。

  容器镜像标准 image spec：包含了文件系统、config文件、manifest文件、index文件；

  容器运行时标准 runtime spec：包含容器运行状态以及需要提供的命令；
* runC：一个轻量工具，基于libcontainer库，由golang语言实现，不需要docker引擎，它根据 OCI 标准来创建和运行容器的，即runC是OCI运行时标准的一个实现，不包含镜像管理功能。

  runC成为容器运行时实现的标准，而containerd是Docker出的一个中间层，为runC提供接口，供上层Docker Daemon调用。
* OCI bundle ：包括容器的文件系统和一个 config.json 文件。有了容器的根文件系统后就可以通过 runc spec 命令来生成 config.json 文件，config.json 文件用于说明如何运行容器，包括要运行的命令、权限、环境变量等等内容

OCI 定义的容器状态：

> * creating：使用 create 命令创建容器，这个过程称为创建中。
> * created：容器已经创建出来，但是还没有运行，表示镜像文件和配置没有错误，容器能够在当前平台上运行。
> * running：容器里面的进程处于运行状态，正在执行用户设定的任务。
> * stopped：容器运行完成，或者运行出错，或者 stop 命令之后，容器处于暂停状态。这个状态，容器还有很多信息保存在平台中，并没有完全被删除。
> * paused：暂停容器中的所有进程，可以使用 resume 命令恢复这些进程的执行。

## Docker

### 进程

> * dockerd ：Docker Engine守护进程，直接面向操作用户。dockerd 启动时会启动 containerd 子进程，他们之前通过RPC进行通信。
> * containerd ：dockerd和runc之间的一个中间交流组件。他与 dockerd 的解耦是为了让Docker变得更为的中立，而支持OCI 的标准 。
> * containerd-shim ：用来真正运行的容器的，每启动一个容器都会起一个新的shim进程， 它主要通过指定的三个参数：容器id，boundle目录（containerd的对应某个容器生成的目录，一般位于：/var/run/docker/libcontainerd/containerID）， 和运行命令（默认为 runc）来创建一个容器。
> * docker-proxy ：用户级的代理路由。只要你用 ps -elf 这样的命令把其命令行打出来，你就可以看到其就是做端口映射的。如果不想要这个代理的话，可以在 dockerd 启动命令行参数上加上： --userland-proxy=false 这个参数。

### Dockerfile

[指令详解](https://www.cnblogs.com/panwenbin-logs/p/8007348.html)

* RUN

后面一般接shell命令，但是会构建一层镜像

要注意RUN每执行一次指令都会在docker上新键一层，如果层数太多，镜像就会太过膨胀影响性能，虽然docker允许的最大层数是127层。

有多条命令可以使用&&连接

* CMD

要注意CMD只允许有一条，如果有多条只有最后一条会生效

## 参考

极客时间-深入剖析k8s-张磊

极客时间-容器实战高手课

[Linux资源管理之cgroups简介](https://tech.meituan.com/2015/03/31/cgroups.html)
