Używanie Rusta w startupie: przestroga
Rdza jest niesamowita, do pewnych rzeczy. Ale zastanów się dwa razy, zanim wybierzesz go dla startupu, który musi działać szybko.

Wahałem się przed napisaniem tego posta, ponieważ nie chcę rozpoczynać ani wdawać się w świętą wojnę o języki programowania. (Aby zmyć płomień przynęty z drogi, Visual Basic jest najlepszym językiem na świecie!) Ale wiele osób pytało mnie o moje doświadczenia z Rustem i czy powinni używać Rusta do swoich projektów. Chciałbym więc podzielić się niektórymi zaletami i wadami, jakie widzę w korzystaniu z Rusta w środowisku startupowym, gdzie szybkie poruszanie się i skalowanie zespołów jest naprawdę ważne.
Chcę wyraźnie zaznaczyć, że jestem fanem Rusta w pewnych kwestiach . Ten post nie jest o tym, że Rust jest zły jako język ani nic w tym rodzaju. Chcę jednak porozmawiać o tym, jak używanie Rusta prawie na pewno pociągnie za sobą nietrywialne uderzenie w produktywność, które może być głównym czynnikiem, jeśli próbujesz działać szybko. Ostrożnie rozważ, czy wpływ prędkości jest wart korzyści płynących z języka dla Twojej firmy i produktu.
Z góry powinienem powiedzieć, że Rust jest bardzo dobry w tym, do czego został zaprojektowany , i jeśli twój projekt potrzebuje konkretnych zalet Rust (język systemowy o wysokiej wydajności, super silnym typowaniu, bez potrzeby zbierania elementów bezużytecznych itp.) w takim razie Rust to świetny wybór. Ale myślę, że Rust jest często używany w sytuacjach, w których nie jest dobrze dopasowany, a zespoły płacą cenę za złożoność i koszty ogólne Rust, nie uzyskując większych korzyści.
Moje podstawowe doświadczenie z Rust pochodzi z pracy z nim przez nieco ponad 2 lata w poprzednim startupie. Ten projekt był opartym na chmurze produktem SaaS, który jest mniej więcej konwencjonalną aplikacją CRUD: jest to zestaw mikrousług, które zapewniają punkt końcowy API REST i gRPC przed bazą danych, a także kilka innych back- końcowe mikroserwisy (same zaimplementowane w kombinacji Rusta i Pythona). Rust był używany przede wszystkim dlatego, że kilku założycieli firmy było ekspertami od Rusta. Z biegiem czasu znacznie powiększyliśmy zespół (zwiększając liczbę inżynierów prawie 10-krotnie), a także znacznie wzrósł rozmiar i złożoność bazy kodu.
Wraz z rozwojem zespołu i bazy kodu czułem, że z biegiem czasu płacimy coraz większy podatek za dalsze używanie Rusta. Rozwój był czasami powolny, uruchamianie nowych funkcji trwało dłużej, niż się spodziewałem, a zespół odczuł prawdziwy spadek produktywności dzięki tej wczesnej decyzji o użyciu Rusta. Przepisanie kodu w innym języku na dłuższą metę znacznie usprawniłoby programowanie i przyspieszyło czas dostarczania, ale znalezienie czasu na główne przepisanie byłoby niezwykle trudne. Więc trochę utknęliśmy z Rustem, chyba że zdecydowaliśmy się ugryźć kulę i przepisać dużą część kodu.
Rdza ma być najlepszą rzeczą od czasów krojonego chleba, więc dlaczego nie sprawdziła się u nas tak dobrze?

Rust ma ogromną krzywą uczenia się.
Pracowałem w dziesiątkach języków w swojej karierze i, z nielicznymi wyjątkami, większość współczesnych języków proceduralnych (C++, Go, Python, Java itp.) wszystkie bardzo podobne pod względem podstawowych pojęć. Każdy język ma swoje różnice, ale zwykle jest to kwestia nauczenia się kilku kluczowych wzorców, które różnią się w zależności od języka, a wtedy można dość szybko osiągnąć produktywność. Jednak w przypadku Rusta trzeba nauczyć się zupełnie nowych pomysłów — takich rzeczy, jak okresy życia, własność i sprawdzanie pożyczek. Nie są to pojęcia znane większości osób pracujących w innych popularnych językach, a krzywa uczenia się jest dość stroma, nawet dla doświadczonych programistów.
Niektóre z tych „nowych” pomysłów są oczywiście obecne w innych językach — zwłaszcza funkcjonalnych — ale Rust wprowadza je do „głównego nurtu” języka, a zatem będą nowe dla wielu nowicjuszy.
Pomimo tego, że byłem jednym z najmądrzejszych i najbardziej doświadczonych programistów, z którymi pracowałem, wiele osób w zespole (w tym ja) miało problem ze zrozumieniem kanonicznych sposobów robienia pewnych rzeczy w Rust, jak wyłapywać często tajemnicze komunikaty o błędach z kompilatora lub jak zrozumieć, jak działają kluczowe biblioteki (więcej na ten temat poniżej). Zaczęliśmy organizować cotygodniowe sesje „uczenia się Rusta” dla zespołu, aby pomóc dzielić się wiedzą i doświadczeniem. To wszystko znacznie odbiło się na produktywności i morale zespołu, ponieważ wszyscy odczuwali powolne tempo rozwoju.
Dla porównania, jak wygląda przyjęcie nowego języka w zespole programistów, jeden z moich zespołów w Google był jednym z pierwszych, który całkowicie przestawił się z C++ na Go i zajęło to nie więcej niż dwa tygodnie, zanim cała 15-osobowy zespół po raz pierwszy całkiem komfortowo kodował w Go. W przypadku Rust, nawet po miesiącach codziennej pracy w tym języku, większość osób w zespole nigdy nie czuła się w pełni kompetentna. Wielu deweloperów powiedziało mi, że często byli zawstydzeni tym, że wylądowanie ich funkcji trwało dłużej, niż się spodziewali, i że spędzali tak dużo czasu próbując ogarnąć Rust.
Istnieją inne sposoby rozwiązania problemów, które Rust próbuje rozwiązać.
Jak wspomniano powyżej, usługa, którą tworzyliśmy, była dość prostą aplikacją CRUD. Oczekiwane obciążenie tej usługi miało wynosić maksymalnie kilka zapytań na sekundę przez cały okres eksploatacji tego konkretnego systemu. Usługa była nakładką na dość rozbudowany potok przetwarzania danych, którego uruchomienie mogło zająć wiele godzin, więc nie oczekiwano, że sama usługa będzie wąskim gardłem wydajności. Nie było szczególnej obawy, że konwencjonalny język, taki jak Python, miałby jakiekolwiek problemy z zapewnieniem dobrej wydajności. Nie było żadnych specjalnych potrzeb w zakresie bezpieczeństwa ani współbieżności poza tym, z czym musi sobie radzić każda usługa internetowa. Jedynym powodem, dla którego używaliśmy Rusta, był fakt, że oryginalni autorzy systemu byli ekspertami od Rusta, a nie dlatego, że szczególnie dobrze pasował do budowania tego rodzaju usług.
Rust podjął decyzję, że bezpieczeństwo jest ważniejsze niż produktywność programistów. Jest to właściwy kompromis w wielu sytuacjach — na przykład budowanie kodu w jądrze systemu operacyjnego lub w przypadku systemów wbudowanych z ograniczoną pamięcią — ale nie sądzę, aby był to właściwy kompromis we wszystkich przypadkach, zwłaszcza w startupach, w których szybkość ma kluczowe znaczenie. Jestem pragmatykiem. Wolałbym, aby mój zespół poświęcał czas na debugowanie okazjonalnych wycieków pamięci lub błędów typowych dla kodu napisanego, powiedzmy, w Pythonie lub Go, niż żeby wszyscy w zespole cierpieli z powodu 4-krotnego spadku produktywności z powodu używania języka zaprojektowanego w celu całkowitego uniknięcia tych problemów .
Jak wspomniałem powyżej, mój zespół w Google stworzył usługę całkowicie w Go, która z czasem rozrosła się do obsługi ponad 800 milionów użytkowników i około 4-krotnego QPS wyszukiwarki Google w szczytowym momencie. Mogę policzyć na palcach jednej ręki, ile razy napotkaliśmy problem spowodowany przez system typu Go lub Garbage Collector w latach budowania i uruchamiania tej usługi. Zasadniczo problemy, których Rust ma unikać, można rozwiązać na inne sposoby — przez dobre testowanie, dobre linting, dobre przeglądanie kodu i dobre monitorowanie. Oczywiście nie wszystkie projekty oprogramowania mają ten luksus, więc mogę sobie wyobrazić, że Rust może być dobrym wyborem w innych sytuacjach.

Będziesz miał trudności z zatrudnieniem programistów Rust.
Podczas mojego pobytu w tej firmie zatrudniliśmy mnóstwo ludzi, ale tylko dwie lub trzy z ponad 60 osób, które dołączyły do zespołu inżynierów, miały wcześniejsze doświadczenie z Rust. Nie było to spowodowane chęcią znalezienia twórców Rusta — po prostu ich tam nie ma. (Z tego samego powodu wahaliśmy się, czy zatrudniać ludzi, którzy chcieli tylko programować w Rust, ponieważ uważam, że to złe oczekiwanie w środowisku start-upowym, w którym wybór języka i innych technologii musi być dokonywany w zwinny sposób). Talent deweloperów Rust będzie się zmieniał z czasem, gdy Rust stanie się bardziej popularny, ale budowanie wokół Rusta przy założeniu, że będziesz w stanie zatrudnić ludzi, którzy już go znają, wydaje się ryzykowne.
Innym drugorzędnym czynnikiem jest to, że używanie Rust prawie na pewno doprowadzi do schizmy między ludźmi w zespole, którzy znają Rusta, a tymi, którzy go nie znają. Ponieważ wybraliśmy „ezoteryczny” język programowania dla tej usługi, inni inżynierowie w firmie, którzy w przeciwnym razie mogliby być pomocni w budowaniu funkcji, debugowaniu problemów produkcyjnych itd. ogony bazy kodu Rust. Ten brak zamienności w zespole inżynierów może być prawdziwym obciążeniem, gdy próbujesz działać szybko i wykorzystać połączone mocne strony wszystkich członków zespołu. Z mojego doświadczenia wynika, że ludzie na ogół nie mają trudności z poruszaniem się między językami takimi jak C++ i Python, ale Rust jest na tyle nowy i na tyle złożony, że stanowi barierę dla osób pracujących razem.
Biblioteki i dokumentacja są niedojrzałe.
Jest to problem, który (mam nadzieję!) zostanie rozwiązany z czasem, ale w porównaniu, powiedzmy, z Go, biblioteka i ekosystem dokumentacji Rusta są niesamowicie niedojrzałe. Teraz Go miał tę zaletę, że został opracowany i wspierany przez cały oddany zespół Google, zanim został udostępniony światu, więc dokumenty i biblioteki były dość dopracowane. Dla porównania, rdza od dawna wydawała się dziełem w toku. Dokumentacja wielu popularnych bibliotek jest dość skąpa i często trzeba przeczytać kod źródłowy danej biblioteki, aby zrozumieć, jak z niej korzystać. To jest złe.
Apologeci rdzy w zespole często mówili takie rzeczy, jak „asynchronizacja/oczekiwanie są wciąż naprawdę nowe” i „tak, brakuje dokumentacji dla tej biblioteki”, ale te niedociągnięcia miały znaczący wpływ na zespół. Popełniliśmy ogromny błąd na początku, przyjmując Actix jako platformę sieciową dla naszej usługi. Decyzja ta doprowadziła do ogromnej ilości bólu i cierpienia, gdy natrafiliśmy na błędy i problemy zakopane głęboko w bibliotece, których nikt nie mógł wymyślić, jak naprawić. (Szczerze mówiąc, było to kilka lat temu i być może do tej pory sytuacja się poprawiła.)
Oczywiście tego rodzaju niedojrzałość nie jest tak naprawdę specyficzna dla Rust, ale stanowi podatek, który twój zespół musi zapłacić. Bez względu na to, jak świetna jest dokumentacja i samouczki dotyczące podstawowego języka, jeśli nie możesz dowiedzieć się, jak korzystać z bibliotek, nie ma to większego znaczenia (chyba że oczywiście planujesz napisać wszystko od zera).
Rdza sprawia, że opracowywanie nowych funkcji jest bardzo trudne.
Nie wiem jak ktokolwiek inny, ale przynajmniej dla mnie, kiedy tworzę nową funkcję, zwykle nie mam z góry opracowanych wszystkich typów danych, interfejsów API i innych drobnych szczegółów. Często po prostu wyrzucam kod, próbując uruchomić jakiś podstawowy pomysł i sprawdzam, czy moje założenia dotyczące tego, jak powinno działać, są mniej więcej poprawne. Zrobienie tego, powiedzmy, w Pythonie jest niezwykle łatwe, ponieważ możesz grać szybko i swobodnie z rzeczami takimi jak pisanie na klawiaturze i nie martwić się, jeśli niektóre ścieżki kodu zostaną przerwane podczas opracowywania pomysłu. Możesz wrócić później i uporządkować wszystko, naprawić wszystkie błędy typu i napisać wszystkie testy.
W Rust ten rodzaj „szkicowego kodowania” jest bardzo trudny, ponieważ kompilator może narzekać i będzie narzekać na każdą cholerną rzecz, która nie przejdzie sprawdzania typu i czasu życia — do czego został wyraźnie zaprojektowany. Ma to sens, gdy musisz zbudować ostateczną, gotową do produkcji implementację, ale jest do niczego, gdy próbujesz coś połączyć, aby przetestować pomysł lub uzyskać podstawowe podstawy. Makro jest unimplemented!
do pewnego stopnia pomocne, ale nadal wymaga, aby wszystko sprawdzało typ w górę iw dół stosu, zanim będzie można nawet skompilować.
To, co naprawdę gryzie, to konieczność zmiany sygnatury typu interfejsu nośnego i spędzanie godzin na zmienianiu każdego miejsca, w którym typ jest używany, tylko po to, aby sprawdzić, czy początkowe dźgnięcie w coś jest wykonalne. A potem ponowne wykonanie całej tej pracy, gdy zdasz sobie sprawę, że musisz to ponownie zmienić.

W czym Rust jest dobry?
Zdecydowanie są rzeczy, które lubię w Ruście i funkcje Rusta, które chciałbym mieć w innych językach. Składnia match
jest świetna. Cechy Option
, Result
, i Error
są naprawdę potężne, a ?
operator to elegancki sposób obsługi błędów. Wiele z tych pomysłów ma odpowiedniki w innych językach, ale podejście Rusta do nich jest szczególnie eleganckie.
Zdecydowanie użyłbym Rusta do projektów, które wymagają wysokiego poziomu wydajności i bezpieczeństwa i dla których nie martwiłbym się zbytnio o konieczność szybkiego rozwijania głównych części kodu z całym szybko rosnącym zespołem. W przypadku indywidualnych projektów lub bardzo małych (powiedzmy 2-3-osobowych) zespołów, Rust prawdopodobnie byłby w porządku. Rust to świetny wybór w przypadku modułów jądra, oprogramowania układowego, silników gier itp., w których wydajność i bezpieczeństwo są najważniejsze, a także w sytuacjach, w których przeprowadzenie naprawdę dokładnych testów przed wysyłką może być trudne.
Dobra, teraz, gdy wystarczająco wkurzyłem połowę czytelników Hacker News, myślę, że teraz jest dobry moment, aby ogłosić temat mojego następnego artykułu: Dlaczego nano
edytor tekstu jest lepszy. Do zobaczenia następnym razem!