Любой способ записи логов так или иначе даёт дополнительную нагрузку на CPU и замедляет выполнение кода. Но запись в syslog имеет одну очень неприятную особенность: оно может быть блокирующем и может приостанавливать поток выполнения на очень значительный промежуток времени.

Причина кроется в использовании сокета домена Unix в режиме датаграмм (socket(AF_UNIX, SOCK_DGRAM, 0)) с путём /dev/log в файловой системе. Этот сокет имеет свой буффер, который может переполняться. При его переполнении, в зависимости от реализации (я бегло просмотрел несколько), может выводиться сообщение об ошибке в консоль либо сам процесс отправки сообщения блокироваться. В реализации библиотеки C от glibc поток выполнения именно блокируется вызовом syslog. Попробую это показать на наглядном примере.

Проверка времени, затраченного на выполнение вызова syslog

Для начала нужен исходный код примера, сохраните его как файл с именем app.c:

/*
 * The example shows that a process can wait on 'syslog' call.
 *
 * Compilation:
 * $ gcc --std=c89 -o app app.c
 *
 * Run example:
 * $ ./app > out
 *
 * In a second terminal:
 * # cpulimit --pid $(pidof systemd-journald) --limit 1
 */
#define _POSIX_C_SOURCE 199309L
#include <stdio.h>
#include <syslog.h>
#include <time.h>

#define MESSAGE_SIZE 8192

int main() {
	struct timespec before, after;
	time_t seconds;
	long useconds;
	char message[MESSAGE_SIZE];
	int i;

	/* fills a message */
	for(i = 0; i < MESSAGE_SIZE - 1; i++) {
		message[i] = 'X';
	}
	message[MESSAGE_SIZE - 1] = '\0';

	/* you can to change flags in your taste */
	openlog("ozilog", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1);

	/* you need to press Ctrl+C to close the program */
	while(1) {
		/* get time before logging */
		clock_gettime(CLOCK_REALTIME, &before);
		/* log the message */
		syslog(LOG_INFO, message);
		/* get time after logging */
		clock_gettime(CLOCK_REALTIME, &after);
		/* calculate time of logging */
		seconds = after.tv_sec - before.tv_sec;
		useconds = (after.tv_nsec / 1000) - (before.tv_nsec / 1000);
		if (useconds > 999999) {
			useconds = 0;
			seconds++;
		}
		if (useconds < 0) {
			useconds += 1000000;
			seconds--;
		}
		/* and output the result in us */
		printf("%u\n", (unsigned) seconds * 1000000 + (unsigned) useconds);
	}
}

Или просто скачайте.

Затем скомпилируйте:

$ gcc --std=c89 -o app app.c

И запустите (для удобства анализа и чтобы избежать ненужных задержек из-за вывода в консоль — с перенаправлением вывода в файл):

$ ./app > out_free

В файле out_free будут накапливаться числа — время выполнения вызова syslog в миллисекундах. Теперь откройте второй терминал и посмотрите на вывод команды top:

Потребление процессора при логировании в syslog

Как видно, наше приложение не может полностью загрузить ядро процессора (а на ПК у меня их 6), что-то ему мешает. Проверим время выполнения вызова syslog. Для этого нужно остановить выполнение программы app и использовать магию bash для сортировки полученных чисел (я опустил за … некоторые значения, слева — кол-во, справа — задержка в миллисекундах):

$ kill -SIGKILL $(pidof app)
$ cat out_free | sort -n | uniq -c
  70444 3
 878422 4
 569593 5
 372045 6
  98590 7
   8680 8
  44451 9
...
      1 1203
      1 1238
      1 1325
      1 2563

Время выполнения отдельных вызовов может достигать микросекунд. Но может быть это просто ошибка в измерениях (ядро и задача не real-time, приоритет не высокий — может кто-то вытеснял) или банально неверная интерпретации результатов? Вполне возможно, всякое бывает.

Попробуем сэмулировать высокую нагрузку на процессор, чтобы демону, читающему данные с сокета /dev/log, оставалось мало времени на обработку входящих сообщений. Или, что несколько проще, ограничим это самое время для демона логирования с помощью команды cpulimit, установить её в Arch Linux можно так:

# pacman -s cpulimit

А в Debian, Ubuntu и т.д. вот так:

# apt-get install cpulimit

Теперь можно ограничить потребление CPU для демона в Linux, который занимается записью логов, оставив тому 1%.

# cpulimit --pid $(pidof systemd-journald) --limit 1

Ещё раз запустим наше приложение с перенаправлением вывода в другой файл:

$ ./app > out_load

И посмотрим на вывод команды top.

Потребление процессора при логировании в syslog при ограничении для демона логирования

Программа практически не работает. В принципе, этого уже достаточно, но можно посмотреть и на сухие числа (в левом столбце — кол-во, в правом — задержка в миллисекундах):

$ kill -SIGKILL $(pidof app)
$ cat out_free | sort -n | uniq -c
      9 5
    158 6
     68 7
    145 8
    533 9
...
      1 2305165
      1 3411570
      1 3513700
      1 5015429

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

Заключение

Если вы не хотите, чтобы под высокой нагрузкой поток выполнения программы был заблокирован надолго, то не стоит использовать syslog, ну а если используете — не нужно очень часто туда что-то писать (но ведь очень часто может писать кто-то другой, а нас вас места уже не хватит).

Можно воспользовать уже готовыми решениями для обхода этой проблемы, благо библиотек для логирования предостаточно. Или же можно самостоятельно открыть сокет /var/log в неблокирующем режиме и что-то придумать для обработки ошибок записи (в первую очередь EAGAIN). К примеру, подобный подход реализован тут.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Навигация по записям