LoginSignup
110
76

More than 3 years have passed since last update.

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

Posted at
1 / 19

自己紹介

趣味でRustをやっている


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

モチベーション

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でしょ!

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

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を使うことも考えてみよう
  • もっと高速化できるよという案募集!
110
76
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
110
76