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

#自己紹介

趣味でRustをやっている

- Twitter
    - https://twitter.com/hatookov
- Github
    - https://github.com/hatoo
- SoundCloud
    - https://soundcloud.com/gfyxxqjngkze

- ネットワーク初心者なので誤りがあるかも知れません:bow:

---
# Rustで高速に大量のHTTPリクエストを投げる
## モチベーション

## oha

https://github.com/hatoo/oha

- HTTPロードジェネレータ
- Apache Bench(ab)みたいな
- [tui-rs](https://github.com/fdehau/tui-rs)でリアルタイム表示
- とにかくいっぱいリクエストを投げたい！

<img width="1000" alt="oha demo" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20535/672b5698-825d-2229-c94d-aec89bae6b7e.gif">

---

#ベンチマーク環境

- WSL 2
- Ryzen 3950x
- 今回はtokio
- https://github.com/hatoo/rust_http_benchmarks

---

#ベンチマーク雛形

- ベンチマーク系のライブラリは複数回実行してしまい、時間がかかるのでやめた
- ざっくりと時間がわかればいいかな
- 簡単のためにサーバーはwarpで自分で作る

```rust
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リクエスト

```rust
   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
```

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

---

[ドキュメント](https://docs.rs/reqwest/0.10.8/reqwest/)より
> 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:

---

#クライアントを再利用

```rust
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

---

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

```rust
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:

```rust
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でリクエストを投げる

```rust
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

長い…

```rust
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>(())
}
```

```rust
    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を使うことも考えてみよう
- もっと高速化できるよという案募集！