用Rust和Pingora轻松构建超越Nginx的高效负载均衡器

目录

  1. 什么是Pingora?
  2. 实现过程
    • 初始化项目
    • 编写负载均衡器代码
    • 代码解析
    • 部署
  3. 总结

1. 什么是Pingora?

Pingora 是一个高性能的 Rust 库,用于构建可负载均衡器的代理服务器,它的诞生是为了弥补 Nginx 存在的缺陷。

Pingora 提供了丰富的功能和高度的扩展性,适用于各种网络应用场景。其高效的性能、易于扩展的设计以及 Rust 语言本身的安全性和速度。使得 Pingora 能够处理大量并发请求,确保高可靠性和稳定性。本文将带您一步步使用 Pingora 构建一个基础的负载均衡器。

如果你还不了解 Pingora 的相关背景, 建议先阅读:《一天为用户节省434年握手时间!Rust编写的Pingora凭什么力压Nginx?》

2. 实现过程

2.1 初始化项目

首先,我们需要一个 Rust 项目,并添加必要的依赖项。在项目根目录下的 Cargo.toml 文件中添加以下内容:

[package]
name = "load_balancer"
version = "0.1.0"
edition = "2021"

[dependencies]
async-trait = "0.1"
pingora = { version = "0.1", features = ["lb"] }

2.2 编写负载均衡器代码

src/main.rs 中编写负载均衡器的实现代码。以下是完整的代码示例:

use async_trait::async_trait;
use pingora::{prelude::*, services::Service};
use std::sync::Arc;

fn main() {
    // 创建一个服务器实例,传入Some(Opt::default())代表使用默认配置,程序执行时支持接收命令行参数
    let mut my_server = Server::new(Some(Opt::default())).unwrap();
    // 初始化服务器
    my_server.bootstrap();
    // 创建一个负载均衡器,包含多个上游服务器
    let mut upstreams = LoadBalancer::try_from_iter(["10.0.0.1:8080", "10.0.0.2:8080", "10.0.0.3:8080"]).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();

    // 创建一个HTTP代理服务,并传入服务器配置和负载均衡器
    let mut lb_service: pingora::services::listening::Service<pingora::proxy::HttpProxy<LB>> =
        http_proxy_service(&my_server.configuration, LB(upstreams));
    // 添加一个TCP监听地址,监听80端口
    lb_service.add_tcp("0.0.0.0:80");

    // 添加一个TLS监听地址,监听443端口
    println!("The cargo manifest dir is: {}", env!("CARGO_MANIFEST_DIR"));
    // 在项目目录下新增一个 keys 目录,对应证书文件放在该目录下
    let cert_path = format!("{}/keys/example.com.crt", env!("CARGO_MANIFEST_DIR"));
    let key_path = format!("{}/keys/example.com.key", env!("CARGO_MANIFEST_DIR"));
    let mut tls_settings =
        pingora::listeners::TlsSettings::intermediate(&cert_path, &key_path).unwrap();
    tls_settings.enable_h2();
    lb_service.add_tls_with_settings("0.0.0.0:443", None, tls_settings);

    // 定义服务列表,这个示例只有一个负载均衡服务,后续有需要可以添加更多,将服务列表添加到服务器中
    let services: Vec<Box<dyn Service>> = vec![Box::new(lb_service)];
    my_server.add_services(services);
    // 运行服务器,进入事件循环
    my_server.run_forever();
}

// 定义一个包含负载均衡器的结构体LB,用于包装Arc指针以实现多线程共享
pub struct LB(Arc<LoadBalancer<RoundRobin>>);

// 使用#[async_trait]宏,异步实现ProxyHttp trait。
#[async_trait]
impl ProxyHttp for LB {
    /// 定义上下文类型,这里使用空元组,对于这个小例子,我们不需要上下文存储
    type CTX = ();
    // 创建新的上下文实例,这里返回空元组
    fn new_ctx(&self) -> () {
        ()
    }
    // 选择上游服务器并创建HTTP对等体
    async fn upstream_peer(&self, _session: &mut Session, _ctx: &mut ()) -> Result<Box<HttpPeer>> {
        // 使用轮询算法选择上游服务器
        let upstream = self
            .0
            .select(b"", 256) // 对于轮询,哈希不重要
            .unwrap();
        println!("上游对等体是:{upstream:?}");
        // 创建一个新的HTTP对等体,设置SNI为example.com
        let peer: Box<HttpPeer> =
            Box::new(HttpPeer::new(upstream, false, "example.com".to_string()));
        Ok(peer)
    }

    // 在上游请求发送前,执行一些额外操作,例如将某些参数插入请求头,这里的示例是插入Host头部
    async fn upstream_request_filter(
        &self,
        _session: &mut Session,
        upstream_request: &mut RequestHeader,
        _ctx: &mut Self::CTX,
    ) -> Result<()> {
        // 将Host头部设置为example.com,当然,在现实需求中,这一步可能是多余的
        upstream_request
            .insert_header("Host", "example.com")
            .unwrap();
        Ok(())
    }
}

3. 代码解析

3.1 对等体健康检查

为了使我们的负载均衡器更可靠,我们添加了健康检查功能到我们的上游对等体。这样,如果有一个对等体已经出现异常,就可以快速停止将流量路由到该对等体。如下代码

fn main() {
    // ...
    // 以下对等体中包含一个异常的对等体
    let upstreams =
        LoadBalancer::try_from_iter(["10.0.0.1:8080", "10.0.0.2:8080", "10.0.0.3:8080"]).unwrap();
    // ...
}

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

curl http://127.0.0.1 -svo /dev/null

如果去掉健康检查的代码片段,我们发现会出现 502: Bad Gateway 的失败情况,这是因为我们的对等体选择严格遵循我们给出的 RoundRobin 选择模式,而没有考虑该对等体是否健康。通过引入一个健康检查的功能来解决这个问题,进而排除掉不健康对等体。关键代码如下

fn main() {
    // ...
    // 健康检查
    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();
    // ...
}

3.2 接收命令行参数

在创建 pingora 服务时,传入了一个 Some(Opt::default())

参数,pingora 将会捕获我们运行的命令行参数,并使用这些参数来配置 pingora 服务。代码变更如下

fn main() {
    // ...
    let mut my_server = Server::new(Some(Opt::default())).unwrap();
    // ...
}

我们可以通过以下命令来看 pingora 负载均衡器的参数说明

cargo run -- -h

这时我们可以了解到 pingora 相关参数提供的功能,后续可以为我们的服务器实现更多的功能。

4. 部署

4.1 后台运行

通过传递 -d 或者 --daemon 参数,可以将 pingora 运行在后台。如果要优雅的停止 pingora,可以使用 pkill 命令并且传递 SIGTERM 信号,那么在关闭的过程中,服务将停止接收新的请求,但是仍然会处理完当前请求再退出。命令如下

# 后台运行,我们使用release模式,因为debug模式下会生成调试信息,会影响性能
cargo run --release -- -d
# 优雅的停止
pkill -SIGTERM load_balancer

4.2 配置

Pingora 配置文件可以定义 Pingora 如何运行,以下定义了 Pingora 的版本、线程数、pid文件、错误日志文件、升级套接字文件的配置,文件名称命名为conf.yaml

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

加载配置文件运行如下:

# 设置日志级别
RUST_LOG=INFO
# 启用
cargo run --release -- -c conf.yaml -d

4.3 优雅地升级

假设我们更改了负载均衡器的代码并重新编译了二进制文件,现在我们希望将正在后台运行的服务升级到这个新版本。如果我们简单地停止旧服务,然后启动新服务,那么在中间到达的一些请求可能会丢失。幸运的是,Pingora 提供了一种优雅的方式来升级服务。

首先,我们通过SIGQUIT停止正在运行的服务,然后使用-u或者--upgrade参数来启动全新的程序,如下命令

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

在升级过程中,Pingora 将会自动将请求路由到新的服务,而不会丢失任何请求。从客户端的角度来看,用户感觉不到任何变化。

5. 总结

到此为止,我们已经拥有了一个功能完备的负载均衡器。通过这个简单的示例,相信大家已经对 Pingora 有了一个初步的了解。不过,这是一个非常基础的负载均衡器。在实际应用中,负载均衡器的配置和功能可能会更加复杂,我们还需要根据实际需求来进行扩展和优化。

在后续,我也会分享一些关于 Pingora 以及新兴热门技术的更多内容,欢迎继续关注!

本文完整示例代码:https://github.com/phyuany/simple-pingora-reverse-proxy

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容