Help us understand the problem. What is going on with this article?

Rustで高速に大量のHTTPリクエストを投げる

Rustで高速に大量のHTTPリクエストを投げる

by hatoo@github
1 / 19

自己紹介

趣味でRustをやっている


Rustで高速に大量のHTTPリクエストを投げる

モチベーション

oha

https://github.com/hatoo/oha

  • HTTPロードジェネレータ
  • Apache Bench(ab)みたいな
  • tui-rsでリアルタイム表示
  • とにかくいっぱいリクエストを投げたい!

oha demo


ベンチマーク環境


ベンチマーク雛形

  • ベンチマーク系のライブラリは複数回実行してしまい、時間がかかるのでやめた
  • ざっくりと時間がわかればいいかな
  • 簡単のためにサーバーはwarpで自分で作る
async fn run<T>(name: &str, f: impl std::future::Future<Output = T>) -> T {
    let now = std::time::Instant::now();
    let r = f.await;
    println!("{}: {:?}", name, now.elapsed());
    r
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tokio::spawn(async {
        let hello = warp::any().map(|| "Hello, World!");
        warp::serve(hello).run(([0, 0, 0, 0], 8080)).await
    });

    run("reqwest naive serial 100", async {
        for _ in 0..1000 {
            reqwest::get("http://127.0.0.1:8080").await.unwrap();
        }
    })
    .await;
}

とりあえずreqwestでしょ!

https://github.com/seanmonstar/reqwest

An ergonomic, batteries-included HTTP Client for Rust.

  • 何も考えずreqwestを使ってみる
  • 直列に1000リクエスト
   run("reqwest naive serial 1000", async {
        for _ in 0..1000 {
            reqwest::get("http://127.0.0.1:8080").await.unwrap();
        }
    })
    .await;

結果

reqwest naive serial 1000: 4.3056912s

1リクエスト約4msか…なんか遅いなあ:thinking:


ドキュメントより

NOTE: If you plan to perform multiple requests, it is best to create a Client and reuse it, taking advantage of keep-alive connection pooling.

Clientを作って再利用すればいいのか!:bulb:


クライアントを再利用

run("reqwest serial 1000", async {
        let client = reqwest::Client::new();
        for _ in 0..1000 {
            client.get("http://127.0.0.1:8080").send().await.unwrap();
        }
    })
    .await;

結果

reqwest serial 1000: 142.0606ms

0.14ms/query


並列でリクエストを送ろう!

run("reqwest naive para 1000", async {
        let client = reqwest::Client::new();
        let futures = (0..1000)
            .map(|_| {
                let client = client.clone();
                tokio::spawn(async move { client.get("http://127.0.0.1:8080").send().await })
            })
            .collect::<Vec<_>>();
        for f in futures {
            let _ = f.await;
        }
    })
    .await;

join_allとかあったな:thinking:

run("reqwest naive para join_all 1000", async {
        let client = reqwest::Client::new();
        let futures = futures::future::join_all((0..1000).map(|_| {
            let client = client.clone();
            tokio::spawn(async move { client.get("http://127.0.0.1:8080").send().await })
        }));
        futures.await;
    })
    .await;

結果(ばらつきあり)

reqwest naive para 1000: 53.2765555s
reqwest naive para join_all 1000: 69.1197139s

ガーン:fearful:


オープンできるファイル数

Linuxではプロセスごとにオープンできるファイル数が決まっている

#デフォルト値
> ulimit -n
1024

少なくない??:thinking:


ulimit -nを増やした

> ulimit -n
65536
reqwest naive para 1000: 215.9434ms
reqwest naive para join_all 1000: 220.8876ms

0.2ms/query

速くなったけど並列化の恩恵が感じられない


1リクエスト1futureはどうなの??

100futuresでリクエストを投げる

run("reqwest para workers 100 1000", async {
        use std::sync::atomic::{AtomicUsize, Ordering};
        let counter = AtomicUsize::new(0);
        let client = reqwest::Client::new();
        let _ = futures::future::join_all((0..100).map(|_| async {
            let client = client.clone();
            while counter.fetch_add(1, Ordering::Relaxed) < 1000 {
                let _ = client.get("http://127.0.0.1:8080").send().await;
            }
        }))
        .await;
    })
    .await;

結果

reqwest para workers 100 1000: 22.6152ms

0.02ms/query

速い:dancer:

もっと速くするには…


hyper

https://github.com/hyperium/hyper

A fast and correct HTTP implementation for Rust.

  • よりローレベルなライブラリ
  • reqwestも内部でhyperを使っている
  • 直接使えば速くなるかも!?

hyper

長い…

async fn create_hyper_conn() -> anyhow::Result<hyper::client::conn::SendRequest<hyper::Body>> {
    let stream = tokio::net::TcpStream::connect("127.0.0.1:8080").await?;
    stream.set_nodelay(true)?;
    stream.set_keepalive(std::time::Duration::from_secs(1).into())?;
    let (send, conn) = hyper::client::conn::handshake(stream).await?;
    tokio::spawn(conn);
    Ok(send)
}

async fn hyper_do_req(counter: &AtomicUsize) -> anyhow::Result<()> {
    use hyper::Request;
    use tokio::stream::StreamExt;

    let mut send_request = create_hyper_conn().await?;

    while counter.fetch_add(1, Ordering::Relaxed) < 1000 {
        while futures::future::poll_fn(|ctx| send_request.poll_ready(ctx))
            .await
            .is_err()
        {
            send_request = create_hyper_conn().await?;
        }
        let request = Request::builder()
            .method("GET")
            .uri("http://127.0.0.1:8080/")
            .body(hyper::Body::empty())?;
        let res = send_request.send_request(request).await?;
        let (_parts, mut stream) = res.into_parts();
        while let Some(_chunk) = stream.next().await {}
    }
    Ok::<(), anyhow::Error>(())
}
    run("hyper para wokers 100 1000", async {
        let counter = AtomicUsize::new(0);
        let _ = futures::future::join_all((0..100).map(|_| async {
            hyper_do_req(&counter).await.unwrap();
        }))
        .await;
    })
    .await;

結果

hyper para wokers 100 1000: 7.055ms

めっちゃ速い!:dancer:


まとめ

  • keep-aliveを使おう
  • ulimit -nに気をつけよう
  • 並列化するときのfutureの数に気をつけよう
  • ローレベルなAPIを使うことも考えてみよう
  • もっと高速化できるよという案募集!
hatoo@github
Rustのお仕事ください!
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away