赞
踩
在介绍 Dockerfile 最佳实践前,这里再强调一下,生产实践中一定优先使用 Dockerfile 的方式构建镜像。 因为使用 Dockerfile 构建镜像可以带来很多好处:
易于版本化管理,Dockerfile 本身是一个文本文件,方便存放在代码仓库做版本管理,可以很方便地找到各个版本之间的变更历史;
过程可追溯,Dockerfile 的每一行指令代表一个镜像层,根据 Dockerfile 的内容即可很明确地查看镜像的完整构建过程;
屏蔽构建环境异构,使用 Dockerfile 构建镜像无须考虑构建环境,基于相同 Dockerfile 无论在哪里运行,构建结果都一致。
虽然有这么多好处,但是如果你 Dockerfile 使用不当也会引发很多问题。比如镜像构建时间过长,甚至镜像构建失败;镜像层数过多,导致镜像文件过大。所以,这一课时我就教你如何在生产环境中编写最优的 Dockerfile。
在介绍 Dockerfile 最佳实践前,我们再聊一下我们平时书写 Dockerfile 应该尽量遵循的原则。
遵循以下 Dockerfile 书写原则,不仅可以使得我们的 Dockerfile 简洁明了,让协作者清楚地了解镜像的完整构建流程,还可以帮助我们减少镜像的体积,加快镜像构建的速度和分发速度。
由于容器的本质是进程,一个容器代表一个进程,因此不同功能的应用应该尽量拆分为不同的容器,每个容器只负责单一业务进程。
Dockerfile 也是一种代码,我们应该保持良好的代码编写习惯,晦涩难懂的代码尽量添加注释,让协作者可以一目了然地知道每一行代码的作用,并且方便扩展和使用。
应该避免安装无用的软件包,比如在一个 nginx 镜像中,我并不需要安装 vim 、gcc 等开发编译工具。这样不仅可以加快容器构建速度,而且可以避免镜像体积过大。
容器的核心是应用,因此只要基础镜像能够满足应用的运行环境即可。例如一个Java
类型的应用运行时只需要JRE
,并不需要JDK
,因此我们的基础镜像只需要安装JRE
环境即可。
在使用git
时,我们可以使用.gitignore
文件忽略一些不需要做版本管理的文件。同理,使用.dockerignore
文件允许我们在构建时,忽略一些不需要参与构建的文件,从而提升构建效率。.dockerignore
的定义类似于.gitignore
。
.dockerignore
的本质是文本文件,Docker 构建时可以使用换行符来解析文件定义,每一行可以忽略一些文件或者文件夹。具体使用方式如下:
规则 | 含义 |
---|---|
# | # 开头的表示注释,# 后面所有内容将会被忽略 |
/tmp | 匹配当前目录下任何以 tmp 开头的文件或者文件夹 |
*.md | 匹配以 .md 为后缀的任意文件 |
tem? | 匹配以 tem 开头并且以任意字符结尾的文件,?代表任意一个字符 |
!README.md | ! 表示排除忽略。 例如 .dockerignore 定义如下: *.md !README.md 表示除了 README.md 文件外所有以 .md 结尾的文件。 |
Docker 构建过程中,每一条 Dockerfile 指令都会提交为一个镜像层,下一条指令都是基于上一条指令构建的。如果构建时发现要构建的镜像层的父镜像层已经存在,并且下一条命令使用了相同的指令,即可命中构建缓存。
Docker 构建时判断是否需要使用缓存的规则如下:
从当前构建层开始,比较所有的子镜像,检查所有的构建指令是否与当前完全一致,如果不一致,则不使用缓存;
一般情况下,只需要比较构建指令即可判断是否需要使用缓存,但是有些指令除外(例如ADD
和COPY
);
对于ADD
和COPY
指令不仅要校验命令是否一致,还要为即将拷贝到容器的文件计算校验和(根据文件内容计算出的一个数值,如果两个文件计算的数值一致,表示两个文件内容一致 ),命令和校验和完全一致,才认为命中缓存。
因此,基于 Docker 构建时的缓存特性,我们可以把不轻易改变的指令放到 Dockerfile 前面(例如安装软件包),而可能经常发生改变的指令放在 Dockerfile 末尾(例如编译应用程序)。
例如,我们想要定义一些环境变量并且安装一些软件包,可以按照如下顺序编写 Dockerfile:
FROM centos:7
# 设置环境变量指令放前面
ENV PATH /usr/local/bin:$PATH
# 安装软件指令放前面
RUN yum install -y make
# 把业务软件的配置,版本等经常变动的步骤放最后
...
按照上面原则编写的 Dockerfile 在构建镜像时,前面步骤命中缓存的概率会增加,可以大大缩短镜像构建时间。
我们从 Docker Hub 拉取的官方操作系统镜像大多数都是 UTC 时间(世界标准时间)。如果你想要在容器中使用中国区标准时间(东八区),请根据使用的操作系统修改相应的时区信息,下面我介绍几种常用操作系统的修改方式:
Ubuntu 和Debian 系统
Ubuntu 和Debian 系统可以向 Dockerfile 中添加以下指令:
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo "Asia/Shanghai" >> /etc/timezone
CentOS系统
CentOS 系统则向 Dockerfile 中添加以下指令:
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
由于我们常用的官方操作系统镜像基本都是国外的,软件服务器大部分也在国外,所以我们构建镜像的时候想要安装一些软件包可能会非常慢。
这里我以 CentOS 7 为例,介绍一下如何使用 163 软件源(国内有很多大厂,例如阿里、腾讯、网易等公司都免费提供的软件加速源)加快镜像构建。
首先在容器构建目录创建文件 CentOS7-Base-163.repo,文件内容如下:
# CentOS-Base.repo
#
# The mirror system uses the connecting IP address of the client and the
# update status of each mirror to pick mirrors that are updated to and
# geographically close to the client. You should use this for CentOS updates
# unless you are manually picking other mirrors.
#
# If the mirrorlist= does not work for you, as a fall back you can try the
# remarked out baseurl= line instead.
#
#
[base]
name=CentOS-$releasever - Base - 163.com
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=os
baseurl=http://mirrors.163.com/centos/$releasever/os/$basearch/
gpgcheck=1
gpgkey=http://mirrors.163.com/centos/RPM-GPG-KEY-CentOS-7
#released updates
[updates]
name=CentOS-$releasever - Updates - 163.com
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=updates
baseurl=http://mirrors.163.com/centos/$releasever/updates/$basearch/
gpgcheck=1
gpgkey=http://mirrors.163.com/centos/RPM-GPG-KEY-CentOS-7
#additional packages that may be useful
[extras]
name=CentOS-$releasever - Extras - 163.com
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras
baseurl=http://mirrors.163.com/centos/$releasever/extras/$basearch/
gpgcheck=1
gpgkey=http://mirrors.163.com/centos/RPM-GPG-KEY-CentOS-7
#additional packages that extend functionality of existing packages
[centosplus]
name=CentOS-$releasever - Plus - 163.com
baseurl=http://mirrors.163.com/centos/$releasever/centosplus/$basearch/
gpgcheck=1
enabled=0
gpgkey=http://mirrors.163.com/centos/RPM-GPG-KEY-CentOS-7
然后在 Dockerfile 中添加如下指令:
COPY CentOS7-Base-163.repo /etc/yum.repos.d/CentOS7-Base.repo
- 1
执行完上述步骤后,再使用yum install
命令安装软件时就会默认从 163 获取软件包,这样可以大大提升构建速度。
在构建镜像时尽可能地减少 Dockerfile 指令行数。例如我们要在 CentOS 系统中安装make
和net-tools
两个软件包,应该在 Dockerfile 中使用以下指令:
RUN yum install -y make net-tools
- 1
而不应该写成这样:
RUN yum install -y make
RUN yum install -y net-tools
- 1
- 2
了解完 Dockerfile 的书写原则后,我们再来具体了解下这些原则落实到具体的 Dockerfile 指令应该如何书写。
下面是我们常用的一些指令,这些指令对于刚接触 Docker 的人来说会非常容易出错,下面我对这些指令的书写建议详细讲解一下。
RUN
指令在构建时将会生成一个新的镜像层并且执行RUN
指令后面的内容。
使用RUN
指令时应该尽量遵循以下原则:
当RUN
指令后面跟的内容比较复杂时,建议使用反斜杠(\) 结尾并且换行;
RUN
指令后面的内容尽量按照字母顺序排序,提高可读性。
例如,我想在官方的 CentOS 镜像下安装一些软件,一个建议的 Dockerfile 指令如下:
FROM centos:7
RUN yum install -y automake \
curl \
python \
vim
CMD
和ENTRYPOINT
指令都是容器运行的命令入口,这两个指令使用中有很多相似的地方,但是也有一些区别。
这两个指令的相同之处,CMD
和ENTRYPOINT
的基本使用格式分为两种。
第一种为CMD
/ENTRYPOINT
["command" , "param"]。这种格式是使用 Linux 的exec
实现的, 一般称为exec
模式,这种书写格式为CMD
/ENTRYPOINT
后面跟 json 数组,也是Docker 推荐的使用格式。
另外一种格式为CMD
/ENTRYPOINT
command param ,这种格式是基于 shell 实现的, 通常称为shell
模式。当使用shell
模式时,Docker 会以 /bin/sh -c command 的方式执行命令。
使用 exec 模式启动容器时,容器的 1 号进程就是 CMD/ENTRYPOINT 中指定的命令,而使用 shell 模式启动容器时相当于我们把启动命令放在了 shell 进程中执行,等效于执行 /bin/sh -c "task command" 命令。因此 shell 模式启动的进程在容器中实际上并不是 1 号进程。
这两个指令的区别:
Dockerfile 中如果使用了ENTRYPOINT
指令,启动 Docker 容器时需要使用 --entrypoint 参数才能覆盖 Dockerfile 中的ENTRYPOINT
指令 ,而使用CMD
设置的命令则可以被docker run
后面的参数直接覆盖。
ENTRYPOINT
指令可以结合CMD
指令使用,也可以单独使用,而CMD
指令只能单独使用。
看到这里你也许会问,我什么时候应该使用ENTRYPOINT
,什么时候使用CMD
呢?
如果你希望你的镜像足够灵活,推荐使用CMD
指令。如果你的镜像只执行单一的具体程序,并且不希望用户在执行docker run
时覆盖默认程序,建议使用ENTRYPOINT
。
最后再强调一下,无论使用CMD
还是ENTRYPOINT
,都尽量使用exec
模式。
ADD
和COPY
指令功能类似,都是从外部往容器内添加文件。但是COPY
指令只支持基本的文件和文件夹拷贝功能,ADD
则支持更多文件来源类型,比如自动提取 tar 包,并且可以支持源文件为 URL 格式。
那么在日常应用中,我们应该使用哪个命令向容器里添加文件呢?你可能在想,既然ADD
指令支持的功能更多,当然应该使用ADD
指令了。然而事实恰恰相反,我更推荐你使用COPY
指令,因为COPY
指令更加透明,仅支持本地文件向容器拷贝,而且使用COPY
指令可以更好地利用构建缓存,有效减小镜像体积。
当你想要使用ADD
向容器中添加 URL 文件时,请尽量考虑使用其他方式替代。例如你想要在容器中安装 memtester(一种内存压测工具),你应该避免使用以下格式:
ADD http://pyropus.ca/software/memtester/old-versions/memtester-4.3.0.tar.gz /tmp/
RUN tar -xvf /tmp/memtester-4.3.0.tar.gz -C /tmp
RUN make -C /tmp/memtester-4.3.0 && make -C /tmp/memtester-4.3.0 install
下面是推荐写法:
RUN wget -O /tmp/memtester-4.3.0.tar.gz http://pyropus.ca/software/memtester/old-versions/memtester-4.3.0.tar.gz \
&& tar -xvf /tmp/memtester-4.3.0.tar.gz -C /tmp \
&& make -C /tmp/memtester-4.3.0 && make -C /tmp/memtester-4.3.0 install
为了使构建过程更加清晰明了,推荐使用 WORKDIR 来指定容器的工作路径,应该尽量避免使用 RUN cd /work/path && do some work 这样的指令。
最后给出几个常用软件的官方 Dockerfile 示例链接,希望可以对你有所帮助。
好了,到此为止,相信你已经对 Dockerfile 的书写原则和一些重要指令有了较深的认识。
当你需要编写编译型语言(例如 Golang、Java)的 Dockerfile 时,如何分离编译环境和运行环境,使得镜像体积尽可能小呢?思考后,可以把你的想法写在留言区。
在第 01 课时“Docker 安装:入门案例带你了解容器技术原理”中,我有介绍到 Docker 是基于 Linux 内核的 Namespace 技术实现资源隔离的,所有的容器都共享主机的内核。其实这与以虚拟机为代表的云计算时代还是有很多区别的,比如虚拟机有着更好的隔离性和安全性,而容器的隔离性和安全性则相对较弱。
在讨论容器的安全性之前,我们先了解下容器与虚拟机的区别,这样可以帮助我们更好地了解容器的安全隐患以及如何加固容器安全。
从图 1 可以看出,虚拟机是通过管理系统(Hypervisor)模拟出 CPU、内存、网络等硬件,然后在这些模拟的硬件上创建客户内核和操作系统。这样做的好处就是虚拟机有自己的内核和操作系统,并且硬件都是通过虚拟机管理系统模拟出来的,用户程序无法直接使用到主机的操作系统和硬件资源,因此虚拟机也对隔离性和安全性有着更好的保证。
而 Docker 容器则是通过 Linux 内核的 Namespace 技术实现了文件系统、进程、设备以及网络的隔离,然后再通过 Cgroups 对 CPU、 内存等资源进行限制,最终实现了容器之间相互不受影响,由于容器的隔离性仅仅依靠内核来提供,因此容器的隔离性也远弱于虚拟机。
你可能会问,既然虚拟机安全性这么好,为什么我们还要用容器呢?这是因为容器与虚拟机相比,容器的性能损耗非常小,并且镜像也非常小,而且在业务快速开发和迭代的今天,容器秒级的启动等特性也非常匹配业务快速迭代的业务场景。
既然我们要利用容器的优点,那有没有什么办法可以尽量弥补容器弱隔离的安全性缺点呢?要了解如何解决容器的安全问题,我们首先需要了解下容器目前存在的安全问题。
Docker 作为一款容器引擎,本身也会存在一些安全漏洞,CVE 目前已经记录了多项与 Docker 相关的安全漏洞,主要有权限提升、信息泄露等几类安全问题。具体 Docker 官方记录的安全问题可以参考这里。
CVE 的维基百科定义:CVE 是公共漏洞和暴露(英语:CVE, Common Vulnerabilities and Exposures)又称常见漏洞与披露,是一个与信息安全有关的数据库,收集各种信息安全弱点及漏洞并给予编号以便于公众查阅。此数据库现由美国非营利组织 MITRE 所属的 National Cybersecurity FFRDC 所营运维护 。
由于 Docker 容器是基于镜像创建并启动,因此镜像的安全直接影响到容器的安全。具体影响镜像安全的总结如下。
镜像软件存在安全漏洞:由于容器需要安装基础的软件包,如果软件包存在漏洞,则可能会被不法分子利用并且侵入容器,影响其他容器或主机安全。
仓库漏洞:无论是 Docker 官方的镜像仓库还是我们私有的镜像仓库,都有可能被攻击,然后篡改镜像,当我们使用镜像时,就可能成为攻击者的目标对象。
用户程序漏洞:用户自己构建的软件包可能存在漏洞或者被植入恶意脚本,这样会导致运行时提权影响其他容器或主机安全。
尽管目前 Namespace 已经提供了非常多的资源隔离类型,但是仍有部分关键内容没有被完全隔离,其中包括一些系统的关键性目录(如 /sys、/proc 等),这些关键性的目录可能会泄露主机上一些关键性的信息,让攻击者利用这些信息对整个主机甚至云计算中心发起攻击。
而且仅仅依靠 Namespace 的隔离是远远不够的,因为一旦内核的 Namespace 被突破,使用者就有可能直接提权获取到主机的超级权限,从而影响主机安全。
由于同一宿主机上所有容器共享主机内核,所以攻击者可以利用一些特殊手段导致内核崩溃,进而导致主机宕机影响主机上其他服务。
既然容器有这么多安全上的问题,那么我们应该如何做才能够既享受到容器的便利性同时也可以保障容器安全呢?下面我带你来逐步了解下如何解决容器的安全问题。
事实上,Docker 从 2013 年诞生到现在,在安全性上面已经做了非常多的努力。目前 Docker 在默认配置和默认行为下是足够安全的。
Docker 自身是基于 Linux 的多种 Namespace 实现的,其中有一个很重要的 Namespace 叫作 User Namespace,User Namespace 主要是用来做容器内用户和主机的用户隔离的。在过去容器里的 root 用户就是主机上的 root 用户,如果容器受到攻击,或者容器本身含有恶意程序,在容器内就可以直接获取到主机 root 权限。Docker 从 1.10 版本开始,使用 User Namespace 做用户隔离,实现了容器中的 root 用户映射到主机上的非 root 用户,从而大大减轻了容器被突破的风险。
因此,我们尽可能地使用 Docker 最新版本就可以得到更好的安全保障。
为保障镜像安全,我们可以在私有镜像仓库安装镜像安全扫描组件,对上传的镜像进行检查,通过与 CVE 数据库对比,一旦发现有漏洞的镜像及时通知用户或阻止非安全镜像继续构建和分发。同时为了确保我们使用的镜像足够安全,在拉取镜像时,要确保只从受信任的镜像仓库拉取,并且与镜像仓库通信一定要使用 HTTPS 协议。
由于仅仅依赖内核的隔离可能会引发安全问题,因此我们对于内核的安全应该更加重视。可以从以下几个方面进行加强。
宿主机及时升级内核漏洞
宿主机内核应该尽量安装最新补丁,因为更新的内核补丁往往有着更好的安全性和稳定性。
使用 Capabilities 划分权限
Capabilities 是 Linux 内核的概念,Linux 将系统权限分为了多个 Capabilities,它们都可以单独地开启或关闭,Capabilities 实现了系统更细粒度的访问控制。
容器和虚拟机在权限控制上还是有一些区别的,在虚拟机内我们可以赋予用户所有的权限,例如设置 cron 定时任务、操作内核模块、配置网络等权限。而容器则需要针对每一项 Capabilities 更细粒度的去控制权限,例如:
cron 定时任务可以在容器内运行,设置定时任务的权限也仅限于容器内部;
由于容器是共享主机内核的,因此在容器内部一般不允许直接操作主机内核;
容器的网络管理在容器外部,这就意味着一般情况下,我们在容器内部是不需要执行ifconfig
、route
等命令的 。
由于容器可以按照需求逐项添加 Capabilities 权限,因此在大多数情况下,容器并不需要主机的 root 权限,Docker 默认情况下也是不开启额外特权的。
最后,在执行docker run
命令启动容器时,如非特殊可控情况,--privileged 参数不允许设置为 true,其他特殊权限可以使用 --cap-add 参数,根据使用场景适当添加相应的权限。
使用安全加固组件
Linux 的 SELinux、AppArmor、GRSecurity 组件都是 Docker 官方推荐的安全加固组件。下面我对这三个组件做简单介绍。
SELinux (Secure Enhanced Linux): 是 Linux 的一个内核安全模块,提供了安全访问的策略机制,通过设置 SELinux 策略可以实现某些进程允许访问某些文件。
AppArmor: 类似于 SELinux,也是一个 Linux 的内核安全模块,普通的访问控制仅能控制到用户的访问权限,而 AppArmor 可以控制到用户程序的访问权限。
GRSecurity: 是一个对内核的安全扩展,可通过智能访问控制,提供内存破坏防御,文件系统增强等多种防御形式。
这三个组件可以限制一个容器对主机的内核或其他资源的访问控制。目前,容器报告的一些安全漏洞中,很多都是通过对内核进行加强访问和隔离来实现的。
资源限制
在生产环境中,建议每个容器都添加相应的资源限制。下面给出一些执行docker run
命令启动容器时可以传递的资源限制参数:
--cpus 限制 CPU 配额
-m, --memory 限制内存配额
--pids-limit 限制容器的 PID 个数
例如我想要启动一个 1 核 2G 的容器,并且限制在容器内最多只能创建 1000 个 PID,启动命令如下:
$ docker run -it --cpus=1 -m=2048m --pids-limit=1000 busybox sh
推荐在生产环境中限制 CPU、内存、PID 等资源,这样即便应用程序有漏洞,也不会导致主机的资源完全耗尽,最大限度降低安全风险。
容器有着轻便快速启动的优点,虚拟机有着安全隔离的优点,有没有一种技术可以兼顾两者的优点,做到既轻量又安全呢?
答案是有,那就是安全容器。安全容器是相较于普通容器的,安全容器与普通容器的主要区别在于,安全容器中的每个容器都运行在一个单独的微型虚拟机中,拥有独立的操作系统和内核,并且有虚拟化层的安全隔离。
安全容器目前推荐的技术方案是 Kata Containers,Kata Container 并不包含一个完整的操作系统,只有一个精简版的 Guest Kernel 运行着容器本身的应用,并且通过减少不必要的内存,尽量共享可以共享的内存来进一步减少内存的开销。另外,Kata Container 实现了 OCI 规范,可以直接使用 Docker 的镜像启动 Kata 容器,具有开销更小、秒级启动、安全隔离等许多优点。
容器技术带来的技术革新是空前的,但是随之而来的容器安全问题也是我们必须要足够重视的。本课时解决 Docker 安全问题的精华我帮你总结如下:
到此,相信你已经了解了 Docker 与虚拟机的本质区别,也知道了容器目前存在的一些安全隐患以及如何在生产环境中尽量避免这些安全隐患。
目前除了 Kata Container 外,你还知道其他的安全容器解决方案吗?知道的同学,可以把你的想法写在留言区。
生产环境中监控容器的运行状况十分重要,通过监控我们可以随时掌握容器的运行状态,做到线上隐患和问题早发现,早解决。所以今天我就和你分享关于容器监控的知识(原理及工具 cAdvisor)。
虽然传统的物理机和虚拟机监控已经有了比较成熟的监控方案,但是容器的监控面临着更大的挑战,因为容器的行为和本质与传统的虚拟机是不一样的,总的来说,容器具有以下特性:
容器是短期存活的,并且可以动态调度;
容器的本质是进程,而不是一个完整操作系统;
由于容器非常轻量,容器的创建和销毁也会比传统虚拟机更加频繁。
Docker 容器的监控方案有很多,除了 Docker 自带的docker stats
命令,还有很多开源的解决方案,例如 sysdig、cAdvisor、Prometheus 等,都是非常优秀的监控工具。
下面我们首先来看下,不借助任何外部工具,如何用 Docker 自带的docker stats
命令实现容器的监控。
使用 Docker 自带的docker stats
命令可以很方便地看到主机上所有容器的 CPU、内存、网络 IO、磁盘 IO、PID 等资源的使用情况。下面我们可以具体操作看看。
首先在主机上使用以下命令启动一个资源限制为 1 核 2G 的 nginx 容器:
$ docker run --cpus=1 -m=2g --name=nginx -d nginx
- 1
容器启动后,可以使用docker stats
命令查看容器的资源使用状态:
$ docker stats nginx
- 1
通过docker stats
命令可以看到容器的运行状态如下:
CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
f742a467b6d8 0.00% 1.387 MiB / 2 GiB 0.07% 656 B / 656 B 0 B / 9.22 kB 2
- 1
- 2
从容器的运行状态可以看出,docker stats
命令确实可以获取并显示 Docker 容器运行状态。但是它的缺点也很明显,因为它只能获取本机数据,无法查看历史监控数据,没有可视化展示面板。
因此,生产环境中我们通常使用另一种容器监控解决方案 cAdvisor。
cAdvisor 是谷歌开源的一款通用的容器监控解决方案。cAdvisor 不仅可以采集机器上所有运行的容器信息,还提供了基础的查询界面和 HTTP 接口,更方便与外部系统结合。所以,cAdvisor很快成了容器指标监控最常用组件,并且 Kubernetes 也集成了 cAdvisor 作为容器监控指标的默认工具。
下面我们以 cAdvisor 0.37.0 版本为例,演示一下 cAdvisor 的安装与使用。
cAdvisor 官方提供了 Docker 镜像,我们只需要拉取镜像并且启动镜像即可。
由于 cAdvisor 镜像存放在谷歌的 gcr.io 镜像仓库中,国内无法访问到。这里我把打好的镜像放在了 Docker Hub。你可以使用 docker pull lagoudocker/cadvisor:v0.37.0 命令从 Docker Hub 拉取。
首先使用以下命令启动 cAdvisor:
$ docker run \
--volume=/:/rootfs:ro \
--volume=/var/run:/var/run:ro \
--volume=/sys:/sys:ro \
--volume=/var/lib/docker/:/var/lib/docker:ro \
--volume=/dev/disk/:/dev/disk:ro \
--publish=8080:8080 \
--detach=true \
--name=cadvisor \
--privileged \
--device=/dev/kmsg \
lagoudocker/cadvisor:v0.37.0
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
此时,cAdvisor 已经成功启动,我们可以通过访问 http://localhost:8080 访问到 cAdvisor 的 Web 界面。
图1 cAdvisor 首页
cAdvisor 不仅可以监控容器的资源使用情况,还可以监控主机的资源使用情况。下面我们就先看下它是如何查看主机资源使用情况的。
访问 http://localhost:8080/containers/ 地址,在首页可以看到主机的资源使用情况,包含 CPU、内存、文件系统、网络等资源,如下图所示。
图2 主机 CPU 使用情况
如果你想要查看主机上运行的容器资源使用情况,可以访问 http://localhost:8080/docker/,这个页面会列出 Docker 的基本信息和运行的容器情况,如下图所示。
图3 Docker 容器
在上图中的 Subcontainers 下会列出当前主机上运行的所有容器,点击其中一个容器即可查看该容器的详细运行状态,如下图所示。
图4 容器监控状态
总体来说,使用 cAdvisor 监控容器具有以下特点:
可以同时采集物理机和容器的状态;
可以展示监控历史数据。
了解 Docker 的监控工具,你是否想问,这些监控数据是怎么来的呢?下面我就带你了解一下容器监控的原理。
我们知道 Docker 是基于 Namespace、Cgroups 和联合文件系统实现的。其中 Cgroups 不仅可以用于容器资源的限制,还可以提供容器的资源使用率。无论何种监控方案的实现,底层数据都来源于 Cgroups。
Cgroups 的工作目录为/sys/fs/cgroup
,/sys/fs/cgroup
目录下包含了 Cgroups 的所有内容。Cgroups包含很多子系统,可以用来对不同的资源进行限制。例如对CPU、内存、PID、磁盘 IO等资源进行限制和监控。
为了更详细的了解 Cgroups 的子系统,我们通过 ls -l 命令查看/sys/fs/cgroup
文件夹,可以看到很多目录:
$ sudo ls -l /sys/fs/cgroup/
total 0
dr-xr-xr-x 5 root root 0 Jul 9 19:32 blkio
lrwxrwxrwx 1 root root 11 Jul 9 19:32 cpu -> cpu,cpuacct
dr-xr-xr-x 5 root root 0 Jul 9 19:32 cpu,cpuacct
lrwxrwxrwx 1 root root 11 Jul 9 19:32 cpuacct -> cpu,cpuacct
dr-xr-xr-x 3 root root 0 Jul 9 19:32 cpuset
dr-xr-xr-x 5 root root 0 Jul 9 19:32 devices
dr-xr-xr-x 3 root root 0 Jul 9 19:32 freezer
dr-xr-xr-x 3 root root 0 Jul 9 19:32 hugetlb
dr-xr-xr-x 5 root root 0 Jul 9 19:32 memory
lrwxrwxrwx 1 root root 16 Jul 9 19:32 net_cls -> net_cls,net_prio
dr-xr-xr-x 3 root root 0 Jul 9 19:32 net_cls,net_prio
lrwxrwxrwx 1 root root 16 Jul 9 19:32 net_prio -> net_cls,net_prio
dr-xr-xr-x 3 root root 0 Jul 9 19:32 perf_event
dr-xr-xr-x 5 root root 0 Jul 9 19:32 pids
dr-xr-xr-x 5 root root 0 Jul 9 19:32 systemd
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
这些目录代表了 Cgroups 的子系统,Docker 会在每一个 Cgroups 子系统下创建 docker 文件夹。这里如果你对 Cgroups 子系统不了解的话,不要着急,后续我会在第 10 课时对 Cgroups 子系统做详细讲解,这里你只需要明白容器监控数据来源于 Cgroups 即可。
下面我们以 memory 子系统(memory 子系统是Cgroups 众多子系统的一个,主要用来限制内存使用,Cgroups 会在第十课时详细讲解)为例,讲解一下监控组件是如何获取到容器的资源限制和使用状态的(即容器的内存限制)。
我们首先在主机上使用以下命令启动一个资源限制为 1 核 2G 的 nginx 容器:
$ docker run --name=nginx --cpus=1 -m=2g --name=nginx -d nginx
## 这里输出的是容器 ID
51041a74070e9260e82876974762b8c61c5ed0a51832d74fba6711175f89ede1
注意:如果你已经创建过名称为 nginx 的容器,请先使用 docker rm -f nginx 命令删除已经存在的 nginx 容器。
容器启动后,我们通过命令行的输出可以得到容器的 ID,同时 Docker 会在/sys/fs/cgroup/memory/docker
目录下以容器 ID 为名称创建对应的文件夹。
下面我们查看一下/sys/fs/cgroup/memory/docker
目录下的文件:
$ sudo ls -l /sys/fs/cgroup/memory/docker
total 0
drwxr-xr-x 2 root root 0 Sep 2 15:12 51041a74070e9260e82876974762b8c61c5ed0a51832d74fba6711175f89ede1
-rw-r--r-- 1 root root 0 Sep 2 14:57 cgroup.clone_children
--w--w--w- 1 root root 0 Sep 2 14:57 cgroup.event_control
-rw-r--r-- 1 root root 0 Sep 2 14:57 cgroup.procs
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.failcnt
--w------- 1 root root 0 Sep 2 14:57 memory.force_empty
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.kmem.failcnt
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.kmem.limit_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.kmem.max_usage_in_bytes
-r--r--r-- 1 root root 0 Sep 2 14:57 memory.kmem.slabinfo
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.kmem.tcp.failcnt
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.kmem.tcp.limit_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.kmem.tcp.max_usage_in_bytes
-r--r--r-- 1 root root 0 Sep 2 14:57 memory.kmem.tcp.usage_in_bytes
-r--r--r-- 1 root root 0 Sep 2 14:57 memory.kmem.usage_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.limit_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.max_usage_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.memsw.failcnt
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.memsw.limit_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.memsw.max_usage_in_bytes
-r--r--r-- 1 root root 0 Sep 2 14:57 memory.memsw.usage_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.move_charge_at_immigrate
-r--r--r-- 1 root root 0 Sep 2 14:57 memory.numa_stat
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.oom_control
---------- 1 root root 0 Sep 2 14:57 memory.pressure_level
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.soft_limit_in_bytes
-r--r--r-- 1 root root 0 Sep 2 14:57 memory.stat
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.swappiness
-r--r--r-- 1 root root 0 Sep 2 14:57 memory.usage_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 14:57 memory.use_hierarchy
-rw-r--r-- 1 root root 0 Sep 2 14:57 notify_on_release
-rw-r--r-- 1 root root 0 Sep 2 14:57 tasks
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
可以看到 Docker 已经创建了以容器 ID 为名称的目录,我们再使用 ls 命令查看一下该目录的内容:
$ sudo ls -l /sys/fs/cgroup/memory/docker/51041a74070e9260e82876974762b8c61c5ed0a51832d74fba6711175f89ede1
total 0
-rw-r--r-- 1 root root 0 Sep 2 15:21 cgroup.clone_children
--w--w--w- 1 root root 0 Sep 2 15:13 cgroup.event_control
-rw-r--r-- 1 root root 0 Sep 2 15:12 cgroup.procs
-rw-r--r-- 1 root root 0 Sep 2 15:12 memory.failcnt
--w------- 1 root root 0 Sep 2 15:21 memory.force_empty
-rw-r--r-- 1 root root 0 Sep 2 15:21 memory.kmem.failcnt
-rw-r--r-- 1 root root 0 Sep 2 15:12 memory.kmem.limit_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 15:21 memory.kmem.max_usage_in_bytes
-r--r--r-- 1 root root 0 Sep 2 15:21 memory.kmem.slabinfo
-rw-r--r-- 1 root root 0 Sep 2 15:21 memory.kmem.tcp.failcnt
-rw-r--r-- 1 root root 0 Sep 2 15:21 memory.kmem.tcp.limit_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 15:21 memory.kmem.tcp.max_usage_in_bytes
-r--r--r-- 1 root root 0 Sep 2 15:21 memory.kmem.tcp.usage_in_bytes
-r--r--r-- 1 root root 0 Sep 2 15:21 memory.kmem.usage_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 15:12 memory.limit_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 15:12 memory.max_usage_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 15:21 memory.memsw.failcnt
-rw-r--r-- 1 root root 0 Sep 2 15:12 memory.memsw.limit_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 15:21 memory.memsw.max_usage_in_bytes
-r--r--r-- 1 root root 0 Sep 2 15:21 memory.memsw.usage_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 15:21 memory.move_charge_at_immigrate
-r--r--r-- 1 root root 0 Sep 2 15:21 memory.numa_stat
-rw-r--r-- 1 root root 0 Sep 2 15:13 memory.oom_control
---------- 1 root root 0 Sep 2 15:21 memory.pressure_level
-rw-r--r-- 1 root root 0 Sep 2 15:21 memory.soft_limit_in_bytes
-r--r--r-- 1 root root 0 Sep 2 15:21 memory.stat
-rw-r--r-- 1 root root 0 Sep 2 15:21 memory.swappiness
-r--r--r-- 1 root root 0 Sep 2 15:12 memory.usage_in_bytes
-rw-r--r-- 1 root root 0 Sep 2 15:21 memory.use_hierarchy
-rw-r--r-- 1 root root 0 Sep 2 15:21 notify_on_release
-rw-r--r-- 1 root root 0 Sep 2 15:21 tasks
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
由上可以看到,容器 ID 的目录下有很多文件,其中 memory.limit_in_bytes 文件代表该容器内存限制大小,单位为 byte,我们使用 cat 命令(cat 命令可以查看文件内容)查看一下文件内容:
$ sudo cat /sys/fs/cgroup/memory/docker/51041a74070e9260e82876974762b8c61c5ed0a51832d74fba6711175f89ede1/memory.limit_in_bytes
2147483648
- 1
- 2
这里可以看到memory.limit_in_bytes 的值为2147483648,转换单位后正好为 2G,符合我们启动容器时的内存限制 2G。
通过 memory 子系统的例子,我们可以知道监控组件通过读取 memory.limit_in_bytes 文件即可获取到容器内存的限制值。了解完容器的内存限制我们来了解一下容器的内存使用情况。
内存使用情况存放在 memory.usage_in_bytes 文件里,同样我们也使用 cat 命令查看一下文件内容:
$ sudo cat /sys/fs/cgroup/memory/docker/51041a74070e9260e82876974762b8c61c5ed0a51832d74fba6711175f89ede1/memory.usage_in_bytes
4259840
- 1
- 2
可以看到当前内存的使用大小为 4259840 byte,约为 4 M。了解了内存的监控,下面我们来了解下网络的监控数据来源。
网络的监控数据来源是从 /proc/{PID}/net/dev 目录下读取的,其中 PID 为容器在主机上的进程 ID。下面我们首先使用 docker inspect 命令查看一下上面启动的 nginx 容器的 PID,命令如下:
$ docker inspect nginx |grep Pid
"Pid": 27348,
"PidMode": "",
"PidsLimit": 0,
- 1
- 2
- 3
- 4
可以看到容器的 PID 为 27348,使用 cat 命令查看一下 /proc/27348/net/dev 的内容:
$ sudo cat /proc/27348/net/dev
Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
lo: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
eth0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
- 1
- 2
- 3
- 4
- 5
/proc/27348/net/dev 文件记录了该容器里每一个网卡的流量接收和发送情况,以及错误数、丢包数等信息。可见容器的网络监控数据都是定时从这里读取并展示的。
总结一下,容器的监控原理其实就是定时读取 Linux 主机上相关的文件并展示给用户。
到此,相信你已经可以使用 docker stats 和 cAdvisor 监控并查看容器的状态了;也可以自己启动一个 cAdvisor 容器来监控主机和主机上的容器,并对监控系统的原理有了较深的了解。
试想下,cAdvisor 虽然可以临时存储一段历史监控数据,并且提供了一个简版的监控面板,在大规模的容器集群中,cAdvisor 有什么明显的不足吗?思考后,把你的想法写在留言区。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。