MySQL · 捉虫动态 · Error in munmap() "Cannot allocate memory"

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介:

前言

最近线上遇到一个问题,一个MySQL实例报错 Error in munmap(): Cannot allocate memory 造成进程异常退出

背景介绍

MySQL 使用 jemalloc 进行内存分配,报错的原因是 MySQL 进程的 VMA 数量大于操作系统上限

这里先介绍几个前序概念

虚拟内存区域 VMA

Linux进程通过vma进行管理,每个进程都有一个结构体中维护一个vma链表,其中每个vma节点对应一段连续的进程内存。这里的连续是指在进程空间中连续,物理空间中不一定连续。如果进程申请一段内存,则内核会给进程增加vma节点

/proc/pid/maps

/proc/pid/maps 记录了进程的虚拟内存使用情况

举个例子,进程b.out的maps如下,每一行代表一个VMA(删除了一部分重复的行

00400000-00401000 r-xp 00000000 fd:01 1574192                            /u01/b.out
00602000-00701000 rw-p 00000000 00:00 0                                  [heap]
7ffff71f8000-7ffff73b0000 r-xp 00000000 fd:01 1049989                    /usr/lib64/libc-2.17.so
7ffff75b6000-7ffff75bb000 rw-p 00000000 00:00 0
7ffff75bb000-7ffff75d0000 r-xp 00000000 fd:01 1052643                    /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7ffff77d1000-7ffff78d2000 r-xp 00000000 fd:01 1049997                    /usr/lib64/libm-2.17.so
fff7ad3000 rw-p 00101000 fd:01 1049997                    /usr/lib64/libm-2.17.so
7ffff7ad3000-7ffff7bbc000 r-xp 00000000 fd:01 1050280                    /usr/lib64/libstdc++.so.6.0.19
dc6000 rw-p 000f1000 fd:01 1050280                    /usr/lib64/libstdc++.so.6.0.19
7ffff7dc6000-7ffff7ddb000 rw-p 00000000 00:00 0
7ffff7ddb000-7ffff7dfc000 r-xp 00000000 fd:01 1049982                    /usr/lib64/ld-2.17.so
7ffff7fce000-7ffff7ff4000 rw-p 00000000 00:00 0
7ffff7ff9000-7ffff7ffa000 rw-p 00000000 00:00 0
7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0                          [vdso]
7ffff7ffc000-7ffff7ffd000 r--p 00021000 fd:01 1049982                    /usr/lib64/ld-2.17.so
7ffff7ffd000-7ffff7ffe000 rw-p 00022000 fd:01 1049982                    /usr/lib64/ld-2.17.so
7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0                          [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
  • 第一列,如00400000-00401000
    虚拟空间的起始和终止地址
  • 第二列,如rw-p
    VMA的权限,前三位rwx分别代表可读、可写、可执行,“-”代表没有该权限;第四位p/s代表私有/共享段
  • 第三列,如00021000
    虚拟内存起始地址在文件中的偏移量,匿名映射为0
  • 第四列,如fd:01
    映射文件所属设备好,匿名映射为0
  • 第五列,如1049982
    映射文件所属节点号,匿名映射为0
  • 第六列,如/u01/b.out /usr/lib64/libstdc++.so.6.0.19 [stack]
    映射文件名,[heap]代表堆,[stack]代表栈

vm.max_map_count

max_map_count 是一个进程内存能拥有的VMA最大数量

当进程达到了VMA上限但又只能释放少量的内存给其他的内核进程使用时,操作系统会抛出内存不足的错误

Error in munmap(): Cannot allocate memory 就是触发了这个错误

问题复现

操作系统 vm.max_map_count=65530

执行以下代码,可以复现munmap无法分配内存的错误

#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#define VM_MAX_MAP_COUNT (65530)
#define VM_SIZE (4096)
#define VM_CNT (VM_MAX_MAP_COUNT * 2)


static void* vma[VM_CNT];

int main(void)
{
    int i;
    for (i = 0; i < VM_CNT; i++)
    {
        vma[i] = mmap(0, VM_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0 );
    }

    for (i = 0; i < VM_CNT; i++)
    {
        if (munmap(vma[i], VM_SIZE) != 0)
            printf("mumap() ERROR");
    }
}

先用 mmap 分配 65530 * 2 个虚拟内存空间

因为是连续分配的,操作系统会合并成一个VMA,下图可以看出,/proc/pid/maps 文件中多了一个VMA

另有两个已存在的VMA被修改

7


多出来的VMA 7fffd73fc000-7ffff71f8000 共有130566个 VM_SIZE
7ffff7fef000-7ffff7ff4000(0x5000) -> 7ffff7dfc000-7ffff7ff5000(0x1f9000) 多出 500 个 VM_SIZE

7ffff7ff9000-7ffff7ffa000 -> 7ffff7ff5000-7ffff7ffa000 起始地址前移 0x4000, 多出 4 个 VM_SIZE

130566 + 500 + 4 = 131070 = 65536 * 2 正好是程序中申请的内存大小

下面再用munmap每隔一个VM_SIZE释放一个VM_SIZE,将原本连续的虚拟内存空间变得不连续,这样就会形成65536个VMA,再加上本来存在的若干个VMA,超过了操作系统设定的VMA上限 65530

实际执行时,VMA数量到达65530时,再执行munmap就会报错

jemalloc 和 glibc malloc

MySQL 使用 jemalloc 分配内存,jemalloc 默认采用mmap()/munmap()分配和释放内存,已经验证 jemalloc 在 max_map_count较小时会触发无法分配内存的异常

使用同样场景验证 glibc malloc 是否存在同样问题,glibc malloc 分配 128k 以上内存是默认使用mmap,128k以下时默认使用sbrk,所以这里把VM_SIZE 改为 129k

#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#define VM_MAX_MAP_COUNT (65530)
#define VM_SIZE (129 * 1024)
#define VM_CNT (VM_MAX_MAP_COUNT * 2)


static void* vma[VM_CNT];

int main(void)
{
    int i;
    for (i = 0; i < VM_CNT; i++)
    {
        vma[i] = malloc(VM_SIZE);
    }

    for (i = 0; i < VM_CNT; i++)
    {
        free(vma[i])
    }
}

8

上图可以看出,除了申请新的VMA,heap空间也增长了

新增VMA 7ffde73e7000-7ffff71f8000 共 67044个VM_SIZE

7ffff7fef000-7ffff7ff4000 -> 7ffff7e00000-7ffff7ff4000 共15个VM_SIZE

[heap] 00602000-00701000 -> 00602000-20467e000 共 65531 个VM_SIZE
多```  
分配出1530个VM_SIZE 这里应该是glibc 内部机制控制的,不做深入研究

重点在于申请了65531个VM_SIZE的[heap]空间,这部分空间由sbrk()申请

###mmap() 和 sbrk()
malloc 申请小于 128k 内存时,使用 sbrk() 分配,大于 128k 默认使用 mmap()

同时,mmap() 分配内存最多65536次,超过后使用 sbrk() 分配

mmap()在虚拟地址空间中找一块空闲地址分配,分配的内存可以被随意释放

sbrk() 将指向数据段的最高地址的_edata指针上推,释放时将_edata下推;显然,sbrk()无法随意释放内存,释放一块内存时,必须把比它地址高的内存全部释放

如图,初始时 _edata 在 heap 最下方,分配A时 _edata 推到 A 下方,分配 B/ C时 _edata 推到 B/C 下方

释放 C 时,再将 _edata 上推到 B 下方,但是在释放 B 之前释放 A 只能讲 A 的内存块标记为未使用,供下次分配,不能移动 _edata
<p style="text-align:center">![9](https://yqfile.alicdn.com/b32ee8615f90dc3b04c83acd0267c7d170d67b85.png)
</p>
显然,释放sbrk()申请的内存时,不会增加VMA数量,所以 glibc malloc 不会使 VMA 数量超过 max_map_count,不会触发上述问题

###总结
问题的原因很明显,VMA 数量超过 max_map_count,调大 max_map_count 可以简单粗暴的解决这个问题
相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
13天前
|
存储 关系型数据库 MySQL
【赵渝强老师】MySQL的Memory存储引擎
MySQL 的存储引擎层负责数据的存储和提取,支持多种存储引擎,如 InnoDB、MyISAM 和 Memory。InnoDB 是最常用的存储引擎,从 MySQL 5.5.5 版本起成为默认引擎。Memory 存储引擎的数据仅存在于内存中,重启后数据会丢失。示例中创建了使用 Memory 引擎的 test3 表,并展示了数据在重启后消失的过程。
|
20天前
|
网络协议 关系型数据库 MySQL
MySQL报ERROR 2002 (HY000)解决
通过上述步骤,可以有效地解决MySQL连接时出现的 `ERROR 2002 (HY000)`错误。这些步骤包括检查和启动MySQL服务、配置文件检查、套接字文件检查、日志文件分析、进程检查、防火墙设置、客户端配置和最终的MySQL重装。确保每个步骤都按顺序执行,有助于快速定位和解决问题,使MySQL服务器恢复正常运行。
250 0
|
3月前
|
SQL 关系型数据库 MySQL
mysql密码错误-ERROR 1045 (28000): Access denied for user ‘root‘@‘localhost‘ (using passwor:yes)
这篇文章提供了解决MySQL数据库"Access denied for user 'root'@'localhost' (using password: YES)"错误的方法,通过跳过密码验证、修改root密码,然后重启服务来解决登录问题。
mysql密码错误-ERROR 1045 (28000): Access denied for user ‘root‘@‘localhost‘ (using passwor:yes)
|
3月前
|
关系型数据库 MySQL
MySQL 8.0 - Authentication plugin ‘caching_sha2_password‘ cannot be loaded 原因及解决办法
MySQL 8.0 - Authentication plugin ‘caching_sha2_password‘ cannot be loaded 原因及解决办法
178 1
|
3月前
|
关系型数据库 MySQL Java
【Azure 应用服务】应用服务连接 Azure MySQL 一直失败,报错 Create connection error
【Azure 应用服务】应用服务连接 Azure MySQL 一直失败,报错 Create connection error
|
4月前
|
关系型数据库 MySQL Linux
error: Failed dependencies: libncurses.so.5()(64bit) is needed by mysql-community-client-8.0.36-1.el7.x86_64 libtinfo.so.5()(64bit) is needed by mysql-community-client-8.0.36-1.el7.x86_64 如何解决?
error: Failed dependencies: libncurses.so.5()(64bit) is needed by mysql-community-client-8.0.36-1.el7.x86_64 libtinfo.so.5()(64bit) is needed by mysql-community-client-8.0.36-1.el7.x86_64 如何解决?
597 3
|
13天前
|
SQL 关系型数据库 MySQL
12 PHP配置数据库MySQL
路老师分享了PHP操作MySQL数据库的方法,包括安装并连接MySQL服务器、选择数据库、执行SQL语句(如插入、更新、删除和查询),以及将结果集返回到数组。通过具体示例代码,详细介绍了每一步的操作流程,帮助读者快速入门PHP与MySQL的交互。
26 1
|
15天前
|
SQL 关系型数据库 MySQL
go语言数据库中mysql驱动安装
【11月更文挑战第2天】
29 4
|
1月前
|
存储 关系型数据库 MySQL
Mysql(4)—数据库索引
数据库索引是用于提高数据检索效率的数据结构,类似于书籍中的索引。它允许用户快速找到数据,而无需扫描整个表。MySQL中的索引可以显著提升查询速度,使数据库操作更加高效。索引的发展经历了从无索引、简单索引到B-树、哈希索引、位图索引、全文索引等多个阶段。
63 3
Mysql(4)—数据库索引
|
22天前
|
监控 关系型数据库 MySQL
数据库优化:MySQL索引策略与查询性能调优实战
【10月更文挑战第27天】本文深入探讨了MySQL的索引策略和查询性能调优技巧。通过介绍B-Tree索引、哈希索引和全文索引等不同类型,以及如何创建和维护索引,结合实战案例分析查询执行计划,帮助读者掌握提升查询性能的方法。定期优化索引和调整查询语句是提高数据库性能的关键。
105 1
下一篇
无影云桌面