快速入门:负载均衡器

引言

这个快速入门展示了如何使用 pingora 和 pingora-proxy 构建一个基础的负载均衡器。

负载均衡器的目标是对于每个传入的 HTTP 请求,以轮询的方式选择两个后端之一:https://1.1.1.1https://1.0.0.1。

构建一个基础的负载均衡器

为我们的负载均衡器创建一个新的 cargo 项目。我们称之为 load_balancer

  1. cargo new load_balancer

包含 Pingora Crate 和基本依赖

在项目的 cargo.toml 文件中添加以下依赖:

  1. async-trait="0.1"
  2. pingora = { version = "0.1", features = ["lb"] }

创建一个 pingora 服务器

首先,让我们创建一个 pingora 服务器。一个 pingora Server 是一个可以托管一个或多个服务的进程。pingora Server 负责配置和命令行参数解析、守护进程化、信号处理和优雅重启或关闭。

推荐的做法是在 main() 函数中初始化 Server 并使用 run_forever() 来启动所有运行时线程,并阻塞主线程直到服务器准备退出。

  1. use async_trait::async_trait;
  2. use pingora::prelude::*;
  3. use std::sync::Arc;
  4. fn main() {
  5. let mut my_server = Server::new(None).unwrap();
  6. my_server.bootstrap();
  7. my_server.run_forever();
  8. }

这将编译并运行,但它没有做任何有趣的事情。

创建一个负载均衡器代理

接下来,让我们创建一个负载均衡器。我们的负载均衡器持有一个静态的上游 IP 列表。pingora-load-balancing crate 已经提供了 LoadBalancer 结构体,它有常见的选择算法,如轮询和哈希。所以让我们使用它。如果用例需要更复杂或定制化的服务器选择逻辑,用户可以简单地在这个函数中实现它。

  1. pub struct LB(Arc<LoadBalancer<RoundRobin>>);

为了使服务器成为代理,我们需要为它实现 ProxyHttp trait。

任何实现了 ProxyHttp trait 的对象本质上定义了代理中如何处理请求。ProxyHttp trait 中唯一需要的方法是 upstream_peer(),它返回请求应该被代理到的地址。

upstream_peer() 的主体中,让我们使用 LoadBalancerselect() 方法在上游 IP 上进行轮询。在这个例子中,我们使用 HTTPS 连接到后端,所以我们还需要指定 use_tls 并在构建我们的 Peer 对象时设置 SNI。

  1. #[async_trait]
  2. impl ProxyHttp for LB {
  3. /// 对于这个小例子,我们不需要上下文存储
  4. type CTX = ();
  5. fn new_ctx(&self) -> () {
  6. ()
  7. }
  8. async fn upstream_peer(&self, _session: &mut Session, _ctx: &mut ()) -> Result<Box<HttpPeer>> {
  9. let upstream = self.0
  10. .select(b"", 256) // 对于轮询,哈希不重要
  11. .unwrap();
  12. println!("上游对等体是:{upstream:?}");
  13. // 设置 SNI 到 one.one.one.one
  14. let peer = Box::new(HttpPeer::new(upstream, true, "one.one.one.one".to_string()));
  15. Ok(peer)
  16. }
  17. }

为了让 1.1.1.1 后端接受我们的请求,必须存在一个主机头。通过 upstream_request_filter() 回调添加这个头部,该回调在与后端建立连接并发送请求头部之前修改请求头部。

  1. impl ProxyHttp for LB {
  2. // ...
  3. async fn upstream_request_filter(
  4. &self,
  5. _session: &mut Session,
  6. upstream_request: &mut RequestHeader,
  7. _ctx: &mut Self::CTX,
  8. ) -> Result<()> {
  9. upstream_request.insert_header("Host", "one.one.one.one").unwrap();
  10. Ok(())
  11. }
  12. }

创建一个 pingora-proxy 服务

接下来,让我们创建一个遵循上述负载均衡器指令的代理服务。

一个 pingora Service 监听一个或多个(TCP 或 Unix 域套接字)端点。当建立新连接时,Service 将连接交给其“应用程序”。pingora-proxy 就是这样一个应用程序,它将 HTTP 请求代理到上面配置的给定后端。

在下面的示例中,我们创建了一个带有两个后端 1.1.1.1:4431.0.0.1:443LB 实例。我们通过 http_proxy_service() 调用将该 LB 实例放入代理 Service 中,然后告诉我们的 Server 托管该代理 Service

  1. fn main() {
  2. let mut my_server = Server::new(None).unwrap();
  3. my_server.bootstrap();
  4. let upstreams =
  5. LoadBalancer::try_from_iter(["1.1.1.1:443", "1.0.0.1:443"]).unwrap();
  6. let mut lb = http_proxy_service(&my_server.configuration, LB(Arc::new(upstreams)));
  7. lb.add_tcp("0.0.0.0:6188");
  8. my_server.add_service(lb);
  9. my_server.run_forever();
  10. }

运行它

现在我们已经将负载均衡器添加到服务中,我们可以使用

  1. cargo run

来运行我们的新项目。

要测试它,只需使用命令发送几个请求到服务器:

  1. curl 127.0.0.1:6188 -svo /dev/null

您也可以通过浏览器访问 http://localhost:6188

以下输出显示负载均衡器正在执行其工作,以平衡两个后端:

  1. 上游对等体是:Backend { addr: Inet(1.0.0.1:443), weight: 1 }
  2. 上游对等体是:Backend { addr: Inet(1.1.1.1:443), weight: 1 }
  3. 上游对等体是:Backend { addr: Inet(1.0.0.1:443), weight: 1 }
  4. 上游对等体是:Backend { addr: Inet(1.1.1.1:443), weight: 1 }
  5. 上游对等体是:Backend { addr: Inet(1.0.0.1:443), weight: 1 }
  6. ...

干得好!到此为止,您已经拥有了一个功能完备的负载均衡器。不过,这是一个非常基础的负载均衡器,所以下一节将带您了解如何使用一些内置的 pingora 工具使其更加健壮。

添加功能

Pingora 提供了几个有用的功能,只需几行代码就可以启用和配置。这些功能从简单的对等体健康检查到能够无缝地使用零服务中断更新运行中的二进制文件。

对等体健康检查

为了使我们的负载均衡器更可靠,我们希望添加一些健康检查到我们的上游对等体。这样,如果有一个对等体已经宕机,我们可以快速停止将流量路由到该对等体。

首先让我们看看当我们的负载均衡器中的一个对等体宕机时,我们的简单负载均衡器的行为。为了做到这一点,我们将更新对等体列表以包括一个保证会宕机的对等体。

  1. fn main() {
  2. // ...
  3. let upstreams =
  4. LoadBalancer::try_from_iter(["1.1.1.1:443", "1.0.0.1:443", "127.0.0.1:343"]).unwrap();
  5. // ...
  6. }

现在如果我们再次运行我们的负载均衡器 cargo run,并用以下命令测试它:

  1. curl 127.0.0.1:6188 -svo /dev/null

我们可以看到每三个请求中就有一个以 502: Bad Gateway 失败。这是因为我们的对等体选择严格遵循我们给出的 RoundRobin 选择模式,而没有考虑该对等体是否健康。我们可以通过添加一个基本的健康检查服务来解决这个问题。

  1. fn main() {
  2. let mut my_server = Server::new(None).unwrap();
  3. my_server.bootstrap();
  4. // 注意现在 upstreams 需要被声明为 `mut`
  5. let mut upstreams =
  6. LoadBalancer::try_from_iter(["1.1.1.1:443", "1.0.0.1:443", "127.0.0.1:343"]).unwrap();
  7. let hc = TcpHealthCheck::new();
  8. upstreams.set_health_check(hc);
  9. upstreams.health_check_frequency = Some(std::time::Duration::from_secs(1));
  10. let background = background_service("health check", upstreams);
  11. let upstreams = background.task();
  12. // `upstreams` 不再需要被包裹在 arc 中
  13. let mut lb = http_proxy_service(&my_server.configuration, LB(upstreams));
  14. lb.add_tcp("0.0.0.0:6188");
  15. my_server.add_service(background);
  16. my_server.add_service(lb);
  17. my_server.run_forever();
  18. }

现在如果我们再次运行并测试我们的负载均衡器,我们看到所有请求都成功,并且损坏的对等体从未被使用。根据我们使用的配置,如果那个对等体重新变得健康,它会在 1 秒内再次被包括在轮询中。

命令行选项

pingora Server 类型提供了许多内置功能,我们可以通过单行更改来利用。

  1. fn main() {
  2. let mut my_server = Server::new(Some(Opt::default())).unwrap();
  3. ...
  4. }

通过这个更改,传递给我们负载均衡器的命令行参数将被 Pingora 消耗。我们可以通过运行:

  1. cargo run -- -h

来测试这一点。

我们应该会看到一个带有现在对我们的负载均衡器可用的参数列表的帮助菜单。我们将在接下来的部分中利用这些参数,免费为我们的负载均衡器做更多的事情。

在后台运行

传递参数 -d--daemon 将告诉程序在后台运行。

  1. cargo run -- -d

要停止此服务,您可以向其发送 SIGTERM 信号以进行优雅关闭,在关闭过程中,服务将停止接受新请求,但会尝试完成所有正在进行的请求后再退出。

  1. pkill -SIGTERM load_balancer

SIGTERMpkill 的默认信号。)

配置

Pingora 配置文件有助于定义如何运行服务。以下是定义服务可以拥有的线程数、pid 文件的位置、错误日志文件和升级协调套接字(我们将在后面解释)的示例配置文件。将以下内容复制并放入您的 load_balancer 项目目录中名为 conf.yaml 的文件。

  1. ---
  2. version: 1
  3. threads: 2
  4. pid_file: /tmp/load_balancer.pid
  5. error_log: /tmp/load_balancer_err.log
  6. upgrade_sock: /tmp/load_balancer.sock

要使用此 conf 文件:

  1. RUST_LOG=INFO cargo run -- -c conf.yaml -d

RUST_LOG=INFO 在这里是为了让服务实际上填充错误日志。

现在您可以找到服务的 pid。

  1. cat /tmp/load_balancer.pid

优雅地升级服务

(仅限 Linux)

假设我们更改了负载均衡器的代码并重新编译了二进制文件。现在我们希望将正在后台运行的服务升级到这个新版本。

如果我们简单地停止旧服务,然后启动新服务,那么在中间到达的一些请求可能会丢失。幸运的是,Pingora 提供了一种优雅的方式来升级服务。

这可以通过首先向正在运行的服务器发送 SIGQUIT 信号,然后使用参数 -u \ --upgrade 启动新服务器来完成。

  1. pkill -SIGQUIT load_balancer &&\
  2. RUST_LOG=INFO cargo run -- -c conf.yaml -d -u

在这个过程中,旧的运行服务器将等待并将其监听套接字交给新服务器。然后旧服务器会一直运行,直到所有正在进行的请求完成。

从客户端的角度来看,服务始终在运行,因为监听套接字从未关闭。

全部示例

这个示例的全部代码可以在本仓库的以下路径找到:

pingora-proxy/examples/load_balancer.rs

您还可能在这里找到其他有用的示例