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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介:

前言

最近线上遇到一个问题,一个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 可以简单粗暴的解决这个问题
相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
27天前
|
关系型数据库 MySQL 数据安全/隐私保护
关于Navicat连接MySQL 报 Authentication plugin ‘caching_sha2_password‘ cannot be loaded
关于Navicat连接MySQL 报 Authentication plugin ‘caching_sha2_password‘ cannot be loaded
|
8天前
|
SQL 存储 关系型数据库
17. Mysql 动态SQL
17. Mysql 动态SQL
|
29天前
|
关系型数据库 MySQL 数据库连接
解决 mysql8.0 ERROR 1045 (28000): Access denied for user ‘ODBC‘@‘localhost‘ (using password: NO)用户访问拒绝
解决 mysql8.0 ERROR 1045 (28000): Access denied for user ‘ODBC‘@‘localhost‘ (using password: NO)用户访问拒绝
220 52
解决 mysql8.0 ERROR 1045 (28000): Access denied for user ‘ODBC‘@‘localhost‘ (using password: NO)用户访问拒绝
|
29天前
|
关系型数据库 MySQL 数据库
MySQL 启动 登录报错Job for mysqld.service failed because the control process exited with error code. See
MySQL 启动 登录报错Job for mysqld.service failed because the control process exited with error code. See
|
29天前
|
SQL 关系型数据库 MySQL
MySQL SQL error: #1271 - Illegal mix of collations for operation ‘UNION‘
MySQL SQL error: #1271 - Illegal mix of collations for operation ‘UNION‘
|
29天前
|
关系型数据库 MySQL 数据库
docker 安装mysql(踩坑踩得想哭 详细解决教程)ERROR 1045 (28000): Access denied for user ‘root‘@‘localhost‘ (using pa
docker 安装mysql(踩坑踩得想哭 详细解决教程)ERROR 1045 (28000): Access denied for user ‘root‘@‘localhost‘ (using pa
67 1
|
2天前
|
关系型数据库 数据管理 数据库
数据管理DMS产品使用合集之如何极速恢复RDS(关系型数据库服务)中的数据表
阿里云数据管理DMS提供了全面的数据管理、数据库运维、数据安全、数据迁移与同步等功能,助力企业高效、安全地进行数据库管理和运维工作。以下是DMS产品使用合集的详细介绍。
|
2天前
|
NoSQL 关系型数据库 Serverless
Serverless 应用引擎产品使用合集之连接RDS、Redis等数据库时,是否需要通过安全组来控制访问权限
阿里云Serverless 应用引擎(SAE)提供了完整的微服务应用生命周期管理能力,包括应用部署、服务治理、开发运维、资源管理等功能,并通过扩展功能支持多环境管理、API Gateway、事件驱动等高级应用场景,帮助企业快速构建、部署、运维和扩展微服务架构,实现Serverless化的应用部署与运维模式。以下是对SAE产品使用合集的概述,包括应用管理、服务治理、开发运维、资源管理等方面。
|
2天前
|
SQL 关系型数据库 MySQL
Python 操作 MySQL 数据库
Python 操作 MySQL 数据库
|
2天前
|
DataWorks 监控 关系型数据库
DataWorks产品使用合集之在 DataWorks 中删除了 la_saas_rds 数据库,为什么其他数据库的同步任务无法打开或添加新表同步
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。