MySQL 锁超时问题的全链路定位与优化实践

1、问题 线上出现很多添加 key 锁超时的 mysql 日志。 https://apm.example.com/app/apm/services/<service_name>/transactions/view?rangeFrom=now-24h%2Fh&rangeTo=now&environment=&transactionName=%23 2、排查 1)查看 APM,发现了一个耗时的请求/sync,做语料同步的。后面插入语料 key 都显Lock wait timeout exceeded。 2)查看同步请求的具体链路,发现有多次批量 INSERT 操作,而且每次批量插入消耗的时间会逐步增加。最终也是执行成功的,花了24 分钟。后面其他事务的 Insert 操作都给阻塞住了。 3)查看锁日志,可以发现并不是死锁,而是普通的锁等待,最开始的批量操作(/sync)占用了大量的锁 4253982 row locks。后面的insert(添加 key)申请插入意向锁都被阻塞,等待一段时间之后上报超时错误给应用。 LOCK WAIT 8 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id <tid_1>, OS thread handle <handle_1>, query id <qid_1> <private-ip> <db_name> update INSERT INTO multilingual_item (item_id, corpus_id, lang, customize,`key`,value, description) VALUES (1001, 2001, 'xx-XX', 'default', 'key_1', '<text>', ''); ------- TRX HAS BEEN WAITING 22 SEC FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 345 page no 83690 n bits 136 index PRIMARY of table `<db_name>`.`multilingual_item` trx id <trx_waiting> lock_mode X insert intention waiting Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; ------------------ ---TRANSACTION <trx_long>, ACTIVE 1166 sec starting index read mysql tables in use 1, locked 1 102372 lock struct(s), heap size 10379472, 4253982 row lock(s), undo log entries 943 MySQL thread id <tid_long>, OS thread handle <handle_long>, query id <qid_long> <private-ip> <db_name> update INSERT INTO multilingual_item (item_id, corpus_id, lang, customize,`key`,value, status, type) VALUES (2001, 3001, 'xx-XX', 'default', 'k1', 'v1', 2, 1), (2002, 3001, 'xx-XX', 'default', 'k2', 'v2', 2, 1); 4)查看代码,发现了调用 importJsonCorpus 进行多次批量操作,这跟 APM 看到的现象一致。这个方法上有事务注解,importJsonCorpus 方法的调用都在一个事务里,所以一次批量操作结束之后并不会释放锁。 ...

July 9, 2025

大文件下载中断问题的全链路排查与稳定性优化

现象 https://oss.example.com/storages/<storage_id>//firmware.zip 1)在浏览器下载固件文件经常出现中断,原本3g的文件,每次都在1g左右下载完成,文件不完整。 2)内网下载速度较慢,插网线为 20M/s 左右,WIFI 状态下为 8M/s 左右。 3)使用网线有部分用户产生问题 1 的中断现象,一部分用户则不会,使用 WIFI 则必现中断。 分析 1)分析业务:在内部平台构建完成之后,后台会自动对整个固件文件夹进行转存,上传到对象存储服务,后续可以随时下载,那么这里就涉及上传和下载,我们可以对多次下载的同一个固件进行 hash 对比,发现他们并不是一致的,首先可以排除是上传文件的问题。 2)我们使用 wget 进行下载,表现为连接关闭重试,自动重新连接之后能成功下载完整文件。所以确实不是因为上传的问题,那么我们从下载方面找问题。 每下载 1G 中断一次 3)我们抓包查看具体细节,可以看到是服务器发送了一个 FIN 包主动断开连接,而且没有出现RST包,可以排除网络错误或网络波动,也不是客户端关闭的连接,而是服务端主动关闭的。 4)接下来需要定位具体是服务端哪个节点主动关闭连接,在哪个节点会限速。分析内部链路,并在每个节点进行测试,测试结果如下。 直连对象存储服务 Pod IP,不会出现下载中断和限速的问题,问题都在网关上。 我们可以暂时定位中断问题在APISIX发生,而限速是在F5发生。如图,使用域名下载文件速度较慢,为18M/s左右,使用后端服务的 Pod IP 下载速度能达到 66M/s。 5)验证猜想,在本地快速搭建Nginx服务进行验证,并查看 Nginx 日志。 可以看到 Nginx 主动 closed connection,这跟我们抓包的现象一致。还看到出现“upstream response is buffered to a temporary file”字样,说明 Nginx 有缓存文件的相关机制。 6)查看官网的相关配置文档,可以看到 proxy_max_temp_file_size 这个参数,配置文件默认不会显示该配置,但是默认是 1G,我们的现象也是 1G 中断,基本可以确认就是这个配置导致的,我们可以尝试修改为1200M,并验证,发现在下载到 1200M 时会中断。 ...

May 8, 2025

OOMKilled 深度排查:内存、PageCache 与 I/O 的联合分析

1、现象 收到Memory hit original limit内存告警与CPU容量水位告警,随后发生OOM,容器无限重启 2、应急解决方案 最重要的事情是先保证生产可用,并增加JVM参数用来观测,做完以下调整后对容器状态进行观察。 1)我们需要快速调高内存,内存参数调整为-Xmx3g -Xms3g。 2)加上JVM参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/logs/dump ,并挂载/app/logs/dump目录,确保堆内存OOM时可以将堆内存dump下来进行分析。 3)加上-XX:NativeMemoryTracking=summary,用来分析堆外内存。 3、收集相关数据信息 1)容器成功恢复健康状态,但是过了一段时间还是触发了OOMKilled,查看/app/logs/dump目录,并没有dump文件,说明有可能不是堆内存溢出,我们查看JVM状态,包括堆内存信息。 2)JVM状态分析 在容器重启之前,CPU占用在15%-30%之间,属于正常水平,堆内存也一直处于低水位状态,非堆内存也没有明显起伏,但是可以看到系统内存占用偏高,我们需要继续追踪堆外内存的情况。 3) 可以看到总的预留内存4.8GB,使用的物理内存3.56GB,而且通过在进程稳定时和内存到达上限时的快照信息分析,每项的值没有明显的变化。 4)查看容器详情 容器重启之前RSS全程没变,但是可以看到PageCache在重启之前飙升上去了。PageCache是文件缓存占用的内存,按道理来说,在内存达到瓶颈时,系统会自动回收才对。 4、问题分析,追究根因 想知道PageCache为什么在内存达到容器限制之前没被回收,我们需要知道k8s的oomkilled机制,在什么情况下会触发。 1)k8s 检测到容器内存达到memory.limit_in_bytes时且没有回收,便会触发 OOMKilled,我们找一个容器进行测试,将memory.limit_in_bytes调为524M(内部监控平台容器配额)。 查看memory.limit_in_bytes:cat /sys/fs/cgroup/memory/memory.limit_in_bytes watch -n 1 “cat /sys/fs/cgroup/memory/memory.usage_in_bytes” 观察内存增长 找一个大文件下载,wget “xxx”,并继续观察memory.usage_in_bytes,可以看到该值在文件下载的过程中一直上升,上升到接近memory.limit_in_bytes时触发了137错误码,即oomkilled。从现象可以得知,k8s的内存限制计算,会包含RSS和PageCache,RSS包含 JVM 进程的所有内存区域(如堆、非堆、JVM 自身代码、Native Memory等),memory.limit_in_bytes = RSS + PageCache。 2)为什么没有触发PageCache的回收,PageCache回收的速度也不可能赶不上带宽下行网速,原因是PageCache的回收是基于全局物理内存压力,不是单个容器的内存限制。而我们是在docker容器内,可以用命令查看物理内存。 容器是 Cgroups 隔离的独立环境,内核无法感知容器内的内存超限风险。即使容器内存即将超限,内核也不会优先回收其 PageCache,导致容器因 PageCache 触发 OOMKilled。 5、项目分析 随着效能平台的迭代,目前已经有了很多大文件下载或转存的场景,目前主要有: 1)效能客户端 APK 数据流上传(蓝线) 使用 MultipartFile 上传文件,spring.servlet.multipart.file-size-threshold 设置为 10M 时,超过 10M 则会存到临时文件,所以当 APK 文件过大时,也有 PageCache 上升的风险。(注释翻译:将基础输出流从基于内存的流切换到由磁盘支持的流。这是我们意识到有过多数据正在写入,无法再保存在内存中,因此选择切换到基于磁盘的存储。) ...

April 10, 2025