Изучая возможности контрольных групп в linux наткнулся на параметр memory.soft_limit_in_bytes:

When the system detects memory contention or low memory, control groups are pushed back to their soft limits. If the soft limit of each control group is very high, they are pushed back as much as possible to make sure that one control group does not starve the others of memory.

Аналог в cgroup2 - memory.low

Звучит интересно! Сделаем несколько тестов для понимания как это все работает.

Немного проблематики

Ресурсная модель в kubernetes основана на requests/limits к CPU и memory - абстракции над параметрыми cgroups.

Причем CPU requests (cpu.shares) используются более разносторонне:

  • при планировании Пода kube-scheduler’ом;
  • для обеспечения “гарантированной” доли CPU на машине.

Ранее писал про это применительно к специфике JVM.

А вот механизмы вокруг memory таким похвастать не могут - реквесты используются только для выбора подходящей ноды.

С другой стороны docker умеет работать с soft limits через флаг --memory-reservation, что расширяет потенциальную область применимости.

К тестам

Дано:

  • виртуальная машина с debian 11 и 8Gb RAM;
  • docker v20.10.17;
  • набор контейнеров, алоцирующие по 1Gb анонимной памяти каждый.

Для воспроизведения код аллокации:

import numpy as np

def allocate_memory(size_in_gb):
    size_in_bytes = size_in_gb * 1024**3
    num_elements = size_in_bytes // np.dtype('float64').itemsize

    memory_block = np.zeros(num_elements, dtype='float64')
    for i in range(num_elements):
        memory_block[i] = i % 256

    while True:
        pass

if __name__ == "__main__":
    allocate_memory(1)

и Dockerfile:

FROM python:3.9-slim
WORKDIR /app
RUN pip install numpy
COPY allocate_memory.py .
CMD ["python", "allocate_memory.py"]

Зафиксируем объем свободной памяти:

# free -h
              total        used        free      shared  buff/cache   available
Mem:          7,8Gi       656Mi       6,8Gi        79Mi       339Mi       6,8Gi
Swap:            0B          0B          0B

И начнем запускаться в разных конфигурациях.

1. Семь контейнеров без софт лимитов

# for i in {1..7}; do docker run --rm -d mem-anon-1gb; done
74a5212e5061d3f4f5cf71af116fc4b151997c58f76e453a16260354106b2ba0
a0cdf9b5c7d43c894b2820cf66a22e5e412781cfda670ca4727dac75a82f1d07
eb2367e472efbce609f4859acbebc82b97830765765a240dcaef8509ec4c5712
4288b784bcd239b3c11deaa71886adf5c59792edaffaadcad23a9b7335c2b24d
8ed56c614be71159f9e64347cfb601e18092e3c65bd1e069beed13c11bb59f34
aa1425bae3f5652d0f38b0658d384adb22de6334a55b604ac69940755854ce4e
79ebce488f401397e52a1171dd3b5652d2b11a52d9e4b000d53038bdf6b78053

# free -h
              total        used        free      shared  buff/cache   available
Mem:          7,8Gi       7,5Gi       124Mi        79Mi       150Mi        23Mi
Swap:            0B          0B          0B

# docker stats --no-stream --format "{{.Container}}: {{.MemUsage}}"
74a5212e5061: 628.3MiB / 7.791GiB
a0cdf9b5c7d4: 1.019GiB / 7.791GiB
eb2367e472ef: 1.022GiB / 7.791GiB
4288b784bcd2: 1.018GiB / 7.791GiB
8ed56c614be7: 1.022GiB / 7.791GiB
aa1425bae3f5: 1.024GiB / 7.791GiB
79ebce488f40: 1.026GiB / 7.791GiB

### прошло несколько минут...

# docker ps -a
...
a0cdf9b5c7d4   ...   Exited (137) 55 seconds ago             ...
...

При запуске седьмого контейнера иссякла память и docker отправил SIGKILL в рандомный контейнер для ее высвобождения:

# free -h
              total        used        free      shared  buff/cache   available
Mem:          7,8Gi       6,8Gi       604Mi        79Mi       454Mi       726Mi
Swap:            0B          0B          0B

# docker stats --no-stream --format "{{.Container}}: {{.MemUsage}}"
74a5212e5061: 1.019GiB / 7.791GiB
eb2367e472ef: 1.018GiB / 7.791GiB
4288b784bcd2: 1.022GiB / 7.791GiB
8ed56c614be7: 1.024GiB / 7.791GiB
aa1425bae3f5: 1.026GiB / 7.791GiB
79ebce488f40: 1.022GiB / 7.791GiB

2. Семь контейнеров без софт лимитов + swap в 2Gb

  1. Создадим swap раздел:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
  1. Запустим первые шесть контейнеров:
# for i in {1..6}; do docker run --rm -d mem-anon-1gb; done
81511a32dc43b8f26610d4ed34891a846bb730b1e64bfb4bf269d388b6910f39
caf7ce67ae058a0979f1ddf81062efd708807e958167ea93fdc1bc2f236c69ee
f7976d6d00c9530936f37702aae24f130e83c70441f98c7804d33b146adc9fd2
696f0950addd7c838ca20fa6a89ed8c4f55ee1043a903d729db140dc997f0fb1
82aa3421260f22f0a94acb3f74aea201e4dc0c7c2f1fd6fb29449e66b26e7a15
e83b17188939bc27693416abeef368ebc301516f27cf54a070e1ba0a0728eafa

# free -h
              total        used        free      shared  buff/cache   available
Mem:          7,8Gi       6,8Gi       653Mi        31Mi       377Mi       746Mi
Swap:         2,0Gi         0Gi       2,0Gi

# docker stats --no-stream --format "{{.Container}}: {{.MemUsage}}"
e83b17188939: 1.022GiB / 7.791GiB
82aa3421260f: 1.024GiB / 7.791GiB
696f0950addd: 1.026GiB / 7.791GiB
f7976d6d00c9: 1.026GiB / 7.791GiB
caf7ce67ae05: 1.03GiB / 7.791GiB
81511a32dc43: 1.033GiB / 7.791GiB

Все контейнеры помещаются в оперативной памяти.

  1. Добавим седьмой:
# docker run --rm -d mem-anon-1gb
f4451acbedcabd9f725dcdeae6c0982550dbac66ebd2c2f6501f951b6a4a9204

# docker stats --no-stream --format "{{.Container}}: {{.MemUsage}}"
f4451acbedca: 1.016GiB / 7.791GiB
e83b17188939: 1013MiB / 7.791GiB
82aa3421260f: 1003MiB / 7.791GiB
696f0950addd: 986MiB / 7.791GiB
f7976d6d00c9: 965.6MiB / 7.791GiB
caf7ce67ae05: 947.8MiB / 7.791GiB
81511a32dc43: 1.028GiB / 7.791GiB

# free -h
              total        used        free      shared  buff/cache   available
Mem:          7,8Gi       7,4Gi       141Mi        19Mi       259Mi       127Mi
Swap:         2,0Gi       513Mi       1,5Gi

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

3. Шесть контейнеров без софт лимитов + один с софт лимитом в 0.5Gb + swap в 2Gb

  1. Запустим конейтенер с софт лимитами:
# docker run --rm -d --memory-reservation="0.5g" mem-anon-1gb
601e16e8ad6fca2990242c8b7b2617a8d042c9eef27ad4d15b5a7ad30c823f4d

# docker stats --no-stream --format "{{.Container}}: {{.MemUsage}}"
601e16e8ad6f: 1.03GiB / 7.791GiB

# free -h
              total        used        free      shared  buff/cache   available
Mem:          7,8Gi       1,6Gi       5,8Gi        28Mi       366Mi       5,9Gi
Swap:         2,0Gi         0Gi       2,0Gi

Контейнер полностью поместился в RAM.

  1. Добавим остальные шесть без лимитов:
# for i in {1..6}; do docker run --rm -d mem-anon-1gb; done
a25f97d7b5954542a4f1fe5227adefecc31e2c4f0c3fec30bc5c238c03e37122
571e76046aaf0af4e9ebca68b7e2da07d9468cc3ab0d473693a65ee6338dd621
7c152c4d7973b6d8e16618d7a673022a1b388dbd66767905af2566df26fffd3e
02c7d1be0ab2633623a8cd604b48d9f2e047954ed7feeda2289cfb6e253e17e6
948a95bd17ab294af18b899cbc1f6177a41b0ad540825020dbc978b6efc50f60
18ca4b465ee5229ee837bb5b22916e37009aa7ab56d341221873719df730a26c

# free -h
              total        used        free      shared  buff/cache   available
Mem:          7,8Gi       7,3Gi       141Mi        29Mi       372Mi       230Mi
Swap:         2,0Gi       558Mi       1,5Gi

# docker stats --no-stream --format "{{.Container}}: {{.MemUsage}}"
18ca4b465ee5: 1.018GiB / 7.791GiB
948a95bd17ab: 1.018GiB / 7.791GiB
02c7d1be0ab2: 1.018GiB / 7.791GiB
7c152c4d7973: 1.018GiB / 7.791GiB
571e76046aaf: 1.018GiB / 7.791GiB
a25f97d7b595: 1.018GiB / 7.791GiB
601e16e8ad6f: 610.4MiB / 7.791GiB

Все как завещали в документации - контейнер с софт лимитами (601e16e8ad6f) “стротлился” в swap. Работает!

Выводы

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

Можно сказать, что это аналог механизма троттлинга CPU.

Из особенностей:

  • софт лимиты не гарантируют высвобождения памяти (best-effort), но стараться будут;
  • не используются в kubernetes (но есть в docker);
  • для вытестенения анонимной памяти потребуется swap раздел;
  • для вытеснения file-backed памяти ничего дополнительно не потребуется - данные будут перетекать из page cache на диск;
  • стоит не забывать о пенальти на производительность при работе с дисками.

Будет здорово узнать ваши кейсы: кто, как и зачем использует софт лимиты в своих системах! Велкам в комменты.

Удачи!