C - 内存初始化时的 Linux 高内核 CPU 使用率



我在服务器上引导我的 Java 应用程序时遇到了 Linux 内核占用高 CPU 的问题。此问题仅发生在生产环境中,在开发服务器上,一切都是光速的。

upd9:关于这个问题有两个问题:

  1. 如何解决? -名义动物建议同步并删除所有内容,这真的很有帮助。sudo sh -c 'sync ; echo 3 > /proc/sys/vm/drop_caches ;有效。upd12:但确实sync就足够了。

  2. 为什么会这样?- 它仍然对我开放,我确实了解将 durty 页面刷新到磁盘会消耗内核 CPU 和 IO 时间,这是正常的。但是什么是strage,为什么即使是用"C"编写的单线程应用程序也会在内核空间中加载所有内核100%?

由于 ref-upd10和ref-upd11,我有一个想法,即不需要echo 3 > /proc/sys/vm/drop_caches来解决内存分配缓慢的问题。在启动占用内存的应用程序之前运行"同步"应该足够了。 可能会在生产中尝试这个 tommorow 并在此处发布结果。

upd10:FS 缓存页丢失情况:

  1. 我执行了cat 10GB.fiel > /dev/null,然后
  2. sync可以肯定的是,没有显示184kb的Durty页面(cat /proc/meminfo |grep ^Dirty
  3. 检查我得到cat /proc/meminfo |grep ^Cached:4GB 缓存
  4. 运行int main(char**)我得到了正常的性能(例如 50 毫秒初始化 32MB 的分配数据)。
  5. 缓存内存减少到 900MB
  6. 测试摘要:我认为 linux 将用作 FS 缓存的页面回收到分配的内存中是没有问题的。

upd11:很多脏页案例。

  1. 列表项

  2. 我用注释read部分运行我的HowMongoDdWorks示例,一段时间后

  3. /proc/meminfo说2.8GB是Dirty,3.6GB是Cached

  4. 我停下HowMongoDdWorks,跑了int main(char**).

  5. 这是结果的一部分:

    初始化 15,时间 0.00s x 0 [尝试 1/部分 0] 时间 1.11s x 1 [尝试 2/部分 0] 时间 0.04s x 0 [尝试 1/第 1 部分] 时间 1.04s x 1 [尝试 2/第 1 部分] 时间 0.05s x 0 [尝试 1/第 2 部分] 时间 0.42s x 1 [尝试 2/第 2 部分] 时间 0.04s

  6. 按测试总结:丢失的 durty 页面会显着减慢对分配内存的首次访问速度(公平地说,只有当总应用程序内存开始与整个操作系统内存相当时,才会发生这种情况,即如果您有 8 个 16 GB 的可用空间,分配 1GB 是没有问题的,从 3GB 左右减慢速度)。

现在我设法在我的开发环境中重现了这种情况,所以这里有新的细节。

开发计算机配置:

Linux
  1. 2.6.32-220.13.1.el6.x86_64 - Scientific Linux Release 6.1 (Carbon)
  2. 内存: 15.55 GB
  3. 处理器: 1 个英特尔® 酷睿(TM) i5-2300 CPU @ 2.80GHz (4 线程) (物理)

这是由FS缓存中的大量durty页面引起的99.9%的问题。这是在脏页面上创建大量内容的应用程序:

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Random;
/**
* @author dmitry.mamonov
*         Created: 10/2/12 2:53 PM
*/
public class HowMongoDdWorks{
public static void main(String[] args) throws IOException {
final long length = 10L*1024L*1024L*1024L;
final int pageSize = 4*1024;
final int lengthPages = (int) (length/pageSize);
final byte[] buffer = new byte[pageSize];
final Random random = new Random();
System.out.println("Init file");
final RandomAccessFile raf = new RandomAccessFile("random.file","rw");
raf.setLength(length);
int written = 0;
int readed = 0;
System.out.println("Test started");
while(true){
{ //write.
random.nextBytes(buffer);
final long randomPageLocation = (long)random.nextInt(lengthPages)*(long)pageSize;
raf.seek(randomPageLocation);
raf.write(buffer);
written++;
}
{ //read.
random.nextBytes(buffer);
final long randomPageLocation = (long)random.nextInt(lengthPages)*(long)pageSize;
raf.seek(randomPageLocation);
raf.read(buffer);
readed++;
}
if (written % 1024==0 || readed%1024==0){
System.out.printf("W %10d R %10d pagesn", written, readed);
}
}
}
}

这是测试应用程序,它会导致内核空间中的 HI(所有内核高达 100%)CPU 负载(同下,但我会再次复制它)。

#include<stdlib.h>
#include<stdio.h>
#include<time.h>
int main(char** argv){
int last = clock(); //remember the time
for(int i=0;i<16;i++){ //repeat test several times
int size = 256 * 1024 * 1024;
int size4=size/4;
int* buffer = malloc(size); //allocate 256MB of memory
for(int k=0;k<2;k++){ //initialize allocated memory twice
for(int j=0;j<size4;j++){ 
//memory initialization (if I skip this step my test ends in 
buffer[j]=k; 0.000s
}
//printing 
printf(x "[%d] %.2fn",k+1, (clock()-last)/(double)CLOCKS_PER_SEC); stat
last = clock();
}
}
return 0;
}

当以前的HowMongoDdWorks程序正在运行时,int main(char** argv)将显示如下结果:

x [1] 0.23
x [2] 0.19
x [1] 0.24
x [2] 0.19
x [1] 1.30 -- first initialization takes significantly longer
x [2] 0.19 -- then seconds one (6x times slowew)
x [1] 10.94 -- and some times it is 50x slower!!!
x [2] 0.19
x [1] 1.10
x [2] 0.21
x [1] 1.52
x [2] 0.19
x [1] 0.94
x [2] 0.21
x [1] 2.36
x [2] 0.20
x [1] 3.20
x [2] 0.20 -- and the results is totally unstable
...

我保留此行以下的所有内容只是为了历史。


UPD1:开发和生产系统都足够大,可以进行此测试。upd7:这不是分页,至少我在问题期间没有看到任何存储 IO 活动。

  1. dev ~ 4 核,16 GM 内存,~8 GB 可用
  2. 空间
  3. 生产 ~ 12 核,24 GB RAM,~ 16 GB 可用(从 8 到 10 GM 在 FS 缓存下,但它没有 不同,即使所有 16GM 都是完全免费的,结果也相同),这台机器也是由 CPU 加载的,但不会太高~10%。

upd8(ref):新的测试用例和潜在解释见尾部。

这是我的测试用例(我也测试了java和python,但"c"应该是最清楚的):

#include<stdlib.h>
#include<stdio.h>
#include<time.h>
int main(char** argv){
int last = clock(); //remember the time
for(int i=0;i<16;i++){ //repeat test several times
int size = 256 * 1024 * 1024;
int size4=size/4;
int* buffer = malloc(size); //allocate 256MB of memory
for(int k=0;k<2;k++){ //initialize allocated memory twice
for(int j=0;j<size4;j++){ 
//memory initialization (if I skip this step my test ends in 
buffer[j]=k; 0.000s
}
//printing 
printf(x "[%d] %.2fn",k+1, (clock()-last)/(double)CLOCKS_PER_SEC); stat
last = clock();
}
}
return 0;
}

开发计算机上的输出(部分):

x [1] 0.13 --first initialization takes a bit longer
x [2] 0.12 --then second one, but the different is not significant.
x [1] 0.13
x [2] 0.12
x [1] 0.15
x [2] 0.11
x [1] 0.14
x [2] 0.12
x [1] 0.14
x [2] 0.12
x [1] 0.13
x [2] 0.12
x [1] 0.14
x [2] 0.11
x [1] 0.14
x [2] 0.12 -- and the results is quite stable
...

生产机器上的输出(部分):

x [1] 0.23
x [2] 0.19
x [1] 0.24
x [2] 0.19
x [1] 1.30 -- first initialization takes significantly longer
x [2] 0.19 -- then seconds one (6x times slowew)
x [1] 10.94 -- and some times it is 50x slower!!!
x [2] 0.19
x [1] 1.10
x [2] 0.21
x [1] 1.52
x [2] 0.19
x [1] 0.94
x [2] 0.21
x [1] 2.36
x [2] 0.20
x [1] 3.20
x [2] 0.20 -- and the results is totally unstable
...

在开发机器上运行此测试时,CPU 使用率甚至没有从 gound 中提高,就像所有内核在 htop 中的使用率都低于 5%。

但是在生产机器上运行此测试,我看到所有内核的 CPU 使用率高达 100%(在 50 核计算机上平均负载上升高达 12%),而且这都是内核时间。

upd2:所有机器都安装了相同的 CentOS Linux 2.6,我使用 ssh 使用它们。

upd3: 答:它不太可能交换,在我的测试期间没有看到任何磁盘活动,而且大量的 RAM 也是免费的。(此外,描述已更新)。– 德米特里 9 分钟前

upd4:htop 说 HI CPU 利用率由内核,高达 100% 的利用率由 al cores(在生产中)。

upd5:初始化完成后 CPU 利用率是否稳定下来?在我的简单测试中 - 是的。对于实际应用程序,它只是有助于停止其他一切以启动新程序(这是无稽之谈)。

我有两个问题:

  1. 为什么会这样?

  2. 如何解决?

upd8:改进了测试和解释。

#include<stdlib.h>
#include<stdio.h>
#include<time.h>
int main(char** argv){
const int partition = 8;
int last = clock();
for(int i=0;i<16;i++){
int size = 256 * 1024 * 1024;
int size4=size/4;
int* buffer = malloc(size);
buffer[0]=123;
printf("init %d, time %.2fsn",i, (clock()-last)/(double)CLOCKS_PER_SEC);
last = clock();
for(int p=0;p<partition;p++){
for(int k=0;k<2;k++){
for(int j=p*size4/partition;j<(p+1)*size4/partition;j++){
buffer[j]=k;
}
printf("x [try %d/part %d] time %.2fsn",k+1, p, (clock()-last)/(double)CLOCKS_PER_SEC);
last = clock();
}
}
}
return 0;
}

结果如下所示:

init 15, time 0.00s -- malloc call takes nothing.
x [try 1/part 0] time 0.07s -- usually first try to fill buffer part with values is fast enough.
x [try 2/part 0] time 0.04s -- second try to fill buffer part with values is always fast.
x [try 1/part 1] time 0.17s
x [try 2/part 1] time 0.05s -- second try...
x [try 1/part 2] time 0.07s
x [try 2/part 2] time 0.05s -- second try...
x [try 1/part 3] time 0.07s
x [try 2/part 3] time 0.04s -- second try...
x [try 1/part 4] time 0.08s
x [try 2/part 4] time 0.04s -- second try...
x [try 1/part 5] time 0.39s -- BUT some times it takes significantly longer then average to fill part of allocated buffer with values.
x [try 2/part 5] time 0.05s -- second try...
x [try 1/part 6] time 0.35s
x [try 2/part 6] time 0.05s -- second try...
x [try 1/part 7] time 0.16s
x [try 2/part 7] time 0.04s -- second try...

我从这次测试中学到的事实。

  1. 内存分配本身很快。
  2. 首次访问分配的内存速度很快(因此它不是惰性缓冲区分配问题)。
  3. 我将分配的缓冲区分成几部分(测试中为 8)。
  4. 并用值
  5. 0 填充每个缓冲区部分,然后用值 1 填充,打印消耗时间。
  6. 第二个缓冲部分填充始终很快。
  7. 但是最远的缓冲区部分填充总是比第二次填充慢一点(我相信在第一页访问时我的内核已经做了一些额外的工作)。
  8. 有时,第一次用值填充缓冲区部分需要更长的时间。

我尝试了建议的anwser,似乎有所帮助。稍后我会重新检查并再次发布结果。

看起来 linux 映射将页面分配给 durty 文件系统缓存页面,并且将页面逐个刷新到磁盘需要花费大量时间。但是完全同步工作速度很快并消除了问题。

运行

sudo sh -c 'sync ; echo 3 > /proc/sys/vm/drop_caches ; sync'

在开发计算机上。这是一种安全、无损的方法,可确保缓存为空。(运行上述命令不会丢失任何数据,即使您碰巧同时保存或写入磁盘。它真的很安全。

然后,确保您没有运行任何 Java 内容,并重新运行上述命令以确保。例如,您可以检查是否有任何 Java 运行

ps axu | sed -ne '/ sed -ne /d; /java/p'

它应该不输出任何内容。如果是这样,请先关闭您的 Java 内容。

现在,重新运行应用程序测试。开发计算机上现在是否也会出现相同的减速?

如果您愿意以任何一种方式发表评论,德米特里,我很乐意进一步探讨这个问题。

编辑补充:我怀疑速度确实发生了,并且是由于Java本身产生的大量启动延迟。这是一个非常普遍的问题,基本上内置于Java中,这是其体系结构的结果。对于较大的应用程序,无论机器有多快,启动延迟通常都是几分之一秒,这仅仅是因为Java必须加载和准备类(大多数也是串行的,所以添加内核将无济于事)。

换句话说,我认为责任应该落在Java身上,而不是Linux;恰恰相反,因为Linux设法通过内核级缓存来减轻开发机器上的延迟 - 这仅仅是因为你几乎一直在运行这些Java组件,所以内核知道缓存它们。

编辑 2:在应用程序启动时查看您的 Java 环境访问了哪些文件将非常有用。您可以使用strace执行此操作:

strace -f -o trace.log -q -tt -T -e trace=open COMMAND...

它创建文件trace.log,其中包含由COMMAND...启动的任何进程完成的open()系统调用。要将输出保存到COMMAND...启动的每个进程的trace.PID,请使用

strace -f -o trace -ff -q -tt -T -e trace=open COMMAND...

比较开发和生产安装上的输出将告诉您它们是否真正等效。其中一个可能有额外或缺失的库,影响启动时间。

如果安装较旧且系统分区已满,则这些文件可能已碎片化,从而导致内核花费更多时间等待 I/O 完成。(请注意,I/O保持不变;如果文件碎片化,只有完成所需的时间会增加。您可以使用命令

LANG=C LC_ALL=C sed -ne 's|^[^"]* open("(.*)", O[^"]*$|1|p' trace.* 
| LANG=C LC_ALL=C sed -ne 's|^[^"]* open("(.*)", O[^"]*$|1|p' 
| LANG=C LC_ALL=C xargs -r -d 'n' filefrag 
| LANG=C LC_ALL=C awk '(NF > 3 && $NF == "found") { n[$(NF-2)]++ }
END { for (i in n) printf "%d extents %d filesn", i, n[i] }' 
| sort -g

检查应用程序使用的文件的碎片程度;它报告有多少文件仅使用一个或多个扩展数据块。请注意,它不包括原始可执行文件(COMMAND...),仅包括它访问的文件。

如果只想获取单个命令访问的文件的碎片统计信息,则可以使用

LANG=C LC_ALL=C strace -f -q -tt -T -e trace=open COMMAND... 2>&1 
| LANG=C LC_ALL=C sed -ne 's|^[0-9:.]* open("(.*)", O[^"]*$|1|p' 
| LANG=C LC_ALL=C xargs -r filefrag 
| LANG=C LC_ALL=C awk '(NF > 3 && $NF == "found") { n[$(NF-2)]++ }
END { for (i in n) printf "%d extents %d filesn", i, n[i] }' 
| sort -g

如果问题不是由于缓存引起的,那么我认为很可能这两个安装并不真正等效。如果是,那么我会检查碎片。之后,我会在两个环境中进行全面跟踪(省略-e trace=open),以查看差异的确切位置。


我相信我现在了解您的问题/情况。

在您的生产环境中,内核页面缓存大多是脏的,即大多数缓存的内容都是将要写入磁盘的内容。

当应用程序分配新页面时,内核仅设置页面映射,实际上不会立即提供物理 RAM。这仅在第一次访问每个页面时发生。

在第一次访问时,内核首先找到一个空闲页面 - 通常是包含"干净"缓存数据的页面,即从磁盘读取但未修改的内容。然后,它将其清除为零,以避免进程之间的信息泄漏。(当使用 C 库分配工具(如malloc()等)而不是直接mmap()系列函数时,库可能会使用/重用映射的一部分。尽管内核确实将页面清除为零,但库可能会"弄脏"它们。使用mmap()获取匿名页面,您确实可以将它们归零。

如果内核手头没有合适的干净页面,它必须先将一些最旧的脏页面刷新到磁盘。(内核内部有一些进程将页面刷新到磁盘,并将它们标记为干净,但如果服务器负载使得页面持续脏污,通常希望大部分是脏页面而不是大部分干净的页面 - 服务器以这种方式完成更多的工作。不幸的是,这也意味着您现在遇到的首页访问延迟增加。

每页的长度为sysconf(_SC_PAGESIZE)字节,对齐。换句话说,指针p指向页面的开头当且仅当((long)p % sysconf(_SC_PAGESIZE)) == 0。 我相信,在大多数情况下,大多数内核确实填充了页面组而不是单个页面,从而增加了首次访问(对每组页面)的延迟。

最后,可能会有一些编译器优化对您的基准测试造成严重破坏。我建议您为基准测试main()编写一个单独的源文件,并在单独的文件中编写每次迭代的实际工作。单独编译它们,并将它们链接在一起,以确保编译器不会重新排列时间函数 wrt。完成的实际工作。基本上,在benchmark.c

#define _POSIX_C_SOURCE 200809L
#include <time.h>
#include <stdio.h>
/* in work.c, adjust as needed */
void work_init(void);      /* Optional, allocations etc. */
void work(long iteration); /* Completely up to you, including parameters */
void work_done(void);      /* Optional, deallocations etc. */
#define PRIMING    0
#define REPEATS  100
int main(void)
{
double          wall_seconds[REPEATS];
struct timespec wall_start, wall_stop;
long            iteration;
work_init();
/* Priming: do you want caches hot? */
for (iteration = 0L; iteration < PRIMING; iteration++)
work(iteration);
/* Timed iterations */
for (iteration = 0L; iteration < REPEATS; iteration++) {
clock_gettime(CLOCK_REALTIME, &wall_start);
work(iteration);
clock_gettime(CLOCK_REALTIME, &wall_stop);
wall_seconds[iteration] = (double)(wall_stop.tv_sec - wall_start.tv_sec)
+ (double)(wall_stop.tv_nsec - wall_start.tv_nsec) / 1000000000.0;
}
work_done();
/* TODO: wall_seconds[0] is the first iteration.
*       Comparing to successive iterations (assuming REPEATS > 0)
*       tells you about the initial latency.
*/
/* TODO: Sort wall_seconds, for easier statistics.
*       Most reliable value is the median, with half of the
*       values larger and half smaller.
*       Personally, I like to discard first and last 15.85%
*       of the results, to get "one-sigma confidence" interval.
*/
return 0;
}

实际的数组分配、释放和填充(每个重复循环)在work.c中定义的work()函数中完成。

当内核用完可用的干净页时,它必须将脏页刷新到磁盘。将大量脏页刷新到磁盘看起来会占用大量 CPU 负载,因为大多数内核端的内容需要一个或多个页面(暂时)才能工作。本质上,内核正在等待 I/O 完成,即使用户空间应用程序调用了与 I/O 无关的内核函数也是如此。

如果你并行运行一个微基准测试,比如说一个程序只是一遍又一遍地连续弄脏一个非常大的映射,并在不调用任何系统调用的情况下测量 CPU 时间(如果在 x86 或 x86-64 上使用 GCC,__builtin_ia32_rdtsc()),你应该看到这个程序获得了大量的 CPU 时间,即使内核似乎正在消耗"所有"的 CPU 时间。只有当一个进程调用内部需要一些内存的内核函数(syscall)时,才会调用"block",卡在内核中等待页面刷新以产生新页面。

运行基准测试时,通常只需在运行基准测试之前运行几次sudo sh -c 'sync ; echo 3 >/proc/sys/vm/drop_caches ; sync'就足够了,以确保在基准测试期间不会有过大的内存压力。我从不在生产环境中使用它。(虽然运行是安全的,即不会丢失数据,但这就像用大锤杀死蚊子一样:工作的工具是错误的。

当您在生产环境中发现由于内核刷新脏页而导致延迟开始变得太大时 - 我相信它以最大设备速度进行,可能会导致应用程序 I/O 速度打嗝--,您可以调整内核脏页刷新机制。基本上,您可以告诉内核更快地将脏页刷新到磁盘,并确保在任何时间点都不会有那么多脏页(如果可能的话)。

格雷戈里·史密斯(Gregory Smith)在这里写过关于冲洗机制的理论和调整的文章。简而言之,/proc/sys/vm/包含您可以修改的内核可调参数。它们在启动时重置为默认值,但您可以轻松地编写一个简单的 init 脚本,以便在启动时将所需的值echo到文件中。如果在生产计算机上运行的进程执行繁重的 I/O,则还可以查看文件系统可调参数。至少,您应该使用relatime标志挂载文件系统(见/etc/fstab),以便仅在文件被修改或其状态更改后才更新首次访问的文件访问时间。

就个人而言,我还为多媒体工作站(以及多媒体服务器,如果我现在有的话)使用具有 1000 Hz 计时器的低延迟抢占式内核。这样的内核在较短的切片中运行用户进程,并且通常提供更好的延迟,尽管最大计算能力略低。如果您的生产服务对延迟敏感,我建议您将生产服务器切换到此类内核。

许多发行版确实已经提供了这样的内核,但我发现重新编译发行版内核,甚至切换到 kernel.org 内核要简单得多。过程很简单:您需要安装内核开发和工具(在 Debian 变体上,make-kpkg非常有用)。要升级内核,您需要获取新的源代码,配置内核(通常使用当前配置作为基础 -make oldconfig),构建新内核并安装软件包,然后再重新启动。大多数人确实发现仅升级硬件比重新编译发行版内核更具成本效益,但我自己发现重新编译内核非常轻松。无论如何,我不会自动重新启动内核升级,因此在重新启动之前添加一个简单的步骤(由运行单个脚本触发)对我来说并不费力。

最新更新