简介Kubernetes 环境下离线部署 MySQL 5.7镜像拉取与 YAML 编排往往最耗时间尤其在内网或受限网络中更为明显。这份全量离线包面向集群运维、开发测试人员无需互联网即可完成部署适合内网、生产隔离环境或快速实验场景。资源共 7 个文件包含 2 个 tar 镜像包、4 个 Kubernetes YAML 编排文件及 1 份说明文档压缩包整体约 281MB其中 tar 包可通过 load 方式直接导入节点YAML 已覆盖持久化卷、PVC、Deployment、Service 等常用对象可直接应用于集群。说明文档附带基本操作指引帮助理清镜像导入与资源创建顺序YAML 中的存储大小、副本数等参数也可按需调整。资源发布至今已有 1095 人学习下载对需要快速搭建 MySQL 5.7 服务或学习有状态应用编排的读者具有不错的参考价值。 内网生产环境Kubernetes集群外网一律不通业务侧还点名要 MySQL 5.7。这种场景我一年能碰上好几回每次都要跟“离线包”较劲。把 MySQL 5.7 的镜像、配置文件、初始化脚本、编排清单全部装进一个离线交付包到了现场解压、导入、应用、验证一条龙跑完才算完事。这篇文章就把这套流程完整地写出来从镜像导出到集群落地一步一步讲清楚适合正在做内网交付、或者打算把 MySQL 5.7 带进 K8s 的运维和开发同学参考。1. 先想清楚一件事离线部署要做“镜像是主角”的架构1.1 为什么不能把 MySQL 装进虚拟机再托管给 K8s有些团队一听到“内网部署 MySQL”就会产生一个惯性思路找一台虚拟机把 MySQL 装进去让 K8s 里的应用通过网络访问不就行了方案本身没毛病适合那些“不打算用容器管理数据库”的团队。但如果你们已经在用 K8s并且希望数据库也纳入容器编排体系那这条路就有点拧巴了。监控、日志、健康检查、扩缩容全部分裂数据库跑在虚拟机上应用跑在 Pod 里排障时要跨两套环境来回切换非常难受。更重要的是K8s 的原生模型决定了“镜像就是应用本体”。Pod 是调度单元容器是运行单元kubelet 只会做一件事按镜像描述把容器拉起来。MySQL 的运行依赖很多glibc 版本、SSL 库、配置文件、初始化脚本、时区数据这些都要提前揉进镜像里才能保证它不管调度到哪台节点上都能以一致的状态启动。离线部署更是如此因为你没有外网去临时 apt-get 安装依赖所有东西必须在镜像里“全量自包含”。1.2 离线部署的完整链路镜像、仓库、编排三者缺一不可离线部署 MySQL 的完整链路看起来只有三步准备镜像、传到内网、部署。但实际落地时至少要拆成四个环节在能联网的机器上拉取 MySQL 5.7 镜像导出成离线 tar 包。把 tar 包通过 U 盘、内网 FTP 等途径传进内网环境。在内网搭建一个镜像仓库把 tar 包导入并推送上去。编写 Kubernetes 的 PVC、Secret、ConfigMap、Deployment、Service 等编排文件让 Pod 从内网仓库拉取镜像并顺利运行。“全量离线包”这个概念我的理解是不只是给一个镜像文件而是一个完整的运维资产包镜像 tar 包、初始化 SQL、my.cnf 配置、K8s YAML 清单、部署说明文档。到了现场照着 README 一步步执行就能完成部署交付姿势才足够专业。2. 镜像准备MySQL 5.7 离线包的获取与导出2.1 镜像选型官方 mysql:5.7 是首选MySQL 5.7 的容器镜像不止一个我见过团队用过官方 mysql:5.7、percona/percona-server:5.7、bitnami/mysql:5.7各有各的长处。选哪个取决于你的场景但如果没有特殊需求官方镜像是最省心的。镜像基础特点适用场景mysql:5.7官方入口脚本成熟自动执行 /docker-entrypoint-initdb.d 下的初始化脚本通用生产环境社区资料最多排障方便percona/percona-server:5.7内置 XtraBackup物理备份能力强大库备份要求高的场景bitnami/mysql:5.7非 root 运行安全加固措施好安全审计要求严格的团队说句实话官方 mysql:5.7 我已经在多个生产项目里跑了好几年遇到问题能搜到的解决方案也最多。Percona 做数据库内核优化确实强但如果你只是需要“稳定跑起来”官方版足够。Bitnami 的镜像目录结构和启动逻辑跟官方差异比较大新手容易卡在配置挂载上不建议离线交付时冒这个险。2.2 导出与校验一条命令搞定镜像归档在能联网的机器上执行docker pull mysql:5.7 docker tag mysql:5.7 10.20.1.10:5000/mysql:5.7 docker save 10.20.1.10:5000/mysql:5.7 -o mysql-5.7-offline.tar这里有个小技巧先按内网仓库地址 tag再 docker save。这样导入内网仓库后镜像名已经带着仓库地址推送时不需要再改一次 tag能少踩一个“镜像名不对导致 push 失败”的坑。导出后的 tar 包大小在 500MB 左右为了传输方便我会再压缩一层gzip mysql-5.7-offline.tar压缩完大概 200MB 上下用 U 盘拷贝、内网传输都快很多。导入的时候 docker load 能直接读 gzip 包不用手动解压。打包之前养成生成校验值的习惯sha256sum mysql-5.7-offline.tar.gz mysql-5.7-offline.tar.gz.sha256这个 sha256 值要写进交付文档里。现场导入前先比对一下防止文件在传输过程中损坏特别是在走 FTP 或者 U 盘拷贝的场景下这一步能救命。3. 落地到集群从 tar 包到可拉取的镜像仓库3.1 选私有仓库还是逐节点导入拿到镜像 tar 包之后第一个岔路口是“逐节点导入”还是“搭一个私有仓库”。方案优点缺点每台节点 docker load / ctr images import不用额外搭服务节点多时操作繁琐Pod 漂移到新节点时镜像可能不存在内网搭建 Registry/Harbor集中管理版本统一K8s 调度随时拉取需要一台机器跑仓库服务初期多几步配置如果你只有一台单节点 K8s逐节点导入还能接受。节点超过三个或者后续有扩容计划我强烈建议搭一个私有仓库。原因很简单K8s 的调度是动态的你永远不知道一个 Pod 会在哪台节点上重启。如果那台节点没有本地镜像Pod 就会卡在 ImagePullBackOff。就算你改成 imagePullPolicy: IfNotPresent也只是“尽量避免拉取”并不能保证节点上一直有正确版本的镜像。统一仓库才是符合 K8s 行为的镜像分发方式。3.2 用 Registry 做中转搭建与连通性修正内网搭一个轻量级 Registry 非常快用一个容器就能搞定docker run -d \ -p 5000:5000 \ --name registry \ -v /data/registry:/var/lib/registry \ --restartalways \ registry:2然后导回镜像并推送docker load -i mysql-5.7-offline.tar.gz docker push 10.20.1.10:5000/mysql:5.7用 Docker 作为容器运行时还好配置一下 /etc/docker/daemon.json 的 insecure-registries 就能用。但现在新集群基本都是 containerd这里才是最容易翻车的地方。你需要修改 /etc/containerd/config.toml给仓库地址加一个 mirror 配置[plugins.io.containerd.grpc.v1.cri.registry.mirrors] [plugins.io.containerd.grpc.v1.cri.registry.mirrors.10.20.1.10:5000] endpoint [http://10.20.1.10:5000]注意 endpoint 里写的是 http 还是 https 要跟实际仓库一致。自建 Registry 默认是 http而 containerd 默认会按 https 尝试不配置 mirror 就必然报证书错误。改完配置重启 containerdsudo systemctl restart containerd验证连通性最好直接拉一次镜像crictl pull 10.20.1.10:5000/mysql:5.7这条命令能过说明 containerd 到仓库这条路彻底通了后面 K8s 部署时不会因为拉镜像卡壳。4. 核心编排MySQL 5.7 的 YAML 逐段精讲4.1 存储PVC 和存储类怎么选MySQL 是状态化应用数据必须落在持久化卷上。很多新手一上来就写 hostPath测试可以生产环境千万别这么干。hostPath 把数据绑死在节点目录上Pod 一旦被调度到别的节点数据就“丢了”——准确说是找不到了因为数据还在原节点上。PVC 才是正解它把存储和 Pod 生命周期解耦开。内网环境常见的选择是 local-path-provisioner 或者 NFS。local-path 用节点本地磁盘性能好但数据还是绑节点NFS 能跨节点共享但 MySQL 数据目录放在 NFS 上要小心 IO 和锁问题高并发下不一定稳定。我的习惯是单机测试环境用 local-path正式环境能用块存储比如 Ceph RBD尽量用块存储。下面这个 PVC 假设集群里有 local-path 存储类apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mysql-data namespace: mysql spec: accessModes: - ReadWriteOnce storageClassName: local-path resources: requests: storage: 20Gi这里 accessModes 用 ReadWriteOnce 是有讲究的一个卷同一时间只能被一个节点挂载。MySQL 单实例部署这样反而符合预期——避免多个 Pod 同时写同一份数据目录造成数据损坏。如果你的场景有多副本共享存储的需求那要考虑的是分布式数据库方案而不是让多个 MySQL Pod 共享一个 NFS。4.2 配置与密钥ConfigMap、Secret 怎么给 MySQL官方 mysql:5.7 镜像有自己的初始化逻辑当 /var/lib/mysql 为空时会执行初始化脚本设置 root 密码、根据环境变量创建数据库和应用账号之后挂载自定义配置文件到 /etc/mysql/conf.d 目录让 mysqld 读取。利用好这个机制整个过程可以做到“零手动操作”。创建一个 Secret 保存数据库密码apiVersion: v1 kind: Secret metadata: name: mysql-secret namespace: mysql type: Opaque data: root-password: cm9vdDEyMzQ1Ng app-password: QXBwMTIzNDU2data 的值是 base64 编码后的密码。注意 Secret 的 yaml 里也要用双引号包好防止特殊字符被解析出错。然后用 ConfigMap 写一份 my.cnfapiVersion: v1 kind: ConfigMap metadata: name: mysql-config namespace: mysql data: my.cnf: | [mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_general_ci default-time-zone08:00 max_connections500 innodb_buffer_pool_size2G lower_case_table_names1 sql_modeSTRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION几个参数逐个说一下utf8mb4 是现代应用的基本盘不然 emoji 存进去直接变问号default-time-zone 设置为 08:00解决容器默认 UTC 时间导致数据库时间差 8 小时的问题lower_case_table_names1 让表名不区分大小写Windows 上开发的应用迁到 Linux 上才不会因为表名大小写问题炸掉sql_mode 剔除了 ONLY_FULL_GROUP_BY给老程序留条活路。4.3 Deployment 与 Service让 Pod 按预期运行我这里用 Deployment 而不是 StatefulSet。单个 MySQL 实例、数据在 PVC 上Deployment 完全够用。如果以后要做主从复制需要稳定的网络标识和序号再考虑换 StatefulSet。写个完整的部署清单apiVersion: apps/v1 kind: Deployment metadata: name: mysql-5-7 namespace: mysql labels: app: mysql spec: replicas: 1 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: securityContext: fsGroup: 999 containers: - name: mysql image: 10.20.1.10:5000/mysql:5.7 imagePullPolicy: IfNotPresent ports: - containerPort: 3306 env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: mysql-secret key: root-password - name: MYSQL_DATABASE value: appdb - name: MYSQL_USER value: app - name: MYSQL_PASSWORD valueFrom: secretKeyRef: name: mysql-secret key: app-password volumeMounts: - name: mysql-data mountPath: /var/lib/mysql - name: mysql-config mountPath: /etc/mysql/conf.d/my.cnf subPath: my.cnf livenessProbe: exec: command: [sh, -c, mysqladmin ping -uroot -p${MYSQL_ROOT_PASSWORD} --silent] initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: exec: command: [sh, -c, mysqladmin ping -uroot -p${MYSQL_ROOT_PASSWORD} --silent] initialDelaySeconds: 10 periodSeconds: 5 volumes: - name: mysql-data persistentVolumeClaim: claimName: mysql-data - name: mysql-config configMap: name: mysql-configfsGroup: 999 非常关键。官方 MySQL 镜像里 mysqld 进程是以 mysql 用户UID 999运行的如果 PVC 目录的属主不是 999容器启动时就会因为没权限写入数据目录而 CrashLoopBackOff。local-path 这类本地存储分配出来的目录权限不一定是 999设置 fsGroup 可以保证 Pod 挂载时把目录组的权限调整到可写状态。探针里的 mysqladmin ping 只关心进程是否活着不关心 SQL 能不能查但对 MySQL 容器来说够用了。如果业务对健康检查要求更高可以换一个 SELECT 1 的探测语句但没必要为单实例 MySQL 搞太复杂。Service 暴露方式很直接apiVersion: v1 kind: Service metadata: name: mysql-svc namespace: mysql spec: selector: app: mysql ports: - port: 3306 targetPort: 3306 type: ClusterIP集群内其他应用直接通过 mysql-svc.mysql.svc.cluster.local:3306 访问。如果需要外部访问可以把 type 改成 NodePort但生产环境更推荐用负载均衡或者直接让应用走集群内部网络。5. 站到“可用”这一步验证、参数调优与日常运维5.1 首次部署后要做的验证部署完成后别急着收工先看 Pod 状态kubectl get pods -n mysql等 Pod 变成 Running 且 Ready 为 1/1 后进入容器验证kubectl exec -it mysql-5-7-xxx -- mysql -uroot -p进去后执行几个关键 SQLSELECT VERSION(); SHOW VARIABLES LIKE character_set_server; SHOW VARIABLES LIKE time_zone; USE appdb; SELECT 1;重点确认三件事版本确实是 5.7、字符集是 utf8mb4 而不是默认的 latin1、时间没有差 8 小时。字符集和时间这两个问题最容易在“表面正常”的情况下埋坑上线后才发现乱码或者日志时间不对那时候再改配置就要重启好几个人才能接受的窗口。5.2 生产常用的初始化参数上面 ConfigMap 里的参数只是基础生产环境还得根据机器规格再调。这里给一份我常用的生产追加配置[mysqld] server-id1 log-bin/var/lib/mysql/mysql-bin binlog_formatrow expire_logs_days7 max_connections500 innodb_buffer_pool_size4G innodb_log_file_size1G sync_binlog1 innodb_flush_log_at_trx_commit1innodb_buffer_pool_size 建议设置为物理内存的 50%~70%前提是这台节点没有跑高占用的其他业务。sync_binlog1 和 innodb_flush_log_at_trx_commit1 组合最安全但会牺牲一部分写入性能追求性能可以折中调整。加 binlog 的好处是之后做增量备份和数据恢复都比较方便反正磁盘便宜这笔账算得过来。5.3 离线环境下的备份与恢复备份这种事一次不跑出了事故全公司都知道。在 K8s 里最稳的备份方式是用 mysqldump 逻辑备份到集群外部kubectl exec -it mysql-5-7-xxx -- mysqldump -uroot -p --single-transaction --master-data2 -A backup.sql恢复就反向操作cat backup.sql | kubectl exec -i mysql-5-7-xxx -- mysql -uroot -p--single-transaction 对 InnoDB 表做一致快照不影响线上写入。如果库特别大逻辑备份会跑很久那就考虑 percona 镜像的 XtraBackup 物理备份但复杂度会高一个档次。备份文件千万别放在 MySQL 同一个 PVC 上否则存储真出了事数据文件和备份一起没那就真的凉了。6. 复盘与避坑清单6.1 我踩过的几个典型问题问题现象根因解决方法Pod 一直 CrashLoopBackOff日志报权限错误/var/lib/mysql 或挂载目录属主不是 999补上 securityContext.fsGroup: 999或手动 chown -R 999:999时间查询结果差 8 小时容器默认 UTCmysqld 也按 UTC 启动在 my.cnf 里加 default-time-zone08:00ImagePullBackOffcrictl pull 报证书错误containerd 没配置 http 仓库的 mirror或 endpoint 写错修改 /etc/containerd/config.toml 的 mirrors 并重启自定义配置不生效字符集还是 latin1挂载方式不对把整个 my.cnf 覆盖了镜像默认配置用 subPath 挂载单个文件到 /etc/mysql/conf.d 目录初始化脚本没有执行业务表没创建映射 /docker-entrypoint-initdb.d 目录后才能执行初始化 SQL把初始化脚本放进 ConfigMap挂到 /docker-entrypoint-initdb.d 目录第一个权限问题是最常见的很多第一次把 MySQL 用 PV 部署到 K8s 的人都栽在这里。第二个时区问题隐蔽性很高表面一切正常数据时间就是不对排查思路要往容器时区想。第三个 pull 镜像失败不要光看 K8s 事件直接到节点上用 crictl pull 手动试一次能快速定位是仓库问题还是节点问题。6.2 离线交付的小技巧交付一个“全量离线包”时我会把它组织成下面这个结构mysql5.7-offline/ ├── mysql-5.7-offline.tar.gz ├── mysql-5.7-offline.tar.gz.sha256 ├── mysql-deploy.yaml ├── init.sql ├── my.cnf └── README.mdREADME.md 里写清楚每一步命令包括镜像导入、仓库搭建、kubectl apply 的顺序以及管理员密码放在哪个 Secret 里。taar 包的 sha256 校验值必须带上现场对比完再导入宁可多花 30 秒校验也不要在传输出错后在集群里排查半天。镜像 tag 尽量固定到具体版本比如 mysql:5.7.44别用 mysql:5.7 这种可变 tag因为时间久了你不会记得这个 tag 当时对应的是哪个小版本。这个交付包做好之后以后遇到同样的环境直接复制整个流程跑一遍就行。整套玩下来你就会发现离线部署 MySQL 5.7 本身并不难难点全在“提前把所有可能发生的事故都预演了一遍”。多跑几次把坑提前踩掉后面就都是体力活了。本文还有配套的精品资源点击获取