Skip to content

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内部事务原理,也不会自动提供高可用和备份。

学习目标

学完后,应能:

  1. 解释官方 MySQL 镜像从 Entrypoint 到正式 mysqld 的首次初始化全过程。
  2. 说明 MYSQL_ROOT_PASSWORDMYSQL_DATABASEMYSQL_USER 为什么对已有数据目录不再重新初始化。
  3. 正确使用 Named Volume、Bind Mount、Secret、只读配置和 init script,不使用 privileged。
  4. 解释 MySQL 内存模型与容器 cgroup 限制,避免 Buffer Pool 和连接数共同触发 OOMKilled。
  5. 设计字符集、排序规则、时区、端口、网络、账号、日志和健康检查。
  6. 完成逻辑备份、恢复验证、binlog/PITR思路和RPO/RTO设计。
  7. 解释为什么不能直接 tar 运行中的 /var/lib/mysql,也不能把 Volume 当成备份。
  8. 制定小版本与大版本升级、金丝雀、数据副本和失败回退方案。
  9. 定位初始化失败、Access denied、数据“消失”、权限、磁盘、OOM、崩溃恢复和升级失败。

一、容器化没有改变MySQL的核心结构

mermaid
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可以使用:

bash
docker pull mysql:8.0

8.0 仍是可变Tag,未来可能指向新的8.0补丁镜像。生产应:

  1. 选择组织支持的MySQL版本线。
  2. 固定已验证补丁Tag或镜像Digest。
  3. 记录镜像来源、Image ID和Digest。
  4. 在预发使用真实数据量副本验证升级。
  5. 阅读MySQL与镜像Release Notes。

查看:

bash
docker image inspect mysql:8.0
docker image ls --digests mysql

CPU架构也要匹配:

bash
docker image inspect \
  --format 'os={{.Os}} arch={{.Architecture}} digests={{json .RepoDigests}}' \
  mysql:8.0

MySQL 5.7、8.0、8.4 等版本线在数据字典、默认认证插件、字符集、参数和升级路径上有差异。具体支持周期会变化,应查当前官方生命周期;不要把旧版数据目录直接交给任意新版镜像试启动。

三、官方镜像Entrypoint首次启动全过程

官方镜像通常不是直接执行一个裸 mysqld。Entrypoint 会准备目录、读取初始化环境变量、判断数据目录是否已经初始化,然后再决定是否执行初始化流程。

mermaid
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 为什么修改环境变量后密码没变

第一次启动:

text
Volume为空

Entrypoint读取MYSQL_ROOT_PASSWORD

创建MySQL账号并写入系统表

后续启动:

text
Volume已有mysql系统数据

Entrypoint判断已初始化

跳过初始化账号逻辑

MySQL继续使用系统表中的原密码

环境变量不是每次启动都覆盖数据库账号。修改已有账号应通过受控SQL:

sql
ALTER USER 'order_app'@'%' IDENTIFIED BY '新的受控密码';

同时更新Secret、连接池并设计轮换窗口。不要删除生产数据目录来“让新密码生效”。

4.2 _FILE为什么更合适

普通环境变量可通过 container inspect、诊断包和进程环境暴露。*_FILE 让Entrypoint从挂载文件读取,适合Docker Secret或受控文件:

yaml
environment:
  MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql-root-password

源Secret文件仍需权限、加密、轮换和审计;本地Compose Secret不自动等于企业Secret Manager。

五、/docker-entrypoint-initdb.d只在首次初始化执行

常见目录:

text
/docker-entrypoint-initdb.d

可放SQL或Shell初始化文件,支持类型和执行规则以镜像版本为准。文件通常按名称排序,因此建议:

text
001-schema.sql
010-dictionary.sql
020-seed-local.sql

示例:

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默认数据目录通常是:

text
/var/lib/mysql

命名卷:

bash
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 VolumeDocker管理位置和生命周期,权限相对简单仍需知道宿主机故障域和备份位置
Bind Mount路径明确,便于纳入宿主机存储规划UID/GID、SELinux、路径错误和误操作更明显
网络/云存储Volume可与外部存储结合时延、fsync语义、锁、故障恢复必须验证

数据库存储不能只看“能挂载”。必须验证 fsync、延迟、IOPS、故障语义、快照一致性和恢复时间。

七、挂载后数据“消失”的常见原因

7.1 项目名变化创建新卷

Compose卷:

yaml
volumes:
  mysql-data:

项目名为 order-prod 时,实际可能叫:

text
order-prod_mysql-data

项目名改为 order-platform 后会创建新卷。新MySQL初始化为空库,旧数据卷可能仍存在。

检查:

bash
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权限

旧教程常见:

text
--privileged=true

它会显著扩大容器能力和设备访问范围,通常与MySQL目录权限问题无关。正确排查:

bash
docker image inspect --format 'user={{.Config.User}}' mysql:8.0
docker inspect --format '{{json .Mounts}}' order-mysql
docker logs --tail 200 order-mysql

Bind Mount时检查宿主机数字UID/GID、目录权限和SELinux标签。不要使用 chmod 777,也不要让MySQL以高权限访问整个宿主机。

九、配置文件如何挂载

官方镜像常读取:

text
/etc/mysql/conf.d/*.cnf

目录和优先级以镜像内默认配置为准。建议使用后缀明确的只读文件:

text
conf.d/zz-order.cnf

示例:

ini
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
default-time-zone=+00:00
max-connections=200
innodb-buffer-pool-size=1G

MySQL配置项常用下划线名称,命令行/配置文件的连字符和下划线兼容规则应按版本确认。启动后以真实变量为准:

sql
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 在启动前退出:

bash
docker logs --timestamps --tail 300 order-mysql
docker inspect --format '{{json .State}}' order-mysql

可在同版本镜像中验证配置,但不要把正在使用的数据卷挂给临时验证容器。使用只读配置和空临时环境执行 mysqld --verbose --help 等检查,具体命令行为按版本确认。

9.2 命令行参数覆盖

yaml
command:
  - --character-set-server=utf8mb4
  - --collation-server=utf8mb4_unicode_ci

命令行通常具有较高优先级。排查时要同时看:

bash
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,不需要发布宿主机端口:

yaml
services:
  mysql:
    image: mysql:8.0
    # 不配置ports

应用使用:

text
jdbc:mysql://mysql:3306/order_db

本地调试若需要宿主机连接:

yaml
ports:
  - "127.0.0.1:13306:3306"

不要默认发布:

text
0.0.0.0:3306

否则可能暴露到局域网或公网,仍受防火墙和云安全组影响。数据库安全还需要账号最小权限、TLS、密码策略和审计。

容器中的 localhost 只表示当前容器。Spring Boot容器连接MySQL不能写 localhost:3306,除非两者确实在同一Network Namespace中,这不是普通Compose架构。

十二、账号与权限

业务应用不要使用root。首次初始化时创建:

text
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:

text
MySQL总内存
├── InnoDB Buffer Pool
├── Performance Schema
├── 数据字典与表缓存
├── redo/binlog缓冲
├── 每连接线程栈
├── 每连接sort/join/read等Buffer
├── 临时表
├── 连接和协议Buffer
└── 其他插件与本地内存

13.1 为什么只设置Buffer Pool还会OOM

假设:

text
容器限制 = 2GiB
innodb_buffer_pool_size = 1.5GiB
max_connections = 500

剩余约0.5GiB还要容纳全部全局结构和连接内存。高并发排序、Join或大包可能使按连接分配的内存迅速增长,最终cgroup OOM Killer杀死mysqld。

13.2 内存预算思路

粗略上界思路:

text
全局常驻内存
+ 活跃连接数 × 每连接实际内存
+ 临时峰值
+ 安全余量
< 容器内存限制

不能简单用 max_connections × 所有session buffer最大值 作为精确常驻内存,因为很多Buffer按需分配;但它能提醒连接配置和SQL行为可能产生巨大峰值。

13.3 容器限制不会自动调小MySQL

不要假定 --memory=2g 会自动把 Buffer Pool、安全连接数和临时表都调整为合理值。必须显式配置并通过压测、RSS和MySQL指标验证。

检查:

bash
docker inspect --format 'memory={{.HostConfig.Memory}} oom={{.State.OOMKilled}} exit={{.State.ExitCode}}' order-mysql
docker stats --no-stream order-mysql

MySQL:

sql
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变慢。

检查容器:

bash
docker stats --no-stream order-mysql
docker inspect --format 'nanoCpus={{.HostConfig.NanoCpus}} blkio={{json .HostConfig.BlkioConfig}}' order-mysql

宿主机:

bash
iostat -xz 1
pidstat -d 1

MySQL慢SQL、锁和Buffer Pool原理回到MySQL主专栏分析,不能看到容器IO高就直接扩容CPU。

十五、健康检查的语义

基础存活:

yaml
healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
  interval: 10s
  timeout: 5s
  start_period: 60s
  retries: 10

mysqladmin ping 主要证明服务端进程可以响应,甚至认证失败时也可能表达“服务器活着”;它不证明:

  • 业务账号可登录。
  • order_db存在。
  • 表结构迁移完成。
  • 主库可写。
  • 复制无延迟。
  • 磁盘空间充足。

商业Readiness应按架构增加低成本检查,但不能每10秒执行重查询。应用仍需连接超时、退避重试和连接池恢复。

15.1 为什么start_period重要

首次初始化、大数据恢复和崩溃恢复可能耗时。过短探针会把正常恢复误判失败,配合自动重启形成永远恢复不完的循环。

十六、SIGTERM、正常关闭与崩溃恢复

容器停止:

bash
docker stop --time 60 order-mysql

正常路径:

mermaid
flowchart TD
    A["Docker向mysqld发送SIGTERM"] --> B["停止接收新连接"]
    B --> C["处理或终止会话与事务"]
    C --> D["刷新必要日志和状态"]
    D --> E["InnoDB执行正常关闭"]
    E --> F["mysqld退出"]

如果宽限期不足,Docker发送SIGKILL,MySQL下次启动需要 InnoDB 崩溃恢复:

text
扫描redo

重放需要的页修改

回滚未提交事务

恢复一致状态

redo原理见 redo log与binlog。崩溃恢复时间与redo量、脏页、大事务、磁盘速度有关。不要因恢复慢再次强杀,可能形成长期不可用。

十七、日志应该从哪里看

第一轮:

bash
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位置继续推进。
  • 数据字典和表空间发生变化。

直接:

text
tar /var/lib/mysql

可能得到不可恢复或逻辑不一致的文件集合。

18.1 三类备份

类型示例特点
逻辑备份mysqldump/mysqlpump类工具跨环境较灵活,恢复大库较慢
物理热备MySQL Enterprise Backup、兼容版本XtraBackup等大库更快,但版本和流程要求高
存储快照LVM/云盘/CSI快照快,但必须与数据库刷盘、锁和一致性协调

Volume快照本身也不自动保证数据库一致性。

十九、逻辑备份Demo

推荐用受控客户端配置文件,避免密码直接出现在命令参数:

ini
[client]
user=root
password=替换为受控Secret
host=localhost

将其只读挂载为:

text
/run/secrets/mysql-client.cnf

备份:

bash
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,再导入:

bash
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与时间点恢复

误删数据恢复通常需要:

text
最近一次可用全量备份
    +
备份之后连续完整的binlog

恢复到误操作前时间点或事务位置

必须提前配置:

  • binlog启用。
  • server_id。
  • binlog格式。
  • 保留时间。
  • 独立归档。
  • 时间/GTID记录。
  • 恢复工具和演练。

binlog与redo职责不同。redo用于InnoDB崩溃恢复,binlog用于复制和增量回放,详见 redo log与binlog

二十二、升级镜像的正确流程

22.1 小版本也要验证

即使同一8.0线,也可能变化:

  • 安全修复。
  • 优化器行为。
  • 默认参数。
  • 认证与TLS。
  • 系统表升级。
  • 镜像操作系统库。

22.2 大版本升级

mermaid
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

目录:

text
mysql-stack/
├── compose.yaml
├── conf.d/
│   └── zz-order.cnf
├── init/
│   └── 001-schema.sql
└── secrets/
    ├── mysql-root-password.txt
    ├── mysql-app-password.txt
    └── mysql-client.cnf

Secret文件只能用于本地学习时手工准备,并加入 .gitignore。生产由Secret Manager或部署平台生成,不提交Git。

23.1 配置文件

ini
[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

yaml
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

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 启动前验证

bash
docker compose config
docker compose pull
docker compose up -d --wait

23.5 启动后验证

bash
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 不带密码会交互提示,避免密码直接出现在命令行历史和进程参数。

进入后:

sql
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 证明初始化脚本只运行一次

  1. 启动并确认orders表存在。
  2. init 新增另一个SQL。
  3. 执行 docker compose down 后重新 up
  4. 新SQL不会因已有Volume自动执行。

不要在有价值数据环境执行 down -v。若要做“空卷首次初始化”实验,应使用全新项目名或明确无价值的新Volume。

二十四、商业场景一:修改密码环境变量不生效

现象:修改 MYSQL_ROOT_PASSWORD_FILE 内容并重启容器,旧密码仍有效。

根因:Volume已经初始化,Entrypoint跳过账号创建逻辑。环境变量是首次初始化输入,不是持续账号控制器。

正确处理:使用 ALTER USER 修改数据库系统表中的账号凭据,更新Secret并让连接池安全刷新。生产密码轮换要处理旧/新凭据切换窗口和失败回退。

二十五、商业场景二:Compose改目录后出现空库

现象:迁移部署目录后执行 docker compose up,MySQL自动初始化,业务表全部不见。

证据:

bash
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

bash
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

按层检查:

bash
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

sql
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW PROCESSLIST;

不能只增大 max_connections。检查应用连接池总和、连接泄漏、慢SQL、长事务、空闲超时和数据库内存预算。

28.5 磁盘满

bash
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 配置没有生效

bash
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切回不等于数据降级。

三十、学习验收

不看答案完成:

  1. 在空Volume启动MySQL,画出Entrypoint临时服务和正式服务过程。
  2. 修改初始化密码后重启,解释为什么旧密码仍生效。
  3. 新增init SQL后重启,证明脚本不会对已有Volume再次执行。
  4. 使用Secret文件、普通业务账号和不对公网发布3306的Compose。
  5. 配置2GiB容器的Buffer Pool、连接数和安全余量,并压测观察RSS。
  6. 分别验证字符集、排序规则、时区、最终Command和Mounts。
  7. 执行逻辑备份,在全新隔离Volume中恢复并验证业务查询。
  8. 说明全量备份、binlog、RPO、RTO和时间点恢复关系。
  9. 模拟项目名变化创建新Volume,安全识别并恢复旧卷。
  10. 写出MySQL小版本和大版本升级、金丝雀、回退和数据差异方案。

关联知识点