云原生运维学习笔记:ECShop全链路容器化部署与Docker底层资源隔离深度剖析

云原生运维学习笔记:ECShop全链路容器化部署与Docker底层资源隔离深度剖析

在前几篇笔记中,我们完成了从Docker镜像定制、私有仓库搭建到网络与数据卷的管理。但这些大多停留在单点组件的操作上。在真实的云原生运维场景中,我们需要将不同组件(如Nginx、PHP、MySQL)有机地串联起来,形成一个完整的业务架构。

今天的实战笔记将分为两个核心部分:首先,我们将从零开始,使用纯 docker run 命令,结合自建的 Harbor 镜像仓库,完成 ECShop 电商平台(LNMP架构) 的全链路容器化部署。在这个环节中,我们将面临大量真实环境中的"坑点",例如镜像层源配置失效、端口冲突、配置覆盖及底层环境兼容性等。其次,我们将打破黑盒,深入 Linux 内核,通过手工创建 Namespace 和配置 Cgroups,去探究 Docker 资源隔离与限制的底层实现原理。


第一部分:综合实战 ------ ECShop电商平台容器化部署

部署一个生产级的业务系统,核心原则是**"环境随镜像走,数据与配置留宿主机"**。运行环境与依赖扩展打包进镜像;业务代码、配置文件、数据库数据、运行日志则通过 -v 参数挂载到宿主机固定目录,实现数据与应用生命周期的解耦。

1. Docker 环境搭建与私有仓库初始化

为了方便后续镜像的高效分发,我们需要先配置好 Docker 并搭建 Harbor 私有仓库。

指令 1:配置防火墙与关闭默认阻挡

在容器网络中,容器的外网通信依赖于宿主机 iptables 的 NAT 规则。如果关闭防火墙,会导致容器无法连通外网。这里我们将防火墙默认区域设为 trusted,放通所有流量。

bash 复制代码
# systemctl enable --now: 设置开机自启并立即启动
[root@docker ~]# systemctl enable firewalld.service --now
# firewall-cmd --set-default-zone=trusted: 将默认区域设为信任区域,放通所有端口
[root@docker ~]# firewall-cmd --set-default-zone=trusted

指令 2:配置 Docker 仓库并安装

CentOS 7 的 $releasever 变量可能会被解析错误,因此我们在 YUM 源配置中直接将版本写死为 7。

bash 复制代码
[root@docker ~]# cat > /etc/yum.repos.d/docker-ce.repo <<'EOF'
[docker-ce-stable]
name=Docker CE Stable - $basearch
baseurl=https://mirrors.huaweicloud.com/docker-ce/linux/centos/7/$basearch/stable
enabled=1
gpgcheck=0
EOF

# 安装 docker-ce 并启动
[root@docker ~]# yum install -y docker-ce
[root@docker ~]# systemctl enable docker.service --now

指令 3:配置镜像加速与私有仓库信任

由于 Harbor 使用的是 HTTP 协议,而 Docker 客户端默认强制要求 HTTPS,必须将其加入 insecure-registries 中信任。同时配置 registry-mirrors 解决外部镜像拉取慢的问题。

bash 复制代码
[root@docker ~]# mkdir -p /etc/docker
[root@docker ~]# tee /etc/docker/daemon.json <<'EOF'
{
  "insecure-registries": ["hub.laoma.cloud", "reg.minwu.cloud"],
  "registry-mirrors": ["http://hub.laoma.cloud", "https://docker.m.daocloud.io"]
}
EOF

# 重启 Docker 使配置生效
[root@docker ~]# systemctl restart docker

指令 4:部署 Harbor 私有仓库

解压离线安装包,修改 harbor.yml,指定主机名、注释 HTTPS 配置并设定数据落盘路径(由于实验环境资源有限,与业务同根于 /ecshop/harbor),然后执行安装脚本。

bash 复制代码
# 下载并解压 Harbor 离线包
[root@docker ~]# wget http://192.168.45.200/class/course-materials/softwares/stage02/harbor-offline-installer-v2.10.2.tgz
[root@docker ~]# tar -xf harbor-offline-installer-v2.10.2.tgz
[root@docker ~]# cd harbor/
# 复制模板并修改
[root@docker harbor]# cp harbor.yml.tmpl harbor.yml
[root@docker harbor]# vim harbor.yml
# hostname: reg.minwu.cloud
# data_volume: /ecshop/harbor

# 一键部署(底层调用 docker-compose 拉起 10 个容器)
[root@docker harbor]# ./install.sh

Harbor 启动后,登录 Web 页面,创建名为 ecshop 的私有项目。


2. 构建专属的 PHP 运行环境镜像

官方 php:7.4-fpm 镜像默认没有安装 ECShop 所需的图像处理(gd)、数据库(mysqli/pdo_mysql)等扩展。我们需要编写 Dockerfile 进行定制。

思路解析 :ECShop 运行需要捕捉错误,但 PHP 7.4 会对一些过时语法报 Deprecated 提示。这里的关键在于,镜像里只装环境,不写死配置,配置文件由外部挂载提供。

编写 Dockerfile:

dockerfile 复制代码
FROM php:7.4-fpm

# 【避坑点】Debian 11 bullseye 安全包已迁入归档源。需要重写 sources.list 指向归档地址,
# 否则 apt-get update/install 会大面积报 404。
RUN printf 'deb http://mirrors.huaweicloud.com/debian-archive/debian bullseye main\ndeb http://mirrors.huaweicloud.com/debian-archive/debian-security bullseye-security main\n' > /etc/apt/sources.list \
    && apt-get update -o Acquire::Retries=3 \
    && apt-get install -y --no-install-recommends \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libpng-dev \
        libzip-dev \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j"$(nproc)" gd mysqli pdo_mysql zip opcache \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /var/www/html
EXPOSE 9000
CMD ["php-fpm"]

指令 5:构建并推送镜像

bash 复制代码
# -t: 指定构建的镜像名和标签
# . : 指代当前目录(构建上下文)
[root@docker ~]# docker build -t php:fpm-ecshop .

# 打标签,使其符合 Harbor 的命名规范:仓库地址/项目名/镜像名:标签
[root@docker ~]# docker tag php:fpm-ecshop reg.minwu.cloud/ecshop/php:fpm-ecshop

# 登录 Harbor
[root@docker ~]# docker login reg.minwu.cloud -u admin -p root123

# 推送镜像
[root@docker ~]# docker push reg.minwu.cloud/ecshop/php:fpm-ecshop

3. 网络与持久化规划

指令 6:创建自定义 Bridge 网络

在使用默认 bridge 时,容器间只能通过 IP 通信。自定义网络不仅支持隔离,还内置了 DNS 解析 ,容器之间可以直接通过容器名互访,比如 Nginx 配置文件中写 fastcgi_pass php:9000 即可。

bash 复制代码
# --driver bridge: 指定网络驱动为桥接
# --subnet 172.20.0.0/16: 指定网段,避免与默认的 172.17 冲突
[root@docker ~]# docker network create --driver bridge --subnet 172.20.0.0/16 ecshop-net

指令 7:准备目录与配置文件

将配置全部放在宿主机的 /ecshop 下,保证容器删除重建时环境无损。

bash 复制代码
[root@docker ~]# mkdir -p /ecshop/{nginx/conf.d,nginx/logs,php/conf,php/logs,mysql/data,mysql/conf,web}

# 从官方镜像中提取默认配置模板
[root@docker ~]# docker run --rm php:fpm-ecshop cat /usr/local/etc/php/php.ini-production > /ecshop/php/conf/php.ini
[root@docker ~]# docker run --rm php:fpm-ecshop cat /usr/local/etc/php-fpm.d/www.conf > /ecshop/php/conf/www.conf

# 修改 php.ini 中的时区
[root@docker ~]# sed -i 's|^;date.timezone =$|date.timezone = Asia/Shanghai|' /ecshop/php/conf/php.ini

Nginx 配置解析 (default.conf):

nginx 复制代码
server {
    listen       80;                 
    server_name  localhost;
    root   /var/www/html;
    index  index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$args;   # ECShop路由重写
    }

    location ~ \.php$ {
        fastcgi_pass   php:9000;     # 利用自定义网络 DNS 解析,不写死 IP
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
}

MySQL 配置解析 (my.cnf):

ini 复制代码
[mysqld]
character-set-server=utf8mb4
# 【避坑点】ECShop数据中包含 0000-00-00 日期,MySQL 5.7 的严格模式会拒绝导入。
# 必须修改 sql_mode 放宽限制。
sql_mode=NO_ENGINE_SUBSTITUTION

4. 启动容器集群

核心避坑 :由于同机还部署了 Harbor,Harbor 的 nginx 反向代理容器占用了宿主机的 80 端口。因此 ECShop 必须改用 8080 端口映射。且启动顺序必须严格为 MySQL -> PHP -> Nginx ,因为 Nginx 在启动加载配置时,需要解析 php 这个域名。

指令 8:启动 MySQL 容器

bash 复制代码
# -d: 后台运行
# --name mysql: 命名容器(后续可被 DNS 解析)
# --network ecshop-net: 加入自定义网络
# --restart always: 容器退出时总是重启(保障高可用)
# -v /ecshop/mysql/data:/var/lib/mysql: 映射数据目录,实现数据持久化
# -v /ecshop/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro: 映射配置文件,只读(ro)
# -e MYSQL_ROOT_PASSWORD=root123: 初始化 root 密码
# -e MYSQL_DATABASE=ecshop: 初始化时创建 ecshop 库
[root@docker ~]# docker run -d --name mysql --network ecshop-net --restart always \
  -v /ecshop/mysql/data:/var/lib/mysql \
  -v /ecshop/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro \
  -e MYSQL_ROOT_PASSWORD=root123 \
  -e MYSQL_DATABASE=ecshop \
  -e MYSQL_USER=ecshop \
  -e MYSQL_PASSWORD=ecshop123 \
  reg.minwu.cloud/ecshop/mysql:5.7

指令 9:启动 PHP 容器

bash 复制代码
# -v /ecshop/web:/var/www/html: 将宿主机代码目录挂载进容器
# -v /ecshop/php/conf/php.ini:/usr/local/etc/php/php.ini:ro: 挂载自定义 php.ini
# -v /ecshop/php/conf/www.conf:/usr/local/etc/php-fpm.d/www.conf:ro: 挂载 FPM 池配置
[root@docker ~]# docker run -d --name php --network ecshop-net --restart always \
  -v /ecshop/web:/var/www/html \
  -v /ecshop/php/conf/php.ini:/usr/local/etc/php/php.ini:ro \
  -v /ecshop/php/conf/www.conf:/usr/local/etc/php-fpm.d/www.conf:ro \
  reg.minwu.cloud/ecshop/php:fpm-ecshop

指令 10:启动 Nginx 容器

bash 复制代码
# -p 8080:80: 将宿主机 8080 端口映射到容器 80 端口(避开 Harbor 占用的 80)
# -v /ecshop/nginx/conf.d:/etc/nginx/conf.d:ro: 挂载站点配置
[root@docker ~]# docker run -d --name ecshop-nginx --network ecshop-net --restart always -p 8080:80 \
  -v /ecshop/web:/var/www/html \
  -v /ecshop/nginx/conf.d:/etc/nginx/conf.d:ro \
  -v /ecshop/nginx/logs:/var/log/nginx \
  reg.minwu.cloud/ecshop/nginx:1.24

执行状态检查:

bash 复制代码
[root@docker ~]# docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES           STATUS         PORTS
ecshop-nginx    Up 2 minutes   0.0.0.0:8080->80/tcp
php             Up 2 minutes   9000/tcp
mysql           Up 2 minutes   3306/tcp, 33060/tcp

此时在浏览器访问 http://10.1.8.10:8080/install/ 进行 ECShop 的安装。安装时,数据库主机必须填写 mysql (即容器名),而不能是 127.0.0.1。


5. 部署过程中的核心避坑指南

在上述部署过程中,有几个极其容易踩的坑,这里做统一复盘:

  1. 源码多套一层目录 :解压 ECShop 后,代码可能在 xxx_release/source/ecshop/ 下,需要手动把里面的文件移到 web 根目录,否则会出现 404 或 403。
  2. FPM 池配置注释符错误 :在 www.conf 中,必须以 ; 作为注释符。如果错写成 #,会导致 php-fpm 启动崩溃,报错 Restarting (78)。
  3. 屏蔽报错失效 :ECShop 安装脚本运行时会通过 ini_set('display_errors', 1) 强制显示错误,这会导致安装进度被打断。必须要在 www.conf 中使用 php_admin_flag[display_errors] = Off,因为 php_admin_ 的级别高于 ini_set(),能锁死配置。

第二部分:深入底层 ------ Docker 的隔离与限制技术

经过上面的实战,我们跑通了整套架构。但容器凭什么能做到"彼此隔离"又"限制资源"?答案在于 Linux 内核的 Namespace(命名空间) 和 Cgroups(控制组)。

1. Namespace:实现视图隔离

Namespace 为进程提供了一个独立的系统视图。就好像在学校里,"三年一班的小明"和"三年二班的小明"在同一份全校花名册上即使重名,只要按班级(命名空间)划分,就能唯一定位。

Linux 包含多种 Namespace,例如 PID(进程)、NET(网络)、MNT(文件系统)、UTS(主机名)等。为了探究网络隔离的真相,我们完全脱离 Docker 命令,手工为容器"织"一张网。

步骤 1:启动一个无网络的容器并获取 PID

bash 复制代码
# --network=none: 不配置任何网卡
[root@docker ~]# docker container run -it --rm --name box --network=none busybox
/ # 

# 在宿主机另起一个终端,获取该容器的 PID
[root@docker ~]# docker container inspect box -f '{{.State.Pid}}'
9897

步骤 2:手工配置网络命名空间

bash 复制代码
[root@docker ~]# pid=9897
# 在 /var/run/netns/ 建立映射,方便 ip 命令管理该命名空间
[root@docker ~]# mkdir -p /var/run/netns
[root@docker ~]# ln -s /proc/$pid/ns/net /var/run/netns/$pid

# 创建一对虚拟网卡 (veth pair) A 和 B
[root@docker ~]# ip link add A type veth peer name B
# 把 A 端插到 docker0 网桥上,并启用
[root@docker ~]# brctl addif docker0 A
[root@docker ~]# ip link set A up

# 把 B 端移到容器的网络命名空间
[root@docker ~]# ip link set B netns $pid
# 进入到容器的网络命名空间,把 B 重命名为 eth0
[root@docker ~]# ip netns exec $pid ip link set dev B name eth0
[root@docker ~]# ip netns exec $pid ip link set eth0 up

# 为容器配置 IP 和默认网关
[root@docker ~]# ip netns exec $pid ip addr add 172.17.0.99/16 dev eth0
[root@docker ~]# ip netns exec $pid ip route add default via 172.17.0.1

步骤 3:验证

此时回到容器内部:

bash 复制代码
/ # ip a show eth0
27: eth0@if28: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
    link/ether 9e:22:b6:89:01:04 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.99/16 scope global eth0
/ # ping -c 1 1.1.1.1
64 bytes from 1.1.1.1: seq=0 ttl=127 time=187.606 ms

这完美复现了 Docker 创建容器时底层网络配置的全过程。

2. Cgroups:实现资源限制

Namespace 解决了"看不见",Cgroups 则解决"吃太多"。它用来限制、记录和隔离进程组所使用的物理资源(CPU、内存、磁盘 IO)。

测试 1:内存限制(OOM Killer 验证)

bash 复制代码
# -m 或 --memory: 限制物理内存最大使用量为 200M
# --vm-bytes 210M: 申请 210M 内存(超过限制)
[root@docker ~]# docker container run -it -m 200M progrium/stress --vm 1 --vm-bytes 210M
stress: dbug: [7] allocating 220200960 bytes ...
stress: FAIL: [1] (416) <-- worker 7 got signal 9

结果解析 :由于超出了 -m 200M 的限制,进程被内核的 OOM Killer 直接发送了 9 号信号(SIGKILL)强制终止。

测试 2:CPU 相对权重限制

bash 复制代码
# 启动容器 A,-c 或 --cpu-shares 设为 1024
[root@docker ~]# docker container run --name container_A -c 1024 progrium/stress --cpu 2

# 启动容器 B,-c 设为 512
[root@docker ~]# docker container run --name container_B -c 512 progrium/stress --cpu 2

# 查看资源使用情况
[root@docker ~]# docker container stats
CONTAINER ID   NAME          CPU %     MEM USAGE / LIMIT
12e2f0a539ff   container_A   132.48%   180KiB / 3.84GiB
654c6c1c3d3d   container_B   66.64%    172KiB / 3.84GiB

结果解析 :在 CPU 资源紧张时,权重分配发挥了作用,A 的 CPU 占用大约是 B 的两倍。但如果 A 被暂停(docker pause container_A),B 就能独占全部空闲的 CPU。

测试 3:磁盘 IO 限制

bash 复制代码
# --device-write-bps: 限制写入 /dev/sda 的速率为 30MB/s
[root@docker ~]# docker container run -it --rm --device-write-bps /dev/sda:30MB centos
# 使用 dd 命令进行写入测试,oflag=direct 绕过内存缓存直接写盘
[root@377b9154d88d /]# dd if=/dev/zero of=test.io bs=1M count=2048 oflag=direct
2147483648 bytes (2.1 GB, 2.0 GiB) copied, 68.2637 s, 31.5 MB/s

写入速度被成功卡在了 31.5 MB/s,限制生效。


总结与思考

今天经历了从繁杂的业务部署到内核底层的思维跳跃。

在 ECShop 部署实战中,我体会最深的是**"排障不仅仅是查日志,更是理解组件的协作逻辑"**。比如因为同机部署了 Harbor,导致宿主机的 80 端口被占用,这是网络规划层级的问题;又比如 Nginx 启动失败退出,是因为 PHP 容器还没起来,导致 DNS 解析不到 fastcgi_pass php:9000,这是依赖顺序的问题。理解了这背后的逻辑,就不再是盲人摸象。

而到了 Namespace 和 Cgroups 的实验,当手动敲下 ip link add A type veth peer name B 并在容器网络栈中看到新增的 eth0 时,我对 Docker 的敬畏感变得具体了。Docker 本身并没有创造魔法,它只是一个极其聪明的"组装工",把 Linux 内核里已经沉睡了十几年的隔离与限制技术,包装成了开发者极其顺手的工具。

掌握了这些,面对再复杂多变的微服务集群,我们都能从容地从资源、网络、依赖这几个维度去定位和拆解问题。云原生运维的路还很长,但今天的这一大步,让我们走得非常踏实。

相关推荐
red_redemption44 分钟前
自由学习记录(230)
学习
进击的荆棘2 小时前
Linux系统——线程同步与互斥之线程池和安全
linux·运维·服务器·安全·单例模式·线程
瑞雪兆丰年兮3 小时前
JavaWeb-day11-SpringBootWeb案例(文件上传)
java·运维·服务器·javaweb·oss
那明眸似水3 小时前
云原生项目三:Docker秒级启动NextCloud个人网盘存储办公系统
linux·运维·docker·云原生·容器
腾飞开源4 小时前
03_Kubernetes 核心概念拆解:从 Spec/Status 收敛到工作负载控制器的设计逻辑
云原生·容器·kubernetes
晓梦林5 小时前
BUUCTF-菜刀流量-MISC-学习笔记
笔记·学习
小南家的青蛙5 小时前
微服务体系下DevOps运维岗位面试方案设计
运维·面试·职场和发展·devops
Godspeed Zhao14 小时前
从零开始学AI25——无监督学习之层次聚类
学习·机器学习·聚类
知已难寻15 小时前
Docker 入门:容器技术与 Docker 基础
java·运维·服务器·docker·云原生·容器