Привет, Хабр!
Меня зовут Данил, и я старший специалист в компании Увеон. Занимаюсь серверной частью Termidesk Assistant - это утилита для удаленных рабочих столов.
К нам в команду пришла интересная задача, нужно было собрать всю серверную часть в один исполняемый файл (.elf) и в дальнейшем на его основе сделать установочный файл (.deb), чтобы создать и запустить сервис.
Все это для того, чтобы оптимизировать наше приложение как по скорости, так и по внешним зависимостям, а также создать возможность использования Termidesk Assistant в локальных изолированных сетях.
О Nuitka мало что известно в Python-среде, особенно мало информации на русском языке, поэтому я решил взяться за написание этой статьи и расписать всё то, что успел собрать за время работы над задачей.
1. Что же такое Nuitka?
Nuitka - транспайлер (транспилирующий компилятор), который транслирует код Python в исполняемые файлы или исходный код C/C++. То есть он переводит код Python в C++, оптимизируя его, а далее в машинный код через C++ компилятор, генерируя исполняемый файл (elf, exe и т.д.)
2. Преимущества Nuitka
При сравнении сборки Django-приложений с помощью Nuitka, Docker-контейнеров и PyInstaller, у Nuitka есть несколько потенциальных преимуществ:
Производительность
Nuitka компилирует Python-код в нативный машинный код, что может привести к повышению производительности по сравнению с интерпретируемым Python-кодом.Размер исполняемого файла
Nuitka обычно создает меньшие по размеру исполняемые файлы по сравнению с PyInstaller, так как включает только необходимые зависимости.Защита исходного кода
Компиляция в машинный код затрудняет обратную разработку, обеспечивая некоторую защиту вашего исходного кода.Кроссплатформенность
Nuitka позволяет создавать исполняемые файлы для разных платформ, что может быть проще, чем управление Docker-контейнерами для разных окружений.Простота развертывания
В отличие от Docker, не требуется устанавливать и настраивать дополнительное ПО на целевой машине.
3. Недостатки Nuitka
Сложность конфигурации
Настройка Nuitka для корректной работы с Django и всеми зависимостями может быть сложнее, чем настройка Docker или PyInstaller.Время компиляции
Процесс компиляции с Nuitka может занимать значительно больше времени, чем создание Docker-образа или сборка с PyInstaller.Ограниченная поддержка динамических аспектов Python
Некоторые динамические функции Python (например, динамическая загрузка модулей) могут работать некорректно или требовать дополнительной настройки.Проблемы совместимости
Не все Python-библиотеки хорошо работают с Nuitka.Отладка
Отлаживать скомпилированное приложение может быть сложнее (дебаг тут невозможен, только print, грубо говоря), чем интерпретируемый код или код в Docker-контейнере.Обновления
Обновление приложения, собранного с Nuitka, требует полной пересборки, в то время как с Docker можно обновить только изменённые слои.Отсутствие изоляции
В отличие от Docker, Nuitka не обеспечивает изоляцию окружения, что может привести к конфликтам с системными библиотеками. Для полной изоляции я использовал аргумент onefile, но оно не предоставляет изоляцию на уровне ОС и также использует системные библиотеки и драйверы, как я это обходил - чуть позже.Меньшее распространение
Nuitka менее популярна в сообществе Django и Python вообще, чем Docker, что может затруднить поиск решений проблем и лучших практик. Поэтому я здесь ?Ограниченная масштабируемость
Docker предоставляет более гибкие возможности для масштабирования приложений, особенно в контексте микросервисной архитектуры.
4. Резюмируем: сравнение Nuitka с Pyinstaller и Docker
Pyinstaller использует в своих пакетах питоновские скрипты и интерпретатор, в то время как Nuitka переводит код в машинный, что увеличивает скорость выполнения и повышает уровень защиты кода от чтения.
В отличие от Docker, Nuitka - открытое ПО. Это вызывает большую уверенность в безопасности у наших клиентов и меньше зависимости от политической обстановки.
5. Способы интеграции Nuitka в различные процессы разработки и развертывания
Использование в bash-скриптах- Автоматизация сборки проекта через shell-скрипты.- Пример:```bash#!/bin/bashpython -m nuitka --follow-imports --standalone --output-dir=build myscript.py```
Интеграция в CI/CD пайплайны- GitFlic CI:```yaml
build_job:
script:
- pip install nuitka
- python -m nuitka --follow-imports --standalone myapp.py
```
- Jenkins:
```groovy
stage('Build') {
steps {
sh 'pip install nuitka'
sh 'python -m nuitka --follow-imports --standalone myapp.py'
}
}
```-
Использование в setup.py- Интеграция Nuitka в процесс установки пакета:
```python
from setuptools import setup
from nuitka.distutils_based_buildsystem.BuildSystem import Nuitkasetup(
name="MyApp",
cmdclass={"build_exe": Nuitka},
# другие параметры...
)
``` GitHub Actions workflow- Автоматизация сборки при push или pull request:
```yaml
name: Build with Nuitka
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
- name: Install dependencies
run: |
pip install nuitka
- name: Build with Nuitka
run: python -m nuitka --follow-imports --standalone myapp.py
```-
Makefile- Упрощение процесса сборки:
```makefile
build:
python -m nuitka --follow-imports --standalone myapp.pyclean:
rm -rf build/
``` Docker- Использование Nuitka внутри Docker для создания компилированных приложений:
```dockerfile
FROM python:3.9
RUN pip install nuitka
COPY . /app
WORKDIR /app
RUN python -m nuitka --follow-imports --standalone myapp.py
```Tox- Интеграция Nuitka в процесс тестирования:
```ini
[testenv:build]
deps = nuitka
commands = python -m nuitka --follow-imports --standalone myapp.py
```Pre-commit hooks- Автоматическая компиляция перед коммитом:
```yaml
- repo: local
hooks:
- id: nuitka-build
name: Build with Nuitka
entry: python -m nuitka --follow-imports --standalone
language: system
files: ^myapp\.py$
```Интеграция с системами управления пакетами- Создание пакетов для систем вроде apt, yum или Homebrew с использованием Nuitka для компиляции.
Эти подходы позволяют интегрировать Nuitka в различные аспекты процесса разработки и развертывания, автоматизируя сборку и делая её частью вашего рабочего процесса. Выбор конкретного метода зависит от вашей инфраструктуры и требований проекта.
6. Переходим к практике: коротко моем опыте
6.1. Оптимизация проекта
Это было необходимо, чтобы сделать из проекта единый монолит. Если без подробностей, я заменил Memcached на python-memcached и Redis на Fakeredis. Тут подробно останавливаться не буду, потому что банально заменил зависимости проекта и добавил небольшие настройки. Далее часть чуть сложнее - прокси-сервер. У нас для этого всегда был Apache2, в связи с этой задачей я выбрал фреймворк FastAPI, если опустить эндпоинты, то вот код:
Загрузка статичных файлов:
def find_static_files():
if getattr(sys, 'frozen', False):
base_path = getattr(sys, '_MEIPASS', os.path.dirname(sys.executable))
else:
base_path = os.path.dirname(os.path.abspath(__file__))
possible_paths = [
os.path.join(base_path, 'static'),
os.path.join(base_path, 'django_termidesk_assistant', 'static'),
os.path.join(base_path, 'src', 'django_termidesk_assistant', 'static'),
os.path.join(os.path.dirname(base_path), 'static'),
'/tmp/onefile_*/static'
]
for path in possible_paths:
if '*' in path:
import glob
matching_paths = glob.glob(path)
if matching_paths:
return matching_paths[0]
elif os.path.exists(path) and os.path.isdir(path):
return path
print("Static files not found in any of the expected locations.")
return None
def setup_static_files(app):
static_path = find_static_files()
if static_path:
if getattr(sys, 'frozen', False):
temp_dir = tempfile.mkdtemp()
temp_static_path = os.path.join(temp_dir, 'static')
shutil.copytree(static_path, temp_static_path)
static_path = temp_static_path
app.mount("/assistant/static", StaticFiles(directory=static_path), name="static")
return app
else:
print("Warning: Static files not found. Static content will not be served.")
return None
fastapi_app = setup_static_files(fastapi_app)
Редирект:
@fastapi_app.exception_handler(404)
async def custom_404_handler(request: Request, exc: Exception):
return RedirectResponse(url="/assistant/")
Генерация сертификатов для https:
def generate_ssl_certificates():
try:
subprocess.run(['openssl', 'genrsa', '-out', 'privkey.pem', '2048'], check=True)
subprocess.run(['openssl', 'req', '-new', '-x509', '-key', 'privkey.pem',
'-out', 'fullchain.pem', '-days', '365', '-subj', "/CN=localhost"], check=True)
except subprocess.CalledProcessError as e:
print(f"Failed to generate SSL certificates: {e}")
raise
Запуск http сервера:
def run_http_server():
config = Config(fastapi_app, host="0.0.0.0", port=80)
server = Server(config=config)
try:
server.run()
except Exception as e:
print(f"Server error on port 80: {e}")
Запуск https сервера:
def run_https_server():
generate_ssl_certificates()
certfile = "fullchain.pem"
keyfile = "privkey.pem"
config = Config(fastapi_app, host="0.0.0.0", port=443,
ssl_keyfile=keyfile, ssl_certfile=certfile)
server = Server(config=config)
try:
server.run()
except Exception as e:
print(f"Server error on port 443: {e}")
Дальше запускаем в отдельных процессах, чтобы не было конфликтов (пробовал асинхрон - не вышло):
multiprocessing.freeze_support()
http_process = multiprocessing.Process(target=run_http_server)
https_process = multiprocessing.Process(target=run_https_server)
daphne_process = multiprocessing.Process(target=run_daphne)
http_process.start()
https_process.start()
daphne_process.start()
run_daphne - скрипт запуска внутреннего сервера соответственно.
6.2. GitFlic-пайплайны, связанные со сборкой в Nuitka
Далее идет процесс сборки пакетов на CI/CD сервере.
6.2.1. Сборка elf-файла
build_nuitka_elf:
image: "python:3.8-buster"
stage: build
tags:
- at-docker
before_script:
- python -m venv venv
- source venv/bin/activate
- pip install --upgrade pip
- pip install --upgrade setuptools wheel
- apt -y update >/dev/null
- apt install -y gettext patchelf >/dev/null
- export PATH=$PATH:/usr/sbin
- pip install -r ${CI_PROJECT_DIR}/src/django_termidesk_assistant/requirements.txt
- pip install uvicorn==0.17.6 fastapi==0.78.0 starlette==0.19.1
- cd ${CI_PROJECT_DIR}/src/django_termidesk_assistant
- python manage.py makemigrations >/dev/null
- python manage.py migrate >/dev/null
- python manage.py createcachetable >/dev/null
- python manage.py collectstatic --noinput >/dev/null
- python manage.py compilemessages >/dev/null
script:
- cd ${CI_PROJECT_DIR}
- export PYTHONPATH=$PYTHONPATH:${CI_PROJECT_DIR}/src/django_termidesk_assistant
- export DJANGO_SETTINGS_MODULE=django_termidesk_assistant.settings
- pip install nuitka==1.5.4
- python -m nuitka
--remove-output
--follow-imports
--include-module=django_termidesk_assistant.settings
--include-package=django_termidesk_assistant
--include-package=termidesk_assistant
--include-package=signalling
--include-module=termidesk_assistant.middleware
--include-package=django
--include-module=django.core.management
--include-package=channels
--include-package=channels_redis
--include-package=fakeredis
--include-package=asgiref
--include-package=django.templatetags.i18n
--include-package=daphne
--include-package=rest_framework
--include-module=django_termidesk_assistant.asgi
--include-package=django_structlog
--include-package=gettext
--include-package=uvicorn
--include-package=fastapi
--include-package=starlette
--include-data-dir=${CI_PROJECT_DIR}/src/django_termidesk_assistant/termidesk_assistant=termidesk_assistant
--include-data-dir=${CI_PROJECT_DIR}/src/django_termidesk_assistant/static=static
--include-data-dir=${CI_PROJECT_DIR}/src/django_termidesk_assistant/locale=locale
--include-data-dir=${CI_PROJECT_DIR}/src/django_termidesk_assistant/var/db=var/db
--include-data-dir=${CI_PROJECT_DIR}/src/django_termidesk_assistant/var/log=var/log
--output-dir=${CI_PROJECT_DIR}/dist
--onefile
--onefile-tempdir-spec=/tmp/onefile_%PID%_%TIME%
--output-filename=termidesk_assistant_server.bin
${CI_PROJECT_DIR}/src/django_termidesk_assistant/run_servers.py
artifacts:
expire_in: 1 day
paths:
- dist/
Давайте разберем построчно (ну, почти).
Выбираем образ докера python:3.8-buster, так как версии выше вызывают ошибку glibc на Astra Linux: glibc_2.33' not found. К тому же наша кодовая база написана для python 3.7, потому что в официальных репозиториях Astra Linux SE последняя доступная версия Python это 3.7.3, как и включенный в ОС интерпретатор.
Далее мы активируем виртуальное окружение и ставим patchelf, необходимый для сборки elf-файлов.
Можно заметить, что uvicorn, fastapi и starlette ставятся отдельно, я специально их вынес из requirements.txt, потому что они нужны исключительно для сборки в Nuitka и кастомизированного прокси-сервера, а у нас еще есть другие версии Termidesk Assistant.
Потом мы проходимся по стандартным Django-командам, которые нужны перед запуском любого Django-приложения.
Давайте перейдем к сборке. Мы ставим Nuitka версии 1.5.4, поскольку эмпирически было выяснено, что она оптимально подходит для python 3.8 и тех библиотек, что у нас поставлены для кодовой базы python 3.7. Пройдемся по самим аргументам Nuitka:
--remove-output - удаление C-файлов из python-модуля после сборки.
--follow-imports - компиляция зависимостей вместе с проектом.
--include-module и --include-package - если есть ошибка во время сборки, то указываем нужный нам импорт. В моем случае через --include-package я включал также внутренние приложения проекта.
--include-data-dir - все нужные для приложения папки, в основном это статические файлы.
--output-dir - папка, в которой будет лежать наш билд-файл внутри докер-контейнера.
--onefile - формат билда, в нашем случае это единый файл без зависимых папок и файлов.
--onefile-tempdir-spec - путь до папки, в которой будут храниться временные файлы во время запуска билда, например, файлы кэша.
--output-filename - имя билд-файла, ограничений по названию нет.
Далее ставим путь до скрипта входа, в моем случае это скрипт запуска процессов серверов.
Далее в artifacts указываем хранение файла сборки на CI/CD - сервере и папку, в которой будет лежать файл внутри zip-файла.
6.2.2. Сборка deb-файла
build_nuitka_deb_package:
image: "python:3.8-buster"
stage: package
tags:
- at-docker
dependencies:
- build_nuitka_elf
before_script:
- apt-get update
- apt-get install -y dpkg-dev
script:
- |
if [ -z "${CI_COMMIT_TAG}" ]; then
export PACKAGE_VERSION=$(date +%Y%m%d%H%M%S)
else
export PACKAGE_VERSION=${CI_COMMIT_TAG}
fi
- export PACKAGE_NAME="termidesk-assistant-server${PACKAGE_VERSION}"
- mkdir -p ${PACKAGE_NAME}/DEBIAN
- mkdir -p ${PACKAGE_NAME}/usr/local/bin
- mkdir -p ${PACKAGE_NAME}/etc/systemd/system
- mkdir -p ${PACKAGE_NAME}/var/lib/termidesk-assistant
- mkdir -p ${PACKAGE_NAME}/var/log/termidesk-assistant
- cp dist/termidesk_assistant_server.bin ${PACKAGE_NAME}/usr/local/bin/
- chmod +x ${PACKAGE_NAME}/usr/local/bin/termidesk_assistant_server.bin
- |
cat << EOF > ${PACKAGE_NAME}/DEBIAN/control
Package: termidesk-assistant-server
Version: ${PACKAGE_VERSION}
Maintainer: Release Team <release@uveon.ru>
Architecture: amd64
Section: non-free/admin
Priority: optional
Description: Termidesk Assistant Server
Homepage: http://uveon.ru/
EOF
- |
cat << EOF > ${PACKAGE_NAME}/etc/systemd/system/termidesk-assistant.service
[Unit]
Description=Termidesk Assistant Server
After=network.target
[Service]
ExecStart=/usr/local/bin/termidesk_assistant_server.bin
Restart=on-failure
RestartSec=5
User=root
Group=root
Environment=PATH=/usr/bin:/usr/local/bin
WorkingDirectory=/var/lib/termidesk-assistant
StandardOutput=append:/var/log/termidesk-assistant/service.log
StandardError=append:/var/log/termidesk-assistant/service.log
[Install]
WantedBy=multi-user.target
EOF
- |
cat << EOF > ${PACKAGE_NAME}/DEBIAN/postinst
#!/bin/bash
set -e
# Log file for installation
LOG_FILE="/var/lib/termidesk-assistant/install.log"
log() {
echo "\$(date): \$1" >> \$LOG_FILE
}
log "Starting postinst script"
# Create necessary directories
mkdir -p /var/lib/termidesk-assistant /var/log/termidesk-assistant
chown root:root /var/lib/termidesk-assistant /var/log/termidesk-assistant
chmod 755 /var/lib/termidesk-assistant /var/log/termidesk-assistant
log "Created necessary directories"
# Reload systemd to recognize the new service
systemctl daemon-reload
log "Systemd reloaded"
# Enable the service to start on boot
systemctl enable termidesk-assistant.service
log "Service enabled"
# Start the service
systemctl start termidesk-assistant.service
log "Service start command issued"
# Check if the service is running
sleep 5 # Give the service a moment to start
if systemctl is-active --quiet termidesk-assistant.service; then
log "Service is running successfully"
else
log "Service failed to start. Check systemctl status termidesk-assistant.service for more info"
systemctl status termidesk-assistant.service >> \$LOG_FILE 2>&1
journalctl -u termidesk-assistant.service --no-pager >> \$LOG_FILE
fi
# Check if the service is enabled
if systemctl is-enabled --quiet termidesk-assistant.service; then
log "Service is enabled for autostart"
else
log "WARNING: Service is not enabled for autostart"
fi
log "Postinst script completed"
exit 0
EOF
- chmod 755 ${PACKAGE_NAME}/DEBIAN/postinst
- dpkg-deb --build ${PACKAGE_NAME}
artifacts:
expire_in: 1 day
paths:
- ./*.deb
Начинается все также, но учитываем, что stage должен быть следующим, иначе не выйдет поставить предыдущий пайплайн в зависимость этому:
dependencies:
- build_nuitka_elf
Кратко, так выглядит зависимость второго пайплайна от первого:
Далее ставим необходимый пакет dpkg-dev для сборки deb-файла.
Перейдем к части script пайплайна:
Вводим переменную PACKAGE_NAME, основанную на имени приложения, тега коммита и времени. Создаем необходимые для deb-файла директории, в том числе и логи. Копируем наш elf-файл из предыдущего пайплайна.
А теперь пишем файлы, без которых наш результат будет нежизнеспособен:
сontrol - описание deb-файла.
service - окружение, файл, на котором будет основан deb-файл, пользователь и группы пользователей, которым дан доступ, рабочая папка и т.д.
postinst - команды, которые должны быть исполнены после установки, в моем случае это создание папки логов, запуск самого сервиса приложения, создание и запись в лог сборки файла.
В artifacts указываем аналогично хранение в один день и сохранение файла с расширением .deb.
7. Резюмируем
В этой статье мы разобрали преимущества и недостатки Nuitka перед ее аналогами, такими как Pyinstaller и Docker. Выяснили, какие существуют варианты интеграции Nuitka в ваше приложение. Пошагово рассмотрели, как собрать исполняемый и установочный файлы на моем примере с Termidesk Assistant.
Если у вас остались какие-то вопросы, пишите в комментариях, с радостью отвечу!
Akorabelnikov
Был ли замерен прирост производительности?
DanilKomyshev Автор
Примерно от 10% до 76% в зависимости от обращений к бд, потому что операции уже происходят на стороне бд, что понижает ускорение. Замеры также были помодульно, поэтому где больше логики на питоне, тем ускорение выше. Вообще по-хорошему сделать тесты на базовых операциях, если и когда этим займемся, обязательно результаты в ответе запишу)