Usando Rust em uma startup: um conto preventivo

Nov 25 2022
A ferrugem é incrível, para certas coisas. Mas pense duas vezes antes de comprá-lo para uma startup que precisa se mover rapidamente.

A ferrugem é incrível, para certas coisas. Mas pense duas vezes antes de comprá-lo para uma startup que precisa se mover rapidamente.

Toda a arte deste post foi gerada usando DALL-E.

Hesitei em escrever este post, porque não quero começar, ou entrar, uma guerra santa sobre linguagens de programação. (Só para tirar a isca do caminho, Visual Basic é a melhor linguagem de todas!) Mas várias pessoas me perguntaram sobre minha experiência com Rust e se deveriam escolher Rust para seus projetos. Então, gostaria de compartilhar alguns dos prós e contras que vejo ao usar o Rust em uma configuração de inicialização, onde mover equipes rapidamente e dimensionar é realmente importante.

Quero deixar claro que sou fã de Rust em certas coisas . Este post não é sobre como Rust é ruim como linguagem ou algo do tipo. O que eu quero falar, no entanto, é como o uso do Rust quase certamente envolverá um golpe de produtividade não trivial que pode ser um fator importante se você estiver tentando se mover rapidamente. Pese cuidadosamente se o impacto da velocidade vale os benefícios do idioma para sua empresa e produto.

De antemão, devo dizer que o Rust é muito bom no que foi projetado para fazer , e se o seu projeto precisar dos benefícios específicos do Rust (uma linguagem de sistemas com alto desempenho, digitação super forte, sem necessidade de coleta de lixo, etc.) então Rust é uma ótima escolha. Mas acho que o Rust costuma ser usado em situações em que não é uma boa opção, e as equipes pagam o preço da complexidade e da sobrecarga do Rust sem obter muitos benefícios.

Minha experiência principal com o Rust vem de trabalhar com ele por pouco mais de 2 anos em uma startup anterior. Este projeto foi um produto SaaS baseado em nuvem que é, mais ou menos, um aplicativo CRUD convencional: é um conjunto de microsserviços que fornecem um endpoint de API REST e gRPC na frente de um banco de dados, bem como alguns outros back- end microservices (eles próprios implementados em uma combinação de Rust e Python). Rust foi usado principalmente porque alguns dos fundadores da empresa eram especialistas em Rust. Com o tempo, aumentamos consideravelmente a equipe (aumentando o número de engenheiros em quase 10 vezes), e o tamanho e a complexidade da base de código também aumentaram consideravelmente.

À medida que a equipe e a base de código cresciam, senti que, com o tempo, estávamos pagando um imposto cada vez mais pesado por continuar usando o Rust. Às vezes, o desenvolvimento era lento, o lançamento de novos recursos demorava mais do que eu esperava e a equipe estava sentindo um verdadeiro impacto na produtividade com a decisão inicial de usar o Rust. Reescrever o código em outro idioma teria, a longo prazo, tornado o desenvolvimento muito mais ágil e acelerado o tempo de entrega, mas encontrar tempo para o grande trabalho de reescrita teria sido extremamente difícil. Portanto, estávamos meio que presos ao Rust, a menos que decidíssemos morder a bala e reescrever uma grande parte do código.

A ferrugem é considerada a melhor coisa desde o pão fatiado, então por que não funcionou tão bem para nós?

Rust tem uma enorme curva de aprendizado.

Trabalhei em dezenas de linguagens em minha carreira e, com poucas exceções, linguagens procedurais mais modernas (C++, Go, Python, Java, etc.) todas muito semelhantes em termos de seus conceitos básicos. Cada idioma tem suas diferenças, mas geralmente é uma questão de aprender alguns padrões-chave que diferem entre os idiomas e, em seguida, pode-se ser produtivo rapidamente. Com o Rust, no entanto, é preciso aprender ideias totalmente novas - coisas como vidas, propriedade e o verificador de empréstimos. Esses conceitos não são familiares para a maioria das pessoas que trabalham em outras linguagens comuns e há uma curva de aprendizado bastante acentuada, mesmo para programadores experientes.

Algumas dessas “novas” ideias estão, é claro, presentes em outras linguagens – especialmente as funcionais – mas Rust as traz para um ambiente de linguagem “mainstream” e, portanto, serão novas para muitos novatos em Rust.

Apesar de ser um dos desenvolvedores mais inteligentes e experientes com quem já trabalhei, muitas pessoas na equipe (inclusive eu) lutaram para entender as maneiras canônicas de fazer certas coisas no Rust, como grocar as mensagens de erro muitas vezes misteriosas do compilador ou como entender como as bibliotecas de chaves funcionam (mais sobre isso abaixo). Começamos a ter sessões semanais de “aprender Rust” para a equipe ajudar a compartilhar conhecimento e experiência. Isso tudo foi um dreno significativo na produtividade e no moral da equipe, pois todos sentiram a lentidão do desenvolvimento.

Como ponto de comparação de como é adotar uma nova linguagem em uma equipe de software, uma das minhas equipes no Google foi uma das primeiras a mudar totalmente de C++ para Go, e não demorou mais do que duas semanas antes que todo o Uma equipe de 15 pessoas estava confortavelmente codificando em Go pela primeira vez. Com o Rust, mesmo depois de meses trabalhando diariamente no idioma, a maioria das pessoas da equipe nunca se sentiu totalmente competente. Vários desenvolvedores me disseram que muitas vezes ficavam envergonhados por estar demorando mais do que esperavam para seus recursos aparecerem e por estarem gastando tanto tempo tentando entender Rust.

Existem outras maneiras de corrigir os problemas que Rust está tentando resolver.

Conforme mencionado acima, o serviço que estávamos construindo era um aplicativo CRUD bastante simples. A carga esperada neste serviço seria da ordem de não mais do que algumas consultas por segundo, no máximo, durante o tempo de vida deste sistema específico. O serviço era um front-end para um pipeline de processamento de dados bastante elaborado que poderia levar muitas horas para ser executado, portanto, não se esperava que o serviço em si fosse um gargalo de desempenho. Não havia nenhuma preocupação particular de que uma linguagem convencional como o Python teria problemas para oferecer um bom desempenho. Não havia necessidades especiais de segurança ou simultaneidade além do que qualquer serviço voltado para a Web precisa lidar. A única razão pela qual estávamos usando o Rust era porque os autores originais do sistema eram especialistas em Rust, não porque era uma boa opção para construir esse tipo de serviço.

A Rust decidiu que a segurança é mais importante do que a produtividade do desenvolvedor. Essa é a compensação certa a ser feita em muitas situações - como criar código em um kernel do sistema operacional ou para sistemas embarcados com restrição de memória - mas não acho que seja a compensação certa em todos os casos, especialmente em startups onde a velocidade é crucial. Eu sou um pragmatista. Eu preferiria que minha equipe perdesse tempo depurando o vazamento de memória ocasional ou erro de tipo para código escrito em, digamos, Python ou Go, do que todos na equipe sofrerem um impacto de produtividade 4x maior por usar uma linguagem projetada para evitar esses problemas inteiramente .

Como mencionei acima, minha equipe no Google criou um serviço, totalmente em Go, que com o tempo cresceu para suportar mais de 800 milhões de usuários e algo como 4x o QPS da Pesquisa do Google em seu pico. Posso contar nos dedos de uma mão o número de vezes que encontramos um problema causado pelo sistema de tipo Go ou coletor de lixo nos anos construindo e executando este serviço. Basicamente, os problemas que o Rust foi projetado para evitar podem ser resolvidos de outras maneiras - por meio de bons testes, bom linting, boa revisão de código e bom monitoramento. Claro, nem todos os projetos de software têm esse luxo, então posso imaginar que o Rust pode ser uma boa escolha nessas outras situações.

Você terá dificuldade em contratar desenvolvedores Rust.

Contratamos uma tonelada de pessoas durante meu tempo nesta empresa, mas apenas cerca de duas ou três das mais de 60 pessoas que se juntaram à equipe de engenharia tinham experiência anterior com Rust. Não foi por falta de tentar encontrar desenvolvedores de Rust - eles simplesmente não estão por aí. (Da mesma forma, hesitamos em contratar pessoas que queriam codificar em Rust, pois acho que é uma expectativa ruim definir um ambiente de inicialização onde a linguagem e outras escolhas de tecnologia precisam ser feitas de maneira ágil.) Essa escassez. do talento de desenvolvimento do Rust mudará com o tempo, à medida que o Rust se tornar mais popular, mas construir em torno do Rust supondo que você poderá contratar pessoas que já sabem que isso parece arriscado.

Outro fator secundário é que o uso do Rust quase certamente levará a um cisma entre as pessoas da equipe que conhecem o Rust e as que não o conhecem. Como havíamos escolhido uma linguagem de programação "esotérica" ​​para este serviço, os outros engenheiros da empresa que poderiam ter sido úteis na criação de recursos, depuração de problemas de produção e assim por diante foram incapazes de ajudar porque não conseguiam fazer cabeças ou caudas da base de código Rust. Essa falta de fungibilidade na equipe de engenharia pode ser uma desvantagem real quando você está tentando se mover rapidamente e aproveitar as forças combinadas de todos na equipe. Na minha experiência, as pessoas geralmente têm pouca dificuldade em se mover entre linguagens como C++ e Python, mas Rust é novo e complexo o suficiente para representar uma barreira para as pessoas trabalharem juntas.

As bibliotecas e a documentação são imaturas.

Este é um problema que (espero!) será corrigido com o tempo, mas comparado a, digamos, Go, a biblioteca e o ecossistema de documentação do Rust são incrivelmente imaturos. Agora, Go teve o benefício de ter sido desenvolvido e apoiado por toda uma equipe dedicada do Google antes de ser lançado para o mundo, então os documentos e as bibliotecas eram bastante polidos. A ferrugem, em comparação, há muito parece um trabalho em andamento. A documentação de muitas bibliotecas populares é bastante esparsa e muitas vezes é preciso ler o código-fonte de uma determinada biblioteca para entender como usá-la. Isto é mau.

Os apologistas do Rust na equipe costumavam dizer coisas como “async/await ainda são realmente novos” e “sim, faltam documentos para essa biblioteca”, mas essas deficiências impactaram a equipe de maneira bastante significativa. Cometemos um grande erro desde o início ao adotar o Actix como a estrutura da web para o nosso serviço, uma decisão que nos levou a muita dor e sofrimento quando nos deparamos com bugs e problemas enterrados na biblioteca que ninguém conseguia descobrir como corrigir. (Para ser justo, isso foi há alguns anos e talvez as coisas tenham melhorado agora.)

Claro, esse tipo de imaturidade não é realmente específico do Rust, mas representa um imposto que sua equipe deve pagar. Não importa quão bons sejam os tutoriais e documentação da linguagem principal, se você não consegue descobrir como usar as bibliotecas, não importa muito (a menos que você esteja planejando escrever tudo do zero, é claro).

A ferrugem torna o desbaste de novos recursos muito difícil.

Não sei sobre mais ninguém, mas pelo menos para mim, quando estou criando um novo recurso, geralmente não tenho todos os tipos de dados, APIs e outros detalhes elaborados antecipadamente. Freqüentemente, estou apenas peidando o código tentando fazer alguma ideia básica funcionar e verificando se minhas suposições sobre como as coisas devem funcionar estão mais ou menos corretas. Fazer isso em, digamos, Python é extremamente fácil, porque você pode jogar rápido e solto com coisas como digitar e não se preocupar se certos caminhos de código forem quebrados enquanto você esboça sua ideia. Você pode voltar mais tarde e deixar tudo arrumado, corrigir todos os erros de digitação e escrever todos os testes.

Em Rust, esse tipo de “codificação de rascunho” é muito difícil, porque o compilador pode e irá reclamar sobre cada maldita coisa que não passa na verificação de tipo e tempo de vida – como foi explicitamente projetado para fazer. Isso faz todo o sentido quando você precisa construir sua implementação final, pronta para produção, mas absolutamente péssimo quando você está tentando criar algo para testar uma ideia ou obter uma base básica no lugar. A unimplemented!macro é útil até certo ponto, mas ainda requer que tudo o tipo verifique para cima e para baixo na pilha antes mesmo de você poder compilar.

O que realmente incomoda é quando você precisa alterar a assinatura de tipo de uma interface de suporte de carga e passa horas mudando todos os lugares onde o tipo é usado apenas para ver se sua tentativa inicial em algo é viável. E depois refazer todo esse trabalho quando perceber que precisa alterá-lo novamente.

No que Rust é bom?

Definitivamente, há coisas de que gosto no Rust e recursos do Rust que adoraria ter em outros idiomas. A matchsintaxe é ótima. As características Option, Resulte Errorsão realmente poderosas, e o ?operador é uma maneira elegante de lidar com erros. Muitas dessas ideias têm equivalentes em outras linguagens, mas a abordagem de Rust com elas é particularmente elegante.

Eu absolutamente usaria o Rust para projetos que precisam de um alto nível de desempenho e segurança e para os quais não estava muito preocupado com a necessidade de evoluir rapidamente as principais partes do código com uma equipe inteira que está crescendo rapidamente. Para projetos individuais ou equipes muito pequenas (digamos, de 2 a 3 pessoas), o Rust provavelmente funcionaria bem. Rust é uma ótima opção para itens como módulos de kernel, firmware, mecanismos de jogo etc.

Ok, agora que já irritei suficientemente metade dos leitores do Hacker News, acho que agora é um bom momento para anunciar o tópico do meu próximo artigo: Por que nanoé o editor de texto superior. Vejo você na próxima vez!