Подтвердить что ты не робот

Использование Linux с высоким ядром процессора при инициализации памяти

У меня возникла проблема с высокой загрузкой процессора ядром linux, при загрузке моих java-приложений на сервере. Эта проблема возникает только в производстве, на серверах dev все работает быстро.

upd9: В этой проблеме было два вопроса:

  • Как это исправить? - Номинальное животное предложило синхронизировать и отбрасывать все, и это действительно помогает. sudo sh -c 'sync ; echo 3 > /proc/sys/vm/drop_caches ; Работает. upd12: Но действительно sync достаточно.

  • Почему это происходит?. Он по-прежнему открыт для меня, я понимаю, что кратковременные страницы на диске потребляют процессор ядра и время ввода-вывода, это нормально. Но что такое страх, почему даже однопоточное приложение, написанное на "C", загружает все ядра на 100% в пространстве ядра?

Из-за ref upd10 и ref upd11 У меня есть идея, что echo 3 > /proc/sys/vm/drop_caches не требуется для исправления моей проблемы при медленном распределении памяти. Достаточно запустить `sync ' перед запуском приложения, потребляющего память. Вероятно, вы попробуете это сделать в производстве и опубликовать результаты здесь.

upd10: Потерянный файл кеша кеша:

  • Я выполнил cat 10GB.fiel > /dev/null, затем
  • sync чтобы убедиться, что никакие страницы durty (cat /proc/meminfo |grep ^Dirty не отображаются 184kb.
  • Проверка cat /proc/meminfo |grep ^Cached Я получил: 4 ГБ кэширования
  • Запуск int main(char**) У меня нормальная производительность (например, 50 мс для инициализации 32 МБ выделенных данных).
  • Объем кэшированной памяти до 900 МБ
  • Сводка тестов: Я думаю, что Linux не проблема для восстановления страниц, используемых в кеше FS, в выделенную память.

upd11: МНОГО грязных страниц.

  • Элемент списка

  • Я запускаю свой HowMongoDdWorks пример с комментариями read и через некоторое время

  • /proc/meminfo сказал, что 2,8 ГБ Dirty, а 3,6 ГБ - Cached.

  • Я остановился HowMongoDdWorks и запустил мой int main(char**).

  • Вот часть результата:

    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,05 с x 0 [попытка 1/часть 2] время 0.42s x 1 [попытка 2/часть 2] время 0,04 с

  • Сводка по тесту: потерянные страницы долготы значительно замедляют первый доступ к выделенной памяти (честно говоря, это начинает происходить только тогда, когда общая память приложения начинает сравниваться со всей памятью ОС, т.е. если у вас есть 8 из 16 ГБ бесплатно, без проблем выделять 1 ГБ, замедление от 3 ГБ или около того).

Теперь мне удалось воспроизвести эту ситуацию в моей среде dev, так что вот новые подробности.

Конфигурация машины Dev:

  • Linux 2.6.32-220.13.1.el6.x86_64 - Scientific Linux release 6.1 (Carbon)
  • Оперативная память: 15,55 ГБ
  • Процессор: 1 X Intel (R) Core i5-2300 CPU @2,80 ГГц (4 потока) (физический)

Это 99,9%, что проблема вызвана большим количеством страниц durty в кеше 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% по всем ядрам) в ядре ядра (то же, что и ниже, но я его еще раз скопирую).

#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: обе, dev и производственные системы достаточно велики для этого теста. upd7: это не пейджинг, по крайней мере, я не видел активности IO хранения во время проблемного времени.

  • dev ~ 4 ядра, 16 ГМ оперативной памяти, ~ 8 ГБ бесплатно
  • производство ~ 12 ядер, 24 ГБ RAM, ~ 16 ГБ бесплатно (от 8 до 10 ГМ находится под кешем FS, но нет разница, такие же результаты, даже если все 16GM полностью бесплатны), также эта машина загружается процессором, но не слишком высока ~ 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
...

При запуске этого теста на машине разработки использование ЦП даже не выросло из gound, так как все ядра имеют менее 5% использования в htop.

Но выполнение этого теста на производственной машине я вижу до 100% использования ЦП всеми ядрами (средняя загрузка увеличивается до 50% на машине с 12 ядрами), и все это время ядра.

UPD2:все машины имеют один и тот же centos linux 2.6, я работаю с ними с помощью ssh.

upd3: A: вряд ли это будет замена, не видели активности диска во время моего теста, и много ОЗУ также бесплатное. (также обновляется описатель). - Дмитрий 9 мин назад

upd4: htop говорит о загрузке процессора HI ядром, до 100% использования al core (на prod).

upd5: работает ли загрузка процессора после завершения инициализации? В моем простом тесте - Да. Для реального применения это помогает остановить все остальное, чтобы начать новую программу (что является бессмыслицей).

У меня есть два вопроса:

  • Почему это происходит?

  • Как это исправить?

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...

Факты, которые я узнал из этого теста.

  • Распределение памяти происходит быстро.
  • Первый доступ к выделенной памяти выполняется быстро (так что это не проблема с ленивым распределением буфера).
  • Я разбил выделенный буфер на части (8 в тесте).
  • И заполняя каждую часть буфера значением 0, а затем со значением 1, время, затрачиваемое на печать.
  • Заполнение второй буфером всегда быстрая.
  • НО заполнение части буфера furst всегда немного медленнее, чем второе заполнение (я считаю, что некоторые дополнительные работы выполняются с моим ядром при доступе к первой странице).
  • В некоторых случаях для заполнения части буфера значения в первый раз требуется ЗНАЧИТЕЛЬНО дольше.

Я попробовал предложить anwser, и, похоже, это помогло. Я снова проведу и снова отправлю результаты.

Похоже, что на картах linux выделены страницы для страниц кэша файловой системы durty, и требуется много времени, чтобы сбрасывать страницы на диск один за другим. Но полная синхронизация работает быстро и устраняет проблему.

4b9b3361

Ответ 1

Run

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

на вашей машине dev. Это безопасный, неразрушающий способ убедиться, что ваши кеши пусты. (Вы не потеряете какие-либо данные, выполнив приведенную выше команду, даже если вам удастся сохранить или записать на диск в одно и то же время. Это действительно безопасно.)

Затем убедитесь, что у вас не работает Java файл, и для повторного запуска вышеуказанной команды. Вы можете проверить, есть ли у вас Java, например,

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

Он ничего не должен выводить. Если это произойдет, сначала закройте свой Java-материал.

Теперь заново запустите тест приложения. Также происходит ли такое же замедление на вашей машине dev?

Если вы хотите оставить комментарий в любом случае, Дмитрий, я был бы рад продолжить изучение проблемы.

Отредактировано для добавления: я подозреваю, что замедление происходит, и связано с большой задержкой запуска, вызванной самой Java. Это очень распространенная проблема и, в основном, встроенная в Java, является результатом ее архитектуры. Для более крупных приложений задержка запуска часто занимает значительную долю секунды, независимо от того, насколько быстро машина, просто потому, что Java должна загружать и готовить классы (в основном серийно, поэтому добавление ядер не поможет).

Другими словами, я считаю, что вина должна быть на Java, а не на Linux; совершенно наоборот, так как Linux удается смягчить задержку на вашей машине разработки с помощью кэширования на уровне ядра - и это только потому, что вы постоянно используете эти компоненты Java, поэтому ядро ​​знает, что они кэшируют их.

Изменить 2: Было бы очень полезно увидеть, к каким файлам ваша среда Java обращается, когда ваше приложение запущено. Вы можете сделать это с помощью strace:

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

который создает файл trace.log, содержащий syscalls open(), выполняемый любым из процессов, запущенных с помощью COMMAND.... Чтобы сохранить вывод trace.PID для каждого процесса, запускается COMMAND..., используйте

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

Сравнение выходов на ваших установках dev и prod скажет вам, действительно ли они эквивалентны. У одного из них могут быть дополнительные или отсутствующие библиотеки, влияющие на время запуска.

В случае, если установка устарела, а системный раздел является достаточно полным, возможно, что эти файлы были фрагментированы, в результате чего ядро ​​потратит больше времени, ожидая завершения ввода-вывода. (Обратите внимание, что количество операций ввода/вывода остается неизменным, и только время, необходимое для завершения, увеличивается, если файлы фрагментированы.) Вы можете использовать команду

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 кеш-страница ядра в основном грязная, т.е. большинство кэшированных файлов - это материал, который будет записан на диск.

Когда ваше приложение выделяет новые страницы, ядро ​​настраивает только сопоставления страниц, это фактически не дает физической ОЗУ сразу. Это происходит только при первом доступе к каждой странице.

При первом доступе ядро ​​сначала находит свободную страницу - обычно страницу, содержащую "чистые" кэшированные данные, то есть что-то прочитанное с диска, но не измененное. Затем он очищает его до нулей, чтобы избежать утечки информации между процессами. (При использовании средств выделения библиотеки 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.

Ответ 2

Когда у ядра заканчиваются доступные чистые страницы, он должен очищать грязные страницы на диске. Промывка большого количества грязных страниц на диск выглядит как высокая загрузка процессора, потому что для большинства приложений на стороне ядра требуется одна или несколько страниц (временно) для работы. По сути, ядро ​​ожидает завершения ввода-вывода, даже если приложения пользовательского пространства называли функцию ядра, не связанную с I/O.

Если вы запускаете параллельную микробиблиотеку, скажите, что программа постоянно обновляет многократное отображение снова и снова и измеряет время CPU (__builtin_ia32_rdtsc() при использовании GCC на x86 или x86-64) без вызова каких-либо системных вызовов, вы должны увидеть, что этот процессор получает много процессорного времени, даже когда ядро, по-видимому, есть "все" процессорного времени. Только когда процесс вызывает функцию ядра (syscall), которая внутренне требует некоторую память, это вызовет "блок", застрявший в ядре для очистки страницы, чтобы получить новые страницы.

При запуске тестов обычно достаточно просто запустить sudo sh -c 'sync ; echo 3 >/proc/sys/vm/drop_caches ; sync' пару раз перед запуском теста, чтобы убедиться, что во время теста не должно быть избыточного давления в памяти. Я никогда не использую его в рабочей среде. (Хотя безопасно работать, т.е. Не теряет данные, это похоже на убийство москитов с помощью кувалды: неправильный инструмент для задания.)

Когда вы находите в рабочей среде, что ваши латентности начинают слишком сильно увеличиваться из-за того, что ядро ​​очищает грязные страницы, что, по моему мнению, на максимальной скорости устройства, возможно, также вызывает икоту в скорости ввода-вывода приложений, вы можете настроить механизм очистки грязной страницы ядра. В принципе, вы можете сказать ядру, чтобы быстрее сбросить грязные страницы на диск, и убедитесь, что в любой момент времени (если возможно) будет не так много грязных страниц.

Грегори Смит написал о теории и настройке механизма промывки здесь. Короче говоря, /proc/sys/vm/ содержит настройки ядра, которые вы можете изменить. Они имеют значение reset по умолчанию при загрузке, но вы можете легко написать простой init script to echo нужные значения для файлов при загрузке. Если процессы, выполняемые на производственной машине, выполняют тяжелые операции ввода-вывода, вы также можете посмотреть на настройки файловой системы. Как минимум, вы должны подключить свои файловые системы (см. /etc/fstab) с помощью флага relatime, чтобы время доступа к файлу обновлялось только для первого доступа после того, как файл был изменен или его статус изменился.

Лично я также использую низкоуровневое предварительно выпущенное ядро ​​с таймером 1000 Гц для мультимедийных рабочих станций (и для серверов мультимедиа, если бы у меня было прямо сейчас). Такие ядра запускают пользовательские процессы в более коротких срезах и обычно обеспечивают гораздо лучшие задержки, хотя максимальная вычислительная способность немного меньше. Если ваши производственные службы чувствительны к задержкам, я рекомендую переключать серверы производства на такие ядра.

Многие дистрибутивы уже предоставляют такие ядра, но мне гораздо проще перекомпилировать ядра дистрибутива или даже переключиться на ядра ядра .org. Процедура проста: вам нужна разработка ядра и установленные инструменты (в вариантах Debian make-kpkg ОЧЕНЬ полезно). Чтобы обновить ядро, вы получаете новые источники, настраиваете ядро ​​(обычно используя текущую конфигурацию в качестве основы - make oldconfig), создавайте новое ядро ​​и устанавливаете пакеты перед перезагрузкой. Большинство людей считают, что просто обновление аппаратного обеспечения более экономически выгодно, чем перекомпиляция ядер дистрибутивов, но я считаю, что перекомпилировать ядра очень легко. В любом случае я не буду автоматически перезагружаться для обновлений ядра, поэтому добавление простого шага (запускаемого одним script) до перезагрузки не слишком много для меня.