Жизненный цикл программного обеспечения

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

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

Основные стадии жизненного цикла

Жизненный цикл ПО состоит из семи стадий:

  1. Анализ. Выясняется, что именно требуется построить.
  2. Проектирование. Определяется, как это будет устроено.
  3. Разработка. Пишется код.
  4. Тестирование. Проверяется, что получилось именно задуманное.
  5. Релиз. Продукт доводится до пользователя.
  6. Поддержка. Сопровождается работающая система.
  7. Завершение жизненного цикла. Продукт снимается с поддержки.

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

Анализ и планирование

На этапе анализа собираются требования к будущему продукту и сводятся в документ Software Requirements Specification (SRS).

IEEE 830-1998

Структуру такого документа задавал стандарт IEEE 830-1998, отменённый и заменённый на ISO/IEC/IEEE 29148. Предложенная им структура сохранилась:

  1. Назначение
  2. Общее описание
  3. Конкретные требования
  4. Интерфейсы
  5. Ограничения
  6. Атрибуты качества

Ключевые задачи этапа анализа

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

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

Проектирование

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

Design-review

Прежде чем писать код, замысел, оформленный в пару страниц, показывают коллегам. На design-review разбираются:

  • что планируется сделать;
  • мотивация, то есть цели, заказчики и пользователи;
  • сравнение с уже существующими аналогами;
  • roadmap.

Самым полезным пунктом является третий. В науке инструмент, уже написанный чужой группой, обнаруживается примерно в половине случаев, и полчаса поиска экономят полгода работы.

Security-review

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

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

MVP (Minimum Viable Product)

Минимально жизнеспособный продукт — самая маленькая версия, которой уже можно пользоваться. Смысл заключается в том, чтобы как можно раньше выяснить, тот ли продукт создаётся. Для лабораторного инструмента MVP обычно выглядит так: одна команда, один формат входных файлов, никаких настроек. Она вычисляет задуманное, и её можно передать коллеге.

Разработка

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

Системы контроля версий

Система контроля версий (VCS) хранит историю изменений с указанием того, кто, что и когда правил, и позволяет вернуться к любому прошлому состоянию. Решать эту задачу начали в 1960-х с IEBUPDTE, затем были SCCS в 1970-х, RCS и CVS в 1980-х, а SVN и Git появились в 2000-х.

Поколения различаются тем, где хранится история. Централизованные системы (CVS, SVN) хранят её на сервере, и без связи с ним нельзя ни просмотреть историю, ни сделать коммит. Распределённые системы (Git, Mercurial) дают каждому участнику полную копию репозитория со всей историей, поэтому работать можно без сети, а синхронизироваться впоследствии.

Стандартом де-факто является Git, написанный Линусом Торвальдсом в 2005 году для разработки ядра Linux, когда команда лишилась доступа к использовавшейся до того проприетарной системе. Главной его особенностью является дешёвое ветвление. Создание ветки ничего не копирует, заводится лишь указатель на коммит, поэтому ветвиться и сливать можно постоянно. Устройству Git и повседневным командам посвящена отдельная глава.

Вокруг ветвления сложились схемы работы. Git Flow поддерживает долгоживущие ветки релизов, более простой GitHub Flow сливает всё в основную ветку через pull request'ы, принятые ревьюером; между ними находится GitLab Flow. Выбор определяется тем, как часто выходят релизы. Научной группе почти всегда достаточно самой простой схемы.

Ревью кода

Pull request (или merge request) — предложение влить в общую ветку сделанные разработчиком изменения. Он решает три задачи: делает правку видимой для всей команды, предоставляет место для обсуждения до того, как код попал в основную ветку, и служит точкой, к которой привязываются автоматические проверки.

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

Монорепозиторий

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

Лаборатории разумно держать в одном репозитории всё, что выпускается и версионируется вместе, и разносить части, развивающиеся независимо.

Тестирование

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

Проверки выстроены пирамидой, от самых дешёвых и частых к самым дорогим и редким.

Статический анализ

Самая дешёвая проверка не запускает программу. Анализатор читает исходный код и ищет в нём ошибки, отклонения от стиля и подозрительные места. Так обнаруживаются неиспользуемые переменные, опечатки в именах, потенциальные None, измеряются цикломатическая сложность и доля дублированных строк. Покрытие тестами также попадает в отчёты о качестве, но получают его иначе, выполняя тесты под coverage.py (pytest --cov): узнать, какие строки выполнялись, не выполнив их, нельзя.

Анализатор, встроенный в редактор и в CI, в дальнейшем работает самостоятельно.

«Самое важное, что я сделал как программист за последние годы, — начал агрессивно применять статический анализ кода» — Джон Кармак

Модульные тесты

Модульный тест (unit test) проверяет одну функцию или класс, отделённые от всего остального. Их пишут много, они выполняются за секунды и запускаются на каждое изменение.

Для научного кода это самая ценная разновидность тестов. Формула, проверенная на случае с известным аналитическим ответом, остаётся проверенной и после того, как через полгода её начнут оптимизировать.

Интеграционные и функциональные тесты

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

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

Нагрузочное тестирование

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

ПрофильЧто проверяет
Performance testingкак меняются задержки при постепенном росте нагрузки
Load testingвыдерживает ли система штатную нагрузку долгое время
Stress testingчто происходит за пределами расчётной нагрузки
Spike testingпереживает ли система резкий всплеск
Warm-up testingкак система ведёт себя сразу после запуска, пока кеши пусты

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

Релиз

Главной заботой при выкатке является возможность быстро откатиться в случае поломки.

Стратегии развёртывания

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

  • Blue-green. Поддерживаются две одинаковые среды. Новая версия выкатывается на неактивную, проверяется, и трафик переключается одним действием. Откат — переключение обратно.
  • Canary. Новая версия сначала показывается небольшой доле пользователей. Если метрики не ухудшились, доля увеличивается.
  • Rolling update. Экземпляры приложения заменяются по одному, сервис всё это время продолжает отвечать.
  • A/B-тестирование. Две версии работают одновременно на разных группах, и выбор между ними делается по измеренному результату.

Feature-флаги

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

Момент выпуска

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

CI/CD

Перечисленные проверки выполняет конвейер CI/CD. За тремя похожими аббревиатурами скрываются три степени автоматизации.

Непрерывная интеграция (CI) — минимальный уровень. Код хранится в системе контроля версий, каждый push запускает сборку, модульные тесты и статический анализ, изменения проходят ревью. Поломка обнаруживается через минуты после появления, а не через недели.

Непрерывная поставка (CD, Continuous Delivery) добавляет автоматическую сборку дистрибутивов и развёртывание на тестовые среды. Система в любой момент готова к выкатке и ожидает решения человека.

Непрерывное развёртывание (CD, Continuous Deployment): прошедшие все проверки изменения попадают в продакшен без участия человека. Это требует уверенности в тестах.

Научной группе обычно достаточно первого уровня. CI, выполняющий на каждый push тесты и линтер, окупается на второй месяц.

Устройство реального конвейера

Ниже приведён конвейер научной библиотеки REDPIC, кода для расчёта динамики пучка, которому отведена отдельная глава. Написанный для GitHub Actions, он умещается в одну страницу.

Первая работа проверяет качество кода. Она запускается на каждый push и на каждый pull request. Подготовка: получить исходники, установить нужную версию Python, установить зависимости.

steps:
  - name: Checkout sources
    uses: actions/checkout@v4

  - name: Set up Python
    uses: actions/setup-python@v5
    with:
      python-version: ${{ matrix.python-version }}

Строка ${{ matrix.python-version }} задаёт матричную сборку. Один и тот же набор шагов выполняется на нескольких версиях Python, и если библиотека перестала работать на 3.12, но работает на 3.10, это выясняется сразу. Для библиотеки, передаваемой незнакомым людям, такая проверка обязательна: разработчик не управляет тем, какой Python установлен у пользователя.

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

  - name: Install deps
    run: |
      python -m pip install --upgrade flake8 pylint
      python -m pip install -r requirements.txt

  - name: Run flake8
    run: python -m flake8 redpic tests setup.py

  - name: Run pylint
    run: python -m pylint redpic tests setup.py

Проверяется не только код библиотеки, но и tests с setup.py. Тесты также являются кодом, и приходят в негодность они не реже основного.

Вторая работа выполняет тесты:

  - name: Run functional test
    run: python -m pytest tests/functional/src

Третья работа выпускает релиз. Она запускается только на поставленный автором тег версии. Собранный пакет отправляется в PyPI и становится доступен по pip install redpic:

  - name: Publish distribution to PyPI
    uses: pypa/gh-action-pypi-publish@release/v1

  - name: Upload Release Asset (sdist) to GitHub
    uses: softprops/action-gh-release@v2
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    with:
      files: dist/${{ env.name }}

Пароли и токены в конвейере никогда не записываются открытым текстом: они помещаются в хранилище секретов репозитория и подставляются по имени (secrets.GITHUB_TOKEN). Секрет, закоммиченный в репозиторий, считается скомпрометированным навсегда: история git хранит всё, и удаление следующим коммитом ничего не исправляет.

Каждое чужое действие — код, выполняемый в конвейере разработчика с доступом к его секретам, поэтому у него должен быть указан тег или хеш коммита. Запись вида @master означает «взять из чужой ветки то, что там находится сегодня», и подменить это может любой, имеющий доступ к тому репозиторию. В примере выше @release/v1 — также ветка, однако PyPA поддерживает её под стабильные выпуски и сама рекомендует такую запись; для строгости лучше указывать хеш коммита.

Четвёртая работа собирает документацию из исходников, публикует её на GitHub Pages и прикладывает PDF-версию к релизу:

  - name: Upload Release Asset (pdf) to GitHub
    uses: softprops/action-gh-release@v2
    with:
      files: manual/main.pdf

  - name: Push documentation changes
    uses: peaceiris/actions-gh-pages@v4
    with:
      github_token: ${{ secrets.GITHUB_TOKEN }}
      publish_dir: gh-pages

Одного тега в git достаточно, чтобы появилась новая версия библиотеки, устанавливаемая через pip, с собранной документацией и PDF-руководством, приложенным к релизу. Ни одного действия вручную, а следовательно, ни одной возможности пропустить шаг.

Это главный аргумент в пользу CI/CD в науке. Релиз, собираемый вручную, рано или поздно окажется собран не из того коммита, без одного теста или с забытым файлом, и обнаружится это через полгода, когда результат уже опубликован.

Задание. Выступить в роли рецензента: «Ревью чужого кода».

Сопровождение

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

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

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

Завершение жизненного цикла

Рано или поздно программу перестают поддерживать. Об этом следует объявить заранее, дать пользователям время и путь перехода на замену, а данные выгрузить в формате, переживающем сам сервис.

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

Резюме

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

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