first commit

This commit is contained in:
张乾
2024-10-16 06:37:41 +08:00
parent 633f45ea20
commit 206fad82a2
3590 changed files with 680090 additions and 0 deletions

View 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内核理解到什么程度了呢你可以把自己当下的起点或者疑惑记录下来等全部学完这个课程再来回顾相信你会有不一样的体会。

View 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 CachePage 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会很容易引起性能抖动。
除了SwapCachedPage 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里面的数据是如何变化的 欢迎在留言区分享你的看法。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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 CachePage Cache才会容易引发那么多问题。在接下来的案例篇里我们就来看看究竟会引发什么样子的问题以及你正确的分析思路是什么样子的。
课后作业
因为每个人的关注点都不一样,对问题的理解也不一样。假如你是一个应用开发者,你会更加关注应用的性能和稳定性;假如你是一个运维人员,你会更加关注系统的稳定性;假如你是初学内核的开发者,你会想要关注内核的实现机制。
所以我留了不同的作业题主题是围绕“Inactive与Active Page Cache的关系”当然了对应的难度也不同
如果你是一名应用开发者那么我想问问你为什么第一次读写某个文件Page Cache是Inactive的如何让它变成Active的呢在什么情况下Active的又会变成Inactive的呢明白了这个问题你会对应用性能调优有更加深入的理解。
如果你是一名运维人员那么建议你思考一下系统中有哪些控制项可以影响Inactive与Active Page Cache的大小或者二者的比例
如果你是一名初学内核的开发者那么我想问你对于匿名页而言当产生一个匿名页后它会首先放在Active链表上而对于文件页而言当产生一个文件页后它会首先放在Inactive链表上。请问为什么会这样子这是合理的吗欢迎在留言区分享你的看法。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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
其中CPU0112435的local node为node 0而CPU12233647的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来判断是否有了直接内存回收。欢迎在留言区分享你的看法。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View File

@ -0,0 +1,210 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 案例篇 如何处理Page Cache容易回收引起的业务性能问题
你好我是邵亚方。我们在前一节课讲了Page Cache难以回收导致的load飙高问题这类问题是很直观的相信很多人都遭遇过。这节课我们则是来讲相反的一些问题即Page Cache太容易回收而引起的一些问题。
这类问题因为不直观所以陷阱会很多,应用开发者和运维人员会更容易踩坑,也正因为这类问题不直观,所以他们往往是一而再再而三地中招之后,才搞清楚问题到底是怎么回事。
我把大家经常遇到的这类问题做个总结,大致可以分为两方面:
误操作而导致Page Cache被回收掉进而导致业务性能下降明显
内核的一些机制导致业务Page Cache被回收从而引起性能下降。
如果你的业务对Page Cache比较敏感比如说你的业务数据对延迟很敏感或者再具体一点你的业务指标对TP9999分位要求较高那你对于这类性能问题应该多多少少有所接触。当然这并不意味着业务对延迟不敏感你就不需要关注这些问题了关注这类问题会让你对业务行为理解更深刻。
言归正传,我们来看下发生在生产环境中的案例。
对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 3slab被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()就退出了在进程退出后这部分内存还被保护吗为什么欢迎在留言区分享你的看法
感谢你的阅读如果你认为这节课的内容有收获也欢迎把它分享给你的朋友我们下一讲见

View 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是如何引起问题的我们还需要掌握更加专业的分析手段专业的分析工具有ftraceebpfperf等。
当然了,这些专业工具的学习成本也相对略高一些,但你不能觉得它难、成本高,就不学了,因为掌握了这些分析工具后,再遇到疑难杂症,你分析起来会更加得心应手。
为了让你在遇到问题时更加方便地找到合适的分析工具我借用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里面记录的PSIPressure-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这个进程触发了compactionbegin和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需要高版本内核的支持。
这是我沉淀下来的定位问题的方法。也希望你在遇到问题时不逃避,刨根问底寻找根本原因是什么,相信你一定也会有自己的问题分析方法论,然后在出现问题时能够快速高效地找到原因。
课后作业
假设现在内存紧张, 有很多进程都在进行直接内存回收,如何统计出来都是哪些进程在进行直接内存回收呢?欢迎在留言区分享你的看法。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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将要请求的虚拟地址传给MMUMemory Management Unit内存管理单元然后MMU先在高速缓存TLBTranslation 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)
...
除了nMajMajor 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、DirtyAddress和Kbytes分别表示起始地址和虚拟内存的大小RSSResident 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很小那可能是进程不停地在申请内存但是却没有对这些内存进行任何的读写操作即虚拟地址空间存在内存泄漏。
同样地,我希望你自己可以写一些测试用例来观察这些指标的变化。
课后作业
课后你可以写一些测试程序,分别分配我们这堂课提到的四种不同类型的内存,观察进程地址空间的变化,以及系统内存指标的变化。欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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来进行观察。欢迎在留言区分享你的看法。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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是指匿名共享内存即进程以mmapMAP_ANON|MAP_SHARED这种方式来申请的内存。你可能会有疑问进程以这种方式来申请的内存不应该是属于进程的RESresident比如下面这个简单的示例
#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有关。我们知道磁盘的速度是远远低于内存的有些应用程序为了提升性能会避免将一些无需持续化存储的数据写入到磁盘而是把这部分临时数据写入到内存中然后定期或者在不需要这部分数据时清理掉这部分内容来释放出内存。在这种需求下就产生了一种特殊的Shmemtmpfs。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我也给社区贡献了一个patchmm, 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和进程被杀的顺序是什么关系。欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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脚本来追踪内核内存的申请和释放。欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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。欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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包后就会发出ACKServer收到该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之前的内核中默认都是1285.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上是60sTCP_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的三次握手和四次挥手过程巩固今天的学习内容。欢迎在留言区分享你的看法。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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拥塞控制的话那就需要将它调整为fqfair queue, 公平队列):
net.core.default_qdisc = fq
经过IP层后数据包再往下就会进入到网卡了然后通过网卡发送出去。至此你需要发送出去的数据就走完了TCP/IP协议栈然后正常地发送给对端了。
接下来,我们来看下数据包是怎样收上来的,以及在接收的过程中会受哪些配置项的影响。
TCP数据包的接收过程会受什么影响
TCP数据包的接收过程同样也可以用一张图来简单表示
从上图可以看出TCP数据包的接收流程在整体上与发送流程类似只是方向是相反的。数据包到达网卡后就会触发中断IRQ来告诉CPU读取这个数据包。但是在高性能网络场景下数据包的数量会非常大如果每来一个数据包都要产生一个中断那CPU的处理效率就会大打折扣所以就产生了NAPINew API这种机制让CPU一次性地去轮询poll多个数据包以批量处理的方式来提升效率降低网卡中断带来的性能开销。
那在poll的过程中一次可以poll多少个呢这个poll的个数可以通过sysctl选项来控制
net.core.netdev_budget = 600
该控制选项的默认值是300在网络吞吐量较大的场景中我们可以适当地增大该值比如增大到600。增大该值可以一次性地处理更多的数据包。但是这种调整也是有缺陷的因为这会导致CPU在这里poll的时间增加如果系统中运行的任务很多的话其他任务的调度延迟就会增加。
接下来继续看TCP数据包的接收过程。我们刚才提到数据包到达网卡后会触发CPU去poll数据包这些poll的数据包紧接着就会到达IP层去处理然后再达到TCP层这时就会面对另外一个很容易引发问题的地方了TCP Receive BufferTCP接收缓冲区
与 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内核社区具体可以看这个committcp: 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吗欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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。这个阶段每经过一个RTTround-trip time发包数量就会翻倍。如下图所示
初始发送数据包的数量是由init_cwnd初始拥塞窗口来决定的该值在Linux内核中被设置为10TCP_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。紧接着发送端就会接收到三个相同的ackack 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这个字段只有16bitwin能够表示的大小最大只有6553564K所以如果想要支持更大的接收窗口以满足高性能网络我们就需要打开下面这个配置项系统中也是默认打开了该选项
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连接还正常吗为什么欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View File

@ -0,0 +1,133 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
14 案例篇 TCP端到端时延变大怎样判断是哪里出现了问题
14 案例篇 TCP端到端时延变大怎样判断是哪里出现了问题
你好,我是邵亚方。
如果你是一名互联网从业者那你对下面这个场景应该不会陌生客户端发送请求给服务端服务端将请求处理完后再把响应数据发送回客户端这就是典型的C/SClient/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处理完该请求然后回responseClient侧收到response后再去发下一个request然后MySQL收到下一个request并处理。也就是说这种模型是典型的串行方式处理完了一个再去处理下一个。所以tcprstat就可以以数据包到达MySQL Server侧作为起始时间点以MySQL将最后一个数据包发出去作为结束时间点然后这二者的时间差就是RTResponse 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抖动时我们就能够区分出抖动是发生在ClientServer还是网络中了这会大大提升分析定位问题的效率。在定位到问题出在哪里后你就可以使用我们在“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来作为这次网络耗时为什么欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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 = (RetransSegslast RetransSegs) (OutSegslast 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重传情况那么直接追踪该函数就可以了。
追踪内核函数最通用的方法是使用KprobeKprobe的大致原理如下
你可以实现一个内核模块该内核模块中使用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重传包吗为什么欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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这种方式你了解这种方式吗这样做的好处是什么欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View File

@ -0,0 +1,130 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
17 基础篇 CPU是如何执行任务的
你好,我是邵亚方。
如果你做过性能优化的话,你应该有过这些思考,比如说:
如何让CPU读取数据更快一些
同样的任务,为什么有时候执行得快,有时候执行得慢?
我的任务有些比较重要CPU如果有争抢时我希望可以先执行这些任务这该怎么办呢
多线程并行读写数据是如何保障同步的?
要想明白这些问题你就需要去了解CPU是如何执行任务的只有明白了CPU的执行逻辑你才能更好地控制你的任务执行从而获得更好的性能。
CPU是如何读写数据的
我先带你来看下CPU的架构因为你只有理解了CPU的架构你才能更好地理解CPU是如何执行指令的。CPU的架构图如下所示
你可以直观地看到对于现代处理器而言一个实体CPU通常会有两个逻辑线程也就是上图中的Core 0和Core 1。每个Core都有自己的L1 CacheL1 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.5nsL2 10ns和内存访问延迟100ns为例我给你举一个实际的案例来说明访问延迟的差异对性能的影响。
之前我在做网络追踪系统时为了更方便地追踪TCP连接我给Linux Kernel提交了一个PATCH来记录每个连接的编号具体你可以参考这个commitnet: 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 fielda和b的地址是一样的只是属于该地址的不同bit。在这种情况下CPU 0去写a a = 1同时CPU 1去写b b = 1就会产生竞争。在总线仲裁后先写的数据就会被后写的数据给覆盖掉。这就是执行RMW操作时典型的竞争问题。在这种场景下就需要同步原语了比如使用atomic操作。
关于位操作我们来看一个实际的案例。这是我前段时间贡献给Linux内核的一个PATCHpsi: 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时的竞争问题欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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线程在申请THPdo_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那你觉得应该要做什么
欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View File

@ -0,0 +1,185 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
19 案例篇 网络吞吐高的业务是否需要开启网卡特性呢?
你好,我是邵亚方。
通过上一讲我们对CPU利用率的细化相信你已经知道对于应用而言它的目标是让CPU的开销尽量用在执行用户代码上而非其他方面。usr利用率越高说明CPU的效率越高。如果usr低就说明CPU执行应用的效率不高。在第18讲里我们还讲了CPU时间浪费在sys里的案例。那今天这一讲我们一起来看看CPU在softirq上花费过多时间所引起的业务性能下降问题这也是我们在生产环境中经常遇到的一类问题。接下来我会为你讲解相关案例以及这类问题常用的观察方法。
中断与业务进程之间是如何相互干扰的?
这是我多年以前遇到的一个案例当时业务反馈说为了提升QPSQuery per Second他们开启了RPSReceivce 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后增加了很多CALFunction 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已经在超负荷工作了。而打开RPSRPS又会消耗额外的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时间来提升吞吐你需要结合你的业务场景来评估是否需要开启它。如果你的网卡支持了硬件多队列那么就可以直接使用硬件多队列了。
课后作业
我们这节课的作业有两种,你可以根据自己的情况进行选择。
入门:
请问如果软中断以及硬中断被关闭的时间太长,会发生什么事?
高级:
如果想要追踪网络数据包在内核缓冲区停留了多长时间才被应用读走,你觉得应该如何来追踪?
欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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() {
我们可以看到pread2是从__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。欢迎你在留言区与我讨论。
最后,感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友。

View 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从而引起死锁。那什么情况下可能会引起死锁呢你可以参考一下我尚未完成的PATCHxfs: 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在我心中的地位。

View 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)