В процессе улучшения подходов к менеджменту зависимостей компонентов нашей Операционной Системы появилась необходимость перейти от монолитной статической сборочной системы на основе CI/CD инструментов к динамическому распределённому подходу с порождением сотен и тысяч автономных задач. Как выяснилось в процессе, это не самый радужный сценарий использования систем автоматизации, но вполне достижимый.


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


Также частично раскроем информацию о том, как мы выполняем распределённую сборку дистрибутивов.


Ожидается много текста и примеров кода.


Введение: отправная точка


К моменту постановки задачи основной сборочный конвейер дистрибутива опирался на Jenkins в качестве среды автоматизации, Docker с развернутыми средствами разработки для воспроизводства эталонного сборочного окружения и Gitea в роли системы управления исходным кодом. Кроме того, использовался ряд дополнительных инструментов для тестирования, управления дефектами, project-management и пр.


Основным камнем преткновения являлась централизованная иерархия ключевых репозиториев, составляющих дистрибутив, выполненная по принципу мета-репов с подключением сабмодулей. Если на уровне подсистем и отдельных команд разработчиков такой подход ещё выдерживает критику, то на этапе объединения в единое окружение все телодвижения команд упираются в необходимость предложения правок в мета-репозитории мейнтейнеров. С одной стороны, это существенно ограничивает свободу действий мейнтейнеров при работе над собственными инфраструктурными прожектами. С другой, никак не связанные с ними разработчики также должны отслеживать чужую кухню и блокировать свои работы до апрува правок в сборочные репы. Автоматизации эта проблема подлежит весьма условно и время от времени дает сбои. Основная цель в этом случае — разграничение объектов контроля и минимизация дедлоков/простоев.


Стоит ли упоминать, что и контроль зависимостей в таком ключе является неоправданно сложным квестом?


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


Вертикальное масштабирование - необходимый или достаточный фактор?

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


Щас купим новый трушный сервер и тогда заживём

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


  • Инженерные nightly билды
  • Релизные билды с учетом специфичных требований различных систем сертификации
  • Контрольные дебаг-билды и билды с code-coverage
  • Отдельные контрольные билды в рамках концепции безопасной разработки
  • Поддержка нескольких дистрибутивов с разными конфигурациями и адресным применением вышеперечисленных опций

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


Нельзя сказать, что действовавший сборочный пайплайн был плох. За последние 6 мажорных релизов осечек он не давал. Но с какого-то момента накопленные требования и общее повышение зрелости процессов разработки привели к утрате целесообразности дальнейшего вложения ресурсов в поддержку «status quo».


Построение графа зависимостей


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


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


Начали со скрипта обхода репозиториев через HTTP API. Он выполняет обход всех доступных пользователю (от имени которого выполняется скрипт с правами RO) репозиториев на сервере и производит извлечение дескрипторов. Пример кода получения результатов выполнения поддерживаемых запросов под спойлером:


Выполнение запросов к Gitea API
import json
import requests
from requests.adapters import HTTPAdapter, Retry

def http_api_request( url, max_pages, params = '' ):
    jsons   = []
    token   = {'content-type':  'application/json',
               'accept':        'application/json',
               'Authorization': 'token xxxxxxxxxxxxxxxxxxxxxxxxx...'}
    session = requests.Session()
    retries = Retry( total = 5, backoff_factor = 0.5,
                     status_forcelist = [500, 502, 503, 504] )
    session.mount( 'http://', HTTPAdapter( max_retries=retries ) )
    try:
        page = 0
        while ( True ):
            reply = session.get( url + '?limit=100&page=' +
                                 str( page + 1 ) + params,
                                 headers = token )
            json_ = reply.json()
            if (json_ == None) or (len( json_ ) == 0):
                break
            else:
                jsons.append( json_ )
                if (max_pages != 0) and (page + 1 >= max_pages):
                    break
            page += 1
    except Exception as e:
        print( 'Error: failed on "' + url + '" (message: "' +
               str( e ) + '")' )
        exit( 1 )
    return jsons

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


Для построения и анализа самого графа использовали networkx. Выбор обусловлен поддержкой формата .dot, в котором хранит графы проект Graphviz, используемый в дальнейшем для визуализации.


Поиск некоторых аномалий в зависимостях:


  • Отсутствие циклов на графе. Наличие хотя бы одного цикла в нашем случае является ошибкой:
    import networkx as nx
    ...
    graph = nx.DiGraph( nx.nx_pydot.read_dot( fname ) )
    cycles = nx.simple_cycles( graph )
    cycle  = ''
    for nodes in cycles:
    for node in nodes:
        cycle += '"' + node + '" → '
    cycle += '"' + nodes[0] + '"'
    break
    return cycle
  • Поиск изолированных вершин. Наличие вершин без пред-/пост- зависимостей в нашем случае является ошибкой:
    nodes = ''
    for node in graph.nodes():
    if graph.degree( node ) == 0:
        nodes += node
    return nodes

Скрипт контролирует и другие аномалии, связанные уже с особенностями пакетного менеджера. Оформлено всё это «хозяйство» в виде автономной задачи в Jenkins, чтобы разработчики в любой команде могли убедиться, что их репозитории включены в дистрибутив непротиворечивым образом.


Результирующий граф «нарезается по слоям», что определяет очерёдность сборки. Множество дескрипторов некоторого «слоя» мощностью M, совместно с тиражированием каждого дескриптора по N поддерживаемым процессорным архитектурам, составляет полный объем потенциально распараллеливаемых сборочных задач M x N на слой. Как разработчикам, так и мейнтейнерам предоставляется сгенерированная тут же простейшая «веб-морда», чтобы визуально посмотреть что они включили в билд, на каком этапе собирается интересующий компонент, правильно ли расставлены зависимости и т.п. Кроме того, формируется слепок вторичных дескрипторов (по архитектурам) и файл с их распределением по слоям для последующей стадии конвейера — сборки.


Тестовый набор пакетов для собственных экспериментов мейнтейнеров на раннем этапе внедрения распределённой системы сборки (каждый дескриптор может продуцировать несколько пакетов, например, *-dev и *-runtime):


Визуализация дерева зависимостей


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


С визуализацией графа капитально помогает Graphviz, поскольку умеет рисовать в SVG. Ну а подпатчить его руками и добавить ссылки — дело не хитрое >}.


Поиск сделали по-простому — распарсили в отдельный .js контент страниц с описанием пакетов и подгрузили его на все страницы:


Описание пакета и форма поиска


Масштабирование серверов


Сразу отмечу, что автор не является разработчиком на Java или DevOps-инженером. На всё это «хозяйство» пришлось смотреть с опытом системного программиста. Какие-то замечания, безусловно, будут обоснованы, как и упрёки в невладении сакральным знанием. Однако, анализ ряда архитектурных решений Jenkins, а также производных от них ограничений, и сейчас, постфактум, вызывает выгорание окружающего пространства в радиусе нескольких парсеков. Данный нюанс не отменяет того обстоятельства, что цель в итоге успешно достигнута.


Только теперь мы подобрались к Jenkins и построению динамического конвейера. Под спойлером термины в том виде, как мы их трактуем для себя; они могут упростить понимание изложенного далее:


Jenkins и его сущности
  • pipeline — множество взаимосвязанных задач, направленных на решение общей цели
  • job — отдельная задача, исполняемая Jenkins на отдельном узле (сервере) и в отдельном контейнере
  • build — запущенный на исполнение job, обычно обладающий артефактами по результатам работы (в статье явно не используется, билдом называется сборка дистрибутива ОС)
  • build ID (BID) — уникальный целочисленный идентификатор билда
  • stage — часть build, характеризующая некоторую последовательность действий на языке, производном от Groovy (т.к. имеет свои особенности и ограничения)
  • step — отдельный шаг в stage, часто связанный с вызовом конкретного плагина Jenkins (примеры: writeFile, readFile)
  • CPS — одна из причин, почему в Jenkins нельзя использовать нормальный Groovy
  • slot — максимальное число одновременно исполняемых job / build на сервере (настраивается администратором Jenkins для каждого сервера)

Пример pipeline, который может на практике оказаться как статическим, так и динамическим:


Пример pipeline


Все имеющиеся сервера были занесены в библиотеку профилей и отранжированы по уменьшению производительности:


  • число ядер и потоков CPU для определения предельного числа параллельно исполняемых сборочных потоков на задачу
  • число slot-ов для определения максимального числа параллельно запускаемых сборочных задач
  • интегральный коэффициент производительности (для расстановки приоритетов серверам: чем больше коэффициент, тем больше вероятность назначения ему задачи)

Перед порождением вороха сборочных задач определяется доступность серверов. Сделать это можно двумя способами:


  1. Спросить у Jenkins:
    if ( Jenkins.getInstance().getNode( "<сервер>" ).toComputer().isOnline() ) {
        ...
    } else {
        ...
    }
  2. Попытаться запустить тестовый job на всех серверах разом и, если посчастливится, даже разбудить кого-то из них:
    def jobs     = [:]
    def statuses = [:]
    for ( server in servers ) {
    def name            = server.name
    statuses["${name}"] = ''
    jobs[server.name]   = {
        try {
            timeout( time: <X>, unit: 'MINUTES' ) {
                def job = build job: '<job-name>',
                                parameters: [...],
                                propagate: false,
                                wait: true
                catchError( buildResult: 'SUCCESS', stageResult: 'FAILURE' ) {
                    if ( (job.result != "SUCCESS") && (job.result != "UNSTABLE") )
                        statuses["${name}"] = 'FAILURE'
                }
                if ( job.result == "SUCCESS" ) {
                    statuses["${name}"] = 'SUCCESS'
                    /* Здесь можно скопировать из job-а артефакты, определив,
                     * например, свободное пространство на SSD/HDD */
                    copyArtifacts( ... );
                }
            }
        } catch ( ... ) {
            /* Чтобы отличить таймаут от прерывания задачи придется заморочиться:
             * stackoverflow.com/questions/51260440 */
        }
    }
    }
    parallel jobs

Первый вариант проще, но не позволяет выводить сервера из suspend-а. В тоже время, без таймаута во втором варианте, job, запущенный на отсутствующем сервере, будет висеть вечно и толку от такого пайплайна не будет. Но сам перехват таймаута достаточно некрасивый — см. ссылку в коде. Также придется потратить время на настройку таймаутов засыпания/пробуждения серверов (в Jenkins) и таймаута ожидания job-а в своем коде. На практике эти параметры оказались достаточно недетерминированными, пришлось их брать с хорошим запасом.


Горизонтальное масштабирование в таком варианте достигается простым подключением новой машины к Jenkins и добавлением её профиля в код (всё это легко выносится в JenkinsLib). Отключили сервер на обслуживание? Скатертью ему дорога, билд дистрибутива не завалится.


Вертикальное масштабирование осуществляется как обычно — обновлением комплектации текущих серверов.


Оптимизация динамического пайплайна


В последнем примере кода приводится один из не самых частых вариантов динамического запуска кода в параллель, причем, не через тиражирование stage-ей или step-ов. Распараллеливанию здесь подлежат именно job-ы, что позволяет контролировать общее их количество и сопоставлять с доступными слотами и производительностью серверов. По классике это сделать сложнее, т.к. в модели вычислений Jenkins статусы каждой отдельной операции или stage формализованы никак плохо.


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


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


Был написан простейший тест, выполняющий создание сотни мелких файлов:


Тест файловых операций
echo "Запись 100 файлов"
for ( int i = 0; i < 100; i++ )
    writeFile file: 'test-' + i.toString(), text: 'empty' + i.toString()
echo "Чтение 100 файлов"
for ( int i = 0; i < 100; i++ )
    if ( fileExists( 'test-' + i.toString() ) ) {
        try {
            def readContent = readFile 'test-' + i.toString()
        } catch (Exception e) {
            echo "Error: " + 'test-' + i.toString()
        }
    } else
        echo "Файл не найден: " + 'test-' + i.toString()

echo "Shell тест"
sh '''
    for ((i = 0 ; i < 100; i++)); do
        echo 'empty' > shell-file-${i}
    done
    for ((i = 0 ; i < 100; i++)); do
        if [ -f shell-file-${i} ]; then
            VAR=`cat shell-file-${i}`
        fi
    done
'''
echo "Shell тест завершён"

Результаты знатно впечатлили. Shell-реализация работает менее секунды, Groovy-реализация от 35 до 45 на разных серверах. Из 15 минут выше около 6 занимала именно запись различных логов. Переделывание на Shell откровенно дурно пахло и, как выяснилось позднее, не было способно решить проблему в принципе.


Проблема оказалась комплексной. By design, один из режимов работы Jenkins подразумевает повышение живучести инстансов сервиса за счет параноидального кэширования всего и вся на хард. В нашем случае это явно было избыточным — "померла, значит померла", лучше перезапустим сборку билда, чем будем 24x7 ловить тормоза. При отключении durabilityHint( 'PERFORMANCE_OPTIMIZED' ) тест стал показывать сопоставимое время, не демонстрируя зависимость от реализации. Время синтетического билда упало до 6.25 мин.


Уже сильно лучше, но всё равно выглядит плохо. В процессе раскуривания логов и подключения на горячую к контейнерам Jenkins обнаружили, что на одном из серверов только что запущенный докер почти минуту что-то делает с загрузкой одного ядра CPU. Поскольку каждый job запускается в параллель на разных серверах, но абсолютно синхронно (см. дальше про асинхронность), а сборка компонентов дистрибутива осуществляется по «слоям», это неизбежно приведёт к повторению таких сценариев снова и снова. На этом сервере нам не фортануло с fakeroot has become extremely slow inside docker. Полечили опциями запуска docker, время упало уже до 4.1 мин.


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


Комбинированный тест файловых операций
echo "start"
for ( int i = 0; i < 100; i++ )
    sh """echo 'empty' > complex-file-${i}"""
for ( int i = 0; i < 100; i++ )
    def stdout = sh( script: """if [ -f complex-file-${i} ]; then
                                    cat complex-file-${i}
                                fi""", returnStdout: true )
echo "stop"

По неизвестной до сих пор причине этот код медленнее любой из предшествующих реализаций, причем в несколько раз. Без отключения maximum durability mode он работает около полутора минут, с отключением все равно более 15 секунд. Мультипликативный эффект суммарно ожидался уже в пределах десятков минут, что также крайне абыдно! Приговорили многократный запуск sh-step, выделили весь его код в отдельный метод. Результат распределения стал укладываться в 2.7 мин. Дальше сам Jenkins решили не мучить, поскольку эффект ожидался минимальным, да и оставались вопросы с более очевидным профитом от оптимизации — балансировка нагрузки и асинхронность.


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


def jobs = [:]
for ( ... ) {
    jobs[key] = {
        ...
    }
}
parallel jobs

и имея M x N задач для распараллеливания, стали ставить эксперименты по группировке дескрипторов:


  • можно собирать все архитектуры одного пакета группой в одной задаче
  • или не группировать вовсе и максимизировать вовлечение всех серверов

После включения сборки компонентов во втором случае на тестовом наборе позитивный эффект варьируется от 15% до 20%.


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


Внимательный специалист в последнем фрагменте кода заметит огромную дыру — он абсолютно синхронен. Допустим, что на X — 1 сервере запущена сборка такого же числа обычных задач, а на последнем ровно одна «тяжелая». Это приведёт к тому, что подавляющее большинство серверов будет большую часть времени простаивать.


Много времени потратили на борьбу с CPS и его паталогической нелюбовью к closures, перепробовали следующие варианты:


  • Перво-наперво, попробовали смастерить привычный на системном уровне пул-потоков. Тут же нагуглился POSIX-подобный класс Thread и даже с почти классическим методом join(), правда без аналога pthread_timedjoin*(). В Jenkins банальные потоки просто не заработали. Как можно было сломать базовую многопоточность я понять отказываюсь, даже учитывая особенности сервиса. Fail!
  • У используемого step-плагина имеется намёк на асинхронность в виде waitForBuild(). Ему честно передается BID, но вот сам build job ... при таком сценарии перестаёт возвращать ID честно запущенного асинхронного job-а. Частичный Fail, т.к. job все-таки стал асинхронным!
  • Искали путь запуска в неблокирующем режиме конструкции parallel. Разработчики Jenkins вообще в эту сторону не думали. И снова Fail!

В итоге и эту проблему преодолели, но иначе. У самого Jenkins с асинхронностью совсем дела плохи.


Резюме


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


Подписывайтесь на наш канал, чтобы быть в курсе свежих новостей

Комментарии (11)


  1. saboteur_kiev
    05.01.2024 11:45

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

    У вас там 100 раз идет sh "echo" и 100 раз идет def stdout = sh ()

    Контролируйте количество внешних программ, которые нужно запустить и выполнить. Это же каждый раз нужно запустить интерпретатор (новый процесс в системе, память под него выделить, внутри еще и cat, который тоже должен выполниться), итого 300 процессов запустить/остановить


    1. a-n-d Автор
      05.01.2024 11:45

      Вопрос не совсем архитектурный в данном случае. Архитектурные проблемы в интерфейсе библиотеки мы устранили, как и было рассказано. И проявление проблемы как бы ушло, но не сама проблема.

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


      1. saboteur_kiev
        05.01.2024 11:45

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


        1. a-n-d Автор
          05.01.2024 11:45

          На том тестовом наборе, на котором всё это исследовалось, логи были не сильно большие. Но за наводку спасибо, может и тут есть что ускорить


  1. dmitry_rozhkov
    05.01.2024 11:45
    +1

    Когда-то, давным давно я работал в Нокии в проекте Maemo. Тогда ещё не было Jenkins, и система сборки была своя. Количество репозиториев было значительно больше 500 и дело не ограничивалось Git'ом. Например, репозиторий браузера был на экзотическом Mercurial, и разработчики наотрез отказывались от переезда на Git, потому что их workflow был завязан на апстримовый репозиторий, который у Mozilla был тоже на Mercurial. Ну, и других VCS было в товарных количествах - Subversion, CVS, Darcs и даже deb-src.

    В общем, чтобы всё было максимально гибко нельзя было полагаться на триггер билда по PR. Некоторые пакеты релизились вместе, и вместе должны были билдиться. Такое было редко, но случалось. Если их билдить по отдельности, то ни тот, ни другой билд не проходил. Ну, например, либы меняла API, и приложение в новой версии использовало новый API. С версионированием API в embedded никто заморачиваться не хотел, особенно на этапе разработки ещё незарелизинной платформы.

    Отсюда вытекала необходимость сканирования всех репозиториев по крону на предмет не PR, а tag'ов с новой версией. Workflow в командах был очень разнообразный, но чаще было так: разработчик делал PR, его ревьювили, особо продвинутые команды запускали свой локальный CI для него. Если всё было хорошо, то его мержили и ставили tag новой версии. Вот эти tag'и уже триггерили общесистемный CI.

    Для построения дерева билда, а точнее топологически отсортированного списка, тоже использовалась NetworkX. Но перестраивались не все новые пакеты со всеми зависимостями, а только новые пакеты с неизменившимися зависимостями первого уровня, чтобы убедиться, что API не сломался. Перестравивать все уровни зависимостей смысла нет, только если для обогрева атмосферы (это основная претензия к системам типа OBS или Yocto). Причём новые бинарники неизменившихся зависимостей в итоговый образ не включались. Это необходимо было для того, чтобы Software Updates были поменьше, и чтобы на этапе тестирования этих updates выявить забытые version bumps из-за сломанного ABI.

    Полные ребилды делались только при апдейте тулчейнов.

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


    1. a-n-d Автор
      05.01.2024 11:45

      Полные ребилды делались только при апдейте тулчейнов.

      Не наш вариант, поскольку и libc тоже обновляется. А от этого компонента зависят все остальные => полный ребилд.


      1. dmitry_rozhkov
        05.01.2024 11:45

        Ну, если libc, то нужно почти всë ребилдить, конечно. Но она ведь далеко не на каждый PR обновляется. Это спасает.


        1. a-n-d Автор
          05.01.2024 11:45

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

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

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


          1. dmitry_rozhkov
            05.01.2024 11:45

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

            Позже один из инженеров написал скрипт, который скрейпил билдсреду на предмет опций компилятора, линкера, еще чего-то, что сейчас входит в стандарт buildinfo с reproducible-builds.org и сохранял вместе с бинарниками. Это упростило обнаружение коллизий и сократило количество билдов.


            1. a-n-d Автор
              05.01.2024 11:45

              Видимо, проблему апдейта устройства вы решаете перефлэшиванием образа целиком

              Почти. Учитывая, что на борту у команды тестирования в единицу времени сразу несколько стабильных релизов разных годов выпуска и пачка актуальных nightly билдов, для контроля регрессий приходится начисто накатывать конкретный дистрибутив на весь тестовый парк. Основное отличие в том, что за счет микроядерности у нас загрузочный образ очень маленький (сетевой стек, sshd, файловая система и пара файловых утилит [fdisk, mkdir, cp, etc]) и грузится на всём парке по сети. Всё остальное просто заливается в переразмеченную с нуля корневую FS в виде тарболов. В соседней статье ребята небольшую вводную на этот счет делали.

              Кроме пары почтенных девайсов, вроде старенького Ateml, апгрейд проходит не более 20 мин. в худшем случае. В среднем около 7 мин.

              Позже один из инженеров ...

              С трудом могу представить безболезненную реализацию кейса, когда, например, какой-то проект, вроде zlib, через свой генерируемый config.h внезапно декларирует/отключает какие-то фичи, а зависимый переконфигурируется динамически на основе детектирования именно этой фичи. И далее по цепочке.

              Учитывая, что многие проекты живут на разных рельсах -- нативная система сборки, ванильный autotools, cmake, meson, qmake, pkg-config ... -- простым такой анализ не будет. И априорно без пересборки словить такие build-time отличия, имхо, сочинить интерпретатор для всех этих тулов.

              А если и так и так пересобирать, то профит не совсем ясен. Коллизия всплывёт либо на этапе компиляции/линковки, либо уже на этапе стат. анализа/тестирования/фаззирования и прочего динамического анализа.


              1. dmitry_rozhkov
                05.01.2024 11:45
                -1

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

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