Docker. Best practices на примере образа Oracle xe 11g



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

    Сегодня можно найти тысячи образов в hub.docker.com. За счет своей простоты в создании образов, буквально за пол часа можно начать вносить свой вклад.

    Но многие забывают о best practices, и за счет этого docker hub наполнился огромным количеством не самых лучших образов.

    В этой статье я хочу описать на сколько просто и полезно создавать образы используя Best Practices на примере.

    В качестве примера я выбрал нетривиальный образ с oracle 11g xe GitHub docker-hub.

    В исходном проекте можно определить слабые места и недоработки, отсортированные по основным пунктам с best practices:

    Использование .dockerignore


    Очень полезный функционал, но, к сожалению, многие о нем не знают и не пользуются.
    В данном примере за счет добавления исключений в .dockerfile скорость сборки образа сократилась и, что самое главное, размер образа стал меньше более чем на 2Gb

    Конечно понятно, что в git хранить тяжелые бинарные файлы совсем не best practice, но пока упустим этот момент, как модно говорить «Работает — не трогай», или как любят говорить в Британии «Так исторически сложилось».

    В итоге 3 простые строчки существенно облегчили образ. Также я настоятельно рекомендую .git вносить в .dockerignore, т.к. в этой папке хранится ввесь репозиторий, в этом случае та-же копия больших бинарных файлов.


    oracle-xe_11.2.0-1.0_amd64.debaa
    oracle-xe_11.2.0-1.0_amd64.debab
    oracle-xe_11.2.0-1.0_amd64.debac
    .git
    .gitignore
    


    Запускать только один процесс на контейнер


    Это довольно распространенная ошибка, и допускается за счет того, что люди не до конца понимают принципы работы и риски.
    В первую очередь в глаза кидается SSHD и не очень правильная инструкция CMD
    CMD sed -i -E "s/HOST = [^)]+/HOST = $HOSTNAME/g" /u01/app/oracle/product/11.2.0/xe/network/admin/listener.ora; \
    	service oracle-xe start; \
    	/usr/sbin/sshd -D
    

    Минусы использования подобного подхода можно обсуждать очень долго, особенно если пользователь захочет «кастомизировать» входящую команду.

    В первую очередь удаляем SSHD так как он нам не нужен, даже если нам будет необходимо выполнить debug или просто подключится к консоле контейнера лучше использовать docker exec -it ${CONTAINER_ID} /bin/bash

    Также очевидно, что при остановке контейнера Gracefully останавливается только SSHD, в то время, как сама база останавливается по TERM сигналу как процесс без паррента, что не есть хорошо, особенно для базы данных, особенно для Oracle DB.

    по «sed» и «service start» уже можно предположить, что просто не будет, и разумно будет перенести ввесь описанный функционал в entrypoint.sh

    При подготовке ENTRYPOINT был вынужден использовать несколько костылей workarounds (в дальнейшем немного полит-корректней). Подробнее ENTRYPOINT разберем немного ниже, т.к. он затрагивает сразу несколько пунктов

    Минимизация количества слоев


    Этот пункт очень прост, но в то же время очень важен, так как Docker работает по наслоению инкрементальных изменений в ФС по одной на каждую инструкцию, вот пример рационального использования, главное стараться оставлять код читабельным и вместить все изменения в одну RUN инструкцию
    # Prepare to install Oracle
    RUN apt-get update && apt-get install -y -q libaio1 net-tools bc curl && \
    apt-get clean && \
    rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/* &&\
    ln -s /usr/bin/awk /bin/awk &&\
    mkdir /var/lock/subsys &&\
    chmod 755 /sbin/chkconfig &&\
    /oracle-install.sh
    

    Функционал по установке oracle перенесен в sh скрипт в пользу читабельности.

    Избегать установки лишних не самых необходимых пакетов


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

    apt-get clean && \
    rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/* /download/directory
    


    Контейнер должен быть эфемерный


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

    В нашем случае это файлы базы данных (принцип работы как холодный бэкап) с возможностью его использования при старте нового контейнера.

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

    Также не мало важно вынести базовые параметры как конфигурирование через ENV переменные.

    В итоге у меня получился вот такой ENTRYPOINT

    #!/bin/bash
    
    # Prevent owner issues on mounted folders
    chown -R oracle:dba /u01/app/oracle
    rm -f /u01/app/oracle/product
    ln -s /u01/app/oracle-product /u01/app/oracle/product
    # Update hostname
    sed -i -E "s/HOST = [^)]+/HOST = $HOSTNAME/g" /u01/app/oracle/product/11.2.0/xe/network/admin/listener.ora
    sed -i -E "s/PORT = [^)]+/PORT = 1521/g" /u01/app/oracle/product/11.2.0/xe/network/admin/listener.ora
    echo "export ORACLE_HOME=/u01/app/oracle/product/11.2.0/xe" > /etc/profile.d/oracle-xe.sh
    echo "export PATH=\$ORACLE_HOME/bin:\$PATH" >> /etc/profile.d/oracle-xe.sh
    echo "export ORACLE_SID=XE" >> /etc/profile.d/oracle-xe.sh
    . /etc/profile
    
    case "$1" in
    	'')
    		#Check for mounted database files
    		if [ "$(ls -A /u01/app/oracle/oradata)" ]; then
    			echo "found files in /u01/app/oracle/oradata Using them instead of initial database"
    			echo "XE:$ORACLE_HOME:N" >> /etc/oratab
    			chown oracle:dba /etc/oratab
    			chown 664 /etc/oratab
    			printf "ORACLE_DBENABLED=false\nLISTENER_PORT=1521\nHTTP_PORT=8080\nCONFIGURE_RUN=true\n" > /etc/default/oracle-xe
    			rm -rf /u01/app/oracle-product/11.2.0/xe/dbs
    			ln -s /u01/app/oracle/dbs /u01/app/oracle-product/11.2.0/xe/dbs
    		else
    			echo "Database not initialized. Initializing database."
    
    			printf "Setting up:\nprocesses=$processes\nsessions=$sessions\ntransactions=$transactions\n"
    			echo "If you want to use different parameters set processes, sessions, transactions env variables and consider this formula:"
    			printf "processes=x\nsessions=x*1.1+5\ntransactions=sessions*1.1\n"
    
    			mv /u01/app/oracle-product/11.2.0/xe/dbs /u01/app/oracle/dbs
    			ln -s /u01/app/oracle/dbs /u01/app/oracle-product/11.2.0/xe/dbs
    
    			#Setting up processes, sessions, transactions.
    			sed -i -E "s/processes=[^)]+/processes=$processes/g" /u01/app/oracle/product/11.2.0/xe/config/scripts/init.ora
    			sed -i -E "s/processes=[^)]+/processes=$processes/g" /u01/app/oracle/product/11.2.0/xe/config/scripts/initXETemp.ora
    			
    			sed -i -E "s/sessions=[^)]+/sessions=$sessions/g" /u01/app/oracle/product/11.2.0/xe/config/scripts/init.ora
    			sed -i -E "s/sessions=[^)]+/sessions=$sessions/g" /u01/app/oracle/product/11.2.0/xe/config/scripts/initXETemp.ora
    
    			sed -i -E "s/transactions=[^)]+/transactions=$transactions/g" /u01/app/oracle/product/11.2.0/xe/config/scripts/init.ora
    			sed -i -E "s/transactions=[^)]+/transactions=$transactions/g" /u01/app/oracle/product/11.2.0/xe/config/scripts/initXETemp.ora
    
    			printf 8080\\n1521\\noracle\\noracle\\ny\\n | /etc/init.d/oracle-xe configure
    
    			echo "Database initialized. Please visit http://#containeer:8080/apex to proceed with configuration"
    		fi
    
    		/etc/init.d/oracle-xe start
    		echo "Database ready to use. Enjoy! ;)"
    
    		##
    		## Workaround for graceful shutdown. oracle... ‿( ́ ̵ _-`)‿
    		##
    		while [ "$END" == '' ]; do
    			sleep 1
    			trap "/etc/init.d/oracle-xe stop && END=1" INT TERM
    		done
    		;;
    
    	*)
    		echo "Database is not configured. Please run /etc/init.d/oracle-xe configure if needed."
    		$1
    		;;
    esac
    


    Резюме



    В итоге, следуя Best Practices, мы получили целый ряд преимуществ:
    • Размер образа уменьшился на 3GB (с 3.8Gb до 825Mb)
    • Поддержка монтирования и повторного использования дата-файлов
    • Graceful остановка сервиса
    • Возможности для более тонкой настройке базы через параметры при старте контейнера


    Результаты работы и детали решения проблем вы можете найти на github и hub.docker.com

    Спасибо за внимание.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 37
    • +3
      У докера нет лучших практик. У докера есть «быстро» и «какой такой продакшен?».
      • +2
        А что вы скажите на Containerizing the Cloud with Docker on Google Cloud Platform и kubernetes который открыл гугл?
        И с личного опыта хочу сказать что продакшн на Docker есть.
        • +2
          Kubernetes не щупал.

          Насчёт второго — в этом и ужас. Оно в таком виде потом на продакшен, а потом вопросы: «ну что за подстава, мы его запускали как wget h ttp://...;docker run, а оно у нас все пароли из базы спёрло».

          У докера в том виде, как его сопровождает комьюнити, нет ни безопасности, ни best practice (в контексте «best of production»). Оно задумывалось как rapid development (мне надо _СЕЙЧАС_ это запустить и насрать на мнение aptitude о зависимостях), а оказалось в продакшене у многих хипстерско-вевбдванольных конторах без нормальных сисадминов (птички-облака-бигдата-а-что-такое-oom-killer).

          При правильной позиции сисадминов и вменяемых (с позиции devops) девелоперов его можно готовить и использовать как любую другую программу. Но в таких средах обычно docker ничего существенно не меняет, потому что у людей и CI нормальный, и пакеты в свой репозиторий (rpm/deb) выкладываются (сами).
          • 0
            ну кое что у докера все-таки есть, а именно имутабельность и повторяемость сетапа на всех этапах. То есть ты сразу пакуешь и енв и все вообще и это все будет повторяться раз за разом.
            К тому же в отличии от deb\rpm, паковать и тестить которые надо как на установку на чистую машину, так и на обновление, docker относительно прост в этом плане. И не надо учиться паковать дебки, и не надо писать сложные chef рецепты, которые по разному работают на разных операционках.

            Ну то есть как я понимаю docker очень сильно позволяет секономить нервов на внедрении devops практик в компании.

            Про секюрность — есть vault, conjur да и kerberos никто не отменял :) Вопросы сохранения и доставки секретов актуальны вообще везде, вне зависимости от докера.

            Конечно докер не серебрянная пуля, но много хорошего и полезного все же есть.
        • 0
          Но все же, согласитесь, всегда приятно если «быстро» будет чуточку легче и еще быстрей.
          Насчет продакшена, что скажете про www.iron.io?
          • +1
            Эм, я не до конца понял как оно у них внутри устроено. А поддержку «для клиентов» вам все впилят куда угодно. Будете самолёты с ПО в докере покупать — будут вам самолёты с докером. Захотите атомные реакторы и сможете за это платить — будут вам атомные реакторы с деплоем через curl.
          • +1
            Если уметь его готовить — это очень мощный инструмент, а если образ всегда начинается с FROM ubuntu, то это уже личные проблемы каждого, такие люди и curl ... | bash и sudo make install делают. Инструмент не виноват в глупости того, кто пользуется инструментом. Молотком можно людей калечить, а можно гвозди забивать.
          • +1
            Я так и не понял как вы связали .dockerignore с уменьшением веса финального образа. Этот файл влияет только на процесс сборки, то есть что будет доступно для копирования в Dockerfile. Однако, если не копировать, то образ и не будет увеличиваться. Так что на вес образа влияла команда вида COPY * /tmp (или ADD).

            Вот тут можно наглядно сравнить два образа: ImageLayers.

            В остальном, спасибо, что несёте добро в массы, а то неоправданно огромные образы буквально завалили DockerHub.
            • 0
              По поводу .dockerignore — в процессе сборки каждая инструкция это инкрементальный слой.
              В начале сборки загружаются все файлы которые есть в папке рядом с Dockerfile, этот первый этап также является слоем
              • +2
                Это неправда. Я даже провёл только что эксперимент:

                $ du -sh .
                413M    .
                
                $ cat Dockerfile
                FROM alpine
                
                COPY serial.txt /tmp/serial.txt
                
                $ cat .dockerignore
                cat: .dockerignore: No such file or directory
                
                $ ls -lah serial.txt
                -rw-r--r-- 1 frol frol 30 May 25 15:43 serial.txt
                
                $ docker build -t qq .
                Sending build context to Docker daemon 432.5 MB
                Sending build context to Docker daemon
                Step 0 : FROM alpine
                 ---> 8697b6cc1f48
                Step 1 : COPY serial.txt /tmp/serial.txt
                 ---> 2209f356a4ea
                Removing intermediate container 9d055644cb5b
                Successfully built 2209f356a4ea
                
                $ docker images | grep qq
                qq                        latest              2209f356a4ea        5 seconds ago       5.238 MB
                


                У меня в папке файлов на 413МБ, никакого .dockerignore нет, docker build запаковал в tar все файлы и получил 435МБ, которые «отправил на сборку» (так работает build) и в образ я добавил только файл serial.txt, весящий 30 байт, но финальный образ весит 5.2МБ!

                Таким образом .dockerignore может сократить этап архивирования для build, но файлы из текущего каталога не попадут в образ если вы их туда не скопируете командами COPY или ADD, что видно из моего эксперимента.
                • –1
                  Интересно, возможно я шибаюсь. Похоже на то что доработали это начиная с версии 1.4
                  • 0
                    Убедитесь сами и поправьте статью дабы не вводить людей в заблуждение, пожалуйста.
                    • 0
                      Как минимум вот тут все описано
                      docs.docker.com/articles/dockerfile_best-practices

                      Use a .dockerignore file
                      For faster uploading and efficiency during docker build, you should use a .dockerignore file to exclude files or directories from the build context and final image. For example, unless.git is needed by your build process or scripts, you should add it to .dockerignore, which can save many megabytes worth of upload time.

                      • 0
                        Предпочитаете верить написанной глупости (неточности/устаревшей информации) вместо своих собственных глаз?
                        • 0
                          возможно в вашем случае dockerignore сработал потому что вы сделали
                          ADD .
                          
                          ?
                          • 0
                            В статье есть все ссылки как на результат так и на исходник
                            Я уже признал что дело было не в этом,
                            вот причина разростания образа в несколько раз:
                            ADD chkconfig /sbin/chkconfig
                            ADD init.ora /
                            ADD initXETemp.ora /
                            ADD oracle-xe_11.2.0-1.0_amd64.debaa /
                            ADD oracle-xe_11.2.0-1.0_amd64.debab /
                            ADD oracle-xe_11.2.0-1.0_amd64.debac /
                            # ADD oracle-xe_11.2.0-1.0_amd64.deb /
                            RUN cat /oracle-xe_11.2.0-1.0_amd64.deba* > /oracle-xe_11.2.0-1.0_amd64.deb
                            
                            ...
                            # Remove installation files
                            RUN rm /oracle-xe_11.2.0-1.0_amd64.deb*
                            
                            • 0
                              ну да, так оно и есть. Я не увидел обновление статьи. Тут скорее надо просто базово понимать как работают слои в докере, я ниже описывал. Если мы добавляем файл создается слой, если удаляем — еще один слой поверх него.
                              Если слои нафиг не нужны можно вообще компактить итоговые имеджи, но зачем это можеть быть надо представить сложно.
                              • 0
                                Если слои нафиг не нужны можно вообще компактить итоговые имеджи, но зачем это можеть быть надо представить сложно.

                                Разве что для кеширования при будущих сборках
                                • 0
                                  ну смысл как раз в том, чтобы запихивать в один шаг все те действия, на которые не желательно создавать один слой. Если имадж скомпактить, тогда апдейт по сети будет не на размер дифа, а на размер имеджа.
                        • 0
                          Да, вы правы, я ошибся…
                          • 0
                            Существенного уменьшения размера образа я добился за счет оптимизации Dockerfile и объединения RUN инструкций и отказа от COPY/ADD
                        • +3
                          Нет, всегда так и было. Если вы добавляете не конкретные файлы, а целые директории, то .dockerignore позволяет указать какие типы дочерних файлов/директорий добавлять всё же не стоит.
                  • 0
                    Очень похоже на вредные советы от Остера :)

                    самое главное, размер образа стал меньше более чем на 2Gb


                    Мне кажется, что самое главное тут не то, что образ стал меньше, а то, что ты не тащишь гит, который не нужен, на прод. Вообще best practices от докера это предварительная подготовка артифактов, возможно тоже в отдельном докер контейнере и экспорт их в новый имедж.
                    К тому же большой плюс dockerignore, что если репо изменилось, например появился новый коммит, или что-то еще — совсем не обязательно снова запускать шаги, которые уже были закешированы. И с этим связан второй «вредный совет»:
                    главное стараться оставлять код читабельным и вместить все изменения в одну RUN инструкцию

                    Вмещать все в одну инструкцию не просто тупо, а очень тупо именно из-за кеширования. Рассматривайте любой RUN как транзакцию, потому что так она и работает — если говорить немного упрощенно, run выполняется как отдельный запуск докер контейнера, на основании предыдущего имеджа, после чего diff по файловой системе сохраняется в новый имадж. Пытаться забить все в один ран актуально только тогда, когда необходимо действия выполнять одной группой, то есть изменение последнего шага должно влиять на повторение выполнения первого шага. Иначе вы будете постоянно повторять одно и то же действие.
                    То есть, например, правильный пример выглядит так:

                    RUN apt-get update && apt-get install ruby-2.1
                    RUN gem install bundler
                    ADD Gemfile /project/Gemfile
                    ADD Gemfile.lock /project/Gemfile.lock
                    RUN bundle install
                    ADD . /project
                    


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

                    Запускать только один процесс на контейнер

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


                    ШТА? докер сам по себе не умеет менеджить процессы. Очень желателен какой-то менеджер процессов, подробнее можете прочитать например тут

                    В общем непонятно как автор вообще может с серьзным лицом говорить что это все бест практис. Это скорее антипаттерны.
                    • 0
                      Под одним процессом подразумевается что не нужно лепить в 1 контейнер кучу сервисов, и SSHD на дисерт, к примеру.
                      • 0
                        ну init это один процес, и процес который init мэнеджит — еще один, уже два.
                        sshd лепили потому что docker exec был жопный. Начиная с 1.5 помойму, уже все стало ок и его не лепят. Ну и разделить доступ к контейнерам, чтоб не давать рута сразу на все контейнеры можно только через sshd, но это уже отдельная песня.
                        • 0
                          ну init это один процес, и процес который init мэнеджит — еще один, уже два.

                          зачем процесс который менеджит init?
                          И под процессом я закладывал смысл не 1 pid а 1 сервис, надеюсь о child процессах не будем прододжать…
                          • 0
                            зачем процесс который менеджит init?

                            процесс который менеджит init это и есть ваш сервис.

                            И под процессом я закладывал смысл не 1 pid а 1 сервис

                            В линукс терминологии есть вполне себе определение процесса.
                            Естественно, навешивать в один имедж и php-fpm и nginx и mysql не надо, но описание один контейнер – один процесс это четкий антипаттерн

                            Если у вас не будет инита, который менеджит запущенные процессы, то будет много всякой фигни. Я выше ссылку дал, там про это подробнее расписано. Яркий пример это как раз поведение child процесса, когда умирает родитель — он должен присвоиться иниту, как происходит во всем линуксе, без инита это просто анменедж процесс, который принесет много головной боли.
                            • 0
                              Антипаттерн и тут? docs.docker.com/articles/dockerfile_best-practices
                              Run only one process per container
                              In almost all cases, you should only run a single process in a single container. Decoupling applications into multiple containers makes it much easier to scale horizontally and reuse containers. If that service depends on another service, make use of container linking.


                              Это официальная дока, и я с ней полностью согласен.
                              В данном случае(с Oracle DB) по другому нормально создать контейнер не получилось, если вы имеете свое мнение по этому поводу — пишите предложения
                              • 0
                                Я уже более мение аргументировал на тему необходимости инита. Да и вообще на эту тему очень много всего уже было написано. Одну из базовых ссылок с инфой я приводил выше. Мне просто не понятно вы полностью согласны с один процесс на контейнер потому что это официальная дока или потому что имеете сильную позицию?

                                Например я знаю и минусы использования инита + процесс — это невозможность пробрасывать сигналы. Но мне кажется что неуправляемые процессы внутри контейнера и как следствие неубиваемые контейнеры это немного сильнее неприятнее, чем определенные нюансы при пробросе сигнала.
                                • 0
                                  Я согласен с вашей точкой зрения.
                                  Я против супервизоров с несколькими сервисами. Я за то чтобы инфраструктуру разбивать на мельчайшие детали(microservices). Это дает больше отказоустойчивости, в случае если какой-либо компонент отваливается — все остальное продолжает работать
                      • 0
                        Установка пакетов с нуля — довольно медленная операция, пусть и не частая. Мы сначала обновляем зависимости, а потом уже запускаем билд контейнера. То есть для докера, обновление пакетов выглядит как обновление всех остальных исходников. Такая схема отрабатывает гораздо быстрее и без лишних промежуточных коммитов.
                        • 0
                          Ну то есть есть некий базовый имедж с зависимостями который вы и обновляете? А потом в имедже с артефактом просто от него наследуетесь? Ну это вполне стандартный способ разделения мух и котлет. Но с точки зрения докера нет никакой разницы между таким имеджем и слоем, кроме как удобство обновления и управления.
                          • 0
                            Нет, примерно таким скриптом:

                            git pull
                            npm install
                            docker build --rm -t my/app .
                            


                            А в Dockerfile уже все сбилженные файлы просто добавляются в образ.
                            • 0
                              а, ну следующий уровень билдить в отдельном докер имадже и версионировать билд окружение и билд скрипт, это достаточно удобно, потому что можно тестировать билд скрипт прямо на машинах разработчиков и не зависить от окружения на билд сервере.
                              • 0
                                Не очень понял о чём вы.
                                • 0
                                  Я имею ввиду саму процедуру, когда ты билдишь артефакт проводить в отдельном контейнере, из которого уже экспортить сам артефакт.
                                  • 0
                                    А, ну у нас сначала собирается образ с билдером, потом он билдит исходники приложения в хостовой директории, а потом билдится образ из получившихся файлов :-)

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.