Запуск контейнеров и сохранение данных

Что это такое

Запустить контейнер — значит стартовать процесс из образа с некоторой конфигурацией: значения окружения, опубликованные порты, подключённое хранилище и имя. Контейнеры обычно недолговечны — один процесс, одна задача, — и интересная работа идёт вокруг них: как они находят друг друга, где живут их данные и как наблюдают за их логами и кодами возврата. Именно это окружение решает, станет ли контейнеризованная система управляемой или превратится в задачу по отладке.

Почему это важно

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

Ключевые идеи

Контейнеры — именованные процессы, а не машины

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

docker run --env PORT=8080 --publish 8080:8080 --name api myapp:1.0
процесс завершился с 0  -> контейнер остановлен
процесс завершился с 1  -> контейнер остановлен и помечен как сбойный

Сети дают контейнерам имена для связи

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

Тома живут дольше контейнеров и остаются единственным долговечным диском

Именованный том лежит вне записываемого слоя контейнера и переживает его удаление; bind-монтирование напрямую отображает каталог хоста. И то, и другое — способ сохранить состояние между перезапусками. Решить нужно, какое состояние вообще должно лежать на машине: в настоящем развёртывании оно обычно живёт в управляемом сервисе, а том остаётся для кэшей и временного места.

именованный том:   живёт после удаления контейнера, им управляет движок
bind-монтирование: путь хоста, удобно для разработки
записываемый слой: исчезает вместе с контейнером

Логи идут в стандартный вывод, а не в файлы

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

Коды возврата управляют перезапуском и проверками работоспособности

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

Compose и оркестраторы формализуют те же идеи

Запускать несколько контейнеров вручную быстро становится неудобно. Compose описывает несколько контейнеров, их сеть и тома для локальной разработки; оркестратор делает то же на множестве машин, добавляя планирование и самовосстановление. И то, и другое — удобство над одними и теми же решениями: образ, окружение, сеть, том, политика перезапуска.

Практика

Запустите сервис с томом, который его переживёт

Запустите контейнер, который пишет файл в подключённый каталог, удалите контейнер и запустите новый с тем же томом.

Готово, когда: второй контейнер читает файл, записанный первым.

Соедините два контейнера по имени

Запустите небольшой веб-сервис и базу данных в одной пользовательской сети и заставьте сервис обращаться к базе по имени контейнера, а не по адресу.

Готово, когда: сервис подключается по имени, и вы можете объяснить, почему в сети по умолчанию это не сработало бы.

Частые ошибки

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

Что читать дальше

  • Образы и слои (06-cloud-devops/06-docker/01-images-and-layers) — как собирается артефакт, который здесь запускается.
  • Docker (06-cloud-devops/06-docker) — модель упаковки, стороной выполнения которой является этот дочерний урок.
  • Kubernetes (06-cloud-devops/14-kubernetes) — что приходит на смену ручным run и compose, когда появляется несколько машин и сервисов.