Pull to refresh

Как Docker помог нам достичь (почти) невозможного

Reading time 8 min
Views 31K
Original author: Travis Reeder
image С тех пор как мы начали работать над Iron.io, мы пытались решить проблему поддержания наших IronWorker-контейнеров в актуальном состоянии относительно новых сред выполнения и пакетов Linux. В течение последних двух лет IronWorker использовал одну и ту же среду выполнения без изменений. Пока, несколько недель назад, мы не выпустили в продакшен различные окружения для языков программирования.

С момента создания нашего сервиса, мы использовали только один контейнер, который содержал набор языковых сред и бинарных пакетов — Ruby, Python, PHP, Java, .NET и другие языки, а также библиотеки такие как ImageMagick, SoX и другие.

Этот контейнер и стратегия его использования начали устаревать, равно как и Ruby 1.9.1, Node 0,8, Mono 2 и прочие языки со старыми версиями, которые использовались в стеке по умолчанию. Со временем проблема стала ещё острее, поскольку люди начали использовать новые вещи, но были вынуждены изменять свой код для работы со старыми версиями языков.

Ограниченный одним LXC контейнером


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

Мы также не могли хранить различные LXC-контейнеры c разными версиями языков, поскольку они содержат полные копии операционной системы и библиотек (то есть ~2 ГБ на каждый образ). На самом деле это прекрасно работало бы в PaaS среде, такой как Heroku где процессы идут бесконечно, и можно просто получить правильный контейнер перед началом запуска процесса. В такой ситуации, были бы большие пользовательские образы для каждого клиента, но в случае с IronWorker всё иначе.

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

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

LXC работал как надо, но мы ломали голову над тем, как обновить или добавить что-либо к нашему существующему контейнеру, не ломая обратную совместимость и не используя безумное количество дискового пространства. Наши варианты казались довольно ограниченными и поэтому мы откладывали решение.

… и тогда пришел Docker


image

Впервые мы услышали о Docker'е более года назад. Мы помогли организовать GoSF MeetUp и Соломон Хайкс, создатель Docker'а посетил конференцию в марте 2013 и продемонстрировал свой новый проект Docker, который был написан на языке Go. На самом деле он опубликовал его именно в тот день, это был первый раз когда его кто-то увидел.

Демо было отличным, и более ста разработчиков в аудитории были впечатлены, тем что он и его команда сделали. (И сразу же, о чем свидетельствует один из его комментариев, Соломон начал новую методологию разработки под названием Shame Driven Development)

«Увы, он был слишком сырой», говорили мы днем ранее, «проект не был готов к продакшену, но действительно был достоен похвалы».

image
Соломон Хайкс и Трэвис Ридер на саммите OpenStack в 2013 году.

Месяцем позже, я встретился с Соломоном на OpenStack Summit в Портленде, чтобы поработать вместе и посмотреть, как мы могли бы использовать Docker для решения нашей задачи. (Я думал, что я пойду только на одну встречу, однако вместо этого мы провели очень много времени, работая с Соломоном и другими разработчиками).
Я играл с Docker'ом, а Соломон помогал мне понять, что он может делать и как работает. Это был не просто хороший проект, он решал трудную задачу хорошо спроектированным путем. И не делало его ущербным, то что он был первопроходцем написанным на языке Go и не имел приличного техничеcкого долга, по крайней мере с моей точки зрения.

Исследование и разработка


До Docker'a мы пробовали различные менеджеры пакетов в том числе попробовали поработать с Nix. Nix отличный проект и у него есть много хороших достоинств, но к сожалению, это было не совсем то, что нам было нужно. Nix поддерживает атомарные обновления, откаты и имеет декларативный подход к конфигурации системы.

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

В начале были такие требования:
Обеспечить различные версии одних и тех же языков (то есть ruby 1.9 и ruby 2.1)
Иметь безопасный способ обновить одну часть системы, не нарушая другие части (например обновить только python-библиотеки и не трогать ruby-библиотеки)
Использовать декларативный подход к конфигурации системы (простые скрипты которые описывают, то что должно быть внутри образа)
Создать простой способ проведения обновлений и их отката

В процессе стало ясно, что есть несколько других преимуществ использования Docker'а, о существовании которых мы и не предпологали. К ним относятся:
Создание отдельных и изолированных сред для каждого runtime/языка
Получение поддержки для CoW файловой системы (что переводит нас на более безопасный и эффективный уровень в управлении образами).
Получение надежного способа переключения между различными средами выполнения на лету

Работа с Docker'ом


Работая с Docker'ом, было не трудно интегрировать его, так как мы уже использовали LXC. (Docker дополняет LXC высокоуровневым API, выполняющимся на уровне процесса. Ссылка на StackOverflow ниже.)

После того как мы мигрировали наши существующие shell скрипты в Dockerfiles и создали образы, все что нам оставалось сделать, чтобы перейти от непосредственного использования LXC — это «docker run» (вместо «lxc-execute ') и указать ID образа, необходимого для каждой задачи.

Команда для запуска LXC-образа:

lxc-execute -n VM_NAME -f CONFIG_FILE COMMAND

Команда для запуска Docker-образа:

docker run -i -name=VM_NAME worker:STACK_NAME COMMAND

Следует отметить что мы немного отходим от рекомендуемых подходов по созданию и установке контейнеров.
Стандартный подход — либо создавать образы во время выполнения с помощью Dockerfiles, либо хранить их в закрытых/открытых репозиториях в облаке. Вместо этого мы создаем образы, а затем делаем снепшоты из них и храним в EBS, прикрепленной к нашей системе. Это сделано потому, что система должна стартовать очень быстро. Создание образов во время выполнения было плохим вариантом, даже загрузка их из внешнего хранилища была бы слишком медленной.

Базовые образы плюс Diff'ы


Использование Docker'a также решило проблему дискового пространства, так как каждый образ это просто набор изменений (diff) от базового образа. А это значит, что мы можем иметь один базовый образ, содержащий операционную систему и библиотеки Linux которые мы используем во всех образах, и использовать его в качестве основы для множества других образов. Размер унаследованного образа включает в себя лишь размер отличий от базового образа.

Например если установить Ruby, в новом образе будут содержатся только файлы, которые были установлены с Ruby. Чтобы это не казалось запутанным, давайте думать об этом, как о репозитарии Git, содержащем все файлы на компьютере, где базовый образ является веткой master, а все другие образы — это различные ветки, произведённые от базового образа. Эта способность включать различия и создавать образы на основе существующих контейнеров очень полезна, поскольку это позвол нам постоянно выпускать новые версии, добавлять библиотеки, пакеты и больше концентрироваться на решении задач.

Некоторые проблемы


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

Ну вот и всё. Вопросы к разработчикам Docker'а, главным образом сводились к нескольким исправлениям А что касается нового функционала Docker'а — того что есть нам достаточно. (Мы до сих пор ничего не пытались добавить в функцинал, поскольку существующий набор функций довольно обширен).

LXC, Контейнеры и Docker


LXC (LinuX Containers) — система виртуализации на уровне операционной системы, которая обеспечивает безопасный способ изолировать один или несколько процессов от других процессов, запущенных в одной и той же системе Linux. При использовании контейнеров ресурсы могут быть изолированы, сервисы ограничены, а процессам выделяется изолированное пространство операционной системы с собственной структурой файловой системы и сетевых интерфейсов. Несколько контейнеров могут использовать одно и то же ядро, но каждый контейнер может быть ограничен использованием только определенного количество ресурсов, таких как CPU, память и операции ввода-вывода. В результате, приложения, задачи и другие процессы могут быть сконфигурированы так, чтобы запускаться в качестве нескольких легких, изолированных экземпляров Linux на одной машине.

Docker построен поверх LXC, что позволяет осуществлять управление образами и развертыванием. Вот статья на StackOverflow от Соломона о различиях и совместимости между LXC и Docker:
Если вы взгляните на особенности Docker, большинство из них уже предусмотрено в LXC. Итак, что же добавляет Docker? Зачем мне использовать Docker, а не простой LXC?
Docker не является заменой LXC. „LXC“ относится к возможностям ядра Linux (в частности пространства имен и контрольные группы), которые позволяют изолировать процессы друг от друга, и контролировать распределение их ресурсов.
Docker предлагает инструмент высокого уровня с несколькими мощными функциональными возможностями поверх низкоуровневых функций ядра.
Читать далее >>

Docker в продакшене


image
Docker — основа „стеков“ IronWorker'a

Мы в настоящее время используем Docker в продакшене в рамках сервиса IronWorker. Вы можете выбрать один из 10 различных „стеков“ (контейнеров) для ваших задач, установив параметр „стек“ при загрузке кода. Если задуматься, это удобная возможность — вы можете указать версию языка для краткосрочной задачи, которая будет выполняться на любом количество ядер.

Использование Docker для управления образами позволяет обновлять образы, не боясь повредить другие части системы. Другими словами мы можем обновить образ Ruby 1.9, не трогая образ Ruby 2.1. (Поддержание согласованности имеет первостепенное значение в любой крупномасштабной системе, особенно когда вы поддерживаете большой набор языков).

У нас также есть более автоматизированный процесс для обновления образов с помощью Dockerfiles, что позволяет нам разворачивать обновления по предсказуемому графику. Кроме того, у нас есть возможность создавать пользовательские образы. Они могут быть сформированы по конкретной версии языка и/или включать определённые фреймворки и библиотеки.

Заглядывая в будущее


Решение использовать Docker в продакшене не было чрезвычайно рискованным шагом. Год назад возможно так было, но сейчас это стабильный продукт. То, что это новый продукт, является в наших глазах преимуществом. Он имеет минимальный набор возможностей и построен для крупномасштабных и динамичных облачных сред как наша.

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

И, на правах совета, мы предлагаем использовать „готовые к использованию“ Dockerfiles, скрипты и обшедоступные образы. Там есть много полезного, с чего можно начать. На самом деле мы вероятно сделаем наши Dockerfiles и образы обшедоступными, что означает, что люди будут иметь возможность легко запускать свои воркеры локально, а также мы сделаем возможность отправлять pull reqeust'ы для их улучшения.

Обработка десятки тысяч часов процессорного времени и миллионы задач каждый день почти в каждом языке — это не просто. Docker позволил нам решить некоторые серьезные проблемы ценой небольших усилий. Это увеличило нашу способность к инновациям, а также к созданию новых возможностей для IronWorker. Но, что не менее важно, это позволяет нам сохранить и даже превзойти гарантированные условия обслуживания над улучшением которых мы много работаем.

Docker имеет большое будущее, и мы рады, что приняли решение включить его в наш стек технологий.
Tags:
Hubs:
+28
Comments 9
Comments Comments 9

Articles