hyperium / hyper

An HTTP library for Rust
https://hyper.rs
MIT License
14.42k stars 1.59k forks source link

The performance problem of hyper v1 gateway of http1 with the log4rs #3586

Closed lsk569937453 closed 7 months ago

lsk569937453 commented 7 months ago

Version hyper = { version = "1.2.0", features = ["full"] } tokio = { version = "1.36.0", features = ["full"] } hyper-util = { version = "0.1.3", features = ["full"] }

Platform Linux

code sample that causes the bug The demo source code is https://gist.github.com/lsk569937453/b42a8cfce21bd20c5da8737db1f5a1b1.

use anyhow::anyhow;
use hyper::{server::conn::http1, service::service_fn};
use std::net::SocketAddr;
use tokio::net::TcpListener;

use bytes::Bytes;
use http_body_util::{combinators::BoxBody, BodyExt, Full};
use hyper::body::Incoming;

use hyper::{Request, Response, Uri};
use hyper_util::client::legacy::{connect::HttpConnector, Client};
use hyper_util::rt::TokioIo;
use std::convert::Infallible;
use tokio::runtime;
#[tokio::main]
async fn main() {
    if let Err(err) = start_with_error().await {
        println!("Failed to serve the connection: {:?}", err);
    }
}
async fn check(uri: Uri) -> Result<(), anyhow::Error> {
    let backend_path = uri.path_and_query().ok_or(anyhow!(""))?.as_str();
    Ok(())
}
async fn do_req(
    client: Client<HttpConnector, BoxBody<Bytes, Infallible>>,
    req: Request<BoxBody<Bytes, Infallible>>,
) -> Result<Response<BoxBody<Bytes, Infallible>>, Infallible> {
    let uri = req.uri().clone();
    check(uri)
        .await
        .map_err(|_| -> Infallible { unreachable!() })?;
    let response_incoming = client
        .request(req)
        .await
        .map_err(|_| -> Infallible { unreachable!() })?;
    let res = response_incoming
        .map(|b| b.boxed())
        .map(|item| item.map_err(|_| -> Infallible { unreachable!() }).boxed());
    Ok(res)
}
async fn start_with_error() -> Result<(), Box<dyn std::error::Error>> {
    let in_addr: SocketAddr = ([0, 0, 0, 0], 6667).into();
    let out_addr_clone = "http://backend:8080";
    let listener = TcpListener::bind(in_addr).await?;

    println!("Listening on http://{}", in_addr);
    println!("Proxying on http://{}", out_addr_clone);
    let client = Client::builder(hyper_util::rt::TokioExecutor::new()).build(HttpConnector::new());
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        let client_clone = client.clone();
        let service = service_fn(move |mut req: Request<Incoming>| {
            let client_clone1 = client_clone.clone();
            let uri_now: hyper::http::uri::Uri = out_addr_clone.parse().unwrap();
            *req.uri_mut() = uri_now.clone();
            let req = req.map(|item| item.map_err(|_| -> Infallible { unreachable!() }).boxed());
            do_req(client_clone1, req)
        });

        tokio::task::spawn(async move {
            if let Err(err) = http1::Builder::new()
                .preserve_header_case(true)
                .title_case_headers(true)
                .serve_connection(io, service)
                .await
            {
                println!("Failed to serve the connection: {:?}", err);
            }
        });
    }
}
[package]
name = "hyper_1"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "silverwind"
path = "src/main.rs"
[dependencies]
hyper = { version = "1.2.0", features = ["full"] }
tokio = { version = "1.36.0", features = ["full"] }
hyper-util = { version = "0.1.3", features = ["full"] }
bytes = "1"
http-body-util = { version = "0.1.0"}
anyhow = { version = "1.0.80", default-features = false }
log4rs = "1.3.0"

Description I use the hyper v1 as the http1 gateway.And I found the above code will cause performance problem.If I delete the crates of log4rs from the Cargo.toml.The QPS of the gateway could be 67000.But If I add the log4rs in the Cargo.toml like the above code ,the QPS of the gateway could be 40000.

[short summary of the bug]

I tried the following actions:

I expected to see this happen: The v1 version of hyper gateway should keep the same behaviour as the 0.14.xx.

Instead, this happened: The v1 version of hyper gatway has the lower QPS than the 0.14.xx version of hyper.

seanmonstar commented 7 months ago

Is there any more to the usage? I don't see log4rs being used in the Rust code. Also, it is quite surprising that adding the logger would make any difference, since with hyper v1 we made tracing an unstable feature, so it won't even emit log events unless you enabled that unstable feature (which is not easy to enable accidentally, by design).

lsk569937453 commented 7 months ago

Yeah,you are right.I have never used the log4rs in the code .But the adding of the log4rs in the Cargo.toml influence the performance.It is weird.I just suspect the dependencies of log4rs influence the other crates.

lsk569937453 commented 7 months ago

I have tried many times for the project.I found that the problem is that the anyhow of default feature decrease the performance of the hyper. code sample that causes the bug: Cargo.toml

[package]
name = "hyper_1"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "silverwind"
path = "src/main.rs"
[dependencies]
hyper = { version = "1.2.0", features = ["full"] }
tokio = { version = "1.36.0", features = ["full"] }
hyper-util = { version = "0.1.3", features = ["full"] }
bytes = "1"
http-body-util = { version = "0.1.0"}
anyhow = { version = "1.0.80" }

main.rs

use anyhow::anyhow;
use hyper::{server::conn::http1, service::service_fn};
use std::net::SocketAddr;
use tokio::net::TcpListener;

use bytes::Bytes;
use http_body_util::{combinators::BoxBody, BodyExt, Full};
use hyper::body::Incoming;

use hyper::{Request, Response, Uri};
use hyper_util::client::legacy::{connect::HttpConnector, Client};
use hyper_util::rt::TokioIo;
use std::convert::Infallible;
use tokio::runtime;
#[tokio::main]
async fn main() {
    if let Err(err) = start_with_error().await {
        println!("Failed to serve the connection: {:?}", err);
    }
}
async fn check(uri: Uri) -> Result<(), anyhow::Error> {
    let backend_path = uri.path_and_query().ok_or(anyhow!(""))?.as_str();
    Ok(())
}
async fn do_req(
    client: Client<HttpConnector, BoxBody<Bytes, Infallible>>,
    req: Request<BoxBody<Bytes, Infallible>>,
) -> Result<Response<BoxBody<Bytes, Infallible>>, Infallible> {
    let uri = req.uri().clone();
    check(uri)
        .await
        .map_err(|_| -> Infallible { unreachable!() })?;
    let response_incoming = client
        .request(req)
        .await
        .map_err(|_| -> Infallible { unreachable!() })?;
    let res = response_incoming
        .map(|b| b.boxed())
        .map(|item| item.map_err(|_| -> Infallible { unreachable!() }).boxed());
    Ok(res)
}
async fn start_with_error() -> Result<(), Box<dyn std::error::Error>> {
    let in_addr: SocketAddr = ([0, 0, 0, 0], 6667).into();
    let out_addr_clone = "http://backend:8080";
    let listener = TcpListener::bind(in_addr).await?;

    println!("Listening on http://{}", in_addr);
    println!("Proxying on http://{}", out_addr_clone);
    let client = Client::builder(hyper_util::rt::TokioExecutor::new()).build(HttpConnector::new());
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        let client_clone = client.clone();
        let service = service_fn(move |mut req: Request<Incoming>| {
            let client_clone1 = client_clone.clone();
            let uri_now: hyper::http::uri::Uri = out_addr_clone.parse().unwrap();
            *req.uri_mut() = uri_now.clone();
            let req = req.map(|item| item.map_err(|_| -> Infallible { unreachable!() }).boxed());
            do_req(client_clone1, req)
        });

        tokio::task::spawn(async move {
            if let Err(err) = http1::Builder::new()
                .preserve_header_case(true)
                .title_case_headers(true)
                .serve_connection(io, service)
                .await
            {
                println!("Failed to serve the connection: {:?}", err);
            }
        });
    }
}

If I change the anyhow = { version = "1.0.80"}to anyhow = { version = "1.0.80",default-features = false}.Everything will run well. Another point is that the project will not use any other creates which use the anyhow of default feature,like log4rs,delay-timer,etc.Otherwise,it will decrease the performance of hyper.(In my docker container of 4Core-8GB RAM,the TPS decrease from 80000 to 40000)

seanmonstar commented 7 months ago

Enabling or disabling the feature also shouldn't be causing such a problem. One possible case: when you add anyhow, are you using that as the error type? Usage of Infallible allows the compiler to remove a bunch of error-related code, which might cause a performance difference. There's no way it should be double.

And also, the unreachable!() lines are not actually unreachable, errors can happen in plenty of places during networking/HTTP.


All in all, this doesn't seem like it's an issue with hyper itself. So, I'm going to close this issue.

lsk569937453 commented 7 months ago

I dont think so.If I change the Infallible to anyhow::Error like following,it will cause the same problem.

use anyhow::anyhow;
use hyper::{server::conn::http1, service::service_fn};
use std::net::SocketAddr;
use tokio::net::TcpListener;

use bytes::Bytes;
use http_body_util::{combinators::BoxBody, BodyExt, Full};
use hyper::body::Incoming;

use hyper::{Request, Response, Uri};
use hyper_util::client::legacy::{connect::HttpConnector, Client};
use hyper_util::rt::TokioIo;
use std::convert::Infallible;
use tokio::runtime;
#[tokio::main]
async fn main() {
    if let Err(err) = start_with_error().await {
        println!("Failed to serve the connection: {:?}", err);
    }
}
async fn check(uri: Uri) -> Result<(), anyhow::Error> {
    let backend_path = uri.path_and_query().ok_or(anyhow!(""))?.as_str();
    Ok(())
}
async fn do_req(
    client: Client<HttpConnector, BoxBody<Bytes, anyhow::Error>>,
    req: Request<BoxBody<Bytes, anyhow::Error>>,
) -> Result<Response<BoxBody<Bytes, anyhow::Error>>, anyhow::Error> {
    let uri = req.uri().clone();
    check(uri).await.map_err(|err| anyhow!(err))?;
    let response_incoming = client.request(req).await.map_err(|err| anyhow!(err))?;
    let res = response_incoming
        .map(|b| b.boxed())
        .map(|item| item.map_err(|err| anyhow!(err)).boxed());
    Ok(res)
}
async fn start_with_error() -> Result<(), Box<dyn std::error::Error>> {
    let in_addr: SocketAddr = ([0, 0, 0, 0], 6667).into();
    let out_addr_clone = "http://backend:8080";
    let listener = TcpListener::bind(in_addr).await?;

    println!("Listening on http://{}", in_addr);
    println!("Proxying on http://{}", out_addr_clone);
    let client = Client::builder(hyper_util::rt::TokioExecutor::new()).build(HttpConnector::new());
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        let client_clone = client.clone();
        let service = service_fn(move |mut req: Request<Incoming>| {
            let client_clone1 = client_clone.clone();
            let uri_now: hyper::http::uri::Uri = out_addr_clone.parse().unwrap();
            *req.uri_mut() = uri_now.clone();
            let req = req.map(|item| item.map_err(|err| anyhow!(err)).boxed());
            do_req(client_clone1, req)
        });

        tokio::task::spawn(async move {
            if let Err(err) = http1::Builder::new()
                .preserve_header_case(true)
                .title_case_headers(true)
                .serve_connection(io, service)
                .await
            {
                println!("Failed to serve the connection: {:?}", err);
            }
        });
    }
}