意外?!サーバー言語として優れているRust

2020/07/23 07:52
この記事は、著者の許可を得て配信しています。
https://stu2b50.dev/posts/rust-is-surpris76171

序文

ある時点で、私はブログや他のページの古い静的サイトジェネレーターのセットアップに飽きてしまいました。修正をしようと思ったときに毎回 ssh するのは面倒だったし、画像をすべて sftp や sshfs したりするのも面倒でした。そして、もしあなたが誰かに何かを書かせたり、編集をさせたりしようとした場合、「おい、俺のサーバーで君をユーザーにするから、君のパブリックキーを教えてくれよ。そうすればsshしてもいいよ」と言われても、ほとんどの人は全く嬉しくないでしょう。

私はもう少しダイナミズムのあるものが欲しかったです。

だから、それはプロジェクトだったのです。この小さなスコープのブログは、すでに信頼されている数人のユーザーが作ることができるもので、編集し、マークダウンで新しいページを投稿することができます(SimpleMDEの素敵なマークダウンエディタのおかげで)。さらに、私はimgurのジャンクバージョンの中に構築したいので、ちゃんと自分のニーズを満たすことができます。

だから、Django を使って何かを作ることができるし、他の言語で実験することもできます。このプロジェクトはシンプルなので、どの言語のエコシステムにも制限されることは想像できません。そして、私は Rust で何か実質的なものを書きたくてうずうずしていました...

どのフレームワークがいいのか?

最大のフレームワークはおそらく actix-web です。しかし、

  1. 数ヶ月前に自分の選択肢を模索していた時に、actix-webのメンテナーが劇的な事件で辞めてしまいました。
  2. 少なくともドキュメントを読む限りでは、テンプレート化されたHTMLを提供するサーバーよりもAPIに適しているようです。
  3. 上記のように、私はこれが毎週のプロジェクトではなく、週末のプロジェクトであることを望んでいたので、一式揃っている方がいいのです。
  4. どの非同期ライブラリが優れていると考えられているかを把握したいとは思いません。そして、それぞれの非同期ライブラリには、その非同期ライブラリでしか動かない独自のライブラリのエコシステムがあることに注意してください。そのため一旦それに決めてしまうと後で覆すのは難しいです。

そう、Rocketです。

メリット

このプロジェクトを構想し始めるまで気づかなかったことがあります。それは、サーバー上では、メモリモデルは実際にはとてもシンプルだということです!状態の多くはデータベースで処理されます。

状態の多くはデータベースで処理されます。実際に借用チェッカと争ったことはありません。争う必要もなかったのですが。ほとんどの場合、すべてのものにはたった一つの所有者とたった一つのライフタイム、リクエストを処理する関数があります。

Rocketもまた、驚くべき「魔法」を使えます。

#[get("/posts/<slug>/"]
pub fn post_view(slug: String) -> Option<Template> {
    ...
		
    Some(Template::render("/posts/post", hashmap! { "post" => post}))
}

Flaskとは対照的です。

@app.route("/posts/<string:slug>")
def post_view(slug):
    ...
		
    return render_template("posts/post.html", post=post)

Rustのマクロシステムには今のところ本当に感銘を受けています。ものすごく上手くいく場合いが多いだけでなく、すべて静的に型付けされてコンパイルされています。

Rocketに最も近いアナログはflask + flaskに隣接するすべてのライブラリ (SQLAlchemy-flaskなど) です。Rocketは、サードパーティインテグレーションの力によって、2つのテンプレートエンジン(ハンドルバーとTera、これは基本的にはJinja2です)、かなりの数のORMs/DBドライバのデータベースプーリングサポートなどを備えています。

とはいえ、まだ自分でauthをロールバックしなければならない段階です。

Django/Rails との比較について聞いたことがありますが、Rustがその方向に進んでいるようには見えません。Django/Rails は意図的に開発者であるあなたを比喩的なレールの上に置き、ファイルの行き先からモデルやビューの更新方法まで、ベストプラクティスを指示します。Rocket はそんなことはしませんし、これからもするべきだとは思いません。

また、私は「コンパイルすれば動く」という経験をたくさんしてきました。私のランタイムエラーのほとんどはテンプレートにありましたが、ちなみにこれは静的に型付けされていない唯一のものです。

私はそこにとても驚きました。「うまくいく!」ということが本当にたくさんありました。 決まりきったシンタクスはあまりないし、型推論は関数をクリーンに保つし、私はライフタイムアノテーションを一度も書いていません。私のrustサーバーはflaskサーバーやDjangoサーバーとそれほど変わらないし、正直言ってJavaサーバーよりもクリーンに見えます。ガベージコレクタもランタイムもありません。

デメリット(そこまでひどくはありません)

次は、私が見た限りでは最も完成度の高いORMであるDIESELの話をします。不満はあるが、「客観的に見て」デメリットではありません。私にはそれがトレードオフだと仮定しています。

1 つは、それは各テーブルのための 2 つの構造体を作ることですが、少し面倒くさいです。テーブルの代わりになる構造体と、挿入するための構造体が必要になります (主なキーなどの自動生成されたカラムはすべて削除されています)。たとえば、次のようなものがあります。

#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Serialize)]
#[belongs_to(BlogPosts, foreign_key="post_id")]
#[table_name = "tags"]
pub struct Tag {
    id: i32,
    tag_name: String,
    post_id: i32,
}

#[derive(Insertable)]
#[table_name = "tags"]
pub struct InsertTag {
    tag_name: String,
    post_id: i32
}

さらに、いくつかのORMではテーブルモデルを書き、ORMがSQLマイグレーションを生成しますが、DieselではSQLマイグレーションを手書きで書き、ORMがマッピングを含むschema.rsファイルを生成します。実は私はこの点はそこまで気にしていません。

また、Diesel は親子関係にしか対応していないので、かなり明示的にしなければなりません。親には、魔法のように子のリストを表示するフィールドはありません。クエリを書いてそれを呼び出すだけです。ある意味では、より洗練されたクエリビルダーを使っているようなものです。

魔法のようなレベルを下げること、それ自体は本当に悪いことではありません。明示的にすることで、ユーザーがその魔法を信じすぎて、N+1を選択するような墓穴を掘ることを防ぐことができます。

しかし、それで自分も少しは遅くなったと言うつもりはありません。あと正直joinを書くのがめちゃくちゃ面倒くさかったのです。それはそれで仕方ないのかもしれませんが、それもまたNoSQLデータベースを生成した原因になったのかもしれません。🤷

見にくくさ

flaskで画像をアップロードする方法は以下の通りです。

@app.route('/images/upload')
def upload_file():
	files = request.files['file']
	if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))

ここでは、サードパーティのライブラリを使用しながら、"よりシンプルな"例を示しています。

こちらで全体を見ていただけます。

#[post("/upload", data = "<data>")]
// signature requires the request to have a `Content-Type`
fn multipart_upload(cont_type: &ContentType, data: Data) -> Result<Stream<Cursor<Vec<u8>>>, Custom<String>> {
    // this and the next check can be implemented as a request guard but it seems like just
    // more boilerplate than necessary
    if !cont_type.is_form_data() {
        return Err(Custom(
            Status::BadRequest,
            "Content-Type not multipart/form-data".into()
        ));
    }

    let (_, boundary) = cont_type.params().find(|&(k, _)| k == "boundary").ok_or_else(
            || Custom(
                Status::BadRequest,
                "`Content-Type: multipart/form-data` boundary param not provided".into()
            )
        )?;

    match process_upload(boundary, data) {
        Ok(resp) => Ok(Stream::from(Cursor::new(resp))),
        Err(err) => Err(Custom(Status::InternalServerError, err.to_string()))
    }
}

fn process_upload(boundary: &str, data: Data) -> io::Result<Vec<u8>> {
    let mut out = Vec::new();

    // saves all fields, any field longer than 10kB goes to a temporary directory
    // Entries could implement FromData though that would give zero control over
    // how the files are saved; Multipart would be a good impl candidate though
    match Multipart::with_body(data.open(), boundary).save().temp() {
        Full(entries) => process_entries(entries, &mut out)?,
        Partial(partial, reason) => {
            writeln!(out, "Request partially processed: {:?}", reason)?;
            if let Some(field) = partial.partial {
                writeln!(out, "Stopped on field: {:?}", field.source.headers)?;
            }

            process_entries(partial.entries, &mut out)?
        },
        Error(e) => return Err(e),
    }

    Ok(out)
}

さて、公平に見て、Rocketはバージョン0.4.5です。このgithubの件から、マルチパートフォームのサポートは0.5.0になるようです。しかし、現在のライブラリがまだまだ未熟であることに変わりはありません。特に、純粋な API サーバーや SPA とは対照的に、テンプレート化された HTML を提供する従来の Web サーバーのいくつかのエッジ機能を欠いています。


Rust のエラーは通常、非常に優れています。しかし、それは、もう少し多くのことをしようとするライブラリに入る前の話です。私はRocketや Dieselのマクロで...興味深いエラーメッセージに何回か遭遇しました。例えばこれを見てください。

the trait bound `(i32, std::string::String, std::string::String, std::string::String, i32, i32, std::string::String, i32, i32): diesel::Queryable<diesel::sql_types::Nullable<(diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Text, diesel::sql_types::Text, diesel::sql_types::Integer, diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Integer, diesel::sql_types::Integer)>, diesel::sqlite::Sqlite>` is not satisfied

the trait `diesel::Queryable<diesel::sql_types::Nullable<(diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Text, diesel::sql_types::Text, diesel::sql_types::Integer, diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Integer, diesel::sql_types::Integer)>, diesel::sqlite::Sqlite>` is not implemented for `(i32, std::string::String, std::string::String, std::string::String, i32, i32, std::string::String, i32, i32)`

help: the following implementations were found:
        <(A, B, C, D, E, F, G, H, I) as diesel::Queryable<(SA, SB, SC, SD, SE, SF, SG, SH, SI), __DB>>
note: required because of the requirements on the impl of `diesel::Queryable<diesel::sql_types::Nullable<(diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Text, diesel::sql_types::Text, diesel::sql_types::Integer, diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Integer, diesel::sql_types::Integer)>, diesel::sqlite::Sqlite>` for `posts::BlogPosts`
note: required because of the requirements on the impl of `diesel::Queryable<((diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Integer), diesel::sql_types::Nullable<(diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Text, diesel::sql_types::Text, diesel::sql_types::Integer, diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Integer, diesel::sql_types::Integer)>), diesel::sqlite::Sqlite>` for `(posts::Tag, posts::BlogPosts)`
note: required because of the requirements on the impl of `diesel::query_dsl::LoadQuery<diesel::SqliteConnection, (posts::Tag, posts::BlogPosts)>` for `diesel::query_builder::SelectStatement<diesel::query_source::joins::JoinOn<diesel::query_source::joins::Join<schema::tags::table, schema::blogposts::table, diesel::query_source::joins::LeftOuter>, diesel::expression::operators::Eq<schema::blogposts::columns::id, schema::tags::columns::post_id>>, diesel::query_builder::select_clause::DefaultSelectClause, diesel::query_builder::distinct_clause::NoDistinctClause, diesel::query_builder::where_clause::WhereClause<diesel::expression::operators::Eq<schema::tags::columns::tag_name, diesel::expression::bound::Bound<diesel::sql_types::Text, &str>>>>`rustc(E0277)
posts.rs(477, 103): the trait `diesel::Queryable<diesel::sql_types::Nullable<(diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Text, diesel::sql_types::Text, diesel::sql_types::Integer, diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Integer, diesel::sql_types::Integer)>, diesel::sqlite::Sqlite>` is not implemented for `(i32, std::string::String, std::string::String, std::string::String, i32, i32, std::string::String, i32, i32)`

下のエラーで横スクロールを開始します。それをずーっと続けます。Googleで使えるとは思えませんが。

C++のテンプレートが出したひどいエラーメッセージを思い出させます。


私はおそらく私の実際のパソコンでたくさんCookieを使用するつもりだったので、ある時点でオプションで1ヶ月間の長いCookieの有効期限を持とうと思っていました。そこで私はrocketのドキュメントの中の対応する項目を見てみました。

よし、それは... Tm?Tmってなんでしょう?では、例を見てみましょう。

良さそうですね、まさに私の希望通りです。期間はマイナス11ヶ月ですが、それでいいと思います。

???

と思ったら、明らかにstd::timeの中のものが正しくありません! なるほど、node_modules Cargo.tomlに別のライブラリを追加することにしました。

もう一つのライブラリは正しいのです。しかし、

🤦 なぜ警告が出たのでしょうか?

この質問の答えは明らかではないか、ただ私がちゃんと把握できていないだけなのかもしれません。ただ他のどこにも似たようなビヘイビアのものが見当たらないのです。

おい、Unix のタイムスタンプのどこが悪いんだよ ; (


node_modulesといえば、正直言ってRustのエコシステムの一部を見るとNPMを思い出します。私のエンドリリースのビルドでは、267種類のライブラリをコンパイルする必要がありますが、それは私のシンプルで小さなブログエンジンのためです。

ライブラリ、自身のライブラリ、またそれ自身のライブラリ。

どうしようもできませんが、これらのleaf依存の1つが危険にさらされている場合、私が気づく方法はありません。しかし、すべてのものが静的にリンクされていて、文字通り必要なものだけを含めるために、おそらくビルド時間ではありませんが、ビルドサイズを削減することができると確信しています。C は、lush の標準ライブラリが充実しているわけではありません。しかし、ライブラリを管理している人たちが、自分たちのディペンデンシーをできるだけ低く抑えようとしている限り、それは紛れもない真実なのです。

まとめ

誰かに次のサーバをRustで書くことをお勧めしますか?すすめません。エコシステムはまだそこには至っておらず、ほとんどのサーバーはまだI/Oに縛られているので、速度の向上はおそらくそれほど重要にはならないでしょう。

しかし、正直なところ一旦エコシステムが完成すれば、Rustはサーバーを書くのに最適な言語だと思います。速度と安全性が向上し、通常の借用チェッカとの戦い(または少なくとも、あるかもしれないと思っている戦い)と同じ代償を支払うことはありません。時々、私は本当にマイルドで冗長なpythonで書いているように感じました。とても楽しかったです。だからRocket の待望の 0.5.0 リリースにとても期待しています。

しかし、それは変更のみのアップグレードではなく、チェックアウト機能のブランチアップグレードになると確信しています。

最終的に手に入れたものには満足しています。バックグラウンドで余計なランタイムを走らせることなく、必要なことを正確に実行する、小さくてかわいいサーバーです。

appstore
googleplay
会員登録
URLからPICKする

会員登録して、もっと便利に利用しよう

  • 1.

    記事をストックできる
    気になる記事をPickして、いつでも読み返すことができます。
  • 2.

    新着ニュースをカスタマイズできます
    好きなニュースフィードをフォローすると、新着ニュースが受け取れます。