0%

docker学习

强烈推荐这个博主的系列文章 docker 基础

基础命令

docker images 显示出所有的镜像
docker tag 给镜像打 tag eg docker tag unbuntu myubuntu:last
docker inspect 查看镜像信息
docker history 显示 layer 基础信息
docker search —filter=is-automated=true nginx
docker rmi [-f 强行删除] 镜像 tag/镜像 ID 删除镜像 当镜像有容器在运行时,会提示无法删除。当不加 tag 时默认为 latest
docker commit [OPTIONS]

  • -a —author 作者信息
  • -c —change = [] 提交的时候执行 dockerfile 指令
  • -m —message 提交信息
  • -p —pause=true 提交时暂停容器运行

docker commit -a “xxx” -m “xxx” [容器名称 容器 id] 镜像名称 基于已有镜像的容器创建
docekr create [options] 镜像名 新建一个容器 此时容器处于停止状态
docker run [option] 镜像名 新建一个容器 并且容器处于运行状态

  • -P 暴露在 Dockerfile 中 Expose 的端口
  • -idt -i 让容器标准输入打开 -d 创建后台守护进程 -t 给 docker 分配一个伪终端
  • —name 命名容器
  • docker run -d -P —name web -v /src/webapp:/opt/webapp:ro [镜像 id 镜像名称] 容器名称 运行程序
  • docker run -it -v /dbdata —name dbdata ubuntu 创建数据卷容器
  • docker run -it —volumes-from dbdata —name db1 ubuntu 共享数据卷
  • docker run -d -P —name web —link db:db 镜像名 程序名 —link name:alias

docker start 容器名或容器 ID
docker restart 容器名或者容器 ID 先将一个运行态的容器先停止,然后重新启动它
docker attach 容器名称或容器 ID 所有窗口会同步显示,当有一个窗口阻塞的时候,其他窗口也会阻塞
docker exec -it 容器名或容器 id /bin/bash
docker rm 删除处于终止或者退出状态的容器
docker top 镜像名 查看镜像内的进程
docker logs 镜像名 获取容器的日志

Dockerfile 指令

可以查看这本 Gitbook Docker 从入门到实践 以下记录一些我不熟悉的知识点,便于复习

VOLUME 定义匿名卷

格式为:
VOLUME [“<路径 1>”, “<路径 2>”…]
VOLUME <路径>
容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据
VOLUME /data
这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置。比如:

1
docker run -d -v mydata:/data xxxx

在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。
如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 docker rm -v 这个命令

docker volume prune 清理无主的数据卷

如何修改已存在容器的配置

直接修改 /var/lib/docker/contains/[container_hash]/ 目录下的 config.v2.json 和 hostconfig.json 文件,修改之后需要重启 docker,再重新启动容器,就生效啦

docker 架构

docker 采用 C/S 架构,包含客户端和服务端,通过镜像仓库来存储镜像。客户端和服务端可以不在一个机器上,通过 socket 或者 restful api 来进行通信。

什么是镜像

镜像是文件和 meta data 的集合 root filesystem

如一个 centos 的镜像,里面包含一些最精简版的 centos 文件系统,还有其他软件包等。

镜像是分层的,每层都可以添加删除改变文件,变成一个新的 image

不同的 image 可以共享同一层 layer

镜像本身是只读的

什么是容器

容器通过 image 创建

在 image layer 之上创建了一个 container layer,这个容器层可以读写操作

image 负责 app 的存储和分发,container 负责运行 app

容器的 created 状态

docker create 只创建容器,但是不启动,docker run 是启动并且创建容器

docker start 可以启动容器

ADD 和 COPY

ADD 功能更强大,会拷贝文件并且解压,COPY 只会拷贝文件

服务端

一般在宿主机后台运行, dockerd 作为服务端接受来自客户的请求,通过 containerd 具体处理与容器相关的请求。包括创建,运行,删除容器等。

dockerd 为客户端提供 rest api,响应来自客户端的请求,采用模块化的解构,通过专门的 Engine 模块来分发管理各个来自客户端的任务。

docker-proxy 是 dockerd 的子进程,当需要进行容器端口映射时,docker-proxy 完成网络映射配置。

containerd 是 docker 的子进程,提供 gRPC 接口响应来自 dockerd 的请求,对下管理 runC 镜像和容器环境

containerd——shim, containerd 的子进程,作为容器内进程的根进程。

runC 是从 Docker 公司开源的 libcontainer 项目演化而来,。runC 支持 linux 系统中容器相关技术栈。

dockerd 默认监听本地 unix:///var/run/docker.sock 套接字,只允许本地 root 用户或者 docker 用户组成员能访问。

可以通过 -H 选项修改监听方式。

Docker 还支持通过 TLS 认证方式来验证访问。

docker-proxy 只有当启动容器并且开启端口映射才会执行,负责配置容器的端口映射规则。

客户端

默认通过本地的 unix:///var/run/docker.sock 套接字向服务端发出命令。如果服务端没有监听默认地址,需要手动指定。

命名空间

namespace 是 linux 内核一个强大特性,利用这一特性,每个容器都可以拥有自己的单独命名空间。运行在其中的应用都像在独立的操作系统环境中一样。命名空间机制保证了容器之间彼此不受影响。

进程命名空间

Linux 通过进程命名空间管理进程号。对于同一个进程在不同的命名空间中,看到的进程号不同。每个进程命名空间有一套自己的进程号管理方法。进程命名空间是一个父子关系的结构,子空间的进程对于父空间是可见的。新 fork 出来的一个进程,在父命名空间和子命名空间将分别对应不同的进程号。

一般运行一个容器,以 docker run —name test -d ubuntu:18.04 sleep 9999 为例,相关进程关系如下:

docker 服务主进程 dockerd 作为父进程启动了 docker-containerd 进程,docker-containerd 作为父进程启动了 docker-containerd-shim,其作为容器内所有进程的根进程。

IPC 命名空间

容器内进程交互还是采用了 Linux 常见的进程间交互方法,包括信号量,消息队列和共享内存等方式.PID 命名空间和 IPC 命名空间可以组合起来使用,同一个 IPC 命名空间的进程可以交互,不同空间的进程则无法交互。

网络命名空间

有了进程命名空间,不同命名空间中的进程号可以相互隔离,但是网络端口还是共享本地系统的端口。通过网络命名空间,可以实现网络隔离。一个网络命名空间为进程提供了一个完全独立的网络协议栈的视图。包括网络设备接口,IPV4 和 IPV6 协议栈,IP 路由表,防火墙规则,sockets 等。

docker 采用虚拟网络设备 Virtual Network Device VND 的方式,将不同命名空间的网络设备连接到一起。默认情况下,Docker 在宿主机上创建多个虚拟网桥,如默认的 docker0 网桥,容器中的虚拟网卡通过网桥进行桥接。

docker network ls 可以查看当前系统中的网桥。

docker网桥

使用 brctl 工具,可以看到连接到网桥上的虚拟网口信息。每个容器默认分配一个网桥上的虚拟网口,并将 docker0 的 IP 地址设置为默认的网关。容器发起的网络流量通过 宿主机的 iptables 规则进行转发。

虚拟网卡 veth

这里参考了这篇文章 Linux 网络命名空间,下面简述一下其中的内容。

虚拟网卡成对出现,像一个管道的两端,从这个管道一端的 veth 进去的数会从另一端的 veth 出来,可以用 veth 接口把一个网络命名空间连接到外部的默认命名空间或者 global 命名空间,而物理网卡就存在这些命名空间里。

创建一个网络命名空间,默认包含 lo 回环网络。然后创建一对虚拟网卡,将其中一端 veth1 加入命名空间中,设置该命名空间中的路由,使得找不到目的地址的数据包都通过 veth1 转发。这时就可以 ping 通 host 上的 veth0 网卡了。但是依然不能连接外部网络。

Linux 中 IP 转发的意思是 Linux 主机存在多个网卡的时候,允许一个网卡的数据包转发到另外一张网卡。

1
iptables -t nat -A POSTROUTING -s 10.1.1.0/255.255.255.0 -o ens160 -j MASQUERADE

添加了一条规则到 NAT 表的 POSTROUTING 链中,对于源 IP 地址为 10.1.1.0 网段的数据包,用 ens160 网口的 IP 地址替换并发送。这样在容器中就可以访问外部网络了。

整个流程

两个容器为什么能通信

只要有容器运行,docker0 接口的状态就从 down 变成了 up。

主机上的 veth 只能和 docker0 通信。容器内的 veth 可以和主机上的配对的 veth 通信。

veth 成对出现,docker0 是多个容器之间能通信的关键。

docker容器间通信

1
docker run -d --name=test2 --link test1 busybox /bin/sh -c "while true; do sleep 3600; done"

当于添加了 DNS 解析,同时 link 是单向的。只能在 test2 通过 test1 ping 通 test1。

none 和 host 网络

连接到 none 网络

1
docker run -d test1 --network none busybox /bin/sh

使用 none 模式,没有物理地址和 ip 地址,容器内只有 lo 回环网络,意味着容器不能被其他容器访问。

连接到 host 网络

1
docker run -d test1 --network host busybox /bin/sh

容器和外层 linux 共享一套网络接口

数据持久化 data volume

在 docker 中,默认数据存储在 container layer,也就是容器层中,因为只有这一层是可读可写的。但这样如果容器关闭了,并且被删除了,数据也就被删除了。docker 为了解决这种问题,出现一种机制。就是把数据转移到外挂或者 mount 的磁盘上。例如把容器里数据,同步到外层 linux 主机磁盘上。这样即使容器被删除了,容器里面数据还是保留在 linux 主机上,下次我们再启动一个容器,配置读取外层 linux 这个外挂的文件系统中数据,这个容器恢复正常服务功能。

VOLUME 的类型

有两种,第一种是受管理的 data Volume,由 docker 后台自动创建。第二种是绑定挂载的 Volume,具体挂载位置可以由用户指定。

安全防护与配置

Docker 是基于 Linux 操作系统实现的应用虚拟化。运行在容器内的进程,与运行在本地系统的进程在本质上没有区别。Docker 容器的安全性,很大程度上依赖于 Linux 系统自身。在评估 Linux 安全性上,主要考虑一下几个方面

  • Linux 内核的命名空间机制提供的容器隔离安全
  • Linux 控制组机制对容器资源的控制能力安全
  • Linux 内核的能力机制所带来的操作权限安全
  • Docker 程序本身的抗攻击性
  • 其他安全增强机制对容器安全性的影响

从网络架构上来看,所有的容器实际上是通过本地主机的网络接口 docker0 来进行相互通信,就像物理机器通过物理交换机通信一样。

与虚拟机方式相比,通过命名空间来实现的隔离并不那个绝对,运行在容器中的应用可以直接访问系统内核和部分系统文件。因此用户必须保证容器中的应用是安全可信的。

控制组是 Linux 容器机制的另一个关键组件,它负责实现资源的审计和限制。当用户执行 docker run 启动一个 容器的时候,docker 将通过 Linux 相关的调用,在后台为容器创建一个独立的控制组策略集合,该集合将限制容器内应用对资源的消耗。

控制组提供了很多有用的特性,他可以确保各个容器公平地分享主机的内存,cpu,磁盘 IO 等资源,当然,更重要的是,通过控制组可以限制容器对资源的占用,确保了当某个容器对资源消耗过大时,不会影响到本地主机系统和其他容器。

Linux 内核自 2.2 版本起支持能力机制,将权限划分为更加细粒度的操作能力,默认情况下,Docker 启动的容器有严格限制,只允许使用内核的一部分能力。通常,在服务器上会运行一堆特权进程,包括 ssh,cron,syslogd 等,容器与这些进程是不同的,而容器大部分情况下不需要真正的 root 权限,为了加强安全,容器可以禁用一些没必要的权限,包括:

  • 完全禁止任何文件挂载操作
  • 禁止直接访问本地主机的套接字
  • 禁止访问一些文件系统的操作
  • 禁止模块加载

高级网络功能

容器内 dns

docker 服务启动后悔默认启用一个内嵌的 DNS 服务,来自动解析同一个网络中的容器主机名和地址,如果无法解析,则通过容器内的 DNS 相关配置来进行解析。

容器运行时,可以再运行中的容器直接编辑 /etc/hosts /etc/hostname /etc/resolve.conf 文件,但这种改动连 docker commit 也无法保存。

容器访问外部的实现

假设容器内部的网络地址为 172.17.0.2,本地网络地址为 10.0.2.2。容器要能访问外部网络,需要进行源地址映射(Source NAT),修改为本地系统的 IP 地址 10.0.2.2。

映射是通过 iptables 的源地址伪装操作实现的。 主机 nat 表上的 POSTROUTING 链规则负责网包离开主机前,改写其源地址:

外部访问容器实现

容器允许外部访问,可以再 docker run 时候通过 -p 或者 -P 参数来启用。其实也是在本地的 iptable 的 nat 表 中添加相应的规则,将访问外部 IP 地址的包进行目标地址 DNAT,将目标地址修改为容器的 IP 地址。