FreeBSD virtual environment management and repository

2020-10 upd: we reached the first fundraising goal and rented a server in Hetzner for development! Thank you for donating !

Альтернативные источники получения модификаций FS

Один из популярных вопросов системных инженеров является задача по детализации и выявлению, какие процессы порождают определенные модификации на файловой системе, а также, где и какие файлы модифицируются в целом.

Например, если вы разрабатываете Embedded-устройство с FreeBSD в качестве firmware, установленной на MicroSD — и вам жизненно необходимо собрать статистику, какие файлы постоянно модифицируются, дабы эти места не представляли опасность для носителя информации с низким количеством циклов запись-форматирование.

Готовых решений для подобных задач — тьма (FAM,tripware,csync2 и различные решения, базирующиеся на kqueue/inotify механизмах)

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

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

К тому же, популярные решения как Linux DRDB и FreeBSD HASTD работают на raw-партишенах, находясь ниже уровня файловой системы и обслуживат байт-в-байт большие куски данных, что иногда может быть избыточно.

Например, из большого количества каталогов на жестком диске, вам нужно реплицировать только несколько, но разбросанные по всей иерархии:

		/usr/local/etc/nginx
		/root/etc
		/usr/home/web/sites.my.domain
		

при этом в репликации каталога /usr/home/web/sites.my.domain вам необходимо задать маску для исключений файлов, скажем, с расширением .php$

В этом случае, DRDB/HASTD будут могут выглядеть избыточно, особенно при репликации между дата-центрами на линках с высоким latency и желании иметь DFS в режиме multi-master

И если на работу периодически сканирующих локации программ вроде csync2 без слез смотреть невозможно (огромные всплески I/O), то решения на базе inotify/kqueue-based с первого взгляда, подходят идеально.

Проблемы начинаются, если вы хотите следить за файловой системой с миллионом каталогов и файлов (неэффективность использования классических FS в таких случаях оставим в стороне — в жизни случается всякое ;-)

И inotify и kqueue решения в подобных случаях, превращают сервер в тыкву, поскольку на каждый вложенный каталог требуется отдельный дескриптор, рекурсивно следить на всеми подкаталогами — нельзя.

Обдумывая возможные варианты реализации подобной DFS для реализации своей маленькой CDN сети, обратил внимание на давно известный функционал AUDITd в FreeBSD, имеющий во всех частях ядра хуки и позволяющий через фильтры выводить громадное количество событий, в том числе и fo (открытие), fw (запись), fc (закрытие) файлов.

При этом — никакого слежения ненадо, все работает событийно, независимо от используемой FS.

В связи с чем, можно сделать элементарный Auditd-based watcher:

		/*
		#1. Sample of /etc/security/audit_control:
		dir:/var/audit
		#реагируем только за file/dir creation и write операциями
		flags:+fc,+fd,+fw
		minfree:5
		naflags:+fc,+fd,+fw
		policy:cnt,argv
		filesz:1M
		expire-after:10M

		2) service auditd onestart

		3) How to use:
		 cc mypr.c -o mypr -lbsm
		 ./mypr <watchdirectory>
		*/

		#include <bsm/libbsm.h>

		#include <stdio.h>
		#include <stdlib.h>
		#include <unistd.h>
		#include <string.h>
		#include <regex.h>
		#include <fcntl.h>
		#include <sys/types.h>
		#include <sys/stat.h>
		#include <libgen.h>

		const char *auditpipe = "/dev/auditpipe";

		//skip for modification under /dev and sockets file
		int file_is_special(char *fp)
		{
			struct stat sb;
			int f,n;

			f = open( fp, O_RDONLY );
			if( f < 0 ) {
				return 2;
			}
			n = fstat( f, &sb );

			if( n < 0 ) {
				close(f);
				return 2;
			}

		if ((sb.st_mode & S_IFMT) == S_IFREG ) n=0;
			else n=1;
		close(f);
		return n;
		}
		int regex_match(char *tp,char *t1)
		{
		regex_t preg;
		regmatch_t substmatch[1];
		const char *ss = "";
		int match=0;

		regcomp(&preg, tp, REG_EXTENDED);
		if ( regexec(&preg, t1, 1, substmatch, 0) == 0 )
			{
			char *ns = malloc(substmatch[0].rm_so + 1 + strlen(ss) + (strlen(t1) - substmatch[0].rm_eo) + 2);
			  memcpy(ns, t1, substmatch[0].rm_so+1);
			  memcpy(&ns[substmatch[0].rm_so], ss, strlen(ss));
			  memcpy(&ns[substmatch[0].rm_so+strlen(ss)], &t1[substmatch[0].rm_eo],
						strlen(&t1[substmatch[0].rm_eo]));
			  ns[ substmatch[0].rm_so + strlen(ss) +
				  strlen(&t1[substmatch[0].rm_eo]) ] = 0;
			  match=1;
			  free(ns);
		   } else {
			  match=0;
		   }
		   regfree(&preg);
		   return match;
		}
		static int
		print_path(FILE *fp, char *dst)
		{
				u_char *buf;
				char sbuf[1024];
				tokenstr_t tok;
				int reclen;
				int bytesread;
				int part;
				int raw=1;

				while ((reclen = au_read_rec(fp, &buf)) != -1) {
						bytesread = 0;
						part=0;
						while (bytesread < reclen) {
								if (-1 == au_fetch_tok(&tok, buf + bytesread,
									reclen - bytesread))
										break;
								bytesread += tok.len;
								//token id ?
								if ((tok.id == 35)&&(regex_match(dst,tok.tt.path.path))) {
									if (file_is_special(tok.tt.path.path)==0) { 
									   printf("%s\n", tok.tt.path.path);
									   fflush(stdout);
									}
								}
						 }
						free(buf);
				}
				return (0);
		}

		int
		main(int argc, char **argv)
		{
				int ch;
				int i;
				FILE *fp;
				char *dst;

		if (argc!=2) {
				printf("Give me destination dir\r\n");
				exit(1);
		}
		i=sizeof(argv[1]);
		dst=malloc(i+3);
		snprintf(dst,i+3,"^%s",argv[1]);

		for (i = optind; i < argc; i++) {
						fp = fopen(auditpipe, "r");
						if ((fp == NULL) || (print_path(fp,dst) == -1))
								perror(argv[i]);
						if (fp != NULL)
								fclose(fp);
		}

		return (1);
		}
		

Что дальше делать с поступающими данными? Все что угодно: кормить анализаторам и делать статистику на аггресивность-частоту записи, приземлять в различные API для проливки файлов в различные GoogleCloud/Amazon/CDN

Следует учитывать, что /dev/auditpipe имеет определенный буфер и чисто теоретически, под большим DoS-м изменений на файловой системе возможны потери событий, если следить в real-time (в AUDITd есть отдельный флаг cnt, блокирующий выполнение кода до тех пор, пока AUDITd не обработает событие, те, на уровне AUDITD потерть быть не должно).

На практике же, даже без данного флага, мои попытки через random-генератор устроить флуд в виде огромного количества модификаций на файловой системе в несколько потоков, потерь не выявили (однако нагрузка на обработчик/auditd возрастала до 20% со стороны процессора).

На базе подобных открытий, для cbsd+csync2 (впоследствии, csync2 был выкинут — локи при апдейте SQLite3 базы делали схему медлительной) был написан демон примерно следующей реализации: (в его задачу входила реплика /usr/jails/jails-data/jail-XXX — данных клеток на N-ое количество серверов):

  • замена сканирования у csync2 на событийность, предоставляемую AUDIT. Те, csync2 не требовалось сканировать все файлы: все что менялось на ФС, обрабатывалось и скармливалось соотв. демоном.
  • Демон работал не с /dev/auditpipe, а с /var/audit/current (поскольку это был своего рода журнал, по которому упавшие ноды могли «догнать» состояние, а также, один источник модификаций можно было обработать различными инстансами)
  • Через kqueue механизмы, производилось слежение за /var/audit/current для своевременных переключений при ротации журнала.
  • про ротации или удалении/создании нового файла, журнал читается и обрабатывается до конца EOF, сохраняя смещение/позицию в памяти, используемую при следующем открытии через fseek(fp,journal_offset, SEEK_SET)
  • изменения проливались не моментально, а накапливались в определенную очередь (например в 5 слотов/записей), которую перед проливкой можно обработать на предмет сортировки дубляжей (не обрабатывать дважды один и тот же файл, проливать лишь его последнее состояние)
  • Кроме этого, имелся таймер по бездействию, по которому очередь, не заполнившаяся до 5, уходила на синхронизацию

Работа демона с выводом очереди (очередь установлена в 5 записей):

		./a.out /usr/jails/jails/jail1 /tmp jail1
		Cant open audit journal. Sleeping for a while...
		Cant open audit journal. Sleeping for a while...
		

— сервис каждые N секунд пробует открыть /var/audit/current — симлинк, которым управляет AUDITd. Если auditd не запущен, то файла нет, поэтому демон погружается в спячку на N секунду

После команды

		service auditd onestart
		Starting auditd.
		

Демон просыпается и начинается цикл чтения-обработки журнала, периодически переоткрывая файл когда AUDITd производит ротацию

		Cant open audit journal. Sleeping for a while...
		Symlink to /var/audit/20121013182808.not_terminated
		
		root@b9-64:/var/audit # service auditd onerestart
		Trigger sent.
		Starting auditd.
		
		Symlink change detected from [/var/audit/20121013182808.not_terminated] to [/var/audit/20121013183048.not_terminated], reopen
		Symlink to /var/audit/20121013183048.not_terminated
		

(вывод при выполнении cbsd jstop jail1 && cbsd jstart jail1)

		5,Q: /usr/jails/jails/jail1/var/run/syslog.pid
		4,Q: /usr/jails/jails/jail1/var/run/logpriv
		3,Q: /usr/jails/jails/jail1/var/run/log
		2,Q: /usr/jails/jails/jail1/var/run/sendmail.pid
		1,Q: /usr/jails/jails/jail1/var/spool/clientmqueue/sm-client.pid
		5,Q: /usr/jails/jails/jail1/var/run/ld-elf.so.hints
		4,Q: /usr/jails/jails/jail1/var/run/cron.pid
		3,Q: /usr/jails/jails/jail1/var/spool/lock/clean_var
		2,Q: /usr/jails/jails/jail1/var/run/clean_var
		1,Q: /usr/jails/jails/jail1/var/.diskless
		5,Q: /usr/jails/jails/jail1/var/run/clean_var
		

При достижении очереди в 5 записей, вызывается процедура на обновление файлов на удаленных нодах по соответствующему списку.

Вместо эпилога: Механизмы AUDITd на уровне ядра выглядят неплохой альтернативой, а главное — более легковесной при огромном количестве файлов. Что, в свою очередь, открывает еще один и довольно простой из вариантов реализации DFS master->many slave и, чуть посложнее но и интереснее: multimaster

К слову, ядро FreeBSD/Solaris и MacOSX, имеют одинаковый код AUDITd (в Линуксе он другой, но я не интересовался насколько он близок по функционалу) и кроме него имеют еще одно мощное средство, которое можно использовать для подобных задач — DTRACE.

Например, на языке D следить за открытием файлов можно так:

		BEGIN
		{
			dir = $1;
			dirlen = strlen(dir);
		}

		syscall::open*:entry
		/
		arg1 & (O_WRONLY | O_RDWR) &&
			substr(copyinstr(arg0),0,dirlen))==dir
		/
		{
			printf("%s\n",copyinstr(arg0));
		}
		

(именно на базе DTRACE-проб в FreeBSD 9.1+ появился filemon(4) — функционал, позволяющий запускать приложение и видеть его работу с дескрипторами.

Более того, в моих нагрузочных тестах, где под флудом модификаций на FS AUDITd прогружал CPU до 20%, DTRACE в качестве источника на тех же нагрузках cpu выше 2-4% не поднимал.

Дополнительно хочется добавить что AUDITD/DTRACE не единственные дешевые варианты получения модификаций из ядра и без следящих элементов, например, таким источником могут выступать различные proxy-like FS, например nullfs.