问题 linux高内核cpu用法对内存初始化


我有一个Linux内核高CPU消耗的问题,同时在服务器上引导我的java应用程序。这个问题只发生在生产中,在开发服务器上一切都是光速。

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

  1. 怎么解决? - 名义动物 建议同步和删除所有内容,这确实有帮助。 sudo sh -c 'sync ; echo 3 > /proc/sys/vm/drop_caches ; 作品。 upd12:但确实如此 sync 足够。

  2. 为什么会这样?  - 它对我来说仍然是开放的,我知道将durty页面刷新到磁盘会占用内核CPU和IO时间,这很正常。 但是什么是strage,为什么即使用“C”编写的单线程应用程序我在内核空间中加载所有内核100%?

由于参考upd10 和ref-upd11 我有一个想法 echo 3 > /proc/sys/vm/drop_caches 不需要用缓慢的内存分配来修复我的问题。 它应该足以运行`sync' 之前 开始消耗内存的应用程序。 可能会在生产中尝试这个tommorow并在此发布结果。

upd10: 丢失的FS缓存页面案例:

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

upd11: 很多脏页案例。

  1. 项目清单

  2. 我跑了我的 HowMongoDdWorks 评论的例子 read 部分,过了一段时间

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

  4. 我停下了 HowMongoDdWorks 并运行我的 int main(char**)

  5. 这是结果的一部分:

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

  6. 测试摘要: 丢失的durty页面显着减慢了首次访问分配的内存(公平地说,只有当整个应用程序内存开始与整个OS内存相当时才开始发生,即如果你有16 GB的8个免费,那么就没问题了分配1GB,从3GB左右减速start)。

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

开发机配置:

  1. Linux 2.6.32-220.13.1.el6.x86_64 - Scientific Linux版本6.1(Carbon)
  2. RAM:15.55 GB
  3. CPU:1 X Intel(R)Core(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 pages\n", 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] %.2f\n",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 RAM,~8 GB免费
  2. 生产~12核,24 GB RAM,大约16 GB免费(从8到10 GM在FS Cache下,但没有 差异,即使所有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] %.2f\n",k+1, (clock()-last)/(double)CLOCKS_PER_SEC); stat
          last = clock();
      }
   }
   return 0;
}

dev机器上的输出(部分):

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%(12核机器上的平均负载上升高达50%),而且这都是内核时间。

UPD2: 所有机器都安装了相同的centos linux 2.6,我使用ssh与他们合作。

upd3: 答:我不太可能进行交换,在测试期间没有看到任何磁盘活动,并且大量的RAM也是免费的。 (另外,descriptin更新)。 - 德米特里9分钟前

upd4: htop说核心的CPU利用率,al核心利用率高达100%(prod)。

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 %.2fs\n",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 %.2fs\n",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. 并使用值0填充每个缓冲区部分,然后使用值1填充打印消耗的时间。
  5. 第二缓冲部分填充总是很快。
  6. 但是,第一次缓冲部分填充总是慢一点填充(我相信一些额外的工作是在第一页访问时完成我的内核)。
  7. 有些时候,第一次使用值填充缓冲区部分需要更长的时间。

我试过建议anwser,它似乎有帮助。我将在稍后再次检查并发布结果。

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


10848
2017-09-28 22:09


起源

问:“顶级”说什么?问:初始化完成后CPU利用率是否稳定下来? ......而且,最重要的......问:你的服务器上有足够的内存吗?您是否有任何证据表明发生了任何“交换”? - paulsm4
您是直接在开发机器上运行测试,还是在生产机器上通过SSH或类似工具运行? - J P
这看起来与分页有关。我认为在长时间的初始化中,应用程序已经用完了页面,因此要求Linux提供更多信息。如果内存已满(或者这些页面没有足够的空间),Linux必须花时间分页。我觉得你正在经历颠簸。服务器上还运行了什么? - Linuxios
12芯意味着多芯片?有单独的缓存?导致缓存同步问题? - Bo Persson
upd8是正确的。当应用程序在Linux中分配内存时,内核只设置映射(页面),但实际上并不填充其中的任何RAM。这只有在您第一次读取或写入映射时才会发生;它也发生在逐页的基础上(页面正在 sysconf(_SC_PAGESIZE) 字节)。内核首先使用干净(不是脏的,即与磁盘相比没有修改的)页表页面,所以通常初始延迟是最小的。在您的prod env中,大多数页面缓存页面都是脏的,必须在重用之前刷新到磁盘。这就是为什么手工做首先让一切变得更快。 - Nominal Animal


答案:


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 含有 open() 系统调用由任何启动的进程完成 COMMAND...。要将输出保存到 trace.PID 对于每个过程 COMMAND... 开始,使用

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

比较dev和prod安装的输出将告诉你它们是否真正等效。其中一个可能有额外或缺少的库,影响启动时间。

如果安装旧并且系统分区相当满,则这些文件可能已碎片化,导致内核花费更多时间等待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 files\n", 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 files\n", i, n[i] }' \
| sort -g

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


我相信我现在明白你的问题/情况。

在你的prod环境中,内核页面缓存大多是脏的,即大多数缓存的东西都是要写入磁盘的东西。

当您的应用程序分配新页面时,内核仅设置页面映射,它实际上不会立即提供物理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() 函数定义 work.c


8
2017-09-29 11:23



我们尝试了同步,但没有丢弃缓存。不能说它有多大帮助。将尝试遵循您的建议,未刷新页面的情况看起来非常真实。此外,它可能没有帮助,因为经过几次运行我测试程序以上fs缓存显着下降,但问题仍然存在。再好,好主意,会尝试。 - Dmitry
我已经使用了我的应用程序,而python由于内存分配而使用mmap,在java中根本没有在内存分配循环中调用内核级别。 - Dmitry
哇,我拍了你建议的命令 sudo sh -c 'sync ; echo 3 > /proc/sys/vm/drop_caches ; sync' 它似乎真的有帮助。明天会再试一次确定。另外,我更新了我的测试用例,在slownewss中显示更多细节(将在几分钟内更新描述)。 - Dmitry
我确认建议修复同步,drop-caches修复我的问题。此外,我设法在我的测试环境中重现相同的情况。附:可能使用GCC -O0更容易关闭优化。稍后将更新帖子以及更多细节。 - Dmitry
我删除缓存仅用于基准测试目的。我将分别添加一个关于如何处理原始问题(大量脏缓存)的新答案;希望它更清楚。 - Nominal Animal


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

如果你并行运行一个微基准测试,那就说一个程序不断地反复弄脏一个非常大的映射,并测量CPU时间(__builtin_ia32_rdtsc() 如果在x86或x86-64上使用GCC而不调用任何系统调用,你会发现即使内核看起来正在吃掉“全部”CPU时间,这个也会获得足够的CPU时间。只有当进程调用内部需要一些内存的内核函数(syscall)时,才会调用“阻塞”,在内核中等待页面刷新以产生新页面。

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

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

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

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

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


2
2017-10-03 17:33



感谢您的明确解释,目前正在研究中。 - Dmitry
非常欢迎你。我不在乎我的答案是否被接受,但我很想知道哪种解决方案能给你最好的结果。还有另一种方法,动态调整进程CPU和I / O优先级(nice 和 ionice),这可以 非常 对服务管理很有用,但我认为它们对延迟没有多大帮助。如果你告诉我们你的结果,我会非常感激,德米特里。 :) - Nominal Animal