2025, Oct 06 03:17

POSIX‑память между Docker‑контейнерами в GitLab CI без ipc_mode

Почему ipc_mode не влияет на POSIX‑память (multiprocessing.shared_memory) и как делиться ею в Docker и pytest: общий том в /dev/shm и единый контекст GitLab CI

Разделять Python multiprocessing.shared_memory между контейнерами на ноутбуке разработчика кажется простым, но CI меняет правила. Как только pytest запускается внутри контейнера и оркестрирует дополнительные контейнеры через Docker SDK, привычные подходы вроде bind-монтирования /dev/shm становятся хрупкими, а переключение ipc_mode не приносит пользы. Ниже — практическое разъяснение того, что действительно управляет POSIX‑разделяемой памятью в этой конфигурации, и как добиться стабильной работы на GitLab runner.

Постановка задачи

Цель — позволить pytest создать и записывать в блок POSIX‑разделяемой памяти, из которого другие контейнеры Docker смогут читать. Локально кажется, что помогает bind-монт хостового /dev/shm в тестовые контейнеры. В GitLab CI pytest сам запускается внутри контейнера и общается с демоном Docker через Docker SDK, что усложняет видимость разделяемой памяти и изоляцию между контейнерами.

Пример кода начального подхода

Локально создаём блок POSIX‑разделяемой памяти, примонтируем /dev/shm и стартуем контейнер, который открывает ту же память по имени:

from multiprocessing.shared_memory import SharedMemory
import docker

api = docker.from_env()
mem_label = "example_shm"

segment = SharedMemory(create=True, name=mem_label, size=int(1e6))

bind_map = docker.types.Mount(source="/dev/shm", target="/dev/shm", type="bind")

api.containers.run(
    image="alpine",
    name="sample_worker",
    detach=True,
    remove=True,
    command="tail -f /dev/null",
    mounts=[bind_map],
    environment={
        "SHM_NAME": mem_label
    }
)

В CI возникает соблазн переключиться на ipc_mode, попытавшись определить ID контейнера с pytest:

from multiprocessing.shared_memory import SharedMemory
import docker

api = docker.from_env()
mem_label = "example_shm"

container_id = fetch_self_id()  # Некоторая логика для определения ID текущего контейнера
segment = SharedMemory(create=True, name=mem_label, size=int(1e6))

if container_id is None:
    mount_list = [docker.types.Mount(source="/dev/shm", target="/dev/shm", type="bind")]
    ipc_cfg = None
else:
    mount_list = []
    ipc_cfg = f"container:{container_id}"

api.containers.run(
    image="alpine",
    name="sample_worker",
    detach=True,
    remove=True,
    command="tail -f /dev/null",
    mounts=mount_list,
    ipc_mode=ipc_cfg,
    environment={
        "SHM_NAME": mem_label
    }
)

И вариант, где поднимается дополнительный «якорный» контейнер, помеченный как shareable:

import docker

api = docker.from_env()
anchor_ref = api.containers.run(
    image="alpine",
    name="mem_anchor",
    detach=True,
    remove=True,
    command="tail -f /dev/null",
    ipc_mode="shareable",
)
ipc_cfg = f"container:{anchor_ref.id}"

Эти подходы спотыкаются на GitLab runner: в предоставленной среде определение текущего ID контейнера может быть ненадёжным, а главное — ipc_mode не тот рычаг, который нужен для POSIX‑разделяемой памяти.

Что происходит на самом деле

Пространства имён IPC управляют System V IPC, а не POSIX‑разделяемой памятью. Документация ipc_namespaces(7) описывает эффект пространств имён для объектов SysV, а sysvipc(7) — системные вызовы System V IPC. В Python multiprocessing.shared_memory реализует POSIX‑стиль разделяемой памяти через shm_open, а не через SysV. Иными словами, переключение ipc_mode не сделает сегменты POSIX видимыми между контейнерами.

Возможность двух процессов подключиться к одному блоку POSIX‑разделяемой памяти зависит от того, видят ли они один и тот же файловый бэкенд под /dev/shm, чтобы вызов shm_open разрешался к одному и тому же объекту. На практике этого проще всего добиться, разделив одну и ту же директорию через Docker‑том, примонтированный в /dev/shm у всех участников, а не полагаясь на пространства имён IPC или bind‑монт хостового /dev/shm.

Минимальная воспроизводимая проверка

Следующая конфигурация демонстрирует, как два контейнера делят область POSIX‑разделяемой памяти без использования --ipc. Именованный Docker‑том монтируется в /dev/shm в обоих контейнерах, а имя SharedMemory совпадает по обе стороны.

Скрипт запуска:

#!/usr/bin/env bash
set -euo pipefail
docker build . -t memx-test
docker run -i -e PYTHONUNBUFFERED=1 -v shared_mem:/dev/shm memx-test python3 /code/server.py &
docker run -i -e PYTHONUNBUFFERED=1 -v shared_mem:/dev/shm memx-test python3 /code/client.py &
wait

Dockerfile:

FROM python:3.13

RUN mkdir /code
COPY server.py /code
COPY client.py /code

Серверный процесс:

from multiprocessing.shared_memory import SharedMemory
import time

region = SharedMemory(create=True, name='foo2', size=int(1e6))
print("Created SHM")
time.sleep(1)
region.buf[0] = 10
print("wrote value")
time.sleep(1)
region.unlink()
print("Removed shm")

Клиентский процесс:

from multiprocessing.shared_memory import SharedMemory
import time

time.sleep(0.5)
view = SharedMemory(create=False, name='foo2', size=int(1e6), track=False)
print("buf0", view.buf[0])
time.sleep(1)
print("buf0", view.buf[0])

При запуске видно, что у клиента значение buf[0] меняется с 0 на 10 — значит, память действительно общая. Это работает даже без флага --ipc и с --ipc=private, подтверждая, что в данном сценарии ipc_mode не влияет на POSIX‑разделяемую память.

Практическое решение

Вместо того чтобы делать bind‑монт хостового /dev/shm, создайте Docker‑том и монтируйте его как /dev/shm в контейнер с pytest и во все контейнеры, которые запускаются во время теста. Пока обе стороны используют одно и то же имя SharedMemory и видят один и тот же /dev/shm через общий том, они подключаются к одной области.

Есть одна особенность для CI. Если pytest общается с отдельным демоном Docker‑in‑Docker, а сам pytest при этом не работает в той же среде DinD, то демон DinD не увидит тома, созданные хостовым демоном Docker. Документация GitLab описывает это разделение. Чтобы всё оставалось в одном Docker‑контексте, запускайте pytest тоже внутри среды Docker‑in‑Docker и примонтируйте docker.sock этого демона DinD в контейнер pytest, чтобы pytest управлял контейнерами в том же демоне, где создан том.

Почему это важно

Опора на хостовый bind‑монт /dev/shm ненадёжна: возможны остатки от прошлых запусков и «перекрёстные помехи» между параллельными задачами. Именованный Docker‑том даёт чистый и воспроизводимый путь: его монтируете только в те контейнеры, которым нужно участвовать, и он согласуется с тем, как POSIX‑разделяемая память выбирает базовый объект через shm_open. Заодно это позволяет не смешивать модели POSIX и System V IPC, по-разному ведущие себя в пространствах имён.

Вывод

Для интеграционных тестов под управлением pytest, где нужна общая память между контейнерами, рассматривайте POSIX‑разделяемую память как ресурс на основе файловой системы и сделайте /dev/shm одной и той же директорией для всех участников с помощью Docker‑тома. Не полагайтесь на ipc_mode — он относится к System V IPC и не влияет на shm_open. В GitLab CI убедитесь, что pytest работает с тем же демоном Docker, который запускает тестовые контейнеры. При этих условиях достаточно имени Python SharedMemory и общего /dev/shm, чтобы надёжно делиться памятью между контейнерами.

Статья основана на вопросе с StackOverflow от user2416984 и ответе Nick ODell.