๐ Introduction
Lately, there's been a lot of buzz around Rust & Go. Why all the hype? Which one truly holds the edge? Let's fire up servers on 127.0.0.1 & find out.
GitHub: Here's the Simple HTTP Server code for benchmarking:
- Web Server Code: Access the server implementations for both languages:
- Rust: Rust-Serve Code
- Go: Go-Serve Code
๐ฆ Benchmarking Setup with WRK
To start, we'll need WRK(powerful benchmarking tool). If you're on a Mac like me - you'll find it on HomeBrew.
- Benchmarking Tool: We're using WRK
- Installation: Mac users, you can easily install WRK with Homebrew command:
brew install wrk
๐ง Setting Up the Rust Web Server
For Rust, you'll need a specific directory structure to get started:
|_ Cargo.toml
|_ src
|_ main.rs
Once everything is in place, initiating the server is straightforward with cargo:
cargo build --release
cargo run
๐ Rust Web Server Code
Interested in the nitty-gritty? Dive into the server code!
Explore the Code
Rust: main.rs
// Import necessary modules and types from the actix_web crate and standard library.
use actix_web::{web, App, HttpServer, HttpResponse, Responder, middleware::Logger};
use std::env;
// The main function is marked with `actix_web::main`, which sets up an async runtime.
// This function will return a Result that, if an error occurs, will contain an `std::io::Error`.
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Set the environment variable for logging level to "info" for actix_web logs.
env::set_var("RUST_LOG", "actix_web=info");
// Initialize the env_logger logger, which will log information based on the RUST_LOG environment variable.
env_logger::init();
// Create and run an HTTP server.
HttpServer::new(|| {
// Initialize the Actix web application.
App::new()
// Add the Logger middleware to log all incoming requests.
.wrap(Logger::default())
// Define a route for the root URL ("/") that handles GET requests with the `root_handler` function.
.route("/", web::get().to(root_handler))
})
// Bind the server to listen on the localhost address and port 8080.
.bind("127.0.0.1:8080")?
// Start the server and await its completion, handling any errors that occur.
.run()
.await
}
// Define an asynchronous handler function for the root URL.
// This function returns a type that implements the `Responder` trait, which can be converted into an HTTP response.
async fn root_handler() -> impl Responder {
// Create an HTTP response with the status code 200 OK and the body "Hello, World! This is a cool web test!".
HttpResponse::Ok().body("Hello, World! This is a cool web test!")
}
Rust: Cargo.toml
[package]
name = "rust_server"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.0"
actix-rt = "2.5"
env_logger = "0.9"
[dev-dependencies]
criterion = "0.3"
๐ง Setting Up the Go Web Server
For Go, Effortlessly run the following:
go run main.go
๐ Go Web-Server Code
Interested in the nitty-gritty? Dive into the server code!
Server Code
Go: main.go
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
// Create a new ServeMux (router)
mux := http.NewServeMux()
// Register a handler function for the root URL
mux.HandleFunc("/", rootHandler)
// Create a new HTTP server
server := &http.Server{
Addr: "127.0.0.1:8080",
Handler: logRequest(mux),
// Wrap the handler with the logging middleware
}
// Start the server and log if there's an error
fmt.Println("Server is running on http://127.0.0.1:8090")
if err := server.ListenAndServe(); err != nil {
log.Fatal("Error starting server: ", err)
}
}
// rootHandler responds to requests at the root URL
func rootHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, World! This is a cool web test!")
}
// logRequest is a middleware that logs each request
func logRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
duration := time.Since(start)
log.Printf("Request: %s %s, Duration: %s\n", r.Method, r.URL.Path, duration)
})
}
๐ Rust or Go: Key Metrics
Rust: Metrics Release Mode Not Enabled
wrk -t2 -c100 -d30s --latency http://127.0.0.1:8080
Running 30s test @ http://127.0.0.1:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 7.36ms 5.32ms 68.59ms 91.98%
Req/Sec 7.54k 1.51k 12.48k 69.83%
Latency Distribution
50% 5.94ms
75% 7.67ms
90% 10.77ms
99% 33.52ms
450491 requests in 30.02s, 48.98MB read
Requests/sec: 15004.41
Transfer/sec: 1.63MB
Rust: Metrics Release - Enabled
wrk -t2 -c100 -d30s --latency http://127.0.0.1:8080
Running 30s test @ http://127.0.0.1:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 6.41ms 6.39ms 95.91ms 92.02%
Req/Sec 9.47k 2.42k 14.95k 65.50%
Latency Distribution
50% 4.73ms
75% 6.31ms
90% 10.66ms
99% 36.55ms
566141 requests in 30.05s, 61.55MB read
Requests/sec: 18838.67
Transfer/sec: 2.05MB
Go: Metrics
wrk -t2 -c100 -d30s --latency http://127.0.0.1:8090
Running 30s test @ http://127.0.0.1:8090
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 3.12ms 3.13ms 46.54ms 94.85%
Req/Sec 18.12k 3.18k 25.08k 69.95%
Latency Distribution
50% 2.62ms
75% 3.23ms
90% 4.23ms
99% 20.40ms
1082616 requests in 30.05s, 160.03MB read
Requests/sec: 36021.81
Transfer/sec: 5.32MB
๐ Lets Chart it.. Rust or Go?:
Let's compare performance of Rust server & Go servers:
๐ Rust Server Performance: Release Mode - Not Enabled
- Average Latency: 7.36ms
- Standard Deviation of Latency: 5.32ms
- Maximum Latency: 68.59ms
- Requests per Second: 7.54k (average), 12.48k (maximum)
- Latency Distribution:
- Median (50%): 5.94ms
- 75th Percentile: 7.67ms
- 90th Percentile: 10.77ms
- 99th Percentile: 33.52ms
- Total Requests: 450,491 in 30.02 seconds
- Data Transferred per Second: 1.63MB
๐ Rust Server Performance: Release Mode - Enabled
- Average Latency: 6.41ms (down from 7.36ms)
- Standard Deviation of Latency: 6.39ms (up from 5.32ms)
- Maximum Latency: 95.91ms (up from 68.59ms)
- Requests per Second:
- Average: 9.47k (up from 7.54k)
- Maximum: 14.95k (up from 12.48k)
- Latency Distribution:
- Median (50%): 4.73ms (down from 5.94ms)
- 75th Percentile: 6.31ms (down from 7.67ms)
- 90th Percentile: 10.66ms (up from 10.77ms)
- 99th Percentile: 36.55ms (up from 33.52ms)
- Total Requests: 566,141 in 30.05 seconds (up from 450,491)
- Data Transferred per Second: 2.05MB (up from 1.63MB)
๐ Go Server Performance:
- Average Latency: 3.85ms
- Standard Deviation of Latency: 3.72ms
- Maximum Latency: 60.31ms
- Requests per Second: 14.71k (average), 24.66k (maximum)
- Latency Distribution:
- Median (50%): 3.14ms
- 75th Percentile: 3.95ms
- 90th Percentile: 5.45ms
- 99th Percentile: 21.33ms
- Total Requests: 879,736 in 30.06 seconds
- Data Transferred per Second: 4.33MB
๐ค Well.. Is it Rust or is it Go?
Latency: The Go server has lower average, median, and 99th percentile latencies compared to the Rust server. This suggests that for each individual request, the Go server is generally able to respond faster.
Requests per Second (Throughput): The Go server is handling nearly double the number of requests per second compared to the Rust server. This indicates that the Go server has a higher throughput under the tested load conditions.
Data Transfer: The Go server is transferring more data per second than the Rust server, which aligns with its higher requests per second.
๐ Conclusion:
Well.. In this benchmark, the Go server is outperforming the Rust server in terms of both latency and throughput. However, it's important to note a few key considerations when interpreting these results:
Server Configuration: The configuration of the server, such as the use of asynchronous code, thread pool sizes, and other optimizations, can significantly impact performance.
Workload Characteristics: Depending on what the servers are actually doing (static file serving, database queries, CPU-bound tasks), the performance characteristics could change.
Benchmarking Conditions: The system on which the benchmark is run, other running processes, network conditions, and even the specifics of how
wrk
is used can affect the results.Code Maturity and Optimizations: The specific Rust and Go code being benchmarked could be at different levels of optimization. More mature or optimized code can perform significantly better.