Программирование
Использование Rust в стартапе: поучительная история
Rust великолепен для некоторых вещей. Но подумайте дважды, прежде чем выбрать его для стартапа, которому нужно двигаться быстро.
Я не решался написать этот пост, потому что не хочу начинать или ввязываться в священную войну языков программирования (просто чтобы убрать с дороги приманку для флейма, Visual Basic — лучший язык!). Но у меня было несколько людей, которые спрашивали меня о моем опыте работы с Rust и о том, следует ли им выбрать Rust для своих проектов. Итак, я хотел бы поделиться некоторыми плюсами и минусами, которые я вижу в использовании Rust на начальных стадиях стартапа, где действительно важно быстро двигаться и быстро масштабировать команду.
Стартап на Rust
Я хочу прояснить, что я фанат Rust в определенных вещах. Этот пост не о том, насколько Rust плох как язык или что-то в этом роде. Однако я хочу поговорить о том, что использование Rust почти наверняка повлечет за собой нетривиальный удар по производительности, который может стать важным фактором, если вы пытаетесь двигаться быстро. Тщательно взвесьте, стоит ли потеря скорости для вашей компании и продукта всех преимуществ языка.
Сразу скажу, что Rust очень хорош в том, для чего он предназначен, и если вашему проекту нужны определенные преимущества Rust (системный язык с высокой производительностью, сверхстрогая типизация, отсутствие необходимости в сборке мусора и т.п.), тогда Rust — отличный выбор. Но я думаю, что Rust часто используется в ситуациях, когда он не очень подходит, и команды платят за сложность и накладные расходы Rust, не получая при этом особой пользы.
Мой основной опыт общения с Rust связан с работой с ним в течение чуть более 2 лет в предыдущем стартапе. Этот проект был облачным SaaS-продуктом, который более или менее является обычным CRUD-приложением: это набор микросервисов, которые предоставляют конечную точку REST и gRPC API перед базой данных, а также некоторые другие вспомогательные back-end микросервисы (реализованные в сочетании Rust и Python). Rust использовался в первую очередь потому, что несколько основателей компании были экспертами по Rust. Со временем мы значительно увеличили команду (увеличив количество инженеров почти в 10 раз), а также значительно увеличились размер и сложность кодовой базы.
По мере роста команды и кодовой базы я чувствовал, что со временем мы платим все более высокий налог за то, что продолжаем использовать Rust. Разработка иногда шла вяло, запуск новых функций занимал больше времени, чем я ожидал, и команда ощутила настоящий удар по производительности из-за того раннего решения использовать Rust. Переписывание кода на другом языке, в конечном счете, сделало бы разработку намного более гибкой и ускорило время доставки. Но найти время для огромной работы по рефакторингу было чрезвычайно сложно. Так что мы как бы застряли в Rust до тех пор, пока бы не решили стиснуть зубы и переписать большое количество кода.
Предполагается, что Rust — лучшая вещь после нарезанного хлеба, так почему же он не работал так хорошо для нас?
У Rust огромная кривая обучения.
За свою карьеру я работал с десятками языков, и, за немногими исключениями, с самыми современными процедурными языками (C++, Go, Python, Java и т.д.). Все они очень похожи с точки зрения их основных концепций. У каждого языка есть свои отличия, но обычно это вопрос изучения нескольких ключевых шаблонов, которые различаются в разных языках. Поняв их уже можно довольно быстро добиться продуктивной работы. Однако с Rust нужно изучать совершенно новые идеи — такие вещи, как время жизни, право собственности и проверка заимствования. Эти концепции незнакомы большинству людей, работающих на других распространенных языках, и даже для опытных программистов существует довольно крутая кривая обучения.
Некоторые из этих «новых» идей, конечно же, присутствуют и в других языках — особенно в функциональных — но Rust привносит их в «мейнстрим» языка и, следовательно, будет новым для многих новичков в Rust.
Несмотря на то, что они были одними из самых умных и опытных разработчиков, с которыми я работал, многие люди в команде (включая меня) изо всех сил пытались понять канонические способы делать определенные вещи в Rust, понять часто загадочные сообщения об ошибках от компилятора или понять, как работают ключевые библиотеки (подробнее об этом ниже). Мы начали проводить еженедельные сессии «изучаем Rust», чтобы команда могла делиться знаниями и опытом. Все это значительно снизило производительность и моральный дух команды, поскольку все чувствовали медленный темп разработки.
В качестве сравнения того, как выглядит переход на новый язык в команде разработчиков программного обеспечения, приведу одну из моих команд в Google, которая одной из первых впервые полностью перешла с C++ на Go, и потребовалось не более двух недель, прежде чем вся команда из 15 человек вполне комфортно начала программировать на Go. С Rust, даже после нескольких месяцев ежедневной работы над языком, большинство людей в команде никогда не чувствовали себя полностью компетентными. Некоторые разработчики сказали мне, что их часто смущало то, что на разработку их фич требовалось больше времени, чем они ожидали, и что они тратили много времени, пытаясь понять Rust.
Есть и другие способы исправить проблемы, которые пытается решить Rust
Как упоминалось выше, сервис, который мы создавали, был довольно простым CRUD-приложением. Ожидаемая нагрузка на этот сервис должна была быть не более нескольких запросов в секунду, максимум, в течение всего срока службы данной конкретной системы. Служба была внешним интерфейсом к довольно сложному конвейеру обработки данных, запуск которого мог занять много часов, поэтому не ожидалось, что сама служба будет узким местом в производительности. Не было особых опасений, что обычный язык, такой как Python, не сможет обеспечить хорошую производительность. Не было особых потребностей в безопасности или параллелизме, кроме тех, с которыми должен иметь дело любой веб-сервис. Единственная причина, по которой мы использовали Rust, заключалась в том, что первоначальные авторы системы были экспертами в Rust, а не потому, что он особенно хорошо подходил для создания такого рода сервисов.
Для Rust принято решение, что безопасность важнее продуктивности разработчиков. Это правильный компромисс во многих ситуациях — например, при создании кода в ядре ОС или для встроенных систем с ограниченной памятью — но я не думаю, что это правильный компромисс во всех случаях, особенно в стартапах, где скорость имеет решающее значение. Я прагматик. Я бы предпочел, чтобы моя команда тратила время на отладку случайных утечек памяти или ошибок с неправильными типами переменных для кода, написанного, скажем, на Python или Go, чем чтобы все в команде страдали от 4-кратного снижения производительности из-за использования языка, разработанного для полного устранения этих проблем.
Как я упоминал выше, моя команда в Google создала сервис полностью на Go. Со временем он вырос до поддержки более 800 миллионов пользователей и обслуживал примерно в 4x больше запросов в секунду, чем Google Search в пике. Я могу сосчитать по пальцам одной руки, сколько раз за годы создания и запуска этого сервиса мы сталкивались с проблемой, вызванной системой типов Go или сборщиком мусора. По сути, проблемы, которых Rust призван избегать, можно решить другими способами — хорошим тестированием, хорошим анализом кода, хорошим code review и хорошим мониторингом. Конечно, не все программные проекты имеют такую роскошь, поэтому я могу предположить, что Rust может быть хорошим выбором в других ситуациях.
Вам будет трудно нанять разработчиков Rust
За время моей работы в этой компании мы наняли массу людей, но только двое или трое из 60+ человек, присоединившихся к команде инженеров, имели предыдущий опыт работы с Rust. Это было не из-за отсутствия попыток найти разработчиков Rust — их просто нет. (Rust является одним из самых высокооплачиваемых языков программирования. Мы также не решались нанимать людей, которые хотели программировать только на Rust, так как я думаю, что это плохое подход в условиях стартапа, когда выбор языка и других технологий делается гибко.) Эта нехватка талантливых Rust-разработчиков со временем конечно изменится по мере того, как Rust будет становится все более популярным, но строить проект вокруг Rust, исходя из предположения, что вы сможете нанять людей, которые уже знают его, кажется рискованным.
Еще один второстепенный фактор заключается в том, что использование Rust почти наверняка приведет к расколу между людьми в команде, которые знают Rust, и теми, кто его не знает. Поскольку для этого сервиса мы выбрали «эзотерический» язык программирования, другие инженеры в компании, которые в противном случае могли бы помочь в разработке функций, отладке производственных проблем и т.д., по большей части не могли помочь, потому что они не могли работать с кодовой базой Rust. Это отсутствие взаимозаменяемости в команде инженеров может стать реальной проблемой, когда вы пытаетесь двигаться быстро и использовать объединенные сильные стороны всех в команде. По моему опыту, людям обычно несложно переключаться между такими языками, как C++ и Python, но Rust достаточно нов и достаточно сложен, чтобы создавать препятствия для совместной работы людей.
Библиотеки и документация несовершенны
Это проблема, которая (я надеюсь!) со временем будет исправлена, но по сравнению, скажем, с Go, библиотеки и экосистема документации Rust невероятно незрелы. Преимущество Go заключалось в том, что его разрабатывала и поддерживала целая специальная команда Google до того, как он был выпущен для всего мира, поэтому документы и библиотеки были достаточно отшлифованы. Для сравнения, Rust уже давно ощущается как “work in progress”. Документации для многих популярных библиотек довольно мало, и часто нужно прочитать исходный код данной библиотеки, чтобы понять, как ее использовать. Это плохо.
Апологеты Rust в команде часто говорили такие вещи, как «async/await все еще очень новые» и «да, документации для этой библиотеки не хватает», но эти недостатки довольно сильно повлияли на команду. Мы совершили огромную ошибку на раннем этапе, приняв Actix в качестве веб-фреймворка для нашего сервиса, решение, которое привело к огромному количеству боли и страданий, поскольку мы столкнулись с ошибками и проблемами, скрытыми глубоко в библиотеке, которые никто не мог понять, как исправить. (Честно говоря, это было несколько лет назад, и, возможно, сейчас ситуация улучшилась.)
Конечно, такая незрелость на самом деле характерна не только для Rust, но она представляет собой налог, который ваша команда должна платить. Независимо от того, насколько хороша документация и учебные пособия по основному языку, если вы не можете понять, как использовать библиотеки, они не имеют большого значения (если, конечно, вы не планируете писать все с нуля).
Rust очень усложняет разработку новых функций.
Я не знаю, как у кого-то еще, но когда я создаю новую функцию, у меня обычно нет всех типов данных, API и других мелких деталей, проработанных заранее. Я часто пишу говно-код, пытаясь заставить работать какую-то базовую идею и проверяя, более или менее верны мои предположения о том, как все должно работать. Сделать это, скажем, в Python чрезвычайно просто, потому что вы можете быстро и свободно играться с кодом, не беспокоясь о том, что определенные пути в нем не работают, пока вы прототипируете свою идею. Вы можете вернуться позже и привести все в порядок, исправить все ошибки типов и написать все тесты.
В Rust такое «черновое кодирование» очень сложно, потому что компилятор может и будет жаловаться на каждую чертову штуку, которая не проходит проверку типов и времени жизни — так это было специально задумано. Это имеет смысл, когда вам нужно построить свою окончательную, готовую к продакшену реализацию, но совершенно бесполезно, когда вы пытаетесь собрать что-то вместе, чтобы проверить идею или создать основу. Макрос unimplemented! полезен до некоторой степени, но все же требует, чтобы все типы проверялись вверх и вниз по стеку, прежде чем вы сможете просто скомпилировать код.
Что действительно больно, так это когда вам нужно изменить тип несущего интерфейса, и вы тратите часы на изменение каждого места, где используется этот тип, только для того, чтобы увидеть, осуществим ли ваш первоначальный подход к чему-либо. А затем переделываете всю эту работу, когда понимаете, что вам нужно изменить ее снова.
Чем хорош Rust?
Определенно есть вещи, которые мне нравятся в Rust, и есть фичи Rust, которые я хотел бы иметь в других языках. Синтаксис match великолепен. Трейты Option, Result и Error действительно мощные, а оператор ? — это элегантный способ обработки ошибок. У многих из этих идей есть аналоги в других языках, но подход к ним в Rust особенно элегантен.
Я бы абсолютно точно использовал Rust для проектов, которым требуется высокий уровень производительности и безопасности и в которых я не очень бы беспокоился о необходимости быстрой разработки основных частей кода в быстрорастущей команде. Для индивидуальных проектов или очень маленьких (скажем, 2-3 человека) команд Rust, скорее всего, подойдет. Rust — отличный выбор для таких вещей, как модули ядра, прошивки, игровые движки и т.д., где производительность и безопасность имеют первостепенное значение, а также в ситуациях, когда может быть сложно провести действительно тщательное тестирование перед поставкой.
Хорошо, теперь, когда я достаточно разозлил половину читателей Hacker News, я думаю, что сейчас самое подходящее время, чтобы объявить тему моей следующей статьи: почему nano лучший текстовый редактор. Увидимся в следующий раз!