This commit is contained in:
yunwei37
2023-08-15 15:18:30 +00:00
parent abd70d5a8e
commit ae50b3d00f
7 changed files with 583 additions and 47 deletions

View File

@@ -166,19 +166,288 @@
<div id="content" class="content">
<main>
<h1 id="ebpf-入门实践教程编写-ebpf-程序-biopattern-统计随机顺序磁盘-io"><a class="header" href="#ebpf-入门实践教程编写-ebpf-程序-biopattern-统计随机顺序磁盘-io">eBPF 入门实践教程:编写 eBPF 程序 Biopattern: 统计随机/顺序磁盘 I/O</a></h1>
<h2 id="背景"><a class="header" href="#背景">背景</a></h2>
<h1 id="ebpf-入门实践教程十七编写-ebpf-程序统计随机顺序磁盘-io"><a class="header" href="#ebpf-入门实践教程十七编写-ebpf-程序统计随机顺序磁盘-io">eBPF 入门实践教程十七:编写 eBPF 程序统计随机/顺序磁盘 I/O</a></h1>
<p>eBPF扩展的伯克利数据包过滤器是 Linux 内核中的一种新技术,允许用户在内核空间中执行自定义程序,而无需更改内核代码。这为系统管理员和开发者提供了强大的工具,可以深入了解和监控系统的行为,从而进行优化。</p>
<p>在本篇教程中,我们将探索如何使用 eBPF 编写程序来统计随机和顺序的磁盘 I/O。磁盘 I/O 是计算机性能的关键指标之一,特别是在数据密集型应用中。</p>
<h2 id="随机顺序磁盘-io"><a class="header" href="#随机顺序磁盘-io">随机/顺序磁盘 I/O</a></h2>
<p>随着技术的进步和数据量的爆炸性增长,磁盘 I/O 成为了系统性能的关键瓶颈。应用程序的性能很大程度上取决于其如何与存储层进行交互。因此,深入了解和优化磁盘 I/O特别是随机和顺序的 I/O变得尤为重要。</p>
<ol>
<li>
<p><strong>随机 I/O</strong>:随机 I/O 发生在应用程序从磁盘的非连续位置读取或写入数据时。这种 I/O 模式的主要特点是磁盘头需要频繁地在不同的位置之间移动,导致其通常比顺序 I/O 的速度慢。典型的产生随机 I/O 的场景包括数据库查询、文件系统的元数据操作以及虚拟化环境中的并发任务。</p>
</li>
<li>
<p><strong>顺序 I/O</strong>:与随机 I/O 相反,顺序 I/O 是当应用程序连续地读取或写入磁盘上的数据块。这种 I/O 模式的优势在于磁盘头可以在一个方向上连续移动,从而大大提高了数据的读写速度。视频播放、大型文件的下载或上传以及连续的日志记录都是产生顺序 I/O 的典型应用。</p>
</li>
</ol>
<p>为了实现存储性能的最优化,了解随机和顺序的磁盘 I/O 是至关重要的。例如,随机 I/O 敏感的应用程序在 SSD 上的性能通常远超于传统硬盘,因为 SSD 在处理随机 I/O 时几乎没有寻址延迟。相反,对于大量顺序 I/O 的应用,如何最大化磁盘的连续读写速度则更为关键。</p>
<p>在本教程的后续部分,我们将详细探讨如何使用 eBPF 工具来实时监控和统计这两种类型的磁盘 I/O。这不仅可以帮助我们更好地理解系统的 I/O 行为,还可以为进一步的性能优化提供有力的数据支持。</p>
<h2 id="biopattern"><a class="header" href="#biopattern">Biopattern</a></h2>
<p>Biopattern 可以统计随机/顺序磁盘I/O次数的比例。</p>
<p>TODO</p>
<h2 id="实现原理"><a class="header" href="#实现原理">实现原理</a></h2>
<p>Biopattern 的ebpf代码在 tracepoint/block/block_rq_complete 挂载点下实现。在磁盘完成IO请求
程序会经过此挂载点。Biopattern 内部存有一张以设备号为主键的哈希表,当程序经过挂载点时, Biopattern
会获得操作信息根据哈希表中该设备的上一次操作记录来判断本次操作是随机IO还是顺序IO并更新操作计数。</p>
<h2 id="编写-ebpf-程序"><a class="header" href="#编写-ebpf-程序">编写 eBPF 程序</a></h2>
<p>TODO</p>
<h3 id="总结"><a class="header" href="#总结">总结</a></h3>
<p>Biopattern 可以展现随机/顺序磁盘I/O次数的比例对于开发者把握整体I/O情况有较大帮助。</p>
<p>TODO</p>
<p>首先,确保你已经正确安装了 libbpf 和相关的工具集,可以在这里找到对应的源代码:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">bpf-developer-tutorial</a></p>
<p>导航到 <code>biopattern</code> 的源代码目录,并使用 <code>make</code> 命令进行编译:</p>
<pre><code class="language-bash">cd ~/bpf-developer-tutorial/src/17-biopattern
make
</code></pre>
<p>编译成功后,你应该可以在当前目录下看到 <code>biopattern</code> 的可执行文件。基本的运行命令如下:</p>
<pre><code class="language-bash">sudo ./biopattern [interval] [count]
</code></pre>
<p>例如要每秒打印一次输出并持续10秒你可以运行</p>
<pre><code class="language-console">$ sudo ./biopattern 1 10
Tracing block device I/O requested seeks... Hit Ctrl-C to end.
DISK %RND %SEQ COUNT KBYTES
sr0 0 100 3 0
sr1 0 100 8 0
sda 0 100 1 4
sda 100 0 26 136
sda 0 100 1 4
</code></pre>
<p>输出列的含义如下:</p>
<ul>
<li><code>DISK</code>:被追踪的磁盘名称。</li>
<li><code>%RND</code>:随机 I/O 的百分比。</li>
<li><code>%SEQ</code>:顺序 I/O 的百分比。</li>
<li><code>COUNT</code>:在指定的时间间隔内的 I/O 请求次数。</li>
<li><code>KBYTES</code>:在指定的时间间隔内读写的数据量(以 KB 为单位)。</li>
</ul>
<p>从上述输出中,我们可以得出以下结论:</p>
<ul>
<li><code>sr0</code><code>sr1</code> 设备在观测期间主要进行了顺序 I/O但数据量很小。</li>
<li><code>sda</code> 设备在某些时间段内只进行了随机 I/O而在其他时间段内只进行了顺序 I/O。</li>
</ul>
<p>这些信息可以帮助我们了解系统的 I/O 模式,从而进行针对性的优化。</p>
<h2 id="ebpf-biopattern-实现原理"><a class="header" href="#ebpf-biopattern-实现原理">eBPF Biopattern 实现原理</a></h2>
<p>首先,让我们看一下 biopattern 的核心 eBPF 内核态代码:</p>
<pre><code class="language-c">#include &lt;vmlinux.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &quot;biopattern.h&quot;
#include &quot;maps.bpf.h&quot;
#include &quot;core_fixes.bpf.h&quot;
const volatile bool filter_dev = false;
const volatile __u32 targ_dev = 0;
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 64);
__type(key, u32);
__type(value, struct counter);
} counters SEC(&quot;.maps&quot;);
SEC(&quot;tracepoint/block/block_rq_complete&quot;)
int handle__block_rq_complete(void *args)
{
struct counter *counterp, zero = {};
sector_t sector;
u32 nr_sector;
u32 dev;
if (has_block_rq_completion()) {
struct trace_event_raw_block_rq_completion___x *ctx = args;
sector = BPF_CORE_READ(ctx, sector);
nr_sector = BPF_CORE_READ(ctx, nr_sector);
dev = BPF_CORE_READ(ctx, dev);
} else {
struct trace_event_raw_block_rq_complete___x *ctx = args;
sector = BPF_CORE_READ(ctx, sector);
nr_sector = BPF_CORE_READ(ctx, nr_sector);
dev = BPF_CORE_READ(ctx, dev);
}
if (filter_dev &amp;&amp; targ_dev != dev)
return 0;
counterp = bpf_map_lookup_or_try_init(&amp;counters, &amp;dev, &amp;zero);
if (!counterp)
return 0;
if (counterp-&gt;last_sector) {
if (counterp-&gt;last_sector == sector)
__sync_fetch_and_add(&amp;counterp-&gt;sequential, 1);
else
__sync_fetch_and_add(&amp;counterp-&gt;random, 1);
__sync_fetch_and_add(&amp;counterp-&gt;bytes, nr_sector * 512);
}
counterp-&gt;last_sector = sector + nr_sector;
return 0;
}
char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;
</code></pre>
<ol>
<li>全局变量定义</li>
</ol>
<pre><code class="language-c"> const volatile bool filter_dev = false;
const volatile __u32 targ_dev = 0;
</code></pre>
<p>这两个全局变量用于设备过滤。<code>filter_dev</code> 决定是否启用设备过滤,而 <code>targ_dev</code> 是我们想要追踪的目标设备的标识符。</p>
<p>BPF map 定义:</p>
<pre><code class="language-c"> struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 64);
__type(key, u32);
__type(value, struct counter);
} counters SEC(&quot;.maps&quot;);
</code></pre>
<p>这部分代码定义了一个 BPF map类型为哈希表。该映射的键是设备的标识符而值是一个 <code>counter</code> 结构体,用于存储设备的 I/O 统计信息。</p>
<p>追踪点函数:</p>
<pre><code class="language-c"> SEC(&quot;tracepoint/block/block_rq_complete&quot;)
int handle__block_rq_complete(void *args)
{
struct counter *counterp, zero = {};
sector_t sector;
u32 nr_sector;
u32 dev;
if (has_block_rq_completion()) {
struct trace_event_raw_block_rq_completion___x *ctx = args;
sector = BPF_CORE_READ(ctx, sector);
nr_sector = BPF_CORE_READ(ctx, nr_sector);
dev = BPF_CORE_READ(ctx, dev);
} else {
struct trace_event_raw_block_rq_complete___x *ctx = args;
sector = BPF_CORE_READ(ctx, sector);
nr_sector = BPF_CORE_READ(ctx, nr_sector);
dev = BPF_CORE_READ(ctx, dev);
}
if (filter_dev &amp;&amp; targ_dev != dev)
return 0;
counterp = bpf_map_lookup_or_try_init(&amp;counters, &amp;dev, &amp;zero);
if (!counterp)
return 0;
if (counterp-&gt;last_sector) {
if (counterp-&gt;last_sector == sector)
__sync_fetch_and_add(&amp;counterp-&gt;sequential, 1);
else
__sync_fetch_and_add(&amp;counterp-&gt;random, 1);
__sync_fetch_and_add(&amp;counterp-&gt;bytes, nr_sector * 512);
}
counterp-&gt;last_sector = sector + nr_sector;
return 0;
}
</code></pre>
<p>在 Linux 中,每次块设备的 I/O 请求完成时,都会触发一个名为 <code>block_rq_complete</code> 的追踪点。这为我们提供了一个机会,通过 eBPF 来捕获这些事件,并进一步分析 I/O 的模式。</p>
<p>主要逻辑分析:</p>
<ul>
<li><strong>提取 I/O 请求信息</strong>:从传入的参数中获取 I/O 请求的相关信息。这里有两种可能的上下文结构,取决于 <code>has_block_rq_completion</code> 的返回值。这是因为不同版本的 Linux 内核可能会有不同的追踪点定义。无论哪种情况,我们都从上下文中提取出扇区号 (<code>sector</code>)、扇区数量 (<code>nr_sector</code>) 和设备标识符 (<code>dev</code>)。</li>
<li><strong>设备过滤</strong>:如果启用了设备过滤 (<code>filter_dev</code><code>true</code>),并且当前设备不是目标设备 (<code>targ_dev</code>),则直接返回。这允许用户只追踪特定的设备,而不是所有设备。</li>
<li><strong>统计信息更新</strong>
- <strong>查找或初始化统计信息</strong>:使用 <code>bpf_map_lookup_or_try_init</code> 函数查找或初始化与当前设备相关的统计信息。如果映射中没有当前设备的统计信息,它会使用 <code>zero</code> 结构体进行初始化。
- <strong>判断 I/O 模式</strong>:根据当前 I/O 请求与上一个 I/O 请求的扇区号,我们可以判断当前请求是随机的还是顺序的。如果两次请求的扇区号相同,那么它是顺序的;否则,它是随机的。然后,我们使用 <code>__sync_fetch_and_add</code> 函数更新相应的统计信息。这是一个原子操作,确保在并发环境中数据的一致性。
- <strong>更新数据量</strong>:我们还更新了该设备的总数据量,这是通过将扇区数量 (<code>nr_sector</code>) 乘以 512每个扇区的字节数来实现的。
- <strong>更新最后一个 I/O 请求的扇区号</strong>:为了下一次的比较,我们更新了 <code>last_sector</code> 的值。</li>
</ul>
<p>在 Linux 内核的某些版本中,由于引入了一个新的追踪点 <code>block_rq_error</code>,追踪点的命名和结构发生了变化。这意味着,原先的 <code>block_rq_complete</code> 追踪点的结构名称从 <code>trace_event_raw_block_rq_complete</code> 更改为 <code>trace_event_raw_block_rq_completion</code>。这种变化可能会导致 eBPF 程序在不同版本的内核上出现兼容性问题。</p>
<p>为了解决这个问题,<code>biopattern</code> 工具引入了一种机制来动态检测当前内核使用的是哪种追踪点结构,即 <code>has_block_rq_completion</code> 函数。</p>
<ol>
<li><strong>定义两种追踪点结构</strong></li>
</ol>
<pre><code class="language-c"> struct trace_event_raw_block_rq_complete___x {
dev_t dev;
sector_t sector;
unsigned int nr_sector;
} __attribute__((preserve_access_index));
struct trace_event_raw_block_rq_completion___x {
dev_t dev;
sector_t sector;
unsigned int nr_sector;
} __attribute__((preserve_access_index));
</code></pre>
<p>这里定义了两种追踪点结构,分别对应于不同版本的内核。每种结构都包含设备标识符 (<code>dev</code>)、扇区号 (<code>sector</code>) 和扇区数量 (<code>nr_sector</code>)。</p>
<p><strong>动态检测追踪点结构</strong></p>
<pre><code class="language-c"> static __always_inline bool has_block_rq_completion()
{
if (bpf_core_type_exists(struct trace_event_raw_block_rq_completion___x))
return true;
return false;
}
</code></pre>
<p><code>has_block_rq_completion</code> 函数使用 <code>bpf_core_type_exists</code> 函数来检测当前内核是否存在 <code>trace_event_raw_block_rq_completion___x</code> 结构。如果存在,函数返回 <code>true</code>,表示当前内核使用的是新的追踪点结构;否则,返回 <code>false</code>,表示使用的是旧的结构。在对应的 eBPF 代码中,会根据两种不同的定义分别进行处理,这也是适配不同内核版本之间的变更常见的方案。</p>
<h3 id="用户态代码"><a class="header" href="#用户态代码">用户态代码</a></h3>
<p><code>biopattern</code> 工具的用户态代码负责从 BPF 映射中读取统计数据,并将其展示给用户。通过这种方式,系统管理员可以实时监控每个设备的 I/O 模式,从而更好地理解和优化系统的 I/O 性能。</p>
<p>主循环:</p>
<pre><code class="language-c"> /* main: poll */
while (1) {
sleep(env.interval);
err = print_map(obj-&gt;maps.counters, partitions);
if (err)
break;
if (exiting || --env.times == 0)
break;
}
</code></pre>
<p>这是 <code>biopattern</code> 工具的主循环,它的工作流程如下:</p>
<ul>
<li><strong>等待</strong>:使用 <code>sleep</code> 函数等待指定的时间间隔 (<code>env.interval</code>)。</li>
<li><strong>打印映射</strong>:调用 <code>print_map</code> 函数打印 BPF 映射中的统计数据。</li>
<li><strong>退出条件</strong>:如果收到退出信号 (<code>exiting</code><code>true</code>) 或者达到指定的运行次数 (<code>env.times</code> 达到 0),则退出循环。</li>
</ul>
<p>打印映射函数:</p>
<pre><code class="language-c"> static int print_map(struct bpf_map *counters, struct partitions *partitions)
{
__u32 total, lookup_key = -1, next_key;
int err, fd = bpf_map__fd(counters);
const struct partition *partition;
struct counter counter;
struct tm *tm;
char ts[32];
time_t t;
while (!bpf_map_get_next_key(fd, &amp;lookup_key, &amp;next_key)) {
err = bpf_map_lookup_elem(fd, &amp;next_key, &amp;counter);
if (err &lt; 0) {
fprintf(stderr, &quot;failed to lookup counters: %d\n&quot;, err);
return -1;
}
lookup_key = next_key;
total = counter.sequential + counter.random;
if (!total)
continue;
if (env.timestamp) {
time(&amp;t);
tm = localtime(&amp;t);
strftime(ts, sizeof(ts), &quot;%H:%M:%S&quot;, tm);
printf(&quot;%-9s &quot;, ts);
}
partition = partitions__get_by_dev(partitions, next_key);
printf(&quot;%-7s %5ld %5ld %8d %10lld\n&quot;,
partition ? partition-&gt;name : &quot;Unknown&quot;,
counter.random * 100L / total,
counter.sequential * 100L / total, total,
counter.bytes / 1024);
}
lookup_key = -1;
while (!bpf_map_get_next_key(fd, &amp;lookup_key, &amp;next_key)) {
err = bpf_map_delete_elem(fd, &amp;next_key);
if (err &lt; 0) {
fprintf(stderr, &quot;failed to cleanup counters: %d\n&quot;, err);
return -1;
}
lookup_key = next_key;
}
return 0;
}
</code></pre>
<p><code>print_map</code> 函数负责从 BPF 映射中读取统计数据,并将其打印到控制台。其主要逻辑如下:</p>
<ul>
<li><strong>遍历 BPF 映射</strong>:使用 <code>bpf_map_get_next_key</code><code>bpf_map_lookup_elem</code> 函数遍历 BPF 映射,获取每个设备的统计数据。</li>
<li><strong>计算总数</strong>:计算每个设备的随机和顺序 I/O 的总数。</li>
<li><strong>打印统计数据</strong>:如果启用了时间戳 (<code>env.timestamp</code><code>true</code>),则首先打印当前时间。接着,打印设备名称、随机 I/O 的百分比、顺序 I/O 的百分比、总 I/O 数量和总数据量(以 KB 为单位)。</li>
<li><strong>清理 BPF 映射</strong>:为了下一次的统计,使用 <code>bpf_map_get_next_key</code><code>bpf_map_delete_elem</code> 函数清理 BPF 映射中的所有条目。</li>
</ul>
<h2 id="总结"><a class="header" href="#总结">总结</a></h2>
<p>在本教程中,我们深入探讨了如何使用 eBPF 工具 biopattern 来实时监控和统计随机和顺序的磁盘 I/O。我们首先了解了随机和顺序磁盘 I/O 的重要性,以及它们对系统性能的影响。接着,我们详细介绍了 biopattern 的工作原理,包括如何定义和使用 BPF maps如何处理不同版本的 Linux 内核中的追踪点变化,以及如何在 eBPF 程序中捕获和分析磁盘 I/O 事件。</p>
<p>您可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<ul>
<li>完整代码:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/17-biopattern">https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/17-biopattern</a></li>
<li>bcc 工具:<a href="https://github.com/iovisor/bcc/blob/master/libbpf-tools/biopattern.c">https://github.com/iovisor/bcc/blob/master/libbpf-tools/biopattern.c</a></li>
</ul>
</main>