Microsserviços em Rust - Estrutura do Projeto
Por Nikita Bishonen • 7 minutos de leitura •
Olá! Hoje partilho os meus pensamentos sobre uma estrutura de projeto conveniente para desenvolver microsserviços em Rust. Este é o primeiro post de uma potencial série “Microsserviços em Rust”. Se conseguir terminá-la, compilarei tudo num livro completo, por isso se notares alguma imprecisão ou tiveres os teus próprios pensamentos sobre o tema - ficaria feliz se os partilhasse comigo.
Começarei este post com uma breve introdução. Se estás confiante no teu conhecimento do sistema de pacotes e módulos do Rust, podes saltar diretamente para o conceito central.
Pacotes, Crates e Módulos em Rust
Se não estás familiarizado com Rust, recomendo estudar a fonte original - o capítulo Managing Growing Projects with Packages, Crates and Module na documentação oficial do Rust. O que se segue é a minha interpretação, que pode ser imprecisa ou incompleta.
Portanto, em projetos Rust temos:
- Um Pacote contendo:
Cargo(.toml | .lock)- Um ou muitos crates, cada um tendo:
- Uma raiz do crate -
src/(lib.rs | main.rs | bin/*.rs) - Módulos
- inline -
mod _nome_ { _corpo } - como ficheiros separados -
mod _nome_;+src/(_nome_.rs | _nome_/mod.rs)
- inline -
- Uma raiz do crate -
Isto cria uma matrioska ou o conto de Koschei, o Imortal - a agulha (módulo) num ovo, o ovo (crate) numa galinha, a galinha (pacote) num baú (projeto).

Vamos examinar as regras de acesso aos módulos.
- Os módulos podem ser privados (ocultos) e públicos.
- Por defeito, os módulos são privados.
- As entidades dentro de um módulo também podem ser privadas e públicas.
- Por defeito, as entidades são privadas.
- E então começam interações e combinações interessantes, que proponho examinar com exemplos de código.
mod parent {
mod children_private {
struct Private;
pub struct Public;
}
pub mod children_public {
struct Private;
pub struct Public;
mod grandchildren {
struct Private;
pub struct Public;
}
}
}
mod neighbour {
}Visualmente, a estrutura parece-se com isto:

Com este exemplo, quero mostrar as possíveis conexões entre módulos e entidades (neste exemplo, limitamo-nos a structs vazios).
Vamos dividir isto em várias situações, que distinguiremos pelo lado que faz a chamada.
O Que Pode um Módulo Pai Aceder?
fn test_visibility() {
// children_private::Private; // Falha devido à privacidade da struct
children_private::Public;
// children_public::Private; // Falha devido à privacidade da struct
children_public::Public;
// children_public::grandchildren::Private; // Falha devido à privacidade do módulo.
// children_public::grandchildren::Public; // Falha devido à privacidade do módulo.
}Acima, mostro um excerto de teste para o módulo pai. Como podemos ver, o acesso está disponível a ambos os módulos filhos (privado e público), mas apenas às structs públicas. O acesso aos “netos” privados e às suas structs públicas não está disponível. Visualmente, isto parece-se com:

O Que Pode um Módulo Filho Aceder?
Para isso, faremos uma adição e adicionaremos duas structs ao módulo pai, análogas aos filhos.
/// Testar visibilidade do modelo super e de outros sub-modelos do módulo um nível acima.
fn test_visibility() {
super::Public;
super::Private;
// super::children_private::Private; // Falha devido à privacidade da struct
super::children_private::Public;
// self::grandchildren::Private; // Falha devido à privacidade da struct
self::grandchildren::Public;
}
Como podemos ver, os tentáculos do nosso filho alcançam bastante longe, e apenas as structs privadas de módulos irmãos e filhos são inacessíveis. No entanto, as structs privadas do módulo pai estão bem ao alcance.
O Que Pode um Bisneto Aceder?
/// Testar visibilidade do modelo super e de outros sub-modelos do módulo dois níveis acima.
fn test_visibility() {
super::Private;
super::Public;
// super::super::children_private::Private; // Falha devido à privacidade da struct
super::super::children_private::Public;
super::super::Private;
super::super::Public;
}
Para o bisneto, a situação é semelhante. Decidi mostrá-lo para uma melhor compreensão de como este mecanismo multinível funciona em Rust.
O Que Pode um Vizinho Aceder?
Infelizmente, só reparei nesta fase que todos os diagramas até agora incluíam o vizinho dentro do módulo pai. Na realidade, está localizado ao lado, não dentro do pai. O diagrama abaixo contém a colocação correta deste módulo.
/// Testar visibilidade de sub-módulos dentro de outro módulo no mesmo nível.
fn test_visibility() {
crate::parent::Public;
// crate::parent::Private; // Falha devido à privacidade da struct
// crate::parent::children_private::Private; // Falha devido à privacidade do módulo
// crate::parent::children_private::Public; // Falha devido à privacidade do módulo
// crate::parent::children_public::Private; // Falha devido à privacidade da struct
crate::parent::children_public::Public;
// crate::parent::children_public::grandchildren::Private; // Falha devido à privacidade de um módulo
// crate::parent::children_public::grandchildren::Public; // Falha devido à privacidade de um módulo
}
A situação do vizinho é bastante aborrecida - apenas entidades públicas e caminhos para elas são acessíveis. Note que o módulo pai não é público, ainda assim o vizinho consegue vê-lo 👀
SOVMS
Ou Estrutura Baseada na Visibilidade dos Módulos. A ideia é que em Rust, já temos regras de acesso definidas (que descrevi em detalhe acima) às entidades dos módulos dependendo das suas relações (que relação o módulo que faz a chamada tem com o módulo chamado).
Decidi que a estrutura do projeto também deveria ser organizada com base nestas características da linguagem Rust para ser mais idiomática. A ideia é nova, e à medida que a testar, atualizarei este post.
Para obter uma estrutura de projeto idiomática, devemos colocar os módulos com base em quem e o que os deve ver. Esta parte do código é usada em todo o serviço? Mova-a o mais alto possível. Outra parte do código é específica? Isole-a num ramo separado ou esconda-a mais profundamente.
O ponto é que os módulos não devem ser decompostos de acordo com os domínios de negócio (pelo menos isto não deve ser a base da divisão). Afinal, na arquitetura de microsserviços, estes devem ser serviços separados (ou pacotes Rust separados para aplicações monolíticas). Desta forma, aspiramos à cobiçada proporção áurea de fraco acoplamento e alta coesão.
TL;DR
Como vejo a utilização desta abordagem na escrita de microsserviços? Deixe-me não perder mais do seu tempo a ler os meus pensamentos e mostrar-lhe um modelo:
//! Este é o crate base do serviço. Melhor torná-lo uma biblioteca, para que a funcionalidade
//! sobre como exatamente executá-lo seja armazenada separadamente dentro de um main.rs ou diferentes bins.
mod interfaces {
//!
mod graphql {
//!
}
mod http {
//!
}
mod grpc {
//!
}
}
mod core {
//!
mod api {
//!
}
mod config {
//!
}
}
mod resources {
//!
mod db {
//!
}
mod kafka {
//!
}
mod third_party_integration {
//!
}
}Podes criar um novo pacote com dois crates:
- main é responsável apenas por iniciar a aplicação
- lib armazena a estrutura e é usado em main, testes (integração), e “bins” adicionais se necessário
Exemplo Real
Neste exemplo, alguns nomes foram alterados, mas esta é uma estrutura real de um de vários serviços. Escrevê-los na verdade levou-me a derivar alguma teoria geral sobre a estruturação de microsserviços em Rust.
src
├── bin
│ └── events_processor.rs
├── core
│ ├── api.rs
│ ├── configuration.rs
│ └── mod.rs
├── interfaces
│ ├── graphql
│ │ ├── mod.rs
│ │ ├── mutations.rs
│ │ ├── queries.rs
│ │ └── subscriptions.rs
│ └── mod.rs
├── lib.rs
├── main.rs
└── resources
├── node_proxy.rs
├── indexer_proxy.rs
├── db.rs
├── mod.rs
├── reservoir_proxy.rs
└── real_time_data_ws.rsA Minha Experiência
Posso dizer que acho esta estrutura conveniente porque combina as vantagens das antigas arquiteturas de três camadas e das arquiteturas hexagonais aplicáveis a microsserviços. Dividimos a nossa aplicação em três ramos:
Resources- tudo o que está fora do nosso serviço e onde somos consumidores vai aqui. Bases de dados, outros serviços, message brokers (parcialmente). Tudo onde iniciamos a comunicação e onde sabemos mais sobre o interlocutor do que eles sabem sobre nós. (Anteriormente chamava esta parteclients, mas o nome resources parece mais apropriado agora). A tarefa principal dos resources é lidar com as especificidades da interação com estes recursos. Aqui podemos ter implementações de traits para codificar e descodificar os nossos tipos de dados para uma base de dados específica, e aqui escrevemos “proxies” que transformam esquemas de outros serviços no que operamos dentro do serviço.Interfaces- tudo o que serve como pontos de entrada para pedidos ao nosso serviço vai aqui. API HTTP/gRPC, GraphQL, até message brokers se fizerem parte do processamento de pedidos recebidos - tudo vai aqui. Tal como os resources, aqui tentamos isolar todas as especificidades relacionadas com o mecanismo particular de interação cliente-servidor (é por isso que anteriormente chamava esta parteservers). Por exemplo, todas as deserializações de pedidos e serializações de respostas devem ser implementadas aqui. Depois disso, devemos trabalhar com tipos “limpos”.Core- este é o lugar “mais limpo” e “mais importante” no nosso serviço. A lógica de negócio principal é implementada aqui. Aqui operamos não com JSON que veio pela rede, mas com a sua representação estaticamente tipada e verificada. A partir do core, podemos chamar resources, mas idealmente não devemos saber nada sobre interfaces. No entanto, não devemos ter acesso a conceitos de baixo nível dos resources. Apenas à API pública.
Como é que a colocação de módulos ajuda aqui? Vemos que todos os três ramos são vizinhos entre si, o que significa que só podem conhecer coisas públicas, e as especificidades são seguras para mudar (por exemplo, mudar a base de dados ou suportar REST junto com gRPC).
Dentro dos ramos, vamos do geral para o específico e também separamos partes independentes em ramos.
graphql consiste em queries, mutations, subscriptions independentes, mas o esquema comum está no módulo raiz:
//! graphql/mod.rs
pub async fn schema() -> Result<Schema<Query, Mutation, Subscription>> {
let configuration = Configuration::from_env()?;
let pool = database::connect(&configuration.database).await?;
let producer = kafka::build_producer(&configuration.kafka)?;
Ok(Schema::build(Query, Mutation, Subscription)
.enable_federation()
.enable_subscription_in_federation()
...Cada sub-módulo chamará métodos públicos “limpos” de core/api.rs.
api.rs no meu exemplo contém tipos comuns na raiz (tanto privados como públicos), aos quais os seus sub-módulos api::queries e api::commands têm acesso total, mas tipos privados e implementações do próprio módulo api são inacessíveis a qualquer pessoa exceto aos seus sub-módulos.
Materiais Adicionais
Se estás interessado em “brincar” com o exemplo deste artigo, aqui está o código completo do projeto do capítulo de módulos:
#![allow(clippy::all, dead_code, path_statements)]
fn main() {
println!("Hello, modules privacy!");
}
mod parent {
struct Private;
pub struct Public;
mod children_private {
struct Private;
pub struct Public;
}
pub mod children_public {
struct Private;
pub struct Public;
mod grandchildren {
struct Private;
pub struct Public;
/// Testar visibilidade do modelo super e de outros sub-modelos do módulo dois níveis acima.
fn test_visibility() {
super::Private;
super::Public;
// super::super::children_private::Private; // Falha devido à privacidade da struct
super::super::children_private::Public;
super::super::Private;
super::super::Public;
}
}
/// Testar visibilidade do modelo super e de outros sub-modelos do módulo um nível acima.
fn test_visibility() {
super::Public;
super::Private;
// super::children_private::Private; // Falha devido à privacidade da struct
super::children_private::Public;
// self::grandchildren::Private; // Falha devido à privacidade da struct
self::grandchildren::Public;
}
}
/// Testar visibilidade de sub-módulos dentro do módulo atual.
fn test_visibility() {
// children_private::Private; // Falha devido à privacidade da struct
children_private::Public;
// children_public::Private; // Falha devido à privacidade da struct
children_public::Public;
// children_public::grandchildren::Private; // Falha devido à privacidade do módulo.
// children_public::grandchildren::Public; // Falha devido à privacidade do módulo.
}
}
mod neighbour {
/// Testar visibilidade de sub-módulos dentro de outro módulo no mesmo nível.
fn test_visibility() {
crate::parent::Public;
// crate::parent::Private; // Falha devido à privacidade da struct
// crate::parent::children_private::Private; // Falha devido à privacidade do módulo
// crate::parent::children_private::Public; // Falha devido à privacidade do módulo
// crate::parent::children_public::Private; // Falha devido à privacidade da struct
crate::parent::children_public::Public;
// crate::parent::children_public::grandchildren::Private; // Falha devido à privacidade de um módulo
// crate::parent::children_public::grandchildren::Public; // Falha devido à privacidade de um módulo
}
}