Я проработал в Stripe около семи лет, с 2012 по 2019 год. За это время я применял и создавал многие поколения среды разработчика инструментов Stripe, которые инженеры использовали для написания и тестирования кода каждый день. Я считаю, что Stripe проделала очень хорошую работу по разработке и построению такого опыта для разработчиков, что после ухода неоднократно рассказывал об особенностях этой среды друзьям и коллегам.
Этот пост — попытка описать основные особенности среды, какой я ее помню. Я также попытаюсь поразмышлять о контексте, ограничениях и мотивах в выборе решений; думаю, что с точки зрения контекста это был хороший выбор, инженеры были глубоко информированы о бизнес- и техническом контексте, и каким-то другим командам требуются варианты.
И кое-какие предостережения. Прошло почти пять лет, и я не сомневаюсь, что неправильно запомнил некоторые детали, хотя в общей картине уверен. А еще уверен, что Stripe продолжает развиваться, и я не утверждаю, что этот текст отражает опыт разработчиков Stripe сегодня.
Кроме того, хотя я внес свой вклад во многие компоненты, которые описываю, я не могу и не пытаюсь претендовать на какую-то всеобъемлющую заслугу. Все, что здесь представлено, создавалось годами и при участии многих талантливых инженеров, как при разработке концепции, так и при ее реализации.
Контекст Stripe
В то время большая часть кодовой базы Stripe была написана на Ruby и жила в одном большом монорепозитории. Эта кодовая база поддерживала множество сервисов, активно использующих общий код. У Stripe также было большое, растущее количество сервисов за пределами этого монорепозитория и на других языках, но они (в общем) были не так важны для бизнеса. В частности, API Stripe почти полностью состоял из одного сервиса Ruby в монорепозитории. Инструментарий и опыт разработки были задуманы и построены в первую очередь для того, чтобы поддерживать разработку в монорепозитории. Другие сервисы и языки поддерживались между прочим, если вообще поддерживались.
Инструменты Stripe создавались и поддерживались рядом команд и отдельных людей, но я буду называть их авторов и владельцев в совокупности командой «продуктивности разработчиков» (для краткости — devprod). Так называлась эта команда в последний раз за несколько лет моего пребывания в Stripe.
Вклад в инструменты
Первоначально Stripe сформировал команду, занятую внутренними инструментами и производительностью. Впоследствии она стала командой, создавшей эти инструменты, — довольно рано, еще в мое время. За последние три или четыре года она существенно выросла и по-настоящему раскрыла свой потенциал. В эту команду всегда входили отличные инженеры. В том числе несколько старших разработчиков микросхем. Думаю, более любого другого технического решения, о которых я скажу, основными движущими силами всех успехов разработки Stripe были само существование команды, ее вклад в стабильность и надежность инструментов разработчика. Последние качества обслуживались командой чуть позже, как только она выросла достаточно, чтобы незамедлительно решать проблемы, планировать и делать свой вклад. Безусловно, технический выбор и инженерия важны, но чтобы работать, они должны поддерживаться достаточным числом занятых людей и соответствующим текущим сопровождением.
Эта надежность и стабильность среды, особенно по мере роста и развития команды, будут повторяющейся темой. Лучшие в мире инструменты изо всех сил пытаются компенсировать «регулярную потерю дня на отладку среды», поэтому разрешение проблем этой стороны может легко перевесить почти любые решения. Многие решения по дизайну принимались, чтобы позволить команде разработчиков легче и последовательнее мониторить и поддерживать инструменты, давать возможность отлаживать код и решать проблемы централизованно, а не навязывая их отдельным инженерам.
Архитектура среды разработки
Код в разработке, выполняемый в облаке
Определяющий для среды разработки вопрос: выполняется ли код разработки локально на ноутбуке разработчика, или его необходимо запускать удаленно, на экземплярах или контейнерах внутри среды, предоставляемой централизованно?
Многие годы инженеры Stripe применяли оба варианта, в зависимости от предпочтений разработчиков, своеобразных решений и подробностей о том, что именно, в какой среде и в какой момент времени работало. Когда команда devprod решила вкладываться в единую среду, мы остановились на поддержке экземпляров для каждого разработчика («devbox’ах») в облачной среде Stripe, за пределами продакшна. Во время разработки код запускался в devbox и для тестов из minitest, и для интерактивного тестирования, экспериментов.
Эти инстансы разработки были предоставлены стандартными инструментами управления конфигурацией Stripe, они были эфемерными: одна команда могла уничтожить ваш экземпляр и предоставить новый (сохранялось несколько прогретых частей, что обычно очень ускоряло операцию). В целях управления инструментарием реестром отслеживалось, какой экземпляр и у какого инженера активен. Все devbox были доступны всем инженерам по ssh. Это упрощало совместную работу и устранение проблем среды.
Эта архитектура означала, что большинство проблем с конфигурацией среды можно решать централизованно — группами разработчиков инструментов или владельцами сервисов, а разработчикам в большинстве случаев не приходилось беспокоиться об обновлении собственных сред разработки, чтобы не отставать.
Включение новых зависимостей
Stripe постоянно добавлял и переделывал зависимости для API или других сервисов; например, для реализации ограничения скорости в API требовался кластер Redis. В мире, где код разработки выполняется на ноутбуках, это означало, что на каждом ноутбуке теперь потребуется установленный и потенциально сконфигурированный Redis. Обновить конфигурации на ноутбуках разработчиков было сложно, и часто из уст в уста передавалось: «Эй, я вытянул master и теперь получаю странную ошибку — кто-нибудь знает, как ее исправить?». После чего товарищ по команде отправляет соответствующую команду настройки. С другой стороны, вызов brew для установки Redis мог проскользнуть в случайном скрипте, который разработчики регулярно запускают в надежде решить проблему, также в хрупком месте и приводящую к замедлению работы [другого] случайного инструмента.
При использовании модели devbox команда, добавляюшая Redis, уже отвечала за его конфигурацию в продакшне, и они могли добавить соответствующую конфигурацию Puppet, чтобы гарантировать, что она также будет установлена и запущена на devbox. В случае, если у пользователя возникнут проблемы с этим новым путем, члены этой команды или devprod смогут подключиться по ssh прямо к их devbox, отладить проблему, а затем напрямую обновить Puppet или исходный код Ruby, чтобы проблема не повлияла на других пользователей.
Большую часть моего пребывания в Stripe новые функции и изменения инфраструктуры постоянно приводили к новым зависимостям или изменениям конфигурации существующих зависимостей той или иной формы; стандартизация devbox облегчила работу этих команд и резко снизила вероятность того, что изменения в инфраструктуре поломают чужой опыт разработки. И то, и другое было крайне ценно.
Редакторы и контроль версий
Прежде чем запустить новый код в процессе разработки, к нему нужно получить доступ через систему контроля версий и отредактировать его.
В Stripe, несмотря на то, что код выполнялся в облаке, чекауты и редакторы Git располагались локально, на ноутбуках разработчиков. Stripe остановился на этом подходе по ряду причин, в том числе это:
- Поддержка широкого круга редакторов и сред IDE. Некоторые редакторы (например,
emacsилиvim) можно достаточно хорошо запускать через сеанс SSH, а некоторые (например, VS Code или emacs сtramp) поддерживают запуск локального пользовательского интерфейса на удаленной файловой системе, но многие редакторы этого не делают. Сохраняя код на ноутбуке, Stripe позволял разработчикам продолжать использовать любой редактор, который им хотелось. - Задержка. В Stripe трудились разработчики со всего мира, но в то время у компании еще не было существенной инфраструктуры за пределами США. Редактировать код по ту сторону [океана] с задержкой сети более 200 мс болезненно. Вариантом была установка devbox по всему миру, но это по многим причинам оказалось бы сложно.
- Долговечность исходного кода и производительность файловой системы. Сохраняя источник истины на ноутбуках и за пределами devbox, гораздо проще рассматривать среду выполнения как эфемерную и временную. Это свойство, в свою очередь, было очень полезно, чтобы поддерживать актуальность среды и предотвращать дрейф. Исходный код можно было бы хранить в доступном по сети хранилище отдельно от devbox (например, в EFS или на томе EBS), но, помимо стоимости сложности, эти варианты обладает относительно высокой задержкой, а исходный код имеет тенденцию зависеть (по крайней мере) от быстрого поиска метаданных файлов; операции
gitчерез NFS или EBS, как правило, выполняются крайне медленно, если без героической настройки и оптимизации.
Автоматическая синхронизация
Хранение кода на ноутбуках и его выполнение в облаке ставит новую проблему: как отредактированный код попадает с ноутбука в devbox?
Еще до того, как я присоединился к команде, в Stripe был скрипт «синхронизации», который объединял средство мониторинга файлов с rsync: он отслеживал изменения в локальных чекаутах и копировал их в devbox. Исторически сложилось так, что разработчики вручную запускали и отслеживали этот скрипт в отдельном терминале или окне tmux.
В конце концов, команда devprod взяла на себя ответственность за этот инструмент и вложила в него значительный труд, в основном с целью «отполировать», сделать его цельным и едва видимым для других разработчиков.
Примечательно, что они сделали синхронизацию неявной, без необходимости конфигурации или вмешательства: они работали с ИТ-командой над установкой скрипта синхронизации в качестве сервиса launchd на каждом ноутбуке разработчика, а также использовали вышеупомянутый реестр, чтобы автоматически находить правильный devbox.
Они также значительно вложились в надежность и самовосстановление в случае ошибок и проблем с сетью. Одно «внутреннее», но существенное улучшение заключалось в переходе на watchman для мониторинга файлов: при тестировании в Stripe это, безусловно, самый надежный мониторинг файлов, который мы обнаружили. Watchman значительно снизил частоту пропущенных обновлений или «застреваний» мониторинга файлов. Мы также смогли использовать некоторые продвинутые функции этого монитора. О них я расскажу немного позже.
Эти вложения во многом увенчались успехом: большинство разработчиков смогли относиться к синхронизации кода просто как к «факту из жизни», и им редко приходилось отлаживать ее или думать о ней.
Один из недостатков синхронизации — сложнее становится писать «код, действующий на код», например инструменты автоматической миграции или даже просто линтеры, инструменты генерации кода. В конечном счете Stripe полагалась на несколько генерируемых кодом компонентов, которые по разным причинам требовали проверки. На протяжении всего моего пребывания в должности это оставалось своего рода болевой точкой; мы обходились сочетанием стратегий:
- Работать на ноутбуке разработчика и решать проблемы среды.
- Запускать в devbox, а затем каким-то образом «синхронизировать сгенерированные файлы обратно». У нас был небольшой протокол для запуска скриптов в оболочке с обратной синхронизацией, они могли попросить скопировать файл обратно на ноутбук, но подход оставался несколько неуклюжим, неэргономичным, а иногда и ненадежным.
HTTP-сервисы в devbox
Большая часть кода Stripe выполняется внутри HTTP-сервисов, включая, в частности, Stripe API. Команда devprod создала инструменты для поддержки разработки этих сервисов, в том числе:
- DNS разрешал имена хостов по строкам
*.$username.stripe-dev.comв текущий devbox пользователя. - Сервис внешнего интерфейса запускался на каждом devbox, который завершал SSL, обрабатывал любую аутентификацию [nz] на основе клиентских сертификатов и сопоставлял имена сервисов соответствующему локальному экземпляру. Каждому сервису в Stripe статически назначался локальный порт, используемый интерфейсом для маршрутизации трафика. Например, сервису API назначался порт 3000, поэтому интерфейс пересылал
api.nelhage.stripe-dev.comнаlocalhost:3000. - Отдельный прокси-сервис прослушивал каждый из этих портов и по требованию запускал сервис поддержки на Ruby. Таким образом, при первом запросе к
localhost:3000сервис API запускался, а затем продолжал работать, чтобы обрабатывать будущие запросы напрямую. - Эти сервисы использовали автозагрузчик Stripe в devbox и, таким образом, загружали код Ruby только по мере необходимости, что приводило к быстрому запуску.
- Автозагрузчик также отслеживал, какие исходные файлы и в какой сервис загружались, следил за изменениями файловой системы; если загруженный файл изменился на диске, все использующие его сервисы автоматически перезапускались, чтобы принять изменения.
Конечный эффект такой инфраструктуры в том, что разработчики могли изменять код, а затем почти сразу же — без ручного перезапуска — получать доступ к копии сервиса. Там их новый код располагался по фиксированному URL-адресу, доступному для совместного использования внутри Stripe. Этот URL оставался стабильным, даже когда предоставлялся новый devbox. Кроме того, хотя эта функция работала почти для всех внутренних сервисов, каждый devbox запускал только те сервисы, которые на самом деле использовались конкретным разработчиком. Это снижало нагрузку на процессор и память.
Команда pay
Devprod создала и поддерживала инструмент командной строки pay, который предлагал унифицированный доступ к ряду функций и рабочих процессов devbox.
Доступ к devbox можно было получить через ssh (инкапсулированный как pay ssh), но pay также обертывал самые распространенные паттерны применения:
pay test...выполнял в devbox тестыminitest, под капотом используяssh, а также отправлял назад выходные данные и статус завершения локально;pay curlоборачивалcurlи предоставлял хелпер ручного запуска командcurl, для сервисов в devbox разработчика. Часто это было полезно при выполнении интуитивного ручного тестирования конечных точек API;pay typecheckоборачивал Sorbet, чтобы проверять типы в коде на devbox.
Барьеры синхронизации
Вдобавок к удобству эти инструменты предлагали еще одну ключевую особенность: интеграцию с процессом синхронизации исходного кода.
Команды pay, работающие с исходным кодом, взаимодействовали с процессом синхронизации и гарантировали, что перед удаленным выполнением кода синхронизация будет надлежащим образом «подхвачена». Эта функция стала критическим улучшением прозрачности синхронизации: исторически сложилось так, что обычным делом было редактировать файл, а затем запускать тест до завершения синхронизации. Таким образом, тесты запускались на старой версии кода, а вы верили, что тестируете новый код. Часто это приводило к ложному убеждению, что изменения не сработали, и могло привести к необдуманным действиям и огромному разочарованию.
Запуская рабочие процессы с ноутбука, подкоманды pay гарантировали, что они видят то же состояние файловой системы, которое видит и редактор. Вступая в сговор с синхронизацией, они могли гарантировать, что devbox увидит состояние «в данный момент или после».
Прямолинейный подход к такому «барьеру синхронизации» требует, например, чтобы pay test запускала новую синхронизацию. Это приводило к неприятным задержкам в рабочих процессах, даже если вы вообще ничего не меняли. Используя функцию Watchman clock, мы смогли добиться большего и избежать ненужных циклов обработки данных.
В обычном случае, когда инженер редактировал файл и запускал тест, временная шкала событий выглядела так:
- Пользователь редактирует и сохраняет файл.
pay syncпробуждается черезwatchman. Watchman делает отметку времени файловой системы и запускаетrsync.- Пользователь запускает команду
pay test. pay testпроситpay syncдождаться завершения синхронизации.- В ответ
pay syncсначала еще раз проверяет часы файловой системы. - Поскольку файловая система не изменилась с момента предыдущего сохранения, эта временная метка совпадает с меткой, связанной с текущей
rsync. pay syncтеперь знает, что, прежде чем уведомлятьpay test, ему нужно только дождаться командыrsyncуже в работе.
Использование часов watchman делает команду устойчивой к изменению порядка событий. Если вызов pay test произойдет до, во время или после синхронизации, все равно получается корректное поведение.
Эта координация pay sync также предоставила полезный инструмент наблюдения за синхронизацией. Ноутбуки разработчиков регулярно отключаются и снова подключаются к сети. Сбои синхронизации или даже длительные периоды ее задержки могут быть и нормальными, что затрудняет сбор полезной статистики ошибок из скрипта синхронизации. Однако если пользователь запускает команду pay, которая будет работать в devbox, это убедительно доказывает, что он ожидает работоспособности синхронизации. Так что если вызов барьера синхронизации в процессе синхронизации завершается неудачей или истекает время ожидания, то момент подходит, чтобы сообщить об ошибке в центральный трекер исключений Stripe. Отчет об ошибке позволял devprod отслеживать общее состояние системы синхронизации и опыт пользователя.
LSP и инструментарий редактора
Как упоминалось выше, инженеры Stripe могли использовать любой редактор по своему выбору, но к 2019 году devprod приняла решение вкладываться в VS Code. Они объявили редактор предпочитаемым и, в частности, вкладывались в инструментарий этой среды.
Часть этой работы состояла из внутренних плагинов с небольшими, но важными улучшениями качества жизни, такими как поддержка запуска pay test для конкретного теста под курсором.
Однако более существенные улучшения произошли, когда достиг зрелости и получил реализацию сервера LSP Sorbet — инструмент проверки типов Ruby от Stripe. LSP — протокол языкового сервера. Он определяет для сервера интерфейс, предлагающий функции, которые зависят от языка. Например, поиск определения или автодополнение кода. Эти функции могут использоваться различными редакторами.
Stripe настроил VS Code для запуска LSP-сервера Sorbet в devbox по ssh, общаясь через stdin и stdout, как локальный LSP-сервер. Это позволило Sorbet использовать большой объем оперативной памяти devbox. Серверы LSP, как правило, имеют тенденцию потреблять много памяти на больших кодовых базах, Sorbet не был исключением, а также позволило работать в среде Linux, где команде Sorbet было проще мониторить, отлаживать и тестировать.
В целом этот подход работал довольно хорошо. Серверы LSP обычно должны выдерживать некоторую задержку, поэтому дополнительная пересылка по сети и задержка из-за синхронизации файлов в большинстве случаев не создавали проблем. Например, протокол LSP предназначен для обработки крайне распространенного случая, когда пользователь внес изменения в редакторе, но еще не сохранил их на диск, поэтому редактор отправляет соответствующие изменения прямо на сервер. По сути, этот процесс также автоматически обеспечивает устойчивость к некоторой задержке при синхронизации файлов.
Размышления
По моему мнению, описанные здесь инструменты были довольно аккуратными, работали довольно хорошо и создали эффективный и продуктивный опыт разработки для многих инженеров Stripe. Здесь я хочу поразмышлять над некоторыми деталями организации и технического стека Stripe, которые, я считаю, сформировали инструменты. На эти области, вероятно, стоит обратить внимание, если вы работаете над инструментами разработчика в другой организации.
Масштаб организации
Эти инструменты разрабатывались в период, когда команда инженеров Stripe выросла с нескольких сотен до более чем тысячи человек. За это время devprod превратился из одного штатного сотрудника в команду, насчитывающую примерно десять инженеров. Большая часть того инструментария создана, когда в команде было 3+ инженера.
Этот масштаб — масштаб devprod и, в свою очередь, масштаб всей организации, такой, что она могла позволить себе 10 штатных сотрудников по инструментам — был основным фактором нашего выбора. Я описал множество довольно сложных, нестандартных инструментов; нам нужно было достаточно инженеров, чтобы создать и сопровождать его, а также достаточно инженеров-«клиентов», чтобы эти вложения окупились. Я еще раз подчеркну удобство сопровождения и надежность: соединить средство мониторинга файлов и rsync для синхронизации файлов с ноутбука легко — первая версия появилась в 2012 году и была реализована менее чем за день первых вложений. В конечном счете магия заключалось не в том, что такой инструмент существовал, а в том, что он был достаточно надежным и оставался таковым по мере роста и меняющихся потребностей. Пользователю не нужно было думать о нем.
Кроме того, Stripe быстро росла. В быстрорастущей организации гораздо важнее иметь среду разработки, которая сразу же «просто работает», с минимумом трудностей при настройке для новых инженеров. В более статичной организации инженеры могут изучить особенности инструментов и способы их обхода, и это в основном единовременные затраты. В быстро растущей компании эти затраты постоянно несет каждый новый сотрудник и обучающие их инженеры, а цена возврата к стабильности и бесперебойности растет.
Это свойство было важным компонентом модели devbox. Локальная разработка на ноутбуке имеет свои преимущества, но гораздо сложнее централизованно управлять средой или помогать пользователям в отладке, поэтому почти неизбежно нужно больше опыта и участия отдельных разработчиков. Перейдя к централизованным эфемерным боксам разработки, давайте централизуем большую часть сопровождения и первой рекомендацией отладки сделаем общий принцип «вытяните master и пересоздайте свой бокс разработки».
Кодовая база
Я уже упоминал, что эта инфраструктура поддерживает большой монорепозиторий с относительно согласованной архитектурой и паттернами как во времени, так и во всей кодовой базе. Стабильная цель создавала точки влияния, благодаря которым команда продуктивности разработчиков могла создавать общие инструменты, например, автозагрузчик и менеджер сервисов devbox. Они давали достаточно глубокую интеграцию со средой и кодовой базой, предлагали дружественные пользователю функции. В среде, содержащей гораздо большее разнообразие языков, сред выполнения и паттернов, нет смысла сильно специализироваться отдельно. Инфраструктурным командам необходимо создавать более обобщенные инструменты, рассматривающие пользовательский код как более прозрачный ящик.
Более того, по различным историческим, деловым и техническим причинам кодовая база Stripe была во многих отношениях довольно тесно связана; тем из нас, кто работал в команде devprod, было ясно, что его легко или быстро по каким-либо параметрам, например по большему или меньшему количеству микросервисов, не разложить. Мы постоянно вкладывались в инструменты и паттерны ради модульности и абстракции в монорепозитории, но уверенность в общей «липкости» структуры монорепозитория Ruby также оправдывала наши крупные вложения в специализированные инструменты.
Замечу, что даже в Stripe этот выбор был несколько спорным и не всегда единогласным. Несмотря на то, что большая часть кода и бизнес-ценности находилась в монорепозитории Ruby, у Stripe было большое количество других кодовых баз, на которых выполнялись компоненты инфраструктуры или части специализированных конвейеров. Они, как правило, получали меньшую поддержку со стороны групп по инструментам и инфраструктуре. Это было постоянным источником напряженности.
Тот факт, что монорепозиторий был написан именно на Ruby, также повлиял на многие наши решения в деталях. Например, компилируемый язык с более несвязным исходным кодом и артефактами мог бы подтолкнуть нас в другом направлении. Кроме того, монорепозиторий Stripe был (насколько нам известно) крупнейшей из существующих кодовых баз на Ruby, а это означало, что мы часто были предоставлены самим себе, что существующие инструменты так или иначе поддерживали наш масштаб с трудом.
Заключительные мысли
Поддерживать продуктивность разработчиков по мере роста инженерной организации сложно. Почти неизбежно производительность труда каждого инженера по мере роста организации и кодовой базы в некоторой степени падает, хотя оценить этот эффект количественно практически невозможно.
Проблемы возникают на всех уровнях организации и являются социальными и организационными так же часто, как и техническими. Я никогда не буду утверждать, что у меня есть ответы на все вопросы или что Stripe нашел оптимальные или даже «достаточно хорошие» решения всех аспектов этой проблемы. Тем не менее я думаю, что примерно к 2019 году Stripe вложила в инструменты для разработчиков достаточно средств, и мы во многих отношениях улучшили средние впечатления разработчиков, если сравнивать со многими менее значительными этапами роста. Здесь я попытался передать основные решения и инфраструктуру, поддерживающую этот опыт.
И наконец. Опыт разработки, конечно, только часть истории: полный жизненный цикл кода и функциональности продолжается до CI и ревью кода. И, в конечном счете, до развертывания в рабочей среде, где он будет наблюдаться, отлаживаться и развиваться.
Читайте также:
- Сравниваем REST, GraphQL и gRPC
- Claude-in-Chrome постепенно становится лучшим отладчиком фронтенда, который я когда-либо использовал
- Веб сломан
Читайте нас в Telegram, VK и Дзен
Перевод статьи Nelson Elhage: Stripe’s monorepo developer environment





