#自己紹介
趣味でRustをやっている
-
Twitter
-
Github
-
SoundCloud
-
ネットワーク初心者なので誤りがあるかも知れません
Rustで高速に大量のHTTPリクエストを投げる
モチベーション
oha
- HTTPロードジェネレータ
- Apache Bench(ab)みたいな
- tui-rsでリアルタイム表示
- とにかくいっぱいリクエストを投げたい!
#ベンチマーク環境
- WSL 2
- Ryzen 3950x
- 今回はtokio
- https://github.com/hatoo/rust_http_benchmarks
#ベンチマーク雛形
- ベンチマーク系のライブラリは複数回実行してしまい、時間がかかるのでやめた
- ざっくりと時間がわかればいいかな
- 簡単のためにサーバーは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か…なんか遅いなあ
ドキュメントより
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
を作って再利用すればいいのか!
#クライアントを再利用
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とかあったな
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
ガーン
#オープンできるファイル数
Linuxではプロセスごとにオープンできるファイル数が決まっている
#デフォルト値
> ulimit -n
1024
少なくない??
#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
速い
もっと速くするには…
#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
めっちゃ速い!
#まとめ
- keep-aliveを使おう
-
ulimit -n
に気をつけよう - 並列化するときのfutureの数に気をつけよう
- ローレベルなAPIを使うことも考えてみよう
- もっと高速化できるよという案募集!