Ограничение памяти Docker приводит к тому, что SLUB не может выделяться с большим кешем страниц

Учитывая процесс, который создает большой кеш страниц ядра Linux через файлы mmap'd, выполнение в докер-контейнере (cgroup) с ограничением памяти вызывает ошибки выделения слэба ядра:

Jul 18 21:29:01 ip-10-10-17-135 kernel: [186998.252395] SLUB: Unable to allocate memory on node -1 (gfp=0x2080020)
Jul 18 21:29:01 ip-10-10-17-135 kernel: [186998.252402]   cache: kmalloc-2048(2412:6c2c4ef2026a77599d279450517cb061545fa963ff9faab731daab2a1f672915), object size: 2048, buffer size: 2048, default order: 3, min order: 0
Jul 18 21:29:01 ip-10-10-17-135 kernel: [186998.252407]   node 0: slabs: 135, objs: 1950, free: 64
Jul 18 21:29:01 ip-10-10-17-135 kernel: [186998.252409]   node 1: slabs: 130, objs: 1716, free: 0

Просмотр slabtop Я вижу, что количество объектов buffer_head, radix_tree_node и kmalloc * сильно ограничено в контейнере, запущенном с ограничением памяти. По-видимому, это имеет патологические последствия для пропускной способности ввода-вывода в приложении и наблюдается с помощью iostat. Этого не происходит, даже если кэш страниц использует всю доступную память на хост-ОС, работающей вне контейнера или контейнера без ограничения памяти.

Это кажется проблемой в учете памяти ядра, когда кеш страниц ядра не учитывается в памяти контейнеров, а в объектах SLAB, которые его поддерживают. Поведение кажется неправильным, потому что при запуске, когда большой пул объектов slab предварительно выделен, контейнер с ограниченной памятью работает нормально, свободно используя существующее пространство slab. Только плита, выделенная в контейнере, засчитывается в контейнер. Кажется, что никакая комбинация параметров контейнера для памяти и памяти ядра не решает проблему (за исключением того, что не устанавливают ограничение памяти вообще или ограничение настолько большое, что не ограничивает slab, но это ограничивает адресуемое пространство). Я попытался полностью отключить учет kmem, пропустив cgroup.memory=nokmem при загрузке.

Информация о системе:

  • Linux ip-10-10-17-135 4.4.0-1087-aws # 98-Ubuntu SMP
  • AMI Ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-20190204.3
  • Docker версия 18.09.3, сборка 774a1f4
  • Java 10.0.1 2018-04-17

Чтобы воспроизвести проблему, вы можете использовать мой PageCache Java-код. Это простой пример репродукции встроенной библиотеки базы данных, которая активно использует файлы, отображаемые в памяти, для развертывания в очень быстрой файловой системе. Приложение развертывается на экземплярах AWS i3.baremetal через ECS. Я сопоставляю большой том от хоста до док-контейнера, где хранятся файлы с отображенной памятью. Агент AWS ECS требует установки ненулевого предела памяти для всех контейнеров. Ограничение памяти вызывает поведение патологической плиты и результирующая пропускная способность ввода-вывода приложения совершенно неприемлема.

Это полезно для drop_caches между запусками с использованием echo 3 > /proc/sys/vm/drop_caches. Это очистит кэш страницы и связанный пул объектов slab.

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

UPDATE Похоже, что обновление до Ubuntu 18.04 с ядром 4.15 исправляет наблюдаемую ошибку выделения kmalloc. Версия Java, кажется, не имеет значения. Похоже, это связано с тем, что каждая C1-группа v1 может выделять кеш страниц только до предела памяти (с несколькими cgroups это сложнее, поскольку только одна cgroup "взимается" за распределение по схеме учета общих страниц), Я считаю, что это теперь соответствует предполагаемому поведению. В ядре 4.4 мы обнаружили, что наблюдаемые ошибки kmalloc были пересечением использования программного raid0 в C1-группе v1 с ограничением памяти и очень большим кешем страниц. Я полагаю, что cgroups в ядре 4.4 смогли отобразить неограниченное количество страниц (ошибка, которую мы сочли полезной) до момента, когда ядру не хватило памяти для связанных объектов slab, но у меня все еще нет курение пистолета за дело.

В ядре 4.15 нашим контейнерам Docker требуется установить предел памяти (через AWS ECS), поэтому мы выполнили задачу по снятию ограничения памяти, как только контейнер будет создан в /sys/fs/cgroup/memory/docker/{contarainer_id}/memory.limit_in_bytes. Кажется, это работает, хотя это не очень хорошая практика, чтобы быть уверенным. Это позволяет нам вести себя так, как мы хотим - неограниченное использование ресурсов кэша страниц на хосте. Поскольку мы запускаем приложение JVM с фиксированной кучей, риск снижения производительности ограничен.

Для нашего варианта использования было бы замечательно иметь возможность полностью исключить кэш страницы (дисковое пространство mmap) и связанные объекты slab для cgroup, но сохранить ограничение на кучу & стек для процесса докера. Существующая схема учета совместно используемых страниц довольно сложна для осмысления, и мы бы предпочли, чтобы кеш страниц LRU (и связанные ресурсы SLAB) использовали полный объем памяти хостов, как в случае, когда предел памяти вообще не установлен.

Я начал следить за некоторыми разговорами о LWN, но я немного в неведении. Может быть, это ужасная идея? Я не знаю... добро пожаловать совет о том, как действовать дальше или куда идти дальше.

Ответ 1

java 10.0.1 2018-04-17

Вы должны попробовать более свежую версию Java 10 (или 11 или...)

В мае прошлого года (2019) я упомянул вподдержке Docker в Java 8 - наконец-то!", что новые изменения в Java 10, перенесенные в Java 8, означают, что Docker будет более точно сообщать об используемой памяти.

Эта статья за май 2018 года сообщает:

Succes! Without providing any flags Java 10 (10u46 -- Nightly) correctly detected Dockers memory limits.


ОП Дэвид подтверждает в комментариях:

Интеграция docker - jvm - большое улучшение в Java 10.
Это действительно связано с настройкой вменяемого XMS и количества процессоров. Теперь они учитывают ограничения контейнера докера, а не собирают значения экземпляра хоста (вы можете отключить эту функцию, используя -XX:-UseContainerSupport в зависимости от вашего варианта использования).

Я не нашел это полезным в работе с кешем страниц.
Лучшее решение, которое я нашел, - отключить ограничение памяти докера после создания контейнера, если это необходимо.
Это определенно взломщик - пользователь остерегается.