
先说结论这个故障最后证明是两个问题叠加出来的——一个是端口映射层配置错乱另一个是容器没设内存上限导致MySQL 8.0被系统OOM Killer直接杀掉。前者让人误以为“数据库连不上”后者才是容器反复崩溃的真凶。两者单独出现都不算难查但缠在一起的时候迷惑性非常大。我这边负责的一个内部业务系统数据量不大但早晚高峰请求量曲线很陡。为了统一运维我们把数据库迁到了Docker容器里用MySQL 8.0官方镜像跑单实例。上线第一周很平静第二周开始监控告警数据库连接失败率突然升高接口超时堆了一屏。登上服务器一看docker ps输出里容器的STATUS列一直在“Restarting (1) 3 seconds ago”和“Up 2 seconds”之间跳动Docker的--restartalways策略像复读机一样把容器拉起来然后又看着它倒下。当时的第一反应是配置文件写坏了或者数据目录权限出问题。但翻docker logs折腾了半小时日志里根本没有典型的“InnoDB: Unable to lock”或者“unknown variable”这类启动失败信息MySQL自己甚至都没来得及记录异常退出。这个细节后来才理解——系统级SIGKILL是不会有SQL日志的。今天把整个排查链路、参数计算、修复动作和避坑点完整整理出来希望能让遇到类似问题的朋友少走点弯路。1. 故障现场业务秒崩容器秒挂日志却“异常安静”1.1 监控告警与第一波排查动作先描述一下当时最直观的现象。告警平台上数据库存活检测每小时跑一次结果连续三个周期都失败业务方说写操作开始报“could not connect to MySQL”。我登录服务器执行docker ps -a看到的状况非常奇葩CONTAINER ID IMAGE STATUS a1b2c3d4e5f6 mysql:8.0 Restarting (1) 5 seconds ago容器启动5秒就退出Docker立刻拉起再退出再拉起。RestartCount已经积累到二十几次。那感觉就像系统在“抖”——不是彻底挂掉而是不断抽搐。我的第一轮排查动作可以说把所有新手该犯的错误都犯了一遍反复执行docker restart mysql8指望重启能“恢复正常”删掉容器重新docker run结果还是一样检查my.cnf里有没有写错的参数没发现明显异常把数据目录权限改成777以为能解决InnoDB文件锁问题当然也没用。现在回头看这些动作全部绕开了真正的根因。排查这类问题第一步应该做的不是重启而是看两样东西容器的完整状态信息和系统层面的内存/端口情况。1.2 为什么日志会“安静”MySQL 8.0正常启动时错误日志会记录到/var/lib/mysql下的*.err文件容器把这些日志打到stdoutdocker logs能看到。如果mysqld因为配置错误退出OS或者因为初始化失败主动退出日志里通常会有明确报错。但这次日志的结尾非常平淡停在InnoDB初始化完成、准备接收连接的阶段没有任何SQL层面的错误。原因在于OOM Killer杀进程时直接向mysqld发送SIGKILL信号这个信号无法被进程捕获也没机会去写错误日志、刷新数据文件进程在一瞬间被“处决”。所以日志看起来干干净净这也解释了为什么只看docker logs会完全摸不着头脑。判断这种“无声崩溃”的关键是容器退出码和内核日志而不是应用日志。退出码137就是一个强信号这个后面会详细说。2. 第一阶段排查端口误用是怎么被揪出来的2.1 扫描宿主端口发现3306被“别人”占着业务方报“连不上数据库”我顺着应用配置里的连接串去查端口写的是127.0.0.1:3306。执行ss -tlnp | grep -E 3306|3307输出让我愣住了LISTEN 0 50 127.0.0.1:3306 0.0.0.0:* users:((mysqld,pid2871,fd18)) LISTEN 0 128 0.0.0.0:3307 0.0.0.0:* users:((docker-proxy,pid4522,fd4))宿主机上有一个mysqld进程直接监听在3306端口它并不是Docker容器进程而是之前遗留的通过systemd管理的MySQL 5.7实例。Docker启动新容器时因为3306被占用启动命令里写的是-p 3307:3306把宿主机的3307映射到了容器的3306。也就是说新容器跑得好好的MySQL 8.0明明在3307等着但应用侧的连接串还傻傻地连向3306。这造成了两个后果第一应用连接的是旧MySQL 5.7遇到表结构、字符集或者权限差异后各种报错第二我一开始也误以为新容器“坏了”反复重启它可真正连错对象的反而是旧实例。2.2 端口映射为什么会踩坑三种典型情况复盘这次端口误用我总结了三条经验特别适合多实例共存的容器环境。第一种是把端口映射的左右顺序写反。-p 宿主机端口:容器端口容易写成-p 3306:3307结果外部访问3306Docker把流量转发到容器的3307但MySQL默认监听3306等于把流量拐到了一个空房间。第二种是多个容器竞争同一个宿主机端口。同一台服务器上跑两个MySQL第二个必须换映射端口比如-p 3307:3306。但换完端口应用连接串、防火墙放行规则、安全组白名单都必须同步改任何一环漏了都会表现为“数据库连接不通”。第三种是容器内部配置文件与映射端口不一致。比如my.cnf里写了port3306但Docker命令用的-p 3307:3307结果宿主机3307映射到容器3307容器里3307根本没进程监听。这种问题最隐蔽因为你在容器内用3307去连接表面上什么都是对的实际上监听端口和映射端口完全错位。正确做法是进入容器统一验证docker exec -it mysql8 bash mysql -uroot -p -h127.0.0.1 -P3306能连上说明容器内监听正常再与docker port mysql8输出的映射关系对照就一目了然。2.3 修复端口问题停旧例、改映射、验连通既然找到了误用点修复动作是三步走第一步把宿主机3306彻底释放。旧MySQL 5.7如果已经没业务依赖直接停用systemctl stop mysqld systemctl disable mysqld第二步重新创建新容器端口映射改回-p 3306:3306让MySQL 8.0接管宿主3306docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDxxxx \ -v /data/mysql8:/var/lib/mysql \ mysql:8.0第三步验证连接正常mysql -h127.0.0.1 -P3306 -uroot -p -e SELECT VERSION();看到版本号输出8.0.x端口问题算是收工。但这里有个隐患——我当时太开心以为一切结束了结果没等多久监控再次响起容器又在那里“Restarting”。端口修好了崩溃还在继续说明崩的不是端口问题。提示Docker容器端口映射校验最快的方法是docker port 容器名它会清晰列出宿主机端口到容器端口的绑定关系。请把这个命令加入你的默认排障工具箱。3. 第二阶段定位退出码137和OOMKilled才是真正的照妖镜3.1 一次inspect让真相浮出水面端口修好后容器还在反复崩溃这时候我意识到问题比想象中深。这次我没有立刻重启而是先执行了docker inspect mysql8 --format {{.State.Status}} | ExitCode{{.State.ExitCode}} | OOMKilled{{.State.OOMKilled}} | RestartCount{{.RestartCount}}输出running | ExitCode137 | OOMKilledtrue | RestartCount23ExitCode137在容器世界里是一个标志性信号。137等于128加9128是系统信号的基准偏移量9对应SIGKILL——也就是说容器主进程是被内核用SIGKILL强制杀掉的。再配合OOMKilledtrue几乎可以断定是内存溢出。为了拿到铁证我又看了内核日志dmesg -T | grep -i -E oom|killed process | tail -20很快出现了这么一条kernel: Out of memory: Killed process 12345 (mysqld) total-vm:4987648kB, anon-rss:4211728kB, file-rss:0kBanon-rss高达4GB说明mysqld进程占用了大约4GB的匿名内存系统资源耗尽后内核OOM Killer根据一系列评分选中了这个“内存大户”一刀切掉。此时真相终于浮出水面MySQL 8.0容器没有设置内存上限导致它在流量高峰把宿主机内存吃满最终被系统强杀。3.2 MySQL 8.0是一个真正的内存大户三笔账要算清既然要解决OOM就必须先搞清楚MySQL 8.0的内存到底花在哪里。我把它拆成三个部分每个部分都是一笔账。第一笔账是InnoDB缓冲池。innodb_buffer_pool_size是MySQL内存占比最大的一项官方默认128MB但生产环境模板经常会调得很大。我这边现场配置文件里写的是2G在4核8G的机器上看起来不算离谱问题是宿主机上还有旧MySQL 5.7、Java应用、监控agent等一大堆进程物理内存早就捉襟见肘。你以为2G是留给MySQL的“专属空间”实际上是大家一起抢同一块地。第二笔账是连接级内存。每个MySQL连接都会预分配一系列排序和连接缓冲区包括sort_buffer_size、join_buffer_size、read_buffer_size、read_rnd_buffer_size。它们单个默认值分别是256KB、256KB、128KB、256KB如果同时存在300个连接单是这部分就是300×(256256128256)KB约等于270MB。而且一旦有复杂的ORDER BY、GROUP BY或临时表操作这些缓冲区会动态扩展实际占用远不止基础值。第三笔账是全局缓存与内部结构。包括innodb_log_buffer_size默认16MB、key_buffer_size默认8MB、table_open_cache、thread_cache_size等。这里面最容易被忽视的是performance_schema在MySQL 8.0里默认开启它会为性能监控维护大量内存统计表一个活跃实例轻松占用数百MB甚至接近1GB而且这个开销跟业务数据量没有直接关系纯属“监控税”。我当时快速估算了一下这台的账innodb_buffer_pool_size2Gperformance_schema约500MB200个连接的基础连接内存约200MB再加上其他全局缓存开销总量轻轻松松超过2.9GB。宿主机只有8G内存再被其他服务分去一大部分晚高峰连接数一上来直接爆掉。3.3 内核OOM Killer到底是怎么“挑人”的顺带把OOM Killer机制讲透。Linux内核在物理内存耗尽时会启动一个“裁判”进程按oom_score对当前所有进程排序选一个牺牲品杀掉以释放内存。进程的oom_score与内存占用、CPU占用、运行时间、是否为root进程等因素相关默认情况下占用内存越大的进程分数越高越容易被选中。容器技术在默认情况下如果没有显式设置--memorycgroup的内存限制不会生效容器内进程的oom_score和宿主机普通进程一样参与全局排名。mysqld在线上通常就是内存占用最高的进程所以一旦系统整体OOM它几乎必死。这就是为什么Docker容器建议永远要设内存上限——不是为了限制MySQL“不够用”而是为了避免它成为内核的优先牺牲品。我在那台机器上用docker stats看了一眼当时容器的内存占用docker stats --no-stream mysql8输出显示内存使用量在4.5G左右已经远远超出预期。而宿主机总内存8G其他服务占用2G留给内核和文件缓存的空间几乎为零OOM只是时间问题。4. 双管齐下修复MySQL参数调优与容器内存限制4.1 MySQL侧把几个大头参数按需调下来修复的第一层是在MySQL内部收紧参数。我的思路是业务数据量本身不大没必要把缓冲池顶到2G512M完全够跑连接数限制到150防止流量洪峰造成内存无限扩张排序和连接缓冲保持1M以下这套组合可以在不牺牲明显性能的前提下把正常内存峰值压住。最终my.cnf里这样写[mysqld] innodb_buffer_pool_size 512M performance_schema OFF max_connections 150 sort_buffer_size 1M join_buffer_size 1M read_buffer_size 512K read_rnd_buffer_size 512K innodb_log_buffer_size 16M table_open_cache 1024 thread_cache_size 64逐条解释一下关键选择innodb_buffer_pool_size从2G降到512M。如果业务是重IO场景这个幅度确实太大但我们的库只有几十GB且访问集中512M足够容纳热点数据。真到不够用的时候首先要看的是索引和SQL质量而不是盲目加内存。performance_schema OFF。关掉之后内存立省约500MB。前提是团队有替代的监控手段比如Prometheus的mysqld_exporter它不依赖performance_schema也能采集到大量指标。max_connections 150。8.0默认值是151降为150不算激进但配合应用连接池限制能有效防止连接数失控。sort_buffer_size和join_buffer_size不要贪大。如果应用里有复杂的排序和连接操作1M的默认值足以覆盖大部分场景真需要提速应该先优化SQL让临时文件少产生而不是给每条连接预留更多内存。4.2 容器侧给容器戴上“内存紧箍咒”第二层是在容器编排层面强制限制资源。docker run方式是这样docker run -d \ --name mysql8 \ --memory1g \ --memory-swap1g \ --cpus2 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDxxxx \ -v /data/mysql8:/var/lib/mysql \ -v /etc/mysql8/my.cnf:/etc/mysql/conf.d/my.cnf \ mysql:8.0--memory1g表示容器最多使用1GB物理内存--memory-swap1g表示内存加swap的总和上限也是1G相当于禁止容器把数据换到swap里。这样当mysqld试图申请超过1G的内存时cgroup会在内核层面直接拒绝正常情况下MySQL会因内存分配失败而报错退出但不会拖垮整个宿主机。如果和我一样用docker compose写法更清晰services: mysql8: image: mysql:8.0 container_name: mysql8 restart: always ports: - 3306:3306 environment: MYSQL_ROOT_PASSWORD: xxxxxx mem_limit: 1g mem_reservation: 512m cpus: 2 volumes: - /data/mysql8:/var/lib/mysql - /etc/mysql8/my.cnf:/etc/mysql/conf.d/my.cnf这里mem_limit: 1g对应--memorymem_reservation: 512m对应--memory-reservation后者表示内存比较紧张时的预留下限可以让调度器在竞争压力下更容易收缩这个容器的内存是保证宿主机整体稳定的一个缓冲。4.3 重启验证与参数确认调整完后不要直接docker restart。原因很简单如果当前内存已经吃紧重启时Docker会向mysqld发送SIGTERM等待默认10秒超时后直接SIGKILL可能导致InnoDB需要走崩溃恢复流程而走正常关闭流程能让MySQL自己完成检查点刷盘。我用的命令是docker exec mysql8 mysqladmin -uroot -p shutdown docker start mysql8重启后确认参数全部生效docker exec mysql8 mysql -uroot -p -e SHOW VARIABLES LIKE innodb_buffer_pool_size; SHOW VARIABLES LIKE performance_schema; SHOW VARIABLES LIKE max_connections; SHOW VARIABLES LIKE sort_buffer_size; 第一次执行时我发现performance_schema居然还是ON原因是8.0镜像的默认配置和我的my.cnf加载顺序冲突。官方镜像会先执行/etc/mysql/conf.d/下的配置但某些部署模板会在command里覆盖参数两者优先级不一样。最后我把--performance-schemaOFF这个参数直接写进了compose的command列表重新创建容器才彻底关掉。参数全部生效后我又持续观察了两周。docker stats显示容器内存稳定在600M到800M之间再没有出现OOMKilledtrue的现象业务侧彻底安静下来。提示MySQL官方镜像的配置加载顺序是/etc/my.cnf→/etc/mysql/conf.d/*.cnf→/etc/mysql/mysql.conf.d/*.cnf挂载配置时要注意后加载的配置会覆盖前面的同名参数。而docker run里通过command传入的参数优先级最高如果你想确保某个参数百分之百生效直接放到command里最省心。5. 复盘常见问题速查表与三条独家心得5.1 整个故障演进的关键链条这个故障花了我一整天很大一部分时间浪费在“凭直觉乱操作”上。事后回头画一条主线其实脉络非常清晰业务告警连接失败 → 检查应用连接串指向宿主机3306扫描宿主端口发现3306被旧MySQL 5.7占用新MySQL 8.0容器映射在3307 → 端口误用确认停旧MySQL释放3306重新以3306:3306创建容器应用连接恢复 → 但容器仍在“颤抖”查看docker inspect发现OOMKilledtrue、退出码137 → 锁定OOM查看dmesg确认mysqld被OOM Killer杀掉调整MySQL内存参数、设置容器mem_limit与mem_reservation验证无崩溃。链条中的转折点在第4步。如果一直停留在“端口不对”的认知里端口问题修复后还是会继续朝配置文件、网络层面上找原因很难转过弯来想到OOM才是崩溃元凶。5.2 容器化MySQL故障速查表现象可能原因建议排查手段容器反复重启日志尾部无明确报错内存OOM被内核SIGKILLdocker inspect查看OOMKilled、退出码dmesg查OOM记录应用连接容器报Connection refused端口映射错误或容器内监听端口不对ss -tlnp看宿主机端口docker port看映射进容器验证监听端口docker run -p 3306:3306报port is already allocated宿主机3306被其他进程占用ss -tlnp外部能连通3306但连接到的数据对不上宿主机另一个实例占用3306容器映射到其他端口检查docker port映射查看当前连接的MySQL版本与实例ID容器内SHOW VARIABLES和my.cnf配置不一致配置加载顺序或command覆盖优先级差异统一在command和挂载配置中写一致值用SHOW VARIABLES确认docker stats显示容器内存持续增长缓冲池偏大、连接数上升或performance_schema未关监控连接数按数据量调低buffer pool关闭performance_schema5.3 三条实操心得第一容器排障永远是“看状态”优先于“看日志”。docker logs只能看到容器内进程打到stdout的内容系统级SIGKILL是瞬间的进程没机会输出日志。docker inspect里的State.ExitCode和State.OOMKilled两个字段十秒内就能锁定是不是OOM效率比逐行翻日志高太多。第二端口映射是容器化与传统运维最大的思维差异。裸机上修改mysqld端口重启进程就能全部生效容器里要同步检查的环节非常多——宿主机端口占用、-p映射参数、应用连接串、防火墙放行规则、安全组白名单。任何一个环节漏掉都表现为“连不上”或“连错实例”特别容易被误判成数据库崩溃。第三MySQL参数不能脱离容器资源限制来配置。我现在的习惯是先给容器定一个客观的mem_limit然后倒推MySQL各内存参数让任何一项配置都有明确上限可依。比如mem_limit1g那innodb_buffer_pool_size最多给512M其他留给连接级内存、全局缓存、性能监控模块。而不是先抄一个模板里的2G buffer pool再指望Docker去兜底——Docker只在cgroup层面拦截不会替你优化我的基准是在容器场景下宁可buffer pool小一点连接数控制严一点也要保证进程不因OOM退出。6. 最后分享一个排查后的额外优化修复完主故障之后我并没有就此罢手而是把“事后补救”变成了“前置预防”。做的第一件事是给容器挂了内存监控用docker stats --no-stream --format配合定时任务采集内存数据再接入Prometheus的cAdvisor exporter在Grafana上把容器的内存变化曲线、连接数曲线、MySQL查询耗时曲线放到同一个面板。第二件事是给连接数加了告警。一旦MySQL的连接数连续5分钟超过120就触发钉钉通知提醒运维介入。这个阈值是根据我们业务峰值连接数大约80到100加上30%余量定义的既能提前预警又不会频繁误报。第三件事是简化了容器的启动编排。把原来散落的docker run命令统一收敛到docker compose文件里restart: always、mem_limit: 1g、mem_reservation: 512m都写死避免团队成员手工启动时漏掉关键参数。这一步很大程度上减少了“人肉配置”带来的不确定因素。整个过程下来最大的体会是容器化MySQL的难点并不在MySQL本身而在于“资源边界”和“映射关系”这两个维度。你既要在容器层为它划定清晰的内存边界又要保证端口映射、配置参数、应用侧连接串三者严格对齐。只要有一环脱节故障表现就会变得特别有迷惑性——明明MySQL活着业务却连不上明明连上了内存又悄悄逼近危险水位。希望这篇实录能帮到正在做数据库容器化的朋友。遇到这类“查不动、看不透”的故障稳住心态先看系统层证据再逐层往下确认最后你会发现最顽固的故障往往都是由几个不起眼的小细节叠加出来的。