MySQL 容器化与数据安全:初始化、持久化、备份、升级和恢复
MySQL 是有状态服务。把 mysql:8.0 跑起来只是起点,真正要理解的是:官方镜像 Entrypoint 如何判断数据目录是否为空,初始化环境变量和 /docker-entrypoint-initdb.d 为什么只执行一次,/var/lib/mysql 中有哪些不能随意复制的文件,容器内存如何被 Buffer Pool、连接线程和临时缓冲共同消耗,SIGTERM 后 InnoDB 如何关闭,以及升级镜像后为什么不能简单切回旧版本。
本页聚焦“MySQL如何安全运行在Docker中”。SQL、索引、事务、MVCC、redo/binlog、复制和SQL优化的数据库原理继续学习 MySQL从零到生产级掌握。容器不会改变MySQL内部事务原理,也不会自动提供高可用和备份。
学习目标
学完后,应能:
- 解释官方 MySQL 镜像从 Entrypoint 到正式 mysqld 的首次初始化全过程。
- 说明
MYSQL_ROOT_PASSWORD、MYSQL_DATABASE、MYSQL_USER为什么对已有数据目录不再重新初始化。 - 正确使用 Named Volume、Bind Mount、Secret、只读配置和 init script,不使用 privileged。
- 解释 MySQL 内存模型与容器 cgroup 限制,避免 Buffer Pool 和连接数共同触发 OOMKilled。
- 设计字符集、排序规则、时区、端口、网络、账号、日志和健康检查。
- 完成逻辑备份、恢复验证、binlog/PITR思路和RPO/RTO设计。
- 解释为什么不能直接 tar 运行中的
/var/lib/mysql,也不能把 Volume 当成备份。 - 制定小版本与大版本升级、金丝雀、数据副本和失败回退方案。
- 定位初始化失败、Access denied、数据“消失”、权限、磁盘、OOM、崩溃恢复和升级失败。
一、容器化没有改变MySQL的核心结构
flowchart TD
A["客户端或Spring Boot"] --> B["Docker网络与容器3306"]
B --> C["mysqld连接层"]
C --> D["Server层解析、优化、执行和binlog"]
D --> E["InnoDB Buffer Pool、锁、MVCC和redo"]
E --> F["/var/lib/mysql中的表空间、redo、undo和数据字典"]
F --> G["Named Volume或宿主机存储"]Docker主要改变:
- mysqld如何被打包和启动。
- 配置、Secret和数据如何注入。
- 网络和端口如何暴露。
- cgroup如何限制CPU、内存和PIDs。
- 容器和数据目录生命周期如何分离。
Docker不会自动实现:
- 事务一致性。
- 主从复制。
- 自动故障转移。
- 时间点恢复。
- 跨节点Volume高可用。
- SQL优化。
二、镜像Tag、版本和CPU架构
学习Demo可以使用:
docker pull mysql:8.0但 8.0 仍是可变Tag,未来可能指向新的8.0补丁镜像。生产应:
- 选择组织支持的MySQL版本线。
- 固定已验证补丁Tag或镜像Digest。
- 记录镜像来源、Image ID和Digest。
- 在预发使用真实数据量副本验证升级。
- 阅读MySQL与镜像Release Notes。
查看:
docker image inspect mysql:8.0
docker image ls --digests mysqlCPU架构也要匹配:
docker image inspect \
--format 'os={{.Os}} arch={{.Architecture}} digests={{json .RepoDigests}}' \
mysql:8.0MySQL 5.7、8.0、8.4 等版本线在数据字典、默认认证插件、字符集、参数和升级路径上有差异。具体支持周期会变化,应查当前官方生命周期;不要把旧版数据目录直接交给任意新版镜像试启动。
三、官方镜像Entrypoint首次启动全过程
官方镜像通常不是直接执行一个裸 mysqld。Entrypoint 会准备目录、读取初始化环境变量、判断数据目录是否已经初始化,然后再决定是否执行初始化流程。
flowchart TD
A["容器启动docker-entrypoint"] --> B["解析最终mysqld命令和配置"]
B --> C["准备数据目录和运行用户权限"]
C --> D{"数据目录是否已有系统数据"}
D -- "已有" --> E["跳过MYSQL_*初始化和init脚本"]
D -- "为空" --> F["校验root密码等初始化变量"]
F --> G["初始化MySQL系统表和数据字典"]
G --> H["启动仅供初始化的临时mysqld"]
H --> I["创建root策略、默认库和业务用户"]
I --> J["按顺序执行init目录脚本"]
J --> K["停止临时mysqld"]
K --> L["exec正式mysqld作为主进程"]
E --> L具体脚本实现和支持扩展名会随镜像版本变化,但核心判断是:
数据目录是否已经初始化。
3.1 为什么要启动临时mysqld
创建数据库、用户、授权和执行SQL需要一个可运行的MySQL实例。Entrypoint通常先启动限制网络访问的临时服务,在容器内部完成初始化,停止后再启动正式mysqld。
3.2 为什么第一次启动更慢
首次启动可能执行:
- 创建系统表和数据字典。
- 初始化InnoDB文件。
- 创建账号与数据库。
- 导入初始化SQL。
- 写入大量初始数据。
因此健康检查要有合理 start_period。不能应用刚启动几秒就判死并反复重启,否则初始化可能永远无法完成。
四、初始化环境变量只对空数据目录生效
常见变量:
| 变量 | 首次初始化作用 |
|---|---|
MYSQL_ROOT_PASSWORD | 设置root密码 |
MYSQL_ROOT_PASSWORD_FILE | 从文件读取root密码 |
MYSQL_DATABASE | 创建默认数据库 |
MYSQL_USER | 创建普通用户 |
MYSQL_PASSWORD | 设置普通用户密码 |
MYSQL_PASSWORD_FILE | 从文件读取普通用户密码 |
MYSQL_RANDOM_ROOT_PASSWORD | 生成随机root密码并输出到受控日志,使用时需安全提取 |
MYSQL_ALLOW_EMPTY_PASSWORD | 允许空root密码,生产不应使用 |
实际支持项以目标镜像版本文档为准。
4.1 为什么修改环境变量后密码没变
第一次启动:
Volume为空
↓
Entrypoint读取MYSQL_ROOT_PASSWORD
↓
创建MySQL账号并写入系统表后续启动:
Volume已有mysql系统数据
↓
Entrypoint判断已初始化
↓
跳过初始化账号逻辑
↓
MySQL继续使用系统表中的原密码环境变量不是每次启动都覆盖数据库账号。修改已有账号应通过受控SQL:
ALTER USER 'order_app'@'%' IDENTIFIED BY '新的受控密码';同时更新Secret、连接池并设计轮换窗口。不要删除生产数据目录来“让新密码生效”。
4.2 _FILE为什么更合适
普通环境变量可通过 container inspect、诊断包和进程环境暴露。*_FILE 让Entrypoint从挂载文件读取,适合Docker Secret或受控文件:
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql-root-password源Secret文件仍需权限、加密、轮换和审计;本地Compose Secret不自动等于企业Secret Manager。
五、/docker-entrypoint-initdb.d只在首次初始化执行
常见目录:
/docker-entrypoint-initdb.d可放SQL或Shell初始化文件,支持类型和执行规则以镜像版本为准。文件通常按名称排序,因此建议:
001-schema.sql
010-dictionary.sql
020-seed-local.sql示例:
CREATE TABLE IF NOT EXISTS orders (
id BIGINT NOT NULL AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_orders_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;5.1 为什么后来新增SQL没有执行
因为Volume已经初始化,Entrypoint会跳过整个 initdb 流程。这个目录适合首次创建环境,不适合持续管理生产Schema。
商业项目应使用:
- Flyway。
- Liquibase。
- 受控DDL平台。
- 版本化数据库变更流程。
迁移脚本需要版本、幂等边界、失败处理、向前兼容和回滚策略。
5.2 初始化脚本失败会怎样
可能留下部分初始化状态。下一次Entrypoint是否重新执行取决于数据目录已形成到什么程度和镜像逻辑。不能假定自动从头原子重试。
测试环境可以清理明确无价值的实验Volume后重新初始化;生产必须检查系统表、已执行SQL和业务数据,再制定修复脚本,不能直接删卷。
六、为什么必须挂载/var/lib/mysql
容器可写层随容器删除。MySQL默认数据目录通常是:
/var/lib/mysql命名卷:
docker volume create order-mysql-data
docker run -d \
--name order-mysql \
--mount type=volume,source=order-mysql-data,target=/var/lib/mysql \
mysql:8.0这里只展示挂载,不包含密码初始化,完整可运行Demo见后文。
6.1 数据目录里有什么
可能包含:
- InnoDB系统表空间和独立表空间。
- 数据字典。
- redo log。
- undo表空间。
- binlog及索引。
- 系统库和用户权限。
- 临时与状态文件。
这些文件相互存在一致性关系,不是普通静态文件集合。
6.2 Named Volume与Bind Mount怎么选
| 方式 | 优点 | 风险 |
|---|---|---|
| Named Volume | Docker管理位置和生命周期,权限相对简单 | 仍需知道宿主机故障域和备份位置 |
| Bind Mount | 路径明确,便于纳入宿主机存储规划 | UID/GID、SELinux、路径错误和误操作更明显 |
| 网络/云存储Volume | 可与外部存储结合 | 时延、fsync语义、锁、故障恢复必须验证 |
数据库存储不能只看“能挂载”。必须验证 fsync、延迟、IOPS、故障语义、快照一致性和恢复时间。
七、挂载后数据“消失”的常见原因
7.1 项目名变化创建新卷
Compose卷:
volumes:
mysql-data:项目名为 order-prod 时,实际可能叫:
order-prod_mysql-data项目名改为 order-platform 后会创建新卷。新MySQL初始化为空库,旧数据卷可能仍存在。
检查:
docker compose ls
docker compose config
docker volume ls
docker inspect --format '{{json .Mounts}}' <mysql容器>7.2 Bind路径写错
宿主机源路径拼错、相对路径基准变化或Docker Desktop文件共享异常,都会挂到另一个空目录。
7.3 执行down -v或删除Volume
docker compose down -v 可能删除项目卷。Volume只是独立于普通容器删除,不是备份。
7.4 原来就没有挂载
数据一直写在容器可写层,删除旧容器后真正丢失。必须在创建时 inspect Mounts,而不是事后只看当前Compose文件。
八、不要使用--privileged解决MySQL权限
旧教程常见:
--privileged=true它会显著扩大容器能力和设备访问范围,通常与MySQL目录权限问题无关。正确排查:
docker image inspect --format 'user={{.Config.User}}' mysql:8.0
docker inspect --format '{{json .Mounts}}' order-mysql
docker logs --tail 200 order-mysqlBind Mount时检查宿主机数字UID/GID、目录权限和SELinux标签。不要使用 chmod 777,也不要让MySQL以高权限访问整个宿主机。
九、配置文件如何挂载
官方镜像常读取:
/etc/mysql/conf.d/*.cnf目录和优先级以镜像内默认配置为准。建议使用后缀明确的只读文件:
conf.d/zz-order.cnf示例:
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
default-time-zone=+00:00
max-connections=200
innodb-buffer-pool-size=1GMySQL配置项常用下划线名称,命令行/配置文件的连字符和下划线兼容规则应按版本确认。启动后以真实变量为准:
SHOW VARIABLES LIKE 'character_set_server';
SHOW VARIABLES LIKE 'collation_server';
SHOW VARIABLES LIKE 'time_zone';
SHOW VARIABLES LIKE 'max_connections';
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';9.1 配置语法错误
错误参数可能让 mysqld 在启动前退出:
docker logs --timestamps --tail 300 order-mysql
docker inspect --format '{{json .State}}' order-mysql可在同版本镜像中验证配置,但不要把正在使用的数据卷挂给临时验证容器。使用只读配置和空临时环境执行 mysqld --verbose --help 等检查,具体命令行为按版本确认。
9.2 命令行参数覆盖
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci命令行通常具有较高优先级。排查时要同时看:
docker inspect --format 'path={{json .Path}} args={{json .Args}}' order-mysql
docker inspect --format '{{json .Mounts}}' order-mysql十、字符集、排序规则和时区
10.1 使用utf8mb4
MySQL历史 utf8 通常不是完整四字节UTF-8,Emoji等字符需要 utf8mb4。
要统一:
- Server默认字符集。
- Database和Table字符集。
- Column字符集。
- JDBC连接字符集。
- 排序规则。
排序规则影响大小写、重音、比较和索引结果。MySQL 8新增的某些collation不能直接用于旧版本。跨版本兼容场景要明确选择。
10.2 时区分层
存在:
- 宿主机时钟与NTP。
- 容器
TZ。 - mysqld
default_time_zone。 - MySQL global/session time_zone。
- JDBC时区。
- 应用JVM时区。
- TIMESTAMP/DATETIME语义。
生产更适合统一时间点为UTC,在展示层转换业务时区。只设置 TZ=Asia/Shanghai 不能解决所有时间问题。
十一、网络与端口暴露
如果只有同一Docker网络中的Spring Boot访问MySQL,不需要发布宿主机端口:
services:
mysql:
image: mysql:8.0
# 不配置ports应用使用:
jdbc:mysql://mysql:3306/order_db本地调试若需要宿主机连接:
ports:
- "127.0.0.1:13306:3306"不要默认发布:
0.0.0.0:3306否则可能暴露到局域网或公网,仍受防火墙和云安全组影响。数据库安全还需要账号最小权限、TLS、密码策略和审计。
容器中的 localhost 只表示当前容器。Spring Boot容器连接MySQL不能写 localhost:3306,除非两者确实在同一Network Namespace中,这不是普通Compose架构。
十二、账号与权限
业务应用不要使用root。首次初始化时创建:
MYSQL_DATABASE=order_db
MYSQL_USER=order_app
MYSQL_PASSWORD_FILE=/run/secrets/mysql-app-password还需根据业务最小授权。不要给应用用户:
- 全局管理员权限。
- 任意账号管理。
- 不必要的DDL权限。
- FILE等高风险权限。
生产Schema迁移可使用独立迁移账号,运行账号只具备必要DML权限。账号来源host、TLS要求、密码轮换和连接池刷新必须一起设计。
十三、MySQL内存如何超过容器限制
容器限制覆盖整个mysqld进程,而MySQL内存不只有Buffer Pool:
MySQL总内存
├── InnoDB Buffer Pool
├── Performance Schema
├── 数据字典与表缓存
├── redo/binlog缓冲
├── 每连接线程栈
├── 每连接sort/join/read等Buffer
├── 临时表
├── 连接和协议Buffer
└── 其他插件与本地内存13.1 为什么只设置Buffer Pool还会OOM
假设:
容器限制 = 2GiB
innodb_buffer_pool_size = 1.5GiB
max_connections = 500剩余约0.5GiB还要容纳全部全局结构和连接内存。高并发排序、Join或大包可能使按连接分配的内存迅速增长,最终cgroup OOM Killer杀死mysqld。
13.2 内存预算思路
粗略上界思路:
全局常驻内存
+ 活跃连接数 × 每连接实际内存
+ 临时峰值
+ 安全余量
< 容器内存限制不能简单用 max_connections × 所有session buffer最大值 作为精确常驻内存,因为很多Buffer按需分配;但它能提醒连接配置和SQL行为可能产生巨大峰值。
13.3 容器限制不会自动调小MySQL
不要假定 --memory=2g 会自动把 Buffer Pool、安全连接数和临时表都调整为合理值。必须显式配置并通过压测、RSS和MySQL指标验证。
检查:
docker inspect --format 'memory={{.HostConfig.Memory}} oom={{.State.OOMKilled}} exit={{.State.ExitCode}}' order-mysql
docker stats --no-stream order-mysqlMySQL:
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Threads_running';
SHOW VARIABLES LIKE 'max_connections';
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
SHOW GLOBAL STATUS LIKE 'Created_tmp%';十四、CPU与IO限制
MySQL性能常受:
- cgroup CPU quota和throttling。
- 磁盘IOPS与延迟。
- fsync语义。
- Buffer Pool命中率。
- 刷脏页。
- redo/binlog刷盘。
- 大查询和排序。
- 锁等待。
容器CPU低会影响查询、后台刷脏和崩溃恢复。存储延迟高会使事务提交和checkpoint变慢。
检查容器:
docker stats --no-stream order-mysql
docker inspect --format 'nanoCpus={{.HostConfig.NanoCpus}} blkio={{json .HostConfig.BlkioConfig}}' order-mysql宿主机:
iostat -xz 1
pidstat -d 1MySQL慢SQL、锁和Buffer Pool原理回到MySQL主专栏分析,不能看到容器IO高就直接扩容CPU。
十五、健康检查的语义
基础存活:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
interval: 10s
timeout: 5s
start_period: 60s
retries: 10mysqladmin ping 主要证明服务端进程可以响应,甚至认证失败时也可能表达“服务器活着”;它不证明:
- 业务账号可登录。
order_db存在。- 表结构迁移完成。
- 主库可写。
- 复制无延迟。
- 磁盘空间充足。
商业Readiness应按架构增加低成本检查,但不能每10秒执行重查询。应用仍需连接超时、退避重试和连接池恢复。
15.1 为什么start_period重要
首次初始化、大数据恢复和崩溃恢复可能耗时。过短探针会把正常恢复误判失败,配合自动重启形成永远恢复不完的循环。
十六、SIGTERM、正常关闭与崩溃恢复
容器停止:
docker stop --time 60 order-mysql正常路径:
flowchart TD
A["Docker向mysqld发送SIGTERM"] --> B["停止接收新连接"]
B --> C["处理或终止会话与事务"]
C --> D["刷新必要日志和状态"]
D --> E["InnoDB执行正常关闭"]
E --> F["mysqld退出"]如果宽限期不足,Docker发送SIGKILL,MySQL下次启动需要 InnoDB 崩溃恢复:
扫描redo
↓
重放需要的页修改
↓
回滚未提交事务
↓
恢复一致状态redo原理见 redo log与binlog。崩溃恢复时间与redo量、脏页、大事务、磁盘速度有关。不要因恢复慢再次强杀,可能形成长期不可用。
十七、日志应该从哪里看
第一轮:
docker logs --timestamps --tail 300 order-mysql
docker inspect --format '{{json .HostConfig.LogConfig}}' order-mysql
docker inspect --format '{{.LogPath}}' order-mysql官方镜像默认错误日志行为受镜像配置影响,常可从stdout/stderr看到初始化和启动信息。如果自行将 error log、slow log 写文件,必须设计:
- 文件路径和Volume。
- UID/GID。
- 轮转。
- 集中采集。
- 磁盘告警。
- 敏感SQL脱敏。
慢查询日志会增加IO和磁盘,应按分析目标配置阈值和采集,不应无限保留。
十八、为什么不能直接复制运行中的数据目录
运行中不同文件可能处于不同时间点:
- 数据页尚未刷盘。
- redo已写但页未写。
- undo记录进行中。
- binlog位置继续推进。
- 数据字典和表空间发生变化。
直接:
tar /var/lib/mysql可能得到不可恢复或逻辑不一致的文件集合。
18.1 三类备份
| 类型 | 示例 | 特点 |
|---|---|---|
| 逻辑备份 | mysqldump/mysqlpump类工具 | 跨环境较灵活,恢复大库较慢 |
| 物理热备 | MySQL Enterprise Backup、兼容版本XtraBackup等 | 大库更快,但版本和流程要求高 |
| 存储快照 | LVM/云盘/CSI快照 | 快,但必须与数据库刷盘、锁和一致性协调 |
Volume快照本身也不自动保证数据库一致性。
十九、逻辑备份Demo
推荐用受控客户端配置文件,避免密码直接出现在命令参数:
[client]
user=root
password=替换为受控Secret
host=localhost将其只读挂载为:
/run/secrets/mysql-client.cnf备份:
docker exec order-mysql \
mysqldump \
--defaults-extra-file=/run/secrets/mysql-client.cnf \
--single-transaction \
--routines \
--events \
--triggers \
--hex-blob \
--set-gtid-purged=OFF \
--databases order_db \
> order_db.sql注意:MySQL客户端通常要求 --defaults-extra-file 放在较前位置,具体工具解析规则按版本验证。
19.1 --single-transaction解决什么
它在一致性快照中导出 InnoDB 表,减少长时间锁表。但:
- 非事务表不获得同等一致性。
- 备份期间DDL可能破坏一致性。
- 大事务和长快照会影响undo清理。
- 它不自动包含备份后的binlog增量。
19.2 备份文件还要做什么
- 检查退出码。
- 检查文件非空和大小变化。
- 计算校验和。
- 加密。
- 上传独立存储。
- 设置保留策略。
- 记录MySQL版本、GTID/binlog位置。
- 定期恢复演练。
只有生成SQL文件,没有验证恢复,不算完成备份。
二十、恢复必须在隔离环境验证
不要先覆盖生产。启动同版本测试MySQL并使用独立Volume,再导入:
docker exec -i mysql-restore-test \
mysql \
--defaults-extra-file=/run/secrets/mysql-client.cnf \
< order_db.sql恢复后验证:
- 数据库和表数量。
- 关键行数与校验。
- 视图、触发器、存储过程、事件。
- 字符集和排序规则。
- 业务账号权限。
- 应用关键查询。
- 恢复耗时。
20.1 RPO与RTO
- RPO:最多允许丢多少时间的数据。
- RTO:故障后多久必须恢复服务。
每天一次全量备份意味着理论上可能丢接近一天数据,若RPO要求5分钟,需要binlog持续归档、复制或更高频策略。恢复1TB逻辑备份可能远超RTO,需要物理备份、快照、预热副本等方案。
二十一、binlog与时间点恢复
误删数据恢复通常需要:
最近一次可用全量备份
+
备份之后连续完整的binlog
↓
恢复到误操作前时间点或事务位置必须提前配置:
- binlog启用。
- server_id。
- binlog格式。
- 保留时间。
- 独立归档。
- 时间/GTID记录。
- 恢复工具和演练。
binlog与redo职责不同。redo用于InnoDB崩溃恢复,binlog用于复制和增量回放,详见 redo log与binlog。
二十二、升级镜像的正确流程
22.1 小版本也要验证
即使同一8.0线,也可能变化:
- 安全修复。
- 优化器行为。
- 默认参数。
- 认证与TLS。
- 系统表升级。
- 镜像操作系统库。
22.2 大版本升级
flowchart TD
A["阅读目标版本升级文档"] --> B["全量备份、binlog和恢复演练"]
B --> C["克隆生产数据到隔离环境"]
C --> D["执行兼容性检查和升级"]
D --> E["验证启动、表、权限、查询和性能"]
E --> F["灰度副本或维护窗口切换"]
F --> G{"是否正常"}
G -- "否" --> H["停止写入并按备份/旧副本回退"]
G -- "是" --> I["继续监控并完成升级"]22.3 为什么不能直接切回旧镜像
新MySQL可能已经升级数据字典和磁盘格式。把升级后的同一数据目录交给旧版本,可能不支持降级甚至进一步损坏。
可靠回退通常依赖:
- 升级前一致性备份。
- 未升级的复制副本。
- 升级前存储快照与数据库协调。
- 明确停止写入和数据差异处理。
“镜像Tag切回去”不是数据库回滚。
二十三、可运行Compose Demo
目录:
mysql-stack/
├── compose.yaml
├── conf.d/
│ └── zz-order.cnf
├── init/
│ └── 001-schema.sql
└── secrets/
├── mysql-root-password.txt
├── mysql-app-password.txt
└── mysql-client.cnfSecret文件只能用于本地学习时手工准备,并加入 .gitignore。生产由Secret Manager或部署平台生成,不提交Git。
23.1 配置文件
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
default-time-zone=+00:00
max-connections=200
innodb-buffer-pool-size=1G
server-id=1
log-bin=mysql-bin
binlog-format=ROW
binlog-expire-logs-seconds=604800该配置按2GiB容器给出学习起点。生产需结合连接数、SQL、复制、备份和压测调整。binlog-expire-logs-seconds 等参数要求目标MySQL版本支持。
23.2 Compose
name: order-mysql-lab
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql-root-password
MYSQL_DATABASE: order_db
MYSQL_USER: order_app
MYSQL_PASSWORD_FILE: /run/secrets/mysql-app-password
secrets:
- mysql-root-password
- mysql-app-password
- source: mysql-client-cnf
target: mysql-client.cnf
volumes:
- mysql-data:/var/lib/mysql
- type: bind
source: ./conf.d
target: /etc/mysql/conf.d
read_only: true
- type: bind
source: ./init
target: /docker-entrypoint-initdb.d
read_only: true
ports:
- "127.0.0.1:13306:3306"
mem_limit: 2g
cpus: 2
pids_limit: 300
stop_grace_period: 60s
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
interval: 10s
timeout: 5s
start_period: 60s
retries: 10
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
secrets:
mysql-root-password:
file: ./secrets/mysql-root-password.txt
mysql-app-password:
file: ./secrets/mysql-app-password.txt
mysql-client-cnf:
file: ./secrets/mysql-client.cnf
volumes:
mysql-data:23.3 初始化SQL
USE order_db;
CREATE TABLE IF NOT EXISTS orders (
id BIGINT NOT NULL AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_orders_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;23.4 启动前验证
docker compose config
docker compose pull
docker compose up -d --wait23.5 启动后验证
docker compose ps -a
docker compose logs --timestamps --tail 300 mysql
docker compose top mysql
docker compose exec mysql mysql -uorder_app -p order_db-p 不带密码会交互提示,避免密码直接出现在命令行历史和进程参数。
进入后:
SHOW DATABASES;
USE order_db;
SHOW TABLES;
SHOW VARIABLES LIKE 'character_set_server';
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
SELECT @@version, @@global.time_zone, @@session.time_zone;23.6 证明初始化脚本只运行一次
- 启动并确认orders表存在。
- 在
init新增另一个SQL。 - 执行
docker compose down后重新up。 - 新SQL不会因已有Volume自动执行。
不要在有价值数据环境执行 down -v。若要做“空卷首次初始化”实验,应使用全新项目名或明确无价值的新Volume。
二十四、商业场景一:修改密码环境变量不生效
现象:修改 MYSQL_ROOT_PASSWORD_FILE 内容并重启容器,旧密码仍有效。
根因:Volume已经初始化,Entrypoint跳过账号创建逻辑。环境变量是首次初始化输入,不是持续账号控制器。
正确处理:使用 ALTER USER 修改数据库系统表中的账号凭据,更新Secret并让连接池安全刷新。生产密码轮换要处理旧/新凭据切换窗口和失败回退。
二十五、商业场景二:Compose改目录后出现空库
现象:迁移部署目录后执行 docker compose up,MySQL自动初始化,业务表全部不见。
证据:
docker compose ls
docker volume ls
docker inspect --format '{{json .Mounts}}' <mysql容器>根因:默认项目名随目录变化,创建了新前缀Volume。旧卷仍在,但新容器挂的是空卷。
处理:停止写入,确认旧卷身份和备份,在维护窗口挂回正确卷。长期使用显式项目名或 external volume,并在发布前比较 docker compose config 和 Mounts。
二十六、商业场景三:容器扩容内存后仍OOM
现象:MySQL容器从2GiB扩到4GiB后短暂稳定,流量增长时再次 OOMKilled=true。
发现:
- Buffer Pool占2.5GiB。
- max_connections设为2000。
- 报表SQL大量排序和临时表。
- 应用连接池总和远超数据库设计容量。
根因不是单纯“内存小”,而是连接容量、每连接峰值、SQL和Buffer Pool未统一预算。修复包括限制应用连接池、优化SQL、控制并发、调整内存项并压测;扩内存只能止损。
二十七、商业场景四:升级后无法切回旧镜像
现象:MySQL 8大版本升级后查询异常,直接切回旧镜像,旧服务拒绝读取数据目录。
根因:数据字典或磁盘格式已经升级,镜像回滚不等于数据库降级。
正确方案:升级前在隔离环境演练,保留一致性备份或未升级副本;异常时停止写入,按预案切换旧副本或恢复备份并处理升级期间增量。
二十八、常见故障排查
28.1 容器一直Restarting
docker ps -a --filter name=order-mysql
docker inspect --format '{{json .State}}' order-mysql
docker logs --timestamps --tail 500 order-mysql
docker inspect --format '{{json .Mounts}}' order-mysql分类:
- 初始化密码缺失。
- 配置参数错误。
- 数据目录权限。
- 数据版本不兼容。
- 磁盘满或只读。
- OOMKilled。
- 崩溃恢复尚未完成。
28.2 Access denied
TCP已经连接到MySQL,重点检查:
- 用户名。
- 密码和Secret是否同步。
- 用户的host匹配。
- 认证插件与客户端驱动。
- TLS要求。
- 数据库权限。
不要继续修改Docker bridge或开放3306公网端口。
28.3 Connection refused或timeout
按层检查:
docker compose ps
docker network inspect <网络名>
docker logs --tail 200 order-mysql
docker port order-mysql- refused:目标可达但端口未监听或明确拒绝。
- timeout:路由、防火墙、丢包或服务卡住。
- Spring容器应通过服务名
mysql:3306,不是localhost。
28.4 Too many connections
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW PROCESSLIST;不能只增大 max_connections。检查应用连接池总和、连接泄漏、慢SQL、长事务、空闲超时和数据库内存预算。
28.5 磁盘满
df -h
df -i
docker system df -v
docker inspect --format '{{json .Mounts}}' order-mysql检查数据文件、binlog、临时文件、慢日志、Docker日志和备份。不要手工删除InnoDB、redo、undo或当前binlog文件;使用MySQL支持的保留和清理机制。
28.6 启动很慢
日志若显示 InnoDB recovery,可能正在重放redo和回滚事务。检查磁盘IO、上次是否SIGKILL、大事务和恢复进度。不要因“还没启动”反复强杀。
28.7 配置没有生效
docker inspect --format '{{json .Mounts}}' order-mysql
docker inspect --format 'path={{json .Path}} args={{json .Args}}' order-mysql再用 SHOW VARIABLES 看最终值。修改Compose配置后只restart旧容器不一定更新Mount或Command,需要重建。
二十九、面试标准回答
MySQL官方镜像首次启动做什么
Entrypoint解析mysqld配置并准备目录,判断
/var/lib/mysql是否已有系统数据。空目录时校验MYSQL_*初始化变量,初始化系统表和数据字典,启动临时mysqld,创建root策略、默认库和普通用户,按顺序执行initdb目录脚本,再停止临时服务并exec正式mysqld。已有数据目录则跳过初始化变量和脚本,直接启动正式服务。
为什么修改MYSQL_ROOT_PASSWORD不生效
它是空数据目录首次初始化的输入。第一次启动后密码已经存入MySQL系统表,后续同一Volume启动时Entrypoint检测到已初始化并跳过账号创建,所以修改环境变量不会覆盖已有密码。应通过ALTER USER受控轮换账号,同时更新Secret和连接池,不能删除生产Volume。
为什么MySQL必须挂Volume但Volume又不是备份
Volume让数据生命周期独立于容器,删除或重建容器后可以重新挂载;但Volume通常仍在单宿主机或单存储故障域,误删、磁盘损坏、down -v和错误升级仍会丢数据。备份还需要一致性副本、独立存储、保留、校验、binlog和恢复演练。
为什么不能tar运行中MySQL数据目录
运行时数据页、redo、undo、binlog和数据字典可能处于不同时间点,直接文件复制可能得到逻辑不一致的集合。应使用mysqldump一致性快照、兼容物理热备工具,或在数据库协调下做存储快照,并必须恢复验证。
MySQL容器为什么会OOM
容器限制覆盖整个mysqld,除Buffer Pool外还有Performance Schema、表缓存、连接线程栈、sort/join/read Buffer、临时表和插件内存。Buffer Pool过大、max_connections过高和重SQL并发会共同突破cgroup限制。应统一预算、限制应用连接池、优化SQL并压测,不能只调大容器内存。
MySQL镜像升级为什么不能直接回滚Tag
新版本可能升级系统表、数据字典和磁盘格式,同一数据目录未必支持旧版本读取。数据库回退依赖升级前一致性备份、存储快照或未升级副本,并处理升级期间新增数据;镜像Tag切回不等于数据降级。
三十、学习验收
不看答案完成:
- 在空Volume启动MySQL,画出Entrypoint临时服务和正式服务过程。
- 修改初始化密码后重启,解释为什么旧密码仍生效。
- 新增init SQL后重启,证明脚本不会对已有Volume再次执行。
- 使用Secret文件、普通业务账号和不对公网发布3306的Compose。
- 配置2GiB容器的Buffer Pool、连接数和安全余量,并压测观察RSS。
- 分别验证字符集、排序规则、时区、最终Command和Mounts。
- 执行逻辑备份,在全新隔离Volume中恢复并验证业务查询。
- 说明全量备份、binlog、RPO、RTO和时间点恢复关系。
- 模拟项目名变化创建新Volume,安全识别并恢复旧卷。
- 写出MySQL小版本和大版本升级、金丝雀、回退和数据差异方案。
