mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-17 08:46:40 +08:00
680 lines
29 KiB
HTML
680 lines
29 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
|
||
<head>
|
||
|
||
<head>
|
||
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
|
||
<link rel="icon" href="/static/favicon.png">
|
||
|
||
<title>14 容器中的内存与IO:容器写文件的延时为什么波动很大?.md.html</title>
|
||
|
||
<!-- Spectre.css framework -->
|
||
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
|
||
<!-- theme css & js -->
|
||
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
|
||
</head>
|
||
<body>
|
||
<div class="book-container">
|
||
|
||
<div class="book-sidebar">
|
||
|
||
<div class="book-brand">
|
||
|
||
<a href="/">
|
||
|
||
<img src="/static/favicon.png">
|
||
|
||
<span>技术文章摘抄</span>
|
||
|
||
</a>
|
||
|
||
</div>
|
||
|
||
<div class="book-menu uncollapsible">
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="../">上一级</a></li>
|
||
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/00 开篇词 一个态度两个步骤,成为容器实战高手.md.html">00 开篇词 一个态度两个步骤,成为容器实战高手.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/01 认识容器:容器的基本操作和实现原理.md.html">01 认识容器:容器的基本操作和实现原理.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/02 理解进程(1):为什么我在容器中不能kill 1号进程?.md.html">02 理解进程(1):为什么我在容器中不能kill 1号进程?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/03 理解进程(2):为什么我的容器里有这么多僵尸进程?.md.html">03 理解进程(2):为什么我的容器里有这么多僵尸进程?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/04 理解进程(3):为什么我在容器中的进程被强制杀死了?.md.html">04 理解进程(3):为什么我在容器中的进程被强制杀死了?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/05 容器CPU(1):怎么限制容器的CPU使用?.md.html">05 容器CPU(1):怎么限制容器的CPU使用?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/06 容器CPU(2):如何正确地拿到容器CPU的开销?.md.html">06 容器CPU(2):如何正确地拿到容器CPU的开销?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/07 Load Average:加了CPU Cgroup限制,为什么我的容器还是很慢?.md.html">07 Load Average:加了CPU Cgroup限制,为什么我的容器还是很慢?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/08 容器内存:我的容器为什么被杀了?.md.html">08 容器内存:我的容器为什么被杀了?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/09 Page Cache:为什么我的容器内存使用量总是在临界点.md.html">09 Page Cache:为什么我的容器内存使用量总是在临界点.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/10 Swap:容器可以使用Swap空间吗?.md.html">10 Swap:容器可以使用Swap空间吗?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/11 容器文件系统:我在容器中读写文件怎么变慢了.md.html">11 容器文件系统:我在容器中读写文件怎么变慢了.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/12 容器文件Quota:容器为什么把宿主机的磁盘写满了?.md.html">12 容器文件Quota:容器为什么把宿主机的磁盘写满了?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/13 容器磁盘限速:我的容器里磁盘读写为什么不稳定.md.html">13 容器磁盘限速:我的容器里磁盘读写为什么不稳定.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
<a class="current-tab" href="/专栏/容器实战高手课/14 容器中的内存与IO:容器写文件的延时为什么波动很大?.md.html">14 容器中的内存与IO:容器写文件的延时为什么波动很大?.md.html</a>
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/15 容器网络:我修改了procsysnet下的参数,为什么在容器中不起效?.md.html">15 容器网络:我修改了procsysnet下的参数,为什么在容器中不起效?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/16 容器网络配置(1):容器网络不通了要怎么调试.md.html">16 容器网络配置(1):容器网络不通了要怎么调试.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/17 容器网络配置(2):容器网络延时要比宿主机上的高吗.md.html">17 容器网络配置(2):容器网络延时要比宿主机上的高吗.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/18 容器网络配置(3):容器中的网络乱序包怎么这么高?.md.html">18 容器网络配置(3):容器中的网络乱序包怎么这么高?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/19 容器安全(1):我的容器真的需要privileged权限吗.md.html">19 容器安全(1):我的容器真的需要privileged权限吗.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/20 容器安全(2):在容器中,我不以root用户来运行程序可以吗?.md.html">20 容器安全(2):在容器中,我不以root用户来运行程序可以吗?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/加餐01 案例分析:怎么解决海量IPVS规则带来的网络延时抖动问题?.md.html">加餐01 案例分析:怎么解决海量IPVS规则带来的网络延时抖动问题?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/加餐02 理解perf:怎么用perf聚焦热点函数?.md.html">加餐02 理解perf:怎么用perf聚焦热点函数?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/加餐03 理解ftrace(1):怎么应用ftrace查看长延时内核函数?.md.html">加餐03 理解ftrace(1):怎么应用ftrace查看长延时内核函数?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/加餐04 理解ftrace(2):怎么理解ftrace背后的技术tracepoint和kprobe?.md.html">加餐04 理解ftrace(2):怎么理解ftrace背后的技术tracepoint和kprobe?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/加餐05 eBPF:怎么更加深入地查看内核中的函数?.md.html">加餐05 eBPF:怎么更加深入地查看内核中的函数?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/加餐06 BCC:入门eBPF的前端工具.md.html">加餐06 BCC:入门eBPF的前端工具.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/加餐福利 课后思考题答案合集.md.html">加餐福利 课后思考题答案合集.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/容器实战高手课/结束语 跳出舒适区,突破思考的惰性.md.html">结束语 跳出舒适区,突破思考的惰性.md.html</a>
|
||
</li>
|
||
|
||
</ul>
|
||
</div>
|
||
|
||
</div>
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
|
||
<div class="sidebar-toggle-inner"></div>
|
||
|
||
</div>
|
||
<script>
|
||
|
||
function add_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.add('show')
|
||
|
||
}
|
||
function remove_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.remove('show')
|
||
|
||
}
|
||
function sidebar_toggle() {
|
||
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let content = document.querySelector('.off-canvas-content')
|
||
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
|
||
sidebar_toggle.classList.remove('extend')
|
||
|
||
sidebar.classList.remove('hide')
|
||
|
||
content.classList.remove('extend')
|
||
|
||
} else { // hide
|
||
|
||
sidebar_toggle.classList.add('extend')
|
||
|
||
sidebar.classList.add('hide')
|
||
|
||
content.classList.add('extend')
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
function open_sidebar() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.add('show')
|
||
|
||
overlay.classList.add('show')
|
||
|
||
}
|
||
|
||
function hide_canvas() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.remove('show')
|
||
|
||
overlay.classList.remove('show')
|
||
|
||
}
|
||
</script>
|
||
<div class="off-canvas-content">
|
||
|
||
<div class="columns">
|
||
|
||
<div class="column col-12 col-lg-12">
|
||
|
||
<div class="book-navbar">
|
||
|
||
<!-- For Responsive Layout -->
|
||
|
||
<header class="navbar">
|
||
|
||
<section class="navbar-section">
|
||
|
||
<a onclick="open_sidebar()">
|
||
|
||
<i class="icon icon-menu"></i>
|
||
|
||
</a>
|
||
|
||
</section>
|
||
|
||
</header>
|
||
|
||
</div>
|
||
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
|
||
overflow-x: auto;
|
||
|
||
overflow-y: hidden;">
|
||
|
||
<div class="book-post">
|
||
|
||
<p id="tip" align="center"></p>
|
||
|
||
<div><h1>14 容器中的内存与IO:容器写文件的延时为什么波动很大?</h1>
|
||
|
||
<p>你好,我是程远。这一讲,我们继续聊一聊容器中写文件性能波动的问题。</p>
|
||
|
||
<p>你应该还记得,我们上一讲中讲过 Linux 中的两种 I/O 模式,Direct I/O 和 Buffered I/O。</p>
|
||
|
||
<p>对于 Linux 的系统调用 write() 来说,Buffered I/O 是缺省模式,使用起来比较方便,而且从用户角度看,在大多数的应用场景下,用 Buffered I/O 的 write() 函数调用返回要快一些。所以,Buffered I/O 在程序中使用得更普遍一些。</p>
|
||
|
||
<p>当使用 Buffered I/O 的应用程序从虚拟机迁移到容器,这时我们就会发现多了 Memory Cgroup 的限制之后,write() 写相同大小的数据块花费的时间,延时波动会比较大。</p>
|
||
|
||
<p>这是怎么回事呢?接下来我们就带着问题开始今天的学习。</p>
|
||
|
||
<h2>问题再现</h2>
|
||
|
||
<p>我们可以先动手写一个小程序,用来模拟刚刚说的现象。</p>
|
||
|
||
<p>这个小程序我们这样来设计:从一个文件中每次读取一个 64KB 大小的数据块,然后写到一个新文件中,它可以不断读写 10GB 大小的数据。同时我们在这个小程序中做个记录,记录写每个 64KB 的数据块需要花费的时间。</p>
|
||
|
||
<p>我们可以先在虚拟机里直接运行,虚拟机里内存大小是大于 10GB 的。接着,我们把这个程序放到容器中运行,因为这个程序本身并不需要很多的内存,我们给它做了一个 Memory Cgroup 的内存限制,设置为 1GB。</p>
|
||
|
||
<p>运行结束后,我们比较一下程序写数据块的时间。我把结果画了一张图,图里的纵轴是时间,单位 us;横轴是次数,在这里我们记录了 96 次。图中橘红色的线是在容器里运行的结果,蓝色的线是在虚拟机上运行的结果。</p>
|
||
|
||
<p>结果很明显,在容器中写入数据块的时间会时不时地增高到 200us;而在虚拟机里的写入数据块时间就比较平稳,一直在 30~50us 这个范围内。</p>
|
||
|
||
<p><img src="assets/7c494f4bc587b618f4b7db3db9ce4ac0.jpg" alt="img" /></p>
|
||
|
||
<p>通过这个小程序,我们再现了问题,那我们就来分析一下,为什么会产生这样的结果。</p>
|
||
|
||
<h2>时间波动是因为 Dirty Pages 的影响么?</h2>
|
||
|
||
<p>我们对文件的写入操作是 Buffered I/O。在前一讲中,我们其实已经知道了,对于 Buffer I/O,用户的数据是先写入到 Page Cache 里的。而这些写入了数据的内存页面,在它们没有被写入到磁盘文件之前,就被叫作 dirty pages。</p>
|
||
|
||
<p>Linux 内核会有专门的内核线程(每个磁盘设备对应的 kworker/flush 线程)把 dirty pages 写入到磁盘中。那我们自然会这样猜测,也许是 Linux 内核对 dirty pages 的操作影响了 Buffered I/O 的写操作?</p>
|
||
|
||
<p>想要验证这个想法,我们需要先来看看 dirty pages 是在什么时候被写入到磁盘的。这里就要用到 /proc/sys/vm 里和 dirty page 相关的内核参数了,我们需要知道所有相关参数的含义,才能判断出最后真正导致问题发生的原因。</p>
|
||
|
||
<p>现在我们挨个来看一下。为了方便后面的讲述,我们可以设定一个比值 A,A 等于 dirty pages 的内存 / 节点可用内存 *100%。</p>
|
||
|
||
<p>第一个参数,dirty_background_ratio,这个参数里的数值是一个百分比值,缺省是 10%。如果比值 A 大于 dirty_background_ratio 的话,比如大于默认的 10%,内核 flush 线程就会把 dirty pages 刷到磁盘里。</p>
|
||
|
||
<p>第二个参数,是和 dirty_background_ratio 相对应一个参数,也就是 dirty_background_bytes,它和 dirty_background_ratio 作用相同。区别只是 dirty_background_bytes 是具体的字节数,它用来定义的是 dirty pages 内存的临界值,而不是比例值。</p>
|
||
|
||
<p>这里你还要注意,dirty_background_ratio 和 dirty_background_bytes 只有一个可以起作用,如果你给其中一个赋值之后,另外一个参数就归 0 了。</p>
|
||
|
||
<p>接下来我们看第三个参数,dirty_ratio,这个参数的数值也是一个百分比值,缺省是 20%。</p>
|
||
|
||
<p>如果比值 A,大于参数 dirty_ratio 的值,比如大于默认设置的 20%,这时候正在执行 Buffered I/O 写文件的进程就会被阻塞住,直到它写的数据页面都写到磁盘为止。</p>
|
||
|
||
<p>同样,第四个参数 dirty_bytes 与 dirty_ratio 相对应,它们的关系和 dirty_background_ratio 与 dirty_background_bytes 一样。我们给其中一个赋值后,另一个就会归零。</p>
|
||
|
||
<p>然后我们来看 dirty_writeback_centisecs,这个参数的值是个时间值,以百分之一秒为单位,缺省值是 500,也就是 5 秒钟。它表示每 5 秒钟会唤醒内核的 flush 线程来处理 dirty pages。</p>
|
||
|
||
<p>最后还有 dirty_expire_centisecs,这个参数的值也是一个时间值,以百分之一秒为单位,缺省值是 3000,也就是 30 秒钟。它定义了 dirty page 在内存中存放的最长时间,如果一个 dirty page 超过这里定义的时间,那么内核的 flush 线程也会把这个页面写入磁盘。</p>
|
||
|
||
<p>好了,从这些 dirty pages 相关的参数定义,你会想到些什么呢?</p>
|
||
|
||
<p>进程写操作上的时间波动,只有可能是因为 dirty pages 的数量很多,已经达到了第三个参数 dirty_ratio 的值。这时执行写文件功能的进程就会被暂停,直到写文件的操作将数据页面写入磁盘,写文件的进程才能继续运行,所以进程里一次写文件数据块的操作时间会增加。</p>
|
||
|
||
<p>刚刚说的是我们的推理,那情况真的会是这样吗?其实我们可以在容器中进程不断写入数据的时候,查看节点上 dirty pages 的实时数目。具体操作如下:</p>
|
||
|
||
<pre><code>watch -n 1 "cat /proc/vmstat | grep dirty"
|
||
|
||
</code></pre>
|
||
|
||
<p>当我们的节点可用内存是 12GB 的时候,假设 dirty_ratio 是 20%,dirty_background_ratio 是 10%,那么我们在 1GB memory 容器中写 10GB 的数据,就会看到它实时的 dirty pages 数目,也就是 / proc/vmstat 里的 nr_dirty 的数值,这个数值对应的内存并不能达到 dirty_ratio 所占的内存值。</p>
|
||
|
||
<p><img src="assets/ccd0b41e3bd9420c539942b84d88f968.png" alt="img" /></p>
|
||
|
||
<p>其实我们还可以再做个实验,就是在 dirty_bytes 和 dirty_background_bytes 里写入一个很小的值。</p>
|
||
|
||
<pre><code>echo 8192 > /proc/sys/vm/dirty_bytes
|
||
|
||
echo 4096 > /proc/sys/vm/dirty_background_bytes
|
||
|
||
</code></pre>
|
||
|
||
<p>然后再记录一下容器程序里每写入 64KB 数据块的时间,这时候,我们就会看到,时不时一次写入的时间就会达到 9ms,这已经远远高于我们之前看到的 200us 了。</p>
|
||
|
||
<p>因此,我们知道了这个时间的波动,并不是强制把 dirty page 写入到磁盘引起的。</p>
|
||
|
||
<h2>调试问题</h2>
|
||
|
||
<p>那接下来,我们还能怎么分析这个问题呢?</p>
|
||
|
||
<p>我们可以用 perf 和 ftrace 这两个工具,对容器里写数据块的进程做个 profile,看看到底是调用哪个函数花费了比较长的时间。顺便说一下,我们在专题加餐里会专门介绍如何使用 perf、ftrace 等工具以及它们的工作原理,在这里你只要了解我们的调试思路就行。</p>
|
||
|
||
<p>怎么使用这两个工具去定位耗时高的函数呢?我大致思路是这样的:我们发现容器中的进程用到了 write() 这个函数调用,然后写 64KB 数据块的时间增加了,而 write() 是一个系统调用,那我们需要进行下面这两步操作。</p>
|
||
|
||
<p>第一步,我们要找到内核中 write() 这个系统调用函数下,又调用了哪些子函数。想找出主要的子函数我们可以查看代码,也可以用 perf 这个工具来得到。</p>
|
||
|
||
<p>然后是第二步,得到了 write() 的主要子函数之后,我们可以用 ftrace 这个工具来 trace 这些函数的执行时间,这样就可以找到花费时间最长的函数了。</p>
|
||
|
||
<p>好,下面我们就按照刚才梳理的思路来做一下。首先是第一步,我们在容器启动写磁盘的进程后,在宿主机上得到这个进程的 pid,然后运行下面的 perf 命令。</p>
|
||
|
||
<pre><code>perf record -a -g -p <pid>
|
||
|
||
</code></pre>
|
||
|
||
<p>等写磁盘的进程退出之后,这个 perf record 也就停止了。</p>
|
||
|
||
<p>这时我们再执行 perf report 查看结果。把 vfs_write() 函数展开之后,我们就可以看到,write() 这个系统调用下面的调用到了哪些主要的子函数,到这里第一步就完成了。</p>
|
||
|
||
<p><img src="assets/9191caa5db8c0afe2363540bc31e1d9d.png" alt="img" /></p>
|
||
|
||
<p>下面再来做第二步,我们把主要的函数写入到 ftrace 的 set_ftrace_filter 里,然后把 ftrace 的 tracer 设置为 function_graph,并且打开 tracing_on 开启追踪。</p>
|
||
|
||
<pre><code># cd /sys/kernel/debug/tracing
|
||
|
||
# echo vfs_write >> set_ftrace_filter
|
||
|
||
# echo xfs_file_write_iter >> set_ftrace_filter
|
||
|
||
# echo xfs_file_buffered_aio_write >> set_ftrace_filter
|
||
|
||
# echo iomap_file_buffered_write
|
||
|
||
# echo iomap_file_buffered_write >> set_ftrace_filter
|
||
|
||
# echo pagecache_get_page >> set_ftrace_filter
|
||
|
||
# echo try_to_free_mem_cgroup_pages >> set_ftrace_filter
|
||
|
||
# echo try_charge >> set_ftrace_filter
|
||
|
||
# echo mem_cgroup_try_charge >> set_ftrace_filter
|
||
# echo function_graph > current_tracer
|
||
|
||
# echo 1 > tracing_on
|
||
|
||
</code></pre>
|
||
|
||
<p>这些设置完成之后,我们再运行一下容器中的写磁盘程序,同时从 ftrace 的 trace_pipe 中读取出追踪到的这些函数。</p>
|
||
|
||
<p>这时我们可以看到,当需要申请 Page Cache 页面的时候,write() 系统调用会反复地调用 mem_cgroup_try_charge(),并且在释放页面的时候,函数 do_try_to_free_pages() 花费的时间特别长,有 50+us(时间单位,micro-seconds)这么多。</p>
|
||
|
||
<pre><code> 1) | vfs_write() {
|
||
|
||
1) | xfs_file_write_iter [xfs]() {
|
||
|
||
1) | xfs_file_buffered_aio_write [xfs]() {
|
||
|
||
1) | iomap_file_buffered_write() {
|
||
|
||
1) | pagecache_get_page() {
|
||
|
||
1) | mem_cgroup_try_charge() {
|
||
|
||
1) 0.338 us | try_charge();
|
||
|
||
1) 0.791 us | }
|
||
|
||
1) 4.127 us | }
|
||
|
||
…
|
||
1) | pagecache_get_page() {
|
||
|
||
1) | mem_cgroup_try_charge() {
|
||
|
||
1) | try_charge() {
|
||
|
||
1) | try_to_free_mem_cgroup_pages() {
|
||
|
||
1) + 52.798 us | do_try_to_free_pages();
|
||
|
||
1) + 53.958 us | }
|
||
|
||
1) + 54.751 us | }
|
||
|
||
1) + 55.188 us | }
|
||
|
||
1) + 56.742 us | }
|
||
|
||
…
|
||
|
||
1) ! 109.925 us | }
|
||
|
||
1) ! 110.558 us | }
|
||
|
||
1) ! 110.984 us | }
|
||
|
||
1) ! 111.515 us | }
|
||
</code></pre>
|
||
|
||
<p>看到这个 ftrace 的结果,你是不是会想到,我们在容器内存那一讲中提到的 Page Cahe 呢?</p>
|
||
|
||
<p>是的,这个问题的确和 Page Cache 有关,Linux 会把所有的空闲内存利用起来,一旦有 Buffered I/O,这些内存都会被用作 Page Cache。</p>
|
||
|
||
<p>当容器加了 Memory Cgroup 限制了内存之后,对于容器里的 Buffered I/O,就只能使用容器中允许使用的最大内存来做 Page Cache。</p>
|
||
|
||
<p>那么如果容器在做内存限制的时候,Cgroup 中 memory.limit_in_bytes 设置得比较小,而容器中的进程又有很大量的 I/O,这样申请新的 Page Cache 内存的时候,又会不断释放老的内存页面,这些操作就会带来额外的系统开销了。</p>
|
||
|
||
<h2>重点总结</h2>
|
||
|
||
<p>我们今天讨论的问题是在容器中用 Buffered I/O 方式写文件的时候,会出现写入时间波动的问题。</p>
|
||
|
||
<p>由于这是 Buffered I/O 方式,对于写入文件会先写到内存里,这样就产生了 dirty pages,所以我们先研究了一下 Linux 对 dirty pages 的回收机制是否会影响到容器中写入数据的波动。</p>
|
||
|
||
<p>在这里我们最主要的是理解这两个参数,dirty_background_ratio 和 dirty_ratio,这两个值都是相对于节点可用内存的百分比值。</p>
|
||
|
||
<p>当 dirty pages 数量超过 dirty_background_ratio 对应的内存量的时候,内核 flush 线程就会开始把 dirty pages 写入磁盘 ; 当 dirty pages 数量超过 dirty_ratio 对应的内存量,这时候程序写文件的函数调用 write() 就会被阻塞住,直到这次调用的 dirty pages 全部写入到磁盘。</p>
|
||
|
||
<p>在节点是大内存容量,并且 dirty_ratio 为系统缺省值 20%,dirty_background_ratio 是系统缺省值 10% 的情况下,我们通过观察 /proc/vmstat 中的 nr_dirty 数值可以发现,dirty pages 不会阻塞进程的 Buffered I/O 写文件操作。</p>
|
||
|
||
<p>所以我们做了另一种尝试,使用 perf 和 ftrace 工具对容器中的写文件进程进行 profile。我们用 perf 得到了系统调用 write() 在内核中的一系列子函数调用,再用 ftrace 来查看这些子函数的调用时间。</p>
|
||
|
||
<p>根据 ftrace 的结果,我们发现写数据到 Page Cache 的时候,需要不断地去释放原有的页面,这个时间开销是最大的。造成容器中 Buffered I/O write() 不稳定的原因,正是容器在限制内存之后,Page Cache 的数量较小并且不断申请释放。</p>
|
||
|
||
<p>其实这个问题也提醒了我们:在对容器做 Memory Cgroup 限制内存大小的时候,不仅要考虑容器中进程实际使用的内存量,还要考虑容器中程序 I/O 的量,合理预留足够的内存作为 Buffered I/O 的 Page Cache。</p>
|
||
|
||
<p>比如,如果知道需要反复读写文件的大小,并且在内存足够的情况下,那么 Memory Cgroup 的内存限制可以超过这个文件的大小。</p>
|
||
|
||
<p>还有一个解决思路是,我们在程序中自己管理文件的 cache 并且调用 Direct I/O 来读写文件,这样才会对应用程序的性能有一个更好的预期。</p>
|
||
|
||
<h2>思考题</h2>
|
||
|
||
<p>我们对 dirty_bytes 和 dirty_background_bytes 做下面的设置:</p>
|
||
|
||
<pre><code>-bash-4.2# echo 8192 > /proc/sys/vm/dirty_bytes
|
||
|
||
-bash-4.2# echo 4096 > /proc/sys/vm/dirty_background_bytes
|
||
|
||
</code></pre>
|
||
|
||
<p>然后再运行下面的 fio 测试,得到的结果和缺省 dirty_* 配置的时候会有差别吗?</p>
|
||
|
||
<pre><code># fio -direct=1 -iodepth=64 -rw=write -ioengine=libaio -bs=4k -size=10G -numjobs=1 -name=./fio.test
|
||
|
||
</code></pre>
|
||
|
||
<p>欢迎你在留言区提出你的思考或是疑问。如果这篇文章对你有帮助的话,也欢迎你分享给你的朋友、同事,一起学习进步。</p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/容器实战高手课/13 容器磁盘限速:我的容器里磁盘读写为什么不稳定.md.html">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/容器实战高手课/15 容器网络:我修改了procsysnet下的参数,为什么在容器中不起效?.md.html">下一页</a>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
|
||
</div>
|
||
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099779ebe013cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
|
||
</body>
|
||
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
|
||
<script>
|
||
|
||
window.dataLayer = window.dataLayer || [];
|
||
function gtag() {
|
||
|
||
dataLayer.push(arguments);
|
||
|
||
}
|
||
gtag('js', new Date());
|
||
|
||
gtag('config', 'G-NPSEEVD756');
|
||
|
||
var path = window.location.pathname
|
||
|
||
var cookie = getCookie("lastPath");
|
||
|
||
console.log(path)
|
||
|
||
if (path.replace("/", "") === "") {
|
||
|
||
if (cookie.replace("/", "") !== "") {
|
||
|
||
console.log(cookie)
|
||
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
setCookie("lastPath", path)
|
||
|
||
}
|
||
function setCookie(cname, cvalue) {
|
||
|
||
var d = new Date();
|
||
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
|
||
var expires = "expires=" + d.toGMTString();
|
||
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
|
||
}
|
||
function getCookie(cname) {
|
||
|
||
var name = cname + "=";
|
||
|
||
var ca = document.cookie.split(';');
|
||
|
||
for (var i = 0; i < ca.length; i++) {
|
||
|
||
var c = ca[i].trim();
|
||
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
|
||
}
|
||
|
||
return "";
|
||
|
||
}
|
||
</script>
|
||
</html>
|
||
|