一、背景接上一篇《Docker复习之部署篇》,本文继续对Docker之日常维护常用操作做回顾总结。相关资源:官方文档、菜鸟教程、Docker 基础教程、Dockerfile、Docker Engine for Linux安装脚本、Docker博客、hostconfig.json vs config.v2.json、深入架构原理与实践1.1 Docker之namespace回顾Docker依赖于Linux内核技术 chroot 、namespace 和 cgroup。Docker通过namespace实现了资源隔离,通过cgroups实现了资源限制,通过写时复制机制(copy-on-write)实现了高效的文件操作。首先,我们回顾下 namespace ;我们知道,Docker本质上是宿主机上的进程(容器进程),而容器的资源隔离也就是指进程资源的隔离。而实现这种资源隔离的核心技术就是 Linux namespace。这种隔离是一种逻辑层面的,更像是抽象出多个轻量级的内核(容器进程),这些进程可以充分利用宿主机的资源,宿主机拥有的所有资源,这些容器进程都可以享有,但容器进程之间是隔离的,对应的,不同容器进程之间使用资源也是隔离的,这样,即使彼此之间进行相同的操作,也不会互相干扰,安全性得到保障。Linux namespace 实现了6 项资源隔离,基本上涵盖了一个小型操作系统的运行要素,包括主机名、用户权限、文件系统、网络、进程号、进程间通信,这6 项资源隔离分别对应6 种系统调用,通过传入上表中的参数,调用 clone() 函数来完成。如下表:查看当前进程下有哪些 namespace 隔离,可以查看文件 /proc/[pid]/ns ,/proc/[pid]/ns里设置这些符号链接的另一个作用是,一旦这些链接文件被打开,那么就算该namespace下的所有进程都已经结束,这个namespace也会一直存在,后续进程也可以再加进来。把/proc/[pid]/ns目录文件使用–bind方式挂载可以直到同样的作用,比如mount --bind /proc/2454/ns/uts uts;在Docker中,我们可以在进程都结束的情况下,通过这种形式把namespace保留下来,为以后有进程加入做准备。在Docker中,使用docker exec命令可以使用setns()函数加入已经存在的命名空间,在已经运行着的容器中执行一个新命令。如下所示:可以看到,每一项 namespace 都附带一个编号,这是唯一标识 namespace 的,如果两个进程指向的 namespace 编号相同,则表示它们同在该 namespace 下。另外,在4.6 版本的内核中,namespace会多一个cgroup;其中:1UTS namespace提供了主机名和域名的隔离,这样每个容器就拥有独立的主机名和域名了,在网络上就可以被视为一个独立的节点,在容器中对 hostname 的命名不会对宿主机造成任何影响。2IPC namespace实现了进程间通信的隔离,包括常见的几种进程间通信机制,如信号量,消息队列和共享内存。我们知道,要完成 IPC,需要申请一个全局唯一的标识符,即 IPC 标识符,所以 IPC 资源隔离主要完成的就是隔离 IPC 标识符。(ipcs -q可查看消息队列情况;ipcmk -Q可创建一个消息队列)3PID namespace完成的是进程号的隔离,它对进程PID重新标号; 为了做到进程空间的隔离,会首先在容器内创建出PID为1的进程(root namespace),其后它会再创建的新PID namespace被称为child namespace(树的子节点),而原来的PID namespace就是新创建的PID namespace的parent namespace(树的父节点)。这样,不同的PID namespace会形成一个层级体系。所属的父节点可以看到子节点中的进程,并可以通过信号等方式对子节点中的进程产生影响。但子节点却不能看到父节点PID namespace中的任何内容。另外,会发现容器内shell里执行ps,top等命令,还是可以看得到所有进程。说明并没有完全隔离。这是因为,像ps, top这些命令会去读/proc文件系统,因为/proc文件系统在父进程和子进程都是一样的,所以这些命令显示的东西都是一样的,当然我们可以临时重新挂载/proc来实现只看到PID namespace本身应该看到的进程。因此,我们还需要对文件系统进行隔离。4mount namespace:通过隔离文件系统挂载点对隔离文件系统来提供支持的,通过CLONE_NEWNS创建mount namespace后,父进程会把自己的文件结构复制给子进程中。而子进程中新的namespace中的所有mount操作都只影响自身的文件系统,而不对外界产生任何影响,即隔离后,不同的mount namespace中的文件结构发生变化也互不影响。这样可以做到比较严格地隔离。我们可以通过/proc/[pid]/mounts查看到所有挂载在当前namespace中的文件系统,通过/proc/[pid]/mountstats看到mount namespace中文件设备的统计信息,包括挂载文件的名字、文件系统的类型、挂载位置等。需要注意的是,文件系统结构复制这种隔离存在例外,比如父节点namespace中的进程挂载了一张CD-ROM,这时子节点namespace复制的目录结构是无法自动挂载上这张CD-ROM的,因为这种操作会影响到父节点的文件系统。5cgroup:控制组cgroup ,它提供了一套机制用于控制一组特定进程对资源的使用。cgroup绑定一个进程集合到一个或多个子系统上。使​​​用​​​ cgroup,可​​​更​​​具​​​体​​​地​​​控​​​制​​​对​​​系​​​统​​​资​​​源​​​的​​​分​​​配​​​、​​​优​​​先​​​顺​​​序​​​、​​​拒​​​绝​​​、​​​管​​​理​​​和​​​监​​​控​​​。​​​可​​​更​​​好​​​地​​​根​​​据​​​任​​​务​​​和​​​用​​​户​​​分​​​配​​​硬​​​件​​​资​​​源​​​,提​​​高​​​总​​​体​​​效​​​率​​​。关于cgroup定义,参考官方解释:cgroups, are a Linux kernel featurewhichallow processes to be organized into hierarchical(层级树)groupswhose usage of various types of resources canthen be limited and monitored. The kernel's cgroup interface is provided through a pseudo-filesystem called cgroupfs. Grouping is implementedinthe core cgroup kernel code,whileresource tracking and limits are implementedinasetof per-resource-type subsystems(memory, CPU, and so on). cgroups,是 Linux内核的功能,允许将进程组织按不通层级分配使用各种类型资源并且红纸和监控这些资源的使用。内核的 cgroup 接口就是通过名为 cgroupfs 的伪文件系统提供。分组在核心 cgroup 内核代码中实现,而资源跟踪和控制则在一组各类资源子系统中实现(如内存、CPU 等)。subsystem,可理解为一个通过cgroup提供的工具和接口来管理进程集合的模块。一个子系统就是一个典型的“资源控制器”,用来调度资源或者控制资源使用的上限。每种资源可以看作是一个子系统。子系统可以是以进程为单位的任何东西,Cgroups主要由task,cgroup,subsystem及hierarchy构成:●Task : 在Cgroups中,task就是系统的一个进程。●Cgroup : Cgroups中的资源控制都以cgroup为单位实现的。cgroup表示按照某种资源控制标准划分而成的任务组,包含一个或多个Subsystems。一个任务可以加入某个cgroup,也可以从某个cgroup迁移到另外一个cgroup。●Subsystem : Cgroups中的subsystem就是一个资源调度控制器(Resource Controller)。比如CPU子系统可以控制CPU时间分配,内存子系统可以限制cgroup内存使用量。●Hierarchy(层级树) : hierarchy由一系列cgroup以一个树状结构排列而成,每个hierarchy通过绑定对应的subsystem进行资源调度。hierarchy中的cgroup节点可以包含零或多个子节点,子节点继承父节点的属性。整个系统可以有多个hierarchy。cgroup主要提供了如下功能:1Resource limitation: 限制资源使用,比如内存使用上限以及文件系统的缓存限制。2Prioritization: 优先级控制,比如:CPU利用和磁盘IO吞吐。3Accounting: 一些审计或一些统计,主要目的是为了计费。4Control: 挂起进程,恢复执行进程。【cgroup术语解析】:kio: 这个subsystem可以为块设备设定输入/输出限制,比如物理驱动设备(包括磁盘、固态硬盘、USB等)。 cpu: 这个subsystem使用调度程序控制task对cpu的使用。 cpuacct: 这个subsystem自动生成cgroup中task对cpu资源使用情况的报告。 cpuset: 这个subsystem可以为cgroup中的task分配独立的cpu(此处针对多处理器系统)和内存。 devices 这个subsystem可以开启或关闭cgroup中task对设备的访问。 freezer 这个subsystem可以挂起或恢复cgroup中的task。 memory 这个subsystem可以设定cgroup中task对内存使用量的限定,并且自动生成这些task对内存资源使用情况的报告。 perfevent 这个subsystem使用后使得cgroup中的task可以进行统一的性能测试。{![perf: Linux cpu性能探测器,详见https://perf.wiki.kernel.org/index.PHP/MainPage]}*net_cls 这个subsystem Docker没有直接使用,它通过使用等级识别符(classid)标记网络数据包,从而允许 Linux 流量控制程序(TC:Traffic Controller)识别从具体cgroup中生成的数据包。查看/proc/[pid]/cgroup进程获取cgroup信息,输出内容以hierarchy-ID:controller-list:cgroup-path来显示。查看/sys/fs/cgroup/目录,可查看每项资源的使用情况。比如:cpuacct/cpuacct.usage_percpu、cpuset/cpuset.cpus、cpu/cpu.cfs_period_us、cpu/cpu.cfs_quota_us、cpuacct/cpuacct.usage查看/proc/stat文件,可获取所有CPU活动的信息,该文件中的所有值都是从系统启动开始累计到当前时刻eg:cat /proc/stat | grep 'cpu '(周期/时间片/jiffies) ##得到的数字相加/HZ(cat /boot/config-`uname -r` | grep ‘^CONFIG_HZ=’,然后乘以10*9就是系统时间(纳秒)。实践中,一般利用CGroup会做下面这些事:●隔离一个进程集合(比如:nginx的所有进程),并限制他们所消费的资源,比如绑定CPU的核。●为这组进程分配其足够使用的内存●为这组进程分配相应的网络带宽和磁盘存储限制●限制访问某些设备(通过设置设备的白名单)eg:cat /boot/config-${uname -r} | grep CGROUP //查看linux是否启用了linux cgroups,“y”代表已经打开linux cgroups功能更多参考Linux cgroup详解二、操作命令2.1 Docker 容器查看首先,管理员对docker做日常维护,需要了解现有环境有多少docker实例,并连接进入docker实例虚拟机进行对应的管理维护;我们执行 docker ps 命令来查看容器名称:docker ps -a或docker ps //显示当前正在运行的容器,输出如下:参数概览:示例:1)显示最后被创建的容器:docker ps -l //相当于 docker ps -n 12) 详细输出,长内容不会截断:docker ps --no-trunc //其中 trunc 是 truncate 的缩写3)只显示容器 ID:$ docker ps -q4)显示容器文件大小:$ docker ps -s //输出结果可获得 2 个数值:一个是容器真实增加的大小,一个是整个容器的虚拟大小(容器虚拟大小 = 容器真实增加大小 + 容器镜像大小)。5)docker ps –format参数可以实现格式化输出自定义列,提供了基于 Go模板 的日志格式化输出辅助功能。-format=“TEMPLATE”Pretty-print containers using a Go template.Valid placeholders:.ID - Container ID.Image - Image ID.Command - Quoted command.CreatedAt - Time when the container was created..RunningFor - Elapsed time since the container was started..Ports - Exposed ports..Status - Container status..Size - Container disk size..Names - Container names..Labels - All labels assigned to the container..Label - Value of a specific label for this container. For example { {.Label “com.docker.swarm.cpu”}}.Mounts - Names of the volumes mounted in this container示例:docker ps --format “table { {.ID}}\t{ {.Names}}\t{ {.Ports}}\t{ {.Status}}” //点号表示当前对象及上下文,直接通过{ {.}}获取当前对象。Go模板可用的可用的占位符如下:eg:docker ps --format “{ {.ID}}: { {.Command}}” // 当使用了 --format 选项,那么 ps 命令只会输出 template 中指定的内容;如果想带上表格列头,需要再 template 中加上 table 指令:docker ps --format “table { {.ID}}: { {.Command}}”6)过滤显示:Filter当容器数量过多,或者想排除干扰容器,可以通过 --filter 或 -f 选项,过滤需要显示的容器。支持的过滤条件非常丰富,包括:使用该选项时,可按以下 3 条准则来进行过滤操作:1选项后跟的都是键值对 key=value(可不带引号),如果有多个过滤条件,就多次使用 filter 选项。例如:eg1:docker ps --filter id=b61c9e9040e0 --filter name=vpm.redis.12相同条件之间的关系是或,不同条件之间的关系是与。例如:eg:dockerps--filtername=vpm.redis.1--filtername=indexserver--filterstatus=running以上过滤条件会找出 name 包含 vpm.redis.1 或 indexserver 并且 status 为 running 的容器。3 id 和 name,支持正则表达式eg:dockerps--filtername=^/bingohuang$ //精确匹配 name 为 bingohuang 的容器。注意,容器实际名称,开头是有一个正斜线 / ,可用dockerinspect看到eg:dockerps--filtername=\.*bingohuang.\* //匹配 name 包含 bingohuang 的容器,和--filtername=bingohuang 一个效果 eg:dockerrm$(dockerps-q--filtername=.*bingohuang.*--filterstatus=exited--filterstatus=dead2/dev/null)//清理名称包含 bingohuang,且状态为 exited 或 dead 的容器其他知识:容器的 metadata在 /var/lib/docker/containers/containerId/ 目录下,其中 containerId-json.log 文件中记录了回写的内容。7)容器与宿主机端口映射查看除docker ps 外,我们还可使用 docker port 可以查看指定 (ID 或者名字)容器的某个确定端口映射到宿主机的端口号。dockerport container_iddockerport container_name2.2 Docker 容器连接连接指定容器,实际是运行一个容器或在已运行的容器中执行命令来连接进入容器,执行:1)运行:docker run -d -P --name runoob training/webapp python app.py //–name 标识来命名容器,为让外部也可以访问这些应用,可以通过 -P(大) 或 -p(小) 参数来指定端口映射。P :是容器内部端口随机映射到主机的高端口。-p : 是容器内部端口绑定到指定的主机端口。dockerrun-d-p5000:5000 training/webapp python app.pydockerrun-d-p127.0.0.1:5001:5000 training/webapp python app.py //-d(–detach)容器后台运行,并且打印容器id验证:docker port 命令可以查看端口的绑定/映射情况,如:docker port CONTAINER [PRIVATE_PORT[/PROTO]]eg:docker port adoring_stonebraker50002)连接:docker exec-it 容器名称/容器ID sh //-i(–interactive)表即使没有连接,也要保持标准输入保持打开状态,一般与 -t (–tty,分配一个伪tty)连用;-t 指示 docker 要创建一个伪 tty 终端,连接容器的标准输入接口,之后用户就可以通过终端进行输入。退出时直接exit即可。当然也可以使用:docker attach 容器名称,区别如下:attach进入终端后,没办法退出而不停止容器,要退出只能输入exit,但这样就将容器停止了,另外一个缺点是,如果多个容器同时attach到相同的容器,在一个窗口中操作的结果,会同步显示到所有窗口。因此我们才用docker exec -it 容器名称 sh;docker attach [container_id]//连接一个正在运行的container实例(即实例必须为start状态,可以多个窗口同时attach一个container实例)eg:dockerexecnginxipa //在容器内执行命令,查看容器的ipdocker start -i//启动一个container并进入交互模式(相当于先start,在attach);使用start是启动已经创建过得container,使用run则通过image开启一个新的container。在容器上做了一堆操作后,比如在ubuntu的基础上安装了一些软件、部署了一些应用之类,希望分发到其它机器,最简单的办法就是把容器重新生成一个新镜像,然后其它人或到新的主机上直接docker pull你的新镜像就可以了。docker commit -a 作者名字 -m 提交原因 -p 容器ID 镜像名称:版本号eg:docker commit-m="你的镜像声明"-a="junn"基于的当前container_ID junn/ubuntu:v2(你要发布的镜像名称:标签)dockerrun-t-ijunn/ubuntu:v2 /bin/bash //用上述镜像启动新的容器即可,这样就无缝迁移到新环境了提交完成后查看执行:docker images附:查看docker容器初始登录后的root密码,因该密码是随机分配的,执行:dockerlogs[container_id]21|grep'User: '|tail-n12.3 容器镜像Docker 使用 Union FS (可将不同物理位置的多个目录内容联合挂载到同一虚拟目录下,支持写时复制(CoW)机制)将不同的数据层结合到一起作为一个镜像工docker读取和写。通常Union FS有2个用途, 一方面可以实现不借助 LVM、RAID 将多个 disk 挂到同一个目录下,另一个更常用的就是将一个只读的分支和一个可写的分支联合在一起供docker容器运行加载读取;UnionFs 把文件系统的每一次修改作为一个个层进行叠加,容器镜像设计中,为了解决各类依赖以及依赖共享,正是利用 UnionFs 实现了镜像分层,再结合 bootfs、rootfs,一层层继承、叠加。启动容器时把相关的层挂载到一个目录,作为容器的根文件系统,这就是容器镜像的原理。当运行容器时,使用的镜像如果在本地中不存在,docker 就会自动从 docker 镜像仓库中下载,默认是从 Docker Hub 公共镜像源下载。1)可以使用 docker images 来列出本地主机上的镜像;使用-q :只显示镜像ID;其中,REPOSITORY:表示镜像的仓库源;TAG:镜像的标签;IMAGE ID:镜像ID;CREATED:镜像创建时间;SIZE:镜像大小;同一仓库源可以有多个 TAG,代表这个仓库源的不同个版本;dockerimages--tree:显示镜像的所有层(layer)'dockerrmiimage ID: 删除一个或多个image2)利用镜像创建容器实例dockerrun-t-iubuntu:14.04 /bin/bash //-i: 交互式操作,-t为实例分配一个终端,bash表进入容器进入shell环境,这也是默认的3)在线查找镜像可以从 Docker Hub网站来搜索镜像,也可以使用 docker search 命令来搜索镜像,–automated :只列出 automated build类型的镜像;-s :列出收藏数不小于指定值的镜像。dockersearch image_name //从Docker HUB上搜索镜像然后找到需要的镜像,然后pull下载即可。4)获取/下载一个新的镜像:当想下载这个镜像,我们可以使用 docker pull 命令来拉取下载它。eg:docker pull ubuntu:13.105)自定义创建镜像当我们从 docker 镜像仓库中下载的镜像不能满足我们的需求时,我们可以通过以下两种方式对镜像进行更改。A、从已经创建的容器中更新镜像,并且提交这个镜像;B、使用 Dockerfile 指令来创建一个新的镜像,Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。镜像选用/配置建议:Docker中,基础镜像(Base Image)的选择对于构建高效、安全的容器至关重要。基础镜像的选择取决于你的应用需求、安全要求以及维护的便利性。我们在构建基础镜像时,并不是越新越好,而是越可信、越精简越好。优先用 Docker 官方镜像仓库中标有“Official”标签的版本,禁用 latest 这类浮动标签,应固定到具体小版本(如 nginx:1.30-alpine),避免自动更新引入未知漏洞。Alpine 镜像比 Ubuntu 小一个数量级,系统包更少,扫描出的 CVE 数量通常下降 80% 以上。用好多阶段构建,只留运行必需的组件,多阶段构建不是为了减体积,而是为了收缩攻击面。构建阶段可以装编译器、测试工具、调试器;运行阶段只复制最终二进制、必要证书和配置文件,其余一概不带,比如不安装 bashcurlapk 等工具,攻击者即使进入容器也难以横向移动或下载恶意载荷。配置镜像时,不要依赖运行时加 --user 参数,而要在 Dockerfile 里固化权限控制,比如:用 RUN adduser -D -u 1001 -s /sbin/nologin appuser 创建无登录能力的低权限用户;紧接 USER 1001 切换身份,确保所有后续指令(包括 COPY 和 CMD)都以该 UID 执行;最好配合 --read-only 启动参数,让根文件系统只读,仅通过 VOLUME 或 --mount 挂载可写路径。2、集成安全自动扫描与签名:构建完立即用 trivy image --severity CRITICAL --exit-code 1 myapp:v1 扫描,发现高危漏洞就中断流水线;推送前用 Cosign 对镜像签名:cosign sign --key cosign.key ghcr.io/org/app:v1;在 Kubernetes Admission Controller 中配置策略,拒绝未签名或签名失效的镜像拉取;签名密钥建议托管在 Azure Key Vault 或 HashiCorp Vault,私钥永不落盘。3、镜像里不能出现密码、密钥、数据库连接串,靠运行时注入,比如:Dockerfile 中删除所有 ENV DB_PASS=xxx 类语句;用 docker build --secret 传入临时密钥(如解密配置用的 AES 密钥);生产环境统一走外部 Secret 管理器:Kubernetes Secrets、Docker Swarm Secrets 或 Vault Agent 注入;若必须打包配置,先用 openssl enc 加密,启动时由 entrypoint 脚本调用 openssl dec 解密到内存或 tmpfs。这里只讨论第2种,使用命令 docker build 从零开始来创建一个新的镜像。首先这需要创建一个Dockerfile 文件,它包含了一组指令,告诉 Docker 该如何构建我们的镜像。Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。如下就是一个dockerfile的示例:FROM centos:6.7##指明创建镜像基于的原始镜像,即你要使用哪个镜像源MAINTAINER Fisher"fisher@sudops.com"RUN /bin/echo'root:123456'|chpasswd //告诉docker启动后在镜像内要执行的命令 RUNuseraddrunoob //RUN后跟的命令行命令等同于,在终端操作的 shell 命令 RUN /bin/echo'runoob:123456'|chpasswd RUN /bin/echo-e"LANG=\"en_US.UTF-8\""/etc/default/local EXPOSE22EXPOSE80CMD /usr/sbin/sshd-D确认后,执行:docker build -t runoob/centos:6.7 -f ./dockerfile //-t :指定要创建的目标镜像名,创建过程会打印到标准输出; -f:读取的镜像配置注意:Dockerfile 的指令每执行一次都会在 docker 上新建一层。所以过多无意义的层,会造成镜像膨胀过大。如是多条语句,可用,只创建一层镜像。镜像配置文件参数:–build-arg=[ ] :设置镜像创建时的变量;–cpu-shares :设置 cpu 使用权重;–cpu-period :限制 CPU CFS周期;–cpu-quota :限制 CPU CFS配额;–cpuset-cpus :指定使用的CPU id;–cpuset-mems :指定使用的内存 id;–disable-content-trust :忽略校验,默认开启;-f :指定要使用的Dockerfile路径;–force-rm :设置镜像过程中删除中间容器;–isolation :使用容器隔离技术;–label=[] :设置镜像使用的元数据;-m :设置内存最大值;–memory-swap :设置Swap的最大值为内存+swap,"-1"表示不限swap;–no-cache :创建镜像的过程不使用缓存;–pull :尝试去更新镜像的新版本;–quiet, -q :安静模式,成功后只输出镜像 ID;–rm :设置镜像成功后删除中间容器;–shm-size :设置/dev/shm的大小,默认值是64M;–ulimit :Ulimit配置。–tag, -t: 镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签。–network: 默认 default。在构建期间设置RUN指令的网络模式eg:docker build-tnginx:v3.//其中最后的.表构建容器镜像的上下文路径,即当前路径下所有文件都会被当做构建镜像的文件,发给docker引擎(Sending build context to Docker daemon)注意:上下文路径下不要放无用的文件,因为会一起打包发送给 docker 引擎,如果文件过多会造成镜像构建过程缓慢。6)设置镜像标签使用 docker tag 命令,为镜像添加一个新的标签。