Знакомство с Docker или зачем это всё нужно

e246efc8f6479ce5280bd7f75e9e6df9.png

Всем привет. Меня зовут Алексей, вместе с командой я занимаюсь разработкой прикладных решений в системе Saby компании Тензор. В своей статье хочу поговорить про Docker.

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

Сейчас, когда Docker используется повсеместно, многие разработчики (особенно молодые) относятся к нему, как к данности, при этом не до конца понимая, зачем, собственно, он используется и какие проблемы решает. На Хабре есть ознакомительные статьи про Docker, однако они не в полной мере (вернее, не в той мере, в которой хотелось бы мне:)) освещают данный вопрос. Так возникла идея написания этой статьи. При ее подготовке были использованы: информация из книги Docker In Action, данные с профильных сайтов, собственные разработки для выступления на внутрикорпоративном митапе Тензора, материалы с IT-форумов и, конечно, личный опыт. Если вы опытный разработчик/администратор/devops, и уже давно используете Docker, вы вряд ли узнаете что-то новое из статьи и можете смело проходить мимо. Если же ваш профессиональный путь только начинается, надеюсь, что этот материал поможет вам в освоении данной технологии.

Разработка программного обеспечения — это не только написание кода. Скажу больше: во многих прикладных задачах написать сам код не так уж сложно (если говорить об отрасли в среднем). Получить данные из БД (или любого другого хранилища), отобразить эти данные, обработать, сохранить обратно в БД, вызвать внешние сервисы и т.п. — все это довольно распространенные задачи, с которыми, уверен, сталкивались многие. Но это еще не все. Кроме этого необходимо обеспечить выполнение нефункциональных требований к системе:

  • корректная обработка ошибок/исключений;

  • производительность;

  • безопасность;

  • масштабируемость;

  • наблюдаемость;

  • обновляемость;

  • стоимость обслуживания и поддержки после ввода в промышленную эксплуатацию;

  • отказоустойчивость;

  • и т.д.

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

407df49fa7b2b2fe5104de4814e0cb1a.jpg

Узкое место очевидно: если что-то случится с сервером, вся система перестанет работать. Как обеспечить масштабируемость и отказоустойчивость? Одно из решений — увеличить число узлов нашей системы (т.е. использовать горизонтальное масштабирование. В свою очередь под вертикальным масштабированием подразумевают увеличение мощности железа — CPU, памяти и т.п.). Этого можно добиться, например, переместив компоненты системы с физического сервера на виртуальные машины и запустив их на нескольких серверах:

f5d663c72c2574a90818480692c280a1.jpg

Теперь, если какой-либо компонент системы, ВМ или сервер, упадет, — остальные продолжат работать, и вся система продолжит функционировать хотя, возможно, с чуть меньшей производительностью. Это, конечно, сильно упрощенная схема, но она позволяет понять суть. В рамках данной статьи не будем углубляться в детали, как организовать оркестрацию кластера, теорию репликации БД, настройку распределенного кеша и другие связанные вопросы. Все это достаточно сложные и обширные темы, по которым написана не одна статья и книга.

Итак, использование кластера позволило улучшить отказоустойчивость системы. Однако, если заглянем внутрь виртуальных машин, то обнаружим, что ситуация с компонентами и зависимостями не изменилась. Внутри ОС компоненты по-прежнему используют различные библиотеки. При этом возможно, что одному компоненту нужна одна версия определенной библиотеки, а другому — иная версия этой же библиотеки.

Как обновлять такой кластер? Можно загружать обновления на каждую виртуальную машину и обновлять все компоненты один за одним. В небольшой системе это, возможно, сработает, но что произойдет, когда количество компонентов вырастет, и количество сред, в которых эти компоненты нужно запускать, тоже увеличится? Зависимости будут накапливаться, как снежный ком, делая поддержку системы все сложнее и дороже. В итоге получаем нечто похожее на следующую таблицу, которую также называют «матрицей ада»:

3183b26fba166d9c0975bab8919cf770.jpg

Число элементов этой матрицы будет тем больше, чем больше компонентов и сред будет в нашей системе. Также проблемы с зависимостями придется решать при настройке CI/CD — чем их больше, тем сложнее будет настройка.

Можно ли как-нибудь улучшить ситуацию? Например, если бы можно было упаковать компонент/сервис нашей распределенной системы вместе со всеми необходимыми зависимостями, конфигурацией, переменными среды и т. д. в «нечто», что позволило бы нам запускать этот компонент/сервис в любой среде на любой ОС (на ноутбуке разработчика, автономном сервере, в кластере и т.п.). Тогда мы могли бы просто переносить это «нечто» между средами:

7ab74ee9f157c88a5e48238da8cc76eb.jpg

Здесь на сцену выходят контейнеры и Docker. Говоря о контейнерах, в воображении возникает баржа, перевозящая грузовые контейнеры. Как мы увидим ниже, образ Docker — это своего рода грузовой контейнер, внутри которого содержатся компоненты и их зависимости. Эта аналогия отражена в логотипе Docker:

6bab66427fd289c4daa2478ff5ed8a68.jpg

Термин «контейнер» пришел из операционных систем на базе Unix. Первоначально использовался термин «jail» (тюрьма), но с 2005 года с выпуском Sun Solaris 10 и Sun Containers более распространенным стал термин «контейнер». Контейнер — это изолированная среда выполнения приложений. По умолчанию, приложению, запущенному внутри контейнера, запрещен доступ к ресурсам за пределами своего контейнера. Основной проблемой ручного создания и конфигурирования контейнеров была сложность и, как следствие, вероятность непреднамеренных ошибок.

Docker упрощает создание контейнеров. В контексте Docker контейнеры — это дочерние процессы фоновой службы Docker. Контейнеры запускаются из образов (images). Как уже упоминалось выше, образ Docker является хорошим аналогом грузового контейнера. У каждого образа есть т.н. базовый образ (указывается в команде FROM … в Dockerfile — скрипте для создания образов), поверх которого слоями добавляются ваши изменения (напр. установка расширения mysql для образа на основе php fpm добавляет новый слой, с указанным расширением). Базовый образ может быть основан как на Linux, так и на Windows Server Core. Проверить, на основе какой ОС построен ваш образ, можно с помощью команды docker inspect.

Образы хранятся в репозиториях (repositories), которые, в свою очередь, организованы в реестры (registries). Самый известный общедоступный реестр образов — DockerHub (тот самый, который не так давно нам неожиданно заблокировали, а потом также неожиданно разблокировали). Также возможно запустить собственный локальный реестр образов внутри компании.

Docker состоит из нескольких частей:

  • утилита командной строки;

  • фоновая служба;

  • набор удаленных сервисов (DockerHub, JFrog и др.)

Вместе они упрощают работу с контейнерами и позволяют построить собственную инфраструктуру управления контейнерами:

cb7280a56f456581171bde8ff2c1a179.jpg

Docker имеет открытый исходный код и работает в Linux, Windows (поверх Hyper-V или WSL2) и MacOS. Обратите внимание, что хотя запустить Linux контейнер (контейнер, у которого базовый образ основан на Linux) в Docker, работающем в Windows, не составляет труда (под капотом WSL2 представляет собой легковесную виртуальную машину с Linux ядром), в то же время запустить Windows контейнер (контейнер, у которого базовый образ основан на Windows Server Core) в Docker, работающем в Linux, не так просто:

1e970f5038a687d552168c8e41fb31c0.jpg

Для этого есть решения, но не совсем очевидные. Из того, что я нашел: можно запустить ОС Windows Server Core внутри VirtualBox, который, в свою очередь, работает в контейнере Docker в Linux, либо использовать оболочку Wine. Также при этом необходимо решить проблемы с лицензией для Windows.

Обратите внимание, что контейнер — это не то же самое, что виртуальная машина:

b7abdcda773114cfb8c017550a4a6e2f.jpg

Виртуальные машины:

  • запускают собственную ОС, в которой запускается установленное ПО;

  • требуют больше ресурсов (зависит от сценариев, но в среднем на стандартном ПК можно запустить лишь несколько ВМ);

  • стартуют медленнее;

  • проблемы снэпшотов: большой размер, сложности с отслеживанием изменений (diffs) и версионностью;

  • из одного набора VMX/VMDK файлов можно запустить одну ВМ.

С другой стороны, контейнеры:

  • работают в одной родительской ОС;

  • требуют меньше ресурсов (также зависит от сценариев, но опять же на обычном ПК можно запустить множество контейнеров одновременно);

  • обычно стартуют в течение нескольких секунд (сейчас не берем во внимание сервисы в docker compose с зависимостями друг от друга, т.е. когда один контейнер ждет, пока другой будет полностью запущен и готов к работе);

  • изменения добавляются как дополнительный слой: можно отследить изменения и просматривать историю;

  • из одного образа (image) можно запустить множество контейнеров.

Теперь, обладая этими знаниями, можно решить упомянутую выше «матрицу ада», используя контейнеры:

58abb07da748f9496504b520bc456c1d.jpg

Но, как это часто бывает, решая одну проблему, мы получаем другую: как управлять этой матрицей? За ответом на этот вопрос необходимо обратиться к технологиям оркестрации контейнеров, таким как Kubernetes и Docker Swarm. Однако, эта тема уже выходит за рамки текущей статьи.

В заключение хотел бы отметить, что Docker интересен именно как часть большего пазла, позволяющего построить инфраструктуру развертывания. Представьте, что программисты публикуют релиз в git. Система сборки по вебхукам (webhooks) получает уведомление, с помощью Dockerfile собирает образы компонентов, используя последнюю версию исходного кода, и добавляет их в локальный реестр образов. После этого уже система оркестрации получает уведомление, запускает контейнеры из новых образов, а контейнеры из старых образов останавливаются (если говорить про Kubernetes, то этому соответствует запуск и остановка pod-ов). В итоге наша система не только сама мониторит свое состояние и умеет адаптироваться под увеличенные нагрузки (с помощью связки Kubernetes + Docker), но и обновляет сама себя. По-моему, звучит достаточно интересно, чтобы потратить время на изучение этих технологий.

© Habrahabr.ru