20 мая 2017 г. 12:37
Написал soar

Полгода с Docker Swarm Mode в production

Disclaimer

Я не являюсь ни ярым фанатом Docker'а, ни его противником. Этой технологии нужно отдать должное, ведь так или иначе - она совершила революцию в IT. Пользуюсь ли я им? Определенно, даже мой блог давно крутится в Докере. Считаю ли я что его нужно использовать везде - нет.

Но Docker это уже давно не просто пакер для софта. И если к самому Докеру, как упаковщику приложений вопросов не так уж много (хотя что может быть ужаснее этих bash-oneline'еров в Dockerfiles?), то вот к инфраструктуре, которую вокруг него строят у меня вопросов миллион.

И именно о проблемах, связанных с одной из таких новомодных технологий пойдет речь. Мне пришлось использовать Docker Swarm Mode с момента его релиза в версии 1.12 (октябрь 2016-го), и на момент написания этой статьи я могу "похвастаться" тем, что уже полгода наш production работает на нём.

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

Замечу, что ниже приведены те, и только те проблемы, с которыми я столкнулся лично. Возможно у вас что-то пройдет лучше. Возможно, если вы только запускаете кластер - вы не столкнетесь с большинством из них. Я лишь описываю свой опыт и хочу предостеречь тех, кто хочет испытать новую технологию.

Те самые причины

Overlay network partitioning

Это проблема стала первой головной болью в работе над проектом. Я использовал самый первый релиз ветки 1.12 и был готов к тому, что с ним не всё будет гладко. Так и вышло. Проблема заключалась в том, что сервисы на одной ноде и в одной подсети видели друг друга отлично. Но попытки обратиться к другому сервису в той же подсети но на другой ноде в какой-то момент переставали быть успешными. Сервисы не видели друг друга и всё вставало. Единственное решение на тот момент: удалить все сервисы (потому что отключить сервис от сети было невозможно), удалить сеть, создать её заново и заново создать все сервисы. Не самая приятная и безболезненная процедура.

Нужно отдать должное, что проблему исправили примерно за 2 недели в одном из release candidates версии 1.13 и с тех пор в таком ужасном виде она не повторялась. Но...

Multiple overlay networks

  • Дата: 2016.12.20
  • Версии: 1.12.x - 17.03.x

Если вы используете Swarm Mode - вы используете overlay networks. Это его базис и без них использование технологии не имеет смысла. И, безусловно, когда я только начал работать с этим - я тут же разделил все тестовые окружения, все субпроекты и все микросервисы в отдельные подсети.

Каково же было моё удивление, когда балансировщик, торчащий одновременно в blue и green окружения, переставал видеть то одно из них, то другое. Но если периодически возникающую Gateway timeout на ранних стадиях проекта еще можно было как-то вытерпеть, то когда начал отваливаться контейнер базы данных, так же подключенный к двум overlay-сетям одновременно - я понял, что нужно что-то срочно менять. Чтобы решить эту проблему мне на 2 месяца пришлось забыть о безопасности и логическом разделении окружений и засунуть всё в одну overlay-сеть. В таком виде это и работало до релиза 17.03, где это вроде как исправили и на данный момент порядка 10 сетей у меня сосуществуют без проблем.

context deadline exceeded

Выглядит это так: любое действие над сервисом, нодой или любой другой сущностью приводит к ошибке Error response from daemon: rpc error: code = 4 desc = context deadline exceeded. По-сути управляющий сервис основного менеджера уходит в дедлок, а управляющий сервис резервного менеджера не может перехватить инициативу, т.к. ждет подтверждения от основного. Я потратил несколько часов на попытки вывести менеджеров из такого состояния, но они не увенчались успехом. В конце концов мне пришлось полностью пересоздать кластер с нуля и заново задеплоить все сервисы.

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

sandbox does not exist for container

С данной проблемой можно было столкнуться, когда по каким-либо причинам необходимо было удалить сервис и пересоздать его - например, чтобы подключить его к другой overlay-сети. Вы делаете docker service rm, потом - docker service create ..., и видите в логах следующее:

time="2017-04-12T09:22:42.470156578Z" level=error msg="fatal task error" error="task: non-zero exit (1)" module="node/agent/taskmanager" node.id=phdqmmtqot0fi2qkyxpppz0tl service.id=qfwfmtkeo9t9e58tstl94chqd task.id=wl22e7gs0lrarae904bn1kv7f 
time="2017-04-12T09:22:43.242218228Z" level=warning msg="failed to deactivate service binding for container green-mycoolapp.1.ofbvxy6w6zvawv1g92k4rvpxh" error="network sandbox does not exist for container green-mycoolapp.1.ofbvxy6w6zvawv1g92k4rvpxh" module="node/agent" node.id=phdqmmtqot0fi2qkyxpppz0tl 
time="2017-04-12T09:23:09.905115654Z" level=error msg="remove task failed" error="removal of container green-mycoolapp.1.y0dq252rvysa8bujus1xowhwe is already in progress" module="node/agent" node.id=phdqmmtqot0fi2qkyxpppz0tl task.id=y0dq252rvysa8bujus1xowhwe 
time="2017-04-12T09:23:09.906448299Z" level=error msg="error removing 0e89db687587e5379e453fe8b7049e467b042343443b8f7e28a4ec8f4b43a32f: No such container: 0e89db687587e5379e453fe8b7049e467b042343443b8f7e28a4ec8f4b43a32f" 

При этом сервис с другим именем создается без проблем.

forced certificate renewal

  • Дата: 2017.04.03
  • Версия: 17.03.0-ce .. 17.04.0-ce
  • Обсуждение: issues/31803

Допустим, у вас есть рабочий кластер на версии 17.03.0-ce и вы решили обновить его до 17.04.0-ce. Представьте, что вы обновили одну из manager-нод. И получили вот такую ошибку:

Apr 11 09:37:16 dmgr-01 dockerd[646]: time="2017-04-11T09:37:16.624218451Z" level=info msg="forced certificate renewal" module="node/tls" node.id=bdaa7r9e0p7uw3w4svbh0k12z node.role=swarm-manager
Apr 11 09:37:16 dmgr-01 dockerd[646]: time="2017-04-11T09:37:16.651542734Z" level=info msg="forced certificate renewal" module="node/tls" node.id=bdaa7r9e0p7uw3w4svbh0k12z node.role=swarm-manager
Apr 11 09:37:16 dmgr-01 dockerd[646]: time="2017-04-11T09:37:16.677872457Z" level=info msg="forced certificate renewal" module="node/tls" node.id=bdaa7r9e0p7uw3w4svbh0k12z node.role=swarm-manager

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

GELF problems

  • Дата: 2017.03.20
  • Версия: 17.03.0-ce
  • Обсуждение: issues/31942

Docker поддерживает различные варианты сбора логов. По-умолчанию используется json-file и если не забыть про его параметры max-size и max-file, то ни с какими проблемами вы не столкнётесь (по-умолчанию эти параметры не заданы и если логов много - у вас может внезапно закончиться место). Но ведь нам интересно собирать логи, аггрегировать и анализировать их? Для этого есть logging drivers, позволяющие отправлять строки куда-нибудь в syslog, gelf, fluentd и других форматах. Казалось бы, что может пойти не так?

К сожалению - может. Достаточно вашему, например, gelf-endpoint'у упасть и какое-то время не принимать сообщения, то dockerd начнёт складывать их в память до тех пор, пока не упадёт. А если вы пишете хотя бы 20-30 тысяч строк в минуту - это произойдет невероятно быстро. Что уж говорить о том, что если падает демон на мастер-ноде, это ведёт к leadership election, перестроению кластера и 502-ым ошибкам на десяток секунд.

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

Service Discovery Stuck

  • Дата: 2017.05.15
  • Версия: 17.04.0-ce
  • Обсуждение: issues/30134

Представьте, что вы поменяли конфигурацию вашего стека. Или даже просто передеплоили контейнер. Что может пойти не так?

root@balancer:/# echo > /dev/tcp/myservice/80
root@balancer:/# echo > /dev/tcp/myservice/80
bash: connect: Connection refused
bash: /dev/tcp/myservice/80: Connection refused
root@balancer:/# echo > /dev/tcp/myservice/80
root@balancer:/# echo > /dev/tcp/myservice/80
bash: connect: Connection refused
bash: /dev/tcp/myservice/80: Connection refused
root@balancer:/# echo > /dev/tcp/myservice/80
root@balancer:/# echo > /dev/tcp/myservice/80
bash: connect: Connection refused
bash: /dev/tcp/myservice/80: Connection refused

Что здесь происходит, спросите вы? Встроенный service-discovery сошёл с ума и балансирует запросы между новым контейнером и уже не существующим. Ни последующие обновления сервиса, ни даже его пересоздание - не помогают решить проблему. Опытным путем было выяснено, что помогает перезагрузка ноды, где был запущен проблеммный сервис. Ноды. Целиком.

Pulling from private registries

  • Дата: 2017.05.18
  • Версия: 17.03.0-ce .. 17.05.0-ce
  • Обсуждение: issues/31534

С самого начала использования Docker в Swarm Mode мы использовали сервисы с имиджами из приватных репозиториев, а вернее - прямо с Docker.io. Да, примерно в 13-ой версии был баг, из-за которого кроме передачи параметра --with-registry-auth необходимо было пройти по всем нодам в кластере и сделать docker login, однако после этих манипуляций всё работало и ничто не предвещало беды.

Однако когда мы решили попробовать новый функционал, а вернее docker stack deploy - мы столкнулись с тем, что он категорически не работает с приватными репозиториями и не скачивает имиджи. Я перепробовал все возможные варианты, проверил что на всех нодах сделан docker login, пробовал с ключом --with-registry-auth и без него - никаких успехов я не достиг.

root@dmgr-01:~# docker stack ps --no-trunc mycoolapp 
ID                          NAME                         IMAGE                                                                                  NODE                DESIRED STATE       CURRENT STATE                     ERROR                                                                                          PORTS
kf9wjfgib16whpgdksr15ftnk   mycoolapp_mycoolapp2.1       registry.hub.docker.com/alekseysmyrnov/dockercloud-quickstart-python                   node1               Ready               Assigned less than a second ago                                                                                                  
v6xocjb0tobg8xlum50pphcit    \_ mycoolapp_mycoolapp2.1   registry.hub.docker.com/alekseysmyrnov/dockercloud-quickstart-python                   node1               Shutdown            Rejected less than a second ago   "No such image: registry.hub.docker.com/alekseysmyrnov/dockercloud-quickstart-python:latest"

В логах же только информация об отсутствии доступа:

May 31 10:49:13 node1 dockerd[21939]: time="2017-05-31T10:49:13.318212282+02:00" level=error msg="Not continuing with pull after error: errors:\ndenied: requested access to the resource is denied\nunauthorized: authentication required\n"
May 31 10:49:13 node1 dockerd[21939]: time="2017-05-31T10:49:13.318267734+02:00" level=info msg="Ignoring extra error returned from registry: unauthorized: authentication required"
May 31 10:49:13 node1 dockerd[21939]: time="2017-05-31T10:49:13.318322422+02:00" level=info msg="Translating \"denied: requested access to the resource is denied\" to \"repository registry.hub.docker.com/alekseysmyrnov/dockercloud-quickstart-python not found: does not exist or no pull access\""
May 31 10:49:13 node1 dockerd[21939]: time="2017-05-31T10:49:13.318379105+02:00" level=error msg="pulling image failed" error="repository registry.hub.docker.com/alekseysmyrnov/dockercloud-quickstart-python not found: does not exist or no pull access" module="node/agent/taskmanager" node.id=mbq71ibxlna3ptqgfaovpmq5d service.id=xcn9h60zlr7b83z0otpt2ju0t task.id=pva4ak3tnclu3a3rnlh7akbvv
May 31 10:49:13 node1 dockerd[21939]: time="2017-05-31T10:49:13.319201194+02:00" level=error msg="fatal task error" error="No such image: registry.hub.docker.com/alekseysmyrnov/dockercloud-quickstart-python:latest" module="node/agent/taskmanager" node.id=mbq71ibxlna3ptqgfaovpmq5d service.id=xcn9h60zlr7b83z0otpt2ju0t task.id=pva4ak3tnclu3a3rnlh7akbvv

Нужно заметить, что этот же самый имидж успешно запускается в том же сворме на той же ноде через docker service create.

Swarm unstable under (heavy) load

Тут мне даже нечего добавить к обсуждению на GitHub, я могу лишь подтвердить, что чем выше у вас будет нагрузка - тем чаще вы будете встречаться с тем, что иногда вам в ответ прилетает 50x. Даже при размещении всех сервисов на одной ноде (я специально экспериментировал) между сервисами может произойти Request timeout, Hostname not found или еще какое-нибудь безобразие.

Port publishing

  • Дата: 2017.05.30
  • Версия: 17.05.0-ce
  • Обсуждение: issues/33430

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

Выглядит это так:
- У вас был сервис, слушающий определенный порт на всех нодах (ingress режим)
- Вы обновляете ваш swarm
- У вас больше нет сервиса, слушающего этот порт

Причем не помогает ни удаление сервиса и его запуск заново (с теми же параметрами), ни перезагрузка нод, ни даже цикличный перезапуск нод управляющих (да, и такое я делал от безысходности). Есть мнение, что причина связана с тем, что где-то под капотом движок запомнил неправильные значения, связанные с сетью. Хотя в логах нет никаких ошибок. Более того, в режиме прослушивания порта на одном хосте (в режиме docker-proxy) всё отлично работает.

Решение: удалить старый сервис и запустить новый с параметрами docker service create ... --mode global --publish mode=host,target=X,published=Y,protocol=Z. Да, теперь у вас N запущенных тасков вместо одного и вы больше не используете одну из фич, ради которой вообще взялись за Docker Swarm Mode.

Strange failover behavior

Допустим, ваша инфраструктура развернута на DigitalOcean. И сегодня вы испытываете проблемы с сетью (у DigitalOcean так бывает), такие, что допустим теряется 10% пакетов. Что будет делать оркестратор? Правильно, выведет ноду из кластера:

Jun  1 13:35:21 dmgr-01 dockerd[655]: time="2017-06-01T13:35:21.043977730Z" level=info msg="me
mberlist: Suspect dwrk-webapp-green-01-17fb0e89123c has failed, no acks received"
Jun  1 13:35:23 dmgr-01 dockerd[655]: time="2017-06-01T13:35:23.112903450Z" level=info msg="me
mberlist: Marking dwrk-webapp-green-01-17fb0e89123c as failed, suspect timeout reached"
Jun  1 13:35:27 dmgr-01 dockerd[655]: time="2017-06-01T13:35:27.058392487Z" level=warning msg=
"memberlist: Refuting a suspect message (from: dmgr-01-0adf16014854)"

Сразу возникает первый вопрос: почему 90% пакетов между нодами ходит, но нода уже выведена из кластера? Непонятно. Более того, этот порог никак не настраивается и менеджер даже не пытается связаться с воркером еще раз. Он выводит ноду из кластера, overlay-сеть становится недоступна и балансировщик перестает видеть сервисы запущенные на этой ноде.

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

Я вижу тут две логические проблемы:
- Если у вас volumes поверх glusterfs, например, то сервис продолжит писать на этот volume. Ловите split-brain или какие угодно неожиданные результаты.
- Если сервисы не были убиты, когда нода была выведена из кластера, то зачем этот рестарт по возвращению? От чего он может спасти?

Ответов у меня, к сожалению, нет.

manifest verification failed

  • Дата: 2017.06.05
  • Версия: 17.05.0-ce

Допустим у вас есть Swarm из ~10 нод. Вы добавляете еще 3. Какого результата вы ожидаете? Логично предположить, что все глобальные сервисы автоматически поднимутся на новых нодах (при условии правильных constraints конечно). Хорошее предположение, но оно конечно не верно:

Jun  5 15:20:17 node5 dockerd[26154]: time="2017-06-05T15:20:17.232615299+02:00" level=error msg="manifest verification failed for digest sha256:130fcb4909b77a8ceb3f46c09ddb0a74901e8a3e3190ea2fcfabaa520d8daa22"
Jun  5 15:20:17 node5 dockerd[26154]: time="2017-06-05T15:20:17.232718092+02:00" level=info msg="Attempting next endpoint for pull after error: manifest verification failed for digest sha256:130fcb4909b77a8ceb3f46c09ddb0a74901e8a3e3190ea2fcfabaa520d8daa22"

Судя по всему - это еще одна проблема связанная с private registries и опцией --with-registry-auth. Нужно делать docker service rm ... + docker service create ....

Выводы

На момент написания данной статьи проект имеет больше чем 2500 issues только в главном репозитории и это количество постоянно растёт. Несмотря на огромное коммьюнити и на то, что вам даже ответят, выглядит это так, будто у проекта просто не хватает сил на исправления и доработки всего, что было в нём намечено. Мне кажется что это в первую очередь связано с тем, что если Docker как упаковщик ПО интересен всем, то Docker Swarm Mode как оркестратор не так уж необходим рынку. Тем более, когда есть более крупные, надежные, а главное - production-ready игроки. Особенно странно выглядит всё вышесказанное учитывая, что Docker идёт по пути разделения Community Edition и Enterprise Edition - мне кажется что для этого они еще не готовы.

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

Что делать?

Если вам всё же приходится использовать Docker Swarm Mode по каким-либо причинам (требование заказчика, необходимость ультра-быстрого старта или вы просто не хотите разбираться с другими решениями), вот несколько советов:

  • Придерживайтесь KISS. Где можно не делать несколько сетей - не делайте. Где можно использовать одну ноду вместо десяти - используйте.
  • Забудьте про встроенный балансировщик, он не работает так как вам нужно.
  • Держите базу данных как можно ближе к сервисам её использующим. А еще лучше не держите базу в Docker.
  • Если у вас CI/CD - первым делом настройте уборку мусора, место на дисках закончится в самый неподходящий момент.
  • И еще раз посмотрите в сторону других решений.

PS: и да, не используйте DigitalOcean для более-менее серьезных проектов. Или по-крайней мере не надейтесь на надежную связность между виртуальными серверами.

Комментарии