Памяти много не бывает - эта непреложная истина преследует вычислительную технику с самых ее начал. В этой статье я рассмотрю проблемы с исчерпанием виртуальной памяти из-за tmpfs.

Ищем причину
Также как и память приложений, данные tmpfs размещаются в anonymous memory. Таким образом исчерпание памяти в tmpfs приводит к серьезным проблемам - таким же как и при исчерпании памяти - интенсивный paging/swaping на диск и замедление работы системы. При этом наблюдаются ошибки вида:
bash: fork: Not enough space
Ядро при этом сигнализирует о сбое в msgbuf:
/tmp: File system full, swap space limit exceeded
Единственно возможный вариант - создание креш-дампа. Как правило Solaris Crash Analysis Tool уже на этапе проверки креш-дампа обнаружит проблемы с tmpfs:
WARNING: tmpfs filesystem on /tmp using 4.66G virtual memory
Посмотрим, каким образом можно вытащить информацию о "виновнике" проблемы. Самый простой вариант - использование команды tmpfs из Solaris Crash Analysis Tool
CAT(vmcore.3/10U)> tmpfs | sort -nk 4 | tail -5
- 0x3000b120d40 0 10485760 /tmp/test/a10
- 0x30004813580 0 104857600 /tmp/test1
- 0x3000706c940 0 104857600 /tmp/test2
- 0x30008418f40 0 104857600 /tmp/test3
- 0x30007e04d00 0 132653056 /tmp/yeeees!
Начиная с версии 5.3 кстати самые большие файлы подсвечиваются красным.
К сожалению после покупки Sun'а компанией Oracle Solaris Crash Analysis Tool оказался доступен только обладателям поддержки (летом 2011 года поддержку проекта вообще прекращали). Поэтому я написал обертку над mdb -
mdb.py и несколько скриптов, соответствующих используемым в статье командам:
anon summary -
anonusage.py
tmpfs -
tmpfsdump.py
findfiles -
findfiles.py
По-умолчанию они запускают mdb с опцией -k, чтобы изменить поведение нужно изменить строчку MDB_COMMAND = 'mdb -k' в скрипте
Более сложный вариант - когда файл занимает место в tmpfs, но на него был сказан unlink(2), а сам файл не был закрыт - в этом случае, как известно Solaris не удаляет файл до тех пор, пока он не будет закрыт. В этом случае выявить нарушителя спокойствия системного администратора тоже можно. Как уже было сказано, tmpfs хранит свои данные в anonymous memory, и рассказать об использовании памяти может команда SolarisCAT anon summary:
CAT(vmcore.3/10U)> anon summary
reading anon_hash...
total: 82647 (645M)
total with swap assigned: 45761 (357M)
total w/in-memory pages: 39739 (310M)
zero refcnt: 0
pages vnode
=============== =============
16193 (126M) 0x30009961dc0 <--- наибольшие потребители в tmpfs
12800 (100M) 0x3000b173b80
12800 (100M) 0x3000af00f40
12800 (100M) 0x3000b173980
4664 (36.4M) 0x300083ddac0
4576 (35.7M) 0x30002a4b680
...
pages vnode ops
=============== =========
61716 (482M) *tmpfs(bss):tmp_vnodeops
20931 (163M) *genunix(bss):swap_vnodeops
<cut>
Выяснить информацию о vnode можно используя команды getpath и findfiles
CAT(vmcore.3/10U)> getpath 0x30009961dc0
/tmp/yeeees!
CAT(vmcore.3/10U)> findfiles -n 0x30009961dc0
Process 2603: yes
file 1 @ 0x30007e48690 2878344: file @ 0x30007e48690
vnode @ 0x30009961dc0 /tmp/yeeees!
v_op: *tmpfs(bss):tmp_vnodeops
TMPFS file
mode: 100644 size: 132653056 bytes
Проактивно мониторим
Самый простой способ мониторинга - команды df и du, более сложный вариант - DTrace, чем мы и воспользуемся. Простейший скрипт будет выглядеть следующим образом:
tmp_resv:entry,
tmp_unresv:entry {
printf("%d %s %d for file %s", pid, probefunc,
arg2, stringof(((struct tmpnode*) arg1)->tn_vnode->v_path));
}
функция tmp_resv отвечает за резервирование места (и получает в arg2 - требуемый объем в байтах), аналогично tmp_unresv - за ее освобождение.
В итоге, DTrace-скрипт, выводящий статистику по процессам, использующим файлы в /tmp будет выглядеть следующим образом
#define PAGESHIFT `_pageshift
#define PAGEOFFSET `_pageoffset
#define btoprtob(x) ((((x) + PAGEOFFSET) >> PAGESHIFT) << PAGESHIFT)
tmp_resv:entry
/arg3/ {
@s["resv", pid, execname] = sum(btoprtob(arg2));
@c["resvcount", pid, execname] = count();
}
tmp_unresv:entry {
@s["unresv", pid, execname] = sum(btoprtob(arg2));
@c["unresvcount", pid, execname] = count();
}
tick-2s
{
printa(@s);
printa(@c);
clear(@s);
clear(@c);
}
В DTraceToolkit ничего связанного с tmpfs я не нашел, поэтому решил написать свою утилиту. Ее можете скачать здесь: tmptop.py
Вызвать ее можно следующей командой:
./tmptop.py [-h] [-k user,proc,mntpt,file] [-s resv,unresv,delta,total]
[-l limit] [interval [count]]
-h выводит данные в формате human-readable
-k выбирает ключ, по которому группируются данные
-s выбирает ключ, по которому осуществляется сортировка в выборке
-l лимитирует вывод
Параметры interval и count в представлении не нуждаются :)
Пример вывода утилиты:
# ./tmptop.py -h -l 5
RESV/S UNRESV/S RESV UNRESV DELTA TOTAL VP SIZE FILENAME
0.50 0.00 +10.0M 0.0b +10.0M +10.0M 0x3000C4720C0 10.0M /tmp/file10
0.50 0.00 +9.0M 0.0b +9.0M +9.0M 0x3000C4721C0 9.0M /tmp/file9
0.50 0.00 +8.0M 0.0b +8.0M +8.0M 0x3000C4722C0 8.0M /tmp/file8
0.50 0.00 +7.0M 0.0b +7.0M +7.0M 0x3000C4723C0 7.0M /tmp/file7
0.50 0.00 +6.0M 0.0b +6.0M +6.0M 0x3000C4724C0 6.0M /tmp/file6
DELTA - это изменение потребления памяти за интервал измерения, а TOTAL - с момента запуска скрипта
Ограничиваем tmpfs
Самый простой способ установить ограничение на tmpfs - установить свойство size в опциях монтирования tmpfs, тогда строчка монтирования в vfstab будет выглядеть так:
swap - /tmp tmpfs - yes size=100m
После этого в avail появится соответствующее значение:
# df -F tmpfs -h
Filesystem size used avail capacity Mounted on
swap 383M 392K 382M 1% /etc/svc/volatile
swap 100M 32K 100M 1% /tmp
swap 382M 48K 382M 1% /var/run
Ограничение будет вести себя следующим образом:
# mkfile 101m /tmp/oopsy
Could not set length of /tmp/oopsy: No space left on device
Jan 17 14:56:24 unknown tmpfs: WARNING: /tmp: File system full, swap space limit exceeded
Более сложный способ - вводить ограничение на anonymous memory, в этом случае можно использовать возможность Solaris Resource Control, в частности свойство zone.max-swap (в этом случае ограничение устанавливается на зону).
# zonecfg -z test-sparse
zonecfg:test-sparse> add capped-memory
zonecfg:test-sparse:capped-memory> set swap=128m
zonecfg:test-sparse:capped-memory> info
capped-memory:
[swap: 128M]
zonecfg:test-sparse:capped-memory> end
zonecfg:test-sparse> verify
zonecfg:test-sparse> commit
zonecfg:test-sparse> exit
Также как и в случае со свойством size, будет выдаваться сообщение "File system full, swap space limit exceeded". Однако с этим параметром надо быть аккуратнее, так как свойство max-swap влияет как на tmpfs так и на память приложений.
И наконец, в-третьих, в tmpfs есть параметр tmpfs_minfree (указывается в страницах), указывающее, сколько памяти должно оставаться свободной в anonymous memory, чтобы tmpfs могла выделять память. По-умолчанию это свойство равно 2м мегабайтам, что для современных систем явно недостаточно. Установить его можно как через /etc/system:
set tmpfs:tmpfs_minfree = 0x3200
Так и на запущенной системе через mdb:
echo 'tmpfs_minfree/W0x3200' | mdb -kw
Подробнее об этой опции можно почитать в Solaris Tunable Parameters Reference Manual: http://docs.oracle.com/cd/E19683-01/806-7009/chapter2-49/index.html
mdb.py
anonusage.py
tmpfsdump.py
findfiles.py
tmptop.py