用Rust和Pingora輕鬆構建高效負載均衡器

极客开发者發表於2024-06-20

目錄

  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

相關文章