PunchI’d love to play in La Liga one day – RayaESPN DeportesLiga MX: Panorama rumbo a la Liguilla Apertura 2026InquirerTaal Volcano logs 2 more minor phreatomagmatic eruptionsESPNReese's latest double-double fuels Game 3 rout as Dream take 2-1 Finals leadThe Jerusalem PostUS Supreme Court reverses Trump administration push to restrict treatment for transgender inmatesRTP DesportoLiga dos Campeões. Sporting derrotado em casa pelo LASK Linz한겨레K리그2 전남, 구단 내 주취폭력 가해자 “즉각 퇴단 조치”ZDF heuteAktuelle Pressemitteilungen des ZDFUOLBatalhão voltado a coibir roubo em SP chega a um mês com 8.000 abordagens, 67 presos e sete celulares apreendidosSky TG24Ancona, truffa e corruzione in appalti post sisma, 51 indagatiBusiness AMRegering verwacht deze week geen akkoord meer over de begrotingSportstarNew Zealand vs India LIVE streaming info: When, where to watch NZ v IND 1st T20I? Venue; Toss timing
The Daily Newsstand · Free, Always
Thursday, October 22, 2026

Сансара зависимостей: как мы перешли с ROS 1 на ROS 2 без массового переписывания кода

Translate

История о том, как мы в Venti Technologies меняли программную платформу автономных машин, пока сами машины продолжали перевозить контейнеры.

В жизни технологического стартапа рано или поздно наступает момент, когда программное обеспечение, предназначенное для будущего, обнаруживает, что намертво привязано к прошлому. В нашем случае прошлое называлось ROS 1 Kinetic и Ubuntu 16.04.

В сентябре 2024 года мы завершили миграцию на ROS 2 Humble Hawksbill и Ubuntu 22.04. Она заняла около десяти месяцев, потребовала создания параллельной системы сборки, собственного слоя совместимости, нескольких уровней тестирования и почти полугода валидации. При этом нам удалось сохранить практически всю прикладную логику и не останавливать разработку продукта.

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

Эта история не о том, как ROS 2 волшебным образом решила все проблемы старой системы. Скорее о том, как создать условия, в которых старую платформу можно заменить новой, не заставляя весь остальной продукт участвовать в этом процессе.

1. Когда оптимизация становится способом не обновляться

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

К 2023 году используемая нами ROS 1 Kinetic уже более двух лет находилась за пределами официального срока поддержки. Это обстоятельство само по себе не было причиной немедленно всё переписывать. Проблема заключалась в постепенно накапливавшихся ограничениях: старые версии PCL и Boost, сложности с обновлением NVIDIA-драйверов, CUDA и TensorRT, зависимость от давно устаревшего окружения Ubuntu 16.04.

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

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

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

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

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

Почему не ROS 1 Noetic?

Логичный вопрос: зачем вообще переходить на ROS 2, если можно было обновить ROS 1 Kinetic до Noetic?

Такой вариант мы рассматривали. Noetic действительно выглядел менее радикальным изменением, особенно с точки зрения прикладного API. Однако официальной целевой Ubuntu-платформой для него была 20.04, тогда как мы работали на 16.04. Это означало необходимость обновлять операционную систему, компиляторы, библиотеки и связанные зависимости.

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

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

Иными словами, оставшись на ROS 1, мы не избавились бы от значительной части работы по проверке. При этом Noetic изначально имела запланированный конец поддержки в мае 2025 года. В конце 2023-го это уже было вполне обозримое будущее.

С инженерной точки зрения Noetic оставалась возможным промежуточным вариантом. Но мы решили, что если всё равно предстоит серьёзная модернизация и длительная валидация, разумнее вложить эти усилия в переход на ROS 2.

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

2. Две сборки одной реальности

Основной сложностью была даже не несовместимость ROS 1 и ROS 2. Нам необходимо было провести миграцию системы, которая в это же самое время продолжала активно разрабатываться.

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

Поэтому мы приняли принципиальное решение: одна кодовая база, два независимых пути сборки.

Зачем нам понадобился Bazel

Первым шагом стало создание новой системы сборки, независимой от существующего catkin/CMake. Для этого мы выбрали Bazel.

Здесь можно было бы устроить очередной спор о том, какая система сборки лучше, но это не имеет отношения к сути задачи. CMake умеет работать с зависимостями, конфигурациями и различными компиляторами. Мы не переходили на Bazel потому, что CMake принципиально неспособен собирать большой C++-проект.

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

Важно уточнить: возможности Bazel не означают, что любая сборка автоматически становится герметичной, воспроизводимой и быстрой. Для этого необходимо соответствующим образом описывать зависимости и инструменты. Но его модель позволяла нам выстроить именно тот процесс, который требовался для миграции.

Схематически организация выглядела следующим образом:

                  ОБЩАЯ КОДОВАЯ БАЗА
                 Прикладные компоненты
                           |
                           v
                 ROS Abstraction Layer
              (выбор на этапе компиляции)
                           |
               +-----------+-----------+
               |                       |
          catkin/CMake                Bazel
               |                       |
          ROS 1 headers          ROS 2 headers
               |                       |
          ROS 1 Kinetic          ROS 2 Humble
              roscpp                 rclcpp

Схема концептуальная: она показывает выбор реализации, а не буквальную последовательность всех шагов компиляции.

Существующая catkin-сборка продолжала использовать ROS 1 и обслуживать разработку продукта. Параллельно Bazel собирал тот же прикладной код с ROS 2. Новые изменения попадали в общий исходный код, а не в две независимые версии приложения.

Так у нас появилась возможность проверять новую платформу, не превращая её разработку в отдельный, постоянно отстающий от основного продукта проект.

Почему мы не использовали ros1_bridge

Здесь возникает ещё один закономерный вопрос: зачем писать собственный слой совместимости, если существует ros1_bridge?

Потому что он решает другую задачу.

ros1_bridge позволяет двум работающим системам, использующим ROS 1 и ROS 2, обмениваться сообщениями и некоторыми другими типами взаимодействий. Это полезно для постепенной миграции, когда часть нод остаётся на ROS 1, а часть уже работает на ROS 2.

Наша цель была иной. Мы хотели собирать существующие компоненты непосредственно для ROS 2, сохраняя знакомый им интерфейс на уровне исходного кода. Нам не требовалось запускать рядом два middleware и организовывать передачу сообщений между ними.

Мы хотели, чтобы приложение продолжало думать, будто оно разговаривает с ROS 1, хотя реализация под этим интерфейсом уже использовала ROS 2.

ROS Abstraction Layer: сохранить API, заменить реализацию

Центральным элементом миграции стал ROS Abstraction Layer, разработанный командой Software Infrastructure.

Его задача заключалась в воспроизведении используемого приложением интерфейса ROS 1 поверх ROS 2. Флаги компиляции и конфигурация сборки определяли, какие заголовки и реализации будут использоваться. При этом само приложение не должно было знать, какой именно бэкенд выбран.

Рассмотрим знакомый для ROS 1 паттерн публикации сообщения:

auto pub = nh.advertise<MessageType>("topic", 10);
pub.publish(message);

В ROS 2 этот интерфейс реализовывался через rclcpp. В упрощённом виде соответствующий адаптер можно представить так:

template <typename M>
Publisher advertise(
    rclcpp::Node& node,
    const std::string& topic,
    uint32_t queue_size,
    bool latched)
{
    rclcpp::QoS qos{queue_size};

    if (latched)
        qos.transient_local();

    return Publisher{
        node.create_publisher<M>(topic, qos)
    };
}

Это сокращённый пример, иллюстрирующий подход, а не полная копия реализации. В реальном коде присутствовали дополнительные проверки, обработка имён и другие детали.

Суть в том, что объект Publisher внутри использует нативный rclcpp::Publisher, а его publish() делегирует непосредственно в ROS 2. Дополнительного процесса, который принимает сообщения через ROS 1 и затем публикует их через ROS 2, здесь нет.

Мы проектировали этот слой, ориентируясь на принцип zero-cost abstractions: использовать шаблоны, выбор реализации на этапе компиляции и прямые вызовы нативных API там, где это возможно.

При этом было бы неправильно утверждать, что любой вызов через ROS Abstraction Layer гарантированно имеет ровно ту же стоимость, что и прямой вызов rclcpp. Некоторые операции требуют дополнительной адаптации, а строгая оценка накладных расходов потребовала бы отдельных измерений.

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

3. Когда совпадают сигнатуры, но не поведение

Наиболее интересные проблемы начались там, где похожие API скрывали разные модели поведения.

Самый показательный пример — latched topics.

В ROS 1 издатель мог сохранить последнее опубликованное сообщение, чтобы поздно подключившийся подписчик получил его сразу после установления соединения. В ROS 2 похожее поведение реализуется через политику долговечности QoS — transient_local.

Но есть нюанс. Для получения ранее опубликованных сообщений недостаточно выставить transient_local только на publisher. Подписчик также должен запросить соответствующую durability. При volatile он получит лишь новые сообщения, даже если издатель использует transient_local.

Поэтому в нашем ROS Abstraction Layer недостаточно было просто перевести аргумент latching в настройку QoS издателя. Потребовалось также централизованно определить топики, для которых подписчики должны использовать соответствующую политику.

Это решение избавило от необходимости массово изменять существующие вызовы subscribe() в прикладном коде. Но оно создало другой компромисс: список таких топиков необходимо поддерживать. Мы сознательно перенесли сложность из большого количества мест использования в одну контролируемую часть инфраструктуры.

Похожие проблемы возникали с параметрами, временем и преобразованиями координат. Например, имена параметров в существующей системе не всегда соответствовали модели ROS 2, и их приходилось нормализовать. Старые операции с ros::Time и ros::Duration адаптировались к типам rclcpp, а привычные интерфейсы TF реализовывались с использованием tf2.

Особенно интересными оказались ROS services. В ROS 2 взаимодействие с executor и модель асинхронного выполнения отличаются от привычного использования блокирующих вызовов в ROS 1. Нельзя просто заменить один вызов другим и предположить, что всё остальное останется неизменным.

Для части сервисных вызовов потребовалась отдельная адаптация через асинхронный запрос с ожиданием результата. Важно было учитывать ограничения на вызовы, использующие spin_until_future_complete(), в частности невозможность бездумно запускать дополнительное вращение executor для уже выполняющейся ноды.

Некоторые несовместимости удалось скрыть внутри слоя, другие потребовали небольшого количества условных веток компиляции непосредственно в прикладном коде. Именно поэтому мы говорим «без массового переписывания», а не «без единого изменения».

Воспроизводить весь ROS 1 со всеми его особенностями мы не пытались. Нам требовалась совместимость с той частью интерфейсов и поведения, на которую действительно опиралось приложение.

О священности имён

Был и более неожиданный случай. ROS 2 предъявляла более строгие требования к именам файлов сообщений: согласно её соглашениям, файлы .msg должны использовать UpperCamelCase и содержать только буквенно-цифровые символы.

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

Мы решили изменить соответствующие проверки в ROS 2, сохранив существующие имена сообщений. Управление зависимостями через Bazel позволило встроить этот патч в процесс сборки.

Разумеется, у такого решения есть цена. Патч отличается от upstream-реализации, его необходимо учитывать при обновлении зависимости, и он не превращает нестандартные имена в универсально переносимые ROS 2-интерфейсы.

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

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

Как доказать, что машина осталась собой

После реализации совместимости система начала собираться и запускаться на ROS 2. Однако считать миграцию завершённой на этом этапе было бы преждевременно.

В автономном транспорте успешная компиляция не доказывает корректность поведения. Даже совпадающие результаты отдельных юнит-тестов недостаточны, если изменение middleware влияет на доставку сообщений, порядок выполнения callbacks, временные характеристики или взаимодействие между компонентами.

Поэтому параллельно с миграцией мы развивали систему проверки на нескольких уровнях.

Сначала расширяли покрытие юнит-тестами. Затем использовали разработанную внутри Software Infrastructure систему Solo, позволяющую запускать отдельные ноды на ранее записанных bag-файлах или их фрагментах. Это позволяло воспроизводить реалистичные входные данные, проверять выходные результаты и тестировать компоненты, для которых обычные юнит-тесты были слишком ограниченным инструментом.

Следующий уровень — полноценные прогоны симуляций. Они позволяли проверить взаимодействие компонентов в составе системы и сопоставить результаты ROS 1 и ROS 2 в заранее определённых сценариях.

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

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

После успешного прохождения этих этапов мы начали полевые испытания. Машины выполняли реальные задания, поступающие от операторов порта, и перевозили настоящие контейнеры. Во время испытаний внутри находились safety drivers, готовые остановить машину в случае необходимости.

Собранные во время тестов bag-файлы анализировались отдельно. Мы обращали внимание в том числе на частоты сообщений, аномалии и общую корректность поведения системы.

Потому что если машина приехала из точки А в точку Б, это ещё не доказывает, что она сделала это по правильным причинам. Возможно, она приехала туда вопреки ошибке, которая в следующий раз проявится при других условиях.

Этап тестирования и окончательной валидации занял до полугода. В сентябре 2024 года переход был завершён.

Параллельно модернизировались и другие части инфраструктуры продукта. Появилась более удобная система управления конфигурациями — Config System с собственным UI. Отдельно мы заменили RViz в production-интерфейсе на внутренний продукт Viz. Эти изменения не являлись обязательными свойствами перехода на ROS 2, но были частью более широкой модернизации программной платформы.

4. Что осталось после миграции

После того как ROS 2 была проверена и переведена в эксплуатацию, необходимость поддерживать две платформы исчезла. Параллельные сборки выполнили свою задачу, и мы смогли окончательно отказаться от ROS 1.

Это позволило удалить устаревшие зависимости, устранить дублирование и изменить организацию генерации исходников и получения сторонних библиотек.

В начале проекта учитываемый объём кодовой базы составлял приблизительно 1,5 миллиона строк. После полного перехода на Bazel и отказа от ROS 1 — около 600 тысяч.

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

Иными словами, мы не удалили 900 тысяч строк прикладной логики и не стали автоматически работать быстрее только из-за уменьшения количества файлов. Мы избавились от существенной части прежнего инфраструктурного груза и изменили способ управления зависимостями.

Два года спустя

В октябре 2026 года система продолжает работать на том же вычислительном оборудовании.

За два года продукт Venti Technologies значительно развился: добавлялась функциональность, внедрялись новые AI-модели, увеличивался объём задач, выполняемых на борту автономных машин. И всё это продолжает работать на том оборудовании, вычислительных возможностей которого до модернизации уже не хватало для дальнейшего развития.

По статистике ежедневных запусков машин, текущая медианная загрузка ресурсов выглядит следующим образом:

Ресурс

Медиана

Наблюдавшийся пик

CPU

50%

70%

Оперативная память

50%

—

GPU

56%

76%

Видеопамять GPU

67%

—

Это эксплуатационные показатели, а не результат специально подготовленного сравнительного бенчмарка ROS 1 и ROS 2. Для полноценного количественного сравнения производительности потребовались бы одинаковая аппаратная конфигурация, фиксированная нагрузка, определённая методика измерений и результаты обеих реализаций.

Таких выводов из приведённых процентов сделать нельзя. Более того, за прошедшие два года изменились компиляторы, библиотеки, алгоритмы и состав самого приложения, поэтому приписывать весь результат исключительно ROS 2 было бы технически некорректно.

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

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

Что мы на самом деле перенесли

Оглядываясь назад, можно сказать, что центральным результатом проекта был не сам факт появления ROS 2 в списке зависимостей.

Главным оказалось разделение двух вещей, которые в большой системе постепенно начинают казаться неразделимыми: прикладной логики и платформы, на которой она выполняется.

Мы создали границу совместимости, позволяющую сохранить используемый приложением интерфейс, пока реализация под ним менялась. Эта граница не устранила все различия между ROS 1 и ROS 2, но позволила собрать большую их часть в одном контролируемом месте.

Параллельная система сборки дала возможность проводить миграцию без отдельной копии продукта. ROS Abstraction Layer сократил объём необходимых изменений прикладного кода. А многоуровневая система тестирования позволила проверять последствия этих изменений до того, как обновлённая платформа была окончательно принята в эксплуатацию.

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

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

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

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.