Как уменьшить размер контейнера с python проектом
Для многих небольших микросервисов и проектов я использую python. В то же время контейнеры с python внутри не отличаются компактностью. Сегодня на примере одного проекта попробую проиллюстрировать, как избавить контейнер от лишнего веса.
У нас есть микросервис, который общается с postgresql и которому нужна пара зависимостей из pip. Я сделаю следующее: подниму пустой контейнер, поставлю внутрь python3, pip, зависимости и закоммичу его в образ. Затем соберу статический бинарный файл и загружу его в контейнер без python, но с необходимыми зависимостями и закоммичу это во второй образ. Воспринимайте это просто как тест на размер занимаемого контейнером пространства. Естественно, для деплоя будут написаны Dockerfile и docker-compose.yml.
В качестве базового контейнера буду использовать Ubuntu 18.04 LTS. Взглянем на сам файл проекта, точнее, на его зависимости:
import os
import xml.dom.minidom
import requests
import psycopg2cffi
from datetime import datetime
Из этого очевидно, нам необходимо установить requests и psycopg2cffi, а в систему - postgresql-client, postgresql-common и libpq-dev. Поехали!
evgeniy@evgeniy [17:37:11]:~/files/work/test$ docker run -it --rm ubuntu:18.04 bash root@3e6dea583be4:/# apt-get -qy update && apt-get install -qy python3 python3-pip postgresql-client postgresql-common libpq-dev pip3 install requests psycopg2cffi
Далее из соседней консоли я закидываю наш test.py в наш контейнер:
evgeniy@evgeniy [17:43:43]:~/files/work/test$ docker cp test.py 3e6dea583be4:/tmp/
Проверяем работоспособность внутри контейнера:
root@3e6dea583be4:/# python3 /tmp/test.py
Вывод приводить не буду, скажу лишь, что приложение работает и выдаёт ожидаемые результаты своей работы. Теперь можно закоммитить контейнер в образ и посмотреть, сколько места он занимает. Назову его например, test-full:
evgeniy@evgeniy [17:43:54]:~/files/work/test$ docker commit 3e6dea583be4 test-full sha256:5bec61900aa7c007e9704e800d7a77c7c69201c979d97b1bc260184cc255b5ad evgeniy@evgeniy [17:47:53]:~/files/work/test$ docker images | grep test-full test-full latest 5bec61900aa7 32 seconds ago 507MB
Получилось 507 мегабайт. Теперь попробуем скомпилировать наше приложение в бинарный исполняемый файл. Для этого под linux существует несколько средств. Я использую pyinstaller. В том же самом контейнере установим pyinstaller и соберём бинарный файл (для того, чтобы включить все библиотеки, при сборке требуется указать ключ --onefile):
root@3e6dea583be4:/tmp# pip3 install pyinstaller root@3e6dea583be4:/tmp# pyinstaller --onefile test.py
В результате сборки было создано несколько файлов и директорий. Бинарный файл лежит в поддиректории dist. Давайте посмотрим, сколько он занимает:
root@3e6dea583be4:/tmp# file dist/test dist/test: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=28ba79c778f7402713aec6af319ee0fbaf3a8014, stripped root@3e6dea583be4:/tmp# ls -lah dist/test -rwxr-xr-x 1 root root 13M May 30 12:51 dist/test
Конечно, я так же проверил его работоспособность. Результаты работы никак не отличаются от резульатов выполнения файла test.py через интерпретатор. Теперь я скопирую получившийся бинарный файл из контейнера и создам новый пустой контейнер на базе той же убунты, после чего установлю необходимые зависимости, но не буду устанавливать python, pip и библиотеки:
evgeniy@evgeniy [17:47:57]:~/files/work/test$ docker cp 3e6dea583be4:/tmp/dist/test . root@02ac722806b1:/# apt-get -qy update && apt-get -qy install postgresql-client postgresql-common libpq-dev
Из старого контейнера можно выйти, после чего он самоуничтожится, так как мы при его создании использовали ключ --rm. Затем я скопирую в новый контейнер полученный бинарный файл:
evgeniy@evgeniy [17:59:10]:~/files/work/test$ docker cp test 02ac722806b1:/tmp/
В контейнере проверяю работоспособность - работает. После этого коммичу контейнер в образ с названием test-light:
evgeniy@evgeniy [17:59:22]:~/files/work/test$ docker commit 02ac722806b1 test-light sha256:50729ddfb0a71b8cc0bba3be2bb2a23e49604b31dfd609bf3eccd0f094ac1bca evgeniy@evgeniy [18:01:06]:~/files/work/test$ docker images | grep ^test test-light latest 50729ddfb0a7 18 seconds ago 174MB test-full latest 5bec61900aa7 13 minutes ago 507MB
Разница в размере в 2.9 раза или 333 мегабайта! Неплохо, да? Кстати, таким же точно образом собран исполняемый файл docker-compose.
Также я попробовал собрать тестовый микросервис, используя другой инструмент под названием nuitka. Для этого я скопировал в текущий контейнер файл test.py, установил python с необходимыми зависимостями, сам Nuitka и собрал проект:
evgeniy@evgeniy [18:32:15]:~/files/work/test$ docker cp test.py 02ac722806b1:/tmp/ root@02ac722806b1:/tmp# pip3 install requests psycopg2cffi Nuitka root@02ac722806b1:/tmp# python3 -m nuitka --standalone test.py
В отличие от pyinstaller, которому хватало десятка секунда, nuitka работал несколько минут. При этом сначала использовалось одно ядро на 100% (видимо, на этапе трансляции), а затем на 100% стали нагружены все 8 ядер моего не самого старого Core i5. В результате проект собрался и был помещён в поддиректорию test.dist. В отличие от pyinstaller в этой же директории оказалось множество динамических библиотек, размер бинарного файла составил 24 мегабайта, а всей директории - 49 мегабайт:
root@02ac722806b1:/tmp# ls -lah test.dist/ total 48M drwxr-xr-x 4 root root 4.0K May 30 13:37 . drwxrwxrwt 1 root root 4.0K May 30 13:37 .. -rw-r--r-- 1 root root 69K May 30 13:35 _asyncio.so -rw-r--r-- 1 root root 22K May 30 13:35 _bz2.so -rw-r--r-- 1 root root 830K May 30 13:35 _cffi_backend.so -rw-r--r-- 1 root root 147K May 30 13:35 _codecs_cn.so -rw-r--r-- 1 root root 155K May 30 13:35 _codecs_hk.so -rw-r--r-- 1 root root 27K May 30 13:35 _codecs_iso2022.so -rw-r--r-- 1 root root 267K May 30 13:35 _codecs_jp.so -rw-r--r-- 1 root root 135K May 30 13:35 _codecs_kr.so -rw-r--r-- 1 root root 111K May 30 13:35 _codecs_tw.so -rw-r--r-- 1 root root 6.6K May 30 13:35 _crypt.so -rw-r--r-- 1 root root 38K May 30 13:35 _csv.so -rw-r--r-- 1 root root 124K May 30 13:35 _ctypes.so -rw-r--r-- 1 root root 78K May 30 13:35 _curses.so -rw-r--r-- 1 root root 16K May 30 13:35 _curses_panel.so -rw-r--r-- 1 root root 16K May 30 13:35 _dbm.so -rw-r--r-- 1 root root 171K May 30 13:35 _decimal.so -rw-r--r-- 1 root root 30K May 30 13:35 _hashlib.so -rw-r--r-- 1 root root 73K May 30 13:35 _json.so -rw-r--r-- 1 root root 21K May 30 13:35 _lsprof.so -rw-r--r-- 1 root root 33K May 30 13:35 _lzma.so -rw-r--r-- 1 root root 56K May 30 13:35 _multibytecodec.so -rw-r--r-- 1 root root 16K May 30 13:35 _multiprocessing.so -rw-r--r-- 1 root root 6.2K May 30 13:35 _opcode.so -rw-r--r-- 1 root root 91K May 30 13:35 _sqlite3.so -rw-r--r-- 1 root root 118K May 30 13:35 _ssl.so -rw-r--r-- 1 root root 51K May 30 13:35 audioop.so drwxr-xr-x 3 root root 4.0K May 30 13:35 cryptography -rw-r--r-- 1 root root 647K May 30 13:37 libasn1.so.8 -rw-r--r-- 1 root root 66K May 30 13:37 libbz2.so.1.0 -rw-r--r-- 1 root root 14K May 30 13:37 libcom_err.so.2 -rw-r--r-- 1 root root 2.5M May 30 13:37 libcrypto.so.1.1 -rw-r--r-- 1 root root 1.7M May 30 13:37 libdb-5.3.so -rw-r--r-- 1 root root 199K May 30 13:37 libexpat.so.1 -rw-r--r-- 1 root root 146K May 30 13:37 libffi-ae16d830.so.6.0.4 -rw-r--r-- 1 root root 31K May 30 13:37 libffi.so.6 -rw-r--r-- 1 root root 515K May 30 13:37 libgmp.so.10 -rw-r--r-- 1 root root 1.4M May 30 13:37 libgnutls.so.30 -rw-r--r-- 1 root root 260K May 30 13:37 libgssapi.so.3 -rw-r--r-- 1 root root 299K May 30 13:37 libgssapi_krb5.so.2 -rw-r--r-- 1 root root 213K May 30 13:37 libhcrypto.so.4 -rw-r--r-- 1 root root 59K May 30 13:37 libheimbase.so.1 -rw-r--r-- 1 root root 35K May 30 13:37 libheimntlm.so.0 -rw-r--r-- 1 root root 207K May 30 13:37 libhogweed.so.4 -rw-r--r-- 1 root root 294K May 30 13:37 libhx509.so.5 -rw-r--r-- 1 root root 114K May 30 13:37 libidn2.so.0 -rw-r--r-- 1 root root 195K May 30 13:37 libk5crypto.so.3 -rw-r--r-- 1 root root 14K May 30 13:37 libkeyutils.so.1 -rw-r--r-- 1 root root 561K May 30 13:37 libkrb5.so.26 -rw-r--r-- 1 root root 857K May 30 13:37 libkrb5.so.3 -rw-r--r-- 1 root root 43K May 30 13:37 libkrb5support.so.0 -rw-r--r-- 1 root root 55K May 30 13:37 liblber-2.4.so.2 -rw-r--r-- 1 root root 320K May 30 13:37 libldap_r-2.4.so.2 -rw-r--r-- 1 root root 151K May 30 13:37 liblzma.so.5 -rw-r--r-- 1 root root 223K May 30 13:37 libmpdec.so.2 -rw-r--r-- 1 root root 186K May 30 13:37 libncursesw.so.5 -rw-r--r-- 1 root root 215K May 30 13:37 libnettle.so.6 -rw-r--r-- 1 root root 1.2M May 30 13:37 libp11-kit.so.0 -rw-r--r-- 1 root root 14K May 30 13:37 libpanelw.so.5 -rw-r--r-- 1 root root 290K May 30 13:37 libpq.so.5 -rw-r--r-- 1 root root 4.5M May 30 13:37 libpython3.6m.so.1.0 -rw-r--r-- 1 root root 288K May 30 13:37 libreadline.so.7 -rw-r--r-- 1 root root 87K May 30 13:37 libroken.so.18 -rw-r--r-- 1 root root 107K May 30 13:37 libsasl2.so.2 -rw-r--r-- 1 root root 1.1M May 30 13:37 libsqlite3.so.0 -rw-r--r-- 1 root root 424K May 30 13:37 libssl.so.1.1 -rw-r--r-- 1 root root 74K May 30 13:37 libtasn1.so.6 -rw-r--r-- 1 root root 167K May 30 13:37 libtinfo.so.5 -rw-r--r-- 1 root root 1.5M May 30 13:37 libunistring.so.2 -rw-r--r-- 1 root root 27K May 30 13:37 libuuid.so.1 -rw-r--r-- 1 root root 162K May 30 13:37 libwind.so.0 -rw-r--r-- 1 root root 115K May 30 13:37 libz.so.1 -rw-r--r-- 1 root root 25K May 30 13:35 mmap.so drwxr-xr-x 3 root root 4.0K May 30 13:35 psycopg2cffi -rw-r--r-- 1 root root 32K May 30 13:35 readline.so -rw-r--r-- 1 root root 25K May 30 13:35 termios.so -rwxr-xr-x 1 root root 24M May 30 13:37 test root@02ac722806b1:/tmp# du -hs test.dist 49M test.dist
Я проверил работоспособность бинарного файла как в директории с библиотеками, так и вне неё. Как и ожидалось, без динамических библиотек бинарник не запустился:
root@02ac722806b1:/tmp# ./test Traceback (most recent call last): File "/tmp/psycopg2cffi/_impl/libpq.py", line 2, in <module psycopg2cffi._impl.libpq> ImportError: /tmp/psycopg2cffi/_impl/_libpq.so: cannot open shared object file: No such file or directory During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/tmp/test.py", line 4, in <module> import psycopg2cffi File "/tmp/psycopg2cffi/__init__.py", line 4, in <module psycopg2cffi> File "/tmp/psycopg2cffi/extensions.py", line 39, in <module psycopg2cffi.extensions> File "/tmp/psycopg2cffi/_impl/connection.py", line 10, in <module psycopg2cffi._impl.connection> File "/tmp/psycopg2cffi/_impl/exceptions.py", line 12, in <module psycopg2cffi._impl.exceptions> File "/tmp/psycopg2cffi/_impl/libpq.py", line 4, in <module psycopg2cffi._impl.libpq> File "/tmp/psycopg2cffi/_impl/_build_libpq.py", line 173, in <module psycopg2cffi._impl._build_libpq> File "/tmp/cffi/api.py", line 48, in __init__ ImportError: /tmp/_cffi_backend.so: cannot open shared object file: No such file or directory
При запуске из директории с библиотеками работает нормально.
Я завернул директорию в архив, скопировал на хост машину, после чего снова поднял свежий контейнер, добавил в него нужное для работы с postgres, получившийся архив, распаковал его и проверил работоспособность:
evgeniy@evgeniy [18:46:43]:~/files/work/test$ docker cp test.tgz 696b:/tmp/ root@696b026cc4c0:/tmp# tar xvzf test.tgz root@696b026cc4c0:/tmp# cd test.dist/ root@696b026cc4c0:/tmp/test.dist# ./test
Проблем с работоспособностью не было. После этого я закоммитил образ с именем test-light2 и сравнил результат:
evgeniy@evgeniy [18:48:57]:~/files/work/test$ docker images | grep ^test- test-light2 latest 558d05e49336 16 seconds ago 230MB test-light latest 50729ddfb0a7 About an hour ago 174MB test-full latest 5bec61900aa7 About an hour ago 507MB
Как и ожидалось, образ с бинарным файлом, упакованным с помощью Nuitka оказался больше. Вывод очевиден - pyinstaller работает во много раз быстрее, использует меньше ресурсов и создаёт более компактные бинарные файлы. Когда-нибудь я постараюсь сделать несколько нагрузочных тестов, чтобы понять, насколько отличается по производительности бинарный файл, собранный с помощью pyinstaller от бинарного файла, собранного с помощью nuitka.
Теги: админское, containers, docker, python