云原生运维学习笔记: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. 部署过程中的核心避坑指南
在上述部署过程中,有几个极其容易踩的坑,这里做统一复盘:
- 源码多套一层目录 :解压 ECShop 后,代码可能在
xxx_release/source/ecshop/下,需要手动把里面的文件移到 web 根目录,否则会出现 404 或 403。 - FPM 池配置注释符错误 :在
www.conf中,必须以;作为注释符。如果错写成#,会导致php-fpm启动崩溃,报错Restarting (78)。 - 屏蔽报错失效 :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 内核里已经沉睡了十几年的隔离与限制技术,包装成了开发者极其顺手的工具。
掌握了这些,面对再复杂多变的微服务集群,我们都能从容地从资源、网络、依赖这几个维度去定位和拆解问题。云原生运维的路还很长,但今天的这一大步,让我们走得非常踏实。