Быстрая, открытая и безопасная 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

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

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

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

Мы оставили 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

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

Одна часть этих 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

- Основная идеи оптимизации 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 нового дня.

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

С 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% компиляции имеет доступ к этому артефакту и оно решилось само собой на решении предыдущей проблемы, потому что оверлей разрешает мультидоступ к файлам. Без этого, два джоба, которые обращались к одному и тому же кэшу, не могли делать это одновременно.
Про кэширование

У 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, очень обрадовались, теперь это занимает меньше получаса, вместо часа.
Про будущее

Мы решили улучшить транспорт, подсмотрели у опенсор-проектов 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/
