T5、常见问题
T5.1、mysql5.7 内存异常
mysql5.7 通过 docker v25.0.5 或 containerd v1.5.10 启动,会出现内存被异常占用 16G,导致操作系统 OOM 问题,docker v28.0 这个问题已解决。
社区虽然给出了解决方案,但是没有人通过源码给出准确的分析,只能通过结论反推问题的可能性。
通过 ChatGPT 的数据统计,给出以下可能性(仅参考):
根本原因不是 MySQL 随机“吃内存”,而是 mysqld 在启动时把进程的 RLIMIT_NOFILE(ulimit -n)的“当前值”当作可用文件数返回/使用——如果该值异常大(不是 RLIM_INFINITY),就会被直接采纳并用于后续的内存/数组分配,导致巨量分配并耗尽宿主机内存。
1、mysqld 先计算需要的文件数(max_open_files),然后调用 my_set_max_open_files() 去设置/获取 OS 层的限制。
源码条表达式:
max_open_files = max( wanted_files, max_connections*5, open_files_limit );
files = my_set_max_open_files(max_open_files);
这是 mysqld 的启动逻辑,计算想要的文件数然后交给 my_set_max_open_files 去尝试设置/获取实际可用值。
2、my_set_max_open_files / set_max_open_files 会读 getrlimit(RLIMIT_NOFILE)
并在某些条件下直接返回当前 rlimit.rlim_cur。
源码(mysys/my_file.c)逻辑的关键点是:
- 调用
getrlimit(RLIMIT_NOFILE, &rlimit)
,把rlimit.rlim_cur
存在old_cur
。 - 如果
rlimit.rlim_cur == RLIM_INFINITY
则把它设为 max_file_limit(合理); - 如果
rlimit.rlim_cur >= max_file_limit
,函数会直接 return rlimit.rlim_cur(也就是把当前很大的 rlimit 值当作最终值返回)。 - 返回的这个 files 值会影响 mysqld 的内部数据结构分配(比如 my_file_info / my_file_limit 等),因此如果 files 很大就会触发巨量 malloc/分配。
- mysys 的 my_set_max_open_files / my_file.c 内部维护 my_file_limit / my_file_info 等结构,启动时会基于 files 大小(返回值)做内存分配/扩容。若 files 是上面那个异常大的 rlimit 值,分配就会按这个数去做,进而可能申请数十 GB。
- 这是 bug 报告里直接指出的行为:set_max_open_files(max_file_limit) ... can return the current limit ... and later used in a malloc which can become huge。也有实测日志(启动时报 Out of memory / 需要几 GB)。bugs.mysql.com
- 这是一个已知的 bug(触发条件:OS 给出一个非常大的 but ≠ RLIM_INFINITY 的 rlimit),MySQL 在后续版本里修了(8.0.19/8.0.20 提到修复)。
MySQL 的 bug 报告里开发者说明:这个问题已在 8.0.19/8.0.20 中修复(修补方式是对返回值做上限/限制等处理,避免直接用超大 rlimit 导致 OOM)。bugs.mysql.com
在很多现代 Linux/distributions(或 systemd 的某些改动)下,进程启动时内核/父进程可能给进程一个非常大的 RLIMIT_NOFILE(比如 1073741816),因为 mysqld 的启动流程把“当前 rlimit”当成可用文件数并直接返回/使用,最终用于按文件数预分配/扩容某些内部数组(my_file_info / 相关缓存),因此会申请非常大的内存 —— 就会把宿主机内存吃光或触发 swap / OOM。这个正是 bug 报告中复现与分析的核心。
bugs.mysql.com
www.percona.com
解决方案:
你现在的解决办法(在 Docker 启动时加 --ulimit nofile=...)是合理且常用的临时/部署层解决方案;长期方案可以是升级到包含修补的 MySQL 版本(8.0.19+ 的修补)或者在宿主/服务管理层(systemd)显式把 LimitNOFILE 设置到合适值。
docker-compose.yml
version: "3.8"
services:
mysql:
image: mysql:5.7
container_name: mysql57
restart: always
environment:
MYSQL_ROOT_PASSWORD: StrongPassw0rd! # 建议使用安全密码
MYSQL_DATABASE: appdb # 可选:初始化数据库
MYSQL_USER: appuser # 可选:初始化用户
MYSQL_PASSWORD: AppUserPass123! # 可选:初始化用户密码
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql # 数据持久化
- ./my.cnf:/etc/mysql/conf.d/my.cnf:ro # 自定义 MySQL 配置
command:
--default-authentication-plugin=mysql_native_password
ulimits: # 关键!限制文件句柄,防止内存暴涨
nofile:
soft: 262144
hard: 262144
deploy:
resources:
limits:
cpus: "2.0" # 限制 CPU
memory: 4G # 限制最大内存
reservations:
memory: 1G
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 30s
timeout: 10s
retries: 3
volumes:
mysql_data:
my.cnf
[mysqld]
# 基本
user = mysql
port = 3306
bind-address = 0.0.0.0
sql_mode = STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
# InnoDB 性能
innodb_buffer_pool_size = 2G # 占总内存 50%-70%
innodb_log_file_size = 512M
innodb_flush_log_at_trx_commit = 1 # 事务安全
innodb_file_per_table = 1
innodb_flush_method = O_DIRECT
# 连接和缓存
max_connections = 300
table_open_cache = 4096
open_files_limit = 262144 # 与 ulimit 对齐
thread_cache_size = 100
query_cache_type = 0 # MySQL 5.7 默认关闭
tmp_table_size = 64M
max_heap_table_size = 64M
# 日志
log_error = /var/log/mysql/error.log
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
# 字符集
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[client]
default-character-set = utf8mb4
T5.2、etcd 健康问题
给 etcd 配置完数据目录和wal目录后,启动服务出现如下问题:
etcd crashes on startup when wal-dir is bind-mounted to host directory, fails with unlinkat /var/lib/etcd/wal: device or resource busy
社区也遇到了,https://github.com/etcd-io/etcd/issues/20218
根本原因是磁盘的挂载目录名和 etcd 服务配置的目录同名,而 etcd 进程会强制创建指定的目录,如果存在会先删除,但是这个目录又是个挂载点,所以会有 device or resource busy 的异常,如下:
# 专门给 etcd 配置的磁盘,避免数据争用,etcd 对磁盘延迟要求很高
[root@master-01 ~]# df -h | grep "/dev/vdc"
/dev/vdc2 50G 756M 50G 2% /etcd/etcd_wal
/dev/vdc1 50G 399M 50G 1% /etcd/etcd_data
# 设置不同的wal目录,可以避免磁盘io竞争,提高性能
ETCD_DATA_DIR: "/etcd/etcd_data"
ETCD_WAL_DIR: "/etcd/etcd_wal"
这么配置,etcd 会强制创建这两个目录,但是这两个目录又是挂载点,必然会出现这种问题。
可以在挂载点下指定一个子目录,就可以解决问题:
# 设置不同的wal目录,可以避免磁盘io竞争,提高性能
ETCD_DATA_DIR: "/etcd/etcd_data/data"
ETCD_WAL_DIR: "/etcd/etcd_wal/data"