Быстрая, открытая и безопасная CI для больших проектов на Rust

Денис Писарев из Parity Technologies провел хардкорный доклад на Polkadot Weekend про разработку на Rust, где рассказал все про CI, кэширование, контейнеры, docker и прочие слова на языке девелоперов. Сделали конспект его доклада.

Бэкграунд спикера

Живу в Берлине и работаю в Parity Technologies уже четыре года, мне нравится. Сейчас на позиции DevOps Teamlead. В команде у нас семь человек и мы сфокусированы только на CI: хостами, кластерами, мониторингом, конфигурациями CI, делаем экспертизу по всему, что касается контейнеров. Расскажу о том, как моя команда приближала время компиляции CI к горячему кэшу на локальной машине (то есть — к очень быстрой компиляции) и как безопасно разрешить компилировать случайным людям на своем железе.

Про разработку на Rust

У Parity когда-то был один продукт — Parity Ethereum, сейчас это Open Ethereum и он написан на Rust. Это стало стандартом для майнеров и полных нод Ethereum незадолго до того, как я присоединился к компании. Он был быстрее и надежней нативного клиента Geth, даже Ethereum Foundation начали оптимизировать свои тесты под Parity. Но если говорить про супер быстрый бинарник Parity Ethereum — с ним было все классно, кроме времени компиляции в 45+ минут. Первое, что я сделал на своей новой работе — разобрался, почему так долго и снес кэш. И компиляция ускорилась на 30 минут. Не все знают, зачем вообще нужен кэш и как им правильно пользоваться.

В чем проблема с кэшем в Gitlab

У GitLab тоже была такая проблема. Их встроенный кэш нужен был, чтобы зазиповать кучу маленьких файлов с помощью процессора и это очень долго, потом отправить на координатор, скачать нужным раннером (потому что в GitLab специальная система управления очередями) и потом раззиповать. И так происхожит в каждом джобе с кэшом. На то время кэш Parity весил 1.5-2 гб проходя эту процедуру занимал 30 минут в каждом джобе, соответственно убрав кэш, я ускорил время компиляции до 15 минут с 45. Боттлнек (bottleneck) здесь был в архивировании/разархивировании и именно поэтому мы не могли пользоваться GitLab кэшем.

Через три месяца я поехал на Европейский фестиваль опенсорс разработки и встретил там менеджера GitLab. Он очень помог и приоритизировал все issues, которые я ему показал. В целом им интересно было помочь Rust-комьюнити. В итоге они поменяли зипование на tar, что не тратило ресурсов компьютера. Но исправили не все: не сделали алгоритм управляемым, что оставило текущую боттлнек-сеть. Так же как и то, что каждая загрузка происходит с новым джобом. Я изучил все современные, на тот момент, CI-системы и ни одна не подошла. То нельзя хостить раннеры, то раннеры слишком негибкие в конфигурации, то несовместимы с ПО.

Требования к CI

alt_text

Я сформировал требования к CI:

  • обязательная поддержка контейнеров (минимально Docker/ в идеале Podman потому, что можно использовать свои контейнеры);
  • кэширование контейнеров на build-host, иначе скачивать image на каждый джоб очень дорого;
  • предпочтителен self-hosting — это девопс-мантра всего Parity, кроме Cloud VPS;
  • управляемое кэширование;
  • не Jenkins.

Про контейнеры

alt_text

Мы придерживаемся таких правил:

  • только свои images или от известных вендоров — это требование по безопасности;
  • предназначение image — область задач, не микросервис, то есть обычно нужно очень много images и делать их под каждый джоб было бы странно и сложно поддерживать работу такой системы;
  • fat images are ok — наши images занимали 1,5-2 гб под разные проекты, и это нормально. Главное, чтобы они были «слоенными», то есть чтобы нагрузка image была распределена между слоями. Такой толстый image покрывал бы все необходимые требования Polkadot, Substrate и т.д.
  • Rootless, чтобы никто не убежал из экзекьюшн-контейнера во время выполнения компиляции;
  • Они должны ежедневно сканироваться, тестироваться и ребилдиться.

alt_text

Про GitLab

Под эти требования подходит GitLab, который у нас уже был в программе GitLab for opensource. Много раз скажу сегодня про GitLab, но это не значит, что я им полностью доволен. Просто, он единственный подошел под наши задачи. С другой стороны — у них 40К открытых курируемых issues в репозитории GitLab. Но у меня есть список раздражающих штук в этом проекте. Еще нам немного подходит Github actions, но скорее нет. Но все любят Github, поэтому мне не удалось полностью перевести разработку на GitLab. Еще у GitHub есть удобные actions, которые мы вполне используем и даже написали несколько своих. С кэшем он обращается вполне неплохо.

К сожалению, GitLab не подходит для опенсорса. И главная проблема в самой discoverability. В Github отличное комьюнити и люди, имея профили, могут контрибьютить в любой опенсорс-проект, и эти котрибьюшены будут видны во всем Github. В GitLab такого нет.

Про GitHub

alt_text

Мы оставили GitHub Actions для маленьких проектов, для больших он не подходит. Поэтому там могут работать 1-2 разраба. Часто они пользуются нашей вики про ghost practices CI.

Проблемы GitHub:

  • ограниченный кэш — если кэш переростает 10 гб, кэш GitHub Actions начинает удаляться. Это ограничение нельзя обойти даже через селф-хостед раннер;
  • нет возможности управлять кэшем — он всегда загружается через Internet vager в Microsoft Cloud даже с селф-хостед раннером;
  • не хранит docker кэш, что само по себе сильно удлиняет джобы каждый раз.
  • можно иметь собственные vm в Azure, но для этого надо глубоко забираться в vendor lock-in;
  • не разрешает секреты для внешних пайплайнов — у нас есть джобы, которые используют секреты, но они не запустятся для внешнего PR на GitHub Actions.

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

Про безопасность open pipelines

alt_text

Весь код, который разрабатывает Parity — опенсорс. Мы (то есть наши две команды девопсов) открываем даже инфраструктурный код, кроме всяких скучных штук вроде списка юзеров и т.д. Для основных проектов мы проповедуем open pipelines. Те, кто контрибьютят, создают внешний PR и получают точно такой же pipeline как и core-девелопер. И все это на нашем железе, иначе внешние разработчики получат меньше доступа, а это нежелательно. На самом деле, к коду внешних разработчиков мы предъявляем больше требований. Например, они должны подписывать соглашение на лицензирование его кода. Без этого очень сложно быть опенсорс.

Есть нюанс с безопасностью. У нас есть части pipeline, которые билдят, деплоят что-то куда-то. Например, мы собираем бинарник, и его нужно тутже проверить. Для этого должны быть какие-то пароли, которые будут проходить внутри pipeline, который, в частности, запускается и для внешних контрибьюторов. Эти пароли можно потенциально выудить через логи, тут возникает проблема. Не использовать в обычных pipeline серьезных паролей, можно сделать docker registry. Пароли можно менять каждый раз, лучше этот процесс автоматизировать.

Про безопасность контейнеров

alt_text

Одна часть этих images исполняется Kubernetes, мы применили методы для минимизации attack surface. Основная тяжелая компиляция у нас происходит на bare metal host. Там мы применили docker seccomp policies и security compliance. Все хосты получают постоянное обновление типа unattended upgrades. Все возможные желания хостов лучше мониторить и исполнять вовремя. Это базовое требование по безопасности, когда на твоем железе какие-то внешние люди исполняют свой код. По сути, любой CI — список скриптов с конфигом, который делает что попало. Несколько лет назад на GitHub было модно запускать майнеры на всех опенсорсных проектах, где это было возможно. GitHub тогда предпринял меры и запретил использовать секрет по внешних PR. При этом, сам репозиторий GitHub, то есть — код GitHub.com использует более интересную тактику: у них есть специальный action, предваряющий весь pipeline, который проверяет новый PR на файлы, которые никому нельзя редактировать, CI-конфиги и скрипты, которые не запускаются. Даже в середине PR запускается guard job проверяет коммит и фейлит весь pipeline. И только команда CI или команда админов репозитория может менять эти файлы.

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

Оптимизация CI

alt_text

  • Основная идеи оптимизации CI в том, чтобы исключить ненужные действия, скачивания, установки и прочие шаги из каждого джоба. GitHub Action нарушает эту заповедь почти постоянно. Можно сделать это все в разы быстрее. Эту проблему решают наши fat images, в которых есть сразу все и ничего не надо устанавливать перед каждым джобом.
  • Сделать Reproducible Environment Image — это воспроизводимые среды. Контейнер, в котором можно запустить любые джобы по сборке и тестам. Если такой контейнер положить в корень репозитория и опубликовать image, то внезапно можно обнаружить, что им пользуются люди. Если добавить к нему read me и описать, как собрать свой image — становится намного меньше вопросов. Это уже собственный девопс продукт для cv. Контейнер будет толстый, зато слоенный — три слоя, обновляется обычно только верхний слой.
  • Обновляем каждую ночь контейнеры, но они публикуются, только, если vulnerability scanner прошел на зеленый, иначе мы получим предупреждения и прийдется что-то откатывать. Здесь я рекомендую Trivy — опенсорс-сканнер репозиториев. У него самая широкая база уязвимостей. Как только image просканировался, нужно запустить pipelines: Polkadot Substrate, репозитории и т.д. на новом image. Если все проходит успешно, image попадает в CI Linux production и становится image нового дня.

alt_text

  • Используйте DAG вместо stage. Теперь не нужно ждать завершения самого длинного джоба, мелкие джобы идут параллельно с длинными. К финишу они приходят примерно одинаково, но если какой-то из тестов ломается, то весь pipeline закрывается.
  • Не жалейте денег на Rust-компиляцию, так как она расходует много ресурсов, в частности, на работу процессора. Лучше уткуться в другие боттлнеки, например сеть.
  • Настройте уведомления и метрики CI. Не всегда очевидно, какой джоб занимает больше времени, какой фейлится несколько раз подряд, например. Для GitLab нам пришлось разработать уведомления и мониторинг самим.
  • Важно рассказать разработчикам, как работает CI. Тогда они сами начнут контрибьютить, только не забудьте поставить себя во владельцы кода. Иногда нужно синкаться с разработчиками и все уточнять. В больших проектах экспертиза распределена. Например, Polkadot и Substrate — самые большие кодовые базы на Rust в опенсорсе. Комагда девопс просто не может знать всех деталей.

Проблемы с Rust

alt_text

С Rust мало проблем, но они есть.

  • Reproducible builds — классная вещь, которая помогла бы релизиться всем на свете. Но это на Rust не работает. Конкретно эту фичу от Rust ждут NASA, Еврокосмическое агенство. Но в сообществе Rust своя атмосфера, они не хотят идти по стопам Java и как от огня бегут от денег, предложенных за имплементацию отдельных фич. Мы допилили проект, который сильно поможет нам слинковать большую часть бинарника Polkadot статично и это ускорит релизные процессы и избавит бинарник от внешних зависимостей.
  • Компилятор — основная проблема. Локально компилятор работает примитивно, на своей машине Rust компилится довльно хорошо, используя кэш. В распределенных системах кэш не работает, в CI-системах все не так. В связи с этим есть несколько багов. Mtime fingerptinting — cargo использует linux mtime (время создания и редактирования файлов, чтобы понять нужно ли ему перекомпилировать юнит), проблем локально нет, но это становится проблемой в CI-средах, которые создаются с нуля для каждого джоба. GitHub с нуля запускает EVM, в которой ничего нет. Время создания всех файлов датируется началом джоба. И это проблема Rust. В cargo нет другой стратегии fingerprinting. В репозитории компилятора существует PR, чтобы это починить но он встал. Когда CI готовится для выполнения задания, она клонирует репозиторий в среду с mtime, установленным на момент клонирования набора данных — когда создалась EVM и когда туда добавили проект. Fingerprinting начинает помечать каждый файл компиляции как грязный. Мы придумали инструмент, который восстанавливает файлы до их первоначальной модификации и меняет время файлов искусственно.
  • Full path для cargo_target_dir и cargo_home — этот путь необходим, иначе все инвалидируется. Если путь, по которому кэш был создан, не совпадает с путем артефактов, которые я скопировал откуда-то, то их инвалидирует. Это нельзя было обойти, пока мы не придумали его класть в docker image. В итоге мы пришли к выводу, подсовывать в контейнеры кэш в виде оверлея.
  • Это эксклюзивный лог на артефакты кэша — только 1% компиляции имеет доступ к этому артефакту и оно решилось само собой на решении предыдущей проблемы, потому что оверлей разрешает мультидоступ к файлам. Без этого, два джоба, которые обращались к одному и тому же кэшу, не могли делать это одновременно.

Про кэширование

alt_text

У cargo всего три типа кэша:

  • cargo_home — зависимости, репозитории, скачанные архивы, артефакты компиляции;
  • cargo_target_dir — артефакты компиляции всего проекта, в случае, Substrate — 4-7 гб на одну компиляцию;
  • cargo_incremental — инкрементальная фича, которая работает на локальной машине, но больше ни для чего не работает, важно ей присвоить значение 0.

Когда всех этих багов еще не было, появился Mozilla sccash. У них в офисе много десктопов, которые никогда не выключались, они придумали использовать часть этих машин для распределенной компиляции и использования кэша локально. Мы стали использовать это в CI, в этот момент я понял, что fat images — это нормально. Потом я стал класть кэш в стогиговую оперативку Redis, это сделало еще быстрее процессы билдов на 30-70%. sccash не решал проблемы багов. Мы решили форкнуть его и все починить, так появился проект Cashpot, который решает многие проблемы. Но Mozilla это увидели и мы будем дальше поддерживать sccash вместе с ними.

Еще у нас есть инхауз проект Rusty-cashier, он нацелен на распределение кэша между хостами. Мы не знаем, в рамках GitLab, какой хост получит следующий джоб. Он оптимизирует транспорт для кэша, хранение на хостах и доствка в компилятор. Он опирается на спецификацию механизмов распределения джобов в GitLab CI. И каждый участвующий раннер получает архивы кэша для включенных в него проектов из единого централизованного экземпляра репозитория через http, внутри dot центра. Хранение кэша устроено на файловой системе — планировщик распаковывает архивы кэша по известным путям на хостах раннера, чтобы клиент мог использовать их позже в джобах в качестве источника кэша. Кэш предоставляется cargo через docker overlay на основе файловой системы Linux в виде short lived с сохранением пути. Таким образом кэш можно переиспользовать. Мы храним разный кэш для разных веток проекта и Rust, разных джобов. Таким образом можно откатиться на несломанную версию Rust и все будет ок.

Мы упираемся в скорость сети дата-центра, поэтому обновляем кэш тяжелых проектов только на мастере по ночам, но это уже дает прирост к производительности CI от 70% до 90%. Все, кто замучился компилировать на Substrate, очень обрадовались, теперь это занимает меньше получаса, вместо часа.

Про будущее

alt_text

Мы решили улучшить транспорт, подсмотрели у опенсор-проектов Uber и Alibaba Cloud, они используют свои docker registry. Внутри системы шэрят кэш docker через P2P-транспорт, типо торрента. Недоскаченный кэш, image залочен и не может использоваться. Это оптимизирует топологию сети, исключает необходимость централизованного хранения кэша.

  • Кэширование веток вместе с rusty-cashier, не только мастера, но и другие бранчи. Это станет возможным, если договориться с дата-центром, что все компиляционные хосты положат в одну ноду. И сеть станет настолько быстрой, насколько возможно.

  • k8s’ Persistent Volume — это позволит bare metal float переехать в k8s’. Этот вариант убирает все боттлнеки, оставляя потенциальные data race conditions для доступа к Persistent Volume без дубликации кэша. Кластер мы обкатываем в Google k8s’ Engine. Такое решение подходит к большинству альтернативных CI и мы не будем привязаны к GitLab. Или будет хостить свой k8s’

  • Относительно ssccash / cashpot — нужно придумать распределенную компиляцию. Она находится в работе.

  • cago build --timings — аутпут, который показывает таймлайн, как собирается проект в Rust. Распределенная компиляция взяла бы эти задачи на себя. Например, можно представить, ферму с клиентами. Это позволит юзать легкие клиенты для вычисления своей части бинарника, вместо тяжелых вычислений. Хосты должны будут попасть в white list, сдать fingerprint и тогда мало, у кого будет желание что-то испортить.

Полезные ссылки

https://github.com/aquasecurity/trivy

https://github.com/paritytech/scripts

https://about.gitlab.com/solutions/open-source

https://github.com/mozilla/sccache

https://github.com/paritytech/cachepot

https://nexte.st/ - run Rust tests in parallel

https://github.com/TriplEight/documents

Альтернативные build системы

github.com/buildbuddy-io/buildbuddy

buildbuddy.io/docs/config-cache/

bazelbuild.github.io/rules_rust/

bazel.build/community/remote-execution-services

Читайте также

Больше мероприятний