Немного контекста
Эта статья — практический кейс применения Docker в робототехнике, а не туториал. Для того, чтобы понять все что далее написано, нужно на среднем уровне владеть Docker и иметь небольшое понимание принципов работы ROS.
Материал для статьи родился в результате попадания профессионального Android-разработчика с опытом в бэкенде (меня) в робототехническую команду. На протяжении полугода мы готовили двух роботов к соревнованиям Eurobot 2023. Так вышло, что мне пришлось взять на себя роль Core-разработчика и решать инфраструктурные проблемы. В результате я пришел к очень интересным решениям, которыми наконец-то могу поделиться.
Если вы не знакомы со спецификой соревнований Eurobot, то конечно лучше сначала посмотреть официальный YouTube канал. Но если объяснять коротко, то Eurobot — это робототехнические соревнования, в которых на игровом поле автономные роботы двух команд одновременно выполняют задания и набирают очки. Крутость соревнований в том, что нельзя надрочить вылизать идеальную конструкцию робота, которая бы позволяла побеждать из года в год. Каждый год поле, игровые элементы на нем и правила игры меняются. После публикации новых правил в сентябре команды начинают готовиться к соревнованиям.
С сентября 2022 года по май 2023 я был частью команды RESET из Сколтеха. И мы готовились к Eurobot 2023.
Предпосылки перехода на Docker
До 2022 года в команде RESET не использовали Docker для разработки. Способы программирования роботов напоминали веб-разработку 10-летней давности, когда еще мало кто использовал контейнеризацию и все накатывали софт прямо на операционную систему. Поэтому совершенный нами переход я считаю большим шагом вперед.
Зачем нужен ROS
На протяжении всего своего существования в команде RESET разрабатывали софт для роботов при помощи ROS. Это удобно, из-за того что ROS позволяет быстро прототипировать функционал, неплохое решение для соревновательной робототехники.
Я не буду полностью расписывать принципы работы ROS, для этого есть официальные туториалы, но напомню концепты, важные для текущей статьи. Одна из главных фишек, которую дает ROS — это модульность. Вы можете вести разработку подсистем робота в отдельных проектах/репозиториях, устанавливать чужие пакеты, а потом заставить все это работать вместе. Например, системы навигации, управления приводами и принятия решений могут быть разными приложениями, запускаемыми по-отдельности. ROS предоставляет готовый интерфейс для обмена данными между этими приложениями.
Пакеты
Самая крупная "программная единица" в ROS, это пакет (package). Пакет — это один проект на С++ или Python, с файлом package.xml
в корне.
Ноды
Каждый пакет в ROS может содержать в себе ограниченное здравым смыслом количество нод (node). Ноды — это программные сущности, при помощи которых разработчики взаимодействуют со всеми сущностями ROS. В библиотеках C++ и Python ноды это просто классы, от которых надо наследоваться, чтобы создать свои собственные ноды. Подробнее про ноды в официальном туториале.
Топики
Обмен данными между всеми нодами происходит при помощи топиков (topics). Топик — это шина со своим именем и типом данных. Ноды могут публиковать данные в топики, и наоборот, прослушивать топики ожидая данных. Подробнее про топики в официальном туториале.
Резюмируем
ROS, это набор готовых инструментов и библиотек для быстрого прототипирования, позволяющий склеивать не всегда успешно в единую систему множество разнородных пакетов.
Опыт участников предыдущих лет
Опыт команды RESET таков, что из примерно десяти человек, четверо занимаются написанием кода для роботов. Есть еще механики и электронщики, но статья не про них. В команде 4 программиста потому что есть 4 основных направления разработки:
- Behavior Tree (BT): стратегия, принятие решений на поле. Это направление отдали мне.
- Локализация: работа с лидаром и одометрией.
- Навигация: обработка данных с локализации, передвижение робота по полю.
- Компьютерное зрение.
До 2022 года каждый программист команды RESET писал свои ROS пакеты в отдельном репозитории. Все репозитории хранились на GitLab и клонировались на роботов. На роботах использовались *.sh
скрипты, при помощи которых происходила компиляция и синхронный запуск всех пакетов.
Ребята активно использовали этот подход и сталкивались со следующими проблемами:
Конфликты зависимостей
Версии библиотек в одних ROS пакетах могли конфликтовать с версиями библиотек из других. Ничто не мешало сломать чужой код, даже не меняя его. Достаточно было просто обновить какую-нибудь библиотеку.
Невоспроизводимость (irreproducibility)
Никто не может гарантировать что после переустановки системы тот же самый набор пакетов будет работать как раньше.
Время билда
Код на C++ отдельно билдится на компах участников, а после попадания на робота этот же код билдится заново уже на роботе. Роботом в момент компиляции пользоваться нельзя.
Триггер изменений
В 2022 году один из наставников предложил изменить подход к разработке и попробовать запускать код на роботе в Docker контейнерах. В тот год в команду RESET как раз пришел айтишник (это я).
Ребята, я в этом шарю
Я уже не помню, почему изначально наставник предлагал перейти на Docker. Это уже не так важно. Важно, что практика деплоя с Docker внедрена в айтишке повсеместно. Вы не найдете сейчас ни одного бэкендера, который бы не упаковывал свои приложения в образы.
Образы это круто:
- Вы вообще исключаете проблемы с воспроизводимостью. Если упакованное приложение работает у вас на компьютере, то и на другой машине оно заработает.
- Вы не паритесь даже по поводу ОС, на которой будет запущено ваше приложение. Для запуска упакованного приложения нужен только установленный Docker.
- Приложения легко доставлять до конечной машины, на которой оно будет запущено.
По факту, любой робот, это тот же самый бэкенд, только находящийся физически рядом. Так почему бы не применить практики деплоя на бэкенд в робототехнике?
В общем, так мы и начали путешествие от сохи к ракете.
Переходим на Docker
Структура репозитория
Прежде всего, мы перенесли все 4 направления разработки в монорепозий. Это не было обязательно, просто в одном репозитории мне было проще организовать процесс сборки и тестирования кода.
Примерная структура репозитория получилась следующей:
bt/ # behavior tree
├─ src/
│ ├─ ros_package_1/
│ ├─ ros_package_2/
├─ Dockerfile
communication/ # для связи с STM32
├─ src/
│ ├─ ...
├─ Dockerfile
localization/ # локализация
├─ src/
│ ├─ ...
├─ Dockerfile
navigation/ # навигация
├─ src/
│ ├─ ...
├─ Dockerfile
common/ # общий код
├─ src/
│ ├─ ...
Были созданы директории под каждое направление разработки. Назвали мы эти директории воркспейсами (workspace). Название выбрано не случайно, каждая директория это Colcon Workspace. Это значит, что:
- В
<workspace_name>/src/
можно класть пакеты ROS. - Находясь в директории
<workspace_name>/
можно билдить все пакеты при помощиcolcon build
.
Сборка кода воркспейсов во время разработки
Без Docker в целом все обыденно: из под Ubuntu с установленным ROS2 Humble, клонируете себе репозиторий, идете в свой воркспейс и открываете в VS Code те пакеты, с которыми сейчас работаете.
Если ваш воркспейс зависит от пакетов из common/
, нужно сначала сбилдить воркспейс common/
.
Dockerfile для воркспейса
Из каждого воркспейса будет собираться один Docker образ, для этого в корне каждого воркспейса есть Dockerfile
. Структура этого файла во всех воркспейсах одинаковая и состоит из пяти блоков:
1 # Блок 1: Наследуемся от готового ROS образа версии Humble
2 FROM ros:humble-ros-core
3
4 # Блок 2: Общие для всех воркспейсов внешние зависимости
5 RUN apt update && apt install -y ...
6
7 # Блок 3: Специфичные для воркспейса внешние зависимости
8 RUN apt update && apt install -y ...
9 RUN pip3 install setuptools black pytest pygame
10
11 # Блок 4: Расшаривание общего кода между воркспейсами
12 COPY common/src/eurobot_interfaces /root/dependencies/eurobot_interfaces
13 COPY sh/ci/deps.sh /tmp/.deploy_cache/deps.sh
14 RUN bash /tmp/.deploy_cache/deps.sh
15
16 # Блок 5: Сборка пакетов воркспейса
17 ENV EUROBOT_KIND=bt
18 COPY $EUROBOT_KIND/src /tmp/.deploy_cache/src
19 COPY sh/ci/deploy.sh /tmp/.deploy_cache/deploy.sh
20 ARG DEPLOY
21 RUN bash /tmp/.deploy_cache/deploy.sh
В наших докерфайлах слои расположены в порядке возрастания по частоте их редактирования, потому что если меняются нижние слои, то будут пересобраны все слои, находящиеся выше. Самые последние слои редактируются максимально часто, но они настолько легковесные, что это не занимает много времени.
Блок 1: Фиксируем версию базового образа
Для простоты и экономии места я написал, что мы наследуемся от образа ros:humble-ros-core
, но это только наполовину правда. В ходе работы мы заметили что этот образ часто получал обновления, что приводило к полной пересборке наших образов с нуля. Поэтому мы зафиксировали версию образа вот так:
## `ros:humble-ros-core` from Dec 9, 2022: https://github.com/docker-library/repo-info/commit/f150644a260b5a28
FROM ros@sha256:23aa104a31990bb6952f2836cbf431535ae53490d587a70b32e0ed94a9a4fd83
Это плохая практика при написании приложений для прода, но у нас тут соревновательная робототехника, так что пiхуй.
Блок 2: Общие для всех воркспейсов внешние зависимости
Следующим слоем после FROM
идет установка пакетов из apt
:
# Блок 2: Общие для всех воркспейсов внешние зависимости
RUN apt update && apt install -y \
git \
cmake \
build-essential \
python3-pip \
python3-colcon-common-extensions \
ros-$ROS_DISTRO-rmw-cyclonedds-cpp
Мы просто копипастили это во все докерфайлы. Хотя вполне можно было сделать свой базовый образ, зафиксировав так версию ros:humble-ros-core
и первый слой с общими зависимостями. Я просто вовремя не додумался до этого.
Одинаковые слои это такая "оптимизация" — если слой общий у всех докерфайлов, то он будет собираться только один раз и переиспользоваться для всех воркспейсов. Docker использует ранее собранные слои, что значительно ускоряет процесс сборки. В этот слой я поместил самые тяжелые зависимости: инструменты для сборки кода и кастомный DDS.
Блок 3: Специфичные для воркспейса внешние зависимости
Следующие два слоя, это установка пакетов из apt
и pip
. Вынес pip
в отдельный слой, потому что pip
часто выдавал ошибки и руинил весь билд.
# Блок 3: Специфичные для воркспейса внешние зависимости
RUN apt update && apt install -y \
ros-$ROS_DISTRO-behaviortree-cpp-v3 \
python3-tk \
&& apt autoremove && \
# colored prompt
sed -i 's/#force_color_prompt=yes/force_color_prompt=yes/g' ~/.bashrc && \
# ROS source
echo "source /opt/ros/$ROS_DISTRO/setup.bash" >> ~/.bashrc
RUN pip3 install setuptools black pytest pygame
Также в слой с установкой я поместил запись source /opt/ros/$ROS_DISTRO/setup.bash
в файл .bachrc
, для того чтобы при открытии bash внутри контейнера у нас сразу регистрировалось окружение, нужное для запуска бинарей ros.
Блок 4: Расшаривание общего кода между воркспейсами
В директории common/
нет своего Dockerfile
потому что мы не собирали из него отдельный образ. Вместо этого мы хранили в нем общие на весь проект пакеты ROS. В докерфайлах других воркспейсов мы просто копировали нужные пакеты в /root/dependencies
образа. В примере ниже копируется только пакет eurobot_interfaces
:
# Блок 4: Расшаривание общего кода между воркспейсами
COPY common/src/eurobot_interfaces /root/dependencies/eurobot_interfaces
COPY sh/ci/deps.sh /tmp/.deploy_cache/deps.sh
RUN bash /tmp/.deploy_cache/deps.sh
После копирования запускается скрипт deps.sh
, который устанавливает пакеты внутри образа:
#!/bin/bash
source /opt/ros/$ROS_DISTRO/setup.bash || exit 1
# build dependencies if needed
if [ -d /root/dependencies ]; then
cd /root/dependencies
colcon build --symlink-install || exit 1
echo "source /root/dependencies/install/setup.bash" >>~/.bashrc
fi
Логика у скрипта простая: если есть директория /root/dependencies
, запускаем в ней colcon build
и регистрируем результаты сборки в окружении при помощи source
.
Блок 5: Сборка пакетов воркспейса
Последними слоями в наших образах являются результаты сборки уже наших собственных, специфичных для воркспейса пакетов. Переменная окружения EUROBOT_KIND
с названием воркспейса используется дальше в скриптах.
# Блок 5: Сборка пакетов воркспейса
ENV EUROBOT_KIND=bt
COPY $EUROBOT_KIND/src /tmp/.deploy_cache/src
COPY sh/ci/deploy.sh /tmp/.deploy_cache/deploy.sh
ARG DEPLOY
RUN bash /tmp/.deploy_cache/deploy.sh
В образ копируется содержимое воркспейса и запускается скрипт deploy.sh
, который собирает и устанавливает наши пакеты:
#!/bin/bash
PROJECT_DIR="/root/eurobot_main"
PACKAGE_DIR=$PROJECT_DIR/$EUROBOT_KIND
function cleanup() {
rm -rf /tmp/.deploy_cache &>/dev/null
}
trap cleanup EXIT
case "$DEPLOY" in
"ci")
echo "Deploying package in the cloud... "
git clone \
--branch master \
--depth 1 \
https://...@gitlab.com/reset_eurobot2023/eurobot_main.git \
"$PROJECT_DIR" || exit 1
;;
"local")
echo "Deploying package locally... "
[ -d $PACKAGE_DIR/src ] || mkdir -p $PACKAGE_DIR/src
cp -R /tmp/.deploy_cache/src $PACKAGE_DIR || exit 1
;;
*)
echo "Skip deploying package"
exit 0
;;
esac
source /opt/ros/$ROS_DISTRO/setup.bash || exit 1
[ -f /root/dependencies/install/setup.bash ] && source /root/dependencies/install/setup.bash
cd $PACKAGE_DIR
colcon build --symlink-install || exit 1
echo "source $PACKAGE_DIR/install/setup.bash" >>~/.bashrc
echo "Package deployed!"
У этого скрипта логика сложнее, чем у предыдущего. Здесь, в зависимости от переданного в docker аргумента --build-arg DEPLOY=...
будет разное поведение:
Если
DEPLOY
равен"ci"
, то скрипт загрузит последнюю версию кода из репозитория и соберет ее. Этот функционал использовался для сборки "релизных" образов для публикации в наш Docker Registry.Если
DEPLOY
равен"local"
, то скрипт брал тот код воркспейса, который был скопирован в него изначально, и собирал его. При локальных сборках образа использовался именно этот способ.Для всех других значений переменной
DEPLOY
никакой код не собирался. Эта опция использовалась для того чтобы подготовить образ, который можно использовать как VS Code Dev Container.
Превращаем воркспейс в Docker образ
Мы пользовались мета-системой сборки bldr, которые позволяла собирать образы для любых воркспейсов одной командой. Команды запускались из корня репозитория примерно вот так:
./bldr build navigation
Про сам bldr расскажу позднее. При вызове команды выше под капотом запускался примерно такой код:
docker buildx build \
--platform "linux/amd64" \
--output "type=docker" \
--tag "eurobot2023:navigation" \
--build-arg DEPLOY="local" \
--file navigation/Dockerfile \
.
Использовался Docker BuildKit, потому что мы билдили и на ARM, и на x86 компьютераз. Docker позволяет собирать образы под любую архитектуру. В качестве контекста сборки использовался корень репозитория (.
), чтобы можно было копировать пакеты из common/
воркспейса.
Если у вас больше одного ROS пакета в воркспейсе и они написаны на C++, придется смириться с тем фактом, что все они будут пересобираться во время каждого билда, даже если изменения вы внесли всего в один. Да, при таком подходе билд-кэш для С++ завести нельзя, но нам до дня соревнований хватало и этого.
Отправка образов на роботов
Иногда мы использовали Docker Hub для публикации и скачивания наших образов, но в 99% случаев деплоили по локальной сети, избегая публикации в интернете.
К сожалению, для того чтобы рассказать, как я смог этого добиться, нужно такая же по объему статья. Поэтому просто скажу что работало это как черная магия: без вмешательства разработчиков, супер быстро и требовало ввода всего одной команды. Все были довольны. Готовлю статью на эту тему.
Запуск контейнеров на роботе
Классический запуск контейнера происходит через вызов docker run
. Мы успешно пропустили этот этап и перешли на Docker Compose, потому что я с самого начала знал, что нужно будет:
- Запускать несколько контейнеров.
- Соблюсти порядок запуска контейнеров.
- Передавать кучу параметров в каждый контейнер, а делать это вручную нереально.
Чтобы проиллюстрировать третью причину, представляю вашему вниманию запуск контейнера с behavior tree на борту со всеми параметрами:
docker run \
-e ROBOT_NAME=${ROBOT_NAME} \
-e COMPANION_ROBOT_NAME=${COMPANION_ROBOT_NAME} \
-e CYCLONEDDS_URI="<CycloneDDS><Domain><General><NetworkInterfaceAddress>${WLAN_DEVICE_NAME}</NetworkInterfaceAddress></General></Domain></CycloneDDS>" \
-e BT_FILENAME=${BT_FILENAME} \
-e ENEMIES=${ENEMIES} \
-e BT_FREQ=${BT_FREQ} \
-e PLATE_NUMBER=${PLATE_NUMBER} \
-e SIX_BEACONS=${SIX_BEACONS} \
-e STOLEN_CAKE_COST=${STOLEN_CAKE_COST} \
-e DEBUG=true \
-e SORT_CAKES=${SORT_CAKES} \
-e COLLECT_CHERRIES=${COLLECT_CHERRIES} \
-e STEAL_CAKE=${STEAL_CAKE} \
--network host \
--privileged \
--name bt \
-v ~/eurobot_main/strategies:/strategies \
-v /etc:/etc \
eurobot2023:bt \
/bin/bash -c "
source /etc/eurobot_config.sh
source /root/eurobot_main/bt/install/setup.bash
ros2 launch superbt bt_node.launch.py
"
Мы в итоге запускали 6 контейнеров, так что запуск руками или через sh скрипты нам не не подходит.
Сразу на новый уровень: Docker Compose
Для того чтоб удовлетворить все требования под предыдущим заголовком, я решил использовать Docker Compose — самый простой инструмент, который был доступен на момент подготовки к соревнованиям.
У меня уже был опыт работы с ним во время разработки бэкенда для MpeiX, поэтому взял именно его. А на роботах больше и не нужно.
Пишем docker-compose.yaml
Вся конфигурация наших воркспейсов и вся инфа о порядке запуска описывается в единственном файле: docker-compose.yaml
.
Все свои ROS ноды мы писали так, чтобы их можно было настраивать через переменные окружения, поэтому переменных окружения у нас использовалось много:
version: '3'
services:
stm: ...
launcher: ...
bt:
image: eurobot2023:bt
container_name: "bt"
privileged: true
network_mode: host
environment:
- ROBOT_NAME=${ROBOT_NAME}
- COMPANION_ROBOT_NAME=${COMPANION_ROBOT_NAME}
- CYCLONEDDS_URI=<CycloneDDS><Domain><General><NetworkInterfaceAddress>${WLAN_DEVICE_NAME}</NetworkInterfaceAddress></General></Domain></CycloneDDS>
# BtNode Settings
- BT_FILENAME=${BT_FILENAME}
- ENEMIES=${ENEMIES}
- BT_FREQ=${BT_FREQ}
- PLATE_NUMBER=${PLATE_NUMBER}
- SIX_BEACONS=${SIX_BEACONS}
- STOLEN_CAKE_COST=${STOLEN_CAKE_COST}
- DEBUG=true
# BT feature toggles
- SORT_CAKES=${SORT_CAKES}
- COLLECT_CHERRIES=${COLLECT_CHERRIES}
- STEAL_CAKE=${STEAL_CAKE}
command: |
/bin/bash -c "
source /etc/eurobot_config.sh
source /root/eurobot_main/bt/install/setup.bash
ros2 launch superbt bt_node.launch.py
"
volumes:
- ~/eurobot_main/strategies:/strategies
- /etc:/etc
localization: ...
navigation: ...
display: ...
Для приложений обязательно указывать network_mode: host
, чтобы за пределами контейнеров работало обнаружение топиков и других сетевых сущностей. Опцию privileged: true
указывать не обязательно, она нужна только для контейнера stm
для обмена данными с микроконтроллером через USB-UART.
Запускаем все контейнеры
Чтобы запустить все контейнеры и подключиться к их выводу, достаточно запустить:
docker compose up
Дальше работа со всеми ROS приложениями происходит точно так же, как и с любыми приложениями, упакованными в Docker. Поэтому все программисты освоили основные команды для работы с Docker Compose и спокойно запускали и останавливали роботов самостоятельно.
Использование .env
файла
В каждый контейнер передается целая куча параметров через переменные окружения. Сначала мы прописывали их руками прямо в docker-compose.yaml
, но потом начали испрользовать .env
файл. В итоге мы вынесли все значимые параметры для запуска роботов в .env
:
#commons
PLATE_NUMBER=3
SIX_BEACONS=false
# behavior tree
BT_FILENAME="no_brown_for_all.xml"
ENEMIES="2 4"
STOLEN_CAKE_COST=10
BT_FREQ=10
SORT_CAKES=true
COLLECT_CHERRIES=true
STEAL_CAKE=true
# navigation
CAKE_RADIUS=6.0
ROBOT_RADIUS=18.0
ROBOT_RADIUS_WITH_CAKE=20.0
ENEMY_RADIUS=24.0
SAFE_DISTANCE_ROBOTS=4.0
SAFE_DISTANCE_OBJECTS=2.0
MAX_LINEAR_VELO=0.8
MAX_LINEAR_VELO_CV_ONLY=0.5
MAX_ANGULAR_VELO=2.1
MOCK_CAKES=false
# localization
PARTICLE_COUNT=10000
Согласитесь, выглядит приятнее, чем простыня в docker-compose.yaml
.
Подводные камни
Нет готовых инструментов для разработчиков
Docker это супер крутая штука, когда ты умеешь им пользоваться. Но наша команда почти целиком состояла из людей, никогда не работавших с Docker, а в ROS все разбирались на начальном уровне.
Я потратил много недель чтобы выкатить решение, скрывающее большинство подробностей работы с Docker за простым интерфейсом. И еще несколько месяцев я его полировал опираясь на фидбек от команды. В результате этой работы родилась мета-билд система bldr, заточенная под билд и деплой докеризованных ROS приложений. Она была выстрадана ради того чтоб мы могли билдить образ любого воркспейса вот так:
./bldr build navigation
Или устанавливать образ любого воркспейса на робота вот так:
./bldr install localization
В 2022 году команде повезло, что нашелся айтишник который это реализовал. Не факт что в следующем году повезет так же. К сожалению в интернете готовых решений нет, а сложность текущего решения очень высока. Для того чтобы наш опыт можно было переиспользовать, я полагаю, лучше не создавать yet another build system, а написать расширения для системы сборки Bazel.
Дефолтный DDS не может пробиться через Docker
Либо мы оказались недостаточно умными, чтобы его настроить.
Мы очень много намучились с настройкой DDS в ROS. Если объяснять коротко, то DDS это такой middleware, благодаря которому осуществляется общение через топики между нодами. Даже между нодами находящимися на разных машинах в одной локальной сети.
Дефолтная реализация DDS, eProsima Fast DDS, включенная в редакцию ROS2 Humble, отказалась работать в среде Docker контейнеров. Мы регулярно сталкивались с тем что топики, публикуемые нашими нодами, были недоступны извне, за пределами Docker контейнеров. Они не были доступны даже внутри сети Docker.
Путем долгих ковыряний и настроек мы смогли добиться того что топики худо-бедно работали между контейнерами и на локальной машине (роботе) за пределами контейнеров, но в локальной сети Wi-Fi другие машины не видели их. Это большая проблема так как у нас стратегия игры была завязана на возможности обмена данными между роботами.
Итоговым решением, на котором мы остановились, стал переход на Eclipse Cyclone DDS. Судя по бенчмаркам он не самый быстрый, но это имеет значение только когда пересылаешь данные больших объемов. У нас в команде не было людей пересылавших гигабайты данных через топики, а если бы и были, я бы дал им по рукам.
Мы обнаружили, что при использовании Cyclone DDS могут общаться между собой ноды из ROS2 разной редакции. Так, например, наша вышка с камерой работала на ROS2 Foxy и отправляла данные нашим роботам, работающим на ROS2 Humble.
Переход на Cyclone DDS отразился в Dockerfile
и docker-compose.yaml
. Мы добавили в зависимости всех воркспейсов пакет ros-humble-rmw-cyclonedds-cpp
. А также указали переменные окружения для ROS.
Дополнения для Dockerfile
:
# Общие для всех воркспейсов внешние зависимости
RUN apt update && apt install -y \
... \
ros-$ROS_DISTRO-rmw-cyclonedds-cpp # <-- устанавливаем другой DDS
# Меняем дефолтную реализацию DDS
ENV ROS_DOMAIN_ID=0
ENV RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
В docker-compose.yaml
мы также передавали для Cyclone DDS параметр NetworkInterfaceAddress
с именем WLAN интерфейса:
environment:
- CYCLONEDDS_URI=<CycloneDDS><Domain><General><NetworkInterfaceAddress>${WLAN_DEVICE_NAME}</NetworkInterfaceAddress></General></Domain></CycloneDDS>
Забудьте про легковесные образы
В прекрасном мире JVM и Rust я познакомился с базовыми образами от Alpine. Можно спокойно упаковать ваш микросервис в образ, который будет весить 20 мегабайт (5 мегабайт в сжатом виде).
Ну так вот, с ROS про это пока можно забыть. Базовый образ ROS без всего лишнего в распакованном виде занимает примерно 700 мегабайт памяти на диске. Мы в свою очередь докинули еще пакеты типа build-essential
и кастомного DDS, что добавило еще 300 мегабайт. Спасал только тот факт, что Docker образы состоят из слоев и нам не надо было каждый раз во время деплоя грузить гиг мусора на роботов. Мы загружали только верхние слои с нашими бинарями, которые весили от 10 до 50 мегабайт.
Может быть можно скомпилировать ROS под какой-то более легковесный дистрибутив Linux для распространения через Docker, но кто этим будет заниматься? У нас ведь не клуб юных линуксоведов.
К чему мы в итоге пришли
Подход себя зарекомендовал
Можно считать подход "ROS2 + Docker" обкатанным. Мы в итоге выступили на соревах и победили. Docker ни разу нас не подвел. Все неполядки, которые возникали, были из-за отвала механики, отвала электроники, неправильно написанной стратегии или багов ROS. Но Docker обеспечил нам полную идемпотентность запусков и четкий жизненный цикл наших ROS приложений.
Не без труда, но мы избавились от всех проблем старого подхода:
- Мы мобедили проблему с конфликтами зависимостей. Потому что ничего кроме Docker на роботах установлено не было. Все библиотеки теперь зашиты в Docker образы и используются контейнерами в изолированной среде. Обновление библиотек для одного воркспейса перестало затрагивать другие.
- Достигнута почти полная воспроизводимость запусков. Я пишу "почти", потому что к сожалению робот работает в нашем физическом окружении, а оно всегда полно хаоса. Но мы как минимум знали, что если софт уже протестирован, то сам по себе он никогда не отвалится. Проблемы мог создать только рандом, физически возникающий на игровом поле.
- Со временем билда не все так гладко из-за того что я не смог организовать билд-кэш для С++ внутри Docker. Однако мы больше не компилировали на роботе, совсем. Весь билд происходил у нас на лэптопах/ПК, после чего готовые Docker приложения отправлялись на робота. Самый прикол в том, что загружать обновления образов можно даже в тот момент, когда робот занят ездой по игровому полю.
Что можно сделать лучше
Уменьшить вес образов
Как я уже ранее писал, мы устанавливаем все билд-тулы в тот же образ, который потом отправляется на робота. Можно сэкономить 300-500 мегабайт при помощи Docker Multistage Builds.
Проблема multistage сборок в том, что всей команде придется погружаться в Docker. У нас тут не кружок девопсов, а лаборатория робототехники, поэтому такое требование было бы расточительным по отношению к команде.
Можно скрыть сложность multistage сборок за оберткой системы сборки...
Организовать билд-кэш для C++ пакетов
Ближе к соревнованиям воркспейс navigation/
настолько разросся, что каждый билд занимал по две минуты. Разработчики C++ сейчас рассмеются. В целом это терпимо, но каждый раз пересобирать даже неизменившиеся пакеты это расточительно, а на соревнованиях может даже бесить. Все остальное работало настолько стабильно, что время билда было единственной раздражающей вещью.
Пока нет легальных способов вторгаться в окружение собирающегося Docker образа и помещать туда какие-то файлы. Чтобы "достингуть" состояния рабочего билд-кэша, нужно поменять логику сборки образов:
- Нужно сначала собирать образ с установленными билд-тулами и всеми зависимостями.
- Запускать контейнер из такого образа и маунтить к нему директорию воркспейса.
- Внутри контейнера запускать билд. Colcon внутри контейнера будет использовать директорию воркспейса как build output, в том числе будет складывать в нее билд-кэш.
- После успешной сборки можно копировать результаты билда в этот же контейнер и делать коммит в новый образ. Либо копировать результаты билда в другой контейнер, без лишних зависимостей, предназначенный специально для деплоя, и коммитить его в новый образ.
Как вы понимаете, если делать такое вручную 100 раз в день, можно ебнуться очень устать. Нужно чтобы все эти манипуляции выполняла система сборки.
Унифицировать решение
Два предыдущих варианта улучшений невозможно реализовать без программирования сложной логики. Как я уже выше написал, возможно лучше написать новые Bazel rules, а самих разработчиков научить нескольким командам Bazel, чем писать свою собственную систему сборки.
Что я не затронул в статье
Отправка образов на роботов
Изначально я хотел добавить в эту статью и рассказ про то, как проброс SSH тоннелей и кастомные Docker Registry помогают деплоить в локальной сети, но потом понял что это будет невозможно прочитать. Устройство скрипта, отправляющего образы на робота я опишу в отдельной статье и даже приложу рабочий код.
Мета система сборки
Все описанное в статье было бы невозможно использовать без нашей системы сборки bldr. Мы реально в 99% случаев пользовались только ею. Но про нее я напишу отдельную статью.
Dev-контейнеры
Мы использовали описанные тут подходы для того чтобы иногда разрабатывать прямо внутри Docker контейнеров. Для меня это было особенно важно, так как я писал софт для наших роботов в основном на Mac OS и Windows. Но это не относится напрямую к теме статьи, а статья и так большая.
Итог
Деплоить на роботов с Docker реально. Подход обкатан и показал себя надежным. Единственная проблема пока только в отсутствии удобных инструментов, заточенных под эту специфическую задачу.