Expand description
§What this library does
This library provides tools to build and start HTTP, HTTPS and TCP reverse proxies.
The proxies handles network polling, HTTP parsing, TLS in a fast single threaded event loop.
Each proxy is designed to receive configuration changes at runtime instead of reloading from a file regularly. The event loop runs in its own thread and receives commands through a message queue.
§Difference with the crate sozu
To create several workers and manage them all at once (which is the most common way to
use Sōzu), the crate sozu
is more indicated than using the lib directly.
The crate sozu
provides a binary called the main process.
The main process uses sozu_lib
to start and manage workers.
Each worker can handle HTTP, HTTPS and TCP traffic.
The main process receives synchronizes the state of all workers, using UNIX sockets
and custom channels to communicate with them.
The main process itself is is configurable with a file, and has a CLI.
§How to use this library directly
This documentation here explains how to write a binary that will start a single Sōzu worker and give it orders. The method has two steps:
- Starts a Sōzu worker in a distinct thread
- sends instructions to the worker on a UNIX socket via a Sōzu channel
§How to start a Sōzu worker
Before creating an HTTP proxy, we first need to create an HTTP listener.
The listener is an abstraction around a TCP socket provided by the kernel.
We need the sozu_command_lib
to build a listener.
use sozu_command_lib::{config::ListenerBuilder, proto::command::SocketAddress};
let address = SocketAddress::new_v4(127,0,0,1,8080);
let http_listener = ListenerBuilder::new_http(address)
.to_http(None)
.expect("Could not create HTTP listener");
The http_listener
is of the type HttpListenerConfig
, that we can be sent to the worker
to start the proxy.
Then create a pair of channels to communicate with the proxy. The channel is a wrapper around a unix socket.
use sozu_command_lib::{
channel::Channel,
proto::command::{WorkerRequest, WorkerResponse},
};
let (mut command_channel, proxy_channel): (
Channel<WorkerRequest, WorkerResponse>,
Channel<WorkerResponse, WorkerRequest>,
) = Channel::generate(1000, 10000).expect("should create a channel");
Here, the command_channel
end is blocking, it sends WorkerRequest
s and receives
WorkerResponses
, while the proxy_channel
end is non-blocking, and the types are reversed.
Writing the types here isn’t even necessary thanks to the compiler,
but it brings the point accross.
You can now launch the worker in a separate thread, providing the HTTP listener config, the proxy end of the channel, and your custom number of buffers and their size:
use std::thread;
let worker_thread_join_handle = thread::spawn(move || {
let max_buffers = 500;
let buffer_size = 16384;
sozu_lib::http::testing::start_http_worker(http_listener, proxy_channel, max_buffers, buffer_size);
});
§Send orders
Once the thread is launched, the proxy worker will start its event loop and handle events on the listening interface and port specified when building the HTTP Listener. Since no frontends or backends were specified for the proxy, it will receive the connections, parse the requests, then send a default (but configurable) answer.
Before defining a frontend and backends, we need to define a cluster, which describes a routing configuration. A cluster contains:
- one frontend
- one or several backends
- routing rules
A cluster is identified by its cluster_id
, which will be used to define frontends
and backends later on.
use sozu_command_lib::proto::command::{Cluster, LoadBalancingAlgorithms};
let cluster = Cluster {
cluster_id: "my-cluster".to_string(),
sticky_session: false,
https_redirect: false,
load_balancing: LoadBalancingAlgorithms::RoundRobin as i32,
answer_503: Some("A custom forbidden message".to_string()),
..Default::default()
};
The defaults are sensible, so we could define only the cluster_id
.
We can now define a frontend. A frontend is a way to recognize a request and match
it to a cluster_id
, depending on the hostname and the beginning of the URL path.
The address
field must match the one of the HTTP listener we defined before:
use std::collections::BTreeMap;
use sozu_command_lib::proto::command::{PathRule, RequestHttpFrontend, RulePosition, SocketAddress};
let http_front = RequestHttpFrontend {
cluster_id: Some("my-cluster".to_string()),
address: SocketAddress::new_v4(127,0,0,1,8080),
hostname: "example.com".to_string(),
path: PathRule::prefix(String::from("/")),
position: RulePosition::Pre.into(),
tags: BTreeMap::from([
("owner".to_owned(), "John".to_owned()),
("id".to_owned(), "my-own-http-front".to_owned()),
]),
..Default::default()
};
The tags
are keys and values that will appear in the access logs,
which can come in handy.
Now let’s define a backend.
A backend is an instance of a backend application we want to route traffic to.
The address
field must match the IP and port of the backend server.
use sozu_command_lib::proto::command::{AddBackend, LoadBalancingParams, SocketAddress};
let http_backend = AddBackend {
cluster_id: "my-cluster".to_string(),
backend_id: "test-backend".to_string(),
address: SocketAddress::new_v4(127,0,0,1,8000),
load_balancing_parameters: Some(LoadBalancingParams::default()),
..Default::default()
};
A cluster can have multiple backend servers, and they can be added or removed while the proxy is running. If a backend is removed from the configuration while the proxy is handling a request to that server, it will finish that request and stop sending new traffic to that server.
Now we can use the other end of the channel to send all these requests to the worker, using the WorkerRequest type:
use sozu_command_lib::{
proto::command::{Request, request::RequestType, WorkerRequest},
};
command_channel
.write_message(&WorkerRequest {
id: String::from("add-the-cluster"),
content: RequestType::AddCluster(cluster).into(),
})
.expect("Could not send AddHttpFrontend request");
command_channel
.write_message(&WorkerRequest {
id: String::from("add-the-frontend"),
content: RequestType::AddHttpFrontend(http_front).into(),
})
.expect("Could not send AddHttpFrontend request");
command_channel
.write_message(&WorkerRequest {
id: String::from("add-the-backend"),
content: RequestType::AddBackend(http_backend).into(),
})
.expect("Could not send AddBackend request");
println!("HTTP -> {:?}", command_channel.read_message());
println!("HTTP -> {:?}", command_channel.read_message());
println!("HTTP -> {:?}", command_channel.read_message());
The event loop of the worker will process these instructions and add them to its state, and the worker will send back an acknowledgement message.
Now we can let the worker thread run in the background:
let _ = worker_thread_join_handle.join();
Here is the complete example for reference, it matches the examples/http.rs
example:
#[macro_use]
extern crate sozu_command_lib;
use std::{collections::BTreeMap, env, io::stdout, thread};
use anyhow::Context;
use sozu_command_lib::{
channel::Channel,
config::ListenerBuilder,
logging::setup_default_logging,
proto::command::{
request::RequestType, AddBackend, Cluster, LoadBalancingAlgorithms, LoadBalancingParams,
PathRule, Request, RequestHttpFrontend, RulePosition, SocketAddress,WorkerRequest,
},
};
fn main() -> anyhow::Result<()> {
setup_default_logging(true, "info", "EXAMPLE").with_context(|| "could not setup logging")?;
info!("starting up");
let http_listener = ListenerBuilder::new_http(SocketAddress::new_v4(127,0,0,1,8080))
.to_http(None)
.expect("Could not create HTTP listener");
let (mut command_channel, proxy_channel) =
Channel::generate(1000, 10000).with_context(|| "should create a channel")?;
let worker_thread_join_handle = thread::spawn(move || {
let max_buffers = 500;
let buffer_size = 16384;
sozu_lib::http::testing::start_http_worker(http_listener, proxy_channel, max_buffers, buffer_size)
.expect("The worker could not be started, or shut down");
});
let cluster = Cluster {
cluster_id: "my-cluster".to_string(),
sticky_session: false,
https_redirect: false,
load_balancing: LoadBalancingAlgorithms::RoundRobin as i32,
answer_503: Some("A custom forbidden message".to_string()),
..Default::default()
};
let http_front = RequestHttpFrontend {
cluster_id: Some("my-cluster".to_string()),
address: SocketAddress::new_v4(127,0,0,1,8080),
hostname: "example.com".to_string(),
path: PathRule::prefix(String::from("/")),
position: RulePosition::Pre.into(),
tags: BTreeMap::from([
("owner".to_owned(), "John".to_owned()),
("id".to_owned(), "my-own-http-front".to_owned()),
]),
..Default::default()
};
let http_backend = AddBackend {
cluster_id: "my-cluster".to_string(),
backend_id: "test-backend".to_string(),
address: SocketAddress::new_v4(127,0,0,1,8000),
load_balancing_parameters: Some(LoadBalancingParams::default()),
..Default::default()
};
command_channel
.write_message(&WorkerRequest {
id: String::from("add-the-cluster"),
content: RequestType::AddCluster(cluster).into(),
})
.expect("Could not send AddHttpFrontend request");
command_channel
.write_message(&WorkerRequest {
id: String::from("add-the-frontend"),
content: RequestType::AddHttpFrontend(http_front).into(),
})
.expect("Could not send AddHttpFrontend request");
command_channel
.write_message(&WorkerRequest {
id: String::from("add-the-backend"),
content: RequestType::AddBackend(http_backend).into(),
})
.expect("Could not send AddBackend request");
println!("HTTP -> {:?}", command_channel.read_message());
println!("HTTP -> {:?}", command_channel.read_message());
// uncomment to let it run in the background
// let _ = worker_thread_join_handle.join();
info!("good bye");
Ok(())
}
Modules§
- event loop management
- Timer based on timing wheels
- A unified certificate resolver for rustls.
Macros§
- adds a value to a counter
- adds 1 to a counter
Structs§
- exponentially weighted moving average with high sensibility to latency bursts
Enums§
- Used in sessions
- returned by the HTTP, HTTPS and TCP listeners
- Anything that can be registered in mio (subscribe to kernel events)
- Returned by the HTTP, HTTPS and TCP proxies
- used in kawa_h1 module for the Http session state
- Signals transitions between states of a given Session
- Signals transitions between states of a given Protocol
Traits§
- trait that must be implemented by listeners and client sessions