Docker入门与实践(二)
Docker运行环境
docker 是支持跨平台运行,支持MacOS,Windows,Linux操作系统,详见 https://docs.docker.com/get-docker/
- Docker (client-server 架构) 包含一个守护进程Docker daemon(Server端), 一个Docker Client(Cli)
- Linux centos 要求至少centos7 版本,Windows至少要windows10,MacOs至少要MacOS10.14
- Docker 支持RestAPI, 分两类 Docker Engine API 与 Registry API
journalctl -u docker.service //linux查看Docker daemon日志
docker api 使用
vim /usr/lib/systemd/system/docker.service
在 ExecStart=/usr/bin/dockerd 后面直接添加 -H tcp://0.0.0.0:8088 -H unix:///var/run/docker.sock
systemctl daemon-reload
systemctl restart docker
curl -X GET http://127.0.0.1:8088/images/json //查看镜像
详见 https://docs.docker.com/engine/api/
Docker非 root账号运行
groupadd docker //添加docker组,默认已有(安装docker)
gpasswd -a s docker #s表示当前使用的用户
systemctl restart docker //重启docker
Docker 镜像管理
Docker基础镜像
Docker 提供了两种方法来创建基础镜像,一种是通过引入tar包的形式,另外一种是通过一个空白的镜像来一步一步构建,本文使用的是第二种方法,既FROM scratch, 简单例子如下
FROM scratch
ADD hello /
CMD ["/hello"]
Docker 的镜像实际上由一层一层的文件系统组成,这种层级的文件系统就是上文说到的UnionFS。在Docker镜像的最底层是bootfs。这一层与我们典型的Linux/Unix系统是一样的,包含boot加载器和内核。当boot加载完成之后整个内核就都在内存中了,此时内存的使用权已由bootfs转交给内核,此时系统也会卸载bootfs。Docker在bootfs之上的一层是rootfs(根文件系统)。rootfs就是各种不同的操作系统发行版,比如Ubuntu,Centos等等。
常用基础镜像
- scratch :是Docker保留镜像,不能pull,名称不能被其他镜像使用
- busybox :一个集成了一百多个最常用Linux命令和工具,可以理解为一个超级简化版嵌入式Linux系统,只有2M左右
- alpine :Alpine操作系统是一个面向安全的轻型Linux发行版。它不同于通常的Linux发行版,Alpine采用了musl libc和BusyBox以减小系统的体积和运行时资源消耗,只有5M左右,但功能上比BusyBox又完善得多。
- centos :centos操作系统镜像约200M左右
- debian/ubuntu:debian系的服务器操作系统镜像,约100M左右
构建镜像建议使用alpine,此镜像比busybox稍大,但是比其他的系统镜像都小,busybox没有包管理工具,这带来很多不便,早期很多镜像都是基于ubuntu/debian,现在大部分官网镜像都改成alpine作为基础镜像,如果使用Alpine镜像替换Ubuntu基础镜像,安装软件包时需要用apk包管理器替换apt工具,详见 https://hub.docker.com/search?image_filter=official&type=image&category=base 官网基础镜像
Docker镜像分层(CopyOnWrite)
docker通过一个叫做copy-on-write (CoW) 的策略来保证base镜像的安全性,以及更高的性能和空间利用率。
image- 启动容器的时候,最上层容器层是可写层,之下的都是镜像层,只读层,保证镜像的安全
- 当容器需要添加文件时候,直接操作最上面容器层,不会影响镜像层
- 当容器需要删除文件时候,从上往下找文件,在容器层记录删除,软删除
- 当容器需要修改文件时候,从上往下层找文件,复制到容器层修改
- 当容器需要读取文件的时候,从上往下层找,读取并放入内存,若已经存在内存直接使用
详见 https://docs.docker.com/storage/storagedriver/
减小镜像大小的方法
1.同层删除
尽量将中间依赖的安装与卸载操作放在一个步骤中,docker镜像是一个由“层”来堆叠起来的“千层饼”,dockerfile里面每一条命令都会增加一层,所以我们需要将安装、使用、卸载三个部分写在一个步骤中,才能保证空间被释放
FROM alpine:3.12
RUN truncate -s 50M /sample.dat
RUN rm -rf /sample.dat //删除和创建不在同一层,永久保留进镜像层
FROM alpine:3.12
RUN truncate -s 50M /sample.dat && rm -rf /sample.dat // 同层删除,只有一层
2.多阶段编译(主流)
Docker 17.05版本以后,新引入了multi-stage builds这一概念,简单来说,multi-stage builds支持我们将Docker镜像的编译分成多个“阶段”。比如常见的软件编译的情况,我们可以将编译阶段单独提出来,软件编译完成后直接将二进制文件拷贝到一个新的基础镜像中,这样做最大的好处就是,第二个镜像不再包含任何编译阶段使用的中间依赖.
以最常见JAVA项目为例,编译Jar包的时候,我们需要使用到JDK、Maven等工具,但在实际运行阶段,我们只需要JRE环境即可,简单比较一下openjdk:8u275 与 openjdk:8u275-jre,相差一倍多。
FROM golang:1.7.3 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]
详见: https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
3.使用slim版本的镜像
Docker官方的Debian镜像有个slim版本,这个版本的大小比默认的版本要小一倍多,slim的中文意思就是“苗条的”,顾名思义,debian:stretch-slim确实苗条的多,原因是其删除了man文档等许多不会在容器里用到的文件。有一些上层的镜像会基于slim版本的debian进行编写,比如python。如果我们开发python的项目,可以使用python:slim这个基础镜像。
4.使用Alpine Linux基础镜像
Alpine Linux是一个基于BusyBox和Musl Libc的Linux发行版,其最大的优势就是小。一个纯的基础Alpine Docker镜像在压缩后仅有2.67MB。
改善镜像构建时间
Docker在构建镜像时,默认文件名是Dockerfile, 位于当前上下目录中 docker build -t demo:v1 .,其中 . 指定的是上下文路径而不是dockerfile文件的路径。
什么是上下文
用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件,因为Docker采用是C/S设计架构,真正执行构建是在Server端
假设在Dockerfile 文件中 COPY ./package.json /app/ 复制上下文(context)目录下的 package.json。
因此,COPY 这类指令中的源文件的路径都是相对路径。这也是初学者经常会问的为什么 COPY ../package.json /app 或者 COPY /opt/xxxx /app 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去
自定义构建上下文和dockerfile
docker build -f ../Dockerfile1.php -t demo:v1 ../
# 指定dockerfile文件是当前执行命令目录的上级目录的Dockerfile1.php文件,
# 上下文是当前执行命令目录的上级目录
1.减小构建上下文的大小
-
指定合适的上下文路径,上下文路径中的文件或者文件夹是构建镜像必须的
-
通过 .gitignore 一样的语法写一个 .dockerignore,用于剔除文件传给Docker引擎
#comment #注释 可以忽略
*/temp* #例如/somedir/temporary.txt 和目录 /somedir/temp 被剔除
*/*/temp* #例如/somedir/subdir/temporary.txt 被剔除
temp? #例如/tempa 和 /tempb 目录被剔除
详见: https://docs.docker.com/engine/reference/builder/#dockerignore-file
2.采用registry镜像
可以直接才本地registry镜像拉取,不需要每次都要docker hub官网拉取
3.复用镜像层
设计镜像层时要易变动的镜像层独立出来,万一有变动,重新构建时,其它没有变动的镜像层复用(Using cache)
Docker镜像仓库
镜像拉取的过程(执行pull)
-
第一步是下载Manifest, Manifest里包含了前面所说的配置文件和层列表
image - 第二部根据内容里的config部分的digest(镜像配置文件的sha256sum值,即镜像id)下载镜像配置文件以及层列表
镜像配置文件存储目录:/var/lib/docker/image/overlay2/imagedb/content/sha256/${imageid}
镜像层文件存储目录: /var/lib/docker/image/overlay2/layerdb/sha256/${diff_ids}
实际镜像文件存储目录: /var/lib/docker/overlay2/...
设置内网镜像地址
vim /etc/docker/daemon.json //编辑文件
{
"registry-mirrors":["私服仓库URL"]
}
部署内网镜像仓库
Docker hub为我们提供了很多官方镜像和个人上传的镜像,我们可以下载机构或个人提供的镜像(如ubuntu,centos...),也可以上传我们自己的本地镜像,然后我们需要的时候也可以随时下载。看着很是方便,但是也有不方便的地方
- dockerhub 仓库在外网,带宽受限
- 公司内创建的镜像不想对外开放,只允许内网的开发人员下载使用
因此在内部网络搭建docker私有仓库可以使内网人员下载和上传都非常快速,不受外网带宽等因素的影响,同时不在内网的人员无法下载我们的镜像.
两种搭建方式:
- 使用registry,简易仓库
docker pull registry
docker run -d -v /home/registry:/var/lib/registry -p 5000:5000 --restart always --privileged true --name myregistry registry:lastest
- 使用开源Harbor安装, Harbor功能更齐全,含账户权限管理等功能,适用大规模docker集群部署提供仓库服务(推荐使用)
官网: https://goharbor.io/docs/2.1.0
github地址: https://github.com/goharbor/harbor/releases
下载harbor-offline-installer-v2.1.1.tgz
tar xf harbor-offline-installer-v2.1.1.tgz
cd harbor/
#修改harbor.yml配置信息,hostname 值改成机器ip,harbor_admin_passwrd 值为admin账号的密码, 如果不想使用https协议,直接注释掉 #https,#certificate:/your/certificate/path #private_key:/your/private/keypath
sh install.sh
访问http://ip 直接访问harbor
使用docker cli 需配置daemon.json 文件配置"insecure-registries":["ip"]
Docker网络架构
容器运行设置 --network 参数指定网络,不指定的话默认是bridge
安装docker默认创建三个网络:
docker network ls //查看存在的网络
NETWORK_ID | NAME | DRIVER | SCOPE |
---|---|---|---|
... | bridge | bridge | local |
... | host | host | local |
... | none | null | local |
五种网络模式
bridge 模式
启动容器默认的网络驱动模式 --network bridge
容器的网络是独立,有独立的ip和端口,通过-p 做映射, 查看iptables -nvL
使用默认的bridge网桥,容器之间通信只能使用ip,而不能使用容器名和主机名通信,不建议将默认bridge网络用于生产环境,生产环境中独立运行的容器应使用用户自定义的网桥(自定义网桥,容器之间通信可以使用ip、容器名称及主机名)
host 模式
容器的网络堆栈不会与Docker宿主机隔离,指定端口参数-p 或者 -P 不起作用,相当于在宿主机启动一个程序,占用宿主机的端口,即容器的端口就是宿主机的端口,容器没有自己的独立ip, 该网络模式只支持linux系统的主机
overlay 模式
overlay网络驱动程序会在多个docker守护程序(即多个主机上的docker守护程序)之间创建分布式网络,初始化swarm或将Docker主机加入现有swarm时,会在该Docker主机上创建两个新网络:
- 名称为ingress的overlay网络
- 名为docker_gwbridge的桥接网络
macvlan
none 模式
--network none 完全禁用容器上的网络堆栈,容器内仅创建一个环回设备lo,没有eth网络
docker run --network none --name demo alpine:latest ash
docker exec demo ip route #查看容器路由表
自建网络
管理自定义网络
docker network create -d bridge my-net //创建网络my-net,-d 指定网络驱动模式,默认是bridge
docker network rm my-net //删除网络
docker network connect my-net demo //容器demo 连接my-net网络
docker network disconnect my-net demo //容器demo 断开my-net网络
生产环境应该使用自定义网络,不要使用默认的网络,更安全更清晰
Docker资源限制
docker 作为容器的管理者,自然提供了控制容器资源的功能。正如使用内核的 namespace 来做容器之间的隔离,docker 也是通过内核的 cgroups 来做容器的资源限制;包括CPU、内存、磁盘三大方面,基本覆盖了常见的资源配额和使用量控制
详见: https://docs.docker.com/config/containers/resource_constraints/
内存限制相关参数
执行docker run命令时能使用的和内存限制相关的所有选项如下。
参数 | 描述 |
---|---|
-m,--memory | 内存限制,格式是数字加单位,单位可以为 b,k,m,g。最小为 4M |
--memory-swap | 内存+交换分区大小总限制。格式同上。必须必-m设置的大 |
--memory-reservation | 内存的软性限制。格式同上 |
--oom-kill-disable | 是否阻止 OOM killer 杀死容器,默认没设置 |
--oom-score-adj | 容器被 OOM killer 杀死的优先级,范围是[-1000, 1000],默认为 0 |
--memory-swappiness | 用于设置容器的虚拟内存控制行为。值为 0~100 之间的整数 |
--kernel-memory | 核心内存限制。格式同上,最小为 4M |
java项目开发,一般通过设置jvm参数来控制
ENV JAVA_OPTS '-Xms512M -Xmx512M'
ENTRYPOINT java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar /app.jar
docker run --rm -e JAVA_OPTS='-Xms1g -Xmx1g' tomcat
CPU限制
对容器最多能使用的 CPU 时间有两种限制方式:一是有多个 CPU 密集型的容器竞争 CPU 时,设置各个容器能使用的 CPU 时间相对比例。二是以绝对的方式设置容器在每个调度周期内最多能使用的 CPU 时间。
参数 | 描述 |
---|---|
--cpuset-cpus="" | 允许使用的 CPU 集,值可以为 0-3,0,1 |
-c,--cpu-shares=0 | CPU 共享权值(相对权重) |
--cpu-period=0 | 限制 CPU CFS 的周期,范围从 100ms~1s,即[1000, 1000000] |
--cpu-quota=0 | 限制 CPU CFS 配额,必须不小于1ms,即 >= 1000 |
--cpuset-mems="" | 允许在上执行的内存节点(MEMs),只对 NUMA 系统有效 |
CPU 资源的相对限制
我们可以设置容器可以在哪些 CPU 核上运行。
例如:
$ docker run -it --cpuset-cpus="1,3" ubuntu:14.04 /bin/bash
表示容器中的进程可以在 cpu 1 和 cpu 3 上执行。
默认情况下,所有的容器得到同等比例的 CPU 周期。在有多个容器竞争 CPU (竞争同一个cpu)时我们可以设置每个容器能使用的 CPU 时间比例。这个比例叫作共享权值,通过 -c或 --cpu-shares 设置。Docker 默认每个容器的权值为 1024。不设置或将其设置为 0,都将使用这个默认值。系统会根据每个容器的共享权值和所有容器共享权值和比例来给容器分配 CPU 时间。
CPU 资源的绝对限制
Linux 通过 CFS(Completely Fair Scheduler,完全公平调度器)来调度各个进程对 CPU 的使用。CFS 默认的调度周期是 100ms。我们可以设置每个容器进程的调度周期,以及在这个周期内各个容器最多能使用多少 CPU 时间。使用--cpu-period即可设置调度周期,使用--cpu-quota即可设置在每个周期内容器能使用的 CPU 时间。两者一般配合使用。
例如:
$ docker run -it --cpu-period=50000 --cpu-quota=25000 ubuntu:16.04 /bin/bash
将 CFS 调度的周期设为 50000,将容器在每个周期内的 CPU 配额设置为 25000,表示该容器每 50ms 可以得到 50% 的 CPU 运行时间。CFS 周期的有效范围是 1ms~1s,对应的--cpu-period的数值范围是 1000 ~ 1000000。而容器的 CPU 配额必须不小于 1ms,即--cpu-quota的值必须 >= 1000。可以看出这两个选项的单位都是 us。
磁盘IO配额控制
相对于CPU和内存的配额控制,docker对磁盘IO的控制相对不成熟,大多数都必须在有宿主机设备的情况下使用。主要包括以下参数:
- –device-read-bps:限制此设备上的读速度(bytes per second),单位可以是kb、mb或者gb。
- –device-read-iops:通过每秒读IO次数来限制指定设备的读速度。
- –device-write-bps :限制此设备上的写速度(bytes per second),单位可以是kb、mb或者gb。
- –device-write-iops:通过每秒写IO次数来限制指定设备的写速度。
- –blkio-weight:容器默认磁盘IO的加权值,有效值范围为10-100。
- –blkio-weight-device: 针对特定设备的IO加权控制。其格式为DEVICE_NAME:WEIGHT
Docker故障排查
nsenter 工具(外部探测)
nsenter命令仅进入该容器的网络命名空间,使用宿主机的命令调试容器网络
例如:
docker inspect -f {{.State.Pid}} containid #获取容器的pid
nsenter -n -t pid #进入容器的网络命名空间
原理
namespace是Linux中一些进程的属性的作用域,使用命名空间,可以隔离不同的进程。Linux在不断的添加命名空间,目前有:
- mount:挂载命名空间,使进程有一个独立的挂载文件系统,始于Linux 2.4.19
- ipc:ipc命名空间,使进程有一个独立的ipc,包括消息队列,共享内存和信号量,始于Linux 2.6.19
- uts:uts命名空间,使进程有一个独立的hostname和domainname,始于Linux 2.6.19
- net:network命令空间,使进程有一个独立的网络栈,始于Linux 2.6.24
- pid:pid命名空间,使进程有一个独立的pid空间,始于Linux 2.6.24
- user:user命名空间,是进程有一个独立的user空间,始于Linux 2.6.23,结束于Linux 3.8
- cgroup:cgroup命名空间,使进程有一个独立的cgroup控制组,始于Linux 4.6
Linux的每个进程都具有命名空间,可以在/proc/PID/ns目录中看到命名空间的文件描述符。
使用
nsenter [options] [program [arguments]]
options:
-t, --target pid:指定被进入命名空间的目标进程的pid
-m, --mount[=file]:进入mount命令空间。如果指定了file,则进入file的命令空间
-u, --uts[=file]:进入uts命令空间。如果指定了file,则进入file的命令空间
-i, --ipc[=file]:进入ipc命令空间。如果指定了file,则进入file的命令空间
-n, --net[=file]:进入net命令空间。如果指定了file,则进入file的命令空间
-p, --pid[=file]:进入pid命令空间。如果指定了file,则进入file的命令空间
-U, --user[=file]:进入user命令空间。如果指定了file,则进入file的命令空间
-G, --setgid gid:设置运行程序的gid
-S, --setuid uid:设置运行程序的uid
-r, --root[=directory]:设置根目录
-w, --wd[=directory]:设置工作目录
Docker容器日志管理
docker logs CONTAINER 显示当前运行的容器的日志信息, UNIX 和 Linux 的命令有三种 输入输出,分别是 STDIN(标准输入)、STDOUT(标准输出)、STDERR(标准错误输出),docker logs 显示的内容包含 STOUT 和 STDERR。在生产环境,如果我们的应用输出到我们的日志文件里,所以我们在使用 docker logs 一般收集不到太多重要的日志信息。
日志驱动全局配置
修改日志驱动,在配置文件 /etc/docker/daemon.json(注意该文件内容是 JSON 格式的)进行配置即可。
示例:
{
"log-driver": "json-file"
}
单一容器日志驱动配置
docker run -itd --log-driver none alpine ash # 这里指定的日志驱动为 none
使用 Docker-CE 版本,docker logs命令 仅仅适用于以下三种驱动程序
local
默认情况下,local 日志驱动为每个容器保留 100MB 的日志信息,并启用自动压缩来保存。(经过测试,保留100MB 的日志是指没有经过压缩的日志) local 日志驱动的储存位置 /var/lib/docker/containers/容器id/local-logs/ 以 container.log 命名,那么当超过了 100MB 的日志文件,日志文件会继续写入到 container.log,但是会将 container.log 日志中老的日志删除,追加新的
local 驱动支持的选项
选项 | 描述 | 示例值 |
---|---|---|
max-size | 切割之前日志的最大大小。可取值为(k,m,g), 默认为20m。 | --log-opt max-size=10m |
max-file | 可以存在的最大日志文件数。如果超过最大值,则会删除最旧的文件。仅在max-size设置时有效。默认为5。 | --log-opt max-file=3 |
compress | 对应切割日志文件是否启用压缩。默认情况下启用。 | --log-opt compress=false |
全局日志驱动设置为—local
在配置文件 /etc/docker/daemon.json(注意该文件内容是 JSON 格式的)进行配置即可。
{
"log-driver": "local",
"log-opts": {
"max-size": "10m"
}
}
json-file
日志格式为JSON。Docker的默认日志记录驱动程序
json-file 日志驱动日志中不仅包含着 输出日志,还有时间戳和输出格式,json-file 日志的路径位于 /var/lib/docker/containers/container_id/container_id-json.log
json-file 的 日志驱动支持以下选项:
选项 | 描述 | 示例值 | |
---|---|---|---|
max-size | 切割之前日志的最大大小。可取值单位为(k,m,g), 默认为-1(表示无限制)。 | --log-opt max-size=10m | |
max-file | 可以存在的最大日志文件数。如果切割日志会创建超过阈值的文件数,则会删除最旧的文件。仅在max-size设置时有效。正整数。默认为1。 | --log-opt max-file=3 | |
labels | 适用于启动Docker守护程序时。此守护程序接受的以逗号分隔的与日志记录相关的标签列表。 | --log-opt labels=production_status,geo | |
env | 适用于启动Docker守护程序时。此守护程序接受的以逗号分隔的与日志记录相关的环境变量列表。 | --log-opt env=os,customer | |
env-regex | 类似于并兼容env。用于匹配与日志记录相关的环境变量的正则表达式。 | --log-opt env-regex=^(os | customer). |
compress | 切割的日志是否进行压缩。默认是disabled。 | --log-opt compress=true |
**如果没有设置max-size,则日志不受限,但不能超过容器最大10G(默认值)限制 **
journald
将日志消息写入journald。该journald守护程序必须在主机上运行,可以使用 journal API 或者使用 docker logs 来查日志。
除了日志本身以外, journald 日志驱动还会在日志加上下面的数据与消息一起储存。
CONTAINER_ID 容器ID,为 12个字符
CONTAINER_ID_FULL 完整的容器ID,为64个字符
CONTAINER_NAME 启动时容器的名称,如果容器后面更改了名称,日志中的名称不会更改。
CONTAINER_TAG, SYSLOG_IDENTIFIER 容器的tag.
CONTAINER_PARTIAL_MESSAGE 当日志比较长的时候使用标记来表示(显示日志的大小)
支持日志驱动选项
选项 | 是否必须 | 描述 |
---|---|---|
tag | 可选的 | 指定要在日志中设置CONTAINER_TAG和SYSLOG_IDENTIFIER值的模板。 |
labels | 可选的 | 以逗号分隔的标签列表,如果为容器指定了这些标签,则应包含在消息中。 |
env | 可选的 | 如果为容器指定了这些变量,则以逗号分隔的环境变量键列表(应包含在消息中)。 |
env-regex | 可选的 | 与env类似并兼容。用于匹配与日志记录相关的环境变量的正则表达式 。 |
查看日志 journalctl
# 只查询指定容器的相关消息
journalctl CONTAINER_NAME=webserver
# -b 指定从上次启动以来的所有消息
journalctl -b CONTAINER_NAME=webserver
# -o 指定日志消息格式,-o json 表示以json 格式返回日志消息
journalctl -o json CONTAINER_NAME=webserver
# -f 一直捕获日志输出
journalctl -f CONTAINER_NAME=webserver