Статьи

Создание эффективных микросервисов в Rust и Diesel

Руст является одним из самых обсуждаемых языков сейчас. Хотя язык только что выпал 1.0 в прошлом мае, он быстро развился в стабильности и функциях. Благодаря этому быстрому, но стабильному росту, Rust действительно привлек внимание многих в мире программирования. Что же делает Rust таким привлекательным? Это стоит нашего времени и внимания?

Что делает Rust действительно захватывающим для меня, так это то, что он начинает называться альтернативой C / C ++. Лично я никогда особо не заботился о дизайне C и C ++, но Rust, кажется, затрагивает многие вещи, которые мне не нравились в этих языках. Что еще более важно, меня снова порадовало низкоуровневое программирование систем!

Как Rubyist, я невероятно заинтересован в таких проектах, как Helix, которые позволяют разработчикам использовать Rust в качестве средства для расширения функциональности библиотек Ruby (вместо C). С такими идеями, как жесты и взросление Helix, Rust вполне может стать неотъемлемой частью стеков на основе Ruby в ближайшем будущем.

В сообществе Rust происходит много событий, о которых стоит поговорить. Однако, хотя я думаю, что будущее Rust является захватывающим, важно посмотреть, как Rust используется сегодня .

Ранее я писал о том, что добавление довольно нового языка или фреймворка в ваш стек может стать довольно большим риском. Разрывные изменения, как правило, происходят в более молодых языках, и Rust не является исключением. Несмотря на то, что такие компании, как Dropbox и Mozilla, реализуют значительные проекты на основе Rust, я по-прежнему считаю, что для нас разумнее идти на меньший риск, чем на них.

Именно здесь микросервисы имеют сильное преимущество в вашей архитектуре. Они позволяют вам рисковать чем-то новым, таким как Rust, и внедрять его в свою экосистему при довольно низких затратах. Опять же, это не устраняет риск и опасности использования более молодого языка. Тем не менее, это, безусловно, позволяет вам рисковать в меньших дозах.

Идея, которую я хочу исследовать, состоит в том, как было бы создать небольшой микросервис в Rust. Насколько это сложно? Это вообще возможно? Какой инструмент мне понадобится? Есть здоровый список вопросов, которые идут с этой идеей.

Итак, вот что мы сделаем: мы предполагаем, что у нас есть приложение для блога с базовым API. Хотя это звучит немного глупо, мы собираемся создать сервис Rust, который использует этот API и сохраняет каждый пост в базе данных. Предполагая, что у нас есть проект API блога, который выглядит примерно так, давайте используем Rust для создания чего-то великолепного!

Выращивание ржавчины

Прежде чем мы начнем что-либо из этого, нам нужно найти лучший способ представить Rust в нашей среде программирования.

Есть несколько действительно простых способов настроить Rust. Тем не менее, я лично использую RustUp для управления версиями Rust. Это очень похоже на RvM в Ruby или NVM в Node.

Что удивительно в RustUp, так это его способность легко переключаться между двумя каналами выпуска Rust. Для тех, кто не знает, Rust, по сути, имеет два экземпляра своего языка. Один экземпляр (или канал) известен как Стабильный, который содержит стабильные функции и функциональные возможности. Другой известен как Nightly, который содержит функции и функциональные возможности.

Для того, что мы строим сегодня, я буду использовать Nightly. Это может показаться странным выбором, поскольку мы стремимся создать что-то более «стабильное». Однако в Rust планировать будущее будет несколько разумнее, поскольку мы знаем, что многие из этих функций в конечном итоге найдут свое отражение в Stable Rust.

Чтобы установить это в RustUp:

1
2
3
rustup install stable
rustup install nightly
rustup default nightly

Это должно установить Nightly в нашей цепочке для ключей и сделать его источником Rust по умолчанию для любых новых проектов, которые мы создаем. Не беспокойтесь, если хотите использовать Stable. Вы сможете вернуться к нему, когда захотите!

Теперь, чтобы создать проект Rust через Cargo (собственный менеджер пакетов Rust):

1
cargo new rustweb

Это должно создать действительно простой проект Rust в вашем текущем каталоге. Если вы Cargo.toml проект, вы увидите, что он содержит файл с именем Cargo.toml . Здесь будут храниться наши зависимости для файла.

Cargo.toml очень похож на то, что мы видели в package.json NPM или в Gemfile Rails. Файл перечисляет зависимости проекта, и наши замечательные инструменты помогают нам выяснить все остальное.

Зажигать Руст с Дизелем

Удивительная группа разработчиков программного обеспечения недавно создала среду Rust, известную как Diesel . Дизель интересен тем, что он черпает вдохновение в дизайне ORM от Rails Active Record. Из-за моего опыта в Rails я нашел Diesel действительно привлекательным и простым в использовании.

Дизель требует немного дополнительной настройки, но он позволяет нам относительно легко запрашивать данные в базе данных по нашему выбору. Это также поможет нам создать стабильную основу для построения нашего микросервиса. Давайте взглянем.

Настройка дизеля

Мы будем внимательно следить за опубликованным руководством Diesel . Тем не менее, мы создадим наш сервис на середине пути. Если что-то, что я говорю, смущает вас, пожалуйста, обратитесь к их руководству для ясности.

Чтобы ввести Diesel и другие зависимости в наш проект Rust, нам нужно обновить наш Cargo.toml чтобы показать следующее:

Cargo.toml

1
2
3
4
5
6
[dependencies]
diesel = "0.8.0"
diesel_codegen = { version = "0.8.0", features = ["postgres"] }
dotenv = "0.8.0"
hyper = { version = "0.9", default-features = false, features = ["security-framework"] }
rustc-serialize = "0.3.19"

Чтобы включить эти библиотеки в наш проект, запустите cargo build . Это будет не только извлекать библиотеки, но и создавать на них экземпляр нашего приложения. Это особенно полезно для определения того, какие зависимости работают и не работают с вашей базой кода.

Хотя теперь у нас есть библиотека Diesel, интегрированная в наше приложение Rust, нам все еще нужны средства для управления ею через командную строку. Для этого нам нужно установить diesel_cli , запустив: cargo install diesel_cli .

Далее мы расскажем Diesel, где искать нашу базу данных. В нашем случае мы создаем микросервис с собственной новой базой данных. Для нашего приложения я рекомендую использовать Postgres . Тем не менее, дизель предназначен для обработки различных вариантов базы данных, если вы хотите что-то другое

Мы rustweb нашу базу данных: rustweb .

1
echo DATABASE_URL=postgres://username:password@localhost/rustweb > .env

Чтобы завершить нашу настройку, мы собираемся запустить diesel setup чтобы закончить работу. Это должно настроить наш проект и среду для правильной работы с инфраструктурой Diesel. После нескольких простых команд мы готовы приступить к составлению основы нашей базы данных.

Создание нашей базы данных

Поскольку наше приложение представляет собой микросервис, мы предполагаем, что наш проект зависит от какого-либо другого приложения. Является ли это приложение гигантским монолитом или серией небольших микросервисов, нам необходимо спроектировать этот микросервис так, чтобы он мог переводить и хранить данные из других источников. Эффективные микросервисы похожи на взаимозаменяемые части, и наше приложение должно отражать эту модульность.

Мы собираемся начать этот процесс с создания таблицы posts которой будут храниться посты блога, которые мы потребляем через внешний API. Чтобы построить это в Diesel, мы введем diesel migration generate create_posts .

Генератор создаст up.sql и down.sql , которые помогут нам построить базу данных. Я не уверен, насколько вам удобно писать чистый SQL, но Diesel — отличное место для начала обучения.

В нашей миграции up.sql мы собираемся написать логику для создания фактической таблицы posts в нашей базе данных. Учитывая абстрактную модель Post , она, вероятно, должна выглядеть примерно так.

1
2
3
4
Post
- id (int)
- title (string)
- body (text / string)

Мы напишем миграцию SQL в соответствии с:

up.sql

1
2
3
4
5
CREATE TABLE posts (
    id SERIAL PRIMARY KEY,
    title VARCHAR NOT NULL,
    body TEXT NOT NULL
)

Прежде чем мы продолжим, давайте поговорим о примитивных структурах данных Rust.

Rust использует низкоуровневые типы данных, очень похожие на C и C ++. Это может показаться немного странным для тех, кто имеет дело с чем-то вроде Ruby или JavaScript. Тем не менее, это не помешает научиться работать с различными примитивами и структурами данных. Rust дает вам больше контроля над данными, с которыми вы работаете. Тем не менее, это требует еще несколько шагов, чтобы правильно обрабатывать все.

Наш down.sql будет выглядеть намного менее пугающим, потому что у него очень простая задача: сбросить таблицу posts .

down.sql

1
DROP TABLE posts

Чтобы создать таблицы в базе данных, нам нужно запустить diesel migration run .

Это оно! Теперь у нас есть база данных, которую мы хотим. Теперь, когда у нас есть базовая структура базы данных, давайте разберемся с внутренней логикой нашего приложения Diesel.

Построение логики библиотеки

Помните остальные файлы, которые были сгенерированы Cargo? Мы собираемся начать с src/lib.rs Это наш библиотечный файл, который мы собираемся использовать для соединения всех отдельных частей вместе. Думайте об этом как о нашем основном методе для всего нашего приложения.

Для начала нам нужно написать что-то, чтобы связать нас с базой данных, которую мы только что создали. Мы также хотим построить наши модели и импортировать нашу схему. Мы собираемся сделать все это одним махом:

src/lib.rs

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#![feature(proc_macro)]
 
#[macro_use] extern crate diesel;
#[macro_use] extern crate diesel_codegen;
extern crate dotenv;
 
pub mod schema;
pub mod models;
 
use diesel::prelude::*;
use diesel::pg::PgConnection;
use dotenv::dotenv;
use std::env;
 
use self::models::{Post, NewPost};
 
pub fn establish_connection() -> PgConnection {
    dotenv().ok();
 
    let database_url = env::var("DATABASE_URL")
        .expect("DATABASE_URL must be set");
    PgConnection::establish(&database_url)
        .expect(&format!("Error connecting to {}", database_url))
}
 
pub fn create_post(conn: &PgConnection, title: &str, body: &str) -> Post {
    use schema::posts;
 
    let new_post = NewPost {
        title: title,
        body: body,
    };
 
    diesel::insert(≠w_post).into(posts::table)
        .get_result(conn)
        .expect("Error saving new post")
}

Пока у нас есть целая куча содержимого заголовка и две функции. Похоже, много настроек для нескольких методов.

Однако, думайте об этом как о домашней базе для нашего приложения. Все, что мы делаем, будет проходить через это. Это важно, потому что это связывает все это вместе.

Стоит указать, как метод .env просто берет URL базы данных через .env нами .env и пытается подключиться к нему через Postgres. Это довольно просто!

Если вы внимательно посмотрите на раздел, который имеет:

1
2
pub mod schema;
pub mod models;

Вы начинаете задаваться вопросом, где на самом деле находятся разделы схемы и модели. Ну, мы собираемся написать их!

src/models.rs

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
use schema::posts;
 
#[derive(Queryable)]
pub struct Post {
    pub id: i32,
    pub title: String,
    pub body: String
}
 
#[derive(Insertable)]
#[table_name="posts"]
pub struct NewPost<'a> {
    pub title: &'a str,
    pub body: &'a str,
}

Это должно создать объект Post возможностью запроса и NewPost объект NewPost . Мы будем использовать эти фрагменты данных в тандеме для вставки и запроса информации в нашей базе данных. В целом, это довольно простой файл. То же самое можно сказать и о файле schema.rs :

src/schema.rs

1
infer_schema!("dotenv:DATABASE_URL");

Все, что делает этот файл — это .env схему базы данных из созданного ранее файла .env , содержащего URL базы данных. Это дает нам важную связь для взаимодействия с нашей базой данных. Это короткий и приятный пример того, насколько просты Rust и Diesel!

Возвращаясь к нашему файлу lib.rs , у нас есть функция create_post .

01
02
03
04
05
06
07
08
09
10
11
12
pub fn create_post(conn: &PgConnection, title: &str, body: &str) -> Post {
    use schema::posts;
 
    let new_post = NewPost {
        title: title,
        body: body,
    };
 
    diesel::insert(≠w_post).into(posts::table)
        .get_result(conn)
        .expect("Error saving new post")
}

create_post интересен тем, что принимает соединение Postgres, атрибут title и body и создает из NewPost объект NewPost . Мы используем нашу схему здесь, чтобы определить, куда вставить объект NewPost .

Написание исполняемых скриптов

Теперь, когда мы создали действительно прочную основу, давайте напишем код Rust, который напрямую взаимодействует с нашим API.

Проекты библиотеки Rust работают так, что мы создаем исполняемые файлы в папке /bin/ . Затем мы берем фрагменты кода из нашей библиотеки и используем их для выполнения задач. Чтобы продемонстрировать, мы собираемся создать следующее:

get_posts_from_source.rs

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
extern crate rustweb;
extern crate diesel;
extern crate hyper;
extern crate rustc_serialize;
 
use self::rustweb::*;
 
use hyper::{Client};
use std::io::Read;
use rustc_serialize::json;
use std::env::args;
 
// Automatically generates traits to the struct
#[derive(RustcDecodable, RustcEncodable)]
pub struct PostSerializer {
    id: u8,
    title: String,
    body: String,
}
 
fn main() {
    let start_s = args().nth(1).expect("Please provide a min id");
    let start : i32 = match start_s.parse() {
        Ok(n) => {
           n
       },
       Err(_) => {
           println!("error: first argument not an integer");
           return;
       },
   };
    let stop_s = args().nth(2).expect("Please provide a max id");
    let stop : i32 = match stop_s.parse() {
        Ok(n) => {
           n
       },
       Err(_) => {
           println!("error: second argument not an integer");
           return;
       },
   };
    for x in start..stop {
        let url = format!("http://localhost:3000/api/v1/posts/{}", x);
        let response = get_content(&url).unwrap();
        let decoded: PostSerializer = json::decode(&response).unwrap();
        create_post_from_object(&decoded);
    }
}
 
fn get_content(url: &str) -> hyper::Result<String> {
    let client = Client::new();
    let mut response = try!(client.get(url).send());
    let mut buffer = String::new();
    try!(response.read_to_string(μt buffer));
    Ok(buffer)
}
 
fn create_post_from_object(post: &PostSerializer) {
    let connection = establish_connection();
    println!("==========================================================");
    println!("Title: {}", post.title);
    println!("==========================================================\n");
    println!("{}\n", post.body);
    create_post(&connection, &post.title, &post.body);
}

Уф. Это очень важно. Давайте разберем его на части, начиная с модели заголовка и сериализатора:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
extern crate rustweb;
extern crate diesel;
extern crate hyper;
extern crate rustc_serialize;
 
use self::rutweb::*;
 
use hyper::{Client};
use std::io::Read;
use rustc_serialize::json;
use std::env::args;
 
// Automatically generates traits to the struct
#[derive(RustcDecodable, RustcEncodable)]
pub struct PostSerializer {
    id: u8,
    title: String,
    body: String,
}

Что интересно в этом фрагменте кода, так это то, что мы делаем много вещей, которые мы видели раньше. Мы импортируем наш проект и дизель. Тем не менее, есть несколько других людей, вступающих в партию сейчас. Этот файл использует библиотеку Rust Hyper и функциональность Rust Serializer .

Эти дополнительные библиотеки позволяют нам запрашивать и сериализовать ответ JSON с сервера. Гипер делает запрос. Сериализатор выполняет сериализацию. Достаточно просто.

Далее мы рассмотрим get_content нашего файла get_content и create_post_from_object :

1
2
3
4
5
6
7
fn get_content(url: &str) -> hyper::Result<String> {
    let client = Client::new();
    let mut response = try!(client.get(url).send());
    let mut buffer = String::new();
    try!(response.read_to_string(μt buffer));
    Ok(buffer)
}

get_content использует библиотеку Hyper для извлечения данных с нашего локального URL. Он читает ответ и переводит его в строку, понятную Rust. Если все это успешно, он вернет String JSON-представление объекта API Post.

1
2
3
4
5
6
7
8
fn create_post_from_object(post: &PostSerializer) {
    let connection = establish_connection();
    println!("==========================================================");
    println!("Title: {}", post.title);
    println!("==========================================================\n");
    println!("{}\n", post.body);
    create_post(&connection, &post.title, &post.body);
}

create_post_from_object принимает сериализованный объект Post и устанавливает соединение с нашей базой данных. Затем он использует нашу функцию create_post для создания объекта Post в базе данных!

Наконец, мы увидим, как наш метод main() связывает все эти функции вместе.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
fn main() {
    let start_s = args().nth(1).expect("Please provide a min id");
    let start : i32 = match start_s.parse() {
        Ok(n) => {
           n
       },
       Err(_) => {
           println!("error: first argument not an integer");
           return;
       },
   };
    let stop_s = args().nth(2).expect("Please provide a max id");
    let stop : i32 = match stop_s.parse() {
        Ok(n) => {
           n
       },
       Err(_) => {
           println!("error: second argument not an integer");
           return;
       },
   };
    for x in start..stop {
        let url = format!("http://localhost:3000/api/v1/posts/{}", x);
        let response = get_content(&url).unwrap();
        let decoded: PostSerializer = json::decode(&response).unwrap();
        create_post_from_object(&decoded);
    }
}

Мы собираемся перебрать X раз и создать объекты Post в диапазоне записей в нашем API. Мы будем использовать PostSerializer который мы только что создали, чтобы организовать наши данные в то, что Diesel может потреблять.

Предполагая, что у нас есть 10 сообщений в нашей базе данных API, мы можем запустить скрипт, набрав в вашей консоли следующее:

cargo run --bin get_posts_from_source 1 10

Вы должны увидеть наш код в действии, вывести данные постов API и сохранить их в нашей базе данных rustweb!

Давайте распакуем все, что мы здесь прошли.

Завершение

Мы просмотрели много материала, и он может быть ошеломляющим или слишком сложным. Тем не менее, я призываю вас использовать это, чтобы понять, подходит ли вам написание микросервиса Rust. Думайте об этом как о наброске идеи с надеждой, что она станет невероятно полезной.

Если вам интересно, где я возьму эту идею в будущем, следите за проектом на GitHub ! Я хотел бы увидеть некоторые отзывы и советы о том, как я делаю.

Ржавчина может занять некоторое время, чтобы овладеть. Тем не менее, это невероятно полезный язык для изучения и изучения. Возможно, в процессе обучения вы сможете найти что-то, что принесет пользу вам и вашей организации на долгие годы!

Ссылка: Создание Expedient Microservices в Rust and Diesel от нашего партнера JCG Тейлора Джонса в блоге Codeship Blog .