Докер - это просто!

Да-да, это просто. И даже ещё проще. Ну вот прямо совсем.

Я хочу поделиться историей, как я и напарник начали работать с докером. Опишу проблемы, с которыми столкнулись, и как мы их решили.

С чего всё началось

Нам дали проект. Нет, не так. На нас вывали КУЧУ логинов-паролей и сказали, что где-то там есть доступ на сервер, где живёт проект, который мы должны поддерживать.

Смахнув скупую мужскую слезу и сжав волю в кулак, мы начали разбираться.

Оказалось, что "проект" состоит из 10 репозиториев, 4 vds-серверов, 50 докер-контейнеров, ранчера и кибаны. Для нас это было, мягко говоря, неожиданно. Сам по себе проект не был каким-то высоконагруженным приложением, а пользовались им 10-20 человек для внутренних нужд.

Чуть не забыл, все репозитории оказались на nodejs с кучей разных фреймворков под капотом (loopback 2-3, koa, react и прочие радости жизни) и тремя версиями баз данных (MySQL 5, MySQL 8, mongodb). А мы... простые php-бэкэндеры. Конечно, не совсем простые, а даже достаточно опытные, но с таким зоопарком мы никогда не сталкивались.

Проблемы зоопарка

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

Вторая проблема появилась, когда мы попытались добавить свои SSH-ключи через панель управления хостингом. Заключалась она в том, что это требовало перезагрузки сервера. Честно сказать, я был удивлён. Но ещё больше меня удивило то, что это была холодная перезагрузка. Простое добавление ключей на сервер базы данных привело к тому, что VDS перезагрузился без правильного завершения работы. Докер от этого немножко прифигел удивился и на радостях потерял контейнер с базами MySQL. Правда, на помощь докеру подоспел ранчер, который развернул новый контейнер с пустой базой данных. А тут уже и loopback 3 стартовал. Его особенность состоит в том, что он каждый раз при запуске проверяет базу данных на соответствие своим моделям, и, если какой-то таблицы нет, то создаёт её. Понимаете? Он создал абсолютную пустую базу и сделал вид, что всё работает. Сотрудники пришли на работу, а форма логина им говорит, что логин-то ваш какой-то неправильный.

Третья проблема вылезла, когда мы кинулись искать бэкапы. Оказалось что скрипт, которые эти быкапы делает, использует имя контейнера, чтобы выполнять mysqdump. Но контейнеры-то создаёт ранчер, который на любой чих генерирует новый контейнер с уникальным именем. То есть бэкапы жили, пока жил конкретный контейнер. А при холодной перезагрузке, как мы уже выснили, контейнеры создавались заново.

Четвёртое, что нас порадовало, это необычайно "простая" схема выгрузки обновлений. Была такая цепочка: локальные тесты при коммите -> пуш в ветку на битбакете -> мёрдж в ветку для стэйджа -> ждёшь, пока прогонятся тесы, соберётся образ и запушится в докер-репозиторий -> разворачиваешь новый образ на стэйдже -> тестируешь стейдж -> мёрдж в релизную ветку -> ждёшь, пока прогонятся тесты, соберётся образ и запушится в докер-репозиторий -> разворачиваешь образ на продакшине. Это занимало час рабочего времени! Убитый впустую целый час. Не спорю, этот подход оправдан, когда проект большой, когда качество важнее времени. Но тут-то проект на 30 человек...

Чувствуете мощь новых технологий? Нет?! Поехали дальше.

Через какое-то время работы появилась ещё одна напасть - логи. Точнее, мы их не нашли. Вообще, логи в докере, это отдельная тема, но, если вкратце, то логи прияязаны к контейнеру. А при разворачивании новых контейнеров (например, при релизе) старые контейнеры удаляются... вместе с логами. Если вдруг спросят, а что же было месяц назад, мы честно ответим: "Кто ж его знает, логов-то нет." Кстати, именно поэтому в проект была добавлена кибана. Правда, в какой-то момент времени она решила, что с неё уже хватит, и радостно отвалилась. Когда, почему? Кто ж его знает, логов-то нет...

Вот, казалось бы, что ещё может пойти не так. Но у проекта осталась пара фокусов для самых искушённых зрителей. Начались необъяснимые проблемы с записями в базе данных MySQL. Они то пропадали, то появлялись, то опять терялись. Одна и та же запись могла быть в бэкапе за 10, 11, 12 число, потом исчезнуть, появиться 16-го и исчезнуть навсегда 18-го. Тут бы логи посмотреть, но вы помните, да? Логов нет, бэкапы через раз, кибана хранит молчание.

Вишенкой на торт во всём этом безобразии стал истёкший SSL-сертификат, на котором держался докер-репозиторий. Да, у сертификата просто кончился срок годности. Банальнее некуда, но в результате любые попытки что-то залить терпели неудачу, потому что собранный на битбакете образ не мог пролезть в докер-репозиторий через невалидный HTTPS.

Решение проблем

Первым делом, чтобы стабилизировать работу сервера, мы подключили swap. На основном сервере было 4 Гб памяти и 30 контейнеров. По ночам творились какие-то тёмные дела, памяти не хватало и к утру всё падало. Пришлось включить файл подкачки, потому что докупить оперативную память без смены сервера хостер не позволял. Файл подкачки на SSD - не самая лучшая идея, но других дисков там просто не было, пришлось включить так. И это сработало! Проект перестал рандомно падать.

Когда случилась неприятность с пропавшей базой данных, мы перенесли файлы баз из разделов докера (volume) в системные папки и подключили их через монтирование. Почему так не сделали сразу? Честно скажу, я не знаю.

Так же мы зареклись использовать чудесную панель управления хостинга для добавления SSH-ключей. Стали прописывать по старинке, через консоль. Благо, доступ у нас уже был.

Дампы мы стали делать по уникальному префиксу контейнера, который задавался в ранчере. Оказалось, что это делается довольно просто:

docker ps --format '{{.Names}}' | grep префикс_контейнера

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

Чем больше мы занимались проектом, тем больше склонялись к мысли, что надо полностью менять инфраструктуру: убирать ранчер, кибану, переносить базы данных из контейнеров в систему, а в докере оставить только nodejs. Разбирабираться в 50 контейнерах на 4 серверах, конечно, весело, но платят-то нам за другое. Помимо поддержки надо ещё заниматься и разработкой.

В итоге мы заказали VDS с 10 Гб оперативной памяти и 4х-ядерным процессором. Потом перенсли базы MySQL 5 в MySQL 8, которая была установлена в систему, отказались от ранчера, кибаны, платного битбакета и удалённого хранилища образов. Репозитории перенесли на бесплатный гитлаб, а образы решили собирать прямо на сервере. Для этого написали несколько скриптов, которые сами стягивают изменения, собирают контейнеры и переключают nginx.

Плюсы этого решения:

Как мы настроили VDS

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

Подключаем dummy:

modprobe dummy
echo dummy >> /etc/modules

## Для Debian 10 дополнительно надо указать количество интерфейсов
echo "options dummy numdummies=1" >> /etc/modprobe.d/zz_local.conf

Настраиваем сеть:

echo 10.98.0.15 realhost >> /etc/hosts
echo -e "auto dummy0\niface dummy0 inet static\n  address 10.98.0.15/32" >> /etc/network/interfaces.d/dummy0
reboot

Нужно обратить внимание, что после этого должен появиться фиктивный домен realhost:

% ping realhost
PING realhost (10.98.0.15) 56(84) bytes of data.
64 bytes from realhost (10.98.0.15): icmp_seq=1 ttl=64 time=0.044 ms
64 bytes from realhost (10.98.0.15): icmp_seq=2 ttl=64 time=0.058 ms
64 bytes from realhost (10.98.0.15): icmp_seq=3 ttl=64 time=0.058 ms
^C
--- realhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2032ms
rtt min/avg/max/mdev = 0.044/0.053/0.058/0.008 ms

Этот фиктивный домен мы использовали для того, чтобы настроить на нём базы данных MySQL и mongo:

% grep realhost /etc/mysql/mysql.conf.d/mysqld.cnf 
bind-address    = realhost
% grep realhost /etc/mongodb.conf 
bind_ip = 127.0.0.1,realhost

Ещё был вариант использовать интерфейс докера, чтобы там поднять сервисы. Но оказалось, что базы стартуют раньше, чем успевает подняться докер, и интерфейса, который нужен им, ещё нет. Разбираться с приоритетами сервисов я не захотел.

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

Посмотреть скрипты можно в репозитории по адресу:

https://github.com/anton-pribora/docker-example

Итого

Сейчас проект работает стабильно, данные не пропадают, новые релизы можно делать хоть каждые 10 минут. Логи, конечно, никто так и не стал дорабатывать, но и потребности в них не осталось.

Дополнительно мы развернули на том же сервере движок на PHP с прозрачной авторизацией, чтобы быстро делать разные отчёты и узкоспециализированные формы. И жизнь наладилась :)

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

PS. Когда мы перенесли проект на другой хостинг, обнаружился один контейнер, который был собран из образа, которого нет на Dockerhub'е. Исходников образа тоже нет. И теперь ни пересобрать образ, ни понять, как и зачем он был сделан, не представляется возможным. Этот "ёжик", увы, будет жить с нами.

Хорошая статья, мне понравилась. Оставлю отзыв!