first commit
This commit is contained in:
101
专栏/Linux内核技术实战课/00开篇词如何让Linux内核更好地服务应用程序?.md
Normal file
101
专栏/Linux内核技术实战课/00开篇词如何让Linux内核更好地服务应用程序?.md
Normal file
@ -0,0 +1,101 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
00 开篇词 如何让Linux内核更好地服务应用程序?
|
||||
你好,我是邵亚方,欢迎加入我的课程,和我一起学习Linux内核知识。
|
||||
|
||||
从2010年接触Linux内核到现在,因为工作的关系,我参与解决了大量直接与生产环境相关的性能问题。前些年,我还在蘑菇街的时候,那会蘑菇街的业务增长速度非常快。你知道,业务增长了,随之而来的肯定就是服务的稳定性挑战了,比如TCP重传该怎么分析、怎么在运⾏时不打断任务的情况下排查内存泄漏问题、CPU sys利⽤率⾼怎么快速解决,这都是实实在在的问题,你会就会,不会就是不会。
|
||||
|
||||
以我们常见的TCP重传为例,如果你熟悉的话,服务器上一般都会有TCP重传率的监控,如下图所示:
|
||||
|
||||
|
||||
|
||||
就像图片显示的那样,这么高的TCP重传率,必然会导致系统QPS减小,所以你不敢含糊,得赶紧找问题出在哪里。但真正排查的时候,你会发现不知道从哪里开始,因为发生重传时的现场信息并没有记录下来。之所以没有记录下来这些现场信息,是因为记录的成本太昂贵了。如果你排查过网络问题,你应该知道,网络数据量是非常大的,即使只记录TCP头部信息也是非常大的存储开销,而且信息记录的过程也会带来较多的性能开销。
|
||||
|
||||
为了解决这类问题,我们团队就针对TCP重传的监控做了很多的改进,这些改进可以在不对业务性能造成明显影响的情况下,提升团队定位和分析问题的效率。我们记录的TCP重传现场信息如下所示:
|
||||
|
||||
18:21:58 10.17.130.19:20612 124.74.250.144:44 SYN_SENT
|
||||
18:22:00 10.17.130.19:20612 124.74.250.144:443 SYN_SENT
|
||||
18:23:21 10.17.130.19:20716 124.74.250.144:443 SYN_SENT
|
||||
18:23:23 10.17.130.19:20716 124.74.250.144:443 SYN_SENT
|
||||
18:24:39 10.17.130.19:20796 124.74.250.144:443 SYN_SENT
|
||||
18:24:41 10.17.130.19:20796 124.74.250.144:443 SYN_SENT
|
||||
18:25:43 10.17.130.19:20861 124.74.250.144:443 SYN_SENT
|
||||
18:25:45 10.17.130.19:20861 124.74.250.144:443 SYN_SENT
|
||||
18:27:23 10.17.130.19:20973 124.74.250.144:443 SYN_SENT
|
||||
18:27:25 10.17.130.19:20973 124.74.250.144:443 SYN_SENT
|
||||
|
||||
|
||||
通过上面这些信息,你能很简单地看到,TCP重传是发生在哪些服务器(IP地址)之间,哪些业务上(服务端口),以及为什么会重传(SYN_SENT)。你看,这样效率不就高了吗?具体我们是怎么做的,后面咱们课程里我会详细和你说。
|
||||
|
||||
其实在我看来,对于类似TCP重传这样复杂稳定性问题的定位,除了从开发⼈员的视⻆来分析外,更是需要能够从系统、从内核的视⻆来分析,只有这样,你才能够追本溯源、一劳永逸地解决问题。
|
||||
|
||||
而大家之所以觉得这些问题难,本质上还是对Linux内核理解不到位。比如说,我接触过的业务开发者们,基本上都被业务的性能毛刺困扰过,但是很多人在分析这些性能毛刺时,只能分析到哪些系统调用引起的毛刺,而一些业务专家,却可以再往底层看是什么系统资源引起的业务毛刺。再比如,当发生TCP重传时,有人可以从tcpdump里面的信息看到是哪个TCP连接进行重传,然而高手们却可以通过这些信息看到为什么会发生重传。
|
||||
|
||||
能够深入到Linux内核层分析问题的这些人,他们看问题能直击本质,定位、分析问题的能力强,往往能解决别人解决不了的问题,所以他们基本上也是各自领域的翘楚。
|
||||
|
||||
然而,大部分做应用的开发者往往会忽略对Linux内核的学习,这并不难理解,毕竟本职工作更多是在业务代码的优化和调配上,在互联网公司普遍“996”工作的大环境下,很难有精力和时间去深入到内核层面学习。而且,Linux内核本身就是很复杂的,这种复杂度不仅让应用开发者望而却步,也让很多内核初学者知难而退。
|
||||
|
||||
就拿我自己来说吧,我为了学习好Linux内核,就啃过非常多中英文的技术书籍,比如:
|
||||
|
||||
|
||||
为了理解应用程序是如何使用Linux内核的,我把《Unix环境高级编程》这本书读了很多遍;
|
||||
为了掌握系统体系结构,我仔细读完了《深入理解计算机系统》和《支撑处理器的技术》;
|
||||
为了理解应用程序在Linux上是如何编译的,我深入阅读了《An Introduction to GCC》以及《Linkers and Loaders》;
|
||||
为了掌握Linux设备驱动的运行原理,我阅读了《Linux设备驱动程序》这本书,然后,我才开始深入阅读《深入理解Linux内核》这本Linux内核开发者的入门书籍;
|
||||
在学习过程中,为了更好的掌握内存子系统,我阅读了《深入理解Linux虚拟内存管理》;
|
||||
为了掌握网络子系统,我阅读了《TCP/IP Illustrated, Volume 1: The Protocols》以及《TCP/IP Architecture, Design and Implementation in Linux》;
|
||||
为了熟悉其他操作系统的设计原理,我阅读了《The Design and Implementation of the FreeBSD Operating System》来学习FreeBSD;
|
||||
为了更好的分析Linux内核问题,我读了《Debug Hacks : 深入调试的技术和工具》和《Systems Performance : Enterprise and the Cloud》。
|
||||
为了更好的和国外开发者做交流,我又阅读了大量的英文原版书籍来提升自己的英语水平,上面我列的这些书名为英文的就是其中部分原版技术书籍。
|
||||
|
||||
|
||||
不可否认,如果想系统学习Linux内核,成本是非常高的。不过话说回来,如果你不是内核开发者,也确实没有必要去搞懂它的每个细节,去掌握它的每一个机制,去理解它所有的优秀设计思想。
|
||||
|
||||
在我看来,一个优秀的软件,或者一个优秀的代码,存在的本质是为了解决我们遇到的实实在在的问题,更好地满足我们实际的需求。
|
||||
|
||||
也就是说,你能通过掌握的Linux内核知识,解决实际应用层的问题就够了,这也是我开设这门课的初衷。我希望能把自己多年的Linux内核学习和实践经验,通过“解决问题,满足需求”的方式传递给你,让Linux内核更好地服务你的应用程序。
|
||||
|
||||
为了更好地达到这个目的,我会从一些生产环境中比较常见的问题入手,带你去了解:你的应用程序是怎么跟系统资源打交道的?你的业务类型应该要选择什么样的配置才会更好?出了棘手的问题该如何一步步地去排查?
|
||||
|
||||
那么,从系统资源的维度,我们需要关注的问题可以分为四类,分别是磁盘I/O、内存、网络I/O、CPU。那在这系列课程中,我会带你从这四大类中的典型问题入手,深入学习其中的:Page Cache管理问题、内存泄漏问题、TCP重传问题、内核态CPU利用率飙高问题。这也对应着我们课程的四个模块。
|
||||
|
||||
掌握了这四类典型问题以及其分析思路,你会对磁盘I/O、内存、网络I/O和CPU这四类服务器上最基础的资源有更加深入的理解,在遇到其他问题时也能够触类旁通,从此再也不用回避一些棘手的系统问题。
|
||||
|
||||
为了方便你循序渐进地学习,我们的每个模块都会按照基础篇、案例篇和分析篇的方式来呈现。
|
||||
|
||||
在Page Cache管理这个模块中,我会重点分析如何更好地利用Page Cache来减少无谓的I/O开销,Page Cache管理不当会引起的一些问题,以及如何去分析和解决这类问题。
|
||||
|
||||
在内存泄漏这个模块中,我会重点分析应用程序都是如何从系统中申请内存以及如何释放的。通过内存泄露这类案例来带你了解应用程序使用内存的细节,以及如果内存使用不当会引发的一些问题。当然,我也会带你去观察、分析和解决这类问题。
|
||||
|
||||
在TCP重传这个模块中,我会重点分析TCP连接的建立、传输以及断开的过程。这个过程究竟会受哪些配置项的影响?如果配置不当会引起什么网络问题?然后我会从TCP重传这类具体案例出发,来带你认识你必须要去掌握的一些网络细节知识,以及遇到网络相关的问题时,你该如何去分析和解决它。
|
||||
|
||||
在内核态CPU利用率飙高这个模块中,我会分析应用程序该如何高效地使用CPU,以及哪些情况下会导致CPU的使用很低效:比如内核态CPU利用率过高就是一个很低效的表现。那么,针对内核态CPU利用率高的这个案例,我会侧重讲解哪些Linux内核的特性或者系统配置项会引起这种问题,以及如何分析和解决具体的问题。
|
||||
|
||||
在每个模块的最后,我都会总结一下这些常见问题的一般分析思路,让你在面对类型问题时能够有一个大致的分析方向。
|
||||
|
||||
当然,这个课不仅仅是针对应用开发者和运维,对于内核开发者,特别是不那么资深的内核开发者也会很有帮助,它可以帮助你更好地理解业务。Linux内核本质上是给业务服务的,理解好了业务,你才能更好地实现内核特性。就像我给Linux内核提交一些patch时,maintainer们总是会喜欢问这个问题:“What is your real life usecase ?” 结合业务,不盲目设计,也不要过度设计,这是每一个Linux内核开发都需要谨记的。
|
||||
|
||||
这个课程,除了教你如何更好地学习Linux内核之外,也会带你批判性地来看Linux内核。
|
||||
|
||||
Linux内核从Linus在1991年发布第一版开始,迄今已发展了近30年,代码行数也从最开始的1万行代码发展到了现在的几千万行代码,这么复杂甚至有点臃肿的工程,肯定是存在Bug的,而且也存在很多糟糕的设计。所以,你在平时工作中遇到的很多费解的现象, 也有可能是Linux内核缺陷,我们在这个课程里也会带你认识这些随处可见的缺陷。
|
||||
|
||||
在你遇到有些疑惑的地方,或者你觉得Linux内核哪里不够好时,你可以大胆地去质疑它,去向精通Linux内核的人求助,或者向Linux社区求助,正是因为你的这些需求,我们才能把Linux内核建设的更好更强大。虽然Linus本人是个稍微有点脾气的人,但是整体上Linux社区是很友好的,不论你遇到什么样子的难题,总有人能够解答你的疑问,甚至引导你该如何更好地改进Linux内核。
|
||||
|
||||
最后,我来大概介绍一下我在Linux内核领域的工作经历吧。我从2010年开始,在华为正式从事Linux内核的开发工作,然后又经历了外企(Juniper Networks),也经历了互联网企业(蘑菇街)以及现在所在的某知名互联网企业。
|
||||
|
||||
不同的企业风格,不同的业务场景,但是它们对Linux内核都有着相同的诉求:更好更稳定。
|
||||
|
||||
这些不同的业务场景,也丰富了我对Linux内核的认识,我也逐渐感受到Linux内核在处理更多以及更新的业务场景下的不足之处,所以我也慢慢地参与到Linux内核社区中来改进Linux内核。
|
||||
|
||||
我目前主要是活跃在Linux内核的内存管理子系统( [email protected] ),如果你有关注这个邮件列表的话,会经常看到我的名字(Yafang Shao < [email protected] >)。如果你在工作中遇到Linux内核相关的问题,也可以直接发送邮件给我,不过因为平时工作要“996”,我大概率只能在周末才会有精力来回复你。当然,也欢迎你在这个课程下留言提问,我看到都会第一时间处理。
|
||||
|
||||
在最后,我再次温馨地提醒你一下,Linux底层知识的学习并不是一蹴而就的,想要很好地掌握它是会花很多时间的。但是,如果有经验丰富的人带你来学习,你的学习时间会大大缩短,你的学习成本也会降低很多。我相信,通过这个系列课程的学习,你不仅可以很好地掌握必备的Linux基础知识,也能够从中学习到很多解决实际问题的技巧,以及避免去踩很多已经被别人踩过的坑。
|
||||
|
||||
好了,最后欢迎你与我讨论,你现在的工作中有遇到哪些困惑吗?你对Linux内核理解到什么程度了呢?你可以把自己当下的起点或者疑惑记录下来,等全部学完这个课程再来回顾,相信你会有不一样的体会。
|
||||
|
||||
|
||||
|
||||
|
176
专栏/Linux内核技术实战课/01基础篇如何用数据观测PageCache?.md
Normal file
176
专栏/Linux内核技术实战课/01基础篇如何用数据观测PageCache?.md
Normal file
@ -0,0 +1,176 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
01 基础篇 如何用数据观测Page Cache?
|
||||
你好,我是邵亚方。今天我想和你聊一聊Page Cache的话题。
|
||||
|
||||
Page Cache你应该不陌生了,如果你是一名应用开发者或者Linux运维人员,那么在工作中,你可能遇见过与Page Cache有关的场景,比如:
|
||||
|
||||
|
||||
服务器的load飙高;
|
||||
服务器的I/O吞吐飙高;
|
||||
业务响应时延出现大的毛刺;
|
||||
业务平均访问时延明显增加。
|
||||
|
||||
|
||||
这些问题,很可能是由于Page Cache管理不到位引起的,因为Page Cache管理不当除了会增加系统I/O吞吐外,还会引起业务性能抖动,我在生产环境上处理过很多这类问题。
|
||||
|
||||
据我观察,这类问题出现后,业务开发人员以及运维人员往往会束手无策,究其原因在于他们对Page Cache的理解仅仅停留在概念上,并不清楚Page Cache如何和应用、系统关联起来,对它引发的问题自然会束手无策了。所以,要想不再踩Page Cache的坑,你必须对它有个清晰的认识。
|
||||
|
||||
那么在我看来,认识Page Cache最简单的方式,就是用数据说话,通过具体的数据你会更加深入地理解Page Cache的本质。为了帮你消化和理解,我会用两节课的时间,用数据剖析什么是Page Cache,为什么需要Page Cache,Page Cache的产生和回收是什么样的。这样一来,你会从本质到表象,透彻理解它,深切感受它和你的应用程序之间的关系,从而能更好地理解上面提到的四个问题。
|
||||
|
||||
不过,在这里我想给你提个醒,要学习今天的内容,你最好具备一些Linux编程的基础,比如,如何打开一个文件;如何读写一个文件;如何关闭一个文件等等。这样,你理解今天的内容会更加容易,当然了,不具备也没有关系,如果遇到你实在看不懂的地方,你可以查阅《UNIX环境高级编程》这本书,它是每一位Linux开发者以及运维人员必看的入门书籍。
|
||||
|
||||
好了,话不多说,我们进入今天的学习。
|
||||
|
||||
什么是Page Cache?
|
||||
|
||||
我记得很多应用开发者或者运维在向我寻求帮助,解决Page Cache引起的问题时,总是喜欢问我Page Cache到底是属于内核还是属于用户?针对这样的问题,我一般会让他们先看下面这张图:
|
||||
|
||||
|
||||
|
||||
通过这张图片你可以清楚地看到,红色的地方就是Page Cache,很明显,Page Cache是内核管理的内存,也就是说,它属于内核不属于用户。
|
||||
|
||||
那咱们怎么来观察Page Cache呢?其实,在Linux上直接查看Page Cache的方式有很多,包括/proc/meminfo、free 、/proc/vmstat命令等,它们的内容其实是一致的。
|
||||
|
||||
我们拿/proc/meminfo命令举例看一下(如果你想了解/proc/meminfo中每一项具体含义的话,可以去看Kernel Documentation的meminfo这一节,它详细解释了每一项的具体含义,Kernel Documentation是应用开发者想要了解内核最简单、直接的方式)。
|
||||
|
||||
$ cat /proc/meminfo
|
||||
...
|
||||
Buffers: 1224 kB
|
||||
Cached: 111472 kB
|
||||
SwapCached: 36364 kB
|
||||
Active: 6224232 kB
|
||||
Inactive: 979432 kB
|
||||
Active(anon): 6173036 kB
|
||||
Inactive(anon): 927932 kB
|
||||
Active(file): 51196 kB
|
||||
Inactive(file): 51500 kB
|
||||
...
|
||||
Shmem: 10000 kB
|
||||
...
|
||||
SReclaimable: 43532 kB
|
||||
...
|
||||
|
||||
|
||||
根据上面的数据,你可以简单得出这样的公式(等式两边之和都是112696 KB):
|
||||
|
||||
|
||||
Buffers + Cached + SwapCached = Active(file) + Inactive(file) + Shmem + SwapCached
|
||||
|
||||
|
||||
那么等式两边的内容就是我们平时说的Page Cache。请注意你没有看错,两边都有SwapCached,之所以要把它放在等式里,就是说它也是Page Cache的一部分。
|
||||
|
||||
接下来,我带你分析一下这些项的具体含义。等式右边这些项把Buffers和Cached做了一下细分,分为了Active(file),Inactive(file) 和Shmem,因为Buffers更加依赖于内核实现,在不同内核版本中它的含义可能有些不一致,而等式右边和应用程序的关系更加直接,所以我们从等式右边来分析。
|
||||
|
||||
在Page Cache中,Active(file)+Inactive(file)是File-backed page(与文件对应的内存页),是你最需要关注的部分。因为你平时用的mmap()内存映射方式和buffered I/O来消耗的内存就属于这部分,最重要的是,这部分在真实的生产环境上也最容易产生问题,我们在接下来的课程案例篇会重点分析它。
|
||||
|
||||
而SwapCached是在打开了Swap分区后,把Inactive(anon)+Active(anon)这两项里的匿名页给交换到磁盘(swap out),然后再读入到内存(swap in)后分配的内存。由于读入到内存后原来的Swap File还在,所以SwapCached也可以认为是File-backed page,即属于Page Cache。这样做的目的也是为了减少I/O。你是不是觉得这个过程有些复杂?我们用一张图直观地看一下:
|
||||
|
||||
|
||||
|
||||
我希望你能通过这个简单的示意图明白SwapCached是怎么产生的。在这个过程中你要注意,SwapCached只在Swap分区打开的情况下才会有,而我建议你在生产环境中关闭Swap分区,因为Swap过程产生的I/O会很容易引起性能抖动。
|
||||
|
||||
除了SwapCached,Page Cache中的Shmem是指匿名共享映射这种方式分配的内存(free命令中shared这一项),比如tmpfs(临时文件系统),这部分在真实的生产环境中产生的问题比较少,不是我们今天的重点内容,我们这节课不对它做过多关注,你知道有这回事就可以了。
|
||||
|
||||
当然了,很多同学也喜欢用free命令来查看系统中有多少Page Cache,会根据buff/cache来判断存在多少Page Cache。如果你对free命令有所了解的话,肯定知道free命令也是通过解析/proc/meminfo得出这些统计数据的,这些都可以通过free工具的源码来找到。free命令的源码是开源,你可以去看下procfs里的free.c文件,源码是最直接的理解方式,它会加深你对free命令的理解。
|
||||
|
||||
不过你是否好奇过,free命令中的buff/cache究竟是指什么呢?我们在这里先简单地看一下:
|
||||
|
||||
$ free -k
|
||||
total used free shared buff/cache available
|
||||
Mem: 7926580 7277960 492392 10000 156228 430680
|
||||
Swap: 8224764 380748 7844016
|
||||
|
||||
|
||||
通过procfs源码里面的proc/sysinfo.c这个文件,你可以发现buff/cache包括下面这几项:
|
||||
|
||||
|
||||
buff/cache = Buffers + Cached + SReclaimable
|
||||
|
||||
|
||||
通过前面的数据我们也可以验证这个公式: 1224 + 111472 + 43532的和是156228。
|
||||
|
||||
另外,这里你要注意,你在做比较的过程中,一定要考虑到这些数据是动态变化的,而且执行命令本身也会带来内存开销,所以这个等式未必会严格相等,不过你不必怀疑它的正确性。
|
||||
|
||||
从这个公式中,你能看到free命令中的buff/cache是由Buffers、Cached和SReclaimable这三项组成的,它强调的是内存的可回收性,也就是说,可以被回收的内存会统计在这一项。
|
||||
|
||||
其中SReclaimable是指可以被回收的内核内存,包括dentry和inode等。而这部分内容是内核非常细节性的东西,对于应用开发者和运维人员理解起来相对有些难度,所以我们在这里不多说。
|
||||
|
||||
掌握了Page Cache具体由哪些部分构成之后,在它引发一些问题时,你就能够知道需要去观察什么。比如说,应用本身消耗内存(RSS)不多的情况下,整个系统的内存使用率还是很高,那不妨去排查下是不是Shmem(共享内存)消耗了太多内存导致的。
|
||||
|
||||
讲到这儿,我想你应该对Page Cache有了一些直观的认识了吧?当然了,有的人可能会说,内核的Page Cache这么复杂,我不要不可以么?
|
||||
|
||||
我相信有这样想法的人不在少数,如果不用内核管理的Page Cache,那有两种思路来进行处理:
|
||||
|
||||
|
||||
第一种,应用程序维护自己的Cache做更加细粒度的控制,比如MySQL就是这样做的,你可以参考MySQL Buffer Pool ,它的实现复杂度还是很高的。对于大多数应用而言,实现自己的Cache成本还是挺高的,不如内核的Page Cache来得简单高效。
|
||||
第二种,直接使用Direct I/O来绕过Page Cache,不使用Cache了,省的去管它了。这种方法可行么?那我们继续用数据说话,看看这种做法的问题在哪儿?
|
||||
|
||||
|
||||
为什么需要Page Cache?
|
||||
|
||||
通过第一张图你其实已经可以直观地看到,标准I/O和内存映射会先把数据写入到Page Cache,这样做会通过减少I/O次数来提升读写效率。我们看一个具体的例子。首先,我们来生成一个1G大小的新文件,然后把Page Cache清空,确保文件内容不在内存中,以此来比较第一次读文件和第二次读文件耗时的差异。具体的流程如下。
|
||||
|
||||
先生成一个1G的文件:
|
||||
|
||||
|
||||
\( dd if=/dev/zero of=/home/yafang/test/dd.out bs=4096 count=\)((1024*256))
|
||||
|
||||
|
||||
其次,清空Page Cache,需要先执行一下sync来将脏页(第二节课,我会解释一下什么是脏页)同步到磁盘再去drop cache。
|
||||
|
||||
|
||||
$ sync && echo 3 > /proc/sys/vm/drop_caches
|
||||
|
||||
|
||||
第一次读取文件的耗时如下:
|
||||
|
||||
$ time cat /home/yafang/test/dd.out &> /dev/null
|
||||
real 0m5.733s
|
||||
user 0m0.003s
|
||||
sys 0m0.213s
|
||||
|
||||
|
||||
再次读取文件的耗时如下:
|
||||
|
||||
$ time cat /home/yafang/test/dd.out &> /dev/null
|
||||
real 0m0.132s
|
||||
user 0m0.001s
|
||||
sys 0m0.130s
|
||||
|
||||
|
||||
通过这样详细的过程你可以看到,第二次读取文件的耗时远小于第一次的耗时,这是因为第一次是从磁盘来读取的内容,磁盘I/O是比较耗时的,而第二次读取的时候由于文件内容已经在第一次读取时被读到内存了,所以是直接从内存读取的数据,内存相比磁盘速度是快很多的。这就是Page Cache存在的意义:减少I/O,提升应用的I/O速度。
|
||||
|
||||
所以,如果你不想为了很细致地管理内存而增加应用程序的复杂度,那你还是乖乖使用内核管理的Page Cache吧,它是ROI(投入产出比)相对较高的一个方案。
|
||||
|
||||
你要知道,我们在做方案抉择时找到一个各方面都很完美的方案还是比较难的,大多数情况下都是经过权衡后来选择一个合适的方案。因为,我一直坚信,合适的就是最好的。
|
||||
|
||||
而我之所以说Page Cache是合适的,而不是说它是最好的,那是因为Page Cache的不足之处也是有的,这个不足之处主要体现在,它对应用程序太过于透明,以至于应用程序很难有好方法来控制它。
|
||||
|
||||
为什么这么说呢?要想知道这个答案,你就需要了解Page Cache的产生过程,这里卖个关子,我在下一讲会跟你讨论。
|
||||
|
||||
课堂总结
|
||||
|
||||
我们这节课主要是讲述了如何很好地理解Page Cache,在我看来,要想很好的理解它,直观的方式就是从数据入手,所以,我从如何观测Page Cache出发来带你认识什么是Page Cache;然后再从它为什么容易产生问题出发,带你回顾了它存在的意义,我希望通过这样的方式,帮你明确这样几个要点:
|
||||
|
||||
|
||||
Page Cache是属于内核的,不属于用户。
|
||||
Page Cache对应用提升I/O效率而言是一个投入产出比较高的方案,所以它的存在还是有必要的。
|
||||
|
||||
|
||||
在我看来,如何管理好Page Cache,最主要的是你要知道如何来观测它以及观测关于它的一些行为,有了这些数据做支撑,你才能够把它和你的业务更好地结合起来。而且,在我看来,当你对某一个概念很模糊、搞不清楚它到底是什么时,最好的认知方式就是先搞明白如何来观测它,然后动手去观测看看它究竟是如何变化的,正所谓纸上得来终觉浅,绝知此事要躬行!
|
||||
|
||||
这节课就讲到这里,下一节我们使用数据来观察Page Cache的产生和释放,这样一来,你就能了解Page Cache的整个生命周期,从而对于它引发的一些问题能有一个大概的判断。
|
||||
|
||||
课后作业
|
||||
|
||||
最后我给你留一道思考题,请你写一个程序来构造出来Page Cache,然后观察/proc/meminfo和/proc/vmstat里面的数据是如何变化的, 欢迎在留言区分享你的看法。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
204
专栏/Linux内核技术实战课/02基础篇PageCache是怎样产生和释放的?.md
Normal file
204
专栏/Linux内核技术实战课/02基础篇PageCache是怎样产生和释放的?.md
Normal file
@ -0,0 +1,204 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
02 基础篇 Page Cache是怎样产生和释放的?
|
||||
你好,我是邵亚方。
|
||||
|
||||
上一讲,我们主要讲了“什么是Page Cache”(What),“为什么需要Page Cache”(Why),我们这堂课还需要继续了解一下“How”:也就是Page Cache是如何产生和释放的。
|
||||
|
||||
在我看来,对Page Cache的“What-Why-How”都有所了解之后,你才会对它引发的问题,比如说Page Cache引起的load飙高问题或者应用程序的RT抖动问题更加了然于胸,从而防范于未然。
|
||||
|
||||
其实,Page Cache是如何产生和释放的,通俗一点的说就是它的“生”(分配)与“死”(释放),即Page Cache的生命周期,那么接下来,我们就先来看一下它是如何“诞生”的。
|
||||
|
||||
Page Cache是如何“诞生”的?
|
||||
|
||||
Page Cache的产生有两种不同的方式:
|
||||
|
||||
|
||||
Buffered I/O(标准I/O);
|
||||
Memory-Mapped I/O(存储映射I/O)。
|
||||
|
||||
|
||||
这两种方式分别都是如何产生Page Cache的呢?来看下面这张图:
|
||||
|
||||
|
||||
|
||||
从图中你可以看到,虽然二者都能产生Page Cache,但是二者的还是有些差异的:
|
||||
|
||||
标准I/O是写的(write(2))用户缓冲区(Userpace Page对应的内存),然后再将用户缓冲区里的数据拷贝到内核缓冲区(Pagecache Page对应的内存);如果是读的(read(2))话则是先从内核缓冲区拷贝到用户缓冲区,再从用户缓冲区读数据,也就是buffer和文件内容不存在任何映射关系。
|
||||
|
||||
对于存储映射I/O而言,则是直接将Pagecache Page给映射到用户地址空间,用户直接读写Pagecache Page中内容。
|
||||
|
||||
显然,存储映射I/O要比标准I/O效率高一些,毕竟少了“用户空间到内核空间互相拷贝”的过程。这也是很多应用开发者发现,为什么使用内存映射I/O比标准I/O方式性能要好一些的主要原因。
|
||||
|
||||
我们来用具体的例子演示一下Page Cache是如何“诞生”的,就以其中的标准I/O为例,因为这是我们最常使用的一种方式,如下是一个简单的示例脚本:
|
||||
|
||||
#!/bin/sh
|
||||
|
||||
#这是我们用来解析的文件
|
||||
MEM_FILE="/proc/meminfo"
|
||||
|
||||
#这是在该脚本中将要生成的一个新文件
|
||||
NEW_FILE="/home/yafang/dd.write.out"
|
||||
|
||||
#我们用来解析的Page Cache的具体项
|
||||
active=0
|
||||
inactive=0
|
||||
pagecache=0
|
||||
|
||||
IFS=' '
|
||||
|
||||
#从/proc/meminfo中读取File Page Cache的大小
|
||||
function get_filecache_size()
|
||||
{
|
||||
items=0
|
||||
while read line
|
||||
do
|
||||
if [[ "$line" =~ "Active:" ]]; then
|
||||
read -ra ADDR <<<"$line"
|
||||
active=${ADDR[1]}
|
||||
let "items=$items+1"
|
||||
elif [[ "$line" =~ "Inactive:" ]]; then
|
||||
read -ra ADDR <<<"$line"
|
||||
inactive=${ADDR[1]}
|
||||
let "items=$items+1"
|
||||
fi
|
||||
|
||||
|
||||
if [ $items -eq 2 ]; then
|
||||
break;
|
||||
fi
|
||||
done < $MEM_FILE
|
||||
}
|
||||
|
||||
#读取File Page Cache的初始大小
|
||||
get_filecache_size
|
||||
let filecache="$active + $inactive"
|
||||
|
||||
#写一个新文件,该文件的大小为1048576 KB
|
||||
dd if=/dev/zero of=$NEW_FILE bs=1024 count=1048576 &> /dev/null
|
||||
|
||||
#文件写完后,再次读取File Page Cache的大小
|
||||
get_filecache_size
|
||||
|
||||
#两次的差异可以近似为该新文件内容对应的File Page Cache
|
||||
#之所以用近似是因为在运行的过程中也可能会有其他Page Cache产生
|
||||
let size_increased="$active + $inactive - $filecache"
|
||||
|
||||
#输出结果
|
||||
echo "File size 1048576KB, File Cache increased" $size_inc
|
||||
|
||||
|
||||
在这里我提醒你一下,在运行该脚本前你要确保系统中有足够多的free内存(避免内存紧张产生回收行为),最终的测试结果是这样的:
|
||||
|
||||
|
||||
File size 1048576KB, File Cache increased 1048648KB
|
||||
|
||||
|
||||
通过这个脚本你可以看到,在创建一个文件的过程中,代码中/proc/meminfo里的Active(file)和Inactive(file)这两项会随着文件内容的增加而增加,它们增加的大小跟文件大小是一致的(这里之所以略有不同,是因为系统中还有其他程序在运行)。另外,如果你观察得很仔细的话,你会发现增加的Page Cache是Inactive(File)这一项,你可以去思考一下为什么会这样?这里就作为咱们这节课的思考题。
|
||||
|
||||
当然,这个过程看似简单,但是它涉及的内核机制还是很多的,换句话说,可能引起问题的地方还是很多的,我们用一张图简单描述下这个过程:
|
||||
|
||||
|
||||
|
||||
这个过程大致可以描述为:首先往用户缓冲区buffer(这是Userspace Page)写入数据,然后buffer中的数据拷贝到内核缓冲区(这是Pagecache Page),如果内核缓冲区中还没有这个Page,就会发生Page Fault会去分配一个Page,拷贝结束后该Pagecache Page是一个Dirty Page(脏页),然后该Dirty Page中的内容会同步到磁盘,同步到磁盘后,该Pagecache Page变为Clean Page并且继续存在系统中。
|
||||
|
||||
我建议你可以将Alloc Page理解为Page Cache的“诞生”,将Dirty Page理解为Page Cache的婴幼儿时期(最容易生病的时期),将Clean Page理解为Page Cache的成年时期(在这个时期就很少会生病了)。
|
||||
|
||||
但是请注意,并不是所有人都有童年的,比如孙悟空,一出生就是成人了,Page Cache也一样,如果是读文件产生的Page Cache,它的内容跟磁盘内容是一致的,所以它一开始是Clean Page,除非改写了里面的内容才会变成Dirty Page(返老还童)。
|
||||
|
||||
就像我们为了让婴幼儿健康成长,要悉心照料他/她一样,为了提前发现或者预防婴幼儿时期的Page Cache发病,我们也需要一些手段来观测它:
|
||||
|
||||
$ cat /proc/vmstat | egrep "dirty|writeback"
|
||||
nr_dirty 40
|
||||
nr_writeback 2
|
||||
|
||||
|
||||
如上所示,nr_dirty表示当前系统中积压了多少脏页,nr_writeback则表示有多少脏页正在回写到磁盘中,他们两个的单位都是Page(4KB)。
|
||||
|
||||
通常情况下,小朋友们(Dirty Pages)聚集在一起(脏页积压)不会有什么问题,但在非常时期比如流感期间,就很容易导致聚集的小朋友越多病症就会越严重。与此类似,Dirty Pages如果积压得过多,在某些情况下也会容易引发问题,至于是哪些情况,又会出现哪些问题,我们会在案例篇中具体讲解。
|
||||
|
||||
明白了Page Cache的“诞生”后,我们再来看一下Page Cache的“死亡”:它是如何被释放的?
|
||||
|
||||
Page Cache是如何“死亡”的?
|
||||
|
||||
你可以把Page Cache的回收行为(Page Reclaim)理解为Page Cache的“自然死亡”。
|
||||
|
||||
言归正传,我们知道,服务器运行久了后,系统中free的内存会越来越少,用free命令来查看,大部分都会是used内存或者buff/cache内存,比如说下面这台生产环境中服务器的内存使用情况:
|
||||
|
||||
$ free -g
|
||||
total used free shared buff/cache available
|
||||
Mem: 125 41 6 0 79 82
|
||||
Swap: 0 0 0
|
||||
|
||||
|
||||
free命令中的buff/cache中的这些就是“活着”的Page Cache,那它们什么时候会“死亡”(被回收)呢?我们来看一张图:
|
||||
|
||||
|
||||
|
||||
你可以看到,应用在申请内存的时候,即使没有free内存,只要还有足够可回收的Page Cache,就可以通过回收Page Cache的方式来申请到内存,回收的方式主要是两种:直接回收和后台回收。
|
||||
|
||||
那它是具体怎么回收的呢?你要怎么观察呢?其实在我看来,观察Page Cache直接回收和后台回收最简单方便的方式是使用sar:
|
||||
|
||||
$ sar -B 1
|
||||
02:14:01 PM pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff
|
||||
|
||||
|
||||
02:14:01 PM 0.14 841.53 106745.40 0.00 41936.13 0.00 0.00 0.00 0.00
|
||||
02:15:01 PM 5.84 840.97 86713.56 0.00 43612.15 717.81 0.00 717.66 99.98
|
||||
02:16:01 PM 95.02 816.53 100707.84 0.13 46525.81 3557.90 0.00 3556.14 99.95
|
||||
02:17:01 PM 10.56 901.38 122726.31 0.27 54936.13 8791.40 0.00 8790.17 99.99
|
||||
02:18:01 PM 108.14 306.69 96519.75 1.15 67410.50 14315.98 31.48 14319.38 99.80
|
||||
02:19:01 PM 5.97 489.67 88026.03 0.18 48526.07 1061.53 0.00 1061.42 99.99
|
||||
|
||||
|
||||
借助上面这些指标,你可以更加明确地观察内存回收行为,下面是这些指标的具体含义:
|
||||
|
||||
|
||||
pgscank/s : kswapd(后台回收线程)每秒扫描的page个数。
|
||||
pgscand/s: Application在内存申请过程中每秒直接扫描的page个数。
|
||||
pgsteal/s: 扫描的page中每秒被回收的个数。
|
||||
%vmeff: pgsteal/(pgscank+pgscand), 回收效率,越接近100说明系统越安全,越接近0说明系统内存压力越大。
|
||||
|
||||
|
||||
这几个指标也是通过解析/proc/vmstat里面的数据来得出的,对应关系如下:
|
||||
|
||||
|
||||
|
||||
关于这几个指标我说一个小插曲,要知道,如果Linux Kernel本身设计不当会给你带来困扰。所以,如果你观察到应用程序的结果跟你的预期并不一致,也有可能是因为内核设计上存在问题,你可以对内核持适当的怀疑态度哦,下面这个是我最近遇到的一个案例。
|
||||
|
||||
如果你对Linus有所了解的话,应该会知道Linus对Linux Kernel设计的第一原则是“never break the user space”。很多内核开发者在设计内核特性的时候,会忽略掉新特性对应用程序的影响,比如在前段时间就有人(Google的一个内核开发者)提交了一个patch来修改内存回收这些指标的含义,但是最终被我和另外一个人(Facebook的一个内核开发者)把他的这个改动给否决掉了。具体的细节并不是咱们这节课的重点,我就不多说了,我建议你在课下看这个讨论:[PATCH] mm: vmscan: consistent update to pgsteal and pgscan,可以看一下内核开发者们在设计内核特性时是如何思考的,这会有利于你更加全面的去了解整个系统,从而让你的应用程序更好地融入到系统中。
|
||||
|
||||
课堂总结
|
||||
|
||||
以上就是本节课的全部内容了,本节课,我们主要讲了Page Cache是如何“诞生”的,以及如何“死亡”的,我要强调这样几个重点:
|
||||
|
||||
|
||||
Page Cache是在应用程序读写文件的过程中产生的,所以在读写文件之前你需要留意是否还有足够的内存来分配Page Cache;
|
||||
Page Cache中的脏页很容易引起问题,你要重点注意这一块;
|
||||
在系统可用内存不足的时候就会回收Page Cache来释放出来内存,我建议你可以通过sar或者/proc/vmstat来观察这个行为从而更好的判断问题是否跟回收有关
|
||||
|
||||
|
||||
总的来说,Page Cache的生命周期对于应用程序而言是相对比较透明的,即它的分配与回收都是由操作系统来进行管理的。正是因为这种“透明”的特征,所以应用程序才会难以控制Page Cache,Page Cache才会容易引发那么多问题。在接下来的案例篇里,我们就来看看究竟会引发什么样子的问题,以及你正确的分析思路是什么样子的。
|
||||
|
||||
课后作业
|
||||
|
||||
因为每个人的关注点都不一样,对问题的理解也不一样。假如你是一个应用开发者,你会更加关注应用的性能和稳定性;假如你是一个运维人员,你会更加关注系统的稳定性;假如你是初学内核的开发者,你会想要关注内核的实现机制。
|
||||
|
||||
所以我留了不同的作业题,主题是围绕“Inactive与Active Page Cache的关系”当然了,对应的难度也不同:
|
||||
|
||||
|
||||
如果你是一名应用开发者,那么我想问问你为什么第一次读写某个文件,Page Cache是Inactive的?如何让它变成Active的呢?在什么情况下Active的又会变成Inactive的呢?明白了这个问题,你会对应用性能调优有更加深入的理解。
|
||||
|
||||
如果你是一名运维人员,那么建议你思考一下,系统中有哪些控制项可以影响Inactive与Active Page Cache的大小或者二者的比例?
|
||||
|
||||
如果你是一名初学内核的开发者,那么我想问你,对于匿名页而言,当产生一个匿名页后它会首先放在Active链表上;而对于文件页而言,当产生一个文件页后它会首先放在Inactive链表上。请问为什么会这样子?这是合理的吗?欢迎在留言区分享你的看法。
|
||||
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
190
专栏/Linux内核技术实战课/03案例篇如何处理PageCache难以回收产生的load飙高问题?.md
Normal file
190
专栏/Linux内核技术实战课/03案例篇如何处理PageCache难以回收产生的load飙高问题?.md
Normal file
@ -0,0 +1,190 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
03 案例篇 如何处理Page Cache难以回收产生的load飙高问题?
|
||||
你好,我是邵亚方。今天这节课,我想跟你聊一聊怎么处理在生产环境中,因为Page Cache管理不当引起的系统load飙高的问题。
|
||||
|
||||
相信你在平时的工作中,应该会或多或少遇到过这些情形:系统很卡顿,敲命令响应非常慢;应用程序的RT变得很高,或者抖动得很厉害。在发生这些问题时,很有可能也伴随着系统load飙得很高。
|
||||
|
||||
那这是什么原因导致的呢?据我观察,大多是有三种情况:
|
||||
|
||||
|
||||
直接内存回收引起的load飙高;
|
||||
系统中脏页积压过多引起的load飙高;
|
||||
系统NUMA策略配置不当引起的load飙高。
|
||||
|
||||
|
||||
这是应用开发者和运维人员向我咨询最多的几种情况。问题看似很简单,但如果对问题产生的原因理解得不深,解决起来就会很棘手,甚至配置得不好,还会带来负面的影响。
|
||||
|
||||
所以这节课,我们一起来分析下这三种情况,可以说,搞清楚了这几种情况,你差不多就能解决掉绝大部分Page Cache引起的load飙高问题了。如果你对问题的原因排查感兴趣,也不要着急,在第5讲,我会带你学习load飙高问题的分析方法。
|
||||
|
||||
接下来,我们就来逐一分析下这几类情况。
|
||||
|
||||
直接内存回收引起load飙高或者业务时延抖动
|
||||
|
||||
直接内存回收是指在进程上下文同步进行内存回收,那么它具体是怎么引起load飙高的呢?
|
||||
|
||||
因为直接内存回收是在进程申请内存的过程中同步进行的回收,而这个回收过程可能会消耗很多时间,进而导致进程的后续行为都被迫等待,这样就会造成很长时间的延迟,以及系统的CPU利用率会升高,最终引起load飙高。
|
||||
|
||||
我们详细地描述一下这个过程,为了尽量不涉及太多技术细节,我会用一张图来表示,这样你理解起来会更容易。
|
||||
|
||||
|
||||
|
||||
从图里你可以看到,在开始内存回收后,首先进行后台异步回收(上图中蓝色标记的地方),这不会引起进程的延迟;如果后台异步回收跟不上进程内存申请的速度,就会开始同步阻塞回收,导致延迟(上图中红色和粉色标记的地方,这就是引起load高的地址)。
|
||||
|
||||
那么,针对直接内存回收引起load飙高或者业务RT抖动的问题,一个解决方案就是及早地触发后台回收来避免应用程序进行直接内存回收,那具体要怎么做呢?
|
||||
|
||||
我们先来了解一下后台回收的原理,如图:
|
||||
|
||||
|
||||
|
||||
它的意思是:当内存水位低于watermark low时,就会唤醒kswapd进行后台回收,然后kswapd会一直回收到watermark high。
|
||||
|
||||
那么,我们可以增大min_free_kbytes这个配置选项来及早地触发后台回收,该选项最终控制的是内存回收水位,不过,内存回收水位是内核里面非常细节性的知识点,我们可以先不去讨论。
|
||||
|
||||
|
||||
vm.min_free_kbytes = 4194304
|
||||
|
||||
|
||||
对于大于等于128G的系统而言,将min_free_kbytes设置为4G比较合理,这是我们在处理很多这种问题时总结出来的一个经验值,既不造成较多的内存浪费,又能避免掉绝大多数的直接内存回收。
|
||||
|
||||
该值的设置和总的物理内存并没有一个严格对应的关系,我们在前面也说过,如果配置不当会引起一些副作用,所以在调整该值之前,我的建议是:你可以渐进式地增大该值,比如先调整为1G,观察sar -B中pgscand是否还有不为0的情况;如果存在不为0的情况,继续增加到2G,再次观察是否还有不为0的情况来决定是否增大,以此类推。
|
||||
|
||||
在这里你需要注意的是,即使将该值增加得很大,还是可能存在pgscand不为0的情况(这个略复杂,涉及到内存碎片和连续内存申请,我们在此先不展开,你知道有这么回事儿就可以了)。那么这个时候你要考虑的是,业务是否可以容忍,如果可以容忍那就没有必要继续增加了,也就是说,增大该值并不是完全避免直接内存回收,而是尽量将直接内存回收行为控制在业务可以容忍的范围内。
|
||||
|
||||
这个方法可以用在3.10.0以后的内核上(对应的操作系统为CentOS-7以及之后更新的操作系统)。
|
||||
|
||||
当然了,这样做也有一些缺陷:提高了内存水位后,应用程序可以直接使用的内存量就会减少,这在一定程度上浪费了内存。所以在调整这一项之前,你需要先思考一下,应用程序更加关注什么,如果关注延迟那就适当地增大该值,如果关注内存的使用量那就适当地调小该值。
|
||||
|
||||
除此之外,对CentOS-6(对应于2.6.32内核版本)而言,还有另外一种解决方案:
|
||||
|
||||
|
||||
vm.extra_free_kbytes = 4194304
|
||||
|
||||
|
||||
那就是将extra_free_kbytes 配置为4G。extra_free_kbytes在3.10以及以后的内核上都被废弃掉了,不过由于在生产环境中还存在大量的机器运行着较老版本内核,你使用到的也可能会是较老版本的内核,所以在这里还是有必要提一下。它的大致原理如下所示:
|
||||
|
||||
|
||||
|
||||
extra_free_kbytes的目的是为了解决min_free_kbyte造成的内存浪费,但是这种做法并没有被内核主线接收,因为这种行为很难维护会带来一些麻烦,感兴趣的可以看一下这个讨论:add extra free kbytes tunable
|
||||
|
||||
总的来说,通过调整内存水位,在一定程度上保障了应用的内存申请,但是同时也带来了一定的内存浪费,因为系统始终要保障有这么多的free内存,这就压缩了Page Cache的空间。调整的效果你可以通过/proc/zoneinfo来观察:
|
||||
|
||||
$ egrep "min|low|high" /proc/zoneinfo
|
||||
...
|
||||
min 7019
|
||||
low 8773
|
||||
high 10527
|
||||
...
|
||||
|
||||
|
||||
其中min、low、high分别对应上图中的三个内存水位。你可以观察一下调整前后min、low、high的变化。需要提醒你的是,内存水位是针对每个内存zone进行设置的,所以/proc/zoneinfo里面会有很多zone以及它们的内存水位,你可以不用去关注这些细节。
|
||||
|
||||
系统中脏页过多引起load飙高
|
||||
|
||||
接下来,我们分析下由于系统脏页过多引起load飙高的情况。在前一个案例中我们也提到,直接回收过程中,如果存在较多脏页就可能涉及在回收过程中进行回写,这可能会造成非常大的延迟,而且因为这个过程本身是阻塞式的,所以又可能进一步导致系统中处于D状态的进程数增多,最终的表现就是系统的load值很高。
|
||||
|
||||
我们来看一下这张图,这是一个典型的脏页引起系统load值飙高的问题场景:
|
||||
|
||||
|
||||
|
||||
如图所示,如果系统中既有快速I/O设备,又有慢速I/O设备(比如图中的ceph RBD设备,或者其他慢速存储设备比如HDD),直接内存回收过程中遇到了正在往慢速I/O设备回写的page,就可能导致非常大的延迟。
|
||||
|
||||
这里我多说一点。这类问题其实是不太好去追踪的,为了更好追踪这种慢速I/O设备引起的抖动问题,我也给Linux Kernel提交了一个patch来进行更好的追踪:mm/page-writeback: introduce tracepoint for wait_on_page_writeback(),这种做法是在原来的基础上增加了回写的设备,这样子用户就能更好地将回写和具体设备关联起来,从而判断问题是否是由慢速I/O设备导致的(具体的分析方法我会在后面第5讲分析篇里重点来讲)。
|
||||
|
||||
那如何解决这类问题呢?一个比较省事的解决方案是控制好系统中积压的脏页数据。很多人知道需要控制脏页,但是往往并不清楚如何来控制好这个度,脏页控制的少了可能会影响系统整体的效率,脏页控制的多了还是会触发问题,所以我们接下来看下如何来衡量好这个“度”。
|
||||
|
||||
首先你可以通过sar -r来观察系统中的脏页个数:
|
||||
|
||||
$ sar -r 1
|
||||
07:30:01 PM kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
|
||||
09:20:01 PM 5681588 2137312 27.34 0 1807432 193016 2.47 534416 1310876 4
|
||||
09:30:01 PM 5677564 2141336 27.39 0 1807500 204084 2.61 539192 1310884 20
|
||||
09:40:01 PM 5679516 2139384 27.36 0 1807508 196696 2.52 536528 1310888 20
|
||||
09:50:01 PM 5679548 2139352 27.36 0 1807516 196624 2.51 536152 1310892 24
|
||||
|
||||
|
||||
kbdirty就是系统中的脏页大小,它同样也是对/proc/vmstat中nr_dirty的解析。你可以通过调小如下设置来将系统脏页个数控制在一个合理范围:
|
||||
|
||||
|
||||
vm.dirty_background_bytes = 0-
|
||||
vm.dirty_background_ratio = 10-
|
||||
vm.dirty_bytes = 0-
|
||||
vm.dirty_expire_centisecs = 3000-
|
||||
vm.dirty_ratio = 20
|
||||
|
||||
|
||||
调整这些配置项有利有弊,调大这些值会导致脏页的积压,但是同时也可能减少了I/O的次数,从而提升单次刷盘的效率;调小这些值可以减少脏页的积压,但是同时也增加了I/O的次数,降低了I/O的效率。
|
||||
|
||||
至于这些值调整大多少比较合适,也是因系统和业务的不同而异,我的建议也是一边调整一边观察,将这些值调整到业务可以容忍的程度就可以了,即在调整后需要观察业务的服务质量(SLA),要确保SLA在可接受范围内。调整的效果你可以通过/proc/vmstat来查看:
|
||||
|
||||
$ grep "nr_dirty_" /proc/vmstat
|
||||
nr_dirty_threshold 366998
|
||||
nr_dirty_background_threshold 183275
|
||||
|
||||
|
||||
你可以观察一下调整前后这两项的变化。这里我要给你一个避免踩坑的提示,解决该方案中的设置项如果设置不妥会触发一个内核Bug,这是我在2017年进行性能调优时发现的一个内核Bug,我给社区提交了一个patch将它fix掉了,具体的commit见writeback: schedule periodic writeback with sysctl , commit log清晰地描述了该问题,我建议你有时间看一看。
|
||||
|
||||
系统NUMA策略配置不当引起的load飙高
|
||||
|
||||
除了我前面提到的这两种引起系统load飙高或者业务延迟抖动的场景之外,还有另外一种场景也会引起load飙高,那就是系统NUMA策略配置不当引起的load飙高。
|
||||
|
||||
比如说,我们在生产环境上就曾经遇到这样的问题:系统中还有一半左右的free内存,但还是频频触发direct reclaim,导致业务抖动得比较厉害。后来经过排查发现是由于设置了zone_reclaim_mode,这是NUMA策略的一种。
|
||||
|
||||
设置zone_reclaim_mode的目的是为了增加业务的NUMA亲和性,但是在实际生产环境中很少会有对NUMA特别敏感的业务,这也是为什么内核将该配置从默认配置1修改为了默认配置0: mm: disable zone_reclaim_mode by default ,配置为0之后,就避免了在其他node有空闲内存时,不去使用这些空闲内存而是去回收当前node的Page Cache,也就是说,通过减少内存回收发生的可能性从而避免它引发的业务延迟。
|
||||
|
||||
那么如何来有效地衡量业务延迟问题是否由zone reclaim引起的呢?它引起的延迟究竟有多大呢?这个衡量和观察方法也是我贡献给Linux Kernel的:mm/vmscan: add tracepoints for node reclaim ,大致的思路就是利用linux的tracepoint来做这种量化分析,这是性能开销相对较小的一个方案。
|
||||
|
||||
我们可以通过numactl来查看服务器的NUMA信息,如下是两个node的服务器:
|
||||
|
||||
$ numactl --hardware
|
||||
available: 2 nodes (0-1)
|
||||
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 24 25 26 27 28 29 30 31 32 33 34 35
|
||||
node 0 size: 130950 MB
|
||||
node 0 free: 108256 MB
|
||||
node 1 cpus: 12 13 14 15 16 17 18 19 20 21 22 23 36 37 38 39 40 41 42 43 44 45 46 47
|
||||
node 1 size: 131072 MB
|
||||
node 1 free: 122995 MB
|
||||
node distances:
|
||||
node 0 1
|
||||
0: 10 21
|
||||
1: 21 10
|
||||
|
||||
|
||||
其中CPU0~11,24~35的local node为node 0;而CPU12~23,36~47的local node为node 1。如下图所示:
|
||||
|
||||
|
||||
|
||||
推荐将zone_reclaim_mode配置为0。
|
||||
|
||||
|
||||
vm.zone_reclaim_mode = 0
|
||||
|
||||
|
||||
因为相比内存回收的危害而言,NUMA带来的性能提升几乎可以忽略,所以配置为0,利远大于弊。
|
||||
|
||||
好了,对于Page Cache管理不当引起的系统load飙高和业务时延抖动问题,我们就分析到这里,希望通过这篇的学习,在下次你遇到直接内存回收引起的load飙高问题时不再束手无策。
|
||||
|
||||
总的来说,这些问题都是Page Cache难以释放而产生的问题,那你是否想过,是不是Page Cache很容易释放就不会产生问题了?这个答案可能会让你有些意料不到:Page Cache容易释放也有容易释放的问题。这到底是怎么回事呢,我们下节课来分析下这方面的案例。
|
||||
|
||||
课堂总结
|
||||
|
||||
这节课我们讲的这几个案例都是内存回收过程中引起的load飙高问题。关于内存回收这事,我们可以做一个形象的类比。我们知道,内存是操作系统中很重要的一个资源,它就像我们在生活过程中很重要的一个资源——钱一样,如果你的钱(内存)足够多,那想买什么就可以买什么,而不用担心钱花完(内存用完)后要吃土(引起load飙高)。
|
||||
|
||||
但是现实情况是我们每个人用来双十一购物的钱(内存)总是有限的,在买东西(运行程序)的时候总需要精打细算,一旦预算快超了(内存快不够了),就得把一些不重要的东西(把一些不活跃的内容)从购物车里删除掉(回收掉),好腾出资金(空闲的内存)来买更想买的东西(运行需要运行的程序)。
|
||||
|
||||
我们讲的这几个案例都可以通过调整系统参数/配置来解决,调整系统参数/配置也是应用开发者和运维人员在发生了内核问题时所能做的改动。比如说,直接内存回收引起load飙高时,就去调整内存水位设置;脏页积压引起load飙高时,就需要去调整脏页的水位;NUMA策略配置不当引起load飙高时,就去检查是否需要关闭该策略。同时我们在做这些调整的时候,一定要边调整边观察业务的服务质量,确保SLA是可以接受的。
|
||||
|
||||
如果你想要你的系统更加稳定,你的业务性能更好,你不妨去研究一下系统中的可配置项,看看哪些配置可以帮助你的业务。
|
||||
|
||||
课后作业
|
||||
|
||||
这节课我给你布置的作业是针对直接内存回收的,现在你已经知道直接内存回收容易产生问题,是我们需要尽量避免的,那么我的问题是:请你执行一些模拟程序来构造出直接内存回收的场景(小提示: 你可以通过sar -B中的pgscand来判断是否有了直接内存回收)。欢迎在留言区分享你的看法。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
210
专栏/Linux内核技术实战课/04案例篇如何处理PageCache容易回收引起的业务性能问题?.md
Normal file
210
专栏/Linux内核技术实战课/04案例篇如何处理PageCache容易回收引起的业务性能问题?.md
Normal file
@ -0,0 +1,210 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
04 案例篇 如何处理Page Cache容易回收引起的业务性能问题?
|
||||
你好,我是邵亚方。我们在前一节课讲了Page Cache难以回收导致的load飙高问题,这类问题是很直观的,相信很多人都遭遇过。这节课,我们则是来讲相反的一些问题,即Page Cache太容易回收而引起的一些问题。
|
||||
|
||||
这类问题因为不直观所以陷阱会很多,应用开发者和运维人员会更容易踩坑,也正因为这类问题不直观,所以他们往往是一而再再而三地中招之后,才搞清楚问题到底是怎么回事。
|
||||
|
||||
我把大家经常遇到的这类问题做个总结,大致可以分为两方面:
|
||||
|
||||
|
||||
误操作而导致Page Cache被回收掉,进而导致业务性能下降明显;
|
||||
内核的一些机制导致业务Page Cache被回收,从而引起性能下降。
|
||||
|
||||
|
||||
如果你的业务对Page Cache比较敏感,比如说你的业务数据对延迟很敏感,或者再具体一点,你的业务指标对TP99(99分位)要求较高,那你对于这类性能问题应该多多少少有所接触。当然,这并不意味着业务对延迟不敏感,你就不需要关注这些问题了,关注这类问题会让你对业务行为理解更深刻。
|
||||
|
||||
言归正传,我们来看下发生在生产环境中的案例。
|
||||
|
||||
对Page Cache操作不当产生的业务性能下降
|
||||
|
||||
我们先从一个相对简单的案例说起,一起分析下误操作导致Page Cache被回收掉的情况,它具体是怎样发生的。
|
||||
|
||||
我们知道,对于Page Cache而言,是可以通过drop_cache来清掉的,很多人在看到系统中存在非常多的Page Cache时会习惯使用drop_cache来清理它们,但是这样做是会有一些负面影响的,比如说这些Page Cache被清理掉后可能会引起系统性能下降。为什么?
|
||||
|
||||
其实这和inode有关,那inode是什么意思呢?inode是内存中对磁盘文件的索引,进程在查找或者读取文件时就是通过inode来进行操作的,我们用下面这张图来表示一下这种关系:
|
||||
|
||||
|
||||
|
||||
如上图所示,进程会通过inode来找到文件的地址空间(address_space),然后结合文件偏移(会转换成page index)来找具体的Page。如果该Page存在,那就说明文件内容已经被读取到了内存;如果该Page不存在那就说明不在内存中,需要到磁盘中去读取。你可以理解为inode是Pagecache Page(页缓存的页)的宿主(host),如果inode不存在了,那么PageCache Page也就不存在了。
|
||||
|
||||
如果你使用过drop_cache来释放inode的话,应该会清楚它有几个控制选项,我们可以通过写入不同的数值来释放不同类型的cache(用户数据Page Cache,内核数据Slab,或者二者都释放),这些选项你可以去看Kernel Documentation的描述。
|
||||
|
||||
|
||||
|
||||
于是这样就引入了一个容易被我们忽略的问题:当我们执行echo 2来drop slab的时候,它也会把Page Cache给drop掉,很多运维人员都会忽视掉这一点。
|
||||
|
||||
在系统内存紧张的时候,运维人员或者开发人员会想要通过drop_caches的方式来释放一些内存,但是由于他们清楚Page Cache被释放掉会影响业务性能,所以就期望只去drop slab而不去drop pagecache。于是很多人这个时候就运行 echo 2 > /proc/sys/vm/drop_caches,但是结果却出乎了他们的意料:Page Cache也被释放掉了,业务性能产生了明显的下降。
|
||||
|
||||
很多人都遇到过这个场景:系统正在运行着,忽然Page Cache被释放掉了,由于不清楚释放的原因,所以很多人就会怀疑,是不是由其他人/程序执行了drop_caches导致的。那有没有办法来观察这个inode释放引起Page Cache被释放的行为呢?答案是有的。关于这一点,我们在下一节课会讲到。我们先来分析下如何观察是否有人或者有程序执行过drop_caches。
|
||||
|
||||
由于drop_caches是一种内存事件,内核会在/proc/vmstat中来记录这一事件,所以我们可以通过/proc/vmstat来判断是否有执行过drop_caches。
|
||||
|
||||
$ grep drop /proc/vmstat
|
||||
drop_pagecache 3
|
||||
drop_slab 2
|
||||
|
||||
|
||||
如上所示,它们分别意味着pagecache被drop了3次(通过echo 1 或者echo 3),slab被drop了2次(通过echo 2或者echo 3)。如果这两个值在问题发生前后没有变化,那就可以排除是有人执行了drop_caches;否则可以认为是因为drop_caches引起的Page Cache被回收。
|
||||
|
||||
针对这类问题,你除了在执行drop cache前三思而后行之外,还有其他的一些根治的解决方案。在讲这些解决方案之前,我们先来看一个更加复杂一点的案例,它们有一些共性,解决方案也类似,只是接下来这个案例涉及的内核机制更加复杂。
|
||||
|
||||
内核机制引起Page Cache被回收而产生的业务性能下降
|
||||
|
||||
我们在前面已经提到过,在内存紧张的时候会触发内存回收,内存回收会尝试去回收reclaimable(可以被回收的)内存,这部分内存既包含Page Cache又包含reclaimable kernel memory(比如slab)。我们可以用下图来简单描述这个过程:
|
||||
|
||||
|
||||
|
||||
我简单来解释一下这个图。Reclaimer是指回收者,它可以是内核线程(包括kswapd)也可以是用户线程。回收的时候,它会依次来扫描pagecache page和slab page中有哪些可以被回收的,如果有的话就会尝试去回收,如果没有的话就跳过。在扫描可回收page的过程中回收者一开始扫描的较少,然后逐渐增加扫描比例直至全部都被扫描完。这就是内存回收的大致过程。
|
||||
|
||||
接下来我所要讲述的案例就发生在“relcaim slab”中,我们从前一个案例已然知道,如果inode被回收的话,那么它对应的Page Cache也都会被回收掉,所以如果业务进程读取的文件对应的inode被回收了,那么该文件所有的Page Cache都会被释放掉,这也是容易引起性能问题的地方。
|
||||
|
||||
那这个行为是否有办法观察?这同样也是可以通过/proc/vmstat来观察的,/proc/vmstat简直无所不能(这也是为什么我会在之前说内核开发者更习惯去观察/proc/vmstat)。
|
||||
|
||||
$ grep inodesteal /proc/vmstat
|
||||
pginodesteal 114341
|
||||
kswapd_inodesteal 1291853
|
||||
|
||||
|
||||
这个行为对应的事件是inodesteal,就是上面这两个事件,其中kswapd_inodesteal是指在kswapd回收的过程中,因为回收inode而释放的pagecache page个数;pginodesteal是指kswapd之外其他线程在回收过程中,因为回收inode而释放的pagecache page个数。所以在你发现业务的Page Cache被释放掉后,你可以通过观察来发现是否因为该事件导致的。
|
||||
|
||||
在明白了Page Cache被回收掉是如何发生的,以及知道了该如何观察之后,我们来看下该如何解决这类问题。
|
||||
|
||||
如何避免Page Cache被回收而引起的性能问题?
|
||||
|
||||
我们在分析一些问题时,往往都会想这个问题是我的模块有问题呢,还是别人的模块有问题。也就是说,是需要修改我的模块来解决问题还是需要修改其他模块来解决问题。与此类似,避免Page Cache里相对比较重要的数据被回收掉的思路也是有两种:
|
||||
|
||||
|
||||
从应用代码层面来优化;
|
||||
从系统层面来调整。
|
||||
|
||||
|
||||
从应用程序代码层面来解决是相对比较彻底的方案,因为应用更清楚哪些Page Cache是重要的,哪些是不重要的,所以就可以明确地来对读写文件过程中产生的Page Cache区别对待。比如说,对于重要的数据,可以通过mlock(2)来保护它,防止被回收以及被drop;对于不重要的数据(比如日志),那可以通过madvise(2)告诉内核来立即释放这些Page Cache。
|
||||
|
||||
我们来看一个通过mlock(2)来保护重要数据防止被回收或者被drop的例子:
|
||||
|
||||
#include <sys/mman.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
|
||||
#define FILE_NAME "/home/yafang/test/mmap/data"
|
||||
#define SIZE (1024*1000*1000)
|
||||
|
||||
|
||||
int main()
|
||||
{
|
||||
int fd;
|
||||
char *p;
|
||||
int ret;
|
||||
|
||||
|
||||
fd = open(FILE_NAME, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
|
||||
|
||||
/* Set size of this file */
|
||||
ret = ftruncate(fd, SIZE);
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
|
||||
/* The current offset is 0, so we don't need to reset the offset. */
|
||||
/* lseek(fd, 0, SEEK_CUR); */
|
||||
|
||||
|
||||
/* Mmap virtual memory */
|
||||
p = mmap(0, SIZE, PROT_READ|PROT_WRITE, MAP_FILE|MAP_SHARED, fd, 0);
|
||||
if (!p)
|
||||
return -1;
|
||||
|
||||
|
||||
/* Alloc physical memory */
|
||||
memset(p, 1, SIZE);
|
||||
|
||||
|
||||
/* Lock these memory to prevent from being reclaimed */
|
||||
mlock(p, SIZE);
|
||||
|
||||
|
||||
/* Wait until we kill it specifically */
|
||||
while (1) {
|
||||
sleep(10);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Unmap the memory.
|
||||
* Actually the kernel will unmap it automatically after the
|
||||
* process exits, whatever we call munamp() specifically or not.
|
||||
*/
|
||||
munmap(p, SIZE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
在这个例子中,我们通过mlock(2)来锁住了读FILE_NAME这个文件内容对应的Page Cache。在运行上述程序之后,我们来看下该如何来观察这种行为:确认这些Page Cache是否被保护住了,被保护了多大。这同样可以通过/proc/meminfo来观察:
|
||||
|
||||
$ egrep "Unevictable|Mlocked" /proc/meminfo
|
||||
Unevictable: 1000000 kB
|
||||
Mlocked: 1000000 kB
|
||||
|
||||
|
||||
然后你可以发现,drop_caches或者内存回收是回收不了这些内容的,我们的目的也就达到了。
|
||||
|
||||
在有些情况下,对应用程序而言,修改源码是件比较麻烦的事,如果可以不修改源码来达到目的那就最好不过了。Linux内核同样实现了这种不改应用程序的源码而从系统层面调整来保护重要数据的机制,这个机制就是memory cgroup protection。
|
||||
|
||||
它大致的思路是,将需要保护的应用程序使用memory cgroup来保护起来,这样该应用程序读写文件过程中所产生的Page Cache就会被保护起来不被回收或者最后被回收。memory cgroup protection大致的原理如下图所示:
|
||||
|
||||
|
||||
|
||||
如上图所示,memory cgroup提供了几个内存水位控制线memory.{min, low, high, max} 。
|
||||
|
||||
|
||||
memory.max-
|
||||
这是指memory cgroup内的进程最多能够分配的内存,如果不设置的话,就默认不做内存大小的限制。
|
||||
|
||||
memory.high-
|
||||
如果设置了这一项,当memory cgroup内进程的内存使用量超过了该值后就会立即被回收掉,所以这一项的目的是为了尽快的回收掉不活跃的Page Cache。
|
||||
|
||||
memory.low-
|
||||
这一项是用来保护重要数据的,当memory cgroup内进程的内存使用量低于了该值后,在内存紧张触发回收后就会先去回收不属于该memory cgroup的Page Cache,等到其他的Page Cache都被回收掉后再来回收这些Page Cache。
|
||||
|
||||
memory.min-
|
||||
这一项同样是用来保护重要数据的,只不过与memoy.low有所不同的是,当memory cgroup内进程的内存使用量低于该值后,即使其他不在该memory cgroup内的Page Cache都被回收完了也不会去回收这些Page Cache,可以理解为这是用来保护最高优先级的数据的。
|
||||
|
||||
|
||||
那么,如果你想要保护你的Page Cache不被回收,你就可以考虑将你的业务进程放在一个memory cgroup中,然后设置memory.{min,low} 来进行保护;与之相反,如果你想要尽快释放你的Page Cache,那你可以考虑设置memory.high来及时的释放掉不活跃的Page Cache。
|
||||
|
||||
更加细节性的一些设置我们就不在这里讨论了,我建议你可以自己动手来设置后观察一下,这样你理解会更深刻。
|
||||
|
||||
课堂总结
|
||||
|
||||
我们在前一篇讲到了Page Cache回收困难引起的load飙高问题,这也是很直观的一类问题;在这一篇讲述的则是一类相反的问题,即Page Cache太容易被回收而引起的一些问题,这一类问题是不那么直观的一类问题。
|
||||
|
||||
对于很直观的问题,我们相对比较容易去观察分析,而且由于它们比较容易观察,所以也相对能够得到重视;对于不直观的问题,则不是那么容易观察分析,相对而言它们也容易被忽视。
|
||||
|
||||
外在的特征不明显,并不意味着它产生的影响不严重,就好比皮肤受伤流血了,我们知道需要立即止血这个伤病也能很容易得到控制;如果是内伤,比如心肝脾肺肾有了问题,则容易被忽视,但是这些问题一旦积压久了往往造成很严重的后果。所以对于这类不直观的问题,我们还是需要去重视它,而且尽量做到提前预防,比如说:
|
||||
|
||||
|
||||
如果你的业务对Page Cache所造成的延迟比较敏感,那你最好可以去保护它,比如通过mlock或者memory cgroup来对它们进行保护;
|
||||
在你不明白Page Cache是因为什么原因被释放时,你可以通过/proc/vmstat里面的一些指标来观察,找到具体的释放原因然后再对症下药的去做优化。
|
||||
|
||||
|
||||
课后作业
|
||||
|
||||
这节课给你布置的作业是与mlock相关的,请你思考下,进程调用mlock()来保护内存,然后进程没有运行munlock()就退出了,在进程退出后,这部分内存还被保护吗,为什么?欢迎在留言区分享你的看法。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
134
专栏/Linux内核技术实战课/05分析篇如何判断问题是否由PageCache产生的?.md
Normal file
134
专栏/Linux内核技术实战课/05分析篇如何判断问题是否由PageCache产生的?.md
Normal file
@ -0,0 +1,134 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
05 分析篇 如何判断问题是否由Page Cache产生的?
|
||||
你好,我是邵亚方。
|
||||
|
||||
在前面几节课里,我们讲了Page Cache的一些基础知识,以及如何去处理Page Cache引发的一些问题。这节课我们来讲讲,如何判断问题是不是由Page Cache引起的。
|
||||
|
||||
我们知道,一个问题往往牵扯到操作系统的很多模块,比如说,当系统出现load飙高的问题时,可能是Page Cache引起的;也可能是锁冲突太厉害,物理资源(CPU、内存、磁盘I/O、网络I/O)有争抢导致的;也可能是内核特性设计缺陷导致的,等等。
|
||||
|
||||
如果我们没有判断清楚问题是如何引起的而贸然采取措施,非但无法解决问题,反而会引起其他负面影响,比如说,load飙高本来是Page Cache引起的,如果你没有查清楚原因,而误以为是网络引起的,然后对网络进行限流,看起来把问题解决了,但是系统运行久了还是会出现load飙高,而且限流这种行为还降低了系统负载能力。
|
||||
|
||||
那么当问题发生时,我们如何判断它是不是由Page Cache引起的呢?
|
||||
|
||||
Linux问题的典型分析手段
|
||||
|
||||
Linux上有一些典型的问题分析手段,从这些基本的分析方法入手,你可以一步步判断出问题根因。这些分析手段,可以简单地归纳为下图:
|
||||
|
||||
|
||||
|
||||
从这张图中我们可以看到,Linux内核主要是通过/proc和/sys把系统信息导出给用户,当你不清楚问题发生的原因时,你就可以试着去这几个目录下读取一下系统信息,看看哪些指标异常。比如当你不清楚问题是否由Page Cache引起时,你可以试着去把/proc/vmstat里面的信息给读取出来,看看哪些指标单位时间内变化较大。如果pgscan相关指标变化较大,那就可能是Page Cache引起的,因为pgscan代表了Page Cache的内存回收行为,它变化较大往往意味着系统内存压力很紧张。
|
||||
|
||||
/proc和/sys里面的信息可以给我们指出一个问题分析的大致方向,我们可以判断出问题是不是由Page Cache引起的,但是如果想要深入地分析问题,知道Page Cache是如何引起问题的,我们还需要掌握更加专业的分析手段,专业的分析工具有ftrace,ebpf,perf等。
|
||||
|
||||
当然了,这些专业工具的学习成本也相对略高一些,但你不能觉得它难、成本高,就不学了,因为掌握了这些分析工具后,再遇到疑难杂症,你分析起来会更加得心应手。
|
||||
|
||||
为了让你在遇到问题时更加方便地找到合适的分析工具,我借用Bredan Gregg的一张图,并根据自己的经验,把这张图略作了一些改进,帮助你学习该如何使用这些分析工具:
|
||||
|
||||
|
||||
|
||||
在这张图里,整体上追踪方式分为了静态追踪(预置了追踪点)和动态追踪(需要借助probe):
|
||||
|
||||
|
||||
如果你想要追踪的东西已经有了预置的追踪点,那你直接使用这些预置追踪点就可以了;
|
||||
如果没有预置追踪点,那你就要看看是否可以使用probe(包括kprobe和uprobe)来实现。
|
||||
|
||||
|
||||
因为分析工具自身也会对业务造成一些影响(Heisenbug),比如说使用strace会阻塞进程的运行,再比如使用systemtap也会有加载编译的开销等,所以我们在使用这些工具之前也需要去详细了解下这些工具的副作用,以免引起意料之外的问题。
|
||||
|
||||
比如我多年以前在使用systemtap的guru(专家)模式的时候,因为没有考虑到systemtap进程异常退出后,可能不会卸载systemtap模块从而引发系统panic的问题。
|
||||
|
||||
上面这些就是Linux问题的一些典型分析方法,了解了这些分析方法,你再遇到问题就能知道该选择什么样的工具来去分析。对于Page Cache而言,首先我们可以通过/proc/vmstat来做一个大致判断,然后再结合Page Cache的tracepoint来做更加深入的分析。
|
||||
|
||||
接下来我们一起分析两个具体问题。
|
||||
|
||||
系统现在load很高,是由Page Cache引起的吗?
|
||||
|
||||
我相信你肯定会遇到过这种场景:业务一直稳定运行着却忽然出现很大的性能抖动,或者系统一直稳定运行着却忽然出现较高的load值,那怎么去判断这个问题是不是由Page Cache引起的呢?在这里,我根据自己多年的经验,总结了一些分析的步骤。
|
||||
|
||||
分析问题的第一步,就是需要对系统的概括做一个了解,对于Page Cahe相关的问题,我推荐你使用sar来采集Page Cache的概况,它是系统默认配置好的工具,使用起来非常简单方便。
|
||||
|
||||
我在课程的第1讲也提到了对sar的一些使用:比如通过sar -B来分析分页信息(Paging statistics), 以及sar -r来分析内存使用情况统计(Memory utilization statistics)等。在这里,我特别推荐你使用sar里面记录的PSI(Pressure-Stall Information)信息来查看Page Cache产生压力情况,尤其是给业务产生的压力,而这些压力最终都会体现在load上。不过该功能需要4.20以上的内核版本才支持,同时sar的版本也要更新到12.3.3版本以上。比如PSI中表示内存压力的如下输出:
|
||||
|
||||
some avg10=45.49 avg60=10.23 avg300=5.41 total=76464318
|
||||
full avg10=40.87 avg60=9.05 avg300=4.29 total=58141082
|
||||
|
||||
|
||||
你需要重点关注avg10这一列,它表示最近10s内存的平均压力情况,如果它很大(比如大于40)那load飙高大概率是由于内存压力,尤其是Page Cache的压力引起的。
|
||||
|
||||
明白了概况之后,我们还需要进一步查看究竟是Page Cache的什么行为引起的系统压力。
|
||||
|
||||
因为sar采集的只是一些常用的指标,它并没有覆盖Page Cache的所有行为,比如说内存规整(memory compaction)、业务workingset等这些容易引起load飙高的问题点。在我们想要分析更加具体的原因时,就需要去采集这些指标了。通常在Page Cache出问题时,这些指标中的一个或多个都会有异常,这里我给你列出一些常见指标:
|
||||
|
||||
|
||||
|
||||
采集完这些指标后,我们就可以分析Page Cache异常是由什么引起的了。比如说,当我们发现,单位时间内compact_fail变化很大时,那往往意味着系统内存碎片很严重,已经很难申请到连续物理内存了,这时你就需要去调整碎片指数或者手动触发内存规整,来减缓因为内存碎片引起的压力了。
|
||||
|
||||
我们在前面的步骤中采集的数据指标,可以帮助我们来定位到问题点究竟是什么,比如下面这些问题点。但是有的时候,我们还需要知道是什么东西在进行连续内存的申请,从而来做更加有针对性的调整,这就需要进行进一步的观察了。我们可以利用内核预置的相关tracepoint来做更加细致的分析。
|
||||
|
||||
|
||||
|
||||
我们继续以内存规整(memory compaction)为例,来看下如何利用tracepoint来对它进行观察:
|
||||
|
||||
#首先来使能compcation相关的一些tracepoing
|
||||
$ echo 1 >
|
||||
/sys/kernel/debug/tracing/events/compaction/mm_compaction_begin/enable
|
||||
$ echo 1 >
|
||||
/sys/kernel/debug/tracing/events/compaction/mm_compaction_end/enable
|
||||
|
||||
#然后来读取信息,当compaction事件触发后就会有信息输出
|
||||
$ cat /sys/kernel/debug/tracing/trace_pipe
|
||||
<...>-49355 [037] .... 1578020.975159: mm_compaction_begin:
|
||||
zone_start=0x2080000 migrate_pfn=0x2080000 free_pfn=0x3fe5800
|
||||
zone_end=0x4080000, mode=async
|
||||
<...>-49355 [037] .N.. 1578020.992136: mm_compaction_end:
|
||||
zone_start=0x2080000 migrate_pfn=0x208f420 free_pfn=0x3f4b720
|
||||
zone_end=0x4080000, mode=async status=contended
|
||||
|
||||
|
||||
从这个例子中的信息里,我们可以看到是49355这个进程触发了compaction,begin和end这两个tracepoint触发的时间戳相减,就可以得到compaction给业务带来的延迟,我们可以计算出这一次的延迟为17ms。
|
||||
|
||||
很多时候由于采集的信息量太大,我们往往需要借助一些自动化分析的工具来分析,这样会很高效。比如我之前写过一个perf script来分析直接内存回收对业务造成的延迟。另外你也可以参考Brendan Gregg基于bcc(eBPF)写的direct reclaim snoop来观察进程因为direct reclaim而导致的延迟。
|
||||
|
||||
系统load值在昨天飙得很高,是由Page Cache引起的吗?
|
||||
|
||||
上面的问题是实时发生的,对实时问题来说,因为有现场信息可供采集,所以相对好分析一些。但是有时候,我们没有办法及时地去搜集现场信息,比如问题发生在深夜时,我们没有来得及去采集现场信息,这个时候就只能查看历史记录了。
|
||||
|
||||
我们可以根据sar的日志信息来判断当时发生了什么事情。我之前就遇到过类似的问题。
|
||||
|
||||
曾经有一个业务反馈说RT抖动得比较明显,让我帮他们分析一下抖动的原因,我把业务RT抖动的时间和sar -B里的pgscand不为0的时刻相比较后发现,二者在很多时候都是吻合的。于是,我推断业务抖动跟Page Cache回收存在一些关系,然后我让业务方调vm.min_free_kbytes来验证效果,业务方将该值从初始值90112调整为4G后效果立竿见影,就几乎没有抖动了。
|
||||
|
||||
在这里,我想再次强调一遍,调整vm.min_free_kbytes会存在一些风险,如果系统本身内存回收已经很紧张,再去调大它极有可能触发OOM甚至引起系统宕机。所以在调大的时候,一定要先做一些检查,看看此时是否可以调整。
|
||||
|
||||
当然了,如果你的sysstat版本较新并且内核版本较高,那你也可以观察PSI记录的日志信息是否跟业务抖动相吻合。根据sar的这些信息我们可以推断出故障是否跟Page Cache相关。
|
||||
|
||||
既然是通过sar的日志信息来评判,那么对日志信息的丰富度就有一定要求。你需要对常见的一些问题做一些归纳总结,然后把这些常见问题相关联的指标记录在日志中供事后分析,这样可以帮助你更加全面地分析问题,尤其是发生频率较高的一些问题。
|
||||
|
||||
比如,曾经我们的业务经常发生一些业务抖动,在通过我们上述的分析手段分析出来是compation引起的问题后,而且这类问题较多,我们便把/proc/vmstat里compaction相关的指标(我们在上面的表格里有写到具体是哪些指标)记录到我们日志系统中。在业务再次出现抖动后,我们就可以根据日志信息来判断是否跟compaction相关了。
|
||||
|
||||
课堂回顾
|
||||
|
||||
好了,这节课我们就讲到这里,我们简单回顾一下。这节课我们讲了Page Cache问题的分析方法论,按照这个方法论我们几乎可以分析清楚Page Cache相关的所有问题,而且也能帮助我们了解业务的内存访问模式,从而帮助我们更好地对业务来做优化。
|
||||
|
||||
当然这套分析方法论不仅仅适用于Page Cache引发的问题,对于系统其他层面引起的问题同样也适用。让我们再次回顾一下这些要点:
|
||||
|
||||
|
||||
在观察Page Cache的行为时,你可以先从最简单易用的分析工具比如sar入手,来得到一个概况,然后再使用更加专业一些的工具比如tracepoint去做更细致的分析。这样你就能分析清楚Page Cache的详细行为,以及它为什么会产生问题;
|
||||
对于很多的偶发性的问题,往往需要采集很多的信息才能抓取出来问题现场,这种场景下最好使用perf script来写一些自动化分析的工具来提升效率;
|
||||
如果你担心分析工具会对生产环境产生性能影响,你可以把信息采集下来之后进行离线分析,或者使用ebpf来进行自动过滤分析,请注意ebpf需要高版本内核的支持。
|
||||
|
||||
|
||||
这是我沉淀下来的定位问题的方法。也希望你在遇到问题时不逃避,刨根问底寻找根本原因是什么,相信你一定也会有自己的问题分析方法论,然后在出现问题时能够快速高效地找到原因。
|
||||
|
||||
课后作业
|
||||
|
||||
假设现在内存紧张, 有很多进程都在进行直接内存回收,如何统计出来都是哪些进程在进行直接内存回收呢?欢迎在留言区分享你的看法。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
172
专栏/Linux内核技术实战课/06基础篇进程的哪些内存类型容易引起内存泄漏?.md
Normal file
172
专栏/Linux内核技术实战课/06基础篇进程的哪些内存类型容易引起内存泄漏?.md
Normal file
@ -0,0 +1,172 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
06 基础篇 进程的哪些内存类型容易引起内存泄漏?
|
||||
06 基础篇 进程的哪些内存类型容易引起内存泄漏?
|
||||
|
||||
你好,我是邵亚方。今天我们进入课程的第二个模块,来聊一下内存泄漏的话题。
|
||||
|
||||
相信你在平时的工作中,应该遇到过下面这些场景:
|
||||
|
||||
|
||||
伴随着服务器中的后台任务持续地运行,系统中可用内存越来越少;
|
||||
应用程序正在运行时忽然被OOM kill掉了;
|
||||
进程看起来没有消耗多少内存,但是系统内存就是不够用了;
|
||||
……
|
||||
|
||||
|
||||
类似问题,很可能就是内存泄漏导致的。我们都知道,内存泄漏指的是内存被分配出去后一直没有被释放,导致这部分内存无法被再次使用,甚至更加严重的是,指向这块内存空间的指针都不存在了,进而再也无法访问这块内存空间。
|
||||
|
||||
我们平时遇到的内存泄漏可能是应用程序的内存泄漏,也可能是内核(操作系统)的内存泄漏;而应用程序的内存泄漏可能是堆内存(heap)的泄漏,也可能是内存映射区(Memory Mapping Region)的泄漏。这些不同类型的内存泄漏,它们的表现形式也是不一样的,解决方案也不一样,所以为了更好地处理内存泄漏问题,我们首先就需要去了解这些不同的内存类型。
|
||||
|
||||
这些不同的内存类型都可以理解为是进程地址空间(Address Space)的一部分,那地址空间是怎么工作的呢?
|
||||
|
||||
进程的地址空间
|
||||
|
||||
我们用一张图,来表示进程的地址空间。图的左侧是说进程可以通过什么方式来更改进程虚拟地址空间,而中间就是进程虚拟地址空间是如何划分的,右侧则是进程的虚拟地址空间所对应的物理内存或者说物理地址空间。
|
||||
|
||||
|
||||
|
||||
我们来具体聊一下这个过程。
|
||||
|
||||
应用程序首先会调用内存申请释放相关的函数,比如glibc提供的malloc(3)、 free(3)、calloc(3)等;或者是直接使用系统调用mmap(2)、munmap(2)、 brk(2)、sbrk(2)等。
|
||||
|
||||
如果使用的是库函数,这些库函数其实最终也是对系统调用的封装,所以可以理解为是应用程序动态申请释放内存,最终是要经过mmap(2)、munmap(2)、brk(2)、sbrk(2)等这些系统调用。当然从库函数到系统调用,这其中还涉及到这些库本身进行的一些内存层面的优化,比如说,malloc(3)既可能调用mmap(2),又可能会调用brk(2)。
|
||||
|
||||
然后这些内存申请和释放相关的系统调用会修改进程的地址空间 (address space),其中brk(2)和sbrk(2)修改的是heap(堆),而mmap(2)和munmap(2)修改的是Memory Mapping Region(内存映射区)。
|
||||
|
||||
请注意这些针对的都是虚拟地址,应用程序都是跟虚拟地址打交道,不会直接跟物理地址打交道。而虚拟地址最终都要转换为物理地址,由于Linux都是使用Page(页)来进行管理的,所以这个过程叫Paging(分页)。
|
||||
|
||||
我们用一张表格来简单汇总下这些不同的申请方式所对应的不同内存类型,这张表格也包含了我们在课程上一个模块讲的Page Cache,所以你可以把它理解为是进程申请内存的类型大汇总:
|
||||
|
||||
|
||||
|
||||
这里面涉及很多术语,我们对其中重要的部分做些简单介绍,来看看这张表里的哪些部分容易出现内存泄漏。
|
||||
|
||||
进程运行所需要的内存类型有很多种,总的来说,这些内存类型可以从是不是文件映射,以及是不是私有内存这两个不同的维度来做区分,也就是可以划分为上面所列的四类内存。
|
||||
|
||||
|
||||
私有匿名内存。进程的堆、栈,以及mmap(MAP_ANON | MAP_PRIVATE)这种方式申请的内存都属于这种类型的内存。其中栈是由操作系统来进行管理的,应用程序无需关注它的申请和释放;堆和私有匿名映射则是由应用程序(程序员)来进行管理的,它们的申请和释放都是由应用程序来负责的,所以它们是容易产生内存泄漏的地方。
|
||||
共享匿名内存。进程通过mmap(MAP_ANON | MAP_SHARED)这种方式来申请的内存,比如说tmpfs和shm。这个类型的内存也是由应用程序来进行管理的,所以也可能会发生内存泄漏。
|
||||
私有文件映射。进程通过mmap(MAP_FILE | MAP_PRIVATE)这种方式来申请的内存,比如进程将共享库(Shared libraries)和可执行文件的代码段(Text Segment)映射到自己的地址空间就是通过这种方式。对于共享库和可执行文件的代码段的映射,这是通过操作系统来进行管理的,应用程序无需关注它们的申请和释放。而应用程序直接通过mmap(MAP_FILE | MAP_PRIVATE)来申请的内存则是需要应用程序自己来进行管理,这也是可能会发生内存泄漏的地方。
|
||||
共享文件映射。进程通过mmap(MAP_FILE | MAP_SHARED)这种方式来申请的内存,我们在上一个模块课程中讲到的File Page Cache就属于这类内存。这部分内存也需要应用程序来申请和释放,所以也存在内存泄漏的可能性。
|
||||
|
||||
|
||||
了解了进程虚拟地址空间这些不同的内存类型后,我们来继续看下它们对应的物理内存。
|
||||
|
||||
刚刚我们也提到,进程虚拟地址空间是通过Paging(分页)这种方式来映射为物理内存的,进程调用malloc()或者mmap()来申请的内存都是虚拟内存,只有往这些内存中写入数据后(比如通过memset),才会真正地分配物理内存 。
|
||||
|
||||
你可能会有疑问,如果进程只是调用malloc()或者mmap()而不去写这些地址,即不去给它分配物理内存,是不是就不用担心内存泄漏了?
|
||||
|
||||
答案是这依然需要关注内存泄露,因为这可能导致进程虚拟地址空间耗尽,即虚拟地址空间同样存在内存泄露的问题。我们在下节课的案例篇中,也会分析对应的案例 ,这里先不展开描述了。
|
||||
|
||||
接下来,我们继续用一张图片来细化一下分页的过程。
|
||||
|
||||
|
||||
|
||||
如上图所示,Paging的大致过程是,CPU将要请求的虚拟地址传给MMU(Memory Management Unit,内存管理单元),然后MMU先在高速缓存TLB(Translation Lookaside Buffer,页表缓存)中查找转换关系,如果找到了相应的物理地址则直接访问;如果找不到则在地址转换表(Page Table)里查找计算。最终进程访问的虚拟地址就对应到了实际的物理地址。
|
||||
|
||||
了解了地址空间的相关知识之后,你就能够对进程的地址空间做一个合理的规划,或者说合理的控制了。这样出现问题时,不至于产生太严重的影响,你可以把规划好进程的地址空间理解为是进程内存问题的兜底方案。Linux上最典型的规划进程地址空间的方式就是通过ulimit,你可以通过调配它,来规划进程最大的虚拟地址空间、物理地址空间、栈空间是多少,等等。
|
||||
|
||||
对于进程地址空间相关的知识我们先聊到这里,接下来我们看下如何使用工具来观察进程的地址空间。
|
||||
|
||||
用数据观察进程的内存
|
||||
|
||||
学会观察进程地址空间是分析内存泄漏问题的前提,当你怀疑内存有泄漏时,首先需要去观察哪些内存在持续增长,哪些内存特别大,这样才能够判断出内存泄漏大致是出在哪里,然后针对性地去做分析;相反,如果你在没有仔细观察进程地址空间之前,就盲目猜测问题出在哪,处理问题很可能会浪费大量时间,甚至会南辕北辙。
|
||||
|
||||
那么都有哪些观察进程的工具呢?我们常用来观察进程内存的工具,比如说pmap、ps、top等,都可以很好地来观察进程的内存。
|
||||
|
||||
首先我们可以使用top来观察系统所有进程的内存使用概况,打开top后,然后按g再输入3,从而进入内存模式就可以了。在内存模式中,我们可以看到各个进程内存的%MEM、VIRT、RES、CODE、DATA、SHR、nMaj、nDRT,这些信息通过strace来跟踪top进程,你会发现这些信息都是从/proc/[pid]/statm和/proc/[pid]/stat这个文件里面读取的:
|
||||
|
||||
$ strace -p `pidof top`
|
||||
open("/proc/16348/statm", O_RDONLY) = 9
|
||||
read(9, "40509 1143 956 24 0 324 0\n", 1024) = 26
|
||||
close(9) = 0
|
||||
...
|
||||
open("/proc/16366/stat", O_RDONLY) = 9
|
||||
read(9, "16366 (kworker/u16:1-events_unbo"..., 1024) = 182
|
||||
close(9)
|
||||
...
|
||||
|
||||
|
||||
除了nMaj(Major Page Fault, 主缺页中断,指内容不在内存中然后从磁盘中来读取的页数)外,%MEM则是从RES计算而来的,其余的内存信息都是从statm文件里面读取的,如下是top命令中的字段和statm中字段的对应关系:
|
||||
|
||||
|
||||
|
||||
另外如果你观察仔细的话,可能会发现,有些时候所有进程的RES相加起来要比系统总的物理内存大,这是因为RES中有一些内存是被一些进程给共享的。
|
||||
|
||||
在明白了系统中各个进程的内存使用概况后,如果想要继续看某个进程的内存使用细节,你可以使用pmap。如下是pmap来展示sshd进程地址空间里的部分内容:
|
||||
|
||||
$ pmap -x `pidof sshd`
|
||||
Address Kbytes RSS Dirty Mode Mapping
|
||||
000055e798e1d000 768 652 0 r-x-- sshd
|
||||
000055e7990dc000 16 16 16 r---- sshd
|
||||
000055e7990e0000 4 4 4 rw--- sshd
|
||||
000055e7990e1000 40 40 40 rw--- [ anon ]
|
||||
...
|
||||
00007f189613a000 1800 1624 0 r-x-- libc-2.17.so
|
||||
00007f18962fc000 2048 0 0 ----- libc-2.17.so
|
||||
00007f18964fc000 16 16 16 r---- libc-2.17.so
|
||||
00007f1896500000 8 8 8 rw--- libc-2.17.so
|
||||
...
|
||||
00007ffd9d30f000 132 40 40 rw--- [ stack ]
|
||||
...
|
||||
|
||||
|
||||
每一行表示一种类型的内存(Virtual Memory Area),每一列的含义如下。
|
||||
|
||||
|
||||
Mapping,用来表示文件映射中占用内存的文件,比如sshd这个可执行文件,或者堆[heap],或者栈[stack],或者其他,等等。
|
||||
Mode,它是该内存的权限,比如,“r-x”是可读可执行,它往往是代码段(Text Segment);“rw-”是可读可写,这部分往往是数据段(Data Segment);“r–”是只读,这往往是数据段中的只读部分。
|
||||
Address、Kbytes、RSS、Dirty,Address和Kbytes分别表示起始地址和虚拟内存的大小,RSS(Resident Set Size)则表示虚拟内存中已经分配的物理内存的大小,Dirty则表示内存中数据未同步到磁盘的字节数。
|
||||
|
||||
|
||||
可以看到,通过pmap我们能够清楚地观察一个进程的整个的地址空间,包括它们分配的物理内存大小,这非常有助于我们对进程的内存使用概况做一个大致的判断。比如说,如果地址空间中[heap]太大,那有可能是堆内存产生了泄漏;再比如说,如果进程地址空间包含太多的vma(可以把maps中的每一行理解为一个vma),那很可能是应用程序调用了很多mmap而没有munmap;再比如持续观察地址空间的变化,如果发现某些项在持续增长,那很可能是那里存在问题。
|
||||
|
||||
pmap同样也是解析的/proc里的文件,具体文件是/proc/[pid]/maps和/proc/[pid]/smaps,其中smaps文件相比maps的内容更详细,可以理解为是对maps的一个扩展。你可以对比/proc/[pid]/maps和pmaps的输出,你会发现二者的内容是一致的。
|
||||
|
||||
除了观察进程自身的内存外,我们还可以观察进程分配的内存和系统指标的关联,我们就以常用的/proc/meminfo为例,来说明我们上面提到的四种内存类型(私有匿名,私有文件,共享匿名,共享文件)是如何体现在系统指标中的。
|
||||
|
||||
|
||||
|
||||
如上图所示,凡是私有的内存都会体现在/proc/meminfo中的AnonPages这一项,凡是共享的内存都会体现在Cached这一项,匿名共享的则还会体现在Shmem这一项。
|
||||
|
||||
$ cat /proc/meminfo
|
||||
...
|
||||
Cached: 3799380 kB
|
||||
...
|
||||
AnonPages: 1060684 kB
|
||||
...
|
||||
Shmem: 8724 kB
|
||||
...
|
||||
|
||||
|
||||
同样地,我也建议你动手写一些测试用例来观察下,这样你理解得就会更深刻。
|
||||
|
||||
我们对进程的内存管理相关的基础知识就先讲到这里,在下节课我们来讲一讲内存泄漏的实际案例以及其危害。
|
||||
|
||||
课堂总结
|
||||
|
||||
这节课我们讲述进程内存管理相关的一些知识,包括进程的虚拟内存与物理内存,要点如下。
|
||||
|
||||
|
||||
进程直接读写的都是虚拟地址,虚拟地址最终会通过Paging(分页)来转换为物理内存的地址,Paging这个过程是由内核来完成的。
|
||||
进程的内存类型可以从anon(匿名)与file(文件)、private(私有)与shared(共享)这四项来区分为4种不同的类型,进程相关的所有内存都是这几种方式的不同组合。
|
||||
查看进程内存时,可以先使用top来看系统中各个进程的内存使用概况,再使用pmap去观察某个进程的内存细节。
|
||||
|
||||
|
||||
进程的内存管理涉及到非常多的术语,对于常用的一些术语,比如VIRT、RES、SHR等,你还是需要牢记它们的含义的,只有熟练掌握了它们的含义,你在分析内存问题时才会更加地得心应手。比如说,如果RES太高而SHR不高,那可能是堆内存泄漏;如果SHR很高,那可能是tmpfs/shm之类的数据在持续增长,如果VIRT很高而RES很小,那可能是进程不停地在申请内存,但是却没有对这些内存进行任何的读写操作,即虚拟地址空间存在内存泄漏。
|
||||
|
||||
同样地,我希望你自己可以写一些测试用例来观察这些指标的变化。
|
||||
|
||||
课后作业
|
||||
|
||||
课后你可以写一些测试程序,分别分配我们这堂课提到的四种不同类型的内存,观察进程地址空间的变化,以及系统内存指标的变化。欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
200
专栏/Linux内核技术实战课/07案例篇如何预防内存泄漏导致的系统假死?.md
Normal file
200
专栏/Linux内核技术实战课/07案例篇如何预防内存泄漏导致的系统假死?.md
Normal file
@ -0,0 +1,200 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
07 案例篇 如何预防内存泄漏导致的系统假死?
|
||||
你好,我是邵亚方。
|
||||
|
||||
上节课,我们讲了有哪些进程的内存类型会容易引起内存泄漏,这一讲我们来聊一聊,到底应该如何应对内存泄漏的问题。
|
||||
|
||||
我们知道,内存泄漏是件非常容易发生的事,但如果它不会给应用程序和系统造成危害,那它就不会构成威胁。当然我不是说这类内存泄漏无需去关心,对追求完美的程序员而言,还是需要彻底地解决掉它的。
|
||||
|
||||
而有一些内存泄漏你却需要格外重视,比如说长期运行的后台进程的内存泄漏,这种泄漏日积月累,会逐渐耗光系统内存,甚至会引起系统假死。
|
||||
|
||||
我们在了解内存泄漏造成的危害之前,先一起看下什么样的内存泄漏是有危害的。
|
||||
|
||||
什么样的内存泄漏是有危害的?
|
||||
|
||||
下面是一个内存泄漏的简单示例程序。
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define SIZE (1024 * 1024 * 1024) /* 1G */
|
||||
int main()
|
||||
{
|
||||
char *p = malloc(SIZE);
|
||||
if (!p)
|
||||
return -1;
|
||||
|
||||
memset(p, 1, SIZE);
|
||||
/* 然后就再也不使用这块内存空间 */
|
||||
/* 没有释放p所指向的内存进程就退出了 */
|
||||
/* free(p); */
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
我们可以看到,这个程序里面申请了1G的内存后,没有进行释放就退出了,那这1G的内存空间是泄漏了吗?
|
||||
|
||||
我们可以使用一个简单的内存泄漏检查工具(valgrind)来看看。
|
||||
|
||||
$ valgrind --leak-check=full ./a.out
|
||||
==20146== HEAP SUMMARY:
|
||||
==20146== in use at exit: 1,073,741,824 bytes in 1 blocks
|
||||
==20146== total heap usage: 1 allocs, 0 frees, 1,073,741,824 bytes allocated
|
||||
==20146==
|
||||
==20146== 1,073,741,824 bytes in 1 blocks are possibly lost in loss record 1 of 1
|
||||
==20146== at 0x4C29F73: malloc (vg_replace_malloc.c:309)
|
||||
==20146== by 0x400543: main (in /home/yafang/test/mmleak/a.out)
|
||||
==20146==
|
||||
==20146== LEAK SUMMARY:
|
||||
==20146== definitely lost: 0 bytes in 0 blocks
|
||||
==20146== indirectly lost: 0 bytes in 0 blocks
|
||||
==20146== possibly lost: 1,073,741,824 bytes in 1 blocks
|
||||
==20146== still reachable: 0 bytes in 0 blocks
|
||||
==20146== suppressed: 0 bytes in 0 blocks
|
||||
|
||||
|
||||
从valgrind的检查结果里我们可以清楚地看到,申请的内存只被使用了一次(memset)就再没被使用,但是在使用完后却没有把这段内存空间给释放掉,这就是典型的内存泄漏。那这个内存泄漏是有危害的吗?
|
||||
|
||||
这就要从进程地址空间的分配和销毁来说起,下面是一个简单的示意图:
|
||||
|
||||
|
||||
|
||||
从上图可以看出,进程在退出的时候,会把它建立的映射都给解除掉。换句话说,进程退出时,会把它申请的内存都给释放掉,这个内存泄漏就是没危害的。不过话说回来,虽然这样没有什么危害,但是我们最好还是要在程序里加上free §,这才是符合编程规范的。我们修改一下这个程序,加上free§,再次编译后通过valgrind来检查,就会发现不存在任何内存泄漏了:
|
||||
|
||||
$ valgrind --leak-check=full ./a.out
|
||||
==20123== HEAP SUMMARY:
|
||||
==20123== in use at exit: 0 bytes in 0 blocks
|
||||
==20123== total heap usage: 1 allocs, 1 frees, 1,073,741,824 bytes allocated
|
||||
==20123==
|
||||
==20123== All heap blocks were freed -- no leaks are possible
|
||||
|
||||
|
||||
总之,如果进程不是长时间运行,那么即使存在内存泄漏(比如这个例子中的只有malloc没有free),它的危害也不大,因为进程退出时,内核会把进程申请的内存都给释放掉。
|
||||
|
||||
我们前面举的这个例子是对应用程序无害的内存泄漏,我们继续来看下哪些内存泄漏会给应用程序产生危害 。我们同样以malloc为例,看一个简单的示例程序:
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define SIZE (1024 * 1024 * 1024) /* 1G */
|
||||
|
||||
void process_memory()
|
||||
{
|
||||
char *p;
|
||||
p = malloc(SIZE);
|
||||
if (!p)
|
||||
return;
|
||||
memset(p, 1, SIZE);
|
||||
/* Forget to free this memory */
|
||||
}
|
||||
|
||||
/* 处理其他事务,为了简便起见,我们就以sleep为例 */
|
||||
void process_others()
|
||||
{
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
|
||||
int main()
|
||||
{
|
||||
/* 这部分内存只处理一次,以后再也不会用到 */
|
||||
process_memory();
|
||||
|
||||
|
||||
/* 进程会长时间运行 */
|
||||
while (1) {
|
||||
process_others();
|
||||
}
|
||||
return 0;
|
||||
|
||||
|
||||
这是一个长时间运行的程序,process_memory()中我们申请了1G的内存去使用,然后就再也不用它了,由于这部分内存不会再被利用,这就造成了内存的浪费,如果这样的程序多了,被泄漏出去的内存就会越来越多,然后系统中的可用内存就会越来越少。
|
||||
|
||||
对于后台服务型的业务而言,基本上都是需要长时间运行的程序,所以后台服务的内存泄漏会给系统造成实际的危害。那么,究竟会带来什么样的危害,我们又该如何去应对呢?
|
||||
|
||||
如何预防内存泄漏导致的危害?
|
||||
|
||||
我们还是以上面这个malloc()程序为例,在这个例子中,它只是申请了1G的内存,如果说持续不断地申请内存而不释放,你会发现,很快系统内存就会被耗尽,进而触发OOM killer去杀进程。这个信息可以通过dmesg(该命令是用来查看内核日志的)这个命令来查看:
|
||||
|
||||
$ dmesg
|
||||
[944835.029319] a.out invoked oom-killer: gfp_mask=0x100dca(GFP_HIGHUSER_MOVABLE|__GFP_ZERO), order=0, oom_score_adj=0
|
||||
[...]
|
||||
[944835.052448] Out of memory: Killed process 1426 (a.out) total-vm:8392864kB, anon-rss:7551936kB, file-rss:4kB, shmem-rss:0kB, UID:0 pgtables:14832kB oom_score_adj:0
|
||||
|
||||
|
||||
系统内存不足时会唤醒OOM killer来选择一个进程给杀掉,在我们这个例子中它杀掉了这个正在内存泄漏的程序,该进程被杀掉后,整个系统也就变得安全了。但是你要注意,OOM killer选择进程是有策略的,它未必一定会杀掉正在内存泄漏的进程,很有可能是一个无辜的进程被杀掉。而且,OOM本身也会带来一些副作用。
|
||||
|
||||
我来说一个发生在生产环境中的实际案例,这个案例我也曾经反馈给Linux内核社区来做改进,接下来我们详细说一下它。
|
||||
|
||||
这个案例跟OOM日志有关,OOM日志可以理解为是一个单生产者多消费者的模型,如下图所示:
|
||||
|
||||
|
||||
|
||||
这个单生产者多消费者模型,其实是由OOM killer打印日志(OOM info)时所使用的printk(类似于userspace的printf)机制来决定的。printk会检查这些日志需要输出给哪些消费者,比如写入到内核缓冲区(kernel buffer),然后通过dmesg命令来查看;我们通常也都会配置rsyslog,然后rsyslogd会将内核缓冲区的内容给转储到日志文件(/var/log/messages)中;服务器也可能会连着一些控制台(console ),比如串口,这些日志也会输出到这些console。
|
||||
|
||||
问题就出在console这里,如果console的速率很慢,输出太多日志会非常消耗时间,而当时我们配置了“console=ttyS1,19200”,即波特率为19200的串口,这是个很低速率的串口。一个完整的OOM info需要约10s才能打印完,这在系统内存紧张时就会成为一个瓶颈点,为什么会是瓶颈点呢?答案如下图所示:
|
||||
|
||||
|
||||
|
||||
进程A在申请内存失败后会触发OOM,在发生OOM的时候会打印很多很多日志(这些日志是为了方便分析为什么OOM会发生),然后会选择一个合适的进程来杀掉,从而释放出来空闲的内存,这些空闲的内存就可以满足后续内存申请了。
|
||||
|
||||
如果这个OOM的过程耗时很长(即打印到slow console所需的时间太长,如上图红色部分所示),其他进程(进程B)也在此时申请内存,也会申请失败,于是进程B同样也会触发OOM来尝试释放内存,而OOM这里又有一个全局锁(oom_lock)来进行保护,进程B尝试获取(trylock)这个锁的时候会失败,就只能再次重试。
|
||||
|
||||
如果此时系统中有很多进程都在申请内存,那么这些申请内存的进程都会被阻塞在这里,这就形成了一个恶性循环,甚至会引发系统长时间无响应(假死)。
|
||||
|
||||
针对这个问题,我与Linux内核内存子系统的维护者Michal Hocko以及OOM子模块的活跃开发者Tetsuo Handa进行了一些讨论,不过我们并没有讨论出一个完美的解决方案,目前仍然是只有一些规避措施,如下:
|
||||
|
||||
|
||||
在发生OOM时尽可能少地打印信息-
|
||||
通过将vm.oom_dump_tasks调整为0,可以不去备份(dump)当前系统中所有可被kill的进程信息,如果系统中有很多进程,这些信息的打印可能会非常消耗时间。在我们这个案例里,这部分耗时约为6s多,占OOM整体耗时10s的一多半,所以减少这部分的打印能够缓解这个问题。
|
||||
|
||||
但是,这并不是一个完美的方案,只是一个规避措施。因为当我们把vm.oom_dump_tasks配置为1时,是可以通过这些打印的信息来检查OOM killer是否选择了合理的进程,以及系统中是否存在不合理的OOM配置策略的。如果我们将它配置为0,就无法得到这些信息了,而且这些信息不仅不会打印到串口,也不会打印到内核缓冲区,导致无法被转储到不会产生问题的日志文件中。
|
||||
|
||||
调整串口打印级别,不将OOM信息打印到串口-
|
||||
通过调整/proc/sys/kernel/printk可以做到避免将OOM信息输出到串口,我们通过设置console_loglevel来将它的级别设置的比OOM日志级别(为4)小,就可以避免OOM的信息打印到console,比如将它设置为3:
|
||||
|
||||
初始配置(为7):所有信息都会输出到console
|
||||
|
||||
$ cat /proc/sys/kernel/printk
|
||||
7 4 1 7
|
||||
|
||||
调整console_loglevel级别,不让OOM信息打印到console
|
||||
|
||||
$ echo “3 4 1 7” > /proc/sys/kernel/printk
|
||||
|
||||
查看调整后的配置
|
||||
|
||||
$ cat /proc/sys/kernel/printk
|
||||
3 4 1
|
||||
|
||||
|
||||
但是这样做会导致所有低于默认级别(为4)的内核日志都无法输出到console,在系统出现问题时,我们有时候(比如无法登录到服务器上面时)会需要查看console信息来判断问题是什么引起的,如果某些信息没有被打印到console,可能会影响我们的分析。
|
||||
|
||||
这两种规避方案各有利弊,你需要根据你的实际情况来做选择,如果你不清楚怎么选择时,我建议你选择第二种,因为我们使用console的概率还是较少一些,所以第二种方案的影响也相对较小一些。
|
||||
|
||||
OOM相关的一些日志输出后,就到了下一个阶段:选择一个最需要杀死的进程来杀掉。OOM killer在选择杀掉哪个进程时,也是一个比较复杂的过程,而且如果配置不当也会引起其他问题。关于这部分的案例,我们会在下节课来分析。
|
||||
|
||||
课堂总结
|
||||
|
||||
这节课我们讲了什么是内存泄漏,以及内存泄漏可能造成的危害。对于长时间运行的后台任务而言,它存在的内存泄漏可能会给系统带来比较严重的危害,所以我们一定要重视这些任务的内存泄漏问题。
|
||||
|
||||
内存泄漏问题是非常容易发生的,所以我们需要提前做好内存泄漏的兜底工作:即使有泄漏了也不要让它给系统带来很大的危害。长时间的内存泄漏问题最后基本都会以OOM结束,所以你需要去掌握OOM的相关知识,来做好这个兜底工作。
|
||||
|
||||
如果你的服务器有慢速的串口设备,那你一定要防止它接收太多的日志,尤其是OOM产生的日志,因为OOM的日志量是很大的,打印完整个OOM信息kennel会很耗时,进而导致阻塞申请内存的进程,甚至会严重到让整个系统假死。
|
||||
|
||||
墨菲定律告诉我们,如果事情有变坏的可能,不管这种可能性有多小,它总会发生。对应到内存泄漏就是,当你的系统足够复杂后,它总是可能会发生的。所以,对于内存泄漏问题,你在做好预防的同时,也一定要对它发生后可能带来的危害做好预防。
|
||||
|
||||
课后作业
|
||||
|
||||
请写一些应用程序来构造内存泄漏的测试用例,然后使用valgrind来进行观察。欢迎在留言区分享你的看法。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
132
专栏/Linux内核技术实战课/08案例篇Shmem:进程没有消耗内存,内存哪去了?.md
Normal file
132
专栏/Linux内核技术实战课/08案例篇Shmem:进程没有消耗内存,内存哪去了?.md
Normal file
@ -0,0 +1,132 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
08 案例篇 Shmem:进程没有消耗内存,内存哪去了?
|
||||
你好,我是邵亚方。
|
||||
|
||||
在前一节课,我们讲述了进程堆内存的泄漏以及因为内存泄漏而导致的OOM的危害。这节课我们继续讲其他类型的内存泄漏,这样你在发现系统内存越来越少时,就能够想到会是什么在消耗内存。
|
||||
|
||||
有的内存泄漏会体现在进程内存里面,这种相对好观察些;而有的内存泄漏就很难观察了,因为它们无法通过观察进程消耗的内存来进行判断,从而容易被忽视,比如Shmem内存泄漏就属于这种容易被忽视的,这节课我们重点来讲讲它。
|
||||
|
||||
进程没有消耗内存,内存哪去了?
|
||||
|
||||
我生产环境上就遇到过一个真实的案例。我们的运维人员发现某几台机器used(已使用的)内存越来越多,但是通过top以及其他一些命令,却检查不出来到底是谁在占用内存。随着可用内存变得越来越少,业务进程也被OOM killer给杀掉,这给业务带来了比较严重的影响。于是他们向我寻求帮助,看看产生问题的原因是什么。
|
||||
|
||||
我在之前的课程中也提到过,在遇到系统内存不足时,我们首先要做的是查看/proc/meminfo中哪些内存类型消耗较多,然后再去做针对性分析。但是如果你不清楚/proc/meminfo里面每一项的含义,即使知道了哪几项内存出现了异常,也不清楚该如何继续去分析。所以你最好是记住/proc/meminfo里每一项的含义。
|
||||
|
||||
回到我们这个案例,通过查看这几台服务器的/proc/meminfo,发现是Shmem的大小有些异常:
|
||||
|
||||
$ cat /proc/meminfo
|
||||
...
|
||||
Shmem 16777216 kB
|
||||
...
|
||||
|
||||
|
||||
那么Shmem这一项究竟是什么含义呢?该如何去进一步分析到底是谁在使用Shmem呢?
|
||||
|
||||
我们在前面的基础篇里提到,Shmem是指匿名共享内存,即进程以mmap(MAP_ANON|MAP_SHARED)这种方式来申请的内存。你可能会有疑问,进程以这种方式来申请的内存不应该是属于进程的RES(resident)吗?比如下面这个简单的示例:
|
||||
|
||||
#include <sys/mman.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#define SIZE (1024*1024*1024)
|
||||
|
||||
int main()
|
||||
{
|
||||
char *p;
|
||||
|
||||
p = mmap(NULL, SIZE, PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0);
|
||||
if (!p)
|
||||
return -1;
|
||||
|
||||
memset(p, 1, SIZE);
|
||||
|
||||
while (1) {
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
运行该程序后,通过top可以看到确实会体现在进程的RES里面,而且还同时体现在了进程的SHR里面,也就是说,如果进程是以mmap这种方式来申请内存的话,我们是可以通过进程的内存消耗来观察到的。
|
||||
|
||||
但是在我们生产环境上遇到的问题,各个进程的RES都不大,看起来和/proc/meminfo中的Shmem完全对应不起来,这又是为什么呢?
|
||||
|
||||
先说答案:这跟一种特殊的Shmem有关。我们知道,磁盘的速度是远远低于内存的,有些应用程序为了提升性能,会避免将一些无需持续化存储的数据写入到磁盘,而是把这部分临时数据写入到内存中,然后定期或者在不需要这部分数据时,清理掉这部分内容来释放出内存。在这种需求下,就产生了一种特殊的Shmem:tmpfs。tmpfs如下图所示:
|
||||
|
||||
|
||||
|
||||
它是一种内存文件系统,只存在于内存中,它无需应用程序去申请和释放内存,而是操作系统自动来规划好一部分空间,应用程序只需要往这里面写入数据就可以了,这样会很方便。我们可以使用moun命令或者df命令来看系统中tmpfs的挂载点:
|
||||
|
||||
$ df -h
|
||||
Filesystem Size Used Avail Use% Mounted on
|
||||
...
|
||||
tmpfs 16G 15G 1G 94% /run
|
||||
...
|
||||
|
||||
|
||||
就像进程往磁盘写文件一样,进程写完文件之后就把文件给关闭掉了,这些文件和进程也就不再有关联,所以这些磁盘文件的大小不会体现在进程中。同样地,tmpfs中的文件也一样,它也不会体现在进程的内存占用上。讲到这里,你大概已经猜到了,我们Shmem占用内存多,是不是因为Shmem中的tmpfs较大导致的呢?
|
||||
|
||||
tmpfs是属于文件系统的一种。对于文件系统,我们都可以通过df来查看它的使用情况。所以呢,我们也可以通过df来看是不是tmpfs占用的内存较多,结果发现确实是它消耗了很多内存。这个问题就变得很清晰了,我们只要去分析tmpfs中存储的是什么文件就可以了。
|
||||
|
||||
我们在生产环境上还遇到过这样一个问题:systemd不停地往tmpfs中写入日志但是没有去及时清理,而tmpfs配置的初始值又太大,这就导致systemd产生的日志量越来越多,最终可用内存越来越少。
|
||||
|
||||
针对这个问题,解决方案就是限制systemd所使用的tmpfs的大小,在日志量达到tmpfs大小限制时,自动地清理掉临时日志,或者定期清理掉这部分日志,这都可以通过systemd的配置文件来做到。tmpfs的大小可以通过如下命令(比如调整为2G)调整:
|
||||
|
||||
$ mount -o remount,size=2G /run
|
||||
|
||||
|
||||
tmpfs作为一种特殊的Shmem,它消耗的内存是不会体现在进程内存中的,这往往会给问题排查带来一些难度。要想高效地分析这种类型的问题,你必须要去熟悉系统中的内存类型。除了tmpfs之外,其他一些类型的内存也不会体现在进程内存中,比如内核消耗的内存:/proc/meminfo中的Slab(高速缓存)、KernelStack(内核栈)和VmallocUsed(内核通过vmalloc申请的内存),这些也是你在不清楚内存被谁占用时需要去排查的。
|
||||
|
||||
如果tmpfs消耗的内存越积越多而得不到清理,最终的结果也是系统可用内存不足,然后触发OOM来杀掉进程。它很有可能会杀掉很重要的进程,或者是那些你认为不应该被杀掉的进程。
|
||||
|
||||
OOM杀进程的危害
|
||||
|
||||
OOM杀进程的逻辑大致如下图所示:
|
||||
|
||||
|
||||
|
||||
OOM killer在杀进程的时候,会把系统中可以被杀掉的进程扫描一遍,根据进程占用的内存以及配置的oom_score_adj来计算出进程最终的得分,然后把得分(oom_score)最大的进程给杀掉,如果得分最大的进程有多个,那就把先扫描到的那个给杀掉。
|
||||
|
||||
进程的oom_score可以通过/proc/[pid]/oom_score来查看,你可以扫描一下你系统中所有进程的oom_score,其中分值最大的那个就是在发生OOM时最先被杀掉的进程。不过你需要注意,由于oom_score和进程的内存开销有关,而进程的内存开销又是会动态变化的,所以该值也会动态变化。
|
||||
|
||||
如果你不想这个进程被首先杀掉,那你可以调整该进程的oom_score_adj改变这个oom_score;如果你的进程无论如何都不能被杀掉,那你可以将oom_score_adj配置为-1000。
|
||||
|
||||
通常而言,我们都需要将一些很重要的系统服务的oom_score_adj配置为-1000,比如sshd,因为这些系统服务一旦被杀掉,我们就很难再登陆进系统了。
|
||||
|
||||
但是,除了系统服务之外,不论你的业务程序有多重要,都尽量不要将它配置为-1000。因为你的业务程序一旦发生了内存泄漏,而它又不能被杀掉,这就会导致随着它的内存开销变大,OOM killer不停地被唤醒,从而把其他进程一个个给杀掉,我们之前在生产环境中就遇到过类似的案例。
|
||||
|
||||
OOM killer的作用之一,就是找到系统中不停泄漏内存的进程然后把它给杀掉,如果没有找对,那就会误杀其他进程,甚至是误杀了更为重要的业务进程。
|
||||
|
||||
OOM killer除了会杀掉一些无辜进程外,它选择杀进程的策略也未必是正确的。接下来又到了给内核找茬的时刻了,这也是我们这个系列课程的目的:告诉你如何来学些Linux内核,但同时我也要告诉你,要对内核有怀疑态度。下面这个案例就是一个内核的Bug。
|
||||
|
||||
在我们的一个服务器上,我们发现OOM killer在杀进程的时候,总是会杀掉最先扫描到的进程,而由于先扫描到的进程的内存太小,就导致OOM杀掉进程后很难释放出足够多的内存,然后很快再次发生OOM。
|
||||
|
||||
这是在Kubernetes环境下触发的一个问题,Kubernetes会将某些重要的容器配置为Guaranteed (对应的oom_score_adj为-998),以防止系统OOM的时候把该重要的容器给杀掉。 然而,如果容器内部发生了OOM就会触发这个内核Bug,导致总是杀掉最先扫描到的那个进程。
|
||||
|
||||
针对该内核Bug,我也给社区贡献了一个patch(mm, oom: make the calculation of oom badness more accurate)来修复这个选择不到合适进程的问题,在这个patch的commit log里我详细地描述了该问题,感兴趣的话你可以去看下。
|
||||
|
||||
课堂总结
|
||||
|
||||
这节课,我们学习了tmpfs这种类型的内存泄漏以及它的观察方法,这种类型的内存泄漏和其他进程内存泄漏最大的不同是,你很难通过进程消耗的内存来判断是哪里在泄漏,因为这种类型的内存不会体现在进程的RES中。但是,如果你熟悉内存问题的常规分析方法,你就能很快地找到问题所在。
|
||||
|
||||
|
||||
在不清楚内存被谁消耗时,你可以通过/proc/meminfo找到哪种类型的内存开销比较大,然后再对这种类型的内存做针对性分析。
|
||||
你需要配置合适的OOM策略(oom_score_adj)来防止重要的业务被过早杀掉(比如将重要业务的oom_score_adj调小为负值),同时你也需要考虑误杀其他进程,你可以通过比较进程的/proc/[pid]/oom_score,来判断出进程被杀的先后顺序。
|
||||
再次强调一遍,你需要学习内核,但同时你也需要对内核持怀疑态度。
|
||||
|
||||
|
||||
总之,你对不同内存类型的特点了解越多,你在分析内存问题的时候(比如内存泄漏问题)就会更加高效。熟练掌握这些不同的内存类型,你也能够在业务需要申请内存时选择合适的内存类型。
|
||||
|
||||
课后作业
|
||||
|
||||
请你运行几个程序,分别设置不同的oom_score_adj,并记录下它们的oom_score是什么样的,然后消耗系统内存触发OOM,看看oom_score和进程被杀的顺序是什么关系。欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
245
专栏/Linux内核技术实战课/09分析篇如何对内核内存泄漏做些基础的分析?.md
Normal file
245
专栏/Linux内核技术实战课/09分析篇如何对内核内存泄漏做些基础的分析?.md
Normal file
@ -0,0 +1,245 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
09 分析篇 如何对内核内存泄漏做些基础的分析?
|
||||
你好,我是邵亚方。
|
||||
|
||||
如果你是一名应用开发者,那你对应用程序引起的内存泄漏应该不会陌生。但是,你有没有想过,内存泄漏也可能是由操作系统(内核)自身的问题引起的呢?这是很多应用开发者以及运维人员容易忽视的地方,或者是相对陌生的领域。
|
||||
|
||||
然而陌生的领域不代表不会有问题,如果在陌生的领域发生了问题,而你总是习惯于分析应用程序自身,那你可能要浪费很多的分析时间,却依然一无所获。所以,对于应用开发者或者运维人员而言,掌握基本的内核内存泄漏分析方法也是必需的,这样在它发生问题时,你可以有一个初步的判断,而不至于一筹莫展。
|
||||
|
||||
内核内存泄漏往往都会是很严重的问题,这通常意味着要重启服务器来解决了,我们肯定并不希望只能靠重启服务器来解决它,不然那就只能没完没了地重启了。我们希望的应该是,在发生了内存泄漏后,能够判断出来是不是内核引起的问题,以及能够找到引起问题的根因,或者是向更专业的内核开发者求助来找到问题根因,从而彻底解决掉它,以免再次重启服务器。
|
||||
|
||||
那么,我们该如何判断内存泄漏是否是内核导致的呢?这节课我们就来讲一讲内核内存泄漏的基础分析方法。
|
||||
|
||||
内核内存泄漏是什么?
|
||||
|
||||
在进行具体的分析之前,我们需要先对内核内存泄漏有个初步的概念,究竟内核内存泄漏是指什么呢?这得从内核空间内存分配的基本方法说起。
|
||||
|
||||
我们在06基础篇里讲过,进程的虚拟地址空间(address space)既包括用户地址空间,也包括内核地址空间。这可以简单地理解为,进程运行在用户态申请的内存,对应的是用户地址空间,进程运行在内核态申请的内存,对应的是内核地址空间,如下图所示:
|
||||
|
||||
|
||||
|
||||
应用程序可以通过malloc()和free()在用户态申请和释放内存,与之对应,可以通过kmalloc()/kfree()以及vmalloc()/vfree()在内核态申请和释放内存。当然,还有其他申请和释放内存的方法,但大致可以分为这两类。
|
||||
|
||||
从最右侧的物理内存中你可以看出这两类内存申请方式的主要区别,kmalloc()内存的物理地址是连续的,而vmalloc()内存的物理地址则是不连续的。这两种不同类型的内存也是可以通过/proc/meminfo来观察的:
|
||||
|
||||
$ cat /proc/meminfo
|
||||
...
|
||||
Slab: 2400284 kB
|
||||
SReclaimable: 47248 kB
|
||||
SUnreclaim: 2353036 kB
|
||||
...
|
||||
VmallocTotal: 34359738367 kB
|
||||
VmallocUsed: 1065948 kB
|
||||
...
|
||||
|
||||
|
||||
其中vmalloc申请的内存会体现在VmallocUsed这一项中,即已使用的Vmalloc区大小;而kmalloc申请的内存则是体现在Slab这一项中,它又分为两部分,其中SReclaimable是指在内存紧张的时候可以被回收的内存,而SUnreclaim则是不可以被回收只能主动释放的内存。
|
||||
|
||||
内核之所以将kmalloc和vmalloc的信息通过/proc/meminfo给导出来,也是为了在它们引起问题的时候,让我们可以有方法来进行排查。在讲述具体的案例以及排查方法之前,我们先以一个简单的程序来看下内核空间是如何进行内存申请和释放的。
|
||||
|
||||
/* kmem_test */
|
||||
#include <linux/init.h>
|
||||
#include <linux/vmalloc.h>
|
||||
|
||||
#define SIZE (1024 * 1024 * 1024)
|
||||
|
||||
char *kaddr;
|
||||
|
||||
char *kmem_alloc(unsigned long size)
|
||||
{
|
||||
char *p;
|
||||
p = vmalloc(size);
|
||||
if (!p)
|
||||
pr_info("[kmem_test]: vmalloc failed\n");
|
||||
return p;
|
||||
}
|
||||
|
||||
void kmem_free(const void *addr)
|
||||
{
|
||||
if (addr)
|
||||
vfree(addr);
|
||||
}
|
||||
|
||||
|
||||
int __init kmem_init(void)
|
||||
{
|
||||
pr_info("[kmem_test]: kernel memory init\n");
|
||||
kaddr = kmem_alloc(SIZE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void __exit kmem_exit(void)
|
||||
{
|
||||
kmem_free(kaddr);
|
||||
pr_info("[kmem_test]: kernel memory exit\n");
|
||||
}
|
||||
|
||||
module_init(kmem_init)
|
||||
module_exit(kmem_exit)
|
||||
|
||||
MODULE_LICENSE("GPLv2");
|
||||
|
||||
|
||||
这是一个典型的内核模块,在这个内核模块中,我们使用vmalloc来分配了1G的内存空间,然后在模块退出的时候使用vfree释放掉它。这在形式上跟应用申请/释放内存其实是一致的,只是申请和释放内存的接口函数不一样而已。
|
||||
|
||||
我们需要使用Makefile来编译这个内核模块:
|
||||
|
||||
obj-m = kmem_test.o
|
||||
|
||||
all:
|
||||
make -C /lib/modules/`uname -r`/build M=`pwd`
|
||||
clean:
|
||||
rm -f *.o *.ko *.mod.c *.mod *.a modules.order Module.symvers
|
||||
|
||||
|
||||
执行make命令后就会生成一个kmem_test的内核模块,接着执行下面的命令就可以安装该模块了:
|
||||
|
||||
$ insmod kmem_test
|
||||
|
||||
|
||||
用rmmod命令则可以把它卸载掉:
|
||||
|
||||
$ rmmod kmem_test
|
||||
|
||||
|
||||
这个示例程序就是内核空间内存分配的基本方法。你可以在插入/卸载模块前后观察VmallocUsed的变化,以便于你更好地理解这一项的含义。
|
||||
|
||||
那么,在什么情况下会发生内核空间的内存泄漏呢?
|
||||
|
||||
跟用户空间的内存泄漏类似,内核空间的内存泄漏也是指只申请内存而不去释放该内存的情况,比如说,如果我们不在kmem_exit()这个函数中调用kmem_free(),就会产生内存泄漏问题。
|
||||
|
||||
那么,内核空间的内存泄漏与用户空间的内存泄漏有什么不同呢?我们知道,用户空间内存的生命周期与用户进程是一致的,进程退出后这部分内存就会自动释放掉。但是,内核空间内存的生命周期是与内核一致的,却不是跟内核模块一致的,也就是说,在内核模块退出时,不会自动释放掉该内核模块申请的内存,只有在内核重启(即服务器重启)时才会释放掉这部分内存。
|
||||
|
||||
总之,一旦发生内核内存泄漏,你很难有很好的方法来优雅地解决掉它,很多时候唯一的解决方案就是重启服务器,这显然是件很严重的问题。同样地,我也建议你来观察下这个行为,但是你需要做好重启服务器的心理准备。
|
||||
|
||||
kmalloc的用法跟vmalloc略有不同,你可以参考kmalloc API和kfree API来修改一下上面的测试程序,然后观察下kmalloc内存和/proc/meminfo中那几项的关系,我在这里就不做演示了,留给你作为课后作业。
|
||||
|
||||
内核内存泄漏的问题往往会发生在一些驱动程序中,比如说网卡驱动,SSD卡驱动等,以及我们自己开发的一些驱动,因为这类驱动不像Linux内核那样经历过大规模的功能验证和测试,所以相对容易出现一些隐藏很深的问题。
|
||||
|
||||
我们在生产环境上就遇到过很多起这类第三方驱动引发的内存泄漏问题,排查起来往往也比较费时。作为一个解决过很多这类问题的过来人,我对你的建议是,当你发现内核内存泄漏时,首先需要去质疑的就是你们系统中的第三方驱动程序,以及你们自己开发的驱动程序。
|
||||
|
||||
那么,我们该如何来观察内核内存泄漏呢?
|
||||
|
||||
如何观察内核内存泄漏?
|
||||
|
||||
在前面已经讲过,我们可以通过/proc/meminfo来观察内核内存的分配情况,这提供了一个观察内核内存的简便方法:
|
||||
|
||||
|
||||
如果/proc/meminfo中内核内存(比如VmallocUsed和SUnreclaim)太大,那很有可能发生了内核内存泄漏;
|
||||
另外,你也可以周期性地观察VmallocUsed和SUnreclaim的变化,如果它们持续增长而不下降,也可能是发生了内核内存泄漏。
|
||||
|
||||
|
||||
/proc/meminfo只是提供了系统内存的整体使用情况,如果我们想要看具体是什么模块在使用内存,那该怎么办呢?
|
||||
|
||||
这也可以通过/proc来查看,所以再次强调一遍,当你不清楚该如何去分析时,你可以试着去查看/proc目录下的文件。以上面的程序为例,安装kmem_test这个内核模块后,我们可以通过/proc/vmallocinfo来看到该模块的内存使用情况:
|
||||
|
||||
$ cat /proc/vmallocinfo | grep kmem_test
|
||||
0xffffc9008a003000-0xffffc900ca004000 1073745920 kmem_alloc+0x13/0x30 [kmem_test] pages=262144 vmalloc vpages N0=262144
|
||||
|
||||
|
||||
可以看到,在[kmem_test]这个模块里,通过kmem_alloc这个函数申请了262144个pages,即总共1G大小的内存。假设我们怀疑kmem_test这个模块存在问题,我们就可以去看看kmem_alloc这个函数里申请的内存有没有释放的地方。
|
||||
|
||||
上面这个测试程序相对比较简单一些,所以根据/proc/vmallocinfo里面的信息就能够简单地看出来是否有问题。但是,生产环境中运行的一些驱动或者内核模块,在逻辑上会复杂得多,很难一眼就看出来是否存在内存泄漏,这往往需要大量的分析。
|
||||
|
||||
那对于这种复杂场景下的内核内存泄漏问题,基本的分析思路是什么样的呢?
|
||||
|
||||
复杂场景下内核内存泄漏问题分析思路
|
||||
|
||||
如果我们想要对内核内存泄漏做些基础的分析,最好借助一些内核内存泄漏分析工具,其中最常用的分析工具就是kmemleak。
|
||||
|
||||
kmemleak是内核内存泄漏检查的利器,但是,它的使用也存在一些不便性,因为打开该特性会给性能带来一些损耗,所以生产环境中的内核都会默认关闭该特性。该特性我们一般只用在测试环境中,然后在测试环境中运行需要分析的驱动程序以及其他内核模块。
|
||||
|
||||
与其他内存泄漏检查工具类似,kmemleak也是通过检查内核内存的申请和释放,来判断是否存在申请的内存不再使用也不释放的情况。如果存在,就认为是内核内存泄漏,然后把这些泄漏的信息通过/sys/kernel/debug/kmemleak这个文件导出给用户分析。同样以我们上面的程序为例,检查结果如下:
|
||||
|
||||
unreferenced object 0xffffc9008a003000 (size 1073741824):
|
||||
comm "insmod", pid 11247, jiffies 4344145825 (age 3719.606s)
|
||||
hex dump (first 32 bytes):
|
||||
38 40 18 ba 80 88 ff ff 00 00 00 00 00 00 00 00 8@..............
|
||||
f0 13 c9 73 80 88 ff ff 18 40 18 ba 80 88 ff ff ...s.....@......
|
||||
backtrace:
|
||||
[<00000000fbd7cb65>] __vmalloc_node_range+0x22f/0x2a0
|
||||
[<000000008c0afaef>] vmalloc+0x45/0x50
|
||||
[<000000004f3750a2>] 0xffffffffa0937013
|
||||
[<0000000078198a11>] 0xffffffffa093c01a
|
||||
[<000000002041c0ec>] do_one_initcall+0x4a/0x200
|
||||
[<000000008d10d1ed>] do_init_module+0x60/0x220
|
||||
[<000000003c285703>] load_module+0x156c/0x17f0
|
||||
[<00000000c428a5fe>] __do_sys_finit_module+0xbd/0x120
|
||||
[<00000000bc613a5a>] __x64_sys_finit_module+0x1a/0x20
|
||||
[<000000004b0870a2>] do_syscall_64+0x52/0x90
|
||||
[<000000002f458917>] entry_SYSCALL_64_after_hwframe+0x44/0xa9
|
||||
|
||||
|
||||
由于该程序通过vmalloc申请的内存以后再也没有使用,所以被kmemleak标记为了“unreferenced object”,我们需要在使用完该内存空间后就释放它以节省内存。
|
||||
|
||||
如果我们想在生产环境上来观察内核内存泄漏,就无法使用kmemleak了,那还有没有其他的方法呢?
|
||||
|
||||
我们可以使用内核提供的内核内存申请释放的tracepoint,来动态观察内核内存使用情况:
|
||||
|
||||
|
||||
|
||||
当我们使能这些tracepoints后,就可以观察内存的动态申请和释放情况了,只是这个分析过程不如kmemleak那么高效。
|
||||
|
||||
当我们想要观察某些内核结构体的申请和释放时,可能没有对应的tracepiont。这个时候就需要使用kprobe或者systemtap,来针对具体的内核结构体申请释放函数进行追踪了。下面就是我们在生产环境中的一个具体案例。
|
||||
|
||||
业务方反馈说docker里面的可用内存越来越少,不清楚是什么状况,在我们通过/proc下面的文件(/proc/slabinfo)判断出来是dentry消耗内存过多后,写了一个systemtap脚本来观察dentry的申请和释放:
|
||||
|
||||
# dalloc_dfree.stp
|
||||
# usage : stap -x pid dalloc_dfree.stp
|
||||
global free = 0;
|
||||
global alloc = 0;
|
||||
|
||||
probe kernel.function("d_free") {
|
||||
if (target() == pid()) {
|
||||
free++;
|
||||
}
|
||||
}
|
||||
|
||||
probe kernel.function("d_alloc").return {
|
||||
if (target() == pid()) {
|
||||
alloc++;
|
||||
}
|
||||
}
|
||||
|
||||
probe end {
|
||||
printf("alloc %d free %d\n", alloc, free);
|
||||
}
|
||||
|
||||
|
||||
我们使用该工具进行了多次统计,都发现是dentry的申请远大于它的释放:
|
||||
|
||||
alloc 2041 free 1882
|
||||
alloc 18137 free 6852
|
||||
alloc 22505 free 10834
|
||||
alloc 33118 free 20531
|
||||
|
||||
|
||||
于是,我们判断在容器环境中dentry的回收存在问题,最终定位出这是3.10版本内核的一个Bug: 如果docker内部内存使用达到了limit,但是全局可用内存还很多,那就无法去回收docker内部的slab了。当然,这个Bug在新版本内核上已经fix了。
|
||||
|
||||
好了,我们这节课就讲到这里。
|
||||
|
||||
课堂总结
|
||||
|
||||
这节课我们讲了一种更难分析以及引起危害更大的内存泄漏:内核内存泄漏。我们还讲了针对这种内存泄漏的常用分析方法:
|
||||
|
||||
|
||||
你可以通过/proc/meminfo里面的信息来看内核内存的使用情况,然后根据这里面的信息来做一些基本的判断:如果内核太大那就值得怀疑;
|
||||
kmemleak是内核内存分析的利器,但是一般只在测试环境上使用它,因为它对性能会有比较明显的影响;
|
||||
在生产环境中可以使用tracepoint或者kprobe,来追踪特定类型内核内存的申请和释放,从而帮助我们判断是否存在内存泄漏。但这往往需要专业的知识,你在不明白的时候可以去请教一些内核专家;
|
||||
内核内存泄漏通常都是第三方驱动或者自己写的一些内核模块导致的,在出现内核内存泄漏时,你可以优先去排查它们。
|
||||
|
||||
|
||||
课后作业
|
||||
|
||||
我们这节课讲的内容对应用开发者会有些难度,对于运维人员而言也是需要掌握的。所以我们的课后作业主要是针对运维人员或者内核初学者的:请写一个systemtap脚本来追踪内核内存的申请和释放。欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
239
专栏/Linux内核技术实战课/10分析篇内存泄漏时,我们该如何一步步找到根因?.md
Normal file
239
专栏/Linux内核技术实战课/10分析篇内存泄漏时,我们该如何一步步找到根因?.md
Normal file
@ -0,0 +1,239 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
10 分析篇 内存泄漏时,我们该如何一步步找到根因?
|
||||
你好,我是邵亚方。
|
||||
|
||||
通过我们前面的基础篇以及案例篇的学习,你对内存泄漏应该有了自己的一些理解。这节课我来跟你聊一聊系统性地分析内存泄漏问题的方法:也就是说,在面对内存泄漏时,我们该如何一步步去找到根因?
|
||||
|
||||
不过,我不会深入到具体语言的实现细节,以及具体业务的代码逻辑中,而是会从Linux系统上通用的一些分析方法来入手。这样,不论你使用什么开发语言,不论你在开发什么,它总能给你提供一些帮助。
|
||||
|
||||
如何定位出是谁在消耗内存 ?
|
||||
|
||||
内存泄漏的外在表现通常是系统内存不够,严重的话可能会引起OOM (Out of Memory),甚至系统宕机。那在发生这些现象时,惯用的分析套路是什么呢?
|
||||
|
||||
首先,我们需要去找出到底是谁在消耗内存,/proc/meminfo可以帮助我们来快速定位出问题所在。
|
||||
|
||||
/proc/meminfo中的项目很多,我们没必要全部都背下来,不过有些项是相对容易出问题的,也是你在遇到内存相关的问题时,需要重点去排查的。我将这些项列了一张表格,也给出了每一项有异常时的排查思路。
|
||||
|
||||
|
||||
|
||||
总之,如果进程的内存有问题,那使用top就可以观察出来;如果进程的内存没有问题,那你可以从/proc/meminfo入手来一步步地去深入分析。
|
||||
|
||||
接下来,我们分析一个实际的案例,来看看如何分析进程内存泄漏是什么原因导致的。
|
||||
|
||||
如何去分析进程的内存泄漏原因?
|
||||
|
||||
这是我多年以前帮助一个小伙伴分析的内存泄漏问题。这个小伙伴已经使用top排查出了业务进程的内存异常,但是不清楚该如何去进一步分析。
|
||||
|
||||
他遇到的这个异常是,业务进程的虚拟地址空间(VIRT)被消耗很大,但是物理内存(RES)使用得却很少,所以他怀疑是进程的虚拟地址空间有内存泄漏。
|
||||
|
||||
我们在“06讲”中也讲过,出现该现象时,可以用top命令观察(这是当时保存的生产环境信息,部分信息做了脱敏处理):
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
31108 app 20 0 285g 4.0g 19m S 60.6 12.7 10986:15 app_server
|
||||
|
||||
|
||||
可以看到app_server这个程序的虚拟地址空间(VIRT这一项)很大,有285GB。
|
||||
|
||||
那该如何追踪app_server究竟是哪里存在问题呢?
|
||||
|
||||
我们可以用pidstat命令(关于该命令,你可以man pidstat)来追踪下该进程的内存行为,看看能够发现什么现象。
|
||||
|
||||
$ pidstat -r -p 31108 1
|
||||
|
||||
|
||||
04:47:00 PM 31108 353.00 0.00 299029776 4182152 12.73 app_server
|
||||
...
|
||||
04:47:59 PM 31108 149.00 0.00 299029776 4181052 12.73 app_server
|
||||
04:48:00 PM 31108 191.00 0.00 299040020 4181188 12.73 app_server
|
||||
...
|
||||
04:48:59 PM 31108 179.00 0.00 299040020 4181400 12.73 app_server
|
||||
04:49:00 PM 31108 183.00 0.00 299050264 4181524 12.73 app_server
|
||||
...
|
||||
04:49:59 PM 31108 157.00 0.00 299050264 4181456 12.73 app_server
|
||||
04:50:00 PM 31108 207.00 0.00 299060508 4181560 12.73 app_server
|
||||
...
|
||||
04:50:59 PM 31108 127.00 0.00 299060508 4180816 12.73 app_server
|
||||
04:51:00 PM 31108 172.00 0.00 299070752 4180956 12.73 app_server
|
||||
|
||||
|
||||
如上所示,在每个整分钟的时候,VSZ会增大10244KB,这看起来是一个很有规律的现象。然后,我们再来看下增大的这个内存区域到底是什么,你可以通过/proc/PID/smaps来看(关于/proc提供的信息,你可以回顾我们课程的“05讲”):
|
||||
|
||||
增大的内存区域,具体如下:
|
||||
|
||||
$ cat /proc/31108/smaps
|
||||
...
|
||||
7faae0e49000-7faae1849000 rw-p 00000000 00:00 0
|
||||
Size: 10240 kB
|
||||
Rss: 80 kB
|
||||
Pss: 80 kB
|
||||
Shared_Clean: 0 kB
|
||||
Shared_Dirty: 0 kB
|
||||
Private_Clean: 0 kB
|
||||
Private_Dirty: 80 kB
|
||||
Referenced: 60 kB
|
||||
Anonymous: 80 kB
|
||||
AnonHugePages: 0 kB
|
||||
Swap: 0 kB
|
||||
KernelPageSize: 4 kB
|
||||
MMUPageSize: 4 kB
|
||||
7faae1849000-7faae184a000 ---p 00000000 00:00 0
|
||||
Size: 4 kB
|
||||
Rss: 0 kB
|
||||
Pss: 0 kB
|
||||
Shared_Clean: 0 kB
|
||||
Shared_Dirty: 0 kB
|
||||
Private_Clean: 0 kB
|
||||
Private_Dirty: 0 kB
|
||||
Referenced: 0 kB
|
||||
Anonymous: 0 kB
|
||||
AnonHugePages: 0 kB
|
||||
Swap: 0 kB
|
||||
KernelPageSize: 4 kB
|
||||
MMUPageSize: 4 kB
|
||||
|
||||
|
||||
可以看到,它包括:一个私有地址空间,这从rw-p这个属性中的private可以看出来;以及一个保护页 ,这从—p这个属性可以看出来,即进程无法访问。对于有经验的开发者而言,从这个4K的保护页就可以猜测出应该跟线程栈有关了。
|
||||
|
||||
然后我们跟踪下进程申请这部分地址空间的目的是什么,通过strace命令来跟踪系统调用就可以了。因为VIRT的增加,它的系统调用函数无非是mmap或者brk,那么我们只需要strace的结果来看下mmap或brk就可以了。
|
||||
|
||||
用strace跟踪如下:
|
||||
|
||||
$ strace -t -f -p 31108 -o 31108.strace
|
||||
|
||||
|
||||
线程数较多,如果使用-f来跟踪线程,跟踪的信息量也很大,逐个搜索日志里面的mmap或者brk真是眼花缭乱, 所以我们来grep一下这个大小(10489856即10244KB),然后过滤下就好了:
|
||||
|
||||
$ cat 31108.strace | grep 10489856
|
||||
31152 23:00:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31151 23:01:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31157 23:02:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31158 23:03:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31165 23:04:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31163 23:05:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31153 23:06:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31155 23:07:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31149 23:08:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31147 23:09:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31159 23:10:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31157 23:11:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31148 23:12:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31150 23:13:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31173 23:14:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
|
||||
|
||||
从这个日志我们可以看到,出错的是mmap()这个系统调用,那我们再来看下mmap这个内存的目的:
|
||||
|
||||
31151 23:01:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31151 23:01:00 mprotect(0x7fa94bbc0000, 4096, PROT_NONE <unfinished ...> <<< 创建一个保护页
|
||||
31151 23:01:00 clone( <unfinished ...> <<< 创建线程
|
||||
31151 23:01:00 <... clone resumed> child_stack=0x7fa94c5afe50, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND
|
||||
|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID
|
||||
|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fa94c5c09d0, tls=0x7fa94c5c0700, child_tidptr=0x7fa94c5c09d0) = 20610
|
||||
|
||||
|
||||
可以看出,这是在clone时申请的线程栈。到这里你可能会有一个疑问:既然线程栈消耗了这么多的内存,那理应有很多才对啊?
|
||||
|
||||
但是实际上,系统中并没有很多app_server的线程,那这是为什么呢?答案其实比较简单:线程短暂执行完毕后就退出了,可是mmap的线程栈却没有被释放。
|
||||
|
||||
我们来写一个简单的程序复现这个现象,问题的复现是很重要的,如果很复杂的问题可以用简单的程序来复现,那就是最好的结果了。
|
||||
|
||||
如下是一个简单的复现程序:mmap一个40K的线程栈,然后线程简单执行一下就退出。
|
||||
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#define _SCHED_H
|
||||
#define __USE_GNU
|
||||
#include <bits/sched.h>
|
||||
|
||||
#define STACK_SIZE 40960
|
||||
|
||||
int func(void *arg)
|
||||
{
|
||||
printf("thread enter.\n");
|
||||
sleep(1);
|
||||
printf("thread exit.\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int main()
|
||||
{
|
||||
int thread_pid;
|
||||
int status;
|
||||
int w;
|
||||
|
||||
while (1) {
|
||||
void *addr = mmap(NULL, STACK_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0);
|
||||
if (addr == NULL) {
|
||||
perror("mmap");
|
||||
goto error;
|
||||
}
|
||||
printf("creat new thread...\n");
|
||||
thread_pid = clone(&func, addr + STACK_SIZE, CLONE_SIGHAND|CLONE_FS|CLONE_VM|CLONE_FILES, NULL);
|
||||
printf("Done! Thread pid: %d\n", thread_pid);
|
||||
if (thread_pid != -1) {
|
||||
do {
|
||||
w = waitpid(-1, NULL, __WCLONE | __WALL);
|
||||
if (w == -1) {
|
||||
perror("waitpid");
|
||||
goto error;
|
||||
}
|
||||
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
|
||||
}
|
||||
sleep(10);
|
||||
}
|
||||
|
||||
error:
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
然后我们用pidstat观察该进程的执行,可以发现它的现象跟生产环境中的问题是一致的:
|
||||
|
||||
$ pidstat -r -p 535 5
|
||||
11:56:51 PM UID PID minflt/s majflt/s VSZ RSS %MEM Command
|
||||
11:56:56 PM 0 535 0.20 0.00 4364 360 0.00 a.out
|
||||
11:57:01 PM 0 535 0.00 0.00 4364 360 0.00 a.out
|
||||
11:57:06 PM 0 535 0.20 0.00 4404 360 0.00 a.out
|
||||
11:57:11 PM 0 535 0.00 0.00 4404 360 0.00 a.out
|
||||
11:57:16 PM 0 535 0.20 0.00 4444 360 0.00 a.out
|
||||
11:57:21 PM 0 535 0.00 0.00 4444 360 0.00 a.out
|
||||
11:57:26 PM 0 535 0.20 0.00 4484 360 0.00 a.out
|
||||
11:57:31 PM 0 535 0.00 0.00 4484 360 0.00 a.out
|
||||
11:57:36 PM 0 535 0.20 0.00 4524 360 0.00 a.out
|
||||
^C
|
||||
Average: 0 535 0.11 0.00 4435 360 0.00 a.out
|
||||
|
||||
|
||||
你可以看到,VSZ每10s增大40K,但是增加的那个线程只存在了1s就消失了。
|
||||
|
||||
至此我们就可以推断出app_server的代码哪里有问题了,然后小伙伴去修复该代码Bug,很快就把该问题给解决了。
|
||||
|
||||
当然了,应用程序的内存泄漏问题其实是千奇百怪的,分析方法也不尽相同,我们讲述这个案例的目的是为了告诉你一些通用的分析技巧。我们掌握了这些通用分析技巧,很多时候就可以以不变来应万变了。
|
||||
|
||||
课堂总结
|
||||
|
||||
这节课我们讲述了系统性分析Linux上内存泄漏问题的分析方法,要点如下:
|
||||
|
||||
|
||||
top工具和/proc/meminfo文件是分析Linux上内存泄漏问题,甚至是所有内存问题的第一步,我们先找出来哪个进程或者哪一项有异常,然后再针对性地分析;
|
||||
应用程序的内存泄漏千奇百怪,所以你需要掌握一些通用的分析技巧,掌握了这些技巧很多时候就可以以不变应万变。但是,这些技巧的掌握,是建立在你的基础知识足够扎实的基础上。你需要熟练掌握我们这个系列课程讲述的这些基础知识,熟才能生巧。
|
||||
|
||||
|
||||
课后作业
|
||||
|
||||
请写一个内存泄漏的程序,然后观察/proc/[pid]/maps以及smaps的变化(pid即内存泄漏的程序的pid)。欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
171
专栏/Linux内核技术实战课/11基础篇TCP连接的建立和断开受哪些系统配置影响?.md
Normal file
171
专栏/Linux内核技术实战课/11基础篇TCP连接的建立和断开受哪些系统配置影响?.md
Normal file
@ -0,0 +1,171 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
11 基础篇 TCP连接的建立和断开受哪些系统配置影响?
|
||||
你好,我是邵亚方。
|
||||
|
||||
如果你做过Linux上面网络相关的开发,或者分析过Linux网络相关的问题,那你肯定吐槽过Linux系统里面让人眼花缭乱的各种配置项,应该也被下面这些问题困扰过:
|
||||
|
||||
|
||||
Client为什么无法和Server建立连接呢?
|
||||
三次握手都完成了,为什么会收到Server的reset呢?
|
||||
建立TCP连接怎么会消耗这么多时间?
|
||||
系统中为什么会有这么多处于time-wait的连接?该这么处理?
|
||||
系统中为什么会有这么多close-wait的连接?
|
||||
针对我的业务场景,这么多的网络配置项,应该要怎么配置呢?
|
||||
……
|
||||
|
||||
|
||||
因为网络这一块涉及到的场景太多了,Linux内核需要去处理各种各样的网络场景,不同网络场景的处理策略也会有所不同。而Linux内核的默认网络配置可能未必会适用我们的场景,这就可能导致我们的业务出现一些莫名其妙的行为。
|
||||
|
||||
所以,要想让业务行为符合预期,你需要了解Linux的相关网络配置,让这些配置更加适用于你的业务。Linux中的网络配置项是非常多的,为了让你更好地了解它们,我就以最常用的TCP/IP协议为例,从一个网络连接是如何建立起来的以及如何断开的来开始讲起。
|
||||
|
||||
TCP连接的建立过程会受哪些配置项的影响?
|
||||
|
||||
|
||||
|
||||
上图就是一个TCP连接的建立过程。TCP连接的建立是一个从Client侧调用connect(),到Server侧accept()成功返回的过程。你可以看到,在整个TCP建立连接的过程中,各个行为都有配置选项来进行控制。
|
||||
|
||||
Client调用connect()后,Linux内核就开始进行三次握手。
|
||||
|
||||
首先Client会给Server发送一个SYN包,但是该SYN包可能会在传输过程中丢失,或者因为其他原因导致Server无法处理,此时Client这一侧就会触发超时重传机制。但是也不能一直重传下去,重传的次数也是有限制的,这就是tcp_syn_retries这个配置项来决定的。
|
||||
|
||||
假设tcp_syn_retries为3,那么SYN包重传的策略大致如下:
|
||||
|
||||
|
||||
|
||||
在Client发出SYN后,如果过了1秒 ,还没有收到Server的响应,那么就会进行第一次重传;如果经过2s的时间还没有收到Server的响应,就会进行第二次重传;一直重传tcp_syn_retries次。
|
||||
|
||||
对于tcp_syn_retries为3而言,总共会重传3次,也就是说从第一次发出SYN包后,会一直等待(1 + 2 + 4 + 8)秒,如果还没有收到Server的响应,connect()就会产生ETIMEOUT的错误。
|
||||
|
||||
tcp_syn_retries的默认值是6,也就是说如果SYN一直发送失败,会在(1 + 2 + 4 + 8 + 16+ 32 + 64)秒,即127秒后产生ETIMEOUT的错误。
|
||||
|
||||
我们在生产环境上就遇到过这种情况,Server因为某些原因被下线,但是Client没有被通知到,所以Client的connect()被阻塞127s才去尝试连接一个新的Server, 这么长的超时等待时间对于应用程序而言是很难接受的。
|
||||
|
||||
所以通常情况下,我们都会将数据中心内部服务器的tcp_syn_retries给调小,这里推荐设置为2,来减少阻塞的时间。因为对于数据中心而言,它的网络质量是很好的,如果得不到Server的响应,很可能是Server本身出了问题。在这种情况下,Client及早地去尝试连接其他的Server会是一个比较好的选择,所以对于客户端而言,一般都会做如下调整:
|
||||
|
||||
|
||||
net.ipv4.tcp_syn_retries = 2
|
||||
|
||||
|
||||
有些情况下1s的阻塞时间可能都很久,所以有的时候也会将三次握手的初始超时时间从默认值1s调整为一个较小的值,比如100ms,这样整体的阻塞时间就会小很多。这也是数据中心内部经常进行一些网络优化的原因。
|
||||
|
||||
如果Server没有响应Client的SYN,除了我们刚才提到的Server已经不存在了这种情况外,还有可能是因为Server太忙没有来得及响应,或者是Server已经积压了太多的半连接(incomplete)而无法及时去处理。
|
||||
|
||||
半连接,即收到了SYN后还没有回复SYNACK的连接,Server每收到一个新的SYN包,都会创建一个半连接,然后把该半连接加入到半连接队列(syn queue)中。syn queue的长度就是tcp_max_syn_backlog这个配置项来决定的,当系统中积压的半连接个数超过了该值后,新的SYN包就会被丢弃。对于服务器而言,可能瞬间会有非常多的新建连接,所以我们可以适当地调大该值,以免SYN包被丢弃而导致Client收不到SYNACK:
|
||||
|
||||
|
||||
net.ipv4.tcp_max_syn_backlog = 16384
|
||||
|
||||
|
||||
Server中积压的半连接较多,也有可能是因为有些恶意的Client在进行SYN Flood攻击。典型的SYN Flood攻击如下:Client高频地向Server发SYN包,并且这个SYN包的源IP地址不停地变换,那么Server每次接收到一个新的SYN后,都会给它分配一个半连接,Server的SYNACK根据之前的SYN包找到的是错误的Client IP, 所以也就无法收到Client的ACK包,导致无法正确建立TCP连接,这就会让Server的半连接队列耗尽,无法响应正常的SYN包。
|
||||
|
||||
为了防止SYN Flood攻击,Linux内核引入了SYN Cookies机制。SYN Cookie的原理是什么样的呢?
|
||||
|
||||
在Server收到SYN包时,不去分配资源来保存Client的信息,而是根据这个SYN包计算出一个Cookie值,然后将Cookie记录到SYNACK包中发送出去。对于正常的连接,该Cookies值会随着Client的ACK报文被带回来。然后Server再根据这个Cookie检查这个ACK包的合法性,如果合法,才去创建新的TCP连接。通过这种处理,SYN Cookies可以防止部分SYN Flood攻击。所以对于Linux服务器而言,推荐开启SYN Cookies:
|
||||
|
||||
|
||||
net.ipv4.tcp_syncookies = 1
|
||||
|
||||
|
||||
Server向Client发送的SYNACK包也可能会被丢弃,或者因为某些原因而收不到Client的响应,这个时候Server也会重传SYNACK包。同样地,重传的次数也是由配置选项来控制的,该配置选项是tcp_synack_retries。
|
||||
|
||||
tcp_synack_retries的重传策略跟我们在前面讲的tcp_syn_retries是一致的,所以我们就不再画图来讲解它了。它在系统中默认是5,对于数据中心的服务器而言,通常都不需要这么大的值,推荐设置为2 :
|
||||
|
||||
|
||||
net.ipv4.tcp_synack_retries = 2
|
||||
|
||||
|
||||
Client在收到Server的SYNACK包后,就会发出ACK,Server收到该ACK后,三次握手就完成了,即产生了一个TCP全连接(complete),它会被添加到全连接队列(accept queue)中。然后Server就会调用accept()来完成TCP连接的建立。
|
||||
|
||||
但是,就像半连接队列(syn queue)的长度有限制一样,全连接队列(accept queue)的长度也有限制,目的就是为了防止Server不能及时调用accept()而浪费太多的系统资源。
|
||||
|
||||
全连接队列(accept queue)的长度是由listen(sockfd, backlog)这个函数里的backlog控制的,而该backlog的最大值则是somaxconn。somaxconn在5.4之前的内核中,默认都是128(5.4开始调整为了默认4096),建议将该值适当调大一些:
|
||||
|
||||
|
||||
net.core.somaxconn = 16384
|
||||
|
||||
|
||||
当服务器中积压的全连接个数超过该值后,新的全连接就会被丢弃掉。Server在将新连接丢弃时,有的时候需要发送reset来通知Client,这样Client就不会再次重试了。不过,默认行为是直接丢弃不去通知Client。至于是否需要给Client发送reset,是由tcp_abort_on_overflow这个配置项来控制的,该值默认为0,即不发送reset给Client。推荐也是将该值配置为0:
|
||||
|
||||
|
||||
net.ipv4.tcp_abort_on_overflow = 0
|
||||
|
||||
|
||||
这是因为,Server如果来不及accept()而导致全连接队列满,这往往是由瞬间有大量新建连接请求导致的,正常情况下Server很快就能恢复,然后Client再次重试后就可以建连成功了。也就是说,将 tcp_abort_on_overflow 配置为0,给了Client一个重试的机会。当然,你可以根据你的实际情况来决定是否要使能该选项。
|
||||
|
||||
accept()成功返回后,一个新的TCP连接就建立完成了,TCP连接进入到了ESTABLISHED状态:
|
||||
|
||||
|
||||
|
||||
上图就是从Client调用connect(),到Server侧accept()成功返回这一过程中的TCP状态转换。这些状态都可以通过netstat或者ss命令来看。至此,Client和Server两边就可以正常通信了。
|
||||
|
||||
接下来,我们看下TCP连接断开过程中会受哪些系统配置项的影响。
|
||||
|
||||
TCP连接的断开过程会受哪些配置项的影响?
|
||||
|
||||
|
||||
|
||||
如上所示,当应用程序调用close()时,会向对端发送FIN包,然后会接收ACK;对端也会调用close()来发送FIN,然后本端也会向对端回ACK,这就是TCP的四次挥手过程。
|
||||
|
||||
首先调用close()的一侧是active close(主动关闭);而接收到对端的FIN包后再调用close()来关闭的一侧,称之为passive close(被动关闭)。在四次挥手的过程中,有三个TCP状态需要额外关注,就是上图中深红色的那三个状态:主动关闭方的FIN_WAIT_2和TIME_WAIT,以及被动关闭方的CLOSE_WAIT状态。除了CLOSE_WAIT状态外,其余两个状态都有对应的系统配置项来控制。
|
||||
|
||||
我们首先来看FIN_WAIT_2状态,TCP进入到这个状态后,如果本端迟迟收不到对端的FIN包,那就会一直处于这个状态,于是就会一直消耗系统资源。Linux为了防止这种资源的开销,设置了这个状态的超时时间tcp_fin_timeout,默认为60s,超过这个时间后就会自动销毁该连接。
|
||||
|
||||
至于本端为何迟迟收不到对端的FIN包,通常情况下都是因为对端机器出了问题,或者是因为太繁忙而不能及时close()。所以,通常我们都建议将 tcp_fin_timeout 调小一些,以尽量避免这种状态下的资源开销。对于数据中心内部的机器而言,将它调整为2s足以:
|
||||
|
||||
|
||||
net.ipv4.tcp_fin_timeout = 2
|
||||
|
||||
|
||||
我们再来看TIME_WAIT状态,TIME_WAIT状态存在的意义是:最后发送的这个ACK包可能会被丢弃掉或者有延迟,这样对端就会再次发送FIN包。如果不维持TIME_WAIT这个状态,那么再次收到对端的FIN包后,本端就会回一个Reset包,这可能会产生一些异常。
|
||||
|
||||
所以维持TIME_WAIT状态一段时间,可以保障TCP连接正常断开。TIME_WAIT的默认存活时间在Linux上是60s(TCP_TIMEWAIT_LEN),这个时间对于数据中心而言可能还是有些长了,所以有的时候也会修改内核做些优化来减小该值,或者将该值设置为可通过sysctl来调节。
|
||||
|
||||
TIME_WAIT状态存在这么长时间,也是对系统资源的一个浪费,所以系统也有配置项来限制该状态的最大个数,该配置选项就是tcp_max_tw_buckets。对于数据中心而言,网络是相对很稳定的,基本不会存在FIN包的异常,所以建议将该值调小一些:
|
||||
|
||||
|
||||
net.ipv4.tcp_max_tw_buckets = 10000
|
||||
|
||||
|
||||
Client关闭跟Server的连接后,也有可能很快再次跟Server之间建立一个新的连接,而由于TCP端口最多只有65536个,如果不去复用处于TIME_WAIT状态的连接,就可能在快速重启应用程序时,出现端口被占用而无法创建新连接的情况。所以建议你打开复用TIME_WAIT的选项:
|
||||
|
||||
|
||||
net.ipv4.tcp_tw_reuse = 1
|
||||
|
||||
|
||||
还有另外一个选项tcp_tw_recycle来控制TIME_WAIT状态,但是该选项是很危险的,因为它可能会引起意料不到的问题,比如可能会引起NAT环境下的丢包问题。所以建议将该选项关闭:
|
||||
|
||||
|
||||
net.ipv4.tcp_tw_recycle = 0
|
||||
|
||||
|
||||
因为打开该选项后引起了太多的问题,所以新版本的内核就索性删掉了这个配置选项:tcp: remove tcp_tw_recycle.
|
||||
|
||||
对于CLOSE_WAIT状态而言,系统中没有对应的配置项。但是该状态也是一个危险信号,如果这个状态的TCP连接较多,那往往意味着应用程序有Bug,在某些条件下没有调用close()来关闭连接。我们在生产环境上就遇到过很多这类问题。所以,如果你的系统中存在很多CLOSE_WAIT状态的连接,那你最好去排查一下你的应用程序,看看哪里漏掉了close()。
|
||||
|
||||
至此,TCP四次挥手过程中需要注意的事项也讲完了。
|
||||
|
||||
好了,我们这节课就到此为止。
|
||||
|
||||
课堂总结
|
||||
|
||||
这节课我们讲了很多的配置项,我把这些配置项汇总到了下面这个表格里,方便你记忆:
|
||||
|
||||
|
||||
|
||||
当然了,有些配置项也是可以根据你的服务器负载以及CPU和内存大小来做灵活配置的,比如tcp_max_syn_backlog、somaxconn、tcp_max_tw_buckets这三项,如果你的物理内存足够大、CPU核数足够多,你可以适当地增大这些值,这些往往都是一些经验值。
|
||||
|
||||
另外,我们这堂课的目的不仅仅是为了让你去了解这些配置项,最主要的是想让你了解其背后的机制,这样你在遇到一些问题时,就可以有一个大致的分析方向。
|
||||
|
||||
课后作业
|
||||
|
||||
课后请你使用tcpdump这个工具来观察下TCP的三次握手和四次挥手过程,巩固今天的学习内容。欢迎在留言区分享你的看法。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
191
专栏/Linux内核技术实战课/12基础篇TCP收发包过程会受哪些配置项影响?.md
Normal file
191
专栏/Linux内核技术实战课/12基础篇TCP收发包过程会受哪些配置项影响?.md
Normal file
@ -0,0 +1,191 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
12 基础篇 TCP收发包过程会受哪些配置项影响?
|
||||
你好,我是邵亚方。我们这节课来讲一下,TCP数据在传输过程中会受到哪些因素干扰。
|
||||
|
||||
TCP收包和发包的过程也是容易引起问题的地方。收包是指数据到达网卡再到被应用程序开始处理的过程。发包则是应用程序调用发包函数到数据包从网卡发出的过程。你应该对TCP收包和发包过程中容易引发的一些问题不会陌生,比如说:
|
||||
|
||||
|
||||
网卡中断太多,占用太多CPU,导致业务频繁被打断;
|
||||
应用程序调用write()或者send()发包,怎么会发不出去呢;
|
||||
数据包明明已经被网卡收到了,可是应用程序为什么没收到呢;
|
||||
我想要调整缓冲区的大小,可是为什么不生效呢;
|
||||
是不是内核缓冲区满了从而引起丢包,我该怎么观察呢;
|
||||
…
|
||||
|
||||
|
||||
想要解决这些问题呢,你就需要去了解TCP的收发包过程容易受到哪些因素的影响。这个过程中涉及到很多的配置项,很多问题都是这些配置项跟业务场景不匹配导致的。
|
||||
|
||||
我们先来看下数据包的发送过程,这个过程会受到哪些配置项的影响呢?
|
||||
|
||||
TCP数据包的发送过程会受什么影响?
|
||||
|
||||
|
||||
|
||||
上图就是一个简略的TCP数据包的发送过程。应用程序调用write(2)或者send(2)系列系统调用开始往外发包时,这些系统调用会把数据包从用户缓冲区拷贝到TCP发送缓冲区(TCP Send Buffer),这个TCP发送缓冲区的大小是受限制的,这里也是容易引起问题的地方。
|
||||
|
||||
TCP发送缓冲区的大小默认是受net.ipv4.tcp_wmem来控制:
|
||||
|
||||
|
||||
net.ipv4.tcp_wmem = 8192 65536 16777216
|
||||
|
||||
|
||||
tcp_wmem中这三个数字的含义分别为min、default、max。TCP发送缓冲区的大小会在min和max之间动态调整,初始的大小是default,这个动态调整的过程是由内核自动来做的,应用程序无法干预。自动调整的目的,是为了在尽可能少的浪费内存的情况下来满足发包的需要。
|
||||
|
||||
tcp_wmem中的max不能超过net.core.wmem_max这个配置项的值,如果超过了,TCP 发送缓冲区最大就是net.core.wmem_max。通常情况下,我们需要设置net.core.wmem_max的值大于等于net.ipv4.tcp_wmem的max:
|
||||
|
||||
|
||||
net.core.wmem_max = 16777216
|
||||
|
||||
|
||||
对于TCP 发送缓冲区的大小,我们需要根据服务器的负载能力来灵活调整。通常情况下我们需要调大它们的默认值,我上面列出的 tcp_wmem 的 min、default、max 这几组数值就是调大后的值,也是我们在生产环境中配置的值。
|
||||
|
||||
我之所以将这几个值给调大,是因为我们在生产环境中遇到过TCP发送缓冲区太小,导致业务延迟很大的问题,这类问题也是可以使用systemtap之类的工具在内核里面打点来进行观察的(观察sk_stream_wait_memory这个事件):
|
||||
|
||||
# sndbuf_overflow.stp
|
||||
# Usage :
|
||||
# $ stap sndbuf_overflow.stp
|
||||
probe kernel.function("sk_stream_wait_memory")
|
||||
{
|
||||
printf("%d %s TCP send buffer overflow\n",
|
||||
pid(), execname())
|
||||
}
|
||||
|
||||
|
||||
如果你可以观察到sk_stream_wait_memory这个事件,就意味着TCP发送缓冲区太小了,你需要继续去调大wmem_max和tcp_wmem:max的值了。
|
||||
|
||||
应用程序有的时候会很明确地知道自己发送多大的数据,需要多大的TCP发送缓冲区,这个时候就可以通过setsockopt(2)里的SO_SNDBUF来设置固定的缓冲区大小。一旦进行了这种设置后,tcp_wmem就会失效,而且这个缓冲区大小设置的是固定值,内核也不会对它进行动态调整。
|
||||
|
||||
但是,SO_SNDBUF设置的最大值不能超过net.core.wmem_max,如果超过了该值,内核会把它强制设置为net.core.wmem_max。所以,如果你想要设置SO_SNDBUF,一定要确认好net.core.wmem_max是否满足需求,否则你的设置可能发挥不了作用。通常情况下,我们都不会通过SO_SNDBUF来设置TCP发送缓冲区的大小,而是使用内核设置的tcp_wmem,因为如果SO_SNDBUF设置得太大就会浪费内存,设置得太小又会引起缓冲区不足的问题。
|
||||
|
||||
另外,如果你关注过Linux的最新技术动态,你一定听说过eBPF。你也可以通过eBPF来设置SO_SNDBUF和SO_RCVBUF,进而分别设置TCP发送缓冲区和TCP接收缓冲区的大小。同样地,使用eBPF来设置这两个缓冲区时,也不能超过wmem_max和rmem_max。不过eBPF在一开始增加设置缓冲区大小的特性时并未考虑过最大值的限制,我在使用的过程中发现这里存在问题,就给社区提交了一个PATCH把它给修复了。你感兴趣的话可以看下这个链接:bpf: sock recvbuff must be limited by rmem_max in bpf_setsockopt()。
|
||||
|
||||
tcp_wmem以及wmem_max的大小设置都是针对单个TCP连接的,这两个值的单位都是Byte(字节)。系统中可能会存在非常多的TCP连接,如果TCP连接太多,就可能导致内存耗尽。因此,所有TCP连接消耗的总内存也有限制:
|
||||
|
||||
|
||||
net.ipv4.tcp_mem = 8388608 12582912 16777216
|
||||
|
||||
|
||||
我们通常也会把这个配置项给调大。与前两个选项不同的是,该选项中这些值的单位是Page(页数),也就是4K。它也有3个值:min、pressure、max。当所有TCP连接消耗的内存总和达到max后,也会因达到限制而无法再往外发包。
|
||||
|
||||
因tcp_mem达到限制而无法发包或者产生抖动的问题,我们也是可以观测到的。为了方便地观测这类问题,Linux内核里面预置了静态观测点:sock_exceed_buf_limit。不过,这个观测点一开始只是用来观察TCP接收时遇到的缓冲区不足的问题,不能观察TCP发送时遇到的缓冲区不足的问题。后来,我提交了一个patch做了改进,使得它也可以用来观察TCP发送时缓冲区不足的问题:net: expose sk wmem in sock_exceed_buf_limit tracepoint ,观察时你只需要打开tracepiont(需要4.16+的内核版本):
|
||||
|
||||
|
||||
$ echo 1 > /sys/kernel/debug/tracing/events/sock/sock_exceed_buf_limit/enable
|
||||
|
||||
|
||||
然后去看是否有该事件发生:
|
||||
|
||||
|
||||
$ cat /sys/kernel/debug/tracing/trace_pipe
|
||||
|
||||
|
||||
如果有日志输出(即发生了该事件),就意味着你需要调大tcp_mem了,或者是需要断开一些TCP连接了。
|
||||
|
||||
TCP层处理完数据包后,就继续往下来到了IP层。IP层这里容易触发问题的地方是net.ipv4.ip_local_port_range这个配置选项,它是指和其他服务器建立IP连接时本地端口(local port)的范围。我们在生产环境中就遇到过默认的端口范围太小,以致于无法创建新连接的问题。所以通常情况下,我们都会扩大默认的端口范围:
|
||||
|
||||
|
||||
net.ipv4.ip_local_port_range = 1024 65535
|
||||
|
||||
|
||||
为了能够对TCP/IP数据流进行流控,Linux内核在IP层实现了qdisc(排队规则)。我们平时用到的TC就是基于qdisc的流控工具。qdisc的队列长度是我们用ifconfig来看到的txqueuelen,我们在生产环境中也遇到过因为txqueuelen太小导致数据包被丢弃的情况,这类问题可以通过下面这个命令来观察:
|
||||
|
||||
|
||||
$ ip -s -s link ls dev eth0-
|
||||
…-
|
||||
TX: bytes packets errors dropped carrier collsns-
|
||||
3263284 25060 0 0 0 0
|
||||
|
||||
|
||||
如果观察到dropped这一项不为0,那就有可能是txqueuelen太小导致的。当遇到这种情况时,你就需要增大该值了,比如增加eth0这个网络接口的txqueuelen:
|
||||
|
||||
|
||||
$ ifconfig eth0 txqueuelen 2000
|
||||
|
||||
|
||||
或者使用ip这个工具:
|
||||
|
||||
|
||||
$ ip link set eth0 txqueuelen 2000
|
||||
|
||||
|
||||
在调整了txqueuelen的值后,你需要持续观察是否可以缓解丢包的问题,这也便于你将它调整到一个合适的值。
|
||||
|
||||
Linux系统默认的qdisc为pfifo_fast(先进先出),通常情况下我们无需调整它。如果你想使用TCP BBR来改善TCP拥塞控制的话,那就需要将它调整为fq(fair queue, 公平队列):
|
||||
|
||||
|
||||
net.core.default_qdisc = fq
|
||||
|
||||
|
||||
经过IP层后,数据包再往下就会进入到网卡了,然后通过网卡发送出去。至此,你需要发送出去的数据就走完了TCP/IP协议栈,然后正常地发送给对端了。
|
||||
|
||||
接下来,我们来看下数据包是怎样收上来的,以及在接收的过程中会受哪些配置项的影响。
|
||||
|
||||
TCP数据包的接收过程会受什么影响?
|
||||
|
||||
TCP数据包的接收过程,同样也可以用一张图来简单表示:
|
||||
|
||||
|
||||
|
||||
从上图可以看出,TCP数据包的接收流程在整体上与发送流程类似,只是方向是相反的。数据包到达网卡后,就会触发中断(IRQ)来告诉CPU读取这个数据包。但是在高性能网络场景下,数据包的数量会非常大,如果每来一个数据包都要产生一个中断,那CPU的处理效率就会大打折扣,所以就产生了NAPI(New API)这种机制让CPU一次性地去轮询(poll)多个数据包,以批量处理的方式来提升效率,降低网卡中断带来的性能开销。
|
||||
|
||||
那在poll的过程中,一次可以poll多少个呢?这个poll的个数可以通过sysctl选项来控制:
|
||||
|
||||
|
||||
net.core.netdev_budget = 600
|
||||
|
||||
|
||||
该控制选项的默认值是300,在网络吞吐量较大的场景中,我们可以适当地增大该值,比如增大到600。增大该值可以一次性地处理更多的数据包。但是这种调整也是有缺陷的,因为这会导致CPU在这里poll的时间增加,如果系统中运行的任务很多的话,其他任务的调度延迟就会增加。
|
||||
|
||||
接下来继续看TCP数据包的接收过程。我们刚才提到,数据包到达网卡后会触发CPU去poll数据包,这些poll的数据包紧接着就会到达IP层去处理,然后再达到TCP层,这时就会面对另外一个很容易引发问题的地方了:TCP Receive Buffer(TCP接收缓冲区)。
|
||||
|
||||
与 TCP发送缓冲区类似,TCP接收缓冲区的大小也是受控制的。通常情况下,默认都是使用tcp_rmem来控制缓冲区的大小。同样地,我们也会适当地增大这几个值的默认值,来获取更好的网络性能,调整为如下数值:
|
||||
|
||||
|
||||
net.ipv4.tcp_rmem = 8192 87380 16777216
|
||||
|
||||
|
||||
它也有3个字段:min、default、max。TCP接收缓冲区大小也是在min和max之间动态调整 ,不过跟发送缓冲区不同的是,这个动态调整是可以通过控制选项来关闭的,这个选项是tcp_moderate_rcvbuf 。通常我们都是打开它,这也是它的默认值:
|
||||
|
||||
|
||||
net.ipv4.tcp_moderate_rcvbuf = 1
|
||||
|
||||
|
||||
之所以接收缓冲区有选项可以控制自动调节,而发送缓冲区没有,那是因为TCP接收缓冲区会直接影响TCP拥塞控制,进而影响到对端的发包,所以使用该控制选项可以更加灵活地控制对端的发包行为。
|
||||
|
||||
除了tcp_moderate_rcvbuf 可以控制TCP接收缓冲区的动态调节外,也可以通过setsockopt()中的配置选项SO_RCVBUF来控制,这与TCP发送缓冲区是类似的。如果应用程序设置了SO_RCVBUF这个标记,那么TCP接收缓冲区的动态调整就是关闭,即使tcp_moderate_rcvbuf为1,接收缓冲区的大小始终就为设置的SO_RCVBUF这个值。
|
||||
|
||||
也就是说,只有在tcp_moderate_rcvbuf为1,并且应用程序没有通过SO_RCVBUF来配置缓冲区大小的情况下,TCP接收缓冲区才会动态调节。
|
||||
|
||||
同样地,与TCP发送缓冲区类似,SO_RCVBUF设置的值最大也不能超过net.core.rmem_max。通常情况下,我们也需要设置net.core.rmem_max的值大于等于net.ipv4.tcp_rmem的max:
|
||||
|
||||
|
||||
net.core.rmem_max = 16777216
|
||||
|
||||
|
||||
我们在生产环境中也遇到过,因达到了TCP接收缓冲区的限制而引发的丢包问题。但是这类问题不是那么好追踪的,没有一种很直观地追踪这种行为的方式,所以我便在我们的内核里添加了针对这种行为的统计。
|
||||
|
||||
为了让使用Linux内核的人都能很好地观察这个行为,我也把我们的实践贡献给了Linux内核社区,具体可以看这个commit:tcp: add new SNMP counter for drops when try to queue in rcv queue。使用这个SNMP计数,我们就可以很方便地通过netstat查看,系统中是否存在因为TCP接收缓冲区不足而引发的丢包。
|
||||
|
||||
不过,该方法还是存在一些局限:如果我们想要查看是哪个TCP连接在丢包,那么这种方式就不行了,这个时候我们就需要去借助其他一些更专业的trace工具,比如eBPF来达到我们的目的。
|
||||
|
||||
课堂总结
|
||||
|
||||
好了,这节课就讲到这里,我们简单回顾一下。TCP/IP是一个很复杂的协议栈,它的数据包收发过程也是很复杂的,我们这节课只是重点围绕这个过程中最容易引发问题的地方来讲述的。我们刚才提到的那些配置选项都很容易在生产环境中引发问题,并且也是我们针对高性能网络进行调优时必须要去考虑的。我把这些配置项也总结为了一个表格,方便你来查看:
|
||||
|
||||
|
||||
|
||||
这些值都需要根据你的业务场景来做灵活的调整,当你不知道针对你的业务该如何调整时,你最好去咨询更加专业的人员,或者一边调整一边观察系统以及业务行为的变化。
|
||||
|
||||
课后作业
|
||||
|
||||
我们这节课中有两张图,分别是TCP数据包的发送过程 和 TCP数据包的接收过程,我们可以看到,在TCP发送过程中使用到了qdisc,但是在接收过程中没有使用它,请问是为什么?我们可以在接收过程中也使用qdisc吗?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
146
专栏/Linux内核技术实战课/13案例篇TCP拥塞控制是如何导致业务性能抖动的?.md
Normal file
146
专栏/Linux内核技术实战课/13案例篇TCP拥塞控制是如何导致业务性能抖动的?.md
Normal file
@ -0,0 +1,146 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
13 案例篇 TCP拥塞控制是如何导致业务性能抖动的?
|
||||
你好,我是邵亚方。这节课我来跟大家分享TCP拥塞控制与业务性能抖动之间的关系。
|
||||
|
||||
TCP拥塞控制是TCP协议的核心,而且是一个非常复杂的过程。如果你不了解TCP拥塞控制的话,那么就相当于不理解TCP协议。这节课的目的是通过一些案例,介绍在TCP拥塞控制中我们要避免踩的一些坑,以及针对TCP性能调优时需要注意的一些点。
|
||||
|
||||
因为在TCP传输过程中引起问题的案例有很多,所以我不会把这些案例拿过来具体去一步步分析,而是希望能够对这些案例做一层抽象,把这些案例和具体的知识点结合起来,这样会更有系统性。并且,在你明白了这些知识点后,案例的分析过程就相对简单了。
|
||||
|
||||
我们在前两节课(第11讲和第12讲)中讲述了单机维度可能需要注意的问题点。但是,网络传输是一个更加复杂的过程,这中间涉及的问题会更多,而且更加不好分析。相信很多人都有过这样的经历:
|
||||
|
||||
|
||||
等电梯时和别人聊着微信,进入电梯后微信消息就发不出去了;
|
||||
和室友共享同一个网络,当玩网络游戏玩得正开心时,游戏忽然卡得很厉害,原来是室友在下载电影;
|
||||
使用ftp上传一个文件到服务器上,没想到要上传很久;
|
||||
……
|
||||
|
||||
|
||||
在这些问题中,TCP的拥塞控制就在发挥着作用。
|
||||
|
||||
TCP拥塞控制是如何对业务网络性能产生影响的 ?
|
||||
|
||||
我们先来看下TCP拥塞控制的大致原理。
|
||||
|
||||
|
||||
|
||||
上图就是TCP拥塞控制的简单图示,它大致分为四个阶段。
|
||||
|
||||
1. 慢启动
|
||||
|
||||
TCP连接建立好后,发送方就进入慢速启动阶段,然后逐渐地增大发包数量(TCP Segments)。这个阶段每经过一个RTT(round-trip time),发包数量就会翻倍。如下图所示:
|
||||
|
||||
|
||||
|
||||
初始发送数据包的数量是由init_cwnd(初始拥塞窗口)来决定的,该值在Linux内核中被设置为10(TCP_INIT_CWND),这是由Google的研究人员总结出的一个经验值,这个经验值也被写入了RFC6928。并且,Linux内核在2.6.38版本中也将它从默认值3修改为了Google建议的10,你感兴趣的话可以看下这个commit: tcp: Increase the initial congestion window to 10。
|
||||
|
||||
增大init_cwnd可以显著地提升网络性能,因为这样在初始阶段就可以一次性发送很多TCP Segments,更加细节性的原因你可以参考RFC6928的解释。
|
||||
|
||||
如果你的内核版本比较老(低于CentOS-6的内核版本),那不妨考虑增加init_cwnd到10。如果你想要把它增加到一个更大的值,也不是不可以,但是你需要根据你的网络状况多做一些实验,从而得到一个较为理想的值。因为如果初始拥塞窗口设置得过大的话,可能会引起很高的TCP重传率。当然,你也可以通过ip route的方式来更加灵活地调整该值,甚至将它配置为一个sysctl控制项。
|
||||
|
||||
增大init_cwnd的值对于提升短连接的网络性能会很有效,特别是数据量在慢启动阶段就能发送完的短连接,比如针对http这种服务,http的短连接请求数据量一般不大,通常在慢启动阶段就能传输完,这些都可以通过tcpdump来进行观察。
|
||||
|
||||
在慢启动阶段,当拥塞窗口(cwnd)增大到一个阈值( ssthresh,慢启动阈值)后,TCP拥塞控制就进入了下一个阶段:拥塞避免(Congestion Avoidance)。
|
||||
|
||||
2.拥塞避免
|
||||
|
||||
在这个阶段cwnd不再成倍增加,而是一个RTT增加1,即缓慢地增加cwnd,以防止网络出现拥塞。网络出现拥塞是难以避免的,由于网络链路的复杂性,甚至会出现乱序(Out of Order)报文。乱序报文产生原因之一如下图所示:
|
||||
|
||||
|
||||
|
||||
在上图中,发送端一次性发送了4个TCP segments,但是第2个segment在传输过程中被丢弃掉了,那么接收方就接收不到该segment了。然而第3个TCP segment和第4个TCP segment能够被接收到,此时3和4就属于乱序报文,它们会被加入到接收端的ofo queue(乱序队列)里。
|
||||
|
||||
丢包这类问题在移动网络环境中比较容易出现,特别是在一个网络状况不好的环境中,比如在电梯里丢包率就会很高,而丢包率高就会导致网络响应特别慢。在数据中心内部的服务上很少会有数据包在网络链路中被丢弃的情况,我说的这类丢包问题主要是针对网关服务这种和外部网络有连接的服务上。
|
||||
|
||||
针对我们的网关服务,我们自己也做过一些TCP单边优化工作,主要是优化Cubic拥塞控制算法,以缓解丢包引起的网络性能下降问题。另外,Google前几年开源的一个新的拥塞控制算法BBR在理论上也可以很好地缓解TCP丢包问题,但是在我们的实践中,BBR的效果并不好,因此我们最终也没有使用它。
|
||||
|
||||
我们再回到上面这张图,因为接收端没有接收到第2个segment,因此接收端每次收到一个新的segment后都会去ack第2个segment,即ack 17。紧接着,发送端就会接收到三个相同的ack(ack 17)。连续出现了3个响应的ack后,发送端会据此判断数据包出现了丢失,于是就进入了下一个阶段:快速重传。
|
||||
|
||||
3.快速重传和快速恢复
|
||||
|
||||
快速重传和快速恢复是一起工作的,它们是为了应对丢包这种行为而做的优化,在这种情况下,由于网络并没有出现拥塞,所以拥塞窗口不必恢复到初始值。判断丢包的依据就是收到3个相同的ack。
|
||||
|
||||
Google的工程师同样对TCP快速重传提出了一个改进策略:tcp early retrans,它允许一些情况下的TCP连接可以绕过重传延时(RTO)来进行快速重传。3.6版本以后的内核都支持了这个特性,因此,如果你还在使用CentOS-6,那么就享受不到它带来的网络性能提升了,你可以将你的操作系统升级为CentOS-7或者最新的CentOS-8。 另外,再多说一句,Google在网络方面的技术实力是其他公司没法比的,Linux内核TCP子系统的maintainer也是Google的工程师(Eric Dumazet)。
|
||||
|
||||
除了快速重传外,还有一种重传机制是超时重传。不过,这是非常糟糕的一种情况。如果发送出去一个数据包,超过一段时间(RTO)都收不到它的ack,那就认为是网络出现了拥塞。这个时候就需要将cwnd恢复为初始值,再次从慢启动开始调整cwnd的大小。
|
||||
|
||||
RTO一般发生在网络链路有拥塞的情况下,如果某一个连接数据量太大,就可能会导致其他连接的数据包排队,从而出现较大的延迟。我们在开头提到的,下载电影影响到别人玩网络游戏的例子就是这个原因。
|
||||
|
||||
关于RTO,它也是一个优化点。如果RTO过大的话,那么业务就可能要阻塞很久,所以在3.1版本的内核里引入了一种改进来将RTO的初始值从3s调整为1s,这可以显著节省业务的阻塞时间。不过,RTO=1s 在某些场景下还是有些大了,特别是在数据中心内部这种网络质量相对比较稳定的环境中。
|
||||
|
||||
我们在生产环境中发生过这样的案例:业务人员反馈说业务RT抖动得比较厉害,我们使用strace初步排查后发现,进程阻塞在了send()这类发包函数里。然后我们使用tcpdump来抓包,发现发送方在发送数据后,迟迟不能得到对端的响应,一直到RTO时间再次重传。与此同时,我们还尝试了在对端也使用tcpdump来抓包,发现对端是过了很长时间后才收到数据包。因此,我们判断是网络发生了拥塞,从而导致对端没有及时收到数据包。
|
||||
|
||||
那么,针对这种网络拥塞引起业务阻塞时间太久的情况,有没有什么解决方案呢?一种解决方案是,创建TCP连接,使用SO_SNDTIMEO来设置发送超时时间,以防止应用在发包的时候阻塞在发送端太久,如下所示:
|
||||
|
||||
|
||||
ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);
|
||||
|
||||
|
||||
当业务发现该TCP连接超时后,就会主动断开该连接,然后尝试去使用其他的连接。
|
||||
|
||||
这种做法可以针对某个TCP连接来设置RTO时间,那么,有没有什么方法能够设置全局的RTO时间(设置一次,所有的TCP连接都能生效)呢?答案是有的,这就需要修改内核。针对这类需求,我们在生产环境中的实践是:将TCP RTO min、TCP RTO max、TCP RTO init 更改为可以使用sysctl来灵活控制的变量,从而根据实际情况来做调整,比如说针对数据中心内部的服务器,我们可以适当地调小这几个值,从而减少业务阻塞时间。
|
||||
|
||||
上述这4个阶段是TCP拥塞控制的基础,总体来说,拥塞控制就是根据TCP的数据传输状况来灵活地调整拥塞窗口,从而控制发送方发送数据包的行为。换句话说,拥塞窗口的大小可以表示网络传输链路的拥塞情况。TCP连接cwnd的大小可以通过ss这个命令来查看:
|
||||
|
||||
$ ss -nipt
|
||||
State Recv-Q Send-Q Local Address:Port Peer Address:Port
|
||||
ESTAB 0 36 172.23.245.7:22 172.30.16.162:60490
|
||||
users:(("sshd",pid=19256,fd=3))
|
||||
cubic wscale:5,7 rto:272 rtt:71.53/1.068 ato:40 mss:1248 rcvmss:1248 advmss:1448 cwnd:10 bytes_acked:19591 bytes_received:2817 segs_out:64 segs_in:80 data_segs_out:57 data_segs_in:28 send 1.4Mbps lastsnd:6 lastrcv:6 lastack:6 pacing_rate 2.8Mbps delivery_rate 1.5Mbps app_limited busy:2016ms unacked:1 rcv_space:14600 minrtt:69.402
|
||||
|
||||
|
||||
通过该命令,我们可以发现这个TCP连接的cwnd为10。
|
||||
|
||||
如果你想要追踪拥塞窗口的实时变化信息,还有另外一个更好的办法:通过tcp_probe这个tracepoint来追踪:
|
||||
|
||||
/sys/kernel/debug/tracing/events/tcp/tcp_probe
|
||||
|
||||
|
||||
但是这个tracepoint只有4.16以后的内核版本才支持,如果你的内核版本比较老,你也可以使用tcp_probe这个内核模块(net/ipv4/tcp_probe.c)来进行追踪。
|
||||
|
||||
除了网络状况外,发送方还需要知道接收方的处理能力。如果接收方的处理能力差,那么发送方就必须要减缓它的发包速度,否则数据包都会挤压在接收方的缓冲区里,甚至被接收方给丢弃掉。接收方的处理能力是通过另外一个窗口——rwnd(接收窗口)来表示的。那么,接收方的rwnd又是如何影响发送方的行为呢?
|
||||
|
||||
接收方是如何影响发送方发送数据的?
|
||||
|
||||
同样地,我也画了一张简单的图,来表示接收方的rwnd是如何影响发送方的:
|
||||
|
||||
|
||||
|
||||
如上图所示,接收方在收到数据包后,会给发送方回一个ack,然后把自己的rwnd大小写入到TCP头部的win这个字段,这样发送方就能根据这个字段来知道接收方的rwnd了。接下来,发送方在发送下一个TCP segment的时候,会先对比发送方的cwnd和接收方的rwnd,得出这二者之间的较小值,然后控制发送的TCP segment个数不能超过这个较小值。
|
||||
|
||||
关于接收方的rwnd对发送方发送行为的影响,我们曾经遇到过这样的案例:业务反馈说Server向Client发包很慢,但是Server本身并不忙,而且网络看起来也没有问题,所以不清楚是什么原因导致的。对此,我们使用tcpdump在server上抓包后发现,Client响应的ack里经常出现win为0的情况,也就是Client的接收窗口为0。于是我们就去Client上排查,最终发现是Client代码存在bug,从而导致无法及时读取收到的数据包。
|
||||
|
||||
对于这种行为,我同样给Linux内核写了一个patch来监控它:tcp: add SNMP counter for zero-window drops 。这个patch里增加了一个新的SNMP 计数:TCPZeroWindowDrop。如果系统中发生了接收窗口太小而无法收包的情况,就会产生该事件,然后该事件可以通过/proc/net/netstat里的TCPZeroWindowDrop这个字段来查看。
|
||||
|
||||
因为TCP头部大小是有限制的,而其中的win这个字段只有16bit,win能够表示的大小最大只有65535(64K),所以如果想要支持更大的接收窗口以满足高性能网络,我们就需要打开下面这个配置项,系统中也是默认打开了该选项:
|
||||
|
||||
|
||||
net.ipv4.tcp_window_scaling = 1
|
||||
|
||||
|
||||
关于该选项更加详细的设计,你如果想了解的话,可以去参考RFC1323。
|
||||
|
||||
好了,关于TCP拥塞控制对业务网络性能的影响,我们就先讲到这里。
|
||||
|
||||
课堂总结
|
||||
|
||||
TCP拥塞控制是一个非常复杂的行为,我们在这节课里讲到的内容只是其中一些基础部分,希望这些基础知识可以让你对TCP拥塞控制有个大致的了解。我来总结一下这节课的重点:
|
||||
|
||||
|
||||
网络拥塞状况会体现在TCP连接的拥塞窗口(cwnd)中,该拥塞窗口会影响发送方的发包行为;
|
||||
接收方的处理能力同样会反馈给发送方,这个处理是通过rwnd来表示的。rwnd和cwnd会共同作用于发送方,来决定发送方最大能够发送多少TCP包;
|
||||
TCP拥塞控制的动态变化,可以通过tcp_probe这个tracepoint(对应4.16+的内核版本)或者是tcp_probe这个内核模块(对应4.16之前的内核版本)来进行实时观察,通过tcp_probe你能够很好地观察到TCP连接的数据传输状况。
|
||||
|
||||
|
||||
课后作业
|
||||
|
||||
通过ssh登录到服务器上,然后把网络关掉,过几秒后再打开,请问这个ssh连接还正常吗?为什么?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
133
专栏/Linux内核技术实战课/14案例篇TCP端到端时延变大,怎样判断是哪里出现了问题?.md
Normal file
133
专栏/Linux内核技术实战课/14案例篇TCP端到端时延变大,怎样判断是哪里出现了问题?.md
Normal file
@ -0,0 +1,133 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
14 案例篇 TCP端到端时延变大,怎样判断是哪里出现了问题?
|
||||
14 案例篇 TCP端到端时延变大,怎样判断是哪里出现了问题?
|
||||
|
||||
你好,我是邵亚方。
|
||||
|
||||
如果你是一名互联网从业者,那你对下面这个场景应该不会陌生:客户端发送请求给服务端,服务端将请求处理完后,再把响应数据发送回客户端,这就是典型的C/S(Client/Server)架构。对于这种请求-响应式的服务,我们不得不面对的问题是:
|
||||
|
||||
|
||||
如果客户端收到的响应时间变大了,那么这是客户端自身的问题呢,还是因为服务端处理得慢呢,又或者是因为网络有抖动呢?
|
||||
即使我们已经明确了是服务端或者客户端的问题,那么究竟是应用程序自身引起的问题呢,还是内核导致的问题呢?
|
||||
而且很多时候,这种问题往往是一天可能最多抖动一两次,我们很难去抓现场信息。
|
||||
|
||||
|
||||
为了更好地处理这类折磨人的问题,我也摸索了一些手段来进行实时追踪,既不会给应用程序和系统带来明显的开销,又可以在出现这些故障时能够把信息给抓取出来,从而帮助我们快速定位出问题所在。
|
||||
|
||||
因此,这节课我来分享下我在这方面的一些实践,以及解决过的一些具体案例。
|
||||
|
||||
当然,这些实践并不仅仅适用于这种C/S架构,对于其他应用程序,特别是对延迟比较敏感的应用程序,同样具备参考意义。比如说:
|
||||
|
||||
|
||||
如果我的业务运行在虚拟机里面,那怎么追踪呢?
|
||||
如果Client和Server之间还有一个Proxy,那怎么判断是不是Proxy引起的问题呢?
|
||||
|
||||
|
||||
那么,我们就先从生产环境中C/S架构的网络抖动案例说起。
|
||||
|
||||
如何分析C/S架构中的网络抖动问题?
|
||||
|
||||
|
||||
|
||||
上图就是一个典型的C/S架构,Client和Server之间可能经过了很复杂的网络,但是对于服务器开发者或者运维人员而言,这些中间网络可以理解为是一个黑盒,很难去获取这些网络的详细信息,更不用说到这些网络设备上去做debug了。所以,我在这里把它们都简化为了一个Router(路由器),然后Client和Server通过这个路由器来相互通信。比如互联网场景中的数据库服务(像MySQL)、http服务等都是这种架构。而当时给我们提需求来诊断网络抖动问题的也是MySQL业务。因此,接下来我们就以MySQL为例来进行具体讲解。
|
||||
|
||||
MySQL的业务方反馈说他们的请求偶尔会超时很长,但不清楚是什么原因引起了超时,对应到上图中,就是D点收到响应的时刻和A点发出请求的时刻,这个时间差会偶然性地有毛刺。在发生网络问题时,使用tcpdump来抓包是常用的分析手段,当你不清楚该如何来分析网络问题时,那就使用tcpdump先把事故现场保存下来吧。
|
||||
|
||||
如果你使用过tcpdump来分析问题,那你应该也吐槽过用tcpdump分析问题会很麻烦。如果你熟悉wireshark的话,就会相对容易一些了。但是对于大部分人而言呢,学习wirkshark的成本也是很高的,而且wireshark的命令那么多,把每一条命令都记清楚也是件很麻烦的事。
|
||||
|
||||
回到我们这个案例中,在MySQL发生抖动的时候,我们的业务人员也使用了tcpdump来抓包保存了现场,但是他们并不清楚该如何分析这些tcpdump信息。在我帮他们分析这些tcpdump信息的时候,也很难把tcpdump信息和业务抖动的时刻关联起来。这是因为虽然我们知道业务抖动发生的时刻,比如说21:00:00.000这个时刻,但是在这一时刻的附近可能会有非常多的TCP数据包,很难简单地依赖时间戳把二者关联起来。而且,更重要的原因是,我们知道TCP是数据流,上层业务的一个请求可能会被分为多个TCP包(TCP Segment),同样地,多个请求也可能被合并为一个TCP包。也就是说,TCP流是很难和应用数据关联起来的。这就是用tcpdump分析业务请求和响应的难点。
|
||||
|
||||
针对tcpdump难以分析应用协议的问题,有一个思路是,在tcpdump的时候把数据也保存下来,然后使用tcpdump再进一步去解析这些应用协议。但是你会发现,用这种方式来处理生产环境中的抖动问题是很不现实的,因为在生产环境中的 TCP 连接动辄数百上千条,我们往往并不清楚抖动发生在哪个TCP连接上。如果把这些TCP流都dump出来,data部分即使只dump与应用协议有关的数据,这对磁盘I/O也是个负担。那有什么好办法吗?
|
||||
|
||||
好办法还是需要和应用协议关联起来,不过我们可以把这些应用协议做一层抽象,从而可以更简单地来解析它们,甚至无需解析。对于MySQL而言呢,工具tcprstat就是来做这件事的。
|
||||
|
||||
tcprstat的大致原理是利用MySQL的request-response特征来简化对协议内容的处理。request-response是指一个请求到达MySQL后,MySQL处理完该请求,然后回response,Client侧收到response后再去发下一个request,然后MySQL收到下一个request并处理。也就是说这种模型是典型的串行方式,处理完了一个再去处理下一个。所以tcprstat就可以以数据包到达MySQL Server侧作为起始时间点,以MySQL将最后一个数据包发出去作为结束时间点,然后这二者的时间差就是RT(Response Time),这个过程大致如下图所示:
|
||||
|
||||
|
||||
|
||||
tcprstat会记录request的到达时间点,以及request发出去的时间点,然后计算出RT并记录到日志中。当时我们把tcprstat部署到MySQL server侧后,发现每一个RT值都很小,并没有延迟很大的情况,所以看起来服务端并没有问题。那么问题是否发生在Client这里呢?
|
||||
|
||||
在我们想要把tcprstat也部署在Client侧抓取信息时,发现它只支持在Server侧部署,所以我们对tcprstat做了一些改造,让它也可以部署在Client侧。
|
||||
|
||||
这种改造并不麻烦,因为在Client侧解析的也是MySQL协议,只是TCP流的方向跟Server侧相反,Client侧是发请求收响应,而Server侧是收请求发响应。
|
||||
|
||||
在改造完成后,我们就开始部署tcprstat来抓取抖动现场了。在业务发生抖动时,通过我们抓取到的信息显示,Client在收到响应包的时候就已经发生延迟了,也就是说问题同样也不是发生在Client侧。这就有些奇怪了,既然Client和Server都没有问题,难道是网络链路出现了问题?
|
||||
|
||||
为了明确这一点,我们就在业务低峰期使用ping包来检查网络是否存在问题。ping了大概数小时后,我们发现ping响应时间忽然变得很大,从不到1ms的时间增大到了几十甚至上百ms,然后很快又恢复正常。
|
||||
|
||||
根据这个信息,我们推断某个交换机可能存在拥塞,于是就联系交换机管理人员来分析交换机。在交换机管理人员对这个链路上的交换机逐一排查后,最终定位到一台接入交换机确实有问题,它会偶然地出现排队很长的情况。而之所以MySQL反馈有抖动,其他业务没有反馈,只是因为这个接入交换机上的其他业务并不关心抖动。在交换机厂商帮忙修复了这个问题后,就再也没有出现过这种偶发性的抖动了。
|
||||
|
||||
这个结果看似很简单,但是分析过程还是很复杂的。因为一开始我们并不清楚问题发生在哪里,只能一步步去排查,所以这个分析过程也花费了几天的时间。
|
||||
|
||||
交换机引起的网络抖动问题,只是我们分析过的众多抖动案例之一。除了这类问题外,我们还分析过很多抖动是由于Client侧存在问题或者Server侧存在问题。在分析了这么多抖动问题之后,我们就开始思考,能否针对这类问题来做一个自动化分析系统呢?而且我们部署运行tcprstat后,也发现它存在一些不足之处,主要是它的性能开销略大,特别是在TCP连接数较多的情况下,它的CPU利用率甚至能够超过10%,这难以满足我们生产环境中长时间运行的需要。
|
||||
|
||||
tcprstat会有这么高的CPU开销,原因其实与tcpdump是类似的,它在旁路采集数据后会拷贝到用户空间来处理,这个拷贝以及处理时间就比较消耗CPU。
|
||||
|
||||
为了满足生产环境的需求,我们在tcprstat的基础上,做了一个更加轻量级的分析系统。
|
||||
|
||||
如何轻量级地判断抖动发生在哪里?
|
||||
|
||||
我们的目标是在10Gb网卡的高并发场景下尽量地降低监控开销,最好可以控制在1%以内,而且不能给业务带来明显延迟。要想降低CPU开销,很多工作就需要在内核里面来完成,就跟现在很流行的eBPF这个追踪框架类似:内核处理完所有的数据,然后将结果返回给用户空间。
|
||||
|
||||
能够达到这个目标的方案大致有两种:一种是使用内核模块,另一种是使用轻量级的内核追踪框架。
|
||||
|
||||
使用内核模块的缺点是它的安装部署会很不方便,特别是在线上内核版本非常多的情况下,比如说,我们的线上既有CentOS-6的操作系统,也有CentOS-7的操作系统,每个操作系统又各自有很多小版本,以及我们自己发布的版本。统一线上的内核版本是件很麻烦的事,这会涉及到很多变更,不太现实。所以这种现状也就决定了,使用内核模块的方式需要付出比较高的维护成本。而且内核模块的易用性也很差,这会导致业务人员和运维人员排斥使用它,从而增加它的推广难度。基于这些考虑,我们最终选择了基于systemtap这个追踪框架来开发。没有选择eBPF的原因是,它对内核版本要求较高,而我们线上很多都是CentOS-7的内核。
|
||||
|
||||
基于systemtap实现的追踪框架大致如下图所示:
|
||||
|
||||
|
||||
|
||||
它会追踪每一个TCP流,TCP流对应到内核里的实现就是一个struct sock实例,然后记录TCP流经过A/B/C/D这四个点的时刻,依据这几个时间点,我们就可以得到下面的结论:
|
||||
|
||||
|
||||
如果C-B的时间差较大,那就说明Server侧有抖动,否则是Client或网络的问题;
|
||||
如果D-A的时间差较小,那就说明是Client侧问题,否则是Server或者网络的问题。
|
||||
|
||||
|
||||
这样在发生RT抖动时,我们就能够区分出抖动是发生在Client,Server,还是网络中了,这会大大提升分析定位问题的效率。在定位到问题出在哪里后,你就可以使用我们在“11讲”、“12讲”和“13讲”里讲到的知识点,再去进一步分析具体的原因是什么了。
|
||||
|
||||
在使用systemtap的过程中我们也踩了不少坑,在这里也分享给你,希望你可以避免:
|
||||
|
||||
|
||||
systemtap的加载过程是一个开销很大的过程,主要是CPU的开销。因为systemtap的加载会编译systemtap脚本,这会比较耗时。你可以提前将你的systemtap脚本编译为内核模块,然后直接加载该模块来避免CPU开销;
|
||||
systemtap有很多开销控制选项,你可以设置开销阈值来作为兜底方案,以防止异常情况下它占用太多CPU;
|
||||
systemtap进程异常退出后可能不会卸载systemtap模块,在你发现systemtap进程退出后,你需要检查它是否也把对应的内核模块给卸载了。如果没有,那你需要手动卸载一下,以免产生不必要的问题。
|
||||
|
||||
|
||||
C/S架构是互联网服务中比较典型的场景,那针对其他场景我们该如何来分析问题呢?接下来我们以虚拟机这种场景为例来看一下。
|
||||
|
||||
虚拟机场景下该如何判断抖动是发生在宿主机上还是虚拟机里?
|
||||
|
||||
随着云计算的发展,越来越多的业务开始部署在云上,很多企业或者使用自己定制的私有云,或者使用公有云。我们也有很多业务部署在自己的私有云中,既有基于KVM的虚拟机,也有基于Kubernetes和Docker的容器。以虚拟机为例,在Server侧发生抖动的时候,业务人员还想进一步知道,抖动是发生在Server侧的虚拟机内部,还是发生在Server侧的宿主机上。要想实现这个需求,我们只需要进一步扩展,再增加新的hook点,去记录TCP流经过虚拟机的时间点就好了,如下图所示:
|
||||
|
||||
|
||||
|
||||
这样我们就可以根据F和E的时间差来判断抖动是否发生在虚拟机内部。针对这个需求,我们对tcprstat也做了类似的改造,让它可以识别出抖动是否发生在虚拟机内部。这个改造也不复杂,tcprstat默认只处理目标地址为本机的数据包,不会处理转发包,所以我们让它支持混杂模式,然后就可以处理转发包了。当然,虚拟机的具体网络配置是千差万别的,你需要根据你的实际虚拟网络配置来做调整。
|
||||
|
||||
总之,希望你可以举一反三,根据你的实际业务场景来做合理的数据分析,而不要局限于我们这节课所列举的这几个具体场景。
|
||||
|
||||
课堂总结
|
||||
|
||||
我们这堂课以典型的C/S架构为例,分析了RT发生抖动时,该如何高效地识别出问题发生在哪里。我来总结一下这节课的重点:
|
||||
|
||||
|
||||
tcpdump是分析网络问题必须要掌握的工具,但是用它分析问题并不容易。在你不清楚该如何分析网络问题时,你可以先使用tcpdump把现场信息保存下来;
|
||||
TCP是数据流,如何把TCP流和具体的业务请求/响应数据包关联起来,是分析具体应用问题的前提。你需要结合你的业务模型来做合理的关联;
|
||||
RT抖动问题是很棘手的,你需要结合你的业务模型来开发一些高效的问题分析工具。如果你使用的是Redhat或者CentOS,那么你可以考虑使用systemtap;如果是Ubuntu,你可以考虑使用lttng。
|
||||
|
||||
|
||||
课后作业
|
||||
|
||||
结合这堂课的第一张图,在这张图中,请问是否可以用TCP流到达B点的时刻(到达Server这台主机的时间)减去TCP流经过A点的时刻(到达Client这台主机的时间)来做为网络耗时?为什么?
|
||||
|
||||
结合我们在“13讲“里讲到的RTT这个往返时延,你还可以进一步思考:是否可以使用RTT来作为这次网络耗时?为什么?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
159
专栏/Linux内核技术实战课/15分析篇如何高效地分析TCP重传问题?.md
Normal file
159
专栏/Linux内核技术实战课/15分析篇如何高效地分析TCP重传问题?.md
Normal file
@ -0,0 +1,159 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
15 分析篇 如何高效地分析TCP重传问题?
|
||||
你好,我是邵亚方。
|
||||
|
||||
我们在基础篇和案例篇里讲了很多问题,比如说RT抖动问题、丢包问题、无法建连问题等等。这些问题通常都会伴随着TCP重传,所以我们往往也会抓取TCP重传信息来辅助我们分析这些问题。
|
||||
|
||||
而且TCP重传也是一个信号,我们通常会利用这个信号来判断系统是否稳定。比如说,如果一台服务器的TCP重传率很高,那这个服务器肯定是存在问题的,需要我们及时采取措施,否则可能会产生更加严重的故障。
|
||||
|
||||
但是,TCP重传率分析并不是一件很容易的事,比如说现在某台服务器的TCP重传率很高,那究竟是什么业务在进行TCP重传呢?对此,很多人并不懂得如何来分析。所以,在这节课中,我会带你来认识TCP重传是怎么回事,以及如何来高效地分析它。
|
||||
|
||||
什么是TCP重传 ?
|
||||
|
||||
我在“开篇词”中举过一个TCP重传率的例子,如下图所示:
|
||||
|
||||
|
||||
|
||||
这是互联网企业普遍都有的TCP重传率监控,它是服务器稳定性的一个指标,如果它太高,就像上图中的那些毛刺一样,往往就意味着服务器不稳定了。那TCP重传率究竟表示什么呢?
|
||||
|
||||
其实TCP重传率是通过解析/proc/net/snmp这个文件里的指标计算出来的,这个文件里面和TCP有关的关键指标如下:
|
||||
|
||||
|
||||
|
||||
TCP重传率的计算公式如下:
|
||||
|
||||
|
||||
retrans = (RetransSegs-last RetransSegs) / (OutSegs-last OutSegs) * 100
|
||||
|
||||
|
||||
也就是说,单位时间内TCP重传包的数量除以TCP总的发包数量,就是TCP重传率。那我们继续看下这个公式中的RetransSegs和OutSegs是怎么回事,我画了两张示例图来演示这两个指标的变化:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
通过这两个示例图,你可以发现,发送端在发送一个TCP数据包后,会把该数据包放在发送端的发送队列里,也叫重传队列。此时,OutSegs会相应地加1,队列长度也为1。如果可以收到接收端对这个数据包的ACK,该数据包就会在发送队列中被删掉,然后队列长度变为0;如果收不到这个数据包的ACK,就会触发重传机制,我们在这里演示的就是超时重传这种情况,也就是说发送端在发送数据包的时候,会启动一个超时重传定时器(RTO),如果超过了这个时间,发送端还没有收到ACK,就会重传该数据包,然后OutSegs加1,同时RetransSegs也会加1。
|
||||
|
||||
这就是OutSegs和RetransSegs的含义:每发出去一个TCP包(包括重传包),OutSegs会相应地加1;每发出去一个重传包,RetransSegs会相应地加1。同时,我也在图中展示了重传队列的变化,你可以仔细看下。
|
||||
|
||||
除了上图中展示的超时重传外,还有快速重传机制。关于快速重传,你可以参考“13讲”,我就不在这里详细描述了。
|
||||
|
||||
明白了TCP重传是如何定义的之后,我们继续来看下哪些情况会导致TCP重传。
|
||||
|
||||
引起TCP重传的情况在整体上可以分为如下两类。
|
||||
|
||||
|
||||
丢包-
|
||||
TCP数据包在网络传输过程中可能会被丢弃;接收端也可能会把该数据包给丢弃;接收端回的ACK也可能在网络传输过程中被丢弃;数据包在传输过程中发生错误而被接收端给丢弃……这些情况都会导致发送端重传该TCP数据包。
|
||||
拥塞-
|
||||
TCP数据包在网络传输过程中可能会在某个交换机/路由器上排队,比如臭名昭著的Bufferbloat(缓冲膨胀);TCP数据包在网络传输过程中因为路由变化而产生的乱序;接收端回的ACK在某个交换机/路由器上排队……这些情况都会导致发送端再次重传该TCP数据包。
|
||||
|
||||
|
||||
总之,TCP重传可以很好地作为通信质量的信号,我们需要去重视它。
|
||||
|
||||
那么,当我们发现某个主机上TCP重传率很高时,该如何去分析呢?
|
||||
|
||||
分析TCP重传的常规手段
|
||||
|
||||
最常规的分析手段就是tcpdump,我们可以使用它把进出某个网卡的数据包给保存下来:
|
||||
|
||||
$ tcpdump -s 0 -i eth0 -w tcpdumpfile
|
||||
|
||||
|
||||
然后在Linux上我们可以使用tshark这个工具(wireshark的Linux版本)来过滤出TCP重传包:
|
||||
|
||||
$ tshark -r tcpdumpfile -R tcp.analysis.retransmission
|
||||
|
||||
|
||||
如果有重传包的话,就可以显示出来了,如下是一个TCP重传的示例:
|
||||
|
||||
3481 20.277303 10.17.130.20 -> 124.74.250.144 TCP 70 [TCP Retransmission] 35993 > https [SYN] Seq=0 Win=14600 Len=0 MSS=1460 SACK_PERM=1 TSval=3231504691 TSecr=0
|
||||
|
||||
3659 22.277070 10.17.130.20 -> 124.74.250.144 TCP 70 [TCP Retransmission] 35993 > https [SYN] Seq=0 Win=14600 Len=0 MSS=1460 SACK_PERM=1 TSval=3231506691 TSecr=0
|
||||
|
||||
8649 46.539393 58.216.21.165 -> 10.17.130.20 TLSv1 113 [TCP Retransmission] Change Cipher Spec, Encrypted Handshake Messag
|
||||
|
||||
|
||||
借助tcpdump,我们就可以看到TCP重传的详细情况。从上面这几个TCP重传信息中,我们可以看到,这是发生在10.17.130.20:35993 - 124.74.250.144: 443这个TCP连接上的重传;通过[SYN]这个TCP连接状态,可以看到这是发生在三次握手阶段的重传。依据这些信息,我们就可以继续去124.74.250.144这个主机上分析https这个服务为什么无法建立新的连接了。
|
||||
|
||||
但是,我们都知道tcpdump很重,如果直接在生产环境上进行采集的话,难免会对业务造成性能影响。那有没有更加轻量级的一些分析方法呢?
|
||||
|
||||
如何高效地分析TCP重传 ?
|
||||
|
||||
其实,就像应用程序实现一些功能需要调用对应的函数一样,TCP重传也需要调用特定的内核函数。这个内核函数就是tcp_retransmit_skb()。你可以把这个函数名字里的skb理解为是一个需要发送的网络包。那么,如果我们想要高效地追踪TCP重传情况,那么直接追踪该函数就可以了。
|
||||
|
||||
追踪内核函数最通用的方法是使用Kprobe,Kprobe的大致原理如下:
|
||||
|
||||
|
||||
|
||||
你可以实现一个内核模块,该内核模块中使用Kprobe在tcp_retransmit_skb这个函数入口插入一个probe,然后注册一个break_handler,这样在执行到tcp_retransmit_skb时就会异常跳转到注册的break_handler中,然后在break_handler中解析TCP报文(skb)就可以了,从而来判断是什么在重传。
|
||||
|
||||
如果你觉得实现内核模块比较麻烦,可以借助ftrace框架来使用Kprobe。Brendan Gregg实现的tcpretrans采用的就是这种方式,你也可以直接使用它这个工具来追踪TCP重传。不过,该工具也有一些缺陷,因为它是通过读取/proc/net/tcp这个文件来解析是什么在重传,所以它能解析的信息比较有限,而且如果TCP连接持续时间较短(比如短连接),那么该工具就无法解析出来了。另外,你在使用它时需要确保你的内核已经打开了ftrace的tracing功能,也就是/sys/kernel/debug/tracing/tracing_on中的内容需要为1;在CentOS-6上,还需要/sys/kernel/debug/tracing/tracing_enabled也为1。
|
||||
|
||||
$ cat /sys/kernel/debug/tracing/tracing_on
|
||||
1
|
||||
|
||||
|
||||
如果为0的话,你需要打开它们,例如:
|
||||
|
||||
$ echo 1 > /sys/kernel/debug/tracing/tracing_on
|
||||
|
||||
|
||||
然后在追踪结束后,你需要来关闭他们:
|
||||
|
||||
$ echo 0 > /sys/kernel/debug/tracing/tracing_on
|
||||
|
||||
|
||||
由于Kprobe是通过异常(Exception)这种方式来工作的,所以它还是有一些性能开销的,在TCP发包快速路径上还是要避免使用Kprobe。不过,由于重传路径是慢速路径,所以在重传路径上添加Kprobe也无需担心性能开销。
|
||||
|
||||
Kprobe这种方式使用起来还是略有些不便,为了让Linux用户更方便地观察TCP重传事件,4.16内核版本中专门添加了TCP tracepoint来解析TCP重传事件。如果你使用的操作系统是CentOS-7以及更老的版本,就无法使用该Tracepoint来观察了;如果你的版本是CentOS-8以及后续更新的版本,那你可以直接使用这个Tracepoint来追踪TCP重传,可以使用如下命令:
|
||||
|
||||
$ cd /sys/kernel/debug/tracing/events/
|
||||
$ echo 1 > tcp/tcp_retransmit_skb/enable
|
||||
|
||||
|
||||
然后你就可以追踪TCP重传事件了:
|
||||
|
||||
$ cat trace_pipe
|
||||
<idle>-0 [007] ..s. 265119.290232: tcp_retransmit_skb: sport=22 dport=62264 saddr=172.23.245.8 daddr=172.30.18.225 saddrv6=::ffff:172.23.245.8 daddrv6=::ffff:172.30.18.225 state=TCP_ESTABLISHED
|
||||
|
||||
|
||||
可以看到,当TCP重传发生时,该事件的基本信息就会被打印出来。多说一句,在最开始的版本中是没有“state=TCP_ESTABLISHED”这一项的。如果没有这一项,我们就无法识别该重传事件是不是发生在三次握手阶段了,所以我给内核贡献了一个PATCH来显示TCP连接状态,以便于问题分析,具体见tcp: expose sk_state in tcp_retransmit_skb tracepoint这个commit。
|
||||
|
||||
追踪结束后呢,你需要将这个Tracepoint给关闭:
|
||||
|
||||
$ echo 0 > tcp/tcp_retransmit_skb/enable
|
||||
|
||||
|
||||
Tracepoint这种方式不仅使用起来更加方便,而且它的性能开销比Kprobe要小,所以我们在快速路径上也可以使用它。
|
||||
|
||||
因为Tracepoint对TCP重传事件的支持,所以tcpretrans这个工具也跟着进行了一次升级换代。它通过解析该Tracepoint实现了对TCP重传事件的追踪,而不再使用之前的Kprobe方式,具体你可以参考bcc tcpretrans。再多说一句,Brendan Gregg在实现这些基于ebpf的TCP追踪工具之前也曾经跟我讨论过,所以我对他的这个工具才会这么熟悉。
|
||||
|
||||
我们针对TCP重传事件的分析就先讲到这里,希望能给你带来一些启发,去开发一些更加高效的工具来分析你遇到的TCP问题或者其他问题。
|
||||
|
||||
课堂总结
|
||||
|
||||
这堂课我们主要讲了TCP重传的一些知识,关于TCP重传你需要重点记住下面这几点:
|
||||
|
||||
|
||||
TCP重传率可以作为TCP通信质量的信号,如果它很高,那说明这个TCP连接很不稳定;
|
||||
产生TCP重传的问题主要是丢包和网络拥塞这两种情况;
|
||||
TCP重传时会调用特定的内核函数,我们可以追踪该函数的调用情况来追踪TCP重传事件;
|
||||
Kprobe是一个很通用的追踪工具,在低版本内核上,你可以使用这个方法来追踪TCP重传事件;
|
||||
Tracepoint是一个更加轻量级也更加方便的追踪TCP重传的工具,但是需要你的内核版本为4.16+;
|
||||
如果你想要更简单些,那你可以直接使用tcpretrans这个工具。
|
||||
|
||||
|
||||
课后作业
|
||||
|
||||
请问我们提到的tracepoint观察方式,或者tcpretrans这个工具,可以追踪收到的TCP重传包吗?为什么?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
138
专栏/Linux内核技术实战课/16套路篇如何分析常见的TCP问题?.md
Normal file
138
专栏/Linux内核技术实战课/16套路篇如何分析常见的TCP问题?.md
Normal file
@ -0,0 +1,138 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
16 套路篇 如何分析常见的TCP问题?
|
||||
你好,我是邵亚方。
|
||||
|
||||
对互联网服务而言, 网络问题是非常多的,而且很多问题的外在表现都是网络问题,这就需要我们从网络入手,分析清楚根本原因是什么。而要分析各种各样的网络问题,你必须掌握一些分析手段,这样在出现问题的时候,你就可以高效地找到原因。这节课我就带你来了解下TCP的常见问题,以及对应的分析套路。
|
||||
|
||||
在Linux上检查网络的常用工具
|
||||
|
||||
当服务器产生问题,而我们又不清楚问题和什么有关时,就需要运行一些工具来检查系统的整体状况。其中,dstat是我们常用的一种检查工具:
|
||||
|
||||
$ dstat
|
||||
--total-cpu-usage-- -dsk/total- -net/total- ---paging-- ---system--
|
||||
usr sys idl wai stl| read writ| recv send| in out | int csw
|
||||
8 1 91 0 0| 0 4096B|7492B 7757B| 0 0 |4029 7399
|
||||
8 1 91 0 0| 0 0 |7245B 7276B| 0 0 |4049 6967
|
||||
8 1 91 0 0| 0 144k|7148B 7386B| 0 0 |3896 6971
|
||||
9 2 89 0 0| 0 0 |7397B 7285B| 0 0 |4611 7426
|
||||
8 1 91 0 0| 0 0 |7294B 7258B| 0 0 |3976 7062
|
||||
|
||||
|
||||
如上所示,dstat会显示四类系统资源的整体使用情况和两个关键的系统指标。这四类系统资源分别是:CPU、磁盘I/O、 网络和内存。两个关键的系统指标是中断次数(int)和上下文切换次数(csw)。而每个系统资源又会输出它的一些关键指标,这里你需要注意以下几点:
|
||||
|
||||
|
||||
|
||||
如果你发现某一类系统资源对应的指标比较高,你就需要进一步针对该系统资源做更深入的分析。假设你发现网络吞吐比较高,那就继续观察网络的相关指标,你可以用dstat -h来查看,比如针对TCP,就可以使用dstat -tcp:
|
||||
|
||||
$ dstat --tcp
|
||||
------tcp-sockets-------
|
||||
lis act syn tim clo
|
||||
27 38 0 0 0
|
||||
27 38 0 0 0
|
||||
|
||||
|
||||
它会统计并显示系统中所有的TCP连接状态,这些指标的含义如下:
|
||||
|
||||
|
||||
|
||||
在得到了TCP连接的整体状况后,如果你想要看TCP连接的详细信息,你可以使用ss这个命令来继续观察。通过ss你可以查看到每个TCP连接都是什么样的:
|
||||
|
||||
$ ss -natp
|
||||
State Recv-Q Send-Q Local Address:Port Peer Address:Port
|
||||
LISTEN0 100 0.0.0.0:36457 0.0.0.0:* users:(("test",pid=11307,fd=17))
|
||||
LISTEN0 5 0.0.0.0:33811 0.0.0.0:* users:(("test",pid=11307,fd=19))
|
||||
ESTAB 0 0 127.0.0.1:57396 127.0.1.1:34751 users:(("test",pid=11307,fd=106))
|
||||
ESTAB 0 0 127.0.0.1:57384 127.0.1.1:34751 users:(("test",pid=11307,fd=100))
|
||||
|
||||
|
||||
如上所示,我们能查看到每个TCP连接的状态(State)、接收队列大小(Recv-Q)、发送队列大小(Send-Q)、本地IP和端口(Local Address:Port )、远端IP和端口(Peer Address:Port)以及打开该TCP连接的进程信息。
|
||||
|
||||
除了ss命令外,你也可以使用netstat命令来查看所有TCP连接的详细信息:
|
||||
|
||||
$ netstat -natp
|
||||
|
||||
|
||||
不过,我不建议你使用netstat,最好还是用ss。因为netstat不仅比ss慢,而且开销也大。netstat是通过直接读取/proc/net/下面的文件来解析网络连接信息的;而ss使用的是netlink方式,这种方式的效率会高很多。
|
||||
|
||||
netlink在解析时会依赖内核的一些诊断模块,比如解析TCP信息就需要tcp_diag这个诊断模块。如果诊断模块不存在,那么ss就无法使用netlink这种方式了,这个时候它就会退化到和netstat一样,也就是使用解析/proc/net/这种方式,当然了,它的效率也会相应变差。
|
||||
|
||||
另外,如果你去看netstat手册,通过man netstat,你会发现这样一句话“This program is obsolete. Replacement for netstat is ss”。所以,以后在分析网络连接问题时,我们尽量还是使用ss,而不是netstat。
|
||||
|
||||
netstat属于net-tools这个比较古老的工具集,而ss属于iproute2这个工具集。net-tools中的常用命令,几乎都可以用iproute2中的新命令来代替,比如:
|
||||
|
||||
|
||||
|
||||
除了查看系统中的网络连接信息外,我们有时候还需要去查看系统的网络状态,比如说系统中是否存在丢包,以及是什么原因引起了丢包,这时候我们就需要netstat -s或者它的替代工具nstat了:
|
||||
|
||||
$ nstat -z | grep -i drop
|
||||
TcpExtLockDroppedIcmps 0 0.0
|
||||
TcpExtListenDrops 0 0.0
|
||||
TcpExtTCPBacklogDrop 0 0.0
|
||||
TcpExtPFMemallocDrop 0 0.0
|
||||
TcpExtTCPMinTTLDrop 0 0.0
|
||||
TcpExtTCPDeferAcceptDrop 0 0.0
|
||||
TcpExtTCPReqQFullDrop 0 0.0
|
||||
TcpExtTCPOFODrop 0 0.0
|
||||
TcpExtTCPZeroWindowDrop 0 0.0
|
||||
TcpExtTCPRcvQDrop 0 0.0
|
||||
|
||||
|
||||
上面输出的这些信息就包括了常见的丢包原因,因为我的这台主机很稳定,所以你可以看到输出的结果都是0。
|
||||
|
||||
假如你通过这些常规检查手段没有发现异常,那你就需要考虑使用网络分析的必备工具——tcpdump了。
|
||||
|
||||
分析网络问题你必须要掌握的工具:tcpdump
|
||||
|
||||
tcpdump的使用技巧有很多,在这里我们不讲述这些使用技巧,而是讲述一下它的工作原理,以便于你理解tcpdump到底在干什么,以及它能够分析什么样的问题。
|
||||
|
||||
tcpdump的大致原理如下图所示:
|
||||
|
||||
|
||||
|
||||
tcpdump抓包使用的是libpacp这种机制。它的大致原理是:在收发包时,如果该包符合tcpdump设置的规则(BPF filter),那么该网络包就会被拷贝一份到tcpdump的内核缓冲区,然后以PACKET_MMAP的方式将这部分内存映射到tcpdump用户空间,解析后就会把这些内容给输出了。
|
||||
|
||||
通过上图你也可以看到,在收包的时候,如果网络包已经被网卡丢弃了,那么tcpdump是抓不到它的;在发包的时候,如果网络包在协议栈里被丢弃了,比如因为发送缓冲区满而被丢弃,tcpdump同样抓不到它。我们可以将tcpdump的能力范围简单地总结为:网卡以内的问题可以交给tcpdump来处理;对于网卡以外(包括网卡上)的问题,tcpdump可能就捉襟见肘了。这个时候,你需要在对端也使用tcpdump来抓包。
|
||||
|
||||
你还需要知道一点,那就是tcpdump的开销比较大,这主要在于BPF过滤器。如果系统中存在非常多的TCP连接,那么这个过滤的过程是非常耗时的,所以在生产环境中要慎用。但是,在出现网络问题时,如果你真的没有什么排查思路,那就想办法使用tcpdump来抓一下包吧,也许它的输出会给你带来一些意外的惊喜。
|
||||
|
||||
如果生产环境上运行着很重要的业务,你不敢使用tcpdump来抓包,那你就得去研究一些更加轻量级的追踪方式了。接下来,我给你推荐的轻量级追踪方式是TCP Tracepoints。
|
||||
|
||||
TCP疑难问题的轻量级分析手段:TCP Tracepoints
|
||||
|
||||
Tracepoint是我分析问题常用的手段之一,在遇到一些疑难问题时,我通常都会把一些相关的Tracepoint打开,把Tracepoint输出的内容保存起来,然后再在线下环境中分析。通常,我会写一些Python脚本来分析这些内容,毕竟Python在数据分析上还是很方便的。
|
||||
|
||||
对于TCP的相关问题,我也习惯使用这些TCP Tracepoints来分析问题。要想使用这些Tracepoints,你的内核版本需要为4.16及以上。这些常用的TCP Tracepoints路径位于/sys/kernel/debug/tracing/events/tcp/和/sys/kernel/debug/tracing/events/sock/,它们的作用如下表所示:
|
||||
|
||||
|
||||
|
||||
这里多说两句,表格里的tcp_rcv_space_adjust是我在分析RT抖动问题时贡献给内核的,具体你可以看net: introduce a new tracepoint for tcp_rcv_space_adjust这个commit。还有inet_sock_set_state,该Tracepoint也是我贡献给Linux内核的,具体可详见net: tracepoint: replace tcp_set_state tracepoint with inet_sock_set_state tracepoint。其实我对“inet_sock_set_state”这个名字不太满意,本来想使用“inet_sk_set_state”来命名的,因为后者更精炼,但是为了更好地匹配内核里的struct inet_sock结构体,我还是选择了现在这个略显臃肿的名字。
|
||||
|
||||
我们回到TCP Tracepoints这一轻量级的追踪方式。有一篇文章对它讲解得很好,就是Brendan Gregg写的TCP Tracepoints,这里面还详细介绍了基于Tracepoints的一些工具,如果你觉得用Python脚本解析TCP Tracepoints的输出有点麻烦,你可以直接使用里面推荐的那些工具。不过,你需要注意的是,这些工具都是基于ebpf来实现的,而ebpf有一个缺点,就是它在加载的时候CPU开销有些大。这是因为有一些编译工作比较消耗CPU,所以你在使用这些命令时要先看下你的系统CPU使用情况。当ebpf加载起来后,CPU开销就很小了,大致在1%以内。在停止ebpf工具的追踪时,也会有一些CPU开销,不过这个开销比加载时消耗的要小很多,但是你同样需要注意一下,以免影响到业务。
|
||||
|
||||
相比于tcpdump的臃肿,这些TCP Tracepoints就很轻量级了,你有必要用一用它们。
|
||||
|
||||
好了, 我们这节课就讲到这里。
|
||||
|
||||
课堂总结
|
||||
|
||||
我们讲了TCP问题分析的惯用套路,我再次强调一下这节课的重点:
|
||||
|
||||
|
||||
尽量不要使用netstat命令,而是多使用它的替代品ss,因为ss的性能开销更小,运行也更快;
|
||||
当你面对网络问题一筹莫展时,可以考虑使用tcpdump抓包看看,当系统中的网络连接数较大时,它对系统性能会产生比较明显的影响,所以你需要想办法避免它给业务带来实质影响;
|
||||
TCP Tracepoints是比较轻量级的分析方案,你需要去了解它们,最好试着去用一下它们。
|
||||
|
||||
|
||||
课后作业
|
||||
|
||||
请问tcpdump在解析内核缓冲区里的数据时,为什么使用PACKET_MMAP这种方式?你了解这种方式吗?这样做的好处是什么?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
130
专栏/Linux内核技术实战课/17基础篇CPU是如何执行任务的?.md
Normal file
130
专栏/Linux内核技术实战课/17基础篇CPU是如何执行任务的?.md
Normal file
@ -0,0 +1,130 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
17 基础篇 CPU是如何执行任务的?
|
||||
你好,我是邵亚方。
|
||||
|
||||
如果你做过性能优化的话,你应该有过这些思考,比如说:
|
||||
|
||||
|
||||
如何让CPU读取数据更快一些?
|
||||
同样的任务,为什么有时候执行得快,有时候执行得慢?
|
||||
我的任务有些比较重要,CPU如果有争抢时,我希望可以先执行这些任务,这该怎么办呢?
|
||||
多线程并行读写数据是如何保障同步的?
|
||||
…
|
||||
|
||||
|
||||
要想明白这些问题,你就需要去了解CPU是如何执行任务的,只有明白了CPU的执行逻辑,你才能更好地控制你的任务执行,从而获得更好的性能。
|
||||
|
||||
CPU是如何读写数据的 ?
|
||||
|
||||
我先带你来看下CPU的架构,因为你只有理解了CPU的架构,你才能更好地理解CPU是如何执行指令的。CPU的架构图如下所示:
|
||||
|
||||
|
||||
|
||||
你可以直观地看到,对于现代处理器而言,一个实体CPU通常会有两个逻辑线程,也就是上图中的Core 0和Core 1。每个Core都有自己的L1 Cache,L1 Cache又分为dCache和iCache,对应到上图就是L1d和L1i。L1 Cache只有Core本身可以看到,其他的Core是看不到的。同一个实体CPU中的这两个Core会共享L2 Cache,其他的实体CPU是看不到这个L2 Cache的。所有的实体CPU会共享L3 Cache。这就是典型的CPU架构。
|
||||
|
||||
相信你也看到,在CPU外还会有内存(DRAM)、磁盘等,这些存储介质共同构成了体系结构里的金字塔存储层次。如下所示:
|
||||
|
||||
|
||||
|
||||
在这个“金字塔”中,越往下,存储容量就越大,它的速度也会变得越慢。Jeff Dean曾经研究过CPU对各个存储介质的访问延迟,具体你可以看下latency里的数据,里面详细记录了每个存储层次的访问延迟,这也是我们在性能优化时必须要知道的一些延迟数据。你可不要小瞧它,在某些场景下,这些不同存储层次的访问延迟差异可能会导致非常大的性能差异。
|
||||
|
||||
我们就以Cache访问延迟(L1 0.5ns,L2 10ns)和内存访问延迟(100ns)为例,我给你举一个实际的案例来说明访问延迟的差异对性能的影响。
|
||||
|
||||
之前我在做网络追踪系统时,为了更方便地追踪TCP连接,我给Linux Kernel提交了一个PATCH来记录每个连接的编号,具体你可以参考这个commit:net: init sk_cookie for inet socket。该PATCH的大致作用是,在每次创建一个新的TCP连接时(关于TCP这部分知识,你可以去温习上一个模块的内容),它都会使用net namespace(网络命名空间)中的cookie_gen生成一个cookie给这个新建的连接赋值。
|
||||
|
||||
可是呢,在这个PATCH被合入后,Google工程师Eric Dumazet发现在他的SYN Flood测试中网络吞吐量会下降约24%。后来经过分析发现,这是因为net namespace中所有TCP连接都在共享cookie_gen。在高并发情况下,瞬间会有非常多的新建TCP连接,这时候cookie_gen就成了一个非常热的数据,从而被缓存在Cache中。如果cookie_gen的内容被修改的话,Cache里的数据就会失效,那么当有其他新建连接需要读取这个数据时,就不得不再次从内存中去读取。而你知道,内存的延迟相比Cache的延迟是大很多的,这就导致了严重的性能下降。这个问题就是典型的False Sharing,也就是Cache伪共享问题。
|
||||
|
||||
正因为这个PATCH给高并发建连这种场景带来了如此严重的性能损耗,所以它就被我们给回退(Revert)了,你具体可以看Revert “net: init sk_cookie for inet socket”这个commit。不过,cookie_gen对于网络追踪还是很有用的,比如说在使用ebpf来追踪cgroup的TCP连接时,所以后来Facebook的一个工程师把它从net namespace这个结构体里移了出来,改为了一个全局变量。
|
||||
|
||||
由于net namespace被很多TCP连接共享,因此这个结构体非常容易产生这类Cache伪共享问题,Eric Dumazet也在这里引入过一个Cache伪共享问题:net: reorder ‘struct net’ fields to avoid false sharing。
|
||||
|
||||
接下来,我们就来看一下Cache伪共享问题究竟是怎么回事。
|
||||
|
||||
|
||||
|
||||
如上图所示,两个CPU上并行运行着两个不同线程,它们同时从内存中读取两个不同的数据,这两个数据的地址在物理内存上是连续的,它们位于同一个Cache Line中。CPU从内存中读数据到Cache是以Cache Line为单位的,所以该Cache Line里的数据被同时读入到了这两个CPU的各自Cache中。紧接着这两个线程分别改写不同的数据,每次改写Cache中的数据都会将整个Cache Line置为无效。因此,虽然这两个线程改写的数据不同,但是由于它们位于同一个Cache Line中,所以一个CPU中的线程在写数据时会导致另外一个CPU中的Cache Line失效,而另外一个CPU中的线程在读写数据时就会发生cache miss,然后去内存读数据,这就大大降低了性能。
|
||||
|
||||
Cache伪共享问题可以说是性能杀手,我们在写代码时一定要留意那些频繁改写的共享数据,必要的时候可以将它跟其他的热数据放在不同的Cache Line中避免伪共享问题,就像我们在内核代码里经常看到的____cacheline_aligned所做的那样。
|
||||
|
||||
那怎么来观测Cache伪共享问题呢?你可以使用perf c2c这个命令,但是这需要较新版本内核支持才可以。不过,perf同样可以观察cache miss的现象,它对很多性能问题的分析还是很有帮助的。
|
||||
|
||||
CPU在写完Cache后将Cache置为无效(invalidate), 这本质上是为了保障多核并行计算时的数据一致性,一致性问题是Cache这个存储层次很典型的问题。
|
||||
|
||||
我们再来看内存这个存储层次中的典型问题:并行计算时的竞争,即两个CPU同时去操作同一个物理内存地址时的竞争。关于这类问题,我举一些简单的例子给你说明一下。
|
||||
|
||||
以C语言为例:
|
||||
|
||||
struct foo {
|
||||
int a;
|
||||
int b;
|
||||
};
|
||||
|
||||
|
||||
在这段示例代码里,我们定义了一个结构体,该结构体里的两个成员a和b在地址上是连续的。如果CPU 0去写a,同时CPU 1去读b的话,此时不会有竞争,因为a和b是不同的地址。不过,a和b由于在地址上是连续的,它们可能会位于同一个Cache Line中,所以为了防止前面提到的Cache伪共享问题,我们可以强制将b的地址设置为Cache Line对齐地址,如下:
|
||||
|
||||
struct foo {
|
||||
int a;
|
||||
int b ____cacheline_aligned;
|
||||
};
|
||||
|
||||
|
||||
接下来,我们看下另外一种情况:
|
||||
|
||||
struct foo {
|
||||
int a:1;
|
||||
int b:1;
|
||||
};
|
||||
|
||||
|
||||
这个示例程序定义了两个位域(bit field),a和b的地址是一样的,只是属于该地址的不同bit。在这种情况下,CPU 0去写a (a = 1),同时CPU 1去写b (b = 1),就会产生竞争。在总线仲裁后,先写的数据就会被后写的数据给覆盖掉。这就是执行RMW操作时典型的竞争问题。在这种场景下,就需要同步原语了,比如使用atomic操作。
|
||||
|
||||
关于位操作,我们来看一个实际的案例。这是我前段时间贡献给Linux内核的一个PATCH:psi: Move PF_MEMSTALL out of task->flags,它在struct task_struct这个结构体里增加了一个in_memstall的位域,在该PATCH里无需考虑多线程并行操作该位域时的竞争问题,你知道这是为什么吗?我将它作为一个课后思考题留给你,欢迎你在留言区与我讨论交流。为了让这个问题简单些,我给你一个提示:如果你留意过taskstruct这个结构体里的位域被改写的情况,你会发现只有current(当前在运行的线程)可以写,而其他线程只能去读。但是PF*这些全局flag可以被其他线程写,而不仅仅是current来写。
|
||||
|
||||
Linux内核里的task_struct结构体就是用来表示一个线程的,每个线程都有唯一对应的task_struct结构体,它也是内核进行调度的基本单位。我们继续来看下CPU是如何选择线程来执行的。
|
||||
|
||||
CPU是如何选择线程执行的 ?
|
||||
|
||||
你知道,一个系统中可能会运行着非常多的线程,这些线程数可能远超系统中的CPU核数,这时候这些任务就需要排队,每个CPU都会维护着自己运行队列(runqueue)里的线程。这个运行队列的结构大致如下图所示:
|
||||
|
||||
|
||||
|
||||
每个CPU都有自己的运行队列(runqueue),需要运行的线程会被加入到这个队列中。因为有些线程的优先级高,Linux内核为了保障这些高优先级任务的执行,设置了不同的调度类(Scheduling Class),如下所示:
|
||||
|
||||
|
||||
|
||||
这几个调度类的优先级如下:Deadline > Realtime > Fair。Linux内核在选择下一个任务执行时,会按照该顺序来进行选择,也就是先从dl_rq里选择任务,然后从rt_rq里选择任务,最后从cfs_rq里选择任务。所以实时任务总是会比普通任务先得到执行。
|
||||
|
||||
如果你的某些任务对延迟容忍度很低,比如说在嵌入式系统中就有很多这类任务,那就可以考虑将你的任务设置为实时任务,比如将它设置为SCHED_FIFO的任务:
|
||||
|
||||
|
||||
$ chrt -f -p 1 1327
|
||||
|
||||
|
||||
如果你不做任何设置的话,用户线程在默认情况下都是普通线程,也就是属于Fair调度类,由CFS调度器来进行管理。CFS调度器的目的是为了实现线程运行的公平性,举个例子,假设一个CPU上有两个线程需要执行,那么每个线程都将分配50%的CPU时间,以保障公平性。其实,各个线程之间执行时间的比例,也是可以人为干预的,比如在Linux上可以调整进程的nice值来干预,从而让优先级高一些的线程执行更多时间。这就是CFS调度器的大致思想。
|
||||
|
||||
好了,我们这堂课就先讲到这里。
|
||||
|
||||
课堂总结
|
||||
|
||||
我来总结一下这节课的知识点:
|
||||
|
||||
|
||||
要想明白CPU是如何执行任务的,你首先需要去了解CPU的架构;
|
||||
CPU的存储层次对大型软件系统的性能影响会很明显,也是你在性能调优时需要着重考虑的;
|
||||
高并发场景下的Cache Line伪共享问题是一个普遍存在的问题,你需要留意一下它;
|
||||
系统中需要运行的线程数可能大于CPU核数,这样就会导致线程排队等待CPU,这可能会导致一些延迟。如果你的任务对延迟容忍度低,你可以通过一些手段来人为干预Linux默认的调度策略。
|
||||
|
||||
|
||||
课后作业
|
||||
|
||||
这节课的作业就是我们前面提到的思考题:在psi: Move PF_MEMSTALL out of task->flags这个PATCH中,为什么没有考虑多线程并行操作新增加的位域(in_memstall)时的竞争问题?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
209
专栏/Linux内核技术实战课/18案例篇业务是否需要使用透明大页:水可载舟,亦可覆舟?.md
Normal file
209
专栏/Linux内核技术实战课/18案例篇业务是否需要使用透明大页:水可载舟,亦可覆舟?.md
Normal file
@ -0,0 +1,209 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
18 案例篇 业务是否需要使用透明大页:水可载舟,亦可覆舟?
|
||||
你好,我是邵亚方。
|
||||
|
||||
我们这节课的案例来自于我在多年以前帮助业务团队分析的一个稳定性问题。当时,业务团队反映说他们有一些服务器的CPU利用率会异常飙高,然后很快就能恢复,并且持续的时间不长,大概几秒到几分钟,从监控图上可以看到它像一些毛刺。
|
||||
|
||||
因为这类问题是普遍存在的,所以我就把该问题的定位分析过程分享给你,希望你以后遇到CPU利用率飙高的问题时,知道该如何一步步地分析。
|
||||
|
||||
CPU利用率是一个很笼统的概念,在遇到CPU利用率飙高的问题时,我们需要看看CPU到底在忙哪类事情,比如说CPU是在忙着处理中断、等待I/O、执行内核函数?还是在执行用户函数?这个时候就需要我们细化CPU利用率的监控,因为监控这些细化的指标对我们分析问题很有帮助。
|
||||
|
||||
细化CPU利用率监控
|
||||
|
||||
这里我们以常用的top命令为例,来看看CPU更加细化的利用率指标(不同版本的top命令显示可能会略有不同):
|
||||
|
||||
%Cpu(s): 12.5 us, 0.0 sy, 0.0 ni, 87.4 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
|
||||
top命令显示了us、sy、ni、id、wa、hi、si和st这几个指标,这几个指标之和为100。那你可能会有疑问,细化CPU利用率指标的监控会不会带来明显的额外开销?答案是不会的,因为CPU利用率监控通常是去解析/proc/stat文件,而这些文件中就包含了这些细化的指标。
|
||||
|
||||
我们继续来看下上述几个指标的具体含义,这些含义你也可以从top手册里来查看:
|
||||
|
||||
us, user : time running un-niced user processes
|
||||
sy, system : time running kernel processes
|
||||
ni, nice : time running niced user processes
|
||||
id, idle : time spent in the kernel idle handler
|
||||
wa, IO-wait : time waiting for I/O completion
|
||||
hi : time spent servicing hardware interrupts
|
||||
si : time spent servicing software interrupts
|
||||
st : time stolen from this vm by the hypervisor
|
||||
|
||||
|
||||
上述指标的具体含义以及注意事项如下:
|
||||
|
||||
|
||||
|
||||
在上面这几项中,idle和wait是CPU不工作的时间,其余的项都是CPU工作的时间。idle和wait的主要区别是,idle是CPU无事可做,而wait则是CPU想做事却做不了。你也可以将wait理解为是一类特殊的idle,即该CPU上有至少一个线程阻塞在I/O时的idle。
|
||||
|
||||
而我们通过对CPU利用率的细化监控发现,案例中的CPU利用率飙高是由sys利用率变高导致的,也就是说sys利用率会忽然飙高一下,比如在usr低于30%的情况下,sys会高于15%,持续几秒后又恢复正常。
|
||||
|
||||
所以,接下来我们就需要抓取sys利用率飙高的现场。
|
||||
|
||||
抓取sys利用率飙高现场
|
||||
|
||||
我们在前面讲到,CPU的sys利用率高,说明内核函数执行花费了太多的时间,所以我们需要采集CPU在sys飙高的瞬间所执行的内核函数。采集内核函数的方法有很多,比如:
|
||||
|
||||
|
||||
通过perf可以采集CPU的热点,看看sys利用率高时,哪些内核耗时的CPU利用率高;
|
||||
通过perf的call-graph功能可以查看具体的调用栈信息,也就是线程是从什么路径上执行下来的;
|
||||
通过perf的annotate功能可以追踪到线程是在内核函数的哪些语句上比较耗时;
|
||||
通过ftrace的function-graph功能可以查看这些内核函数的具体耗时,以及在哪个路径上耗时最大。
|
||||
|
||||
|
||||
不过,这些常用的追踪方式在这种瞬间消失的问题上是不太适用的,因为它们更加适合采集一个时间段内的信息。
|
||||
|
||||
那么针对这种瞬时的状态,我希望有一个系统快照,把当前CPU正在做的工作记录下来,然后我们就可以结合内核源码分析为什么sys利用率会高了。
|
||||
|
||||
有一个工具就可以很好地追踪这种系统瞬时状态,即系统快照,它就是sysrq。sysrq是我经常用来分析内核问题的工具,用它可以观察当前的内存快照、任务快照,可以构造vmcore把系统的所有信息都保存下来,甚至还可以在内存紧张的时候用它杀掉内存开销最大的那个进程。sysrq可以说是分析很多疑难问题的利器。
|
||||
|
||||
要想用sysrq来分析问题,首先需要使能sysyrq。我建议你将sysrq的所有功能都使能,你无需担心会有什么额外开销,而且这也没有什么风险。使能方式如下:
|
||||
|
||||
|
||||
$ sysctl -w kernel.sysrq = 1
|
||||
|
||||
|
||||
sysrq的功能被使能后,你可以使用它的-t选项把当前的任务快照保存下来,看看系统中都有哪些任务,以及这些任务都在干什么。使用方式如下:
|
||||
|
||||
|
||||
$ echo t > /proc/sysrq-trigger
|
||||
|
||||
|
||||
然后任务快照就会被打印到内核缓冲区,这些任务快照信息你可以通过dmesg命令来查看:
|
||||
|
||||
|
||||
$ dmesg
|
||||
|
||||
|
||||
当时我为了抓取这种瞬时的状态,写了一个脚本来采集,如下就是一个简单的脚本示例:
|
||||
|
||||
#!/bin/sh
|
||||
|
||||
while [ 1 ]; do
|
||||
top -bn2 | grep "Cpu(s)" | tail -1 | awk '{
|
||||
# $2 is usr, $4 is sys.
|
||||
if ($2 < 30.0 && $4 > 15.0) {
|
||||
# save the current usr and sys into a tmp file
|
||||
while ("date" | getline date) {
|
||||
split(date, str, " ");
|
||||
prefix=sprintf("%s_%s_%s_%s", str[2],str[3], str[4], str[5]);
|
||||
}
|
||||
|
||||
sys_usr_file=sprintf("/tmp/%s_info.highsys", prefix);
|
||||
print $2 > sys_usr_file;
|
||||
print $4 >> sys_usr_file;
|
||||
|
||||
# run sysrq
|
||||
system("echo t > /proc/sysrq-trigger");
|
||||
}
|
||||
}'
|
||||
sleep 1m
|
||||
done
|
||||
|
||||
|
||||
这个脚本会检测sys利用率高于15%同时usr较低的情况,也就是说检测CPU是否在内核里花费了太多时间。如果出现这种情况,就会运行sysrq来保存当前任务快照。你可以发现这个脚本设置的是1分钟执行一次,之所以这么做是因为不想引起很大的性能开销,而且当时业务团队里有几台机器差不多是一天出现两三次这种状况,有些机器每次可以持续几分钟,所以这已经足够了。不过,如果你遇到的问题出现的频率更低,持续时间更短,那就需要更加精确的方法了。
|
||||
|
||||
透明大页:水可载舟,亦可覆舟?
|
||||
|
||||
我们把脚本部署好后,就把问题现场抓取出来了。从dmesg输出的信息中,我们发现处于R状态的线程都在进行compcation(内存规整),线程的调用栈如下所示(这是一个比较古老的内核,版本为2.6.32):
|
||||
|
||||
java R running task 0 144305 144271 0x00000080
|
||||
ffff88096393d788 0000000000000086 ffff88096393d7b8 ffffffff81060b13
|
||||
ffff88096393d738 ffffea003968ce50 000000000000000e ffff880caa713040
|
||||
ffff8801688b0638 ffff88096393dfd8 000000000000fbc8 ffff8801688b0640
|
||||
|
||||
Call Trace:
|
||||
[<ffffffff81060b13>] ? perf_event_task_sched_out+0x33/0x70
|
||||
[<ffffffff8100bb8e>] ? apic_timer_interrupt+0xe/0x20
|
||||
[<ffffffff810686da>] __cond_resched+0x2a/0x40
|
||||
[<ffffffff81528300>] _cond_resched+0x30/0x40
|
||||
[<ffffffff81169505>] compact_checklock_irqsave+0x65/0xd0
|
||||
[<ffffffff81169862>] compaction_alloc+0x202/0x460
|
||||
[<ffffffff811748d8>] ? buffer_migrate_page+0xe8/0x130
|
||||
[<ffffffff81174b4a>] migrate_pages+0xaa/0x480
|
||||
[<ffffffff81169660>] ? compaction_alloc+0x0/0x460
|
||||
[<ffffffff8116a1a1>] compact_zone+0x581/0x950
|
||||
[<ffffffff8116a81c>] compact_zone_order+0xac/0x100
|
||||
[<ffffffff8116a951>] try_to_compact_pages+0xe1/0x120
|
||||
[<ffffffff8112f1ba>] __alloc_pages_direct_compact+0xda/0x1b0
|
||||
[<ffffffff8112f80b>] __alloc_pages_nodemask+0x57b/0x8d0
|
||||
[<ffffffff81167b9a>] alloc_pages_vma+0x9a/0x150
|
||||
[<ffffffff8118337d>] do_huge_pmd_anonymous_page+0x14d/0x3b0
|
||||
[<ffffffff8152a116>] ? rwsem_down_read_failed+0x26/0x30
|
||||
[<ffffffff8114b350>] handle_mm_fault+0x2f0/0x300
|
||||
[<ffffffff810ae950>] ? wake_futex+0x40/0x60
|
||||
[<ffffffff8104a8d8>] __do_page_fault+0x138/0x480
|
||||
[<ffffffff810097cc>] ? __switch_to+0x1ac/0x320
|
||||
[<ffffffff81527910>] ? thread_return+0x4e/0x76e
|
||||
[<ffffffff8152d45e>] do_page_fault+0x3e/0xa0
|
||||
[<ffffffff8152a815>] page_fault+0x25/0x30
|
||||
|
||||
|
||||
从该调用栈我们可以看出,此时这个java线程在申请THP(do_huge_pmd_anonymous_page)。THP就是透明大页,它是一个2M的连续物理内存。但是,因为这个时候物理内存中已经没有连续2M的内存空间了,所以触发了direct compaction(直接内存规整),内存规整的过程可以用下图来表示:
|
||||
|
||||
|
||||
|
||||
这个过程并不复杂,在进行compcation时,线程会从前往后扫描已使用的movable page,然后从后往前扫描free page,扫描结束后会把这些movable page给迁移到free page里,最终规整出一个2M的连续物理内存,这样THP就可以成功申请内存了。
|
||||
|
||||
direct compaction这个过程是很耗时的,而且在2.6.32版本的内核上,该过程需要持有粗粒度的锁,所以在运行过程中线程还可能会主动检查(_cond_resched)是否有其他更高优先级的任务需要执行。如果有的话就会让其他线程先执行,这便进一步加剧了它的执行耗时。这也就是sys利用率飙高的原因。关于这些,你也都可以从内核源码的注释来看到:
|
||||
|
||||
/*
|
||||
* Compaction requires the taking of some coarse locks that are potentially
|
||||
* very heavily contended. Check if the process needs to be scheduled or
|
||||
* if the lock is contended. For async compaction, back out in the event
|
||||
* if contention is severe. For sync compaction, schedule.
|
||||
* ...
|
||||
*/
|
||||
|
||||
|
||||
在我们找到了原因之后,为了快速解决生产环境上的这些问题,我们就把该业务服务器上的THP关掉了,关闭后系统变得很稳定,再也没有出现过sys利用率飙高的问题。关闭THP可以使用下面这个命令:
|
||||
|
||||
|
||||
$ echo never > /sys/kernel/mm/transparent_hugepage/enabled
|
||||
|
||||
|
||||
关闭了生产环境上的THP后,我们又在线下测试环境中评估了THP对该业务的性能影响,我们发现THP并不能给该业务带来明显的性能提升,即使是在内存不紧张、不会触发内存规整的情况下。这也引起了我的思考,THP究竟适合什么样的业务呢?
|
||||
|
||||
这就要从THP的目的来说起了。我们长话短说,THP的目的是用一个页表项来映射更大的内存(大页),这样可以减少Page Fault,因为需要的页数少了。当然,这也会提升TLB命中率,因为需要的页表项也少了。如果进程要访问的数据都在这个大页中,那么这个大页就会很热,会被缓存在Cache中。而大页对应的页表项也会出现在TLB中,从上一讲的存储层次我们可以知道,这有助于性能提升。但是反过来,假设应用程序的数据局部性比较差,它在短时间内要访问的数据很随机地位于不同的大页上,那么大页的优势就会消失。
|
||||
|
||||
因此,我们基于大页给业务做性能优化的时候,首先要评估业务的数据局部性,尽量把业务的热点数据聚合在一起,以便于充分享受大页的优势。以我在华为任职期间所做的大页性能优化为例,我们将业务的热点数据聚合在一起,然后将这些热点数据分配到大页上,再与不使用大页的情况相比,最终发现这可以带来20%以上的性能提升。对于TLB较小的架构(比如MIPS这种架构),它可以带来50%以上的性能提升。当然了,我们在这个过程中也对内核的大页代码做了很多优化,这里就不展开说了。
|
||||
|
||||
针对THP的使用,我在这里给你几点建议:
|
||||
|
||||
|
||||
不要将/sys/kernel/mm/transparent_hugepage/enabled配置为always,你可以将它配置为madvise。如果你不清楚该如何来配置,那就将它配置为never;
|
||||
如果你想要用THP优化业务,最好可以让业务以madvise的方式来使用大页,即通过修改业务代码来指定特定数据使用THP,因为业务更熟悉自己的数据流;
|
||||
很多时候修改业务代码会很麻烦,如果你不想修改业务代码的话,那就去优化THP的内核代码吧。
|
||||
|
||||
|
||||
好了,这节课就讲到这里。
|
||||
|
||||
课堂总结
|
||||
|
||||
我们来回顾一下这节课的要点:
|
||||
|
||||
|
||||
细化CPU利用率监控,在CPU利用率高时,你需要查看具体是哪一项指标比较高;
|
||||
sysrq是分析内核态CPU利用率高的利器,也是分析很多内核疑难问题的利器,你需要去了解如何使用它;
|
||||
THP可以给业务带来性能提升,但也可能会给业务带来严重的稳定性问题,你最好以madvise的方式使用它。如果你不清楚如何使用它,那就把它关闭。
|
||||
|
||||
|
||||
课后作业
|
||||
|
||||
我们这节课的作业有三种,你可以根据自己的情况进行选择:
|
||||
|
||||
|
||||
如果你是应用开发者,请问如何来观察系统中分配了多少THP?
|
||||
如果你是初级内核开发者,请问在进行compaction时,哪些页可以被迁移?哪些不可以被迁移?
|
||||
如果你是高级内核开发者,假设现在让你来设计让程序的代码段也可以使用hugetlbfs,那你觉得应该要做什么?
|
||||
|
||||
|
||||
欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
185
专栏/Linux内核技术实战课/19案例篇网络吞吐高的业务是否需要开启网卡特性呢?.md
Normal file
185
专栏/Linux内核技术实战课/19案例篇网络吞吐高的业务是否需要开启网卡特性呢?.md
Normal file
@ -0,0 +1,185 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
19 案例篇 网络吞吐高的业务是否需要开启网卡特性呢?
|
||||
你好,我是邵亚方。
|
||||
|
||||
通过上一讲我们对CPU利用率的细化,相信你已经知道,对于应用而言,它的目标是让CPU的开销尽量用在执行用户代码上,而非其他方面。usr利用率越高,说明CPU的效率越高。如果usr低,就说明CPU执行应用的效率不高。在第18讲里,我们还讲了CPU时间浪费在sys里的案例。那今天这一讲,我们一起来看看CPU在softirq上花费过多时间所引起的业务性能下降问题,这也是我们在生产环境中经常遇到的一类问题。接下来我会为你讲解相关案例,以及这类问题常用的观察方法。
|
||||
|
||||
中断与业务进程之间是如何相互干扰的?
|
||||
|
||||
这是我多年以前遇到的一个案例,当时业务反馈说为了提升QPS(Query per Second),他们开启了RPS(Receivce Packet Steering)来模拟网卡多队列,没想到开启RPS反而导致了QPS明显下降,不知道是什么原因。
|
||||
|
||||
其实,这类特定行为改变引起的性能下降问题相对好分析一些。最简单的方式就是去对比这个行为前后的性能数据。即使你不清楚RPS是什么,也不知道它背后的机制,你也可以采集需要的性能指标进行对比分析,然后判断问题可能出在哪里。这些性能指标包括CPU指标,内存指标,I/O指标,网络指标等,我们可以使用dstat来观察它们的变化。
|
||||
|
||||
在业务打开RPS之前的性能指标:
|
||||
|
||||
$ dstat
|
||||
You did not select any stats, using -cdngy by default.
|
||||
----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system--
|
||||
usr sys idl wai hiq siq| read writ| recv send| in out | int csw
|
||||
64 23 6 0 0 7| 0 8192B|7917k 12M| 0 0 | 27k 1922
|
||||
64 22 6 0 0 8| 0 0 |7739k 12M| 0 0 | 26k 2210
|
||||
61 23 9 0 0 7| 0 0 |7397k 11M| 0 0 | 25k 2267
|
||||
|
||||
|
||||
打开了RPS之后的性能指标:
|
||||
|
||||
$ dstat
|
||||
You did not select any stats, using -cdngy by default.
|
||||
----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system--
|
||||
usr sys idl wai hiq siq| read writ| recv send| in out | int csw
|
||||
62 23 4 0 0 12| 0 0 |7096k 11M| 0 0 | 49k 2261
|
||||
74 13 4 0 0 9| 0 0 |4003k 6543k| 0 0 | 31k 2004
|
||||
59 22 5 0 0 13| 0 4096B|6710k 10M| 0 0 | 48k 2220
|
||||
|
||||
|
||||
我们可以看到,打开RPS后,CPU的利用率有所升高。其中,siq即软中断利用率明显增加,int即硬中断频率也明显升高,而net这一项里的网络吞吐数据则有所下降。也就是说,在网络吞吐不升反降的情况下,系统的硬中断和软中断都明显增加。由此我们可以推断出,网络吞吐的下降应该是中断增加导致的结果。
|
||||
|
||||
那么,接下来我们就需要分析到底是什么类型的软中断和硬中断增加了,以便于找到问题的源头。
|
||||
|
||||
系统中存在很多硬中断,这些硬中断及其发生频率我们都可以通过/proc/interruptes这个文件来查看:
|
||||
|
||||
$ cat /proc/interrupts
|
||||
|
||||
|
||||
如果你想要了解某个中断的详细情况,比如中断亲和性,那你可以通过/proc/irq/[irq_num]来查看,比如:
|
||||
|
||||
$ cat /proc/irq/123/smp_affinity
|
||||
|
||||
|
||||
软中断可以通过/proc/softirq来查看:
|
||||
|
||||
$ cat /proc/softirqs
|
||||
|
||||
|
||||
当然了,你也可以写一些脚本来观察各个硬中断和软中断的发生频率,从而更加直观地查看是哪些中断发生得太频繁。
|
||||
|
||||
关于硬中断和软中断的区别,你可能多少有些了解,软中断是用来处理硬中断在短时间内无法完成的任务的。硬中断由于执行时间短,所以如果它的发生频率不高的话,一般不会给业务带来明显影响。但是由于内核里关中断的地方太多,所以进程往往会给硬中断带来一些影响,比如进程关中断时间太长会导致网络报文无法及时处理,进而引起业务性能抖动。
|
||||
|
||||
我们在生产环境中就遇到过这种关中断时间太长引起的抖动案例,比如cat /proc/slabinfo这个操作里的逻辑关中断太长,它会致使业务RT抖动。这是因为该命令会统计系统中所有的slab数量,并显示出来,在统计的过程中会关中断。如果系统中的slab数量太多,就会导致关中断的时间太长,进而引起网络包阻塞,ping延迟也会因此明显变大。所以,在生产环境中我们要尽量避免去采集/proc/slabinfo,否则可能会引起业务抖动。
|
||||
|
||||
由于/proc/slabinfo很危险,所以它的访问权限也从2.6.32版本时的0644更改为了后来的0400,也就是说只有root用户才能够读取它,这在一定程度上避免了一些问题。如果你的系统是2.6.32版本的内核,你就需要特别注意该问题。
|
||||
|
||||
如果你要分析因中断关闭时间太长而引发的问题,有一种最简单的方式,就是使用ftrace的irqsoff功能。它不仅可以统计出中断被关闭了多长时间,还可以统计出为什么会关闭中断。不过,你需要注意的是,irqsoff功能依赖于CONFIG_IRQSOFF_TRACER这个配置项,如果你的内核没有打开该配置项,那你就需要使用其他方式来去追踪了。
|
||||
|
||||
如何使用irqsoff呢?首先,你需要去查看你的系统是否支持了irqsoff这个tracer:
|
||||
|
||||
$ cat /sys/kernel/debug/tracing/available_tracers
|
||||
|
||||
|
||||
如果显示的内容包含了irqsoff,说明系统支持该功能,你就可以打开它进行追踪了:
|
||||
|
||||
$ echo irqsoff > /sys/kernel/debug/tracing/current_tracer
|
||||
|
||||
|
||||
接下来,你就可以通过/sys/kernel/debug/tracing/trace_pipe和trace这两个文件,来观察系统中的irqsoff事件了。
|
||||
|
||||
我们知道,相比硬中断,软中断的执行时间会长一些,而且它也会抢占正在执行进程的CPU,从而导致进程在它运行期间只能等待。所以,相对而言它会更容易给业务带来延迟。那我们怎么对软中断的执行频率以及执行耗时进行观测呢?你可以通过如下两个tracepoints来进行观测:
|
||||
|
||||
/sys/kernel/debug/tracing/events/irq/softirq_entry
|
||||
/sys/kernel/debug/tracing/events/irq/softirq_exit
|
||||
|
||||
|
||||
这两个tracepoint分别表示软中断的进入和退出,退出时间减去进入时间就是该软中断这一次的耗时。关于tracepoint采集数据的分析方式,我们在之前的课程里已经讲过多次,所以就不在这里继续描述了。
|
||||
|
||||
如果你的内核版本比较新,支持eBPF功能,那你同样可以使用bcc里的softirqs.py这个工具来进行观测。它会统计软中断的次数和耗时,这对我们分析软中断引起的业务延迟来说,是比较方便的。
|
||||
|
||||
为了避免软中断太过频繁,进程无法得到CPU而被饿死的情况,内核引入了ksoftirqd这个机制。如果所有的软中断在短时间内无法被处理完,内核就会唤醒ksoftirqd处理接下来的软中断。ksoftirqd与普通进程的优先级一样,也就是说它会和普通进程公平地使用CPU,这在一定程度上可以避免用户进程被饿死的情况,特别是对于那些更高优先级的实时用户进程而言。
|
||||
|
||||
不过,这也会带来一些问题。如果ksoftrirqd长时间得不到CPU,就会致使软中断的延迟变得很大,它引起的典型问题也是ping延迟。如果ping包无法在软中断里得到处理,就会被ksoftirqd处理。而ksoftirqd的实时性是很难得到保障的,可能需要等其他线程执行完,ksoftirqd才能得到执行,这就会导致ping延迟变得很大。
|
||||
|
||||
要观测ksoftirqd延迟的问题,你可以使用bcc里的runqlat.py。这里我们以网卡中断为例,它唤醒ksoftirqd的逻辑大致如下图所示:
|
||||
|
||||
|
||||
|
||||
我们具体来看看这个过程:软中断被唤醒后会检查一遍软中断向量表,逐个处理这些软中断;处理完一遍后,它会再次检查,如果又有新的软中断要处理,就会唤醒ksoftrqd来处理。ksoftirqd是per-cpu的内核线程,每个CPU都有一个。对于CPU1而言,它运行的是ksoftirqd/1这个线程。ksoftirqd/1被唤醒后会检查软中断向量表并进行处理。如果你使用ps来查看ksoftirqd/1的优先级,会发现它其实就是一个普通线程(对应的Nice值为0):
|
||||
|
||||
$ ps -eo "pid,comm,ni" | grep softirqd
|
||||
9 ksoftirqd/0 0
|
||||
16 ksoftirqd/1 0
|
||||
21 ksoftirqd/2 0
|
||||
26 ksoftirqd/3 0
|
||||
|
||||
|
||||
总之,在软中断处理这部分,内核需要改进的地方还有很多。
|
||||
|
||||
softirq是如何影响业务的?
|
||||
|
||||
在我们对硬中断和软中断进行观察后发现,使能RPS后增加了很多CAL(Function Call Interrupts)硬中断。CAL是通过软件触发硬中断的一种方式,可以指定CPU以及需要执行的中断处理程序。它也常被用来进行CPU间通信(IPI),当一个CPU需要其他CPU来执行特定中断处理程序时,就可以通过CAL中断来进行。
|
||||
|
||||
如果你对RPS的机制有所了解的话,应该清楚RPS就是通过CAL这种方式来让其他CPU去接收网络包的。为了验证这一点,我们可以通过mpstat这个命令来观察各个CPU利用率情况。
|
||||
|
||||
使能RPS之前的CPU利用率如下所示:
|
||||
|
||||
$ mpstat -P ALL 1
|
||||
Average: CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
|
||||
Average: all 70.18 0.00 19.28 0.00 0.00 5.86 0.00 0.00 0.00 4.68
|
||||
Average: 0 73.25 0.00 21.50 0.00 0.00 0.00 0.00 0.00 0.00 5.25
|
||||
Average: 1 58.85 0.00 14.46 0.00 0.00 23.44 0.00 0.00 0.00 3.24
|
||||
Average: 2 74.50 0.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 5.50
|
||||
Average: 3 74.25 0.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 4.75
|
||||
|
||||
|
||||
使能RPS之后各个CPU的利用率情况为:
|
||||
|
||||
$ mpstat -P ALL 1
|
||||
Average: CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
|
||||
Average: all 66.21 0.00 17.73 0.00 0.00 11.15 0.00 0.00 0.00 4.91
|
||||
Average: 0 68.17 0.00 18.33 0.00 0.00 7.67 0.00 0.00 0.00 5.83
|
||||
Average: 1 60.57 0.00 15.81 0.00 0.00 20.80 0.00 0.00 0.00 2.83
|
||||
Average: 2 69.95 0.00 19.20 0.00 0.00 7.01 0.00 0.00 0.00 3.84
|
||||
Average: 3 66.39 0.00 17.64 0.00 0.00 8.99 0.00 0.00 0.00 6.99
|
||||
|
||||
|
||||
我们可以看到,使能RPS之后,softirq在各个CPU之间更加均衡了一些,本来只有CPU1在处理softirq,使能后每个CPU都会处理softirq,并且CPU1的softirq利用率降低了一些。这就是RPS的作用:让网络收包软中断处理在各个CPU间更加均衡,以防止其在某个CPU上达到瓶颈。你可以看到,使能RPS后整体的%soft比原来高了很多。
|
||||
|
||||
理论上,处理网络收包软中断的CPU变多,那么在单位时间内这些CPU应该可以处理更多的网络包,从而提升系统整体的吞吐。可是,在我们的案例中,为什么会引起业务的QPS不升反降呢?
|
||||
|
||||
其实,答案同样可以从CPU利用率中得出。我们可以看到在使能RPS之前,CPU利用率已经很高了,达到了90%以上,也就是说CPU已经在超负荷工作了。而打开RPS,RPS又会消耗额外的CPU时间来模拟网卡多队列特性,这就会导致CPU更加超负荷地工作,从而进一步挤压用户进程的处理时间。因此,我们会发现,在打开RPS后%usr的利用率下降了一些。
|
||||
|
||||
我们知道%usr是衡量用户进程执行时间的一个指标,%usr越高意味着业务代码的运行时间越多。如果%usr下降,那就意味着业务代码的运行时间变少了,在业务没有进行代码优化的前提下,这显然是一个危险的信号。
|
||||
|
||||
由此我们可以发现,RPS的本质就是把网卡特性(网卡多队列)给upload到CPU,通过牺牲CPU时间来提升网络吞吐。如果你的系统已经很繁忙了,那么再使用该特性无疑是雪上加霜。所以,你需要注意,使用RPS的前提条件是:系统的整体CPU利用率不能太高。
|
||||
|
||||
找到问题后,我们就把该系统的RPS特性关闭了。如果你的网卡比较新,它可能会支持硬件多队列。硬件多队列是在网卡里做负载均衡,在这种场景下硬件多队列会很有帮助。我们知道,与upload相反的方向是offload,就是把CPU的工作给offload到网卡上去处理,这样可以把CPU解放出来,让它有更多的时间执行用户代码。关于网卡的offload特性,我们就不在此讨论了。
|
||||
|
||||
好了,这节课就讲到这里。
|
||||
|
||||
课堂总结
|
||||
|
||||
我们来简单回顾一下这节课的重点:
|
||||
|
||||
|
||||
硬中断、软中断以及ksoftirqd这个内核线程,它们与用户线程之间的关系是相对容易引发业务抖动的地方,你需要掌握它们的观测方式;
|
||||
硬中断对业务的主要影响体现在硬中断的发生频率上,但是它也容易受线程影响,因为内核里关中断的地方有很多;
|
||||
软中断的执行时间如果太长,就会给用户线程带来延迟,你需要避免你的系统中存在耗时较大的软中断处理程序。如果有的话,你需要去做优化;
|
||||
ksoftirqd的优先级与用户线程是一致的,因此,如果软中断处理函数是在ksoftirqd里执行的,那它可能会有一些延迟;
|
||||
RPS的本质是网卡特性unload到CPU,靠牺牲CPU时间来提升吞吐,你需要结合你的业务场景来评估是否需要开启它。如果你的网卡支持了硬件多队列,那么就可以直接使用硬件多队列了。
|
||||
|
||||
|
||||
课后作业
|
||||
|
||||
我们这节课的作业有两种,你可以根据自己的情况进行选择。
|
||||
|
||||
|
||||
入门:
|
||||
|
||||
|
||||
请问如果软中断以及硬中断被关闭的时间太长,会发生什么事?
|
||||
|
||||
|
||||
高级:
|
||||
|
||||
|
||||
如果想要追踪网络数据包在内核缓冲区停留了多长时间才被应用读走,你觉得应该如何来追踪?
|
||||
|
||||
欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
165
专栏/Linux内核技术实战课/20分析篇如何分析CPU利用率飙高问题?.md
Normal file
165
专栏/Linux内核技术实战课/20分析篇如何分析CPU利用率飙高问题?.md
Normal file
@ -0,0 +1,165 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
20 分析篇 如何分析CPU利用率飙高问题 ?
|
||||
你好,我是邵亚方。
|
||||
|
||||
如果你是一名应用开发者,那你应该知道如何去分析应用逻辑,对于如何优化应用代码提升系统性能也应该有自己的一套经验。而我们这节课想要讨论的是,如何拓展你的边界,让你能够分析代码之外的模块,以及对你而言几乎是黑盒的Linux内核。
|
||||
|
||||
在很多情况下,应用的性能问题都需要通过分析内核行为来解决,因此,内核提供了非常多的指标供应用程序参考。当应用出现问题时,我们可以查看到底是哪些指标出现了异常,然后再做进一步分析。不过,这些内核导出的指标并不能覆盖所有的场景,我们面临的问题可能更加棘手:应用出现性能问题,可是系统中所有的指标都看起来没有异常。相信很多人都为此抓狂过。那出现这种情况时,内核到底有没有问题呢,它究竟在搞什么鬼?这节课我就带你探讨一下如何分析这类问题。
|
||||
|
||||
我们知道,对于应用开发者而言,应用程序的边界是系统调用,进入到系统调用中就是Linux内核了。所以,要想拓展分析问题的边界,你首先需要知道该怎么去分析应用程序使用的系统调用函数。对于内核开发者而言,边界同样是系统调用,系统调用之外是应用程序。如果内核开发者想要拓展分析问题的边界,也需要知道如何利用系统调用去追踪应用程序的逻辑。
|
||||
|
||||
如何拓展你分析问题的边界?
|
||||
|
||||
作为一名内核开发者,我对应用程序逻辑的了解没有对内核的了解那么深。不过,当应用开发者向我寻求帮助时,尽管我对他们的应用逻辑一无所知,但这并不影响我对问题的分析,因为我知道如何借助分析工具追踪应用程序的逻辑。经过一系列追踪之后,我就能对应用程序有一个大概的认识。
|
||||
|
||||
我常用来追踪应用逻辑的工具之一就是strace。strace可以用来分析应用和内核的“边界”——系统调用。借助strace,我们不仅能够了解应用执行的逻辑,还可以了解内核逻辑。那么,作为应用开发者的你,就可以借助这个工具来拓展你分析应用问题的边界。
|
||||
|
||||
strace可以跟踪进程的系统调用、特定的系统调用以及系统调用的执行时间。很多时候,我们通过系统调用的执行时间,就能判断出业务延迟发生在哪里。比如我们想要跟踪一个多线程程序的系统调用情况,那就可以这样使用strace:
|
||||
|
||||
|
||||
$ strace -T -tt -ff -p pid -o strace.out
|
||||
|
||||
|
||||
不过,在使用strace跟踪进程之前,我希望你可以先明白strace的工作原理,这也是我们这节课的目的:你不只要知道怎样使用工具,更要明白工具的原理,这样在出现问题时,你就能明白该工具是否适用了。
|
||||
|
||||
了解工具的原理,不要局限于如何使用它
|
||||
|
||||
strace工具的原理如下图所示(我们以上面的那个命令为例来说明):
|
||||
|
||||
|
||||
|
||||
我们从图中可以看到,对于正在运行的进程而言,strace可以attach到目标进程上,这是通过ptrace这个系统调用实现的(gdb工具也是如此)。ptrace的PTRACE_SYSCALL会去追踪目标进程的系统调用;目标进程被追踪后,每次进入syscall,都会产生SIGTRAP信号并暂停执行;追踪者通过目标进程触发的SIGTRAP信号,就可以知道目标进程进入了系统调用,然后追踪者会去处理该系统调用,我们用strace命令观察到的信息输出就是该处理的结果;追踪者处理完该系统调用后,就会恢复目标进程的执行。被恢复的目标进程会一直执行下去,直到下一个系统调用。
|
||||
|
||||
你可以发现,目标进程每执行一次系统调用都会被打断,等strace处理完后,目标进程才能继续执行,这就会给目标进程带来比较明显的延迟。因此,在生产环境中我不建议使用该命令,如果你要使用该命令来追踪生产环境的问题,那就一定要做好预案。
|
||||
|
||||
假设我们使用strace跟踪到,线程延迟抖动是由某一个系统调用耗时长导致的,那么接下来我们该怎么继续追踪呢?这就到了应用开发者和运维人员需要拓展分析边界的时刻了,对内核开发者来说,这才算是分析问题的开始。
|
||||
|
||||
学会使用内核开发者常用的分析工具
|
||||
|
||||
我们以一个实际案例来说明吧。有一次,业务开发者反馈说他们用strace追踪发现业务的pread(2)系统调用耗时很长,经常会有几十毫秒(ms)的情况,甚至能够达到秒级,但是不清楚接下来该如何分析,因此让我帮他们分析一下。
|
||||
|
||||
因为已经明确了问题是由pread(2)这个系统调用引起的,所以对内核开发者而言,后续的分析就相对容易了。分析这类问题最合适的工具是ftrace,我们可以使用ftrace的function_trace功能来追踪pread(2)这个系统调用到底是在哪里耗费了这么长的时间。
|
||||
|
||||
要想追踪pread(2)究竟在哪里耗时长,我们就需要知道该系统调用对应的内核函数是什么。我们有两种途径可以方便地获取到系统调用对应的内核函数:
|
||||
|
||||
|
||||
查看include/linux/syscalls.h文件里的内核函数:
|
||||
|
||||
|
||||
你可以看到,与pread有关的函数有多个,由于我们的系统是64bit的,只需关注64bit相关的系统调用就可以了,所以我们锁定在ksys_pread64和sys_read64这两个函数上。通过该头文件里的注释我们能知道,前者是内核使用的,后者是导出给用户的。那么在内核里,我们就需要去追踪前者。另外,请注意,不同内核版本对应的函数可能不一致,我们这里是以最新内核代码(5.9-rc)为例来说明的。
|
||||
|
||||
|
||||
通过/proc/kallsyms这个文件来查找:
|
||||
|
||||
|
||||
|
||||
$ cat /proc/kallsyms | grep pread64-
|
||||
…-
|
||||
ffffffffa02ef3d0 T ksys_pread64-
|
||||
…
|
||||
|
||||
|
||||
/proc/kallsyms里的每一行都是一个符号,其中第一列是符号地址,第二列是符号的属性,第三列是符号名字,比如上面这个信息中的T就表示全局代码符号,我们可以追踪这类的符号。关于这些符号属性的含义,你可以通过man nm来查看。
|
||||
|
||||
接下来我们就使用ftrace的function_graph功能来追踪ksys_pread64这个函数,看看究竟是内核的哪里耗时这么久。function_graph的使用方式如下:
|
||||
|
||||
# 首先设置要追踪的函数
|
||||
$ echo ksys_pread64 > /sys/kernel/debug/tracing/set_graph_function
|
||||
|
||||
# 其次设置要追踪的线程的pid,如果有多个线程,那需要将每个线程都逐个写入
|
||||
$ echo 6577 > /sys/kernel/debug/tracing/set_ftrace_pid
|
||||
$ echo 6589 >> /sys/kernel/debug/tracing/set_ftrace_pid
|
||||
|
||||
# 将function_graph设置为当前的tracer,来追踪函数调用情况
|
||||
$ echo function_graph > /sys/kernel/debug/tracing/current_trace
|
||||
|
||||
|
||||
然后我们就可以通过/sys/kernel/debug/tracing/trace_pipe来查看它的输出了,下面就是我追踪到的耗时情况:
|
||||
|
||||
|
||||
|
||||
我们可以发现pread(2)有102ms是阻塞在io_schedule()这个函数里的,io_schedule()的意思是,该线程因I/O阻塞而被调度走,线程需要等待I/O完成才能继续执行。在function_graph里,我们同样也能看到pread**(2)**是如何一步步执行到io_schedule的,由于整个流程比较长,我在这里只把关键的调用逻辑贴出来:
|
||||
|
||||
21) | __lock_page_killable() {
|
||||
21) 0.073 us | page_waitqueue();
|
||||
21) | __wait_on_bit_lock() {
|
||||
21) | prepare_to_wait_exclusive() {
|
||||
21) 0.186 us | _raw_spin_lock_irqsave();
|
||||
21) 0.051 us | _raw_spin_unlock_irqrestore();
|
||||
21) 1.339 us | }
|
||||
21) | bit_wait_io() {
|
||||
21) | io_schedule() {
|
||||
|
||||
|
||||
我们可以看到,pread(2)是从__lock_page_killable这个函数调用下来的。当pread(2)从磁盘中读文件到内存页(page)时,会先lock该page,读完后再unlock。如果该page已经被别的线程lock了,比如在I/O过程中被lock,那么pread(2)就需要等待。等该page被I/O线程unlock后,pread(2)才能继续把文件内容读到这个page中。我们当时遇到的情况是:在pread(2)从磁盘中读取文件内容到一个page中的时候,该page已经被lock了,于是调用pread(2)的线程就在这里等待。这其实是合理的内核逻辑,没有什么问题。接下来,我们就需要看看为什么该page会被lock了这么久。
|
||||
|
||||
因为线程是阻塞在磁盘I/O里的,所以我们需要查看一下系统的磁盘I/O情况,我们可以使用iostat来观察:
|
||||
|
||||
|
||||
$ iostat -dxm 1
|
||||
|
||||
|
||||
追踪信息如下:
|
||||
|
||||
|
||||
|
||||
其中,sdb是业务pread(2)读取的磁盘所在的文件,通常情况下它的读写量很小,但是我们从上图中可以看到,磁盘利用率(%util)会随机出现比较高的情况,接近100%。而且avgrq-sz很大,也就是说出现了很多I/O排队的情况。另外,w/s比平时也要高很多。我们还可以看到,由于此时存在大量的I/O写操作,磁盘I/O排队严重,磁盘I/O利用率也很高。根据这些信息我们可以判断,之所以pread(2)读磁盘文件耗时较长,很可能是因为被写操作饿死导致的。因此,我们接下来需要排查到底是谁在进行写I/O操作。
|
||||
|
||||
通过iotop观察I/O行为,我们发现并没有用户线程在进行I/O写操作,写操作几乎都是内核线程kworker来执行的,也就是说用户线程把内容写在了Page Cache里,然后kwoker将这些Page Cache中的内容再同步到磁盘中。这就涉及到了我们这门课程第一个模块的内容了:如何观测Page Cache的行为。
|
||||
|
||||
自己写分析工具
|
||||
|
||||
如果你现在还不清楚该如何来观测Page Cache的行为,那我建议你再从头仔细看一遍我们这门课程的第一个模块,我在这里就不细说了。不过,我要提一下在Page Cache模块中未曾提到的一些方法,这些方法用于判断内存中都有哪些文件以及这些文件的大小。
|
||||
|
||||
常规方式是用fincore和mincore,不过它们都比较低效。这里有一个更加高效的方式:通过写一个内核模块遍历inode来查看Page Cache的组成。该模块的代码较多,我只说一下核心的思想,伪代码大致如下:
|
||||
|
||||
iterate_supers // 遍历super block
|
||||
iterate_pagecache_sb // 遍历superblock里的inode
|
||||
list_for_each_entry(inode, &sb->s_inodes, i_sb_list)
|
||||
// 记录该inode的pagecache大小
|
||||
nrpages = inode->i_mapping->nrpages;
|
||||
/* 获取该inode对应的dentry,然后根据该dentry来查找文件路径;
|
||||
* 请注意inode可能没有对应的dentry,因为dentry可能被回收掉了,
|
||||
* 此时就无法查看该inode对应的文件名了。
|
||||
*/
|
||||
dentry = dentry_from_inode(inode);
|
||||
dentry_path_raw(dentry, filename, PATH_MAX);
|
||||
|
||||
|
||||
使用这种方式不仅可以查看进程正在打开的文件,也能查看文件已经被进程关闭,但文件内容还在内存中的情况。所以这种方式分析起来会更全面。
|
||||
|
||||
通过查看Page Cache的文件内容,我们发现某些特定的文件占用的内存特别大,但是这些文件都是一些离线业务的文件,也就是不重要业务的文件。因为离线业务占用了大量的Page Cache,导致该在线业务的workingset大大减小,所以pread(2)在读文件内容时经常命中不了Page Cache,进而需要从磁盘来读文件,也就是说该在线业务存在大量的pagein和pageout。
|
||||
|
||||
至此,问题的解决方案也就有了:我们可以通过限制离线业务的Page Cache大小,来保障在线业务的workingset,防止它出现较多的refault。经过这样调整后,业务再也没有出现这种性能抖动了。
|
||||
|
||||
你是不是对我上面提到的这些名字感到困惑呢?也不清楚inode和Page Cache是什么关系?如果是的话,那就说明你没有好好学习我们这门课程的Page Cache模块,我建议你从头再仔细学习一遍。
|
||||
|
||||
好了,我们这节课就讲到这里。
|
||||
|
||||
课堂总结
|
||||
|
||||
我们这节课的内容,对于应用开发者和运维人员而言是有些难度的。我之所以讲这些有难度的内容,就是希望你可以拓展分析问题的边界。这节课的内容对内核开发者而言基本都是基础知识,如果你看不太明白,说明你对内核的理解还不够,你需要花更多的时间好好学习它。我研究内核已经有很多年了,尽管如此,我还是觉得自己对它的理解仍然不够深刻,需要持续不断地学习和研究,而我也一直在这么做。
|
||||
|
||||
我们现在回顾一下这节课的重点:
|
||||
|
||||
|
||||
strace工具是应用和内核的边界,如果你是一名应用开发者,并且想去拓展分析问题的边界,那你就需要去了解strace的原理,还需要了解如何去分析strace发现的问题;
|
||||
ftrace是分析内核问题的利器,你需要去了解它;
|
||||
你需要根据自己的问题来实现特定的问题分析工具,要想更好地实现这些分析工具,你必须掌握很多内核细节。
|
||||
|
||||
|
||||
课后作业
|
||||
|
||||
关于我们这节课的“自己写分析工具”这部分,我给你留一个作业,这也是我没有精力和时间去做的一件事:请你在sysrq里实现一个功能,让它可以显示出系统中所有R和D状态的任务,以此来帮助开发者分析系统load飙高的问题。
|
||||
|
||||
我在我们的内核里已经实现了该功能,不过在推给Linux内核时,maintainer希望我可以用另一种方式来实现。由于那个时候我在忙其他事情,这件事便被搁置了下来。如果你实现得比较好,你可以把它提交给Linux内核,提交的时候你也可以cc一下我([email protected])。对了,你在实现时,也可以参考我之前的提交记录:scheduler: enhancement to show_state_filter and SysRq。欢迎你在留言区与我讨论。
|
||||
|
||||
最后,感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友。
|
||||
|
||||
|
||||
|
||||
|
299
专栏/Linux内核技术实战课/加餐我是如何使用tracepoint来分析内核Bug的?.md
Normal file
299
专栏/Linux内核技术实战课/加餐我是如何使用tracepoint来分析内核Bug的?.md
Normal file
@ -0,0 +1,299 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
加餐 我是如何使用tracepoint来分析内核Bug的?
|
||||
你好,我是邵亚方。
|
||||
|
||||
我们这个系列课程的目标受众是应用开发者和运维人员,所以,你可以看到课程里的案例在分析应用问题的时候,都在尽量避免分析内核bug,避免把内核代码拿过来逐个函数地解析为什么会这样。我希望这个课程可以降低内核的门槛,让更多人可以更加容易地了解内核机制,从而更好地解决应用难题、提升应用性能。
|
||||
|
||||
不过,在我们这个课程的学习者中还是有一些内核开发者的,因此,我写了这篇加餐来分析内核bug,希望能把分析内核bug的一些经验分享给这些内核开发者们。
|
||||
|
||||
通过对课程的学习,你应该能发现,我对tracepoint和ftrace是极其推崇的。我对它推崇备至不是没有道理的,这节课我就带你来看下我是如何借助tracepoint来分析内核bug的。
|
||||
|
||||
炫技般存在的tracepoint内核源码
|
||||
|
||||
如果你看过tracepoint的内核代码,相信你一定对它炫技般存在的宏定义印象深刻。我在第一眼看到这些宏定义时,也是一脸懵逼,不知从何下手,但是很快我就看懂了。为了证明我看懂了,我还特意给tracepoint的这些宏定义又增加了一些定义,我增加的这个宏定义,其关键部分如下:
|
||||
|
||||
|
||||
|
||||
如果你能看明白这些,那就说明你对这些tracepoint宏的工作机制一清二楚了。当然,这节课我不是来剖析tracepoint内核源码的。如果你不懂tracepoint内核源码,也不妨碍你使用它,不过这对一名内核开发者而言终究是一件憾事。
|
||||
|
||||
因为我经常使用tracepoint,所以我对tracepoint的一些功能也比较上心。比如,最近在我的推动下,tracepoint模块的maintainer Steven Rostedt又给tracepoint增加了一个宏定义。我之所以推动Steven增加该宏,是为了让tracepoint函数可以在头文件中使用,以减少因为额外函数调用而带来的开销。有了这个新增的宏之后,你就可以方便地在头文件中使用tracepoint了。
|
||||
|
||||
接下来我要讲的这个内核bug,就是借助tracepoint来分析的。
|
||||
|
||||
内存申请在慢速路径上为什么会失败
|
||||
|
||||
有一次,业务人员反馈说他们在启动程序时会偶然失败,我查看内核日志后发现了下面这些报错信息(这个系统为CentOS-7,对应的内核版本为3.10):
|
||||
|
||||
kworker/31:0: page allocation failure: order:5, mode:0x104050
|
||||
CPU: 31 PID: 635928 Comm: kworker/31:0 Tainted: G
|
||||
0000000000104050 000000009a44a60e ffff882016b93808 ffffffff81686b13
|
||||
ffff882016b93898 ffffffff81187010 0000000000000000 ffff88207ffd8000
|
||||
0000000000000005 0000000000104050 ffff882016b93898 000000009a44a60e
|
||||
Call Trace:
|
||||
[<ffffffff81686b13>] dump_stack+0x19/0x1b
|
||||
[<ffffffff81187010>] warn_alloc_failed+0x110/0x180
|
||||
[<ffffffff816826a7>] __alloc_pages_slowpath+0x6b7/0x725
|
||||
[<ffffffff8118b5c5>] __alloc_pages_nodemask+0x405/0x420
|
||||
[<ffffffff811cf77a>] alloc_pages_current+0xaa/0x170
|
||||
[<ffffffff81185eee>] __get_free_pages+0xe/0x50
|
||||
[<ffffffff811db01e>] kmalloc_order_trace+0x2e/0xa0
|
||||
[<ffffffff811e05d9>] __kmalloc_track_caller+0x219/0x230
|
||||
[<ffffffff8119f78f>] krealloc+0x4f/0xa0
|
||||
[<ffffffffa07eebe6>] osdmap_set_max_osd+0x76/0x1d0 [libceph]
|
||||
[<ffffffffa07f14f6>] ceph_osdmap_decode+0x216/0x600 [libceph]
|
||||
[<ffffffffa07ecce4>] handle_one_map+0x224/0x250 [libceph]
|
||||
[<ffffffffa07ed98f>] ceph_osdc_handle_map+0x6cf/0x720 [libceph]
|
||||
[<ffffffffa07e3340>] dispatch+0x350/0x7c0 [libceph]
|
||||
[<ffffffffa07deecf>] try_read+0x4df/0x1260 [libceph]
|
||||
[<ffffffffa07dfd09>] ceph_con_workfn+0xb9/0x650 [libceph]
|
||||
[<ffffffff810a845b>] process_one_work+0x17b/0x470
|
||||
[<ffffffff810a9296>] worker_thread+0x126/0x410
|
||||
[<ffffffff810b0a4f>] kthread+0xcf/0xe0
|
||||
[<ffffffff81697118>] ret_from_fork+0x58/0x90
|
||||
Mem-Info:
|
||||
active_anon:13891624 inactive_anon:358552 isolated_anon:0#012 active_file:1652146 inactive_file:14520052 isolated_file:0#012 unevictable:0 dirty:472 writeback:0 unstable:0#012 slab_reclaimable:1071185 slab_unreclaimable:201037#012 mapped:53234 shmem:1046930 pagetables:31965 bounce:0#012 free:966225 free_pcp:185 free_cma:0
|
||||
Node 0 DMA free:15864kB min:48kB low:60kB high:72kB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:15948kB managed:15864kB mlocked:0kB dirty:0kB writeback:0kB mapped:0kB shmem:0kB slab_reclaimable:0kB slab_unreclaimable:0kB kernel_stack:0kB pagetables:0kB unstable:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:0 all_unreclaimable? yes
|
||||
lowmem_reserve[]: 0 1700 64161 64161
|
||||
Node 0 DMA32 free:261328kB min:5412kB low:6764kB high:8116kB active_anon:303220kB inactive_anon:2972kB active_file:51840kB inactive_file:239320kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:1952512kB managed:1741564kB mlocked:0kB dirty:0kB writeback:0kB mapped:1160kB shmem:15968kB slab_reclaimable:687588kB slab_unreclaimable:183756kB kernel_stack:6640kB pagetables:724kB unstable:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:0 all_unreclaimable? no
|
||||
lowmem_reserve[]: 0 0 62460 62460
|
||||
Node 0 Normal free:272880kB min:198808kB low:248508kB high:298212kB active_anon:26813396kB inactive_anon:912660kB active_file:2646440kB inactive_file:30619568kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:65011712kB managed:63960000kB mlocked:0kB dirty:36kB writeback:0kB mapped:144152kB shmem:2567180kB slab_reclaimable:1870364kB slab_unreclaimable:262608kB kernel_stack:12880kB pagetables:45840kB unstable:0kB bounce:0kB free_pcp:652kB local_pcp:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:32 all_unreclaimable? no
|
||||
lowmem_reserve[]: 0 0 0 0
|
||||
Node 1 Normal free:3315332kB min:205324kB low:256652kB high:307984kB active_anon:28449880kB inactive_anon:518576kB active_file:3910304kB inactive_file:27221320kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:67108864kB managed:66056916kB mlocked:0kB dirty:1852kB writeback:0kB mapped:67624kB shmem:1604572kB slab_reclaimable:1726788kB slab_unreclaimable:357784kB kernel_stack:18928kB pagetables:81296kB unstable:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:0 all_unreclaimable? no
|
||||
lowmem_reserve[]: 0 0 0 0
|
||||
Node 0 DMA: 0*4kB 1*8kB (U) 1*16kB (U) 1*32kB (U) 1*64kB (U) 1*128kB (U) 1*256kB (U) 0*512kB 1*1024kB (U) 1*2048kB (M) 3*4096kB (M) = 15864kB
|
||||
Node 0 DMA32: 36913*4kB (UEM) 14087*8kB (UEM) 44*16kB (UEM) 17*32kB (UEM) 0*64kB 0*128kB 0*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 261596kB
|
||||
Node 0 Normal: 69629*4kB (UEM) 411*8kB (UEM) 1*16kB (E) 3*32kB (E) 0*64kB 0*128kB 0*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 281916kB
|
||||
Node 1 Normal: 241701*4kB (UEM) 240734*8kB (UEM) 24010*16kB (UEM) 990*32kB (UEM) 175*64kB (UEM) 20*128kB (UEM) 3*256kB (EM) 0*512kB 0*1024kB 0*2048kB 0*4096kB = 3323044kB
|
||||
Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=1048576kB
|
||||
Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=2048kB
|
||||
Node 1 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=1048576kB
|
||||
|
||||
|
||||
从上面的日志可以看出,这是在__alloc_pages_slowpath()分配内存失败,所以有了这个打印:
|
||||
|
||||
__alloc_pages_slowpath
|
||||
{
|
||||
...
|
||||
nopage:
|
||||
// 这里打印的错误日志
|
||||
warn_alloc_failed(gfp_mask, order, NULL);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
此时申请的内存大小是order 5,也就是32个连续页。紧接着,我们可以看到各个node具体内存使用情况的打印,该机器共有2个node:
|
||||
|
||||
Node 0 DMA free:15864kB min:48kB
|
||||
Node 0 DMA32 free:261328kB min:5412kB
|
||||
Node 0 Normal free:272880kB min:198808kB
|
||||
Node 1 Normal free:3315332kB min:205324kB
|
||||
|
||||
|
||||
从中我们可以发现,各个zone的free内存都大于min,而且相差不止32个page。也就是说,从free内存的大小来看,各个zone都是可以满足需求的。那么,为什么此时会申请内存失败呢?
|
||||
|
||||
接下来,我们一起分析失败的原因。
|
||||
|
||||
逐一排查可能的情况
|
||||
|
||||
对于3.10版本的内核而言,在内存分配慢速路径里失败,原因可以分为以下三种情况:
|
||||
|
||||
|
||||
特殊的GFP flags导致;
|
||||
进程自身的状态;
|
||||
reclaim和compact无法满足需求。
|
||||
|
||||
|
||||
现在,我们来逐一排查究竟是哪种情况导致的。
|
||||
|
||||
GFP flags
|
||||
|
||||
此时的GFP flags是0x104050,对应于下面这几项:
|
||||
|
||||
#define ___GFP_WAIT 0x10u
|
||||
#define ___GFP_IO 0x40u
|
||||
#define ___GFP_COMP 0x4000u
|
||||
#define ___GFP_KMEMCG 0x100000u
|
||||
|
||||
|
||||
看到这里,你是否思考过:为什么不直接在内核日志里打印出这些GFP flags呢?如果你思考了,那么恭喜你,你具备内核开发者的特质;如果你没有思考过,那么你需要加强这方面的思考:我觉得内核这里有点不好,我得改变它。
|
||||
|
||||
我觉得内核日志里打印这些数字不如直接打印对应的GFP flags好,然后我就去查看最新的内核代码,发现这部分已经在新版本的内核里被修改过了,看来其他的内核开发者与我的想法一致。当然,这也说明了使用老版本的内核做开发是一件多么憋屈的事,因为你会发现你在老版本内核里分析清楚的内核bug,早已在新版本中被别人给fix了,这大大限制了我们的发挥空间。
|
||||
|
||||
通过前面的调用栈,我们可以知道申请内存是在osdmap_set_max_osd()这个函数中进行的,它对应的内核代码如下:
|
||||
|
||||
osdmap_set_max_osd
|
||||
addr = krealloc(map->osd_addr, max*sizeof(*addr), GFP_NOFS);
|
||||
if (!addr)
|
||||
return -ENOMEM;
|
||||
|
||||
|
||||
我们看到这里的GFP flags为GFP_NOFS,它的定义如下:
|
||||
|
||||
#define GFP_NOFS (__GFP_WAIT | __GFP_IO)
|
||||
|
||||
|
||||
这与内核日志里的GFP flags是对得上的。然后我们去比较goto nopage;的情况,可以发现这几个flag不会导致这种情况,也就是不会导致nopage。
|
||||
|
||||
关于GFP_NOFS的作用,我在这里大致说明一下。它的作用是为了防止某些路径上触发直接内存回收时,回收到正在进行I/O的page从而引起死锁。那什么情况下可能会引起死锁呢?你可以参考一下我尚未完成的PATCH:xfs: avoid deadlock when trigger memory reclaim in ->writepages。这个链接里描述的问题在3.10版本以及最新版本的内核上都存在,之所以我还没有完成该PATCH,是因为它依赖于我的另外一组PATCH,而我目前很少有精力去写它们。具体的逻辑你可以看下这个PATCH的代码以及描述,我就不在这里细说了。
|
||||
|
||||
现在,我们排除了GFP flags产生nopage的可能,接下来继续看看另外两种情况。
|
||||
|
||||
进程标记current->flag
|
||||
|
||||
在warn_alloc_failed里,我们可以看到,如果是因为进程自身的状态有问题(比如正在退出,或者正在oom过程中等),那么SHOW_MEM_FILTER_NODES这个标记位就会被清掉,然后各个zone的具体内存信息就不会被打印出来了。
|
||||
|
||||
因此,内存申请在慢速路径上失败也不是因为这个原因。
|
||||
|
||||
reclaim和compact无法满足需求
|
||||
|
||||
现在就只有“reclaim和compact无法满足需求”这一种情况了。
|
||||
|
||||
根据前面的信息我们可以知道,此时free的内存其实挺多,可以排除reclaim无法满足需求的情况。所以,只剩下compcat这个因素了。也就是说,由于申请的是连续页,而系统中没有足够的连续页,所以compact也满足不了需求,进而导致分配内存失败。
|
||||
|
||||
那么,在什么情况下compact会失败呢?我们继续来看代码:
|
||||
|
||||
try_to_compact_pages
|
||||
int may_enter_fs = gfp_mask & __GFP_FS;
|
||||
int may_perform_io = gfp_mask & __GFP_IO;
|
||||
|
||||
if (!order || !may_enter_fs || !may_perform_io)
|
||||
return rc;
|
||||
|
||||
|
||||
我们可以看到__GFP_FS没有被设置,无法进行compaction,直接返回了COMPACT_SKIPPED。
|
||||
|
||||
明白了问题所在后,我们需要在生产环境上验证一下,看看到底是不是这个原因。
|
||||
|
||||
使用tracepoint分析生产环境
|
||||
|
||||
tracepoint是一种性能开销比较小的追踪手段,在生产环境上使用它,不会给业务带来明显的性能影响。
|
||||
|
||||
在使用tracepoint分析前,我们需要明确它可以追踪什么事件。
|
||||
|
||||
因为我们目前的问题是compact fail,所以我们需要去追踪direct compact这个事件。新版本的内核里有compact相关的tracepoint,我们直接打开对应的tracepoint就可以了。不过,3.10版本的内核没有compact相关的tracepoint,这个时候我们就需要借助kprobe机制了,最简单的方式是利用ftrace提供的kprobe_events功能或者是ftrace的function tracer功能。我们以function tracer为例来追踪direct compact:
|
||||
|
||||
$ echo function > /sys/kernel/debug/tracing/current_tracer
|
||||
$ echo __alloc_pages_direct_compact > /sys/kernel/debug/tracing/set_ftrace_filter
|
||||
|
||||
|
||||
这样,当发生direct compact时,在trace_pipe中就会有相应的信息输出。不过,这显示不了compact的细节,我们还需要结合其他手段查看是否进行了compact。方式有很多,在这里,我们结合源码来看一种比较简单的方式:
|
||||
|
||||
__alloc_pages_direct_compact
|
||||
try_to_compact_pages
|
||||
/* Check if the GFP flags allow compaction */
|
||||
if (!order || !may_enter_fs || !may_perform_io)
|
||||
return rc;
|
||||
|
||||
// 如果可以进行direct compact的话,会有COMPACTSTALL事件
|
||||
count_compact_event(COMPACTSTALL);
|
||||
|
||||
|
||||
从源码中我们能看到,如果可以进行direct compact的话,会有相应的COMPACTSTALL事件,而该事件会统计在/proc/vmstat中:
|
||||
|
||||
$ cat /proc/vmstat | grep compact
|
||||
|
||||
|
||||
这样我们就可以知道调用__alloc_pages_direct_compact时,有没有真正进行compact的行为。另外,在compact的过程中还会伴随着direct reclaim,我们也需要看下direct reclaim的细节,看看direct claim能否成功回收内存。我们可以借助direct reclaim的tracepoint来查看,该tracepoint在3.10版本的内核里已经有了:
|
||||
|
||||
$ echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_vmscan_direct_reclaim_begin/enable
|
||||
|
||||
$ echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_vmscan_direct_reclaim_end/enable
|
||||
|
||||
|
||||
在追踪这些事件之后,我们就可以看到direct compact前后的信息了。
|
||||
|
||||
direct compact前的vmstat指标为:
|
||||
|
||||
$ cat /proc/vmstat | grep compact
|
||||
compact_migrate_scanned 690141298
|
||||
compact_free_scanned 186406569096
|
||||
comoact_isolated 332972232
|
||||
compact_stall 87914
|
||||
compact_fail 40935
|
||||
compact_success 46979
|
||||
|
||||
|
||||
compact过程中的事件:
|
||||
|
||||
-
|
||||
compact结束后的vmstat指标为:
|
||||
|
||||
$ cat /proc/vmstat | grep compact
|
||||
compact_migrate_scanned 690141298
|
||||
compact_free_scanned 186406569096
|
||||
comoact_isolated 332972232
|
||||
compact_stall 87914
|
||||
compact_fail 40935
|
||||
compact_success 46979
|
||||
|
||||
|
||||
我们可以看到,在compact前后,compcat_stall这个指标没有任何变化,也就是说try_to_compact_pages中没有进行真正的compact行为;从direct reclaim事件中的nr_reclaimed=3067可以看到,此时可以回收到足够的page,也就是说direct reclaim没有什么问题;同样,direct reclaim的“order=5, gfp_flags=GFP_NOFS|GFP_COMP” 也与之前日志里的信息吻合。因此,这些追踪数据进一步印证了我们之前的猜测:__GFP_FS没有被设置,无法进行compaction。
|
||||
|
||||
我们现在再次观察申请内存失败时的日志,可以发现,此时free list上其实有当前order的内存(因为没有GFP_DMA,所以会先从NORMAL zone申请内存):
|
||||
|
||||
Node 0 Normal: 69629*4kB (UEM) 411*8kB (UEM) 1*16kB (E) 3*32kB (E) 0*64kB 0*128kB 0*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 281916kB
|
||||
Node 1 Normal: 241701*4kB (UEM) 240734*8kB (UEM) 24010*16kB (UEM) 990*32kB (UEM) 175*64kB (UEM) 20*128kB (UEM) 3*256kB (EM) 0*512kB 0*1024kB 0*2048kB 0*4096kB = 3323044kB
|
||||
|
||||
|
||||
我们能看到node 1大于order 5(即128K)的连续物理内存有很多,那为什么不能从这些zone->free_area[order]里分配内存呢?
|
||||
|
||||
答案就在于该zone的水位不满足要求(见__zone_watermark_ok()):
|
||||
|
||||
__zone_watermark_ok
|
||||
{
|
||||
...
|
||||
for (o = 0; o < order; o++) {
|
||||
free_pages -= z->free_area[o].nr_free << o;
|
||||
min >>= 1;
|
||||
if (free_pages <= min)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
对于node 1而言,4K/8K/16K/32K/64K内存和为3319716kB,该zone的watermark min为205324kB,该node的总内存为3323044KB,我们可以简单地进行如下比较:
|
||||
|
||||
(3323044-3319716) 为3328KB
|
||||
(205324kB >> 5) 为6416KB
|
||||
|
||||
|
||||
因此,order 5无法满足水位。
|
||||
|
||||
根据上述这些追踪信息,我们可以得出这样的结论:在内存分配慢速路径上失败,是因为当前的内存申请请求无法进行碎片整理,而系统中low order的内存又太多,从而导致了该order的内存申请失败。
|
||||
|
||||
解决方案
|
||||
|
||||
因为此时normal zone的order=5的free page依然有很多,而整体的watermark又满足需求,所以不应该让order=5的内存申请失败,这是一个内核缺陷。我去查看upstream的最新代码时,发现该缺陷已经被修改过了。你可以看到,使用老版本的内核做开发的确是一件很憋屈的事。
|
||||
|
||||
关于upstream的修改方案,你可以参考这个patch以及它的一些依赖:
|
||||
|
||||
mm, page_alloc: only enforce watermarks for order-0 allocations
|
||||
|
||||
如果你无法通过修改内核来解决这一问题的话,那就采取一些规避措施。
|
||||
|
||||
|
||||
规避方案一:-
|
||||
通过drop_caches清理掉pagecache,不过这种做法也有很多缺陷,具体你可以参考我们这个课程的pagecache模块,我在这里就不细说了。
|
||||
规避方案二:-
|
||||
调整碎片指数,确保紧急情况下能够申请到连续页。内存碎片指数对应的文件是/proc/sys/vm/extfrag_threshold,它的默认值是500 ,我们可以适当降低该值,不过在这种情况下,降低它的效果并不会很明显。
|
||||
规避方案三:-
|
||||
手动compact,你可以通过写入/proc/sys/vm/compact_memory来触发compact。
|
||||
规避方案四:-
|
||||
调整 vm.vfs_cache_pressure,降低它的值,让pagecache被回收得更多,以此来减少freelist中order为0的page个数。
|
||||
|
||||
|
||||
至此,我们这个问题的分析就结束了。
|
||||
|
||||
总结
|
||||
|
||||
在比较新的内核上,我们也可以通过eBPF来分析内核bug,比如在我们的生产环境中,我就通过eBPF追踪过fadvise的内核bug引起的业务抖动问题,具体bug你可以看看我贡献给内核的这个PATCH: mm,fadvise: improve the expensive remote LRU cache draining after FADV_DONTNEED,这也再次印证了我的观点:内核开发者只有在新版本的内核里做开发,才会有更多的发挥空间。
|
||||
|
||||
另外,虽然eBPF很强大,但是它依然难以替代ftrace在我心中的地位。
|
||||
|
||||
|
||||
|
||||
|
35
专栏/Linux内核技术实战课/结束语第一次看内核代码,我也很懵逼.md
Normal file
35
专栏/Linux内核技术实战课/结束语第一次看内核代码,我也很懵逼.md
Normal file
@ -0,0 +1,35 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
结束语 第一次看内核代码,我也很懵逼
|
||||
你好,我是邵亚方。
|
||||
|
||||
我们的专栏就要接近尾声了,一路走来,非常感谢你的支持。我在这里想跟你分享一下我学习Linux内核的经历,希望我的个人经历能够给你带来一些启发。
|
||||
|
||||
我在从事Linux内核开发工作之前,几乎没有接触过Linux内核源码,只是对Linux操作系统略微了解一些,这还得益于大学期间Ubuntu的流行。那个时候我对什么都感到好奇,看到Ubuntu忽然流行起来,就在自己的电脑上也安装了Windows+Ubuntu双系统,天天折腾Linux命令和一些Ubuntu的使用技巧。然后没过多久,我就觉得自己是一个Linux专家了,开始在一些论坛上“指指点点”,觉得自己无所不能。
|
||||
|
||||
我们说,从0到1是一个从无到有的过程,对于发明创造来说这很难,但对于学习新东西而言,从0到1就相对容易了。不过,我那个时候把“学习”当成了“发明创造”,认为自己跨越了从0到1的阶段,就算是突破了学习Linux最难的部分,再往后就会顺其自然。
|
||||
|
||||
直到我开始实现一个Linux内核特性,才意识到自己对Linux的认知有多么肤浅。我之前掌握的那些Linux知识,只是让我从0迈出了一小步,而后面实际上还有99步要走。好在我及时意识到了自己的狂妄自大,开始埋头对照着Linux内核源码学习《深入Linux内核》这本书。在我的师傅手把手的指导以及周围同事的热心帮助下,我终于实现了Linux内核特性。
|
||||
|
||||
后来,在我能够独立实现Linux内核特性后,我又开始飘了,认为自己是Linux内核专家了。没错,我又在犯之前的错误,以为掌握一个新技能,只要从0迈向1就是突破了最难的部分,剩下的路就会一马平川。
|
||||
|
||||
这种狂妄自大一直伴随着我,直到我开始参与Linux社区的工作。当我与来自世界各地的开发者们讨论问题时,我再一次发现自己对Linux内核的认知有多肤浅。这并不是说我周围的同事不如社区里的开发者,而是我的同事们会在我遇到困难时帮助我一起想办法解决,与Linux社区的开发者们打交道则不同,我需要自己解决他们的质疑。
|
||||
|
||||
当然,Linux内核社区是很开放的,里面也有很多既热心又谦逊的人帮助你。与他们打交道越多,讨论的越多,你就越会觉得自己对Linux内核的认知很肤浅。有时候,为了反驳他们的质疑,甚至只是为了单纯指出他们逻辑上的缺陷,我会去看大量的内核代码,写大量的内核代码以及大量的测试用例……每当Linux内核子系统的maintainer承认他们的观点是错的,而我的观点是正确的时候,我都会感觉很快乐,因为自己的努力没有白费。
|
||||
|
||||
不过,即便如此,我并不觉得自己对Linux内核的理解已经到了多么深刻的地步,因为我知道我还在从1到99的道路上艰难前进着,我也不知道自己到底走了多少步,不过,这对我来说已经不重要了。俗话说,事不过三,你会发现我的心态已经变了,我不再犯之前的错误了。
|
||||
|
||||
我只是想告诉你,对于Linux的学习者而言,从0到1真的是最简单的一步,往后的每一步会越来越难。走完99步之后,你可能才开始面临最艰难的那一步。也许当你从第99步正式跨越到第100步时,就能够发明创造一个新的东西了?就像Linus向社区发布Linux内核的第一个版本一样,他从0到1创造了Linux。而我们不断地对它学习、研究,在壮大Linux的同时,也在走着自己的0到99步。至于我们从99迈向100时究竟会实现怎样的突破,我并不清楚,因为我自己还没有达到这样的境界,不过,未来的你和我或许有机会能验证它。
|
||||
|
||||
最后,再次感谢你一路以来的支持与陪伴,我们后会有期!
|
||||
|
||||
《Linux内核技术实战课》就要结束了,这里有一份毕业问卷,题目不多,希望你能花两分钟填一下。十分期待能听到你说一说,你对这个课程的想法和建议。
|
||||
|
||||
](https://jinshuju.net/f/mwe1E9)
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user