Docker 0.10/0.11: тестируем многоконтейнерный стенд, упираемся в сеть и тома
Разворачиваем тестовый стенд из трёх контейнеров на Docker 0.10/0.11 и разбираемся где заканчивается магия и начинаются реальные ограничения сети и хранения данных.
Docker 0.10 и 0.11 - предрелизная серия с улучшениями работы с томами и сетевой моделью; проект готовится к выпуску 1.0
В марте мы поставили Docker 0.9 на тестовую виртуалку, упаковали Flask + nginx и остановились на наблюдении «для локальной разработки и CI-стендов - интересно». С тех пор вышли 0.10 и 0.11, и в changelog добавились вещи, которые нас давно беспокоили: работа с томами и сетевая модель. Пришло время проверить, что из этого реально изменилось.
Сразу оговоримся: проект всё ещё пре-релизный, 1.0 не вышел. Мы это помним и продакшн не рассматриваем. Задача - понять где потолок для тестовых стендов и CI-окружений прямо сейчас.
Что нового в 0.10 и 0.11
Changelog обоих релизов достаточно объёмный, но нас интересовало три вещи:
- Улучшения volumes - Docker 0.10 добавил более предсказуемое поведение именованных томов при пересоздании контейнеров. До этого были случаи потери данных при
docker rm. - Сеть - 0.11 принёс доработки изоляции и возможность задавать диапазон адресов для docker0-бриджа через конфигурацию демона. Мелочь, но если в сети уже есть подсеть 172.17.x.x - это было настоящей болью.
- API v1.11 - изменения в
/containers/{id}/top, пара новых параметровdocker run. Нас как операторов это касается меньше, но значит что скрипты автоматизации придётся переписывать при обновлении.
Стенд: три контейнера вместо двух
Расширили мартовский стенд - добавили базу. Теперь картина такая: PostgreSQL, Flask-приложение, nginx. Три контейнера, которые должны знать друг о друге.
nginx (порт 80 -> хост) -> flask (5000) -> postgres (5432)
Запускали в таком порядке - сначала postgres, потом flask с линкингом на postgres, потом nginx с линкингом на flask:
docker run -d --name pg \
-v /data/pgdata:/var/lib/postgresql/data \
postgres:9.3
docker run -d --name flask \
--link pg:postgres \
myapp-flask
docker run -d --name nginx \
--link flask:flask \
-p 80:80 \
myapp-nginx
Работает. Линкинг передаёт в каждый контейнер переменные окружения с адресом и портом зависимости. Flask видит POSTGRES_PORT_5432_TCP_ADDR и POSTGRES_PORT_5432_TCP_PORT. Nginx видит координаты Flask. Всё подключилось с первого раза.
Где начинаются проблемы
Первое ощущение - красиво. Второе ощущение, когда начинаешь щупать края, - несколько менее радостное.
Линкинг работает только на одном хосте. Это принципиальное ограничение текущей модели: --link использует IP-адреса из подсети docker0, которые видны только локально. Стоит захотеть разнести postgres и nginx на разные машины - механизм линкинга не помогает. Нужно либо прокидывать порты наружу и указывать IP хоста явно, либо городить что-то поверх. Для тестового стенда на одной VM - нормально. Для чего-то реального с несколькими хостами - уже вопрос.
Порядок запуска имеет значение. Если Flask стартует раньше Postgres и тот ещё не готов принимать соединения - приложение падает, потому что переменные окружения с адресом получены, но сокет не слушает. Docker никак не отслеживает готовность сервиса внутри контейнера - только факт его запуска. Добавили в entrypoint цикл с pg_isready - некрасиво, но работает. Правильного способа декларировать зависимости между контейнерами в самом Docker пока нет.
Тома - лучше чем было, но не без странностей. Монтирование директории с хоста через -v /data/pgdata:/var/lib/postgresql/data работает предсказуемо: данные живут на хосте, контейнер можно убить и пересоздать, данные остаются. Но если забыть указать -v при пересоздании контейнера - данные уходят вместе с контейнерным слоем. Никакого предупреждения, никакого «вы уверены». Просто удалились.
С именованными томами через docker run -v pgdata:/var/lib/postgresql/data (без явного пути на хосте) ситуация интереснее: том создаётся под управлением Docker в /var/lib/docker/volumes/. При docker rm данные не удаляются - это то, что улучшили в 0.10. Но найти их руками без docker inspect - квест. Бекапить такие тома неудобно.
Логи растут без ограничений. stdout каждого контейнера копится в файлах на хосте. Механизма ротации нет - точнее, он есть в системе, но Docker его не использует. За несколько дней тестирования nginx-контейнер накопил несколько сотен мегабайт лога. На тестовом стенде это не критично, но для долгоживущих контейнеров - проблема, о которой надо помнить заранее.
Что с сетью в 0.11
Новая опция --bip при запуске docker daemon позволяет задать подсеть для docker0-бриджа. Казалось бы, мелочь. Но у нас в тестовой среде 172.17.0.0/16 уже занят маршрутами в сторону одного из клиентских сегментов, и Docker до 0.11 просто вставал на этот адрес без разбора. Конфликт был тихим - пакеты уходили не туда, диагностировалось долго. Теперь можно:
docker -d --bip=10.200.0.1/24
И docker0 встаёт на 10.200.0.1, контейнерам раздаётся 10.200.0.0/24. Это реальное улучшение для тех, у кого сеть не пустая.
Изоляция между контейнерами по умолчанию работает так: все контейнеры на одном хосте видят друг друга через docker0 без ограничений. Опция --icc=false при запуске демона закрывает это и оставляет только явно залинкованные пары. Мы включили - и сразу же обнаружили что один из вспомогательных контейнеров рассчитывал на прямой доступ к соседу без линкинга. Нашли не сразу.
Попытка описать зависимости через скрипт
После нескольких итераций запуск стенда у нас теперь выглядит как shell-скрипт примерно на 30 строк: создать том, запустить postgres, подождать готовности, запустить flask, запустить nginx. Это работает, но очевидно не масштабируется. Есть инструмент Fig (fig.sh) - YAML-описание для многоконтейнерных приложений. Мы смотрели на него бегло: идея правильная, один файл описывает всю конфигурацию. Но и Fig сейчас в активной разработке, и Docker меняет API - не хочется зависеть от двух нестабильных вещей одновременно.
Где это полезно прямо сейчас
После двух месяцев экспериментов у нас сложилась достаточно чёткая картина:
- CI-стенды - разворачивать изолированное окружение для прогона тестов быстро и без следов. Именно это сейчас наиболее зрелый use case.
- Локальная разработка - разработчик получает окружение, идентичное тестовому, без установки PostgreSQL/Redis/чего-угодно прямо на машину.
- Разовые задачи - запустить инструмент в изоляции, не засоряя хост.
Для чего это ещё не готово: долгоживущие сервисы, несколько хостов, что-то с нетривиальными требованиями к хранению данных. Не потому что концепция плохая, а потому что инструмент честно предрелизный и часть нужных механизмов в явной доработке.
Следим за Docker. 1.0 обещают скоро.