11

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

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

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

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

由于ref-upd10ref-upd11 ,我有一个想法echo 3 > /proc/sys/vm/drop_caches不需要解决内存分配缓慢的问题。在启动消耗内存的应用程序之前运行“同步”就足够了。明天可能会在生产中尝试这个并在这里发布结果。

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

  1. 我执行了cat 10GB.fiel > /dev/null,然后
  2. sync可以肯定的是,没有任何页面(cat /proc/meminfo |grep ^Dirty显示 184kb.
  3. 检查cat /proc/meminfo |grep ^Cached我得到:4GB缓存
  4. 运行int main(char**)我得到了正常的性能(比如初始化 32MB 的分配数据需要 50 毫秒)。
  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. 以下是部分结果:

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

  6. 测试总结:丢失的 durty pages 显着减慢了对分配内存的首次访问(公平地说,这仅在应用程序总内存开始与整个 OS 内存相当时才开始发生,即如果您有 8 个 16 GB 空闲,它分配1GB没问题,从3GB左右开始减速)。

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

开发机器配置:

  1. Linux 2.6.32-220.13.1.el6.x86_64 - 科学 Linux 版本 6.1(碳)
  2. 内存:15.55 GB
  3. CPU:1 X Intel(R) Core(TM) i5-2300 CPU @ 2.80GHz(4 线程)(物理)

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

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;
}

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

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 使用率甚至没有从地面上升,就像所有内核在 htop 中的使用率都低于 5%。

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

upd2:所有机器都安装了相同的centos linux 2.6,我使用ssh与它们一起工作。

upd3: A:它不太可能被交换,在我的测试过程中没有看到任何磁盘活动,并且大量的 RAM 也是可用的。(另外,descriptin 已更新)。– 德米特里 9 分钟前

upd4: htop 表示内核的 HI CPU 利用率,al 内核的利用率高达 100%(在产品上)。

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文件系统缓存页面,将页面一一刷新到磁盘需要很多时间。但总同步工作得很快并消除了问题。

4

2 回答 2

8

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

在你的开发机器上。这是一种确保缓存为空的安全、非破坏性方法。(运行上述命令不会丢失任何数据,即使您碰巧同时保存或写入磁盘。确实很安全。)

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

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

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

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

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

编辑补充:我怀疑确实发生了减速,并且是由于 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.

于 2012-09-29T11:23:26.527 回答
2

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

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

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

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

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

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

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

于 2012-10-03T17:33:58.170 回答