资源高效利用--虚拟化

  • 多个应用会有冲突,所以往往一台服务器只能运行特定应用。
  • 物理机是奢侈昂贵,且管理复杂的。

虚拟化常见的是基于内核的虚拟机(Kernel-based Virtual Machine,也就是KVM),一台物理机可以被 “切分” 为多台虚拟机,每台 VM 独立运行一个应用,且共享物理机的 CPU、内存和存储,提高了资源利用率。

KVM也允许快速创建、删除、调整虚拟机。弹性伸缩,灵活响应需求。

除此之外,虚拟机在备份、恢复、迁移等场景也具有优势。

为什么还要Docker

Docker本身是也一种虚拟化技术。

在传统KVM中架构是 :“物理机 → Hypervisor(虚拟化层) → 虚拟机(VM) → OS(客户机操作系统) → 应用

每创建一个虚拟机,底层物理机都要模拟出一套完整的虚拟硬件,如CPU、内存、硬盘、网络设备等,且每个虚拟机都要安装一套独立的操作系统。如果是大量虚拟机安装相同的操作系统,就会存在大量相同副本,造成浪费资源。虽然有类似KSM(Kernel SamePage Merging,内核同页合并)这类技术,但是资源浪费是普遍存在的。

Docker的架构是:“物理机 → 宿主机 OS(如 Linux) → 容器引擎(Docker Engine) → 容器 → 应用

核心是不模拟硬件,而是通过宿主机原生的技术,构建一个“隔离的应用运行环境”,容器内部无完整的操作系统,只包含容器应用必要的依赖库。其运行内核与主机共享。

Docker英文含义就是“码头工人”,他的作用与码头工人类似。将应用程序及其依赖环境 “打包” 成标准化的 “容器”(Container),并且管理它们的运输、装卸和运行,确保它们能在任何支持 Docker 的环境中运行。

Docker基本概念

(一)可复用的模具--镜像(Image)

镜像是一个包含某个应用程序的一切的只读模板

其中包括代码、运行环境、库、环境变量、配置文件等。这些是生成镜像时打包好的。

例如镜像文件mysql:8.4.6。其中包含mysql 8.4.6数据库以及其依赖的一切文件。这些文件能保证无论在任何环境,应用程序都不会因为依赖缺失导致无法运行。

(二)程序运行的实例--容器(Container)

容器是从镜像创建的运行实例

镜像是模具,那么容器就是模具生产出来的产品。他可以被启动、停止、删除。每个容器都是相互隔离的,拥有自己的文件系统、网络和进程。

容器启动速度十分快,它不需要虚拟机那样启动完整的操作系统。如果我们运行一个mysql容器,它就只会运行一个mysql服务。

(三)管理镜像的服务--注册中心Registry

注册中心Registry)是一个集中式的服务,用于存储和管理Docker镜像,用户可以在其中存放、共享、分发镜像。它可以是共有的,如目前最大的公共注册中心 Docker Hub、也可以是其他的公有、私人搭建的注册中心。

一个 注册中心Registry)可以包含多个存储镜像的服务:仓库(Repository)

如Docker Hub这个注册中心中,有多个仓库,例如其中有个仓库叫做mysql。而这个叫做mysql的仓库中保存了多个mysql不同版本的镜像文件。

主机上的docker在运行时,有一个docker守护进程。我们通过docker客户端来访问控制这个进程。例如接到客户端的pull请求后,docker守护进程访问注册中心下载对应镜像文件到本地。

另外docker客户端也不一定和守护进程在同一台服务器上,其他机器运行的docker客户端也能访问控制该机器的守护进程(需要额外配置)。

image-czlD.png

Docker重要的核心思想

写时复制(Copy-on-write)

image-CLMn.png

现在在服务器上安装了一个Tomcat 中间件 /opt/tomcat1/,现在我想添加一个实例,最简单的方式就是将文件复制一份。最后我有了两个文件夹 /opt/tomcat1//opt/tomcat2,通过修改配置文件,然后运行它们,我就有了两个实例。这两个实例,除了部分配置文件以外,其他都是一致的,如程序的通用配置、可执行文件等。现在我需要更多的实例,假设需要1000个,就会发现问题。不同实例之间,除了极少部分文件不同外,其余相同的文件太多啦。创建1000个实例,就要完整的占用1000个程序的空间。

为了解决这个问题,docker采用的是 写时复制(Copy-on-write) 的方法,它允许多个实例在初始阶段共享同一份基础数据。只有当某个实例需要对数据进行修改操作时,才会复制出被修改部分的数据,形成一个新的数据层。未被修改的数据部分仍然由所有实例共同共享。这样一来,无论有多少个实例在运行,只要它们没有对数据进行过多的修改,就只需要占用少量的额外存储空间来保存修改后的数据差异部分,从而显著节省了磁盘空间,提高了资源的使用效率。

分层存储(Layered Storage

写时复制(Copy-on-write) 的核心技术就带来了docker高效的秘诀--分层存储(Layered Storage),每一层都包含文件系统的增量变更,且所有层叠加起来构成完整的应用环境。

尝试导入一个镜像(具体操作后续会讲),我们发现类似如下所示结果,文件感觉是一个接一个,一层接一层的导入。

[root@openEuler2203-sp4 Software]# docker load -i onlyoffice_documentserver-amd64.tar
931b7ff0cb6f: Loading layer [==================================================>]  80.41MB/80.41MB
b8700a9ef2b3: Loading layer [==================================================>]  2.031GB/2.031GB
dcd84592f6cf: Loading layer [==================================================>]   7.68kB/7.68kB
190ab7e92db3: Loading layer [==================================================>]  9.216kB/9.216kB
627c427aa845: Loading layer [==================================================>]  28.67kB/28.67kB
52a6be0521c6: Loading layer [==================================================>]  3.072kB/3.072kB
8ab42df6c5ef: Loading layer [==================================================>]  1.892GB/1.892GB
Loaded image: onlyoffice/documentserver:latest

那是因为Docker 镜像采用分层存储结构,这一结构的实现依赖写时复制技术。

在镜像封装过程中,每一层的操作(如安装软件、修改配置)都会创建新的只读层,所有层叠加形成完整镜像。

例如,我想要封装一个基于ubuntu操作系统的nginx应用。

第一步:获取ubuntu:20.04基础镜像(一般情况Docker基础系统镜像都是操作系统厂商提供)。

第二步:容器引擎会读取这个镜像,通过镜像,创建一个容器实例。

容器引擎依据镜像创建容器实例时,首先会先加载镜像的所有只读层。然后,容器启动后会在这些只读层之上生成一个可写层,这是容器运行时用于进行文件修改操作的唯一可写区域。

例如,当在容器内执行安装软件、修改配置文件等操作时,相关的变更都会被记录在这个可写层中。这个时候我安装完成一个nginx,此时相比于基础只读镜像,我改变的任何文件变更都会写入唯一的可写层中。

第三步:执行打包操作 ,容器的可写层固化为新的只读层,这个新的只读层包含容器从原始镜像创建后可写层中发生的所有变更。这个时候产生新的镜像文件。

重复第二步到第三步,每次容器启动后的修改都在可写层,他们每次打包都会变成一层只读层。这个样一层层的结构构成了容器镜像。

如果我再决定修改nginx的配置文件,然后再打包一次。

那么例子中的镜像文件就有三层结构,这三层结构都是只读的。

  • 基础层:ubuntu:20.04(底层基础镜像)
  • 第二层:安装 nginx 的操作(新增 nginx 文件)
  • 第三层:修改 nginx 配置文件(仅包含被修改的配置文件)

当多个容器基于同一镜像启动时,容器引擎会让所有容器共享镜像的只读层。只有当容器需要修改某一层的文件(如修改nginx.conf),写时复制才会触发:将该文件从只读层复制到容器专属的可写层,仅允许当前容器修改副本,其他容器仍使用原始只读数据。这种机制使得 n 个基于同一镜像的容器,仅需存储 1 份镜像数据 + n份修改差异,而非 n份完整镜像,显著降低存储开销。

  • 也不建议修改底层。替换底层往往需要逐层测试,工作量和重做差不多,而且大部分能拿到的镜像都是别人打包好的。众所周知,人的水平参差不齐,高手的作品都是Long看不懂的。
  • 层在同一个镜像生成多个容器的时候可以复用。如果不同镜像有使用到相同的层,创建不同容器时,他们也能复用。比如我这边有个mysql数据库和nginx应用软件,他们的基础镜像都是同一个ubuntu:20.04,那么实际上可以共用一份操作系统镜像基础的层。

可写层具有临时性,容器被删除时,会立即被销毁。 除非打包固化为只读层并形成新的镜像。

持久化数据(Persistent Data

写时复制(Copy-on-write) 的特性造就了分层存储(Layered Storage) 的特点,而 分层存储(Layered Storage) 给docker带来了一个麻烦的问题:如果运行docker,无法更改只读层的数据,同时容器专属的可写层又是临时的。

那么每次我系统运行的数据都要定期打包,让临时的可写层变成永久的只读层来保存我宝贵的数据?

首先,这压根没有可行性,暂且不论打包、重新生成镜像带来的停机时间。想象一下,系统每日打包固化数据(还得祈祷这天中间不会发生服务器故障,重启会丢数据的),一个月都有30层。下次运行系统时,要从基础镜像读取变更到最后一层,这简直是可怕的噩梦。要是运行了一年,看着300多层的启动时间,项目都凉透了。

所以docker提供了一个巧妙的解决方法。

他能把关键数据写到宿主机!这个就是docker数据的持久化,也叫做数据卷(Volume),挂载这个动作叫做绑定挂载(Bind Mount)。挂载的东西可以是一个目录,也可以是一个文件。

这样,哪怕是容器被删除,只要宿主机上面文件还在,就能随时随地的重建应用。

这一点在容器升级的时候也格外重要,比如我持久化了nginx的配置文件 nginx.conf,那么我只需要删除原来的容器 docker rm nginx_old,然后通过新版nginx的镜像生成新的容器,重新将数据挂载到容器指定位置,只要新版本nginx支持原有的配置文件,那么容器我就直接升级啦(实际上准确的说法是换成新的了,原来的已经被销毁)。

可写层是用来保存容器启动后数据变更的!他绝对!绝对!绝对!不是用来存储业务数据的!

而且由于可写层的写时复制特性,如果这个数据来源于只读层,删除操作也会占用存储空间。删除的时候,它只是在可写层中记录下这个删除操作(可以理解为做一个 “删除标记”)。

如果本身就是在可写层中新建的,那么会根据不同的垃圾回收机制进行回收。

对于业务数据,或者经常变动的配置文件,使用容器的数据卷(Volume)绑定挂载(Bind Mount)。

Docker的网络

Docker既然是一种隔离的应用环境,那么网络理所应当也有隔离性。

正如其他常见的虚拟化一样,docker也提供几种特殊的网络配置,这些网络配置在创建容器时为容器指定。

  • none:这种网络模式下,容器没有网络接口,完全与外界隔离。适用于对安全性要求极高,且不需要与其他容器或外部网络通信的场景。
  • host:在这种模式下,容器共享宿主机的网络命名空间。容器可以直接使用宿主机的网络接口,这意味着容器的网络配置与宿主机完全一致。优点是网络性能较好,因为没有额外的网络隔离开销;缺点是容器与宿主机共享网络,可能会带来一定的安全风险,并且容器的端口可能会与宿主机的端口冲突。
  • bridge:这是 Docker 默认的网络模式。Docker 会创建一个名为 docker0 的虚拟网桥,容器通过虚拟网卡连接到这个网桥上。每个容器会被分配一个属于该网桥子网的 IP 地址。容器之间可以通过这个网桥进行通信,同时也可以通过 NAT(网络地址转换)与外部网络通信。这种模式提供了较好的网络隔离性和灵活性,是最常用的网络模式之一。

docker可以创建自定义网络。不同的容器可以加入到同一个自定义网络中,他们彼此之间可以直接访问,docker会自动进行DNS解析。

和常见的KVM相比,docker虚拟化网络有些不同。

在KVM中

  • 桥接模式: 先创建虚拟网桥,虚拟机的虚拟网卡连接到虚拟网桥上,然后虚拟网桥再与宿主机的物理网卡桥接,使得虚拟机如同独立物理机一样接入物理网络,直接与外部网络交互(在网络层面和宿主机平级 ),不是通过 NAT 的方式和外部通信。

Docker容器的性能

Docker 通过 Linux 的 cgroup(控制组)技术,为每个容器创建独立的资源限制组,将容器相关进程纳入组中实现资源控制。

为了更加直观的表述Docker是如何使用cgroup来限制容器性能的,先介绍一下Docker的线程结构。

Docker的线程结构

假设以systemd管理的docker服务为例

系统启动docker进程后:

  1. 首先启动 dockerd 进程(1 个):
    • 作用:Docker 守护进程,负责接收客户端命令、管理镜像 / 容器 / 网络等。
    • 进程ID(PID)启动时创建、其父进程ID(PPID)为1(systemd)。
  2. dockerd 启动后,会自动启动 containerd 进程(1 个):
    • 作用:容器运行时核心,处理容器生命周期的实际操作(创建、启动等)。
    • 进程ID(PID)启动时创建、父进程ID(PPID)为 dockerd进程PID。

客户端执行创建docker容器命令后:

  1. dockerd 接收命令后,调用 containerd 为该容器创建 containerd-shim-runc-v2 进程(1 个):

    • 作用:作为容器与 containerd 之间的 “垫片”,隔离容器进程、收集日志、监控容器状态。
    • 进程ID(PID)启动时创建、父进程ID(PPID)1。实际由 containerd 启动,但为了防止 containerd意外重启、停止,提高容器的稳定性,containerd-shim-runc-v2会脱离 containerd 的直接控制,成为孤儿进程,被 systemd进程(PID=1)收养。
  2. 如果容器配置网络端口转发,dockerd 接收命令后,会创建 docker-proxy 进程 (1 个):

    • 作用:转发宿主机端口与容器端口的流量。
    • 父进程为 dockerd
  3. containerd-shim-runc-v2进程启动应用进程(1 个或多个,取决于镜像):

    • nginx 容器会启动 nginx 主进程
    • 这些进程的父进程是 containerd-shim-runc-v2

Docker 利用 cgroup 实现资源控制

Linux的cgroup技术(简单介绍,不涉及具体使用)

cgroup(Control Groups)是 Linux 内核提供的功能,可对进程组进行资源(如 CPU、内存、磁盘 I/O、网络带宽等 )的限制、统计和隔离 。

它通过虚拟文件系统(VFS) 向用户态提供操作接口。默认情况下,cgroup 会被挂载到 /sys/fs/cgroup目录(可以通过 mount | grep cgroup查看挂载信息)。

这个目录下的所有 “文件” 和 “目录”并非真实存储在磁盘上,而是内核在内存中维护的虚拟节点。当你读写这些文件时,本质上是通过文件系统接口与内核的 cgroup 子系统(如 cpu、memory、blkio 等)进行通信,完成资源限制、统计等配置。

内核对 cgroup 文件系统中的文件有严格的预定义规则

  • 每个 cgroup 子系统(如 cpu、memory)都有一套固定的 “配置文件”(如 cpu 子系统的 cpu.cfs_quota_uscpu.shares等),这些文件的名称、格式、作用都是内核预先定义的;
  • 当用户写入这些文件时(如设置 cpu.cfs_quota_us=50000),内核会解析内容并应用到对应的进程组;
  • 目录的层级结构(如 docker/<容器ID>)则用于划分 “进程组”,内核通过目录路径关联对应的进程 ID(PID),实现资源隔离。

看着蛮复杂的,结合例子,用通俗的话给大伙解释一下

比如我想限制一个容器的性能,启动的时候就是添加参数,这些参数可以按照需求组合

# 限制容器使用1个CPU核心(100%的一个核心)
docker run --cpus=1 [镜像名]

# 限制容器使用50%的CPU(不管有多少核心)
docker run --cpu-quota=50000 [镜像名]

# 设置CPU权重(相对其他容器的优先级)
docker run --cpu-shares=512 [镜像名]

# 限制容器最多使用512MB内存
docker run -m 512m --memory-reservation 256m [镜像名]

# 限制内存并设置交换空间(总限制为1GB)
docker run -m 512m --memory-swap 1g [镜像名]

首先,Linux系统中有类特殊的文件,如 /sys/fs/cgroup这类属于系统内核动态维护的虚拟目录。它长这个样子,下面有很多子目录,如cpu、内存等。

[root@anonymous ~]# ls -l /sys/fs/cgroup/
总用量 0
dr-xr-xr-x.  2 root root  0  8月 17 23:46 blkio
lrwxrwxrwx.  1 root root 11  8月 17 23:46 cpu -> cpu,cpuacct
lrwxrwxrwx.  1 root root 11  8月 17 23:46 cpuacct -> cpu,cpuacct
dr-xr-xr-x.  2 root root  0  8月 17 23:46 cpu,cpuacct
dr-xr-xr-x.  2 root root  0  8月 17 23:46 cpuset
dr-xr-xr-x. 10 root root  0  8月 17 23:46 devices
dr-xr-xr-x.  2 root root  0  8月 17 23:46 freezer
dr-xr-xr-x.  2 root root  0  8月 17 23:46 hugetlb
dr-xr-xr-x. 10 root root  0  8月 17 23:46 memory
lrwxrwxrwx.  1 root root 16  8月 17 23:46 net_cls -> net_cls,net_prio
dr-xr-xr-x.  2 root root  0  8月 17 23:46 net_cls,net_prio
lrwxrwxrwx.  1 root root 16  8月 17 23:46 net_prio -> net_cls,net_prio
dr-xr-xr-x.  2 root root  0  8月 17 23:46 perf_event
dr-xr-xr-x. 10 root root  0  8月 17 23:46 pids
dr-xr-xr-x.  2 root root  0  8月 17 23:46 rdma
dr-xr-xr-x. 11 root root  0  8月 17 23:46 systemd

/sys/fs/cgroup/cpu文件夹举例。 /sys/fs/cgroup/是cpu文件夹的父级目录,/sys/fs/cgroup/cpu/docker是cpu文件夹的子cgroup目录

不需要手动创建这些文件,docker程序会自动维护这些目录以及文件内容

[root@openEuler2203-sp4 cpu]# ls -l /sys/fs/cgroup/cpu/
总用量 0
-rw-r--r--  1 root root 0  8月 19 19:10 cgroup.clone_children
-rw-r--r--  1 root root 0  8月 17 23:46 cgroup.procs
-r--r--r--  1 root root 0  8月 19 19:10 cgroup.sane_behavior
-r--r--r--  1 root root 0  8月 19 19:10 cpuacct.stat
-rw-r--r--  1 root root 0  8月 19 19:10 cpuacct.usage
-r--r--r--  1 root root 0  8月 19 19:10 cpuacct.usage_all
-r--r--r--  1 root root 0  8月 19 19:10 cpuacct.usage_percpu
-r--r--r--  1 root root 0  8月 19 19:10 cpuacct.usage_percpu_sys
-r--r--r--  1 root root 0  8月 19 19:10 cpuacct.usage_percpu_user
-r--r--r--  1 root root 0  8月 19 19:10 cpuacct.usage_sys
-r--r--r--  1 root root 0  8月 19 19:10 cpuacct.usage_user
-rw-r--r--  1 root root 0  8月 19 19:10 cpu.cfs_burst_us
-rw-r--r--  1 root root 0  8月 19 19:10 cpu.cfs_period_us
-rw-r--r--  1 root root 0  8月 17 23:46 cpu.cfs_quota_us
-rw-r--r--  1 root root 0  8月 17 23:46 cpu.rt_period_us
-rw-r--r--  1 root root 0  8月 19 19:10 cpu.rt_runtime_us
-rw-r--r--  1 root root 0  8月 17 23:46 cpu.shares
-r--r--r--  1 root root 0  8月 19 19:10 cpu.stat
-rw-r--r--  1 root root 0  8月 19 19:10 cpu.tag
drwxr-xr-x  2 root root 0  8月 17 23:46 dev-hugepages.mount
drwxr-xr-x  2 root root 0  8月 17 23:46 dev-mqueue.mount
drwxr-xr-x 11 root root 0  8月 17 23:47 docker
-rw-r--r--  1 root root 0  8月 19 19:10 notify_on_release
drwxr-xr-x  2 root root 0  8月 17 23:46 proc-fs-nfsd.mount
-rw-r--r--  1 root root 0  8月 19 19:10 release_agent
drwxr-xr-x  2 root root 0  8月 17 23:46 sys-fs-fuse-connections.mount
drwxr-xr-x  2 root root 0  8月 17 23:46 sys-kernel-config.mount
drwxr-xr-x  2 root root 0  8月 17 23:46 sys-kernel-debug.mount
drwxr-xr-x  2 root root 0  8月 17 23:46 sys-kernel-tracing.mount
drwxr-xr-x 40 root root 0  8月 17 23:46 system.slice
-rw-r--r--  1 root root 0  8月 19 19:10 tasks
drwxr-xr-x  2 root root 0  8月 17 23:46 user.slice

以docker自动创建的文件夹 /sys/fs/cgroup/cpu/docker为例我们发现,docker对于不同的容器,会创建相应的子目录,着一些子目录也有共有的配置文件。

[root@openEuler2203-sp4 docker]# ls -l /sys/fs/cgroup/cpu/docker
总用量 0
drwxr-xr-x 2 root root 0  8月 20 18:54 01e2e738571d9f427de1791b89724c7b9109814b4e28e3f9bcbf44ad80d56696
drwxr-xr-x 2 root root 0  8月 20 18:55 05ca624e381d08a4fb097185e57d85fbad33636c49c7a06e6d5b1c1ab843a168
drwxr-xr-x 2 root root 0  8月 20 18:54 2f1de09dca3da11aca2b9d79857445330f881e55e76e66dee47cd5f7ad23843d
drwxr-xr-x 2 root root 0  8月 20 18:54 795037eb5cefb4cae91bb941a69ef298c6199ae9c80af686004546fa0a5801c1
drwxr-xr-x 2 root root 0  8月 20 18:54 88bd9e1a3a83f678178cc0b383001a64f09475e7bb3edf65ea3bacf899b8e770
drwxr-xr-x 2 root root 0  8月 20 18:54 bae8ff6517116e63adca3c7e0f5a05812b44d9dd957842afef9c7c711320d091
-rw-r--r-- 1 root root 0  8月 19 19:11 cgroup.clone_children
--w------- 1 root root 0  8月 19 19:11 cgroup.kill
-rw-r--r-- 1 root root 0  8月 19 19:11 cgroup.procs
-r--r--r-- 1 root root 0  8月 19 19:11 cpuacct.stat
-rw-r--r-- 1 root root 0  8月 19 19:11 cpuacct.usage
-r--r--r-- 1 root root 0  8月 19 19:11 cpuacct.usage_all
-r--r--r-- 1 root root 0  8月 19 19:11 cpuacct.usage_percpu
-r--r--r-- 1 root root 0  8月 19 19:11 cpuacct.usage_percpu_sys
-r--r--r-- 1 root root 0  8月 19 19:11 cpuacct.usage_percpu_user
-r--r--r-- 1 root root 0  8月 19 19:11 cpuacct.usage_sys
-r--r--r-- 1 root root 0  8月 19 19:11 cpuacct.usage_user
-rw-r--r-- 1 root root 0  8月 19 19:11 cpu.cfs_burst_us
-rw-r--r-- 1 root root 0  8月 19 19:11 cpu.cfs_period_us
-rw-r--r-- 1 root root 0  8月 19 19:11 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0  8月 19 19:11 cpu.qos_level
-rw-r--r-- 1 root root 0  8月 19 19:11 cpu.rt_period_us
-rw-r--r-- 1 root root 0  8月 19 19:11 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0  8月 19 19:11 cpu.shares
-rw-r--r-- 1 root root 0  8月 19 19:11 cpu.smt_expell
-r--r--r-- 1 root root 0  8月 19 19:11 cpu.stat
-rw-r--r-- 1 root root 0  8月 19 19:11 cpu.tag
drwxr-xr-x 2 root root 0  8月 20 18:54 dbc8a3e012d1f33854430b145502070320b5dfbc35dfcecf1d482eb3d2f664bc
drwxr-xr-x 2 root root 0  8月 20 18:54 df2af2621beb94bf6675a79ad90349dde6f1c71489c626ed241bfb4ff39edca3
drwxr-xr-x 2 root root 0  8月 20 18:54 fdfc2fe73001fafa6f7af6013a9a00c19c2a09c90a1b2d99dad75f7017ebb05b
-rw-r--r-- 1 root root 0  8月 19 19:11 notify_on_release
-rw-r--r-- 1 root root 0  8月 19 19:11 tasks

[root@openEuler2203-sp4 293dcd41038230fded5caf2045d369d5a8c1804dc525a9a914d2189f590f4f7e]# ls -l /sys/fs/cgroup/cpu/docker/293dcd41038230fded5caf2045d369d5a8c1804dc525a9a914d2189f590f4f7e
总用量 0
-rw-r--r-- 1 root root 0  8月 25 03:27 cgroup.clone_children
--w------- 1 root root 0  8月 25 03:27 cgroup.kill
-rw-r--r-- 1 root root 0  8月 25 03:21 cgroup.procs
-r--r--r-- 1 root root 0  8月 25 03:22 cpuacct.stat
-rw-r--r-- 1 root root 0  8月 25 03:22 cpuacct.usage
-r--r--r-- 1 root root 0  8月 25 03:27 cpuacct.usage_all
-r--r--r-- 1 root root 0  8月 25 03:22 cpuacct.usage_percpu
-r--r--r-- 1 root root 0  8月 25 03:27 cpuacct.usage_percpu_sys
-r--r--r-- 1 root root 0  8月 25 03:27 cpuacct.usage_percpu_user
-r--r--r-- 1 root root 0  8月 25 03:27 cpuacct.usage_sys
-r--r--r-- 1 root root 0  8月 25 03:27 cpuacct.usage_user
-rw-r--r-- 1 root root 0  8月 25 03:27 cpu.cfs_burst_us
-rw-r--r-- 1 root root 0  8月 25 03:21 cpu.cfs_period_us
-rw-r--r-- 1 root root 0  8月 25 03:21 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0  8月 25 03:27 cpu.qos_level
-rw-r--r-- 1 root root 0  8月 25 03:27 cpu.rt_period_us
-rw-r--r-- 1 root root 0  8月 25 03:27 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0  8月 25 03:27 cpu.shares
-rw-r--r-- 1 root root 0  8月 25 03:27 cpu.smt_expell
-r--r--r-- 1 root root 0  8月 25 03:22 cpu.stat
-rw-r--r-- 1 root root 0  8月 25 03:27 cpu.tag
-rw-r--r-- 1 root root 0  8月 25 03:27 notify_on_release
-rw-r--r-- 1 root root 0  8月 25 03:27 tasks

cgroup.procs中查看适用于这个cgroup规则的进程号,通过进程号能继续查询到被这个cgroup规则限制的进程。

这里我们也可以验证,该docker应用的父进程为 containerd-shim-runc-v2 进程。

[root@openEuler2203-sp4 293dcd41038230fded5caf2045d369d5a8c1804dc525a9a914d2189f590f4f7e]# cat cgroup.procs
1690619
[root@openEuler2203-sp4 293dcd41038230fded5caf2045d369d5a8c1804dc525a9a914d2189f590f4f7e]# ps -ef | grep 1690619
root     1690619 1690598  0 03:21 pts/0    00:00:00 /www/allinssl/allinssl start
root     1694195 1688888  0 03:28 pts/0    00:00:00 grep --color=auto 1690619
[root@openEuler2203-sp4 293dcd41038230fded5caf2045d369d5a8c1804dc525a9a914d2189f590f4f7e]# ps -ef | grep 1690598
root     1690598       1  0 03:21 ?        00:00:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 293dcd41038230fded5caf2045d369d5a8c1804dc525a9a914d2189f590f4f7e -address /var/run/docker/containerd/containerd.sock
root     1690619 1690598  0 03:21 pts/0    00:00:00 /www/allinssl/allinssl start
root     1694286 1688888  0 03:28 pts/0    00:00:00 grep --color=auto 1690598

接着看该文件夹下面的其他文件

cpu.cfs_period_us 表示CPU调度周期,每经过 cpu.cfs_period_us 这么长的时间,就重新分配一次 CPU 使用权。值小了可能存在调度器频繁切换进程,增加系统开销。值大了可能增加业务对CPU的响应延迟。

cpu.cfs_quota_us表示每个 cpu.cfs_period_us 周期内,当前 cgroup 中的进程组最多可使用的 CPU 时间

单CPU系统中,一个周期(如 0.1 秒)内,最多只能提供 0.1 秒的 CPU 时间(100% 使用率),故无论 cpu.cfs_quota_us设置多大,都只有这个上限。

但是多个CPU系统中,CPU多核心并行能力允许进程在同一个周期内使用多个核心的时间,cpu.cfs_quota_us可以大于 cpu.cfs_period_us。比如周期是0.1s,但我给你的配额是0.2s。这个时候,就用到了两个核心。如果配额是0.4秒,相当于用到了4个核心。

可以近似的理解 cpu.cfs_quota_us / cpu.cfs_period_us 的值就是允许程序使用的cpu的数量。其数量可以超过实际cpu的数量,超过部分无效罢了。

在这个例子中:

父目录中设置CPU调度周期为0.1秒,子目录继承了这个参数。 cpu.cfs_period_us 100000

父目录中设置CPU最多时间为-1,这里表示无限制。但是子目录中设置 cpu.cfs_quota_us 200000

由前面的铺垫可以知道,这个docker进程的最大cpu为 调度配额/调度周期 ,也就是 cpu.cfs_quota_us/cpu.cfs_period_us=200000/100000= 2个。

[root@openEuler2203-sp4 293dcd41038230fded5caf2045d369d5a8c1804dc525a9a914d2189f590f4f7e]# cat /sys/fs/cgroup/cpu/cpu.cfs_period_us
100000
[root@openEuler2203-sp4 293dcd41038230fded5caf2045d369d5a8c1804dc525a9a914d2189f590f4f7e]# cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us
-1
[root@openEuler2203-sp4 293dcd41038230fded5caf2045d369d5a8c1804dc525a9a914d2189f590f4f7e]# cat cpu.cfs_period_us
100000
[root@openEuler2203-sp4 293dcd41038230fded5caf2045d369d5a8c1804dc525a9a914d2189f590f4f7e]# cat cpu.cfs_quota_us
200000

docker会自动维护这些参数文件,不建议自己手动修改,尤其是父目录,子目录如果继承了父目录的参数,修改可能导致系统出现问题。

cgroup目录也是系统自行维护的目录,拥有许多特性。

[root@openEuler2203-sp4 cpu]# mkdir /sys/fs/cgroup/cpu/testgroup
[root@openEuler2203-sp4 cpu]# ls -l /sys/fs/cgroup/cpu/testgroup
总用量 0
-rw-r--r-- 1 root root 0  8月 25 03:40 cgroup.clone_children
--w------- 1 root root 0  8月 25 03:40 cgroup.kill
-rw-r--r-- 1 root root 0  8月 25 03:40 cgroup.procs
-r--r--r-- 1 root root 0  8月 25 03:40 cpuacct.stat
-rw-r--r-- 1 root root 0  8月 25 03:40 cpuacct.usage
-r--r--r-- 1 root root 0  8月 25 03:40 cpuacct.usage_all
-r--r--r-- 1 root root 0  8月 25 03:40 cpuacct.usage_percpu
-r--r--r-- 1 root root 0  8月 25 03:40 cpuacct.usage_percpu_sys
-r--r--r-- 1 root root 0  8月 25 03:40 cpuacct.usage_percpu_user
-r--r--r-- 1 root root 0  8月 25 03:40 cpuacct.usage_sys
-r--r--r-- 1 root root 0  8月 25 03:40 cpuacct.usage_user
-rw-r--r-- 1 root root 0  8月 25 03:40 cpu.cfs_burst_us
-rw-r--r-- 1 root root 0  8月 25 03:40 cpu.cfs_period_us
-rw-r--r-- 1 root root 0  8月 25 03:40 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0  8月 25 03:40 cpu.qos_level
-rw-r--r-- 1 root root 0  8月 25 03:40 cpu.rt_period_us
-rw-r--r-- 1 root root 0  8月 25 03:40 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0  8月 25 03:40 cpu.shares
-rw-r--r-- 1 root root 0  8月 25 03:40 cpu.smt_expell
-r--r--r-- 1 root root 0  8月 25 03:40 cpu.stat
-rw-r--r-- 1 root root 0  8月 25 03:40 cpu.tag
-rw-r--r-- 1 root root 0  8月 25 03:40 notify_on_release
-rw-r--r-- 1 root root 0  8月 25 03:40 tasks

rm -r 无法删除这个目录

[root@openEuler2203-sp4 cpu]# rm -rf /sys/fs/cgroup/cpu/testgroup
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpu.cfs_burst_us': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cgroup.procs': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpu.cfs_period_us': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpu.stat': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpuacct.usage_percpu_sys': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpu.tag': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpu.shares': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpuacct.usage_percpu': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpuacct.stat': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpuacct.usage': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpu.smt_expell': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cgroup.kill': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpu.cfs_quota_us': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/tasks': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpuacct.usage_sys': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpu.qos_level': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpuacct.usage_all': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpuacct.usage_percpu_user': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpu.rt_runtime_us': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/notify_on_release': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpu.rt_period_us': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cgroup.clone_children': Operation not permitted
rm: 无法删除 '/sys/fs/cgroup/cpu/testgroup/cpuacct.usage_user': Operation not permitted

虽然里面看起来好像有东西,但是使用rmdir可以删除(前提是这个组没有在使用状态)

[root@openEuler2203-sp4 cpu]# rmdir  /sys/fs/cgroup/cpu/testgroup
[root@openEuler2203-sp4 cpu]# ls -l /sys/fs/cgroup/cpu/testgroup
ls: 无法访问 '/sys/fs/cgroup/cpu/testgroup': No such file or directory

其余关于cgroup的参数配置不一一列举,有兴趣可以自行查阅相关资料。

Docker的基础操作

Docker安装与配置

yum安装

绝大部分发行版Linux系统都有docker,使用yum安装即可

#yum -y install docker

离线安装

官网下载docker安装包,类似 docker-27.1.1.tgzdocker-compose-linux-x86_64


解压安装包
# tar -zxvf /Software/docker-27.1.1.tgz
docker为解压即用,复制进bin目录即可
# cp docker/* /usr/bin
为docker服务创建service
# vi /usr/lib/systemd/system/docker.service

[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=network-online.target docker.socket firewalld.service containerd.service time-set.target
Wants=network-online.target containerd.service
Requires=docker.socket

[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
#ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutStartSec=0
RestartSec=2
Restart=always

# Note that StartLimit* options were moved from "Service" to "Unit" in systemd 229.
# Both the old, and new location are accepted by systemd 229 and up, so using the old location
# to make them work for either version of systemd.
StartLimitBurst=3

# Note that StartLimitInterval was renamed to StartLimitIntervalSec in systemd 230.
# Both the old, and new name are accepted by systemd 230 and up, so using the old name to make
# this option work for either version of systemd.
StartLimitInterval=60s

# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNPROC=infinity
LimitCORE=infinity

# Comment TasksMax if your systemd version does not support it.
# Only systemd 226 and above support this option.
TasksMax=infinity

# set delegate yes so that systemd does not reset the cgroups of docker containers
Delegate=yes

# kill only the docker process, not all processes in the cgroup
KillMode=process
OOMScoreAdjust=-500

[Install]
WantedBy=multi-user.target

在service中引用了socket,创建这个socket。这个不是必须,但是建议使用
使用socket的优点:1.资源优化,无请求时 Docker 服务可停止,减少内存 / CPU 占用。2.权限安全,socket中可以配置用户权限。3.连接稳定,重启docker不会影响已经有的连接。
# vi /usr/lib/systemd/system/docker.socket

[Unit]
Description=Docker Socket for the API

[Socket]
# If /var/run is not implemented as a symlink to /run, you may need to
# specify ListenStream=/var/run/docker.sock instead.
ListenStream=/run/docker.sock
SocketMode=0660
SocketUser=root
SocketGroup=docker

[Install]
WantedBy=sockets.target

创建docker用户组
# groupadd docker

安装docker-compose工具
#cp docker-compose-linux-x86_64 /usr/local/bin/docker-compose
#chmod +x /usr/local/bin/docker-compose
配置docker常用参数
如工作路径,可以选择docker数据卷保存路径等,方便管理
如注册中心源registry-mirrors,这个地址可以为互联网地址或者私有注册中心地址。由于docker官网网络条件较差,如果有互联网镜像下载需求,可以选择国内镜像源,推荐1panel源
# vi  /etc/docker/daemon.json 
{
        "data-root": "/u01/docker",
        "registry-mirrors": [
                "https://docker.1panel.live"
        ]
        }

镜像文件管理

为了方便镜像的传播,管理。docker中镜像名称有着自己的规范

镜像由 命名空间/镜像名称:tag标签 组成

在使用过程中,最规范的下载方式为指定 “仓库地址 + 命名空间 + 镜像名 + 标签”

docker pull registry.example.com/team1/app:v1.0

但是实际使用过程中:

可以省略仓库地址(使用设置的默认仓库)

可以省略命名空间(官方镜像特定版本)

可以省略版本(自动下载最新版本)

下载镜像-docker pull

配置了镜像源地址后,可以直接下载所需镜像

下载一个nginx,默认下载最新版latest

[root@openEuler2203-sp4 ~]# docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
b1badc6e5066: Pull complete
a2da0c0f2353: Pull complete
e5d9bb0b85cc: Pull complete
14a859b5ba24: Pull complete
716cdf61af59: Pull complete
14e422fd20a0: Pull complete
c3741b707ce6: Pull complete
Digest: sha256:33e0bbc7ca9ecf108140af6288c7c9d1ecc77548cbfd3952fd8466a75edefe57
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest  

通过docker images查看镜像

[root@openEuler2203-sp4 ~]# docker images | grep nginx
nginx                                           latest                         ad5708199ec7   6 days ago      192MB

下载一个redis,指定版本号为

[root@openEuler2203-sp4 ~]# docker pull redis:8.2.1
8.2.1: Pulling from library/redis
b1badc6e5066: Already exists
e51f60c71d8e: Pull complete
311d9cf20af5: Pull complete
f22261c94fe4: Pull complete
6f34ca96de3f: Pull complete
4f4fb700ef54: Pull complete
63edde0222ea: Pull complete
Digest: sha256:cc2dfb8f5151da2684b4a09bd04b567f92d07591d91980eb3eca21df07e12760
Status: Downloaded newer image for redis:8.2.1
docker.io/library/redis:8.2.1

通过docker images查看镜像

[root@openEuler2203-sp4 ~]# docker images | grep redis
redis                                           8.2.1                          9d1fe3a9a889   43 hours ago    137MB

如果不知道镜像版本,推荐下载最新镜像使用默认latest标签

如果想要查看镜像所有版本,可以使用search命令如 docker search redis

search命令使用的是docker官方的index服务,我们一般只配置了镜像加速,故建议直接去官网查询版本https://hub.docker.com/search

导入镜像-docker load -i

可以使用命令导入打包好的镜像文件

[root@openEuler2203-sp4 Software]#  docker load -i onlyoffice_documentserver-amd64.tar
931b7ff0cb6f: Loading layer [==================================================>]  80.41MB/80.41MB
b8700a9ef2b3: Loading layer [==================================================>]  2.031GB/2.031GB
dcd84592f6cf: Loading layer [==================================================>]   7.68kB/7.68kB
190ab7e92db3: Loading layer [==================================================>]  9.216kB/9.216kB
627c427aa845: Loading layer [==================================================>]  28.67kB/28.67kB
52a6be0521c6: Loading layer [==================================================>]  3.072kB/3.072kB
8ab42df6c5ef: Loading layer [==================================================>]  1.892GB/1.892GB
Loaded image: onlyoffice/documentserver:latest

其他常见操作

查看所有镜像 docker image ls

查看镜像软件版本 docker image inspect IMAGE ID | grep -i version

删除镜像 docker image rm <IMAGE ID>

导出镜像(导出镜像、保留镜像分层结构) docker save -o my_image_backup.tar my_image:latest

导出容器快照(打包的是容器最终的所有文件和目录,合并为一层)docker export -o my\_container\_snapshot.tar my\_container

清理虚悬镜像(无标签镜像或者未命名中间层镜像) docker image prune

查看docker空间使用情况 docker system df

清理未使用数据卷 docker volume prune

清理未使用资源 docker system prune

启动一个docker应用-docker run

接下来以一个redis为例,介绍如何启动一个docker应用

下载镜像

[root@openEuler2203-sp4 cpu]# docker pull redis:8.2.1
8.2.1: Pulling from library/redis
b1badc6e5066: Already exists
e51f60c71d8e: Pull complete
311d9cf20af5: Pull complete
f22261c94fe4: Pull complete
6f34ca96de3f: Pull complete
4f4fb700ef54: Pull complete
63edde0222ea: Pull complete
Digest: sha256:cc2dfb8f5151da2684b4a09bd04b567f92d07591d91980eb3eca21df07e12760
Status: Downloaded newer image for redis:8.2.1
docker.io/library/redis:8.2.1

对于redis应用,我们一般关注三点,一是数据的持久化,一个是redis的配置文件,一个是redis的端口。注意:不同的应用对数据持久化要求不同、对端口暴露需求不同,有不同处理方式。

首先创建一些必要的路径和文件

[root@openEuler2203-sp4 cpu]# mkdir -p  /u01/redis/data
[root@openEuler2203-sp4 data]# echo "bind 0.0.0.0
> port 6379
> protected-mode yes
> requirepass your_redis_password
> dir /data
> appendonly yes
> maxmemory 1gb
> maxmemory-policy allkeys-lru
> timeout 300" > /u01/redis/redis.conf
[root@openEuler2203-sp4 data]# cat /u01/redis/redis.conf
bind 0.0.0.0
port 6379
protected-mode yes
requirepass your_redis_password
dir /data
appendonly yes
maxmemory 1gb
maxmemory-policy allkeys-lru
timeout 300

启动redis docker镜像

docker run -d \
  -p 6379:6379 \
  -v /u01/redis/data:/data \
  -v /u01/redis/redis.conf:/etc/redis/redis.conf \
  --name my-redis \
  redis:8.2.1 redis-server /etc/redis/redis.conf 

-d:后台运行,不占用前端窗口

-p:宿主机端口:redis容器内部端口。访问宿主机的6379端口,就会转发到容器内部的6379端口。

-v:宿主机目录:redis容器内目录。容器内部的/data目录中文件会写入宿主机/u01/redis/data目录中。这些数据是持久化到宿主机,容器被删除也不会影响这部分数据。

-v:宿主机文件:redis容器内文件。宿主机文件/u01/redis/redis.conf替换容器内部/etc/redis/redis.conf。

--name 这个容器命名为my-redis,方便管理

redis:8.2.1 :容器使用的镜像、可以用镜像id取代

redis-server /etc/redis/redis.conf :容器启动后的命令。这里,我们启动容器后,自动执行这个命令可以启动redis

docker logs -f 查看my-redis容器启动日志(这里自定义的容器名my-redis也可以用容器id取代)

[root@openEuler2203-sp4 redis]# docker logs -f my-redis
......
1:M 24 Aug 2025 21:14:49.484 * <timeseries> ]
1:M 24 Aug 2025 21:14:49.484 * <timeseries> Detected redis oss
1:M 24 Aug 2025 21:14:49.484 * <timeseries> Enabled diskless replication
1:M 24 Aug 2025 21:14:49.484 * Module 'timeseries' loaded from /usr/local/lib/redis/modules//redistimeseries.so
1:M 24 Aug 2025 21:14:49.484 * <ReJSON> Created new data type 'ReJSON-RL'
1:M 24 Aug 2025 21:14:49.484 * <ReJSON> version: 80200 git sha: unknown branch: unknown
1:M 24 Aug 2025 21:14:49.484 * <ReJSON> Exported RedisJSON_V1 API
1:M 24 Aug 2025 21:14:49.484 * <ReJSON> Exported RedisJSON_V2 API
1:M 24 Aug 2025 21:14:49.484 * <ReJSON> Exported RedisJSON_V3 API
1:M 24 Aug 2025 21:14:49.484 * <ReJSON> Exported RedisJSON_V4 API
1:M 24 Aug 2025 21:14:49.484 * <ReJSON> Exported RedisJSON_V5 API
1:M 24 Aug 2025 21:14:49.484 * <ReJSON> Enabled diskless replication
1:M 24 Aug 2025 21:14:49.484 * <ReJSON> Initialized shared string cache, thread safe: false.
1:M 24 Aug 2025 21:14:49.484 * Module 'ReJSON' loaded from /usr/local/lib/redis/modules//rejson.so
1:M 24 Aug 2025 21:14:49.484 * <search> Acquired RedisJSON_V5 API
1:M 24 Aug 2025 21:14:49.485 * Server initialized
1:M 24 Aug 2025 21:14:49.515 * Creating AOF base file appendonly.aof.1.base.rdb on server start
1:M 24 Aug 2025 21:14:49.541 * Creating AOF incr file appendonly.aof.1.incr.aof on server start
1:M 24 Aug 2025 21:14:49.541 * Ready to accept connections tcp

查看宿主机文件

[root@openEuler2203-sp4 redis]# tree /u01/redis/
/u01/redis/
├── data
│   └── appendonlydir
│       ├── appendonly.aof.1.base.rdb
│       ├── appendonly.aof.1.incr.aof
│       └── appendonly.aof.manifest
└── redis.conf

操作容器内部

docker exec [选项] 容器ID或名称 要执行的命令
docker exec -it my-redis /bin/bash

exec :容器内部执行命令

-i:保存输入流打开

-t:分配一个伪终端

/bin/bash:容器内部系统执行这个命令

该命令的作用是,在容器内部执行/bin/bash命令,并分配一个伪终端作为交互界面,并保留输入功能。

从效果上来讲,好像是我们进入到容器内部,然后执行操作。

[root@openEuler2203-sp4 redis]# docker exec -it my-redis /bin/bash
root@1cbd33f1806c:/data#

查看系统版本,我们发现,这个容器打包的基础镜像为Debian 12系统

root@1cbd33f1806c:/bin# cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

基础镜像往往很精简。容器打包者认为不需要的功能或者命令都会被精简掉。

root@1cbd33f1806c:/bin# ssh
bash: ssh: command not found
root@1cbd33f1806c:/bin# vim
bash: vim: command not found

查看容器 /data目录我们发现实际上和容器挂载到宿主机目录是一致的。对于容器来讲,任何写入该目录的文件都会持久化到宿主机数据卷中

root@1cbd33f1806c:/data# ls -l appendonlydir/*
-rw------- 1 redis redis  88 Aug 24 21:14 appendonlydir/appendonly.aof.1.base.rdb
-rw------- 1 redis redis   0 Aug 24 21:14 appendonlydir/appendonly.aof.1.incr.aof
-rw------- 1 redis redis 102 Aug 24 21:14 appendonlydir/appendonly.aof.manifest

docker自定义网络用法,以下为创建一个名为redis-net的自定义网络

docker network create redis-net

docker启动时,需要通过 --network指定这个网络

docker run -d --network redis-net redis:8.2.1 ......

如果其他容器也同时加入这个网络的话,那么他们相互之间就可以直接通过my-redis:6379这种方式访问(无需ip)

docker run还有很多其他参数,下面简单介绍一些:

--restart always:自动重启,且启动失败,总是重启

-m:限制内存如 -m 1g

--cpus:限制cpu 如--cpus 2

容器编排工具docker-compose

根据上一章节的内容,我们成功启动了一个docker应用。但是实际情况往往是多组件协同工作。单纯使用docker run会带来以下问题。

  • 步骤繁琐:我们要先启动数据库、在启动redis、最后启动应用。我们需要逐一执行这些命令,顺序有严格要求且不能出错。停止docker执行多个操作。当容器数量上升到5个、10个乃至更多,手动维护渐渐变为不可能。
  • 配置分散:所有的配置都分散在一条条docker run命令中,没有统一的配置文件。团队协作中,别人无法快速找到所要的参数,如数据库密码、环境变量等。移植性差,需要保存所有部署命令,新环境无法快速部署,容易出错,挂载数据库错误或者映射端口错误。
  • 依赖失控:应用可能在数据库还未初始化完成就启动了,造成应用异常。有时候还要写等待脚本。

docker-compose工具是docker官方提供的一个解决多个容器效率,一致性,可维护性的工具。

  • 配置集中化:所有涉及容器参数均写在一个文件中,docker-compose.ymlYAML格式,结构清晰
  • 操作一键化:启动 docker compose up -d (-d后台运行)、停止 docker compose down、查看状态 docker compose ps、重启所有 docker compose restart
  • 自动处理依赖:定义 depends_on: [db, cache]等参数,docker compose会先启动指定docker容器,默认是等待容器状态变为running,更为精细的控制可以使用健康检查等功能判断。
  • 网络和数据卷:定义网络和数据卷即可创建,无需手动执行创建命令。且容器可以定义服务名,直接通过服务名方式通讯。
# docker-compose.yml(YAML格式,结构清晰),指定配置文件版本为3.8
version: '3.8' 

# 定义所有服务(每个服务对应一个容器)
services:
  # 1. MySQL服务(命名为db)
  db:
    image: mysql:8.4.6
    container_name: app-db
    environment:  # 环境变量(替代docker run -e)
      MYSQL_ROOT_PASSWORD: 123456
      MYSQL_DATABASE: app_db
    volumes:  # 数据卷(替代docker run -v)
      - db-data:/var/lib/mysql
    networks:  # 网络(替代docker run --network)
      - app-net
    restart: always  # 容器异常时自动重启

  # 2. Redis服务(命名为cache)
  cache:
    image: redis:8.2.1
    container_name: app-cache
    command: --requirepass 123456  # 启动参数(替代docker run后的命令)
    environment:
      REDIS_PASSWORD: 123456
    networks:
      - app-net
    restart: always

  # 3. 后端服务(命名为backend,依赖db和cache)
  backend:
    image: my-backend:1.0
    container_name: app-backend
    ports:  # 端口映射(替代docker run -p)
      - "8080:8080"
    environment:
      DB_HOST: db  # 直接用服务名(db)作为地址,无需记IP
      DB_PASSWORD: 123456
      REDIS_HOST: cache
    networks:
      - app-net
    depends_on:  # 定义依赖关系:backend在db和cache启动后再启动
      - db
      - cache
    restart: always

# 定义数据卷(替代docker volume create)
volumes:
  db-data:

# 定义网络(替代docker network create)
networks:
  app-net:

docker-compose看似功能强大,但是只适用于单机多容器编排,适合在单台主机上定义和运行多个关联的容器。(如一个前端容器+一个后端容器+一个数据库容器)

随着系统的庞大,大规模容器的调度、高可用、自愈、扩缩容等复杂问题凸显出来。这个时候有必要使用更复杂的机制 Kubernetes ,中文取谐音 k8s

k8s具有高可用、高扩展性、大规模容器部署、微服务架构、自动化运维(自动扩缩容、故障自愈、滚动更新)、多资源管理等适合生产环境的功能。

对于docker应用的生产环境中,建议使用k8s。

Docker镜像制作

由前面介绍的Docker分层存储和持久化策略可知,制作镜像好像不是一个难事,好像只需要把我们对容器的更改保存下来便行。

通过运行一个docker基础镜像。然后通过exec xxx /bin/bash的方式操作这个容器,在容器内部安装我们需要的软件工具等

最后通过 docker export 或者 docker commit将这个容器导出为一个镜像。

在这里我想强调一个基础知识,分别是三个命令,他们是有区别的,且容易混淆。docker exportdocker commitdocker save

docker export:直接导出文件系统快照、无docker元数据,会丢失容器启动命令、环境变量、挂载卷、构建历史等。作用就是备份、迁移到非docker环境、创建基础镜像等。

docker commit:将容器的修改固化为镜像,后续使用这个新镜像创建容器时,会直接包含这些修改,无需重复配置,保留容器元数据部分配置(端口映射、挂载卷等无法保留)。但是缺点包含冗余数据,体积过大。另外是“黑箱镜像”,他无法清晰的查到构建的步骤(如装了哪些软件、执行了哪些命令)。对于长期维护的镜像版本极其不友好。

docker save:镜像的完整信息(包括所有分层数据和元数据)打包成 tar 包,注意这里不包含任何容器的数据,单纯的导出镜像。作用是镜像文件的迁移,在互联网机器上下载的镜像可以通过这种方式打包传输到内网环境。tar文件在目标机器上通过 docker load命令导入。

但是无论最后是 docker export还是 docker commit,我们都无法规范的呈现容器制作的过程。这种制作方式,依赖外部文档的记录,在镜像流通之后,很难对打包过程追溯。一旦有修改变动,就会变得极其繁琐易错。

Dockerfile是一种代码化的镜像构建流程,是自动化构建的基础,所有构建步骤都以指令的形式明确记录。在问题追溯、版本控制、提升构建效率有显著优点。是团队协作、标准化、高效化的最佳实践。

另外可以和docker分层存储机制深度结合,把不常变化的步骤(如安装系统工具、依赖库)写在 Dockerfile 前面,常变化的步骤(如复制代码、配置文件)写在后面。这样修改代码时,前面的层能复用缓存,大幅加快构建速度。

除非特殊情况,我们构建docker镜像的唯一方法是使用dockerfile方式。

**CI(Continuous Integration,持续集成)和****CD(Continuous Delivery/Deployment,持续交付 / 部署)**对于生产至关重要。

下面以openEuler操作系统为例,打包一个redis服务。

准备基础镜像

虽然构建docker镜像过程中引用的docker基础镜像会自动下载。但是为了避免网络等方面的问题,加快构建速度。我们先准备docker的基础镜像。

[root@openEuler2203-sp4 jdk-image]# docker pull openeuler/openeuler:22.03-lts-sp4
22.03-lts-sp4: Pulling from openeuler/openeuler
a2b55a6250d0: Pull complete
8982dc177e71: Pull complete
Digest: sha256:5be64305cc73d23e4e370d46a7a73d55669474b37a91806d2c09c33ca2b64569
Status: Downloaded newer image for openeuler/openeuler:22.03-lts-sp4
docker.io/openeuler/openeuler:22.03-lts-sp4

基础镜像只提供最小可用的运行环境:精简内核 + 基础命令工具 + 包管理器

为了方便打包管理等操作,建议容器的基础镜像和打包机器的操作系统保持一致,这样很多包通用也不会搞混。

应用程序需要额外依赖的包需要单独准备,我们给打包机配置yum源。

准备redis编译需要的包

准备编译环境包

[root@openEuler2203-sp4 redis-rpm-workspace]# yumdownloader --resolve \
>   --destdir=/u01/redis-rpm-workspace/rpms \
>   gcc \
>   g++ \
>   make \
>   glibc-devel \
>   libstdc++-devel \
>   openssl-devel \
>   tcl \
>   guile \
>   zlib-devel \
>   libtool \
>   libtool-ltdl \
>   gc \
>   binutils \
>   cpp \
>   glibc-headers \
>   kernel-headers \
>   libmpc \
>   mpfr \
>   gmp \
>   diffutils \
>   sed \
>   grep
Last metadata expiration check: 0:05:06 ago on 2025年08月26日 星期二 20时23分32秒.
[root@openEuler2203-sp4 redis-rpm-workspace]#  ls -l /u01/redis-rpm-workspace/rpms
总用量 72592
-r--r--r-- 1 root root  5681197  8月 26 20:28 binutils-2.37-28.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root  9839481  8月 26 20:28 cpp-10.3.1-62.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root   326801  8月 26 20:28 diffutils-3.8-3.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root   242921  8月 26 20:28 gc-8.0.6-4.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root 30963241  8月 26 20:28 gcc-10.3.1-62.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root 11652357  8月 26 20:28 gcc-c++-10.3.1-62.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root  1906589  8月 26 20:28 glibc-devel-2.34-152.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root   268737  8月 26 20:28 gmp-6.2.1-3.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root   303169  8月 26 20:28 grep-3.7-9.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root  3521941  8月 26 20:28 guile-2.0.14-19.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root  2614937  8月 26 20:28 kernel-headers-5.10.0-216.0.0.115.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root    55657  8月 26 20:28 libmpc-1.2.0-7.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root  2323101  8月 26 20:28 libstdc++-devel-10.3.1-62.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root   607233  8月 26 20:28 libtool-2.4.7-2.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root    29337  8月 26 20:28 libtool-ltdl-2.4.7-2.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root   328097  8月 26 20:28 make-4.3-4.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root   338341  8月 26 20:28 mpfr-4.1.0-3.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root  1848017  8月 26 20:28 openssl-devel-1.1.1wa-7.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root   165693  8月 26 20:28 sed-4.8-6.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root  1178569  8月 26 20:28 tcl-8.6.12-4.oe2203sp4.x86_64.rpm
-r--r--r-- 1 root root    88005  8月 26 20:28 zlib-devel-1.2.11-26.oe2203sp4.x86_64.rpm

准备redis源码包(也可以到redis官网下载后上次)

[root@openEuler2203-sp4 rpms]# mkdir -p  /u01/redis-rpm-workspace/redis-src/
[root@openEuler2203-sp4 rpms]# cd /u01/redis-rpm-workspace/redis-src/
[root@openEuler2203-sp4 redis-src]# wget https://download.redis.io/releases/redis-6.2.14.tar.gz
--2025-08-26 19:16:09--  https://download.redis.io/releases/redis-6.2.14.tar.gz
正在解析主机 download.redis.io (download.redis.io)... 104.18.27.34, 104.18.26.34, 2606:4700::6812:1a22, ...
正在连接 download.redis.io (download.redis.io)|104.18.27.34|:443... 已连接。
已发出 HTTP 请求,正在等待回应... 200 OK
长度:2496149 (2.4M) [application/octet-stream]
正在保存至: “redis-6.2.14.tar.gz”redis-6.2.14.tar.gz          100%[============================================>]   2.38M  1.36MB/s  用时 1.8s2025-08-26 19:16:12 (1.36 MB/s) - 已保存 “redis-6.2.14.tar.gz” [2496149/2496149])[root@openEuler2203-sp4 redis-src]# ls -l
总用量 2440
-rw-r--r-- 1 root root 2496149 10月 18  2023 redis-6.2.14.tar.gz

创建redis基础文件等(虽然后续会通过-v挂载文件或者目录,但是文件需最好提前创建好)

cd /u01/redis-rpm-workspace/
cat > redis.conf << 'EOF'
# 绑定地址(允许所有IP访问)
bind 0.0.0.0
# 端口
port 6379
# 禁止后台运行(容器中由容器管理进程)
daemonize no
# 数据目录(对应/u01/redis/data)
dir /u01/redis/data
# 日志文件路径
logfile "/u01/redis/logs/redis.log"
# 最大内存限制
maxmemory 1gb
maxmemory-policy allkeys-lru
# 持久化配置(RDB)
save 900 1
save 300 10
save 60 10000
EOF

创建构建镜像的Dockerfile

cat > Dockerfile << 'EOF'
# 第一阶段:编译阶段
FROM openeuler/openeuler:22.03-lts-sp4 AS builder

# 定义变量
ENV REDIS_VERSION=6.2.14
ENV REDIS_SRC=/tmp/redis-src
ENV REDIS_INSTALL=/tmp/redis-install

# 复制编译依赖包
COPY rpms /tmp/rpms

# 安装编译依赖
RUN rpm -ivh /tmp/rpms/*.rpm --nodeps --force \
    && rm -rf /tmp/rpms

# 复制并编译Redis源码
COPY redis-src/redis-${REDIS_VERSION}.tar.gz ${REDIS_SRC}/
RUN mkdir -p ${REDIS_SRC} ${REDIS_INSTALL} \
    && cd ${REDIS_SRC} \
    && tar -zxf redis-${REDIS_VERSION}.tar.gz \
    && cd redis-${REDIS_VERSION} \
    && make \
    && make install PREFIX=${REDIS_INSTALL} \
    && rm -rf ${REDIS_SRC}


# 第二阶段:运行阶段
FROM openeuler/openeuler:22.03-lts-sp4

# 定义Redis安装路径
ENV REDIS_BASE=/u01/redis
ENV REDIS_DATA=${REDIS_BASE}/data
ENV REDIS_LOG=${REDIS_BASE}/logs
ENV REDIS_CONF=${REDIS_BASE}/conf

# 创建目录并设置权限
RUN mkdir -p ${REDIS_BASE} ${REDIS_DATA} ${REDIS_LOG} ${REDIS_CONF} \
    && groupadd -r redis && useradd -r -g redis redis

# 从编译阶段复制Redis产物
COPY --from=builder /tmp/redis-install/ ${REDIS_BASE}/
RUN ln -s ${REDIS_BASE}/bin/redis-server /usr/bin/ \
    && ln -s ${REDIS_BASE}/bin/redis-cli /usr/bin/

# 复制配置文件
COPY redis.conf ${REDIS_CONF}/redis.conf

# 配置redis用户权限
RUN chown -R redis:redis /u01

# 暴露端口
EXPOSE 6379

# 切换到非root用户
USER redis

# 启动命令
CMD ["redis-server", "/u01/redis/conf/redis.conf"]
EOF

Dockerfile说明

  1. redis为编译安装,编译过程中需要安装编译环境。使用基础镜像创建临时容器作为编译环境,编译出redis程序。后续在构建实际使用的镜像时再直接引入编译后的redis程序。可以降低镜像大小,摒除冗余功能。
  2. ENV指令可以定义环境变量。
  3. FROM 指令指定基础镜像。
  4. COPY指令可以将主机文件复制进容器。
  5. RUN指令构建镜像时在容器内部执行命令。
  6. EXPOSE在标准docker构建中,建议使用此参数定义需要暴露的端口,但这不是必须的。
  7. USER指令,后续命令全部用USER后指定的用户执行。这个一般放倒最后启动应用之前。一旦切换了用户,后续指令全局生效。如果后续还需要root操作,建议不使用 USER,使用 CMD ["su", "redis", "-c", "redis-server /u01/redis/conf/redis.conf"]
  8. CMD指令,启动镜像时执行的指令。

Docker构建镜像

. 就是告诉 Docker "从当前目录开始查找 Dockerfile 并使用该目录下的文件进行镜像构建"

[root@openEuler2203-sp4 redis-rpm-workspace]#  docker build -t openeuler-redis:6.2.14 .
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
            Install the buildx component to build images with BuildKit:
            https://docs.docker.com/go/buildx/

Sending build context to Docker daemon   76.8MB
Step 1/20 : FROM openeuler/openeuler:22.03-lts-sp4 AS builder
 ---> bf078ac3f759
Step 2/20 : ENV REDIS_VERSION=6.2.14
 ---> Using cache
 ---> 0ec0e8fb68a8
Step 3/20 : ENV REDIS_SRC=/tmp/redis-src
 ---> Using cache
 ---> ffcaa8f73126
Step 4/20 : ENV REDIS_INSTALL=/tmp/redis-install
 ---> Using cache
 ---> 403a9c337e9b
Step 5/20 : COPY rpms /tmp/rpms
 ---> af5bb7884117
Step 6/20 : RUN rpm -ivh /tmp/rpms/*.rpm --nodeps --force     && rm -rf /tmp/rpms
 ---> Running in 2ab46c496eae
warning: /tmp/rpms/binutils-2.37-28.oe2203sp4.x86_64.rpm: Header V4 RSA/SHA256 Signature, key ID b675600b: NOKEY
Verifying...                          ########################################
Preparing...                          ########################################
Updating / installing...
gmp-1:6.2.1-3.oe2203sp4               ########################################
mpfr-4.1.0-3.oe2203sp4                ########################################
libmpc-1.2.0-7.oe2203sp4              ########################################
cpp-10.3.1-62.oe2203sp4               ########################################
zlib-devel-1.2.11-26.oe2203sp4        ########################################
sed-4.8-6.oe2203sp4                   ########################################
libtool-ltdl-2.4.7-2.oe2203sp4        ########################################
libstdc++-devel-10.3.1-62.oe2203sp4   ########################################
kernel-headers-5.10.0-216.0.0.115.oe22########################################
glibc-devel-2.34-152.oe2203sp4        ########################################
gc-8.0.6-4.oe2203sp4                  ########################################
guile-5:2.0.14-19.oe2203sp4           ########################################
binutils-2.37-28.oe2203sp4            ########################################
gcc-10.3.1-62.oe2203sp4               ########################################
gcc-c++-10.3.1-62.oe2203sp4           ########################################
libtool-2.4.7-2.oe2203sp4             ########################################
make-1:4.3-4.oe2203sp4                ########################################
openssl-devel-1:1.1.1wa-7.oe2203sp4   ########################################
tcl-1:8.6.12-4.oe2203sp4              ########################################
grep-3.7-9.oe2203sp4                  ########################################
diffutils-3.8-3.oe2203sp4             ########################################
 ---> Removed intermediate container 2ab46c496eae
 ---> 2a4fa43bda2a
Step 7/20 : COPY redis-src/redis-${REDIS_VERSION}.tar.gz ${REDIS_SRC}/
 ---> 9cf5a1399dda
Step 8/20 : RUN mkdir -p ${REDIS_SRC} ${REDIS_INSTALL}     && cd ${REDIS_SRC}     && tar -zxf redis-${REDIS_VERSION}.tar.gz     && cd redis-${REDIS_VERSION}     && make     && make install PREFIX=${REDIS_INSTALL}     && rm -rf ${REDIS_SRC}
 ---> Running in c6168ed29f79
cd src && make all
make[1]: Entering directory '/tmp/redis-src/redis-6.2.14/src'
    CC Makefile.dep
......省略编译步骤
make[1]: Leaving directory '/tmp/redis-src/redis-6.2.14/src'
 ---> Removed intermediate container c6168ed29f79
 ---> 8176116f62f2
Step 9/20 : FROM openeuler/openeuler:22.03-lts-sp4
 ---> bf078ac3f759
Step 10/20 : ENV REDIS_BASE=/u01/redis
 ---> Running in 27fe5576bbf7
 ---> Removed intermediate container 27fe5576bbf7
 ---> 481ac6e47c03
Step 11/20 : ENV REDIS_DATA=${REDIS_BASE}/data
 ---> Running in ce75077782fe
 ---> Removed intermediate container ce75077782fe
 ---> ac63d8a76bdb
Step 12/20 : ENV REDIS_LOG=${REDIS_BASE}/logs
 ---> Running in 7d73395d3d14
 ---> Removed intermediate container 7d73395d3d14
 ---> 622ebe74deee
Step 13/20 : ENV REDIS_CONF=${REDIS_BASE}/conf
 ---> Running in ac6de7cd7116
 ---> Removed intermediate container ac6de7cd7116
 ---> c9bfcae759a3
Step 14/20 : RUN mkdir -p ${REDIS_BASE} ${REDIS_DATA} ${REDIS_LOG} ${REDIS_CONF}     && groupadd -r redis && useradd -r -g redis redis     && chown -R redis:redis /u01
 ---> Running in 4ee2cadec5c4
 ---> Removed intermediate container 4ee2cadec5c4
 ---> a66371286606
Step 15/20 : COPY --from=builder /tmp/redis-install/ ${REDIS_BASE}/
 ---> 6cb8a3ba6ab5
Step 16/20 : RUN ln -s ${REDIS_BASE}/bin/redis-server /usr/bin/     && ln -s ${REDIS_BASE}/bin/redis-cli /usr/bin/
 ---> Running in b81e78ecf1f6
 ---> Removed intermediate container b81e78ecf1f6
 ---> c4641af4d75b
Step 17/20 : COPY redis.conf ${REDIS_CONF}/redis.conf
 ---> ea803c278ad5
Step 18/20 : EXPOSE 6379
 ---> Running in 3ff10e495c7c
 ---> Removed intermediate container 3ff10e495c7c
 ---> 218d827dd16c
Step 19/20 : USER redis
 ---> Running in c68300e9d2cd
 ---> Removed intermediate container c68300e9d2cd
 ---> 1adb815a1c67
Step 20/20 : CMD ["redis-server", "/u01/redis/conf/redis.conf"]
 ---> Running in 56b6af9f79a8
 ---> Removed intermediate container 56b6af9f79a8
 ---> 5c15fa6321bc
Successfully built 5c15fa6321bc
Successfully tagged openeuler-redis:6.2.14

查看编译的镜像文件

[root@openEuler2203-sp4 redis-rpm-workspace]# docker images | grep openeuler-redis
openeuler-redis                                 6.2.14                         5c15fa6321bc   4 minutes ago    195MB

验证镜像文件

  • 创建工作目录,测试镜像文件
    [root@openEuler2203-sp4 redis-rpm-workspace]# mkdir -p /u01/redis-test/{conf,data,logs}
    [root@openEuler2203-sp4 u01]# chmod -R 777 /u01/redis-test
    [root@openEuler2203-sp4 redis-rpm-workspace]# cd /u01/redis-test
    

先从容器中拷贝出配置文件方便修改

[root@openEuler2203-sp4 redis-test]# docker run --rm openeuler-redis:6.2.14 cat /u01/redis/conf/redis.conf > /u01/redis-test/conf/redis.conf

运行容器

[root@openEuler2203-sp4 redis-test]# docker run -d \
> --name redis-test \
> -p 6379:6379 \
> -v /u01/redis-test/conf/redis.conf:/u01/redis/conf/redis.conf \
> -v /u01/redis-test/data:/u01/redis/data \
> -v /u01/redis-test/logs:/u01/redis/logs \
> openeuler-redis:6.2.14
e9a0cb6485d0de3fdb49bd1238c775768903d080a3b71c5df2b7edee6184f9b8

Docker数据卷权限说明

之前我们的例子中,测试redis-test时,将宿主机目录权限改为了777。这个在实际生产中是不安全的。

宿主机挂载目录的权限会覆盖容器内的权限设置,但是docker容器中运行的应用往往是普通用户,所以为了程序正确的启动,需要设置宿主机Docker数据卷路径的权限

有以下几种处理方法

第一种,将宿主机文件夹权限与docker容器内部用户匹配:

确认容器内部用户的UID

[root@openEuler2203-sp4 redis-test]# docker run --rm --entrypoint bash openeuler-redis:6.2.14 -c "id redis"
uid=998(redis) gid=998(redis) groups=998(redis)

将宿主机路径权限做修改

[root@openEuler2203-sp4 u01]# chown -R 998.998 redis-test/

第二种(推荐方式),用户命名空间映射,这个是docker最高级别的权限隔离方案。哪怕是容器中的root用户,也会映射到宿主机的普通用户。

[root@anonymous Software]# useradd -r -s /sbin/nologin -M dockremap

[root@anonymous Software]# id dockremap
用户id=987(dockremap) 组id=987(dockremap) 组=987(dockremap)

[root@anonymous Software]# echo "dockremap:100000:65536" >> /etc/subuid
[root@anonymous Software]# echo "dockremap:100000:65536" >> /etc/subgid

使用 dockremap 映射的容器,其内部的 UID/GID 会被自动映射为宿主机的 100000 + 容器内UID

[root@anonymous Software]# vi /etc/docker/daemon.json
[root@anonymous Software]# cat  /etc/docker/daemon.json
{
    ......其他配置省略,加上最后这个参数,注意json格式,别少了逗号
    "userns-remap": "dockremap"
}

我们启动测试一下

[root@anonymous Software]# docker run -d --name redis-ns-test openeuler-redis:6.2.14
3a09f4e77e7630fb66b55c5d3d939a66d62d210da26f505c8f997b5f72fd14b2

观察redis的uid

[root@anonymous Software]# ps -ef | grep redis-server
100998     11497   11476  0 18:27 ?        00:00:00 redis-server 0.0.0.0:6379

创建工作目录

[root@anonymous Software]# mkdir -p /u01/redis-test/conf/ /u01/redis-test/data /u01/redis-test/logs

偷懒用现成的redis配置文件

[root@anonymous conf]# docker run --rm openeuler-redis:6.2.14 cat /u01/redis/conf/redis.conf > /u01/redis-test/conf/redis.conf

配置权限

[root@anonymous Software]#  chown -R 100998:100998 /u01/redis-test
[root@anonymous Software]#  chmod -R 750 /u01/redis-test

启动容器

[root@anonymous conf]# docker run -d --name redis-prod \
-p 6379:6379 \
-v /u01/redis-test/conf/redis.conf:/u01/redis/conf/redis.conf \
-v /u01/redis-test/data:/u01/redis/data \
-v /u01/redis-test/logs:/u01/redis/logs \
openeuler-redis:6.2.14 
92a9fbc077e3646b1c799d8248bc3a0104e1e0730d835fb9fff60ea9e1f306df

验证结果

[root@anonymous redis-test]# docker ps
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS          PORTS                                       NAMES
92a9fbc077e3   openeuler-redis:6.2.14   "redis-server /u01/r…"   15 seconds ago   Up 14 seconds   0.0.0.0:6379->6379/tcp, :::6379->6379/tcp   redis-prod
[root@anonymous redis-test]# netstat -lntp | grep 6379
tcp        0      0 0.0.0.0:6379            0.0.0.0:*               LISTEN      20526/docker-proxy
tcp6       0      0 :::6379                 :::*                    LISTEN      20534/docker-proxy
[root@anonymous logs]# tail -f redis.log
1:C 26 Aug 2025 10:48:44.148 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1:C 26 Aug 2025 10:48:44.148 # Redis version=6.2.14, bits=64, commit=00000000, modified=0, pid=1, just started
1:C 26 Aug 2025 10:48:44.148 # Configuration loaded
1:M 26 Aug 2025 10:48:44.148 * monotonic clock: POSIX clock_gettime
1:M 26 Aug 2025 10:48:44.149 * Running mode=standalone, port=6379.
1:M 26 Aug 2025 10:48:44.149 # Server initialized
1:M 26 Aug 2025 10:48:44.149 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
1:M 26 Aug 2025 10:48:44.149 * Ready to accept connections