目錄
- 什麼是Pingora?
- 實現過程
- 初始化專案
- 編寫負載均衡器程式碼
- 程式碼解析
- 部署
- 總結
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