This quick start shows how to build a bare-bones load balancer using pingora and pingora-proxy.
The goal of the load balancer is for every incoming HTTP request, select one of the two backends: https://1.1.1.1 and https://1.0.0.1 in a round-robin fashion.
## Build a basic load balancer
Create a new cargo project for our load balancer. Let's call it `load_balancer`
```
cargo new load_balancer
```
### Include the Pingora Crate and Basic Dependencies
In your project's `cargo.toml` file add the following to your dependencies
```
async-trait="0.1"
pingora = { version = "0.1", features = [ "lb" ] }
```
### Create a pingora server
First, let's create a pingora server. A pingora `Server` is a process which can host one or many
services. The pingora `Server` takes care of configuration and CLI argument parsing, daemonization,
signal handling, and graceful restart or shutdown.
The preferred usage is to initialize the `Server` in the `main()` function and
use `run_forever()` to spawn all the runtime threads and block the main thread until the server is
ready to exit.
```rust
use async_trait::async_trait;
use pingora::prelude::*;
use std::sync::Arc;
fn main() {
let mut my_server = Server::new(None).unwrap();
my_server.bootstrap();
my_server.run_forever();
}
```
This will compile and run, but it doesn't do anything interesting.
### Create a load balancer proxy
Next let's create a load balancer. Our load balancer holds a static list of upstream IPs. The `pingora-load-balancing` crate already provides the `LoadBalancer` struct with common selection algorithms such as round robin and hashing. So let’s just use it. If the use case requires more sophisticated or customized server selection logic, users can simply implement it themselves in this function.
```rust
pub struct LB(Arc<LoadBalancer<RoundRobin>>);
```
In order to make the server a proxy, we need to implement the `ProxyHttp` trait for it.
Any object that implements the `ProxyHttp` trait essentially defines how a request is handled in
the proxy. The only required method in the `ProxyHttp` trait is `upstream_peer()` which returns
the address where the request should be proxied to.
In the body of the `upstream_peer()`, let's use the `select()` method for the `LoadBalancer` to round-robin across the upstream IPs. In this example we use HTTPS to connect to the backends, so we also need to specify to `use_tls` and set the SNI when constructing our [`Peer`](user_guide/peer.md)) object.
let background = background_service("health check", upstreams);
let upstreams = background.task();
// `upstreams` no longer need to be wrapped in an arc
let mut lb = http_proxy_service(&my_server.configuration, LB(upstreams));
lb.add_tcp("0.0.0.0:6188");
my_server.add_service(background);
my_server.add_service(lb);
my_server.run_forever();
}
```
Now if we again run and test our load balancer, we see that all requests
succeed and the broken peer is never used. Based on the configuration we used,
if that peer were to become healthy again, it would be re-included in the round
robin again in within 1 second.
### Command line options
The pingora `Server` type provides a lot of built-in functionality that we can
take advantage of with single-line change.
```rust
fn main() {
let mut my_server = Server::new(Some(Opt::default())).unwrap();
...
}
```
With this change, the command-line arguments passed to our load balancer will be
consumed by Pingora. We can test this by running:
```
cargo run -- -h
```
We should see a help menu with the list of arguments now available to us. We
will take advantage of those in the next sections to do more with our load
balancer for free
### Running in the background
Passing the parameter `-d` or `--daemon` will tell the program to run in the background.
```
cargo run -- -d
```
To stop this service, you can send `SIGTERM` signal to it for a graceful shutdown, in which the service will stop accepting new request but try to finish all ongoing requests before exiting.
```
pkill -SIGTERM load_balancer
```
(`SIGTERM` is the default signal for `pkill`.)
### Configurations
Pingora configuration files help define how to run the service. Here is an
example config file that defines how many threads the service can have, the
location of the pid file, the error log file, and the upgrade coordination
socket (which we will explain later). Copy the contents below and put them into
a file called `conf.yaml` in your `load_balancer` project directory.
```yaml
---
version: 1
threads: 2
pid_file: /tmp/load_balancer.pid
error_log: /tmp/load_balancer_err.log
upgrade_sock: /tmp/load_balancer.sock
```
To use this conf file:
```
RUST_LOG=INFO cargo run -- -c conf.yaml -d
```
`RUST_LOG=INFO` is here so that the service actually populate the error log.
Let's say we changed the code of the load balancer and recompiled the binary. Now we want to upgrade the service running in the background to this newer version.
If we simply stop the old service, then start the new one, some request arriving in between could be lost. Fortunately, Pingora provides a graceful way to upgrade the service.
This is done by, first, send `SIGQUIT` signal to the running server, and then start the new server with the parameter `-u` \ `--upgrade`.
```
pkill -SIGQUIT load_balancer &&\
RUST_LOG=INFO cargo run -- -c conf.yaml -d -u
```
In this process, The old running server will wait and hand over its listening sockets to the new server. Then the old server runs until all its ongoing requests finish.
From a client's perspective, the service is always running because the listening socket is never closed.
## Full examples
The full code for this example is available in this repository under