Знакомство с Docker или зачем это всё нужно
Всем привет. Меня зовут Алексей, вместе с командой я занимаюсь разработкой прикладных решений в системе Saby компании Тензор. В своей статье хочу поговорить про Docker.
При знакомстве с любой технологией важно понимать, зачем инвестировать своё время в её изучение. Для этого нужно иметь хотя бы общее представление о предметной теме.
Сейчас, когда Docker используется повсеместно, многие разработчики (особенно молодые) относятся к нему, как к данности, при этом не до конца понимая, зачем, собственно, он используется и какие проблемы решает. На Хабре есть ознакомительные статьи про Docker, однако они не в полной мере (вернее, не в той мере, в которой хотелось бы мне:)) освещают данный вопрос. Так возникла идея написания этой статьи. При ее подготовке были использованы: информация из книги Docker In Action, данные с профильных сайтов, собственные разработки для выступления на внутрикорпоративном митапе Тензора, материалы с IT-форумов и, конечно, личный опыт. Если вы опытный разработчик/администратор/devops, и уже давно используете Docker, вы вряд ли узнаете что-то новое из статьи и можете смело проходить мимо. Если же ваш профессиональный путь только начинается, надеюсь, что этот материал поможет вам в освоении данной технологии.
Разработка программного обеспечения — это не только написание кода. Скажу больше: во многих прикладных задачах написать сам код не так уж сложно (если говорить об отрасли в среднем). Получить данные из БД (или любого другого хранилища), отобразить эти данные, обработать, сохранить обратно в БД, вызвать внешние сервисы и т.п. — все это довольно распространенные задачи, с которыми, уверен, сталкивались многие. Но это еще не все. Кроме этого необходимо обеспечить выполнение нефункциональных требований к системе:
корректная обработка ошибок/исключений;
производительность;
безопасность;
масштабируемость;
наблюдаемость;
обновляемость;
стоимость обслуживания и поддержки после ввода в промышленную эксплуатацию;
отказоустойчивость;
и т.д.
Если мы все это соберем воедино, история станет не такой простой. Давайте посмотрим на примере. Предположим, что у нас есть некая система, состоящая из нескольких компонентов, работающих на одном сервере. Эти компоненты могут использовать различные библиотеки, вызывать компоненты и функции ОС:
Узкое место очевидно: если что-то случится с сервером, вся система перестанет работать. Как обеспечить масштабируемость и отказоустойчивость? Одно из решений — увеличить число узлов нашей системы (т.е. использовать горизонтальное масштабирование. В свою очередь под вертикальным масштабированием подразумевают увеличение мощности железа — CPU, памяти и т.п.). Этого можно добиться, например, переместив компоненты системы с физического сервера на виртуальные машины и запустив их на нескольких серверах:
Теперь, если какой-либо компонент системы, ВМ или сервер, упадет, — остальные продолжат работать, и вся система продолжит функционировать хотя, возможно, с чуть меньшей производительностью. Это, конечно, сильно упрощенная схема, но она позволяет понять суть. В рамках данной статьи не будем углубляться в детали, как организовать оркестрацию кластера, теорию репликации БД, настройку распределенного кеша и другие связанные вопросы. Все это достаточно сложные и обширные темы, по которым написана не одна статья и книга.
Итак, использование кластера позволило улучшить отказоустойчивость системы. Однако, если заглянем внутрь виртуальных машин, то обнаружим, что ситуация с компонентами и зависимостями не изменилась. Внутри ОС компоненты по-прежнему используют различные библиотеки. При этом возможно, что одному компоненту нужна одна версия определенной библиотеки, а другому — иная версия этой же библиотеки.
Как обновлять такой кластер? Можно загружать обновления на каждую виртуальную машину и обновлять все компоненты один за одним. В небольшой системе это, возможно, сработает, но что произойдет, когда количество компонентов вырастет, и количество сред, в которых эти компоненты нужно запускать, тоже увеличится? Зависимости будут накапливаться, как снежный ком, делая поддержку системы все сложнее и дороже. В итоге получаем нечто похожее на следующую таблицу, которую также называют «матрицей ада»:
Число элементов этой матрицы будет тем больше, чем больше компонентов и сред будет в нашей системе. Также проблемы с зависимостями придется решать при настройке CI/CD — чем их больше, тем сложнее будет настройка.
Можно ли как-нибудь улучшить ситуацию? Например, если бы можно было упаковать компонент/сервис нашей распределенной системы вместе со всеми необходимыми зависимостями, конфигурацией, переменными среды и т. д. в «нечто», что позволило бы нам запускать этот компонент/сервис в любой среде на любой ОС (на ноутбуке разработчика, автономном сервере, в кластере и т.п.). Тогда мы могли бы просто переносить это «нечто» между средами:
Здесь на сцену выходят контейнеры и Docker. Говоря о контейнерах, в воображении возникает баржа, перевозящая грузовые контейнеры. Как мы увидим ниже, образ Docker — это своего рода грузовой контейнер, внутри которого содержатся компоненты и их зависимости. Эта аналогия отражена в логотипе Docker:
Термин «контейнер» пришел из операционных систем на базе 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 и др.)
Вместе они упрощают работу с контейнерами и позволяют построить собственную инфраструктуру управления контейнерами:
Docker имеет открытый исходный код и работает в Linux, Windows (поверх Hyper-V или WSL2) и MacOS. Обратите внимание, что хотя запустить Linux контейнер (контейнер, у которого базовый образ основан на Linux) в Docker, работающем в Windows, не составляет труда (под капотом WSL2 представляет собой легковесную виртуальную машину с Linux ядром), в то же время запустить Windows контейнер (контейнер, у которого базовый образ основан на Windows Server Core) в Docker, работающем в Linux, не так просто:
Для этого есть решения, но не совсем очевидные. Из того, что я нашел: можно запустить ОС Windows Server Core внутри VirtualBox, который, в свою очередь, работает в контейнере Docker в Linux, либо использовать оболочку Wine. Также при этом необходимо решить проблемы с лицензией для Windows.
Обратите внимание, что контейнер — это не то же самое, что виртуальная машина:
Виртуальные машины:
запускают собственную ОС, в которой запускается установленное ПО;
требуют больше ресурсов (зависит от сценариев, но в среднем на стандартном ПК можно запустить лишь несколько ВМ);
стартуют медленнее;
проблемы снэпшотов: большой размер, сложности с отслеживанием изменений (diffs) и версионностью;
из одного набора VMX/VMDK файлов можно запустить одну ВМ.
С другой стороны, контейнеры:
работают в одной родительской ОС;
требуют меньше ресурсов (также зависит от сценариев, но опять же на обычном ПК можно запустить множество контейнеров одновременно);
обычно стартуют в течение нескольких секунд (сейчас не берем во внимание сервисы в docker compose с зависимостями друг от друга, т.е. когда один контейнер ждет, пока другой будет полностью запущен и готов к работе);
изменения добавляются как дополнительный слой: можно отследить изменения и просматривать историю;
из одного образа (image) можно запустить множество контейнеров.
Теперь, обладая этими знаниями, можно решить упомянутую выше «матрицу ада», используя контейнеры:
Но, как это часто бывает, решая одну проблему, мы получаем другую: как управлять этой матрицей? За ответом на этот вопрос необходимо обратиться к технологиям оркестрации контейнеров, таким как Kubernetes и Docker Swarm. Однако, эта тема уже выходит за рамки текущей статьи.
В заключение хотел бы отметить, что Docker интересен именно как часть большего пазла, позволяющего построить инфраструктуру развертывания. Представьте, что программисты публикуют релиз в git. Система сборки по вебхукам (webhooks) получает уведомление, с помощью Dockerfile собирает образы компонентов, используя последнюю версию исходного кода, и добавляет их в локальный реестр образов. После этого уже система оркестрации получает уведомление, запускает контейнеры из новых образов, а контейнеры из старых образов останавливаются (если говорить про Kubernetes, то этому соответствует запуск и остановка pod-ов). В итоге наша система не только сама мониторит свое состояние и умеет адаптироваться под увеличенные нагрузки (с помощью связки Kubernetes + Docker), но и обновляет сама себя. По-моему, звучит достаточно интересно, чтобы потратить время на изучение этих технологий.