Повысьте производительность и безопасность кода Python без изменения исходного кода проекта.

Оглавление

  • Вступление
  • Мотивация
  • Контрольные точки
  • Дальнейшие оптимизации
  • Идеальный Dockerfile для Python

Вступление

Наличие надежного Dockerfile в качестве базы может сэкономить вам часы головной боли и более серьезных проблем в будущем.

В этом посте мы расскажем об «идеальном» файле Python Dockerfile. Конечно, совершенства не бывает, и я с радостью приму отзывы, чтобы исправить возможные проблемы, которые могут у вас возникнуть.

TL;DR;

Пропустите до конца, чтобы найти файл Docker, который на + 20% быстрее, чем тот, который используется по умолчанию в Docker Hub. Он также содержит специальные оптимизации для Gunicorn, чтобы строить быстрее и безопаснее.

Мотивация

В предыдущем проекте я построил ферму эластичных транскодеров, в которой использовались Docker (Alpine), Python и FFmpeg.

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

Оказывается, Alpine может быть небольшим, но в некоторых случаях он может немного замедлить работу из-за использования некоторых эквивалентных библиотек.

Я был в унынии. После того, как я так хорошо поработал с моими проектами Go, я по умолчанию использовал его с Python для получения изображений меньшего размера.

Это заставило меня начать с нуля, протестировать серию дистрибутивов и создать новый образ докера по умолчанию.

Претенденты:

  • python: 3.9-alpine3.13 (базовый уровень)
  • питон: 3.9
  • python: 3.9-тонкий
  • Python: 3.9-buster
  • Python: 3.9-тонкий-buster
  • убунту 20.04 (LTS)

Для тестирования, вместо того чтобы изобретать велосипед, я просто использую pyperformance.

Проект pyperformance призван стать авторитетным источником тестов для всех реализаций Python. Основное внимание уделяется реальным тестам, а не синтетическим тестам, с использованием целых приложений, когда это возможно.

Сравнительная таблица

Как оказалось, Alpine не так уж и медленнее в большинстве тестов по сравнению с другими изображениями из репозитория Python.

На самом деле большим сюрпризом стал тот факт, что использование Ubuntu и ручная установка python явились явным победителем с маржой более 20%.

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

Дальнейшие оптимизации

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

TL; DR; Вы можете увидеть полный файл Dockerfile в конце, следующие примеры предназначены только для объяснения.

Кеширование

Кеширование в докере работает послойно. Каждый «RUN» будет создавать слой, который потенциально может быть кэширован.

Он проверит вашу локальную систему на предмет предыдущих сборок и будет использовать каждый нетронутый слой в качестве кеша.

FROM ubuntu:20.04
RUN apt-get update && apt-get install -y python3.9 python3.9-dev
COPY . .
RUN pip install -r requirements.txt
CMD ["python]

В этом примере при первом запуске каждая команда будет запускаться с нуля.

При втором запуске все шаги будут автоматически пропущены.

Что происходит, когда вы меняете свой код? До этого момента он будет использовать кэшированные слои:

RUN pip install -r requirements.txt

А затем он снова установит все ваши требования, даже если вы их не меняли.

Поскольку установка требований обычно занимает большую часть времени сборки, мы хотим избежать этого.

Мы можем сделать очень простое изменение - скопировать только наш файл требований и установить их перед копированием кода:

FROM ubuntu:20.04
RUN apt-get update && apt-get install -y python3.9 python3.9-dev
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python]

Теперь, даже если вы измените свой код, до тех пор, пока вы не измените свой файл requirements.txt, он всегда будет использовать кеш, если он доступен.

Кеширование и BuildKit

BuildKit - это новый конструктор образов Docker, в него внесено множество улучшений, но мы в основном сосредоточимся на улучшении кэширования с его помощью.

  • Использование удаленного репозитория в качестве кеша.

С помощью BuildKit, помимо локального кэша сборки, построитель может повторно использовать кеш, сгенерированный из предыдущих сборок, с флагом --cache-from, указывающим на изображение в реестре.

Чтобы использовать изображение в качестве источника кеша, метаданные кеша должны быть записаны в изображение при создании. Это можно сделать, установив --build-arg BUILDKIT_INLINE_CACHE=1 при построении изображения. После этого собранный образ можно использовать в качестве источника кеша для последующих сборок.

После импорта кеша построитель извлечет только метаданные JSON из реестра и определит возможные попадания в кеш на основе этой информации. Если происходит попадание в кеш, совпадающие слои втягиваются в локальную среду.

Пример команды:

docker build -t app --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from registry-url/repo

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

  • Кеширование пакетов pip
# syntax=docker/dockerfile:1.2
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y python3.9 python3.9-dev
COPY requirements.txt .
RUN --mount=type=cache,mode=0755,target=/root/.cache pip install -r requirements.txt
COPY . .
CMD ["python"]

С его помощью вы можете указать докеру кэшировать папку /root/.cache, которая используется pip. Я считаю его полезным для своих локальных файлов Docker, где обычно тестируются разные пакеты.

Обратите внимание на первую строку # syntax=docker/dockerfile:1.2. Без этого команда --mount выдаст ошибку.

Пользователь root

Если вам действительно не нужно использовать какую-то черную магию внутри ваших контейнеров, вам следует избегать запуска с правами root. Это сделает вашу производственную среду намного безопаснее, если вы будете следовать принципу наименьших привилегий (PoLP).

В большинстве сценариев единственное, что вы делаете, когда работаете с правами root, - это упрощает жизнь злоумышленнику, используя возможные недостатки безопасности, которые могут даже позволить ему контролировать хост.

FROM ubuntu:20.04
RUN useradd --create-home myuser
USER myuser
CMD ["bash"]

Виртуальная среда

Использование виртуальной среды в докере может быть немного спорным, но я считаю, что у него есть как минимум следующие преимущества:

  • Вы получаете изоляцию от установки python по умолчанию в вашей ОС
  • Легко копировать папку пакетов между многоэтапными сборками
  • Вы можете использовать python вместо команды python3 или python3.9 (да, есть другие способы)
  • У вас может быть один Dockerfile для запуска тестов и развертывания. Установите свои требования к тестированию и производству в разные «папки» базового образа, а затем скопируйте их в «этап тестирования» и «этап производства».
FROM ubuntu:20.04
# create and activate virtual environment
RUN python3.9 -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
CMD ["python"]

В последнем файле докеров мы увидим, что нам просто нужно скопировать содержимое /opt/venv/, чтобы получить все установленные пакеты в многоэтапной сборке.

Многоэтапный

В докере каждый слой неизменен. Это означает, что даже если вы удалите что-то, что было установлено в предыдущем слое, общий размер изображения не уменьшится.

Рекомендуемый способ избежать раздутых изображений - использовать многоэтапные сборки. Даже если вы просто используете Docker для локальной разработки, экономия места всегда будет плюсом!

Но когда мы говорим о продакшене, когда мы платим за дисковое пространство, пропускную способность и время загрузки, учитывается каждый сохраненный МБ!

# using ubuntu LTS version
FROM ubuntu:20.04 AS builder-image

RUN apt-get update && apt-get install --no-install-recommends -y python3.9 python3.9-dev python3.9-venv python3-pip python3-wheel build-essential && \
   apt-get clean && rm -rf /var/lib/apt/lists/*

# create and activate virtual environment
RUN python3.9 -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# install requirements
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt

FROM ubuntu:20.04 AS runner-image
RUN apt-get update && apt-get install --no-install-recommends -y python3.9 python3-venv && \
   apt-get clean && rm -rf /var/lib/apt/lists/*

COPY --from=builder-image /opt/venv /opt/venv

# activate virtual environment
ENV VIRTUAL_ENV=/opt/venv
ENV PATH="/opt/venv/bin:$PATH"

CMD ["python"]

Здесь мы используем первый этап в качестве нашего «построителя», на котором мы устанавливаем такие инструменты, как компилятор gcc, а затем просто копируем необходимые файлы из образа построителя в образ бегуна.

Если вы используете Flask, Django или любое другое приложение WSGI

Это может быть касанием, но поскольку это действительно большая часть экосистемы Python, я решил также включить оптимизацию Gunicorn.

Если вы развертываете Flask или Django, вы всегда должны использовать что-то вроде Gunicorn вместо того, чтобы запускать их отдельно. Это будет иметь огромное значение в производительности. Чтобы узнать больше о Gunicorn, вы можете прочитать эту статью.

Gunicorn использует файловую систему периодических сообщений для отслеживания разветвленных процессов. Хранить его на диске может быть проблематично, и вы даже можете прочитать это предупреждение в их документации:

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

По умолчанию он будет использовать папку /tmp, которая обычно монтируется в памяти. Это не относится к докеру, и если вы уже запускаете gunicorn с докером и заметили несколько случайных зависаний, это может быть причиной.

На мой взгляд, самое чистое решение - просто изменить каталог пульса на каталог с отображением памяти внутри вашего контейнера докеров, в данном случае /dev/shm.

CMD ["gunicorn","-b", "0.0.0.0:5000", "-w", "4", "-k", "gevent", "--worker-tmp-dir", "/dev/shm", "app:app"]

В приведенном выше примере вы можете увидеть, как использовать параметр gunicorn --worker-tmp-dir для использования /dev/shm в качестве каталога пульса.

Идеальный Dockerfile для Python

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

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

Бонус

Хотя .dockerignore не является частью Dockerfile, я думаю, что должен подчеркнуть необходимость его использования.

Как и .gitignore, он служит списком файлов и папок, которые вы хотите игнорировать. В данном случае это означает исключение их из контекста сборки докеров.

Это приводит к более быстрой сборке, уменьшенному изображению, повышению безопасности и предсказуемости (вы можете исключить кеш Python, секреты и т. Д.).

В некоторых случаях можно сэкономить сотни МБ, просто исключив папку .git.

#example of ignoring .git and python cache folder
.git
__pycache__

Дальнейшее чтение





использованная литература

Https://github.com/docker-library/python/issues/501

Https://engineering.bitnami.com/articles/why-non-root-containers-are-important-for-security.html

Https://stackoverflow.com/questions/61459775/docker-buildkit-mount-type-cache-not-working-why

Https://pythonspeed.com/articles/docker-cache-pip-downloads/

Https://en.wikipedia.org/wiki/Principle_of_least_privilege

Https://news.ycombinator.com/item?id=22182226

Https://docs.docker.com/develop/develop-images/build_enhancements/

Https://docs.gunicorn.org/en/stable/settings.html

Https://hynek.me/articles/virtualenv-lives/

Https://github.com/themattrix/python-pypi-template/blob/master/.dockerignore