简介:这份资源面向需要在 Kubernetes 集群中部署 MySQL 的运维与开发人员,提供一套可直接落地的离线部署方案,重点解决内网或无外网环境下镜像拉取困难、YAML 配置零散的问题。压缩包共 7 个文件,约 280.98MB,以 4 个 yaml 清单、2 个 tar 镜像包和 1 个 txt 说明为主:yaml 覆盖 Deployment、Service、持久化存储与存储卷声明等核心对象,tar 包内含 MySQL 5.7 镜像,可通过 load 方式导入本地镜像仓库,txt 则记录部署要点。资源已积累 1095 人学习下载,说明其在实际生产与测试场景中具备一定参考价值。读者拿到后可直接复用整套编排文件,快速完成镜像导入、存储挂载与服务暴露,省去从零编写清单和排查镜像来源的时间,适合作为 Kubernetes 上部署有状态数据库的入门与实战模板。
1. 内网 Kubernetes 里装 MySQL 5.7:为什么离线包才是真正的拦路虎
很多团队第一次在 Kubernetes 上部署 MySQL 5.7,都是在能连外网的环境里跑通的:kubectl apply一个 StatefulSet,镜像从 Docker Hub 拉,几分钟就起来了。可一旦搬到生产内网、金融机房或者边缘节点,情况立刻反转——镜像拉不动,ImagePullBackOff挂在那里,Pod 永远起不来。这时候你才会意识到,kubernetes 部署 mysql 全量离线包(mysql5.7)这件事,难点从来不是 YAML 写得好不好,而是所有依赖能不能在断网环境里自给自足。
这篇笔记面向的是需要在隔离网络里交付 MySQL 5.7 的一线工程师:镜像怎么打包、依赖怎么带进去、StatefulSet 怎么配、初始化脚本怎么塞、出问题怎么查。我会按「先讲清离线包该包含什么,再一步步落地,最后把踩过的坑摊开」的顺序写。如果你手上正好有一台能联网的跳板机和一套内网 K8s 集群,跟着走就能复现。
2. 离线包到底要装什么:镜像、依赖与目录规划
2.1 为什么不能只docker save一个 mysql:5.7
新手最容易犯的错,是以为离线包就是docker save mysql:5.7 -o mysql.tar完事。真到内网docker load之后,Pod 还是起不来,因为 MySQL 5.7 官方镜像在初始化阶段会依赖一批基础层和工具,尤其是当你用了自定义my.cnf、初始化 SQL、或者基于官方镜像做了二次构建时,这些层必须一起带走。
常见做法是:在联网机器上先把最终要用的镜像完整构建好,再整包导出。判断「完整」的标准很简单——在联网机器上用docker run能正常初始化并启动,导出的 tar 在内网docker load后docker images能看到同样的 IMAGE ID,且docker history的层数一致。
我一般会把离线包拆成三块:镜像 tar 包、K8s 资源清单、初始化数据(SQL 脚本和配置文件)。三块分开管理,出问题时能快速定位是哪一块没带全。
2.2 离线包的目录结构设计
一个能复用的离线包目录,我通常这样组织:
mysql57-offline/ ├── images/ │ └── mysql-5.7.44.tar # docker save 导出的镜像 ├── manifests/ │ ├── namespace.yaml │ ├── secret.yaml # root 密码、普通用户密码 │ ├── configmap.yaml # my.cnf 片段 │ ├── service.yaml # Headless Service │ └── statefulset.yaml # 核心工作负载 ├── init/ │ └── init.sql # 建库建表脚本 └── load.sh # 一键导入脚本这个结构的好处是:images/只负责镜像搬运,manifests/只负责 K8s 编排,init/只负责数据初始化。任何一块要更新,不影响其他两块。load.sh把导入动作固化下来,避免每次手敲命令漏步骤。
提示:镜像 tar 包体积通常在 400MB 到 600MB 之间,取决于基础层是否被复用。传输前先确认内网节点的磁盘余量,
/var/lib/docker或 containerd 的存储目录至少留出两倍空间。
2.3 镜像版本与 tag 的确定
MySQL 5.7 的镜像 tag 选择有个细节:mysql:5.7是滚动 tag,不同时间拉到的可能是 5.7.44 也可能是更早的补丁版本。离线交付要求可复现,所以必须锁定到具体小版本,比如mysql:5.7.44。在联网机器上docker pull mysql:5.7.44,然后docker tag成内网私有仓库的地址,再导出。
如果内网有 Harbor 或 registry,导入后重新打 tag 推上去;如果没有,就直接用docker load后的本地镜像,StatefulSet 里imagePullPolicy设成IfNotPresent,避免 K8s 反复尝试去外网拉。
3. 在联网机器上构建并导出 MySQL 5.7 镜像
3.1 拉取并验证镜像完整性
第一步在能联网的机器上操作,确保 Docker 可用:
# 锁定具体小版本,避免滚动 tag 带来的不确定性 docker pull mysql:5.7.44 # 查看镜像的层信息和大小,确认没有异常 docker inspect mysql:5.7.44 --format '{{.Id}} {{.Size}}' # 记录 IMAGE ID,后面内网 load 后要对比 docker images mysql:5.7.44docker pull完成后,docker inspect输出的 Id 是镜像的 sha256 摘要,这是判断内网导入是否一致的唯一可靠依据。Size 字段是压缩后的大小,实际导出 tar 会更大一些。
参数说明:--format用 Go 模板语法提取字段,.Id是完整摘要,.Size是字节数。如果你要写自动化脚本,可以把这两个值存到文件里,内网导入后做 diff。
3.2 导出镜像 tar 包
# 导出为 tar,-o 指定输出文件 docker save mysql:5.7.44 -o mysql-5.7.44.tar # 校验 tar 包完整性,记录 sha256 sha256sum mysql-5.7.44.tar > mysql-5.7.44.tar.sha256 # 查看 tar 包大小 ls -lh mysql-5.7.44.tardocker save会把镜像的所有层打包成一个 tar,包含 manifest、config 和 layer 数据。导出后一定要算 sha256,因为大文件在跨网段传输时偶发损坏,内网导入前先sha256sum -c校验,能省掉很多「镜像损坏」的玄学排查。
逻辑说明:docker save和docker export是两回事。save保留镜像的完整层结构和元数据,load后能还原成镜像;export只导出容器文件系统,丢失层信息,不能用于镜像搬运。离线包场景必须用save。
3.3 准备初始化 SQL 和配置文件
MySQL 5.7 官方镜像支持在/docker-entrypoint-initdb.d/放.sql、.sh、.sql.gz文件,容器首次初始化时按字母序执行。我一般把建库脚本放这里:
-- init.sql:首次启动时自动执行 -- 创建业务库,字符集用 utf8mb4 兼容 emoji CREATE DATABASE IF NOT EXISTS app_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -- 创建业务用户,限制来源网段 CREATE USER IF NOT EXISTS 'app_user'@'%' IDENTIFIED BY 'ChangeMe_2024'; GRANT ALL PRIVILEGES ON app_db.* TO 'app_user'@'%'; FLUSH PRIVILEGES;注意CREATE USER的密码不要写死在脚本里提交到代码库,实际交付时用 Secret 挂载或者启动时通过环境变量注入。这里写明文只是为了演示语法。
配置文件my.cnf片段通过 ConfigMap 挂载,覆盖默认参数:
[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci max_connections=500 innodb_buffer_pool_size=1G slow_query_log=1 long_query_time=2innodb_buffer_pool_size是最关键的参数,一般设为容器内存限制的 50% 到 70%。设太大容器会被 OOM Kill,设太小查询性能上不去。max_connections要结合业务连接池大小,500 是个保守起点。
4. 内网 K8s 落地:从 load 镜像到 StatefulSet 跑起来
4.1 导入镜像并确认
内网节点上执行:
# 先校验文件完整性 sha256sum -c mysql-5.7.44.tar.sha256 # 导入镜像 docker load -i mysql-5.7.44.tar # 确认 IMAGE ID 与联网机器一致 docker images mysql:5.7.44如果集群用的是 containerd 而不是 Docker,导入命令不同:
# containerd 环境导入镜像 ctr -n k8s.io images import mysql-5.7.44.tar # 确认导入结果 ctr -n k8s.io images ls | grep mysql-n k8s.io指定命名空间,K8s 默认从k8s.io这个 namespace 找镜像。如果导到了default,Pod 照样拉不到。这是 containerd 环境最常见的翻车点之一。
4.2 创建 Secret 和 ConfigMap
# secret.yaml apiVersion: v1 kind: Secret metadata: name: mysql-secret namespace: mysql-offline type: Opaque stringData: mysql-root-password: "RootPass_2024" mysql-password: "AppPass_2024"# configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mysql-config namespace: mysql-offline data: my.cnf: | [mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci max_connections=500 innodb_buffer_pool_size=1GstringData会自动做 base64 编码,比手写data字段省事。ConfigMap 里的my.cnf通过 volume 挂载到/etc/mysql/conf.d/,MySQL 启动时会自动读取该目录下的.cnf文件。
4.3 StatefulSet 核心配置
apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql namespace: mysql-offline spec: serviceName: mysql-headless replicas: 1 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:5.7.44 imagePullPolicy: IfNotPresent ports: - containerPort: 3306 env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: mysql-secret key: mysql-root-password - name: MYSQL_DATABASE value: app_db volumeMounts: - name: data mountPath: /var/lib/mysql - name: config mountPath: /etc/mysql/conf.d - name: init mountPath: /docker-entrypoint-initdb.d resources: requests: memory: "2Gi" cpu: "1" limits: memory: "2Gi" cpu: "2" volumes: - name: config configMap: name: mysql-config - name: init configMap: name: mysql-init volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 20Gi关键点:imagePullPolicy: IfNotPresent保证用本地已导入的镜像,不去外网拉。volumeClaimTemplates为每个 Pod 自动创建 PVC,StatefulSet 删除重建后数据仍在。resources.limits.memory要和innodb_buffer_pool_size匹配,2Gi 内存限制配 1G buffer pool 是合理比例。
4.4 验证部署结果
# 查看 Pod 状态 kubectl -n mysql-offline get pods -w # 查看初始化日志,确认 SQL 执行成功 kubectl -n mysql-offline logs mysql-0 | tail -50 # 进入容器验证连接 kubectl -n mysql-offline exec -it mysql-0 -- mysql -uroot -pPod 从Pending到Running通常需要 30 到 60 秒,首次初始化因为要执行 SQL 会更久。如果卡在Pending,先看 PVC 是否绑定成功;如果卡在ContainerCreating,看镜像是否真的导入到了正确的 namespace。
5. 离线部署 MySQL 5.7 的避坑清单
5.1 镜像导入后 Pod 仍报 ImagePullBackOff
现象:kubectl describe pod显示Failed to pull image,但docker images明明能看到镜像。
原因:集群用 containerd 时,docker load导入的镜像在 Docker 的存储里,containerd 看不到。两套运行时各有各的镜像存储。
解决:用ctr -n k8s.io images import重新导入,或者配置crictl确认运行时类型。kubectl get nodes -o wide看 CONTAINER-RUNTIME 列,是docker://还是containerd://。
5.2 初始化 SQL 没执行
现象:Pod 起来了,但app_db不存在,/docker-entrypoint-initdb.d/里的脚本像没跑过。
原因:MySQL 官方镜像只在数据目录为空时执行初始化脚本。如果 PVC 里已经有数据(比如之前跑过一次),再次启动会跳过初始化。
解决:确认 PVC 是全新的,或者手动删掉 PVC 重建。kubectl -n mysql-offline delete pvc>#!/bin/bash # load.sh:内网一键导入 set -euo pipefail IMAGE_TAR="images/mysql-5.7.44.tar" RUNTIME=$(kubectl get nodes -o jsonpath='{.items[0].status.nodeInfo.containerRuntimeVersion}') echo "检测到运行时: $RUNTIME" if [[ "$RUNTIME" == docker* ]]; then docker load -i "$IMAGE_TAR" elif [[ "$RUNTIME" == containerd* ]]; then ctr -n k8s.io images import "$IMAGE_TAR" else echo "未知运行时,请手动导入" exit 1 fi kubectl apply -f manifests/ echo "部署完成,等待 Pod 就绪..." kubectl -n mysql-offline rollout status statefulset/mysql --timeout=180s
set -euo pipefail让脚本遇到错误立即退出,避免半途失败还继续往下跑。rollout status会阻塞直到 StatefulSet 就绪或超时,比手动get pods轮询靠谱。
6.2 验证离线包完整性的检查表
交付前我会过一遍这个表:
| 检查项 | 命令 | 预期结果 |
|---|---|---|
| 镜像 tar 校验 | sha256sum -c | OK |
| 镜像已导入 | docker images或ctr images ls | 有对应 tag |
| Secret 存在 | kubectl get secret | mysql-secret |
| ConfigMap 存在 | kubectl get cm | mysql-config, mysql-init |
| PVC 绑定 | kubectl get pvc | Bound |
| Pod 就绪 | kubectl get pods | Running, Ready 1/1 |
| 初始化 SQL 生效 | kubectl exec ... mysql -e "show databases" | 含 app_db |
这张表可以直接贴到交付文档里,验收时逐项打勾。
6.3 版本升级时的注意事项
MySQL 5.7 小版本升级(比如 5.7.44 升到 5.7.45)在离线环境里要格外小心。官方镜像的 entrypoint 会检查数据目录版本,如果镜像版本低于数据目录版本,会报Invalid MySQL server upgrade并拒绝启动。
我的习惯是:升级前先mysqldump全量备份,然后删掉 StatefulSet 但保留 PVC,导入新镜像后重建。如果新镜像启动时报版本不兼容,说明数据目录的版本号比镜像高,这时候只能回滚镜像或者用mysql_upgrade手动处理。离线环境没有后悔药,备份是唯一的保险。
6.4 一个我踩过的坑
有次交付到客户内网,所有检查项都过了,Pod 也 Running,但业务连不上。排查了半天发现是 Service 的clusterIP: None配了 Headless,而业务方用的是普通 Service 的 ClusterIP 访问。Headless Service 只返回 Pod IP,不提供负载均衡,单副本场景下其实也能用,但客户端如果依赖 DNS 轮询就会出问题。
后来我养成了一个习惯:交付前一定用业务方的连接方式实测一次,而不是只验证kubectl exec能连上。kubectl exec走的是容器内 localhost,和走 Service 完全是两条路径。这个教训让我在后续每个离线包里都加了一个test-connection.sh,模拟业务侧的真实连接。
希望帮到你。
本文还有配套的精品资源,点击获取