Cuisiner un micro-service avec Rust

David Bernard davidB
François Mockers mockersf
Petite recette pour réaliser un micro-service codé en Rust de zéro au déploiement. Afin de découvrir Rust au travers de son écosysteme, ainsi que l'intégration dans une "infrastructure micro-services".
Car un langage n'est pas qu'une syntaxe et un compilateur, mais aussi ce qu'il y a autour: outils, documentation, communauté, ...

Buffet

https://istio.io/docs/guides/bookinfo.html

Ingrédients

https://rust-lang.org/

Déclarer une struct


						struct V2 {
							x: f64,
							y: f64,
						}
					

						pub struct V2 {
							pub x: f64,
							pub y: f64,
						}
					

						fn main(){
							let v = V2{x: 10.0, y: 11_f64};
							println!("display v: {}", v);
						}
					

Ajouter une fonction


							impl V2 {
								fn to_string(&self) -> String {
									format!("V2 {{ x: {}, y: {} }}", self.x, self.y)
								}
							}
							//...
							println!("display v: {}", v.to_string());
					

Idiomatic


						error[E0277]: `V2` doesn't implement `std::fmt::Display`
						--> src/main.rs:8:31
							|
						8	|     println!("display v: {}", v);
							|                               ^ `V2` cannot be formatted with the default formatter; try using `:?` instead if you are using a format string
							|
							= help: the trait `std::fmt::Display` is not implemented for `V2`
							= note: required by `std::fmt::Display::fmt`
					

						use std::fmt;

						impl fmt::Display for V2 {
							fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
								write!(f, "V2 {{ x: {}, y: {} }}", self.x, self.y)
							}
						}
						//...
						println!("display v {}", v);
					

Dérivation automatique


						#[derive(Debug, Clone)]
						pub struct V2 {
							pub x: f64,
							pub y: f64,
						}
						println!("display v {}", v);
						println!("debug v {:?}", v);
						println!("debug pretty v {:#?}", v);
					

						display v V2 { x: 10, y: 11 }
						debug v V2 { x: 10.0, y: 11.0 }
						debug pretty v V2 {
							x: 10.0,
							y: 11.0
						}
					
  • une struct peut dériver un trait si tous ses composants le dérive déjà
  • une enum peut dériver un trait si toutes ses variantes peuvent le dériver

Générique


					fn twice(x: T) -> U
					where
						T: Add<Output=U>,
						T: Copy,
					{
						x + x
					}

					fn main() {
						let one = V2 { x: 1, y: 1 };
						println!("{:?}", twice(one));
					}
					

Déclarer un trait

  • un trait définit une fonctionnalité
    
    							pub trait ToString {
    								fn to_string(&self) -> String;
    							}
    							
  • un trait peut être générique sur ses paramètres / retours
    
    							pub trait Add {
    								type Output;
    								fn add(self, rhs: RHS) -> Self::Output;
    							}
    							

Déclarer un trait

  • un trait peut avoir une implementation par defaut
    
    							pub trait To42 {
    								fn to_42(&self) -> i32 { 
    									42
    								}
    							}
    
    							impl To42 for V2{}
    
    							println!("v.to_42 {}", v.to_42());
    							

Implémenter un operateur


						impl Add for V2 {
							type Output = V2;
							fn add(self, rhs: V2) -> V2 {
								V2 {
									x: self.x + rhs.x,
									y: self.y + rhs.y,
								}
							}
						}
						
						impl AddAssign for V2 {
							fn add_assign(&mut self, rhs: V2) {
								self.x += rhs.x;
								self.y += rhs.y;
							}
						}
					

Enum


						enum Colors {
							Blue,
							Yellow,
							Green,
						}
					

						enum Command {
							Help,
							Kill(i32),
							Run {
								path: String,
								options: Vec<String>,
							}
						}
					
  • permet de contrôler l'exhaustivité d'un pattern matching
  • Result<T, Err>, Option<T>

Macros


						macro_rules! print_twice {
							( $x:expr ) => {
								{
									println!("{:?}", twice($x));
								}
							};
						}
						print_twice!(V2 { x: 1, y: 1 });
						

Macros utiles

  • println!, eprintln!, format!
  • vec!
  • unreachable!, unimplemented!
  • assert!, assert_eq!
  • panic!, try!

Closure


					fn main() {
						let one = V2 { x: 1, y: 1 };
						let twice = |x| x + x;
						
						println!("{:?}", twice(one));
					}
					
  • bloc de code finissant par une expression
  • les types peuvent être inférés
  • peut prendre l'ownership de l'environment: move

Ownership / borrow

prendre l'ownership

						fn f(x: V2) {...}
					
emprunter

						fn f(x: &V2) {...}
					
emprunter et pouvoir modifier

						fn f(x: &mut V2) {...}
					

Langage

  • struct, traits, implementation, enum, generics
  • function, method, closure
  • operators, statement, expression
  • ownership, lifecycle
  • pattern matching
  • macros
  • modules
  • Result, Option, String, &str, Array, slice, Vec, Iterator, iXX, uXX, fXX
  • ...

Guides

Catalogues

Ustensiles

https://play.rust-lang.org/

http://rustup.rs/

curl https://sh.rustup.rs -sSf | sh
rustup default stable
rustup show
rustup update
rustup component add rustfmt-preview
rustup component add rls-preview rust-src rust-analysis

https://www.rust-lang.org/fr-FR/

rustup doc

http://devdocs.io/rust/

Dash, Zeal

Toolchain de base

rustc

								rustc --version
								rustc --help
							
rustfmt
rustfmt --help
rustdoc
rustdoc --help
cargo
cargo --help

"Cargo.toml"

https://doc.rust-lang.org/cargo/reference/manifest.html


						[package]
						name = "hello"
						version = "0.1.0"
						authors = ["name <my@email.com>"]

						[dependencies]
					

".gitignore"


						# Generated by Cargo
						# will have compiled files and executables
						/target/
						
						# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
						# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
						Cargo.lock
						
						# These are backup files generated by rustfmt
						**/*.rs.bk
					

https://github.com/github/gitignore/blob/master/Rust.gitignore

Cargo


						.
						├── Cargo.lock
						├── Cargo.toml
						├── build.rs
						├── benches
						│   └── large-input.rs
						├── examples
						│   └── simple.rs
						├── src
						│   ├── bin
						│   │   └── another_executable.rs
						│   ├── lib.rs
						│   └── main.rs
						└── tests
						    └── some-integration-tests.rs

Cargo


						cargo doc
						open target/doc/hello/index.html
					

						/// This comment documents the `foo` function.
						///
						/// You can use the `rustdoc` tool via `cargo doc` to generate
						/// HTML documentation from these comments!
						fn foo() {
							// ...
						}
					

Cargo

~/.cargo/bin
cargo install ripgrep https
cargo install cargo-watch cargo-make mdbook
cargo install --list

IDE & Editor

Clippy

A collection of lints to catch common mistakes and improve your Rust code.
install

								rustup install nightly
								cargo +nightly install --force clippy
							
run

								cargo +nightly clippy
							

Débuggueur

via GDB / LLDB

Debugging Rust programs with lldb on MacOS | Bryce Fisher-Fleig

Profileur

Benchmarks

  • libtest benchmarks
    
    								cargo +nightly bench
    							
  • with Criterion.rs
    
    								cargo bench
    
    								Running target/release/deps/bench1-0a8dde0ccacc35ee
    								fib 20                  time:   [25.800 us 26.114 us 26.453 us]
    								Found 3 outliers among 100 measurements (3.00%)
    								  1 (1.00%) high mild
    								  2 (2.00%) high severe
    
    								Running target/release/deps/bench1-0a8dde0ccacc35ee
    								fib 20                  time:   [25.662 us 25.993 us 26.302 us]
    								                        change: [-5.4472% -3.3608% -1.1502%] (p = 0.00 < 0.05)
    								                        Performance has improved.
    								Found 5 outliers among 100 measurements (5.00%)
    								  5 (5.00%) high mild
    							

https://crates.io/

  • errors: failures (error_chain)
  • logging: env_logger, slog, ...
  • monitoring: prometheus
  • tracing: opentracing
  • marchaling: serde (rustc-serialize), protobuff
  • protocol: http, gRPC, tower
  • web: hyper, reqwest, ...
  • concurrent: thread, rayon, futures/tokio, may, simd
  • db client: diesel, r2d2, postgres, mongodb, dgraph, ...
  • os: (redox-os)

Préparation

Let's code

https://bit.ly/rust-cook


						git clone https://github.com/davidB/labs_cooking_microservices_with_rust rust-cook
						cd rust-cook/reviews
					

Serveur HTTP

Cuisson

Tests

  • tests unitaires
  • découpage en binaire + library
  • test d'intégration
  • test de doc

Travis CI


						# .travis.yml
						language: rust
						rust:
							- stable
							- beta
							- nightly
						matrix:
							allow_failures:
								- rust: nightly
							fast_finish: true
							include:
								- rust: stable
									env: RUSTFMT
									install:
										- rustup component add rustfmt-preview
									script:
										- cargo fmt -- --write-mode=diff
						cache: cargo
					

Configuration

  • chargée de l'environment : std::env
  • chargée de la ligne de commande
    • clap-rs : permet de tout configurer
    • docopt.rs : génerer à partir de la doc usage
  • struct Config disponible avec lazy_static

Logs

  • log : fonctions de base
  • env_logger : configuration minimale, permet de choisir le niveau de log par variable d'environment
  • fern : beaucoup de configuration possible
  • slog : beaucoup de points d'extension possible (liste)

HealthCheck/Liveness

  • exposé /health
  • time pour avoir un timestamp
  • chrono pour avoir une date formatée
  • env! pour accèder à certaines des informations de build
  • script de build pour le reste (git ref, date de build, ...)

Readiness

  • exposé /ready (200 vs 503)

Métriques

  • exposé /metrics (à la main)
  • crates prometheus, statsd
  • via log, telegraph, feats,...

Tracabilité

  • 2 libs pour OpenTracing + 2
  • propagation du contexte explicite

Dressage

Services

https://istio.io/docs/guides/bookinfo.html

"Dockerfile"

see https://github.com/michiel/docker-rust-microservice


						rustup target add x86_64-unknown-linux-musl
						cargo build --target x86_64-unknown-linux-musl --release
					

"Dockerfile" multi-stage


						# from https://blog.sedrik.se/posts/my-docker-setup-for-rust/
						FROM ekidd/rust-musl-builder as builder
						
						WORKDIR /home/rust/
						
						# Avoid having to install/build all dependencies by copying
						# the Cargo files and making a dummy src/main.rs
						COPY Cargo.toml .
						COPY Cargo.lock .
						RUN echo "fn main() {}" > src/bin.rs
						RUN echo "" > src/lib.rs
						# RUN cargo test
						RUN cargo build --release
						
						# We need to touch our real main.rs file or else docker will use
						# the cached one.
						COPY . .
						RUN sudo touch src/bin.rs src/lib.rs
						RUN sudo chown -R  rust /home/rust
						
						# RUN cargo test
						RUN cargo build --release
						
						# Size optimization & rename to main
						RUN strip target/x86_64-unknown-linux-musl/release/reviews -o target/x86_64-unknown-linux-musl/release/main
						
						# Start building the final image
						FROM scratch
						WORKDIR /home/rust/
						COPY --from=builder /home/rust/target/x86_64-unknown-linux-musl/release/main .
						ENTRYPOINT ["./main"]
					

".dockerignore"


						target
						Dockerfile
						.dockerignore
						.git
						.gitignore
						README*
					

Docker


						docker build -t reviews:v10 .
						docker run -p 9080:9080 -e "RUST_LOG=info" --rm --name reviews reviews:v10
						docker stop reviews
					

Déployement

  • l'executable natif
  • image docker, ami, packer
  • repo, helm, spinnaker...

Dégustation

Troll en vrac

  • taille des images docker
  • garbage collector vs ownership
  • jit
  • temps de démarrage (et pas de chauffage)
  • courbe d'apprentissage
  • syntax et paradigmes "modernes"
  • maturité
  • ecosystem, support

Introduire Rust

  • cli tools: ripgrep (rg), exa, xsv...
  • server & microservices: business, tooling (infra, security, build, test)
    (conduit, chaos manager, i'Krelln)
  • side-car, agent, proxy services
  • function (FaaS)
  • webclient: "target webassembly"
  • embedded
  • fragment code optimisé (pour intégration)
  • databases: mentat, tidb
  • https://www.rust-lang.org/en-US/friends.html

Jouer avec Rust

  • Contribuer a des crates opensource
  • CodinGame, ...
  • exercism.io
  • ???

Merci

http://bit.ly/rust-cook

Premier service: Ratings


						curl localhost:9080/ratings/0
						{
							"id" : 0,
							"ratings" : {
								"Reviewer2" : 4,
								"Reviewer1" : 5
							}
						}
  • gotham
  • serde
  • lazy_static, Arc, Mutex

code

gotham ratings-gotham
  • https://gotham.rs
  • router les requêtes
  • retourner une réponse
  • lire le body d'une requête
serde ratings-serde

code

lazy_static, Arc, Mutex ratings-db
  • https://crates.io/crates/lazy_static
  • variable statique initialisée à sa première utilisation
  • Choisir ses garanties
  • Atomic reference count + Mutex : tout le monde peut devenir owner et modifier les données dans un environment multithread
  • Box, Cow, Rc, Arc, Cell, RefCell, Mutex, RwLock

code

  • ratings-tests
  • ratings-config
  • ratings-logs
  • ratings-healthcheck

Second service: Reviews


						curl localhost:9080/reviews/0
						{
							"id" : 0,
							"reviews" : [
								{
									"text" : "An extremely entertaining play by Shakespeare.",
									"rating" : { "color" : "blue", "stars" : 5 },
									"reviewer" : "Reviewer1"
								},
								...
							]
						}
					
  • actix-web, actix
  • diesel
  • chaînage de futures

code

actix-web reviews-actix-web
  • https://github.com/actix/actix-web
  • router les requêtes
  • retourner une réponse
  • les extractors
actix reviews-actix
  • https://github.com/actix/actix
  • créer un acteur et un type de message
  • envoyer un message et attendre la réponse

code

diesel reviews-diesel
  • http://diesel.rs
  • diesel_cli
    
    								cargo install diesel_cli
    								DATABASE_URL=test.sqlite diesel setup
    							
  • écrire la première migration
    
    									diesel migration generate reviews
    									DATABASE_URL=test.sqlite diesel migration run
    									DATABASE_URL=test.sqlite diesel print-schema > src/schema.rs
    								
  • écrire une requête

code

les futures reviews-futures
  • chainer les futures : map, and_then
  • définir ses erreurs : failures
  • gérer les erreurs : from_err, map_err, or_else
  • retourner un future : Box

Reviews & Graphql



						curl http://127.0.0.1:9080/graphql -d '{"query":"{products{id,reviews{text,rating{stars}}}}"}'
						{
							"data" : {
								"products" : [
									{
										"reviews" : [
										{
											"text" : "An extremely entertaining play by Shakespeare.",
											"rating" : { "stars" : 5 }
										},
										...
										]
									}
								]
							}
						}
					
  • juniper
  • attendre le futur, reqwest

code

juniper graphql-juniper
  • version simple : tout en dérivation automatique
  • endpoint graphql, graphiql (+ cors)
bloquant graphql-data
  • attendre le futur de la requête à la base de données
  • reqwest pour requête http bloquante

code

juniper graphql-filter
  • définit le trait manuellement
  • permet de ne récupérer les données d'un champ que si il est demandé