简介:针对 Kubernetes 集群内网或离线环境中部署 MySQL 5.7 的常见问题,这份资源把镜像包与部署清单打包成套,适合没有外网拉取条件的运维、开发或学习人员使用。压缩包包含 7 个文件,其中有 4 个可直接应用的 YAML 清单、2 个 MySQL 镜像 tar 包以及 1 份启动说明,整体体积约 280.98MB。
YAML 文件覆盖了 Deployment、持久化卷、PVC 与 Service 等关键对象,镜像包可通过 docker load 方式导入本地,再配合 kubectl apply 快速启动服务,省去手工编写资源清单和镜像搬运的重复操作。启动说明文档则为首次使用者提供了导入与部署的先后顺序,降低上手门槛。
目前已有 1095 人学习/下载。除 MySQL 5.7 主镜像外,还附带了另一版本镜像包,便于在离线环境下完成可持久化存储的 MySQL 部署,同时帮助理解 Kubernetes 上有状态应用对存储、网络与服务暴露的基本设计思路。
1. 为什么内网交付最头疼的"全量"两个字
这两年做项目交付,凡是遇到 K8s 环境,十有七八是内网隔离的。客户机房不连外网,你在线 Docker Hub 拉镜像?不存在的。你想着用 pip/apt/yum 二进制的习惯在 K8s 场景里也这么干?同样行不通。Kubernetes 部署 MySQL 5.7 这个需求,单看思路并不复杂,真正让人崩溃的是"全量离线包"这五个字里藏着的依赖闭环——镜像、编排文件、初始化脚本、存储配置、访问入口,任何一环少带了,到了现场就是干瞪眼。
我最初接过一个典型的项目:现场是 K8s 1.20 集群,节点没有外网,要求在上面部署一套 MySQL 5.7.44,用来承接业务系统的订单库。我当时天真地以为,只要把 mysql:5.7 的镜像打成 tar 包带进去,再docker load一下,写个 Deployment 就完事了。结果到现场发现完全不是那么回事——没有镜像仓库,节点是 containerd,编排文件里没考虑存储类,密码和配置没有妥善管理,初始化脚本也没地方放。最后折腾了快一天,才把一套相对完整的全量离线包方案跑通。
这篇文章就是想把这份经验完整沉淀下来:你在内网 K8s 环境部署 MySQL 5.7,到底需要准备哪些东西、怎么制作离线包、到了现场怎么执行,以及最容易在哪里翻车。不管你是要部署一整套集群,还是单节点轻量跑业务,按这个思路做都能少走很多弯路。
先给结论:一套可复现的全量离线包 = MySQL 5.7 镜像 + 配置模板(my.cnf)+ 初始化 SQL + K8s 编排文件 + 辅助脚本。缺少其中任何一个,到了离线环境都会出现"看着包挺全,实际用不了"的尴尬。
2. 离线包到底该装哪些东西:MySQL 5.7 的资源清单
2.1 镜像选择:不要只盯着版本号
MySQL 5.7 官方镜像在 Docker Hub 上最后发布的是 5.7.44。社区里不少镜像还停留在 5.7.36、5.7.40 左右,我能给你的建议是:选择 5.7.44 或者你能确认的、在目标环境验证过的版本,并把它精确到 tag 写死。很多人在线部署习惯了latest,到离线环境就翻车——因为你导出时是 latest,现场加载后还是 latest,但镜像内容已经被后续更新污染过,无法保证一致性。
还要注意 CPU 架构。如果你的部署机器是 x86_64,那就导出 amd64 的镜像;如果是国产化 arm64 环境,就必须导出 arm64 版本。最稳妥的做法是利用docker --platform参数主动拉取对应架构的镜像,而不是靠 Docker Hub 自动选择,避免你在 x86 上拉了个 amd64,结果现场是 arm 节点,直接启动失败。
2.2 编排层资源:StatefulSet 是关键
我们知道 MySQL 属于有状态服务,K8s 里的 Deployment 并不适合直接承载数据库。规划的编排文件至少包括:
- StatefulSet:提供稳定的网络标识(如 mysql-0.mysql-hs.namespace.svc),配合 PersistentVolumeClaim 模板实现一实例一存储。
- Headless Service:为 StatefulSet 提供稳定的 DNS 解析地址。
- ConfigMap:存放 my.cnf 配置片段。
- Secret:存放 MySQL root 密码(也可以加业务账号密码)。
- PVC:数据卷声明,绑定到实际的 PV。
- 普通 Service:用于外部或同集群业务访问。
这几个文件的 apiVersion 在不同 K8s 版本里有差异。以 1.20 为例,StatefulSet 使用的 apiVersion 是apps/v1,Secret/ConfigMap 是v1,Service 是v1。如果你现场环境是更老的 1.16 以下,还可能出现extensions/v1beta1的写法,但现实项目中这类老集群已经很少见,新增部署优先按apps/v1处理即可。
2.3 数据层:初始化脚本和存储准备
MySQL 官方镜像支持把.sql、.sh脚本放到/docker-entrypoint-initdb.d/目录,容器首次初始化空数据目录时会自动执行。这个机制是离线包里非常关键的组成部分,你可以借此完成建库、建用户、设置时区、导入基础数据等动作。
存储准备方面,正常集群应该有一个默认 StorageClass,你可以直接用动态供给创建 PV;如果是裸机环境没有 StorageClass,就得提前创建 PV 并指定nodeAffinity绑定到固定节点,或者使用 local 持久化卷。这块我在第 5 部分单独讲,因为它是排障重灾区。
3. 制作离线包的实操流程:从镜像导出到编排文件定型
3.1 镜像导出与压缩
在有外网的机器上,拉取镜像后导出为 tar 包。实操我一般用如下命令:
docker pull mysql:5.7.44 mkdir -p mysql5.7-offline/images docker save mysql:5.7.44 | gzip > mysql5.7-offline/images/mysql-5.7.44.tar.gz这样打出来的包体积大约在 500MB 左右(MySQL 5.7 镜像本身 400 多MB),要注意现场传输效率和 U 盘文件系统格式。另外有人喜欢直接docker save -o mysql.tar mysql:5.7.44,不压缩也行,但现场拷盘时 tar.gz 明显更友好。
如果你现场不是 Docker,而是 containerd(现在 K8s 1.24+ 默认运行时已经是 containerd),那你在制作端也别只想着 docker 体系。建议在离线包目录里同时放一份原始 tar.gz,因为ctr images import是可以直接识别 docker save 格式的。换句话说,docker save 导出的镜像是通用格式,现场既能 docker load,也能 ctr images import。
3.2 编排文件模板
我习惯把编排文件放在manifests/目录下,与 images 平级。以下是一份经过现场验证的 StatefulSet 示例(节选,完整结构你可以在自己的模板里保持同样的骨架):
apiVersion: v1 kind: Secret metadata: name: mysql-secret namespace: database type: Opaque stringData: root-password: "YourStrongPassw0rd" mysql-user: "appuser" mysql-password: "AppUserPassw0rd"apiVersion: v1 kind: ConfigMap metadata: name: mysql-config namespace: database data: my.cnf: | [mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_general_ci lower_case_table_names=1 max_connections=500 innodb_buffer_pool_size=1G sql_mode=STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION default-time-zone='+08:00'apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql namespace: database spec: serviceName: mysql-hs replicas: 1 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:5.7.44 ports: - containerPort: 3306 env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: mysql-secret key: root-password volumeMounts: - name: data mountPath: /var/lib/mysql - name: mysql-config mountPath: /etc/mysql/conf.d/my.cnf subPath: my.cnf volumes: - name: mysql-config configMap: name: mysql-config volumeClaimTemplates: - metadata: name: data spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 20Gi3.3 初始化 SQL 的归置
初始化脚本里我一般放建库建表、业务账号授权、时区函数等。示例:
CREATE DATABASE IF NOT EXISTS business DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER IF NOT EXISTS 'appuser'@'%' IDENTIFIED BY 'AppUserPassw0rd'; GRANT ALL PRIVILEGES ON business.* TO 'appuser'@'%'; FLUSH PRIVILEGES;把这个脚本放进离线包的init-sql/目录。使用的方案是把整个 SQL 目录通过 ConfigMap 挂载到容器,或者用 initContainer 拷贝进去。这里有个细节:直接挂载单个文件到/docker-entrypoint-initdb.d/是可以的,但如果你有多个 SQL 文件且有先后顺序,建议挂载整个目录,因为 MySQL 官方镜像会按文件名顺序执行,而不是按挂载顺序。
3.4 生成校验文件
离线包传到现场后,很难保证没有损坏。制作完成后我会生成一个 sha256 校验信息:
cd mysql5.7-offline find . -type f -exec sha256sum {} \; > SHA256SUMS到了现场先跑一下sha256sum -c,确认镜像和编排文件没有在拷贝过程中出现损坏。虽然是老生常谈,但我见过真的有人现场镜像加载失败,最后发现是 U 盘格式化问题导致文件截断。
4. 集群里的部署执行手册:离线环境照抄版
4.1 镜像导入
现场若是 containerd 运行时,登录到每一个需要运行 MySQL pod 的节点,执行:
# 选择对应节点,一般用 kubectl get nodes 查看 ctr -n k8s.io images import mysql5.7-offline/images/mysql-5.7.44.tar.gz注意命名空间参数-n k8s.io不能漏。containerd 在不同发行版里的k8s.ionamespace 是 K8s 镜像的默认位置,如果你 import 到默认defaultnamespace,kubelet 在拉取镜像的时候根本找不到。
如果是 docker 运行时,则执行:
docker load -i mysql5.7-offline/images/mysql-5.7.44.tar.gz docker images | grep mysql这一步做完,一定要在节点上确认镜像 tag 完整显示为mysql:5.7.44,而不是<none>:<none>。在现场遇到过镜像 tar 里没有保留 tag 的情况,加载进去一堆<none>,pod 照样拉不到。
4.2 加载编排文件
把离线包里的 manifests 传到能执行 kubectl 的机器上(通常是堡垒机或 master 节点),按顺序执行:
kubectl create namespace database kubectl apply -f manifests/secret.yaml kubectl apply -f manifests/configmap.yaml kubectl apply -f manifests/service.yaml kubectl apply -f manifests/statefulset.yaml注意命名空间。如果你的编排文件里没有写死 namespace,那apply之前要确认当前上下文 namespace 是否正确,或者用-n database参数显式指定,避免 Yaml 应用到默认命名空间里去,后面业务方访问路径对不上。
4.3 等待 Pod 就绪与验证
执行之后观察:
kubectl -n database get pod -o wide kubectl -n database logs -f mysql-0 --tail=100第一次启动时,MySQL 官方镜像会比较慢,因为它要初始化数据目录、执行 initdb 目录里的脚本。如果你看到日志停留在ready for connections出现,基本就成功了一半。此时可以进入 pod 做一次连接测试:
kubectl -n database exec -it mysql-0 -- mysql -uroot -p'YourStrongPassw0rd' -e "SELECT VERSION();"如果输出5.7.44,部署算真正跑通了。接下来再验证从集群内部另一个 pod 是否能通过 Service 访问数据库——这才是业务方真正关心的连通性。
4.4 暴露访问入口
如果业务方需要从集群外直连,你还需要一个 NodePort 类型的 Service 或者 Ingress,而不是直接在 StatefulSet 里改 hostPort(那会限制 pod 调度位置)。示例 Service:
apiVersion: v1 kind: Service metadata: name: mysql-nodeport namespace: database spec: type: NodePort selector: app: mysql ports: - name: mysql port: 3306 targetPort: 3306 nodePort: 303065. 持久化、权限和初始化脚本:最容易踩的三个坑
5.1 存储类不存在时怎么处理
很多离线集群并没有配置 StorageClass,或者 StorageClass 的 provisioner 依赖云厂商组件,但内网环境根本没部署。这时候你用volumeClaimTemplates动态创建 PVC 大概率会卡在 Pending 状态。排查方法就是kubectl describe pvc,看到waiting for a volume to be created, either by external provisioner这类报错,基本确定是 StorageClass 问题。
比较常见的离线方案是手工创建 PV,再利用volumeClaimTemplates自动匹配。为 MySQL 创建一块 20Gi 的 local PV 示例:
apiVersion: v1 kind: PersistentVolume metadata: name: mysql-pv-1 spec: capacity: storage: 20Gi volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: local-storage local: path: /data/mysql-1 nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - node02注意persistentVolumeReclaimPolicy我建议设成Retain,别用Delete。数据库数据卷如果因为误操作被删除 PV,那数据就彻底没了,这不是开玩笑的。
5.2 数据目录权限和挂载问题
MySQL 官方镜像里的 mysqld 进程是以mysql用户运行的。容器第一次启动要往/var/lib/mysql写文件,如果挂载的 PV 目录权限不对,容器会直接报mysqld: Can't create/write to file '/var/lib/mysql/ibtmp1'然后退出。
以前我在一个 local PV 场景下就踩过这坑:宿主机/data/mysql-1的所有者是 root,权限 755,MySQL 容器根本写不进去。解决办法有两类:
- 宿主机上先
chown -R 999:999 /data/mysql-1(镜像内置 mysql 用户 UID 是 999)。 - 或者给容器加
securityContext:
securityContext: fsGroup: 999 runAsUser: 999我建议两种都配上。光靠 fsGroup 有时候对 local PV 也不一定生效,因为宿主机目录本身权限不对,fsGroup 不会自动执行 chown。正确姿势是:要么在宿主机把目录所有权改成 999,要么在 PV 上做初始化时用 initContainer 执行 chown:
initContainers: - name: volume-permission image: busybox:1.36 command: ["sh", "-c", "chown -R 999:999 /var/lib/mysql"] volumeMounts: - name: data mountPath: /var/lib/mysql这个 initContainer 需要在你的离线包里也带上 busybox 镜像,否则现场没镜像照样起不来。很多人的离线包之所以"看着全,实际缺",往往就是少了这种辅助镜像。
5.3 初始化脚本执行时机
官方镜像的机制是:只在数据目录为空时执行/docker-entrypoint-initdb.d/下的脚本。如果数据目录里已经有内容(比如之前初始化失败但目录已创建了文件),脚本就不会再执行。这是个好机制,但也坑了不少人:第一次启动时没把 SQL 脚本挂进去,后来补挂了脚本发现不生效;或者数据目录被初始化了一半,脚本执行失败,修复后再重启,脚本不跑了,导致库和账号都不完整。
给你一个可复现的检查思路:启动时观察日志中是否有Importing SQL ...之类的关键字。如果没有,说明容器认为数据目录非空,跳过了初始化。此时你不该反复删 PVC(除非是测试环境),而应该手动进入容器执行 SQL:
kubectl -n database exec -it mysql-0 -- mysql -uroot -p'YourStrongPassw0rd' < init.sql或者把脚本临时放到容器里再 source。对于正式环境,我更推荐把基线 SQL 的管理从前置初始化移到独立 job 或配置管理流程里,避免太依赖官方镜像初始化机制。
6. 排错链:从镜像导入到 pod Running 的完整排查路径
6.1 镜像导入后 Pod 还是 ImagePullBackOff
这个场景我在多个集群中遇到过。要么镜像根本没导入到目标节点,要么 tag 不对,要么没有私有仓库导致 K8s 默认要去 Docker Hub 拉取。排查路径是:
# 登录对应节点 crictl images | grep mysql # 或者 docker images | grep mysql确认镜像确实在节点上。如果镜像在,但状态还是 ImagePullBackOff,检查 pod 的 image 字段有没有写错版本号,比如 YAML 里写 mysql:5.7 但本地只有 mysql:5.7.44。我的建议是无论什么环境,编排文件里都写完整的 tag,别用简写。
如果你现场搭了私有镜像仓库(例如 registry:2 离线部署),还要注意 containerd/docker 的insecure-registries配置。默认 HTTPS 仓库没有证书会直接拒绝 pull,很多人第一次搭离线仓库就是栽在只有 HTTP 没有配置信任上。
6.2 CrashLoopBackOff 与权限、配置、内存
pod 一直重启,第一件事永远先看日志:
kubectl -n database logs mysql-0 --tail=50常见几类:
- 权限错误:日志里带
Permission denied,去查数据目录所有者和 fsGroup。 - 配置文件错误:比如 sql_mode 写错,mysqld 起不来。这种最好在制作离线包时,先本地用
mysqld --verbose --help验证一遍参数,再到现场调。 - OOMKilled:pod 状态是
OOMKilled,说明内存 limit 设置太小。MySQL 5.7 的 innodb_buffer_pool_size 如果给了 1G,而容器 limit 只有 512Mi,几乎必挂。建议给 MySQL pod 分配至少 2Gi 内存,缓冲池控制在容器内存的 50%-60% 左右。
6.3 连接报 1045 和字符集问题
部署成功后,业务方连接时报Access denied或者中文乱码,别急着改配置文件。先检查 root 密码 Secret 有没有被正确注入,再检查 my.cnf 里的character-set-server是否真的生效:
SHOW VARIABLES LIKE 'character_set_server'; SHOW VARIABLES LIKE 'lower_case_table_names';这里有个需要特别提醒的:lower_case_table_names参数在 MySQL 5.7 初始化之后修改可能不生效,或者在部分场景下导致表名大小写行为不一致。必须在初始化之前就确定好这个参数,写进 ConfigMap。尤其业务方如果从 Windows 开发环境搬过来,经常要求设成 1(不区分大小写),但如果初始化时是默认值 0,后面再去改,很容易出现表找不到的错误。
6.4 连接地址和 Service 问题
同集群内访问 MySQL,推荐用mysql-0.mysql-hs.database.svc.cluster.local:3306这样的 DNS 地址。如果业务 pod 报找不到主机名,先确认 Headless Service 已经存在:
kubectl -n database get svc kubectl -n database get endpoints mysql-hs如果 endpoints 里没有 IP,说明 selector 没匹配到 pod 标签。这种排查往往花的时间比部署本身还长,但套路是固定的:先排 DNS,再排 endpoints,再排网络策略。
7. 一点运维经验收尾
这套全量离线包方案后续还可以继续扩展,比如加一份mysqld_exporter镜像和 ServiceMonitor,把监控也纳入离线体系;或者把 MySQL 部署逻辑打包成 Helm chart,做版本化交付。不过不管怎么扩展,核心原则都是一样的:离线包的每一份资源都必须经过目标环境验证,不要想当然认为"在线能跑离线就能跑"。
我个人在多次交付后的感觉是,把时间花在前期资源梳理和文件校验上,比到现场临时 debug 要节省得多。离线部署最耗时的从来不是 K8s 本身,而是"少带了一个辅助镜像""存储类没确认""初始化脚本时机不对"这些看似不起眼的细节。你只要提前把这几点理清楚,MySQL 5.7 在 K8s 离线环境的部署其实就是机械执行而已。
本文还有配套的精品资源,点击获取