明明还有大量内存,为啥报错“无法分配内存”?

2944445d8cf94fca56577789ac2c5f5d.gif

作者 | 张彦飞allen

来源 | 开发内功修炼

近日小伙伴和我说了线上服务器出现一个诡异的问题,执行任何命令都是报错“fork:无法分配内存”。这个问题最近出现的,前几次重启后解决的,但是每隔 2-3 天就会出现一次。

# service docker stop
-bash fork: 无法分配内存
# vi 1.txt
-bash fork: 无法分配内存

看到这个提示,大家的第一反应肯定是怀疑内存真的不够了。我们这位读者也是这么认为的。但查看内存占用却发现根本没有,内存还空闲了一大把!(多试几次才有机会执行成功一次)

61bb2a2ec807edf12511482fefeb9530.png

飞哥帮出了三个思路:

  • 是不是numa架构下,进程启动的时候绑定了node,导致只有一个node里的内存在起作用?

  • numa架构下,如果所有内存都插到一个槽,其它node就会没内存

  • 查看下现在的进(线)程数是多少,是不是超过最大限制了

这里直接和大家汇报结论,前面关于 numa 内存不足的猜测是错误的。真实的原因是上面第 3 个,这台服务器上面的某几个java进程创建了太多的线程,导致了这个报错的产生,并不真的是内存不够。

8c0b47dc8e30a97797f6d98e19c9f0c9.png

底层过程分析

这个问题中,Linux 报错提示存在误导人的地方。导致大家并没有第一时间往进程数上想。所以才有了这么复杂曲折的排错过程,以至于在群里讨论才得以解决。

于是我想深入到内核里看看,报错到底是如何提示出来这么一个不恰当的错误提示的。然后顺便咱们也来了解了解创建进程的过程。

读者的线上服务器的操作系统是 CentOS 7.8,我查了一下对应的内核版本是 3.10.0-1127。

1.1 do_fork 剖析

在 Linux 内核里,无论是创建进程还是线程,都会调用到最核心的 do_fork 上来。在这个函数内部,通过拷贝的方式来创建新的进程(线程)所需要的内核数据对象。

//file:kernel/fork.c
long do_fork(unsigned long clone_flags, ...)
{//所谓的创建,其实是根据当前进程进行拷贝//注意:倒数第二个参数传入的是 NULLp = copy_process(clone_flags, stack_start, stack_size,child_tidptr, NULL, trace);...
}

整个进程创建的核心都是位于 copy_process 中,我们来看它的源码。

//file:kernel/fork.c
static struct task_struct *copy_process(unsigned long clone_flags, ...struct pid *pid,int trace)
{//内核表示进程(线程)的数据结构叫task_structstruct task_struct *p;......//拷贝方式生成新进程的核心数据结构p = dup_task_struct(current);//拷贝方式生成新进程的其它核心数据retval = copy_semundo(clone_flags, p);retval = copy_files(clone_flags, p);retval = copy_fs(clone_flags, p);retval = copy_sighand(clone_flags, p);retval = copy_mm(clone_flags, p);retval = copy_namespaces(clone_flags, p);retval = copy_io(clone_flags, p);retval = copy_thread(clone_flags, stack_start, stack_size, p);//注意这里!!!!!!//申请整数形式的 pid 值if (pid != &init_struct_pid) {retval = -ENOMEM;pid = alloc_pid(p->nsproxy->pid_ns);if (!pid)goto bad_fork_cleanup_io;}//将生成的整数pid值设置到新进程的 task_struct 上p->pid = pid_nr(pid);p->tgid = p->pid;if (clone_flags & CLONE_THREAD)p->tgid = current->tgid;bad_fork_cleanup_io:if (p->io_context)exit_io_context(p);
......
fork_out:return ERR_PTR(retval); 
}

通过以上代码可以看出,Linux 内核创建整个进程内核对象的创建过程都是通过分别调用不同的 copy_xxx 的方式来实现的,包括 mm 结构体、包括 namespaces等等。

我们来重点 alloc_pid 相关的这一段。在这一段中,目的是要申请一个 pid 对象出来。如果申请失败就返回错误了。大家注意这段代码的细节:无论 alloc_pid 返回的是何种类型的失败,其错误类型都写死的返回 -ENOMEM。。。 为了方便大家理解,我单独把这段逻辑再展示一遍。

//file:kernel/fork.c
static struct task_struct *copy_process(...){......//申请整数形式的 pid 值if (pid != &init_struct_pid) {retval = -ENOMEM;pid = alloc_pid(p->nsproxy->pid_ns);if (!pid)goto bad_fork_cleanup_io;}
bad_fork_cleanup_io:
...
fork_out:return ERR_PTR(retval); 
}

在准备调用 alloc_pid 的时候,直接就先将错误类型设置成了 -ENOMEM(retval = -ENOMEM),只要 alloc_pid 返回的不正确,都是将ENOMEM 这个错误返回给上层。而不管 alloc_pid 内存究竟是因为什么原因产生的错误

我们来查看一下 ENOMEM 的定义。它代表的是 Out of memory 的意思。(内核只是返回错误码,应用层再给出具体的错误提示,所以实际提示的是中文的“无法分配内存”)。

//file:include/uapi/asm-generic/errno-base.h
#define ENOMEM  12 /* Out of memory */

不得不说。内核的这个错误提示太成问题了。给使用者造成了很大的困惑。

1.2 导致 alloc_pid 失败的原因

那我们接着再来详细看看都有哪些情况下分配 pid 会失败呢?来看 alloc_pid 的源码

//file:kernel/pid.c
struct pid *alloc_pid(struct pid_namespace *ns)
{//第一种情况:申请 pid 内核对象失败pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);if (!pid)goto out;//第二种情况:申请整数 pid 号失败//调用到alloc_pidmap来分配一个空闲的pidtmp = ns;pid->level = ns->level;for (i = ns->level; i >= 0; i--) {nr = alloc_pidmap(tmp);if (nr < 0)goto out_free;pid->numbers[i].nr = nr;pid->numbers[i].ns = tmp;tmp = tmp->parent;}...
out:return pid; 
out_free:goto out; 
}

我们平时说的 pid 在内核中并不是一个简单的整数类型,而是一个小结构体来表示的(struct pid),如下。

//file:include/linux/pid.h
struct pid
{atomic_t count;unsigned int level;struct hlist_head tasks[PIDTYPE_MAX];struct rcu_head rcu;struct upid numbers[1];
};

所以需要先到内存中申请一块内存用来存储这个小对象。第一种错误情况是如果内存申请失败,alloc_pid 会返回失败。这种情况下确实是内存问题,出错后内核返回 ENOMEM 无可厚非。

接着往下看第二种情况,alloc_pidmap 是要为当前的进程申请进程号,就是我们平时所说的 PID 编号。如果申请失败,也会返回错误。

对于这种情况来说,只是分配进程编号出错了,和内存不够用半毛钱的关系都没有。但在这种情况下内核却会导致返回给上层的错误类型是 ENOMEM(Out of memory)。这实在是挺不合理的。

通过这里我们还额外学习到了另外一个知识!一个进程并不只是申请一个进程号就够了。而是通过一个 for 循环去申请了多个。

//file:kernel/pid.c
struct pid *alloc_pid(struct pid_namespace *ns)
{//调用到alloc_pidmap来分配一个空闲的pidtmp = ns;pid->level = ns->level;for (i = ns->level; i >= 0; i--) {nr = alloc_pidmap(tmp);if (nr < 0)goto out_free;pid->numbers[i].nr = nr;pid->numbers[i].ns = tmp;tmp = tmp->parent;}
}

假如说当前创建的进程是一个容器中的进程,那么它至少得申请两个 PID 号才行。一个 PID 是在容器命名空间中的进程号,一个是根命名空间(宿主机)中的进程号。

这也符合我们平时的经验。在容器中的每一个进程其实我们在宿主机中也都能看到。但是在容器中看到的进程号一般是和在宿主机上看到的是不一样的。比如一个进程在容器中的 pid 是 5,在宿主机命名空间下是 1256。那么该进程在内核中的对象大概是如下这个样子。

c89338a6ec0cfc645d81646a260306d5.png

5e3401e2210ce7366470bcc2664e75f2.png

新版本是否有所改观

接下来,我首先想到的可能是因为咱们用的内核版本太旧了。(我用的内核版本是 3.10.1)

所以我又到非常新的 Linux 5.16.11 翻了一翻,看看新版本是否有修复这个不恰当的提示。

推荐一个工具:https://elixir.bootlin.com/ 。在这个网站上可以查看任意版本的 linux 内核源码。如果只是临时看一下,用它非常的合适。

//file:kernel/fork.c
static __latent_entropy struct task_struct *copy_process(...)
{...pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid,args->set_tid_size);if (IS_ERR(pid)) {retval = PTR_ERR(pid);goto bad_fork_cleanup_thread;}
}

貌似看起来有戏,retval 不再写死的是 ENOMEM 了,而是根据 alloc_pid 实际的错误进行了设置。我们再来看 alloc_pid 是不是正确地设置错误类型了呢?

当我打开 alloc_pid 的源码里,看到这一大段注释的时候,我的心凉了半截。。。

//file:include/pid.c
struct pid *alloc_pid(struct pid_namespace *ns, ...)
{/** ENOMEM is not the most obvious choice especially for the case* where the child subreaper has already exited and the pid* namespace denies the creation of any new processes. But ENOMEM* is what we have exposed to userspace for a long time and it is* documented behavior for pid namespaces. So we can't easily* change it even if there were an error code better suited.*/retval = -ENOMEM;.......return retval
}

我把这段注释给大家大致翻译一下。它的意思是“ENOMEM不是最明显的选择,尤其是对于 pid 创建失败的情况下。但是,ENOMEM 是我们长期暴露给用户空间的东西。因此,即使有更适合的错误代码,我们也无法轻易更改它

看到这儿,我想起了有不少人也称 Linux 为屎山,可能这就是其中的一坨吧!最新的版本里也并没有很好地解决这个问题。

e7183f5ac923d8087c1e48ff9614ea0c.png

结论

在 Linux 里创建进程时,如果在 pid 不足的时候竟然返回的错误提示是“内存不足”。这个不恰当的错误提示导致很多同学都困惑不已。

通过今天的文章,以后你再遇到这种内存不足错误的时候,你就要多留个心眼儿了,别被内核被蒙骗了,先来看看自己的进程(线程)数是不是过多了。

至于说发现了这个问题该如何解决嘛,可以通过修改内核参数加大可用 pid 数量(/proc/sys/kernel/pid_max)。

但是我觉得最根本的方法还是要揪出来为啥系统中会出现这么多的进程(线程),然后把它干掉。默认情况下的两三万个进程数对于绝大多数的服务器来说已经是一个过于庞大的数字了,连这个数都超过了,一定是不合理的。

f5151b017297a6f766c46d4cf96d7606.gif

往期推荐

Redis 缓存击穿(失效)、缓存穿透、缓存雪崩怎么解决?

如果被问到分布式锁,应该怎样回答?

性能突出的 Redis 是咋使用 epoll 的?

Java 底层知识:什么是 “桥接方法” ?

ba759554b7e002f867177cb0d2f74b91.gif

点分享

14995d99eeb817f583469af35736b60e.gif

点收藏

780005c9b40479ec63858018f3f37513.gif

点点赞

f09529b8b914b1a2ebf72d49f2e3a4bf.gif

点在看

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/512171.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

先行一步,7大技术创新和突破,阿里云把 Serverless 领域的这些难题都给解了

简介&#xff1a; 函数计算 FC 首创 GPU 实例、业内首发实例级别可观测和调试、率先提供端云联调和多环境部署能力、GB 级别镜像启动时间优化至秒级、VPC 网络建连优化至200ms&#xff0c;Serverless 应用引擎 SAE 支持微服务框架无缝迁移、无需容器化改造、业内首创混合弹性策…

基于Delta lake、Hudi格式的湖仓一体方案

简介&#xff1a; Delta Lake 和 Hudi 是流行的开放格式的存储层&#xff0c;为数据湖同时提供流式和批处理的操作&#xff0c;这允许我们在数据湖上直接运行 BI 等应用&#xff0c;让数据分析师可以即时查询新的实时数据&#xff0c;从而对您的业务产生即时的洞察。MaxCompute…

如何新建java内部类_java内部类-1(内部类的定义)

小胖从官网出发&#xff0c;研究下为什么我们需要些内部类&#xff0c;内部类的区别和联系。思考三个问题&#xff1a;(1)为什么需要内部类&#xff1f;静态内部类和非静态内部类有什么区别&#xff1b;(2)为什么内部类可以无条件访问外部类成员&#xff1b;(3)为什么jdk1.8之前…

stack vs heap:栈区分配内存快还是堆区分配内存快 ?

作者 | 码农的荒岛求生来源 | 码农的荒岛求生有伙伴问到底是从栈上分配内存快还是从堆上分配内存快&#xff0c;这是个比较基础的问题&#xff0c;今天就来聊一聊。栈区的内存申请与释放毫无疑问&#xff0c;显然从栈上分配内存更快&#xff0c;因为从栈上分配内存仅仅就是栈指…

CDP 平台简介

简介&#xff1a; EDC 建立在 Cloudera Data Platform(CDP) 之上&#xff0c;该产品结合了 Cloudera Enterprise Data Hub 和 Hortonworks Data Platform Enterprise 的优点&#xff0c;并在技术堆栈中增加了新功能和对已有技术提供了增强功能。这种统一的发行是一个可扩展且可…

400倍加速, PolarDB HTAP实时数据分析技术解密

简介&#xff1a; PolarDB MySQL是因云而生的一个数据库系统, 除了云上OLTP场景&#xff0c;大量客户也对PolarDB提出了实时数据分析的性能需求。对此PolarDB技术团队提出了In-Memory Column Index(IMCI&#xff09;的技术方案&#xff0c;在复杂分析查询场景获得的数百倍的加速…

建立数字化、学习型人事平台,HR 与业务终于不再「隔空对话」

本篇文章暨 CSDN《中国 101 计划》系列数字化转型场景之一。 《中国 101 计划——探索企业数字化发展新生态》为 CSDN 联合《新程序员》、GitCode.net 开源代码仓共同策划推出的系列活动&#xff0c;寻访一百零一个数字化转型场景&#xff0c;聚合呈现并开通评选通道&#xff…

OpenYurt 深度解读|开启边缘设备的云原生管理能力

简介&#xff1a; 北京时间 9 月 27 号&#xff0c;OpenYurt 发布 v0.5.0 版本。新发布版本中首次提出 kubernetes-native非侵入、可扩展的边缘设备管理标准&#xff0c;使 Kubernetes 业务负载模型和 IOT 设备管理模型无缝融合。 作者&#xff5c;贾燚星(VMware), 何淋波(阿里…

Cloudera Manager 术语和架构

简介&#xff1a; 本文介绍了Cloudera Manager 的常见术语和架构 Cloudera Manager 术语 为了有效地使用Cloudera Manager&#xff0c;您应该首先了解其术语。 术语之间的关系如下所示&#xff0c;其定义如下&#xff1a; 有时&#xff0c;术语服务和角色用于同时指代类型和…

冬奥网络安全卫士被表彰突出贡献,探寻冬奥背后的安全竞技

奥运史上首次公开招募白帽子担任“冬奥网络安全卫士”。 据统计&#xff0c;从冬奥会开始到冬残奥会闭幕式结束&#xff0c;奇安信共检测日志数量累积超1850亿&#xff0c;日均检测日志超37亿&#xff0c;累计发现修复漏洞约5800个&#xff0c;发现恶意样本54个&#xff0c;排查…

打破 Serverless 落地边界,阿里云 SAE 发布 5 大新特性

简介&#xff1a; SAE 的 5 大新特性、4 大最佳实践&#xff0c;打破了 Serverless 落地的边界&#xff0c;让 All on Serverless 成为可能. 微服务场景&#xff0c;开源自建真的最快最省最稳的&#xff1f;复杂性真的会成为 Kubernetes 的“致命伤”吗&#xff1f;企业应用容…

java线程一定是thread_深入理解Java多线程(multiThread)

多线程的基本概念一个java程序启动后&#xff0c;默认只有一个主线程(Main Thread)。如果我们要使用主线程同时执行某一件事&#xff0c;那么该怎么操作呢&#xff1f;例如&#xff0c;在一个窗口中&#xff0c;同时画两排圆&#xff0c;一排在10像素的高度&#xff0c;一排在5…

技术解读|云上企业级存储——打开存储新维度,促进用户核心业务创新

简介&#xff1a; 将企业级存储和云的特点进行完美的融合是云上企业级存储的目标&#xff0c;它打开存储更多新的维度&#xff0c;在保障用户业务永续的同时&#xff0c;帮助用户更好的进行业务创新。本文属ESSD技术解读的总篇&#xff0c;总体介绍ESSD 云盘创新融合了云和企业…

金蝶发布2021年财报:云业务同比增44.2%,继续加码研发技术创新

编辑 | 宋慧 出品 | CSDN云计算 金蝶国际软件集团有限公司&#xff08;“金蝶国际”、“金蝶”或“公司”&#xff0c;连同其附属公司统称“集团”&#xff1b;股份编号&#xff1a;0268.HK&#xff09;今日公布其截至2021年12月31日止十二个月&#xff08;“报告期”&#xf…

分布式系统一致性测试框架Jepsen在女娲的实践应用

简介&#xff1a; 女娲团队在过去大半年时间里持续投入女娲2.0研发&#xff0c;将一致性引擎和业务状态机解耦&#xff0c;一致性引擎可支持Paxos、Raft、EPaxos等多种一致性协议&#xff0c;根据业务需求支撑不同的业务状态机。其中的一致性引擎模块是关键&#xff0c;研发一致…

“预习-上课-复习”:达摩院类人学习新范式探索

简介&#xff1a; 预习时关注重点&#xff0c;上课时由易到难&#xff0c;复习时举一反三&#xff0c;能否让机器也按照“预习-上课-复习”的学习范式进行学习呢&#xff1f; 达摩院对话智能&#xff08;Conversational AI&#xff09;团队对这个问题进行了研究探索&#xff0c…

云上虚拟IDC(私有池)如何为客户业务的确定性、连续性保驾护航

简介&#xff1a; 企业业务上云后&#xff0c;还面临特定可用区购买云上特定计算产品实例失败的困境&#xff1f;云上私有池pick一下 Why 云上业务为什么需要资源确定性、服务连续性 云计算正朝着像水电煤一样的基础设施演进&#xff0c;支持用户按需使用、按量付费。目前&am…

java中img属性_如果html img的src属性无效,请输入默认图像?

回答(19)2 years ago你问过一个只有HTML的解决方案....../p>"http://www.w3.org/TR/html4/strict.dtd">Object Test由于第一个图像没有使用不支持object的旧浏览器&#xff0c;因此它将忽略该标记并使用 img 标记 . 有关兼容性&#xff0c;请参见caniuse网站 .…

阿里云日志服务SLS,打造云原生时代智能运维

2021年10月21日&#xff0c;阿里云针对企业运维难题&#xff0c;在云栖大会为大家带来了一场《智能运维论坛》的主题演讲。在会上&#xff0c;阿里云资深技术专家、日志服务技术负责人简志提出“云原生时代&#xff0c;企业业务数字化是对工程师们严峻的挑战。作为运维工程师&a…

实践分享丨企业上云后资源容量如何规划和实施

简介&#xff1a; 企业上云后&#xff0c;云上的预算直接影响上云的优先级、进度、深度。预算投入的多少&#xff0c;与业务发展和资源需求的容量评估紧密相关。精准的容量评估&#xff0c;可以使企业上云的预算规划更科学&#xff0c;同时也更贴合业务发展阶段的需要。本文分享…