Как уменьшить размер контейнера с python проектом

Зайчатки разума

Записная книжка айтишника

Как уменьшить размер контейнера с python проектом

2019-05-30 18:59:31 — Evgeniy Shumilov

  Для многих небольших микросервисов и проектов я использую 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

comments powered by Disqus