User:shimon/12309

Моя борьба с багом 12309

Когда при rsync'е директории размером в 100 Гб, которая присутствовала более чем наполовину на принимающей стороне, я получил практически непригодную к использованию систему и кучу процессов в борьбе за очередь ввода-вывода, пришлось покопать природу бага несколько глубже. Надоело потому что. В результате все пока что работает лучше, чем ожидалось.

Оптимистическое выделение памяти

Возможно, в научных программах какого-нибудь толка позволить выделить терабайт ОЗУ при наличии 3 Гб физической памяти и считается приемлемым, но на десктопе, где много процессов должны спокойно сосуществовать, такой расклад неприемлем — зажравшаяся программа спокойно вытеснит все остальное, после чего система практически остановится. Хуже всего то, что суть бага 12309 в том, что ядро принимает решения о том, какие страницы вытеснять, мягко говоря, неоптимально, а чинить это долго, муторно, и не в каждой ситуации решение будет приемлемым.

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

Пока что же можно сделать так, чтобы на явно дикие запросы malloc() отвечал решительным отказом, и чтобы зажратость программы определялась на этапе выделения памяти, а не тогда, когда программа радостно вывалит туда пару гигабайт данных.

Для этого нужно прописать sysctl

 vm.overcommit_memory = 2

Максимум памяти, который можно будет выделить, будет равен в сумме объему свопа + некоторому проценту физической памяти. Этот процент по умолчанию равен 50, но можно его несколько увеличить. Во всяком случае, я выставил его в 80 и пока что катастроф нет.

 vm.overcommit_ratio = 80

Своп нужен

Некоторые люди полагают, что если отключить своп, то 12309 исчезнет. А вот как бы не так. Своп — это хранилище анонимных страниц памяти. Код исполняемых программ и всяких библиотек неанонимен и по умолчанию неизменяем. В то время как на 32-битных системах исполняемый код зачастую зависим от позиции (начального адреса), что приводит к тому, что, во-первых, динамический линкер проводит вычисление смещений каждый раз при загрузке и, соответственно, страницы кода анонимны (это несет с собой недостаток в виде наличия нескольких копий одной и той же библиотеки, но и преимущество в виде невозможности вытеснить страницы кода для освобождения памяти), то на 64-битных системах практически весь код линкуется в независимом от позиции виде (PIC).

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

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

Уменьшение размеров дисковых буферов

С одной стороны, отдавать под дисковые буферы практически всю свободную память — здравая идея. А с другой стороны, чем больше ОЗУ, на самом деле, тем сильнее это способно ударить в критической ситуации.

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

То есть, кеш на чтение — это ничего так, а слишком большой кеш на запись способен встать поперек горла в критических случаях.

Есть еще одна неприятная особенность, связанная трудно сказать, с чем — возможно, с реализацией DMA, но вполне возможно, что не с ней, или не только с ней. Берем какой-нибудь медленный для записи носитель, типа той же USB-флешки и пробуем записать на него данных побольше, фильм какой или что-то навроде. Мы увидим, что происходит это рывками — сначала заполняется буфер, сколько влезет, а потом весь сбрасывается, потом весь заполняется... и так далее. При этом, суммарно потраченное время, почему-то, ощутимо больше, чем как если бы мы примонтировали носитель с -o sync, а скорость записи на, собственно, носитель невообразимо мала.

Но если уменьшить порог количества грязных блоков, после которого начнется их сброс на носитель, не до сверхмалых величин, но все же — это позволит проводить зачитку данных из источника и запись на носитель параллельными DMA-трансферами. Я у себя выставил этот объем равным 2 мегабайтам, что, с одной стороны, уменьшает количество перезаписей в случае частой смены маленьких файлов и значительно увеличивает скорость переноса больших объёмов данных. Возможно, если поиграться размером, можно найти оптимальное быстродействие, но не думаю, что буфер больше 16 мегабайт будет эффективным.

echo 2097152 >/proc/sys/vm/dirty_bytes
echo 2097152 >/proc/sys/vm/dirty_background_bytes

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

Если что, dirty_bytes должно делиться на 4096 нацело.

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

Старые ядра

Есть такие ситуации и устройства, где ядро слишком старое, чтобы содержать все вышеописанные крутилки. Например, никаких dirty_bytes и dirty_background_bytes нет в ядре 2.6.29, штатном для Nokia N900. Этот телефон быстро теряет всю интерактивность и многозадачность, когда кто-то что-то пишет на eMMC в больших объемах.

Старые ядра тоже имеют крутилки размера буферов, но они чуть хитрее выглядят.

  • /proc/sys/vm/dirty_ratio — процент от количества основной памяти, в страницах (!), при переполнении которых пишущий процесс начинает сбрасывать кеши на носитель.
  • /proc/sys/vm/dirty_background_ratio — то самое для процесса pdflush (который заведует свопом). Своп-то у нас тоже кешируется.

После выставления

vm.dirty_ratio = 10
vm.dirty_background_ratio = 8

отзывчивость устройства резко повысилась (изначально там 40 и 10 стояло).