【一】pingora入门:负载均衡 和 健康检查

原文发布在:Asher的博客 | 【一】pingora入门:负载均衡 和 健康检查

写在前面

自从听说了 Pingora 的性能吊打Nginx,每天处理超过 1 万亿条请求,就一直很想试试,顺便学学Rust的知识,于是让我们从官方教程一步步入手吧。

官方教程,如果你觉得后续文章中有什么错误的地方,可以参考官方教程:quick_start

负载均衡的基础知识:

负载均衡器是位于客户端和后端服务器之间的设备或软件,它接受客户端的请求,并将请求分发到后端的多个服务器上。

负载均衡算法决定将请求分配给哪台后端服务器时,会使用不同的算法来分配流量。常见的算法包括:轮询 (Round Robin)、源地址哈希 (Source IP Hashing)、一致性哈希 (Consistent Hashing)

创建一个简单的负载均衡代理

开始之前可以用curl测试一下能否连接到我的博客域名,因为官方给出的教程是请求到["1.1.1.1:443", "1.0.0.1:443"] 又因为众所周知的缘故,时不时连接不上,所以这里的教程使用自建的服务测试

shell 复制代码
curl https://testapi.runnable.run/hl/check
curl https://testapi2.runnable.run/hl/check

能看到正确返回时则表示没问题

那么 testapi.runnable.runtestapi2.runnable.run 在这次教程中被当作后端服务,而我们需要通过Pingora创建一个负载均衡器。

新建一个Rust项目, 并增加以下依赖

toml 复制代码
async-trait="0.1"
pingora = { version = "0.3", features = [ "lb" ] }
  • pingora是教程中的主角
  • async-trait: 提供了一种可以让你在 trait 方法中使用 async 关键字的方式。在 Rust 中,默认情况下,async 不能直接在 trait 方法中使用,而 async-trait 这个库通过一些底层机制解决了这个问题,使得你可以将异步函数用于 trait 实现中。

代码部份

pingora-load-balancing crate 已为 LoadBalancer 结构提供了常用的选择算法,如循环和散列算法。

rust 复制代码
pub struct LB(Arc<LoadBalancer<RoundRobin>>);

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

upstream_peer() 主体中,让我们使用 LoadBalancer 的 select() 方法对上游 IP 进行轮循。 在本例中,我们使用 HTTPS 连接到后端,因此在构建 节点(peer) 对象时,我们还需要指定使用use_tls 并设置 SNI。

SNI 来确保你的服务能够在同一 IP 地址下处理多个 SSL 证书和域名

rust 复制代码
#[async_trait]
impl ProxyHttp for LB {

    /// 在这个小例子中,我们不需要上下文存储
    type CTX = ();
    fn new_ctx(&self) -> () {
        ()
    }

    async fn upstream_peer(&self, _session: &mut Session, _ctx: &mut ()) -> Result<Box<HttpPeer>> {
        let upstream = self.0
            .select(b"", 256) // 轮询调度策略(round robin)不依赖哈希值
            .unwrap();

        println!("上游节点是: {upstream:?}");

        // 将 SNI 设置为 runnable.run ,SNI 来确保你的服务能够在同一 IP 地址下处理多个 SSL 证书和域名
        let peer = Box::new(HttpPeer::new(upstream, true, "runnable.run".to_string()));
        Ok(peer)
    }

    async fn upstream_request_filter(
        &self,
        _session: &mut Session,
        upstream_request: &mut RequestHeader,
        _ctx: &mut Self::CTX,
    ) -> Result<()> {
        upstream_request.insert_header("Host", "runnable.run").unwrap();
        Ok(())
    }
}

为了让 runnable.run 后端接受我们的请求,主机头必须存在。可以通过 upstream_request_filter() 回调来添加该标头,它可以在与后端建立连接后、发送请求标头前修改请求标头。

rust 复制代码
async fn upstream_request_filter(
        &self,
        _session: &mut Session,
        upstream_request: &mut RequestHeader,
        _ctx: &mut Self::CTX,
    ) -> Result<()> {
        upstream_request.insert_header("Host", "runnable.run").unwrap();
        Ok(())
    }

创建 pingora-proxy 服务

接下来,让我们按照上述负载均衡器的实现创建一个代理服务。

pingora 服务监听一个或多个(TCP 或 Unix socket)endpoints。pingora-proxy 就是这样一个应用程序,它按照上述配置将 HTTP 请求代理到给定的后端。

在下面的示例中,我们创建了一个具有两个后端 ["testapi.runnable.run:443", "testapi2.runnable.run:443"] 的 LB 实例。我们通过 http_proxy_service()调用将 LB 实例置于代理服务中,然后告诉服务器托管该代理服务。

rust 复制代码
fn main() {
    let mut my_server = Server::new(None).unwrap();
    my_server.bootstrap();

    let upstreams = LoadBalancer::try_from_iter(["testapi.runnable.run:443", "testapi2.runnable.run:443"]).unwrap();

    let mut lb = http_proxy_service(&my_server.configuration, LB(Arc::new(upstreams)));
    lb.add_tcp("0.0.0.0:6188");

    my_server.add_service(lb);

    my_server.run_forever();
}

完整代码如下:

Rust 复制代码
use async_trait::async_trait;
use pingora::prelude::*;
use std::sync::Arc;

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

#[async_trait]
impl ProxyHttp for LB {

    /// 在这个小例子中,我们不需要上下文存储
    type CTX = ();
    fn new_ctx(&self) -> () {
        ()
    }

    async fn upstream_peer(&self, _session: &mut Session, _ctx: &mut ()) -> Result<Box<HttpPeer>> {
        let upstream = self.0
            .select(b"", 256) // 轮询调度策略(round robin)不依赖哈希值
            .unwrap();

        println!("上游节点是: {upstream:?}");

        // 将 SNI 设置为 runnable.run ,SNI 来确保你的服务能够在同一 IP 地址下处理多个 SSL 证书和域名
        let peer = Box::new(HttpPeer::new(upstream, true, "runnable.run".to_string()));
        Ok(peer)
    }

    async fn upstream_request_filter(
        &self,
        _session: &mut Session,
        upstream_request: &mut RequestHeader,
        _ctx: &mut Self::CTX,
    ) -> Result<()> {
        upstream_request.insert_header("Host", "runnable.run").unwrap();
        Ok(())
    }
}



fn main() {
    let mut my_server = Server::new(None).unwrap();
    my_server.bootstrap();

    let upstreams = LoadBalancer::try_from_iter(["testapi.runnable.run:443", "testapi2.runnable.run:443"]).unwrap();

    let mut lb = http_proxy_service(&my_server.configuration, LB(Arc::new(upstreams)));
    lb.add_tcp("0.0.0.0:6188");

    my_server.add_service(lb);

    my_server.run_forever();
}

运行

rust 复制代码
cargo run

测试的shell

shell 复制代码
curl 127.0.0.1:6188 -svo /dev/null

这个测试用例 curl 127.0.0.1:6188 -svo /dev/null 主要是为了验证代理服务是否正常工作。下面逐步解释这个命令:

  1. curl:这是一个命令行工具,用于发起 HTTP 请求。
  2. 127.0.0.1:6188:指的是发起请求的目标地址,即本地代理服务器运行的地址(127.0.0.1 表示本地主机,6188 是你的代码中代理服务监听的端口)。
  3. -s:代表 "silent" 模式,隐藏进度条和错误信息,除非有严重错误时才会显示。一般用于安静地测试或调试,不输出太多干扰信息。
  4. -v:代表 "verbose" 模式,会显示详细的请求和响应头信息,包括连接的详细过程,比如发送的请求、返回的响应、重定向情况等。这个可以帮助分析请求的完整细节,验证代理的行为。
  5. -o /dev/null:将输出结果重定向到 /dev/null,即丢弃所有响应的内容,不把请求的响应内容保存到文件或屏幕上。这是为了仅关注请求和响应的头信息,而不关心实际返回的页面内容。
  6. -svo 结合起来的意思:既静默执行(没有额外的进度条和错误信息输出),又在 verbose 模式下展示详细的请求和响应头信息,同时将响应的正文内容丢弃。

通过这个命令,你可以查看你的代理是否正确处理了请求,并且可以通过 verbose 输出看到代理向上游发起的请求头信息,验证如 Host 头是否正确设置为 "one.one.one.one",以及代理连接到了哪个上游节点(1.1.1.1 或 1.0.0.1)。

请求成功时截图

节点健康检查(Peer health checks)

为了使我们的负载均衡器更加可靠,我们希望为上游节点设备添加一些健康检查。这样,如果某个节点设备出现故障,我们就可以迅速停止将流量路由到该节点设备。

首先,让我们看看当其中一个节点设备宕机时,我们的负载均衡器会有什么表现。为此,我们将更新节点设备列表,以包含一个保证会损坏的节点设备

rust 复制代码
fn main() {
    // ...
    let mut upstreams =
        LoadBalancer::try_from_iter(["testapi.runnable.run:443", "testapi2.runnable.run:443", "127.0.0.1:343"]).unwrap();
    // ...
}

现在,如果我们再次使用 cargo run 运行负载均衡器,并使用

rust 复制代码
curl 127.0.0.1:6188 -svo /dev/null

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

Rust 复制代码
fn main() {
    let mut my_server = Server::new(None).unwrap();
    my_server.bootstrap();

    // 请注意,现在需要将上游声明为 `mut`
    let mut upstreams =
        LoadBalancer::try_from_iter(["testapi.runnable.run:443", "testapi2.runnable.run:443", "127.0.0.1:343"]).unwrap();

    let hc = TcpHealthCheck::new();
    upstreams.set_health_check(hc);
    upstreams.health_check_frequency = Some(std::time::Duration::from_secs(1));

    let background = background_service("health check", upstreams);
    let upstreams = background.task();

    // `upstreams`不再需要被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();
}

可以看到没有一次请求到127.0.0.1这个ip,说明我们的健康检查已经生效

相关推荐
苹果醋32 小时前
Golang的容器编排实践
运维·vue.js·spring boot·nginx·课程设计
WITLab2 小时前
Rust 实践教程-开发工具及简单配置(002)
rust
花姐夫Jun3 小时前
基于CentOS的Docker + Nginx + Gitee + Jenkins部署总结
nginx·docker·centos·jenkins
暗碳4 小时前
termux配置nginx+php
运维·nginx·php
运维搬运工11 小时前
nginx
运维·nginx
ChineHe11 小时前
nginx基础篇 - 入门介绍与安装教程
运维·nginx
嗑瓜子儿溜茶水儿11 小时前
docker 部署 NginX
nginx·docker·容器
一二小选手14 小时前
【Nginx】Nginx代理模式相关概念解释及Nginx安装
java·nginx·代理模式
Elcker16 小时前
Koi技术教程-Tauri-第一章 Tauri简介
javascript·rust
m0_7482563421 小时前
Nginx 负载均衡详解
运维·nginx·负载均衡