從Rust模塊化探索到DLB 2.0實踐
一、前言
二、Nginx+Rust 的模塊化探索
三、全面擁抱Rust進入DLB2.0階段
1. 配置體系
2. 流量處理
四、總結
一、前言
在云原生架構高速迭代的背景下,基礎設施的性能瓶頸與安全隱患成為技術演進的關鍵挑戰。本文系統記錄了團隊基于Rust語言改造Nginx組件的完整技術路徑:從接觸Cloudflare的quiche庫,引發對Rust安全特性的探索,到通過FFI實現核心邏輯的跨語言調用;從突破傳統C模塊開發范式自研 ngx_http_rust_module SDK ,到全面采用Pingora框架構建新一代DLB 2.0流量調度平臺。
實踐表明,Rust的內存安全機制與異步高并發能力可顯著提升負載均衡組件的性能邊界與可靠性,為超大規模流量調度場景提供全新解決方案。本技術演進過程將詳述架構設計、核心模塊實現及性能優化策略,為同類基礎設施升級提供可復用的工程經驗。
二、Nginx+Rust 的模塊化探索
探索的起點源于和quiche(cloudflare開發的高效quic實現)的初次邂逅,這扇門將項目組成員引入了 Rust 語言的世界。Rust 以其卓越的內存安全、無懼并發的特性以及出色的性能潛力,迅速展示了其作為系統級編程語言的優勢。這份吸引力促使我們思考:能否將 Rust 的安全與性能注入我們更廣泛的基礎設施中?作為核心組件的 Nginx 自然成為了探索的焦點。
我們首先聚焦于FFI(外部函數接口)技術,通過它構建Rust與C語言的交互橋梁。借助FFI,我們將核心業務邏輯以Rust實現,并將Rust代碼編譯為符合C-ABI規范的動態鏈接庫。這種設計使得Nginx能夠像調用原生C模塊一樣無縫集成Rust編寫的庫,在保障系統穩定性的同時提升性能。
圖片
采用該方案的限流模塊示例如下:
圖片
鑒于單向調用模式在應用場景上的局限性,如果僅僅支持上面的單向調用流,使用的場景將大打折扣,目前Nginx 中大量的功能以三方模塊的形式呈現,C module的開發難度較高,需要理解的組件概念頗多,團隊嘗試開發了ngx_http_rust_module模塊作為一個探索期的折中方案。
圖片
ngx_http_rust_module本質上是一個Rust SDK,是對傳統C模塊開發模式的一種現代化補充嘗試。SDK層封裝好的膠水function極大便利了rust module上層開發,可以實現純Rust編碼來實現業務功能,實踐驗證具備較高工程價值。
目前已封裝的部分SDK展示以及設置響應header方法示例:
圖片
圖片
三、全面擁抱Rust進入DLB2.0階段
完成Nginx模塊的初步探索后,團隊技術路線轉向Cloudflare開源的Pingora框架,該高性能Rust框架專為構建可編程、高可靠的流量調度平臺而設計。
核心優勢
- 云原生架構 :通過異步任務調度消除Nginx的進程隔離瓶頸,實現CPU負載均衡與高效連接復用。
- 性能突破 :實測每秒可處理10萬請求,資源消耗降至傳統方案的三分之一。
- 協議生態 :原生支持HTTP/1-2、Websocket端到端代理。
- 安全演進 :基于Rust內存安全特性,集成FIPS認證的加密模塊解決C/C++方案的安全隱患。
- 擴展能力 :提供可編程負載均衡API與熱升級機制,滿足超大規模流量調度需求。
在原型驗證其技術可行性之后,團隊決定在該框架骨干上構建了DLB 2.0產品體系:
圖片
核心能力設計
- 聲明式配置管理
提供基于YAML的聲明式配置接口,顯著提升配置可讀性與維護效率。
支持熱加載機制,實現流量無損的配置更新,徹底規避傳統代理重載導致的503服務中斷。
- 流量處理
支持單一端口多域名TLS證書托管能力,簡化HTTPS服務部署。
提供與Nginx完全兼容的server/path路由匹配邏輯,確保無縫遷移。
實現路徑重寫引擎,滿足復雜流量調度需求。
采用模塊化Filter鏈設計,支持按需插拔流量處理組件。
- 服務發現
集成靜態資源配置與動態DNS服務發現雙模式。
支持sylas注冊中心。
企業級監控。
提供增強型訪問日志。
輸出完全兼容DLB 1.0的監控指標(VTS格式)。
保留流量錄制數據規范,確保監控體系平滑升級。
每個模塊的設計均遵循"高內聚低耦合"原則,在保障生產環境穩定性的前提下,為超大規模流量調度場景提供可擴展的技術支撐,后續將逐一拆解部分關鍵模塊的技術實現細節與性能優化策略。
配置體系
靜態配置
DLB 2.0在配置層面按類型拆分成多個細粒度的yaml文件,其中最核心的是 server.yaml 以及 upstream.yaml ,為了對標Nginx核心概念、這部分不引入新的術語,繼續沿用 server 、 location 、 upstream 三大基礎模塊。
- 通過 server 模塊聲明虛擬主機,支持多域名監聽及端口綁定,兼容 server_name 的泛域名解析能力,同時實現單端口多域名 TLS 證書的精準匹配。
- location 模塊完整繼承 Nginx 的路徑匹配邏輯(含精確匹配 = 、正則匹配 ~ 等模式),支持基于路徑的請求路由與正則表達式重寫規則,確保策略遷移的零成本適配。同時支持 proxy_pass 、 if 、 proxy_headers 、 return 等核心指令。
- upstream 動態服務發現機制支持權重負載均衡,通過 YAML 結構化配置實現后端集群的聲明式管理,并與DNS的服務發現深度集成,徹底消除傳統配置中硬編碼 IP 的維護負擔。
- id: "hjob.shizhuang-inc.com"
server_name: "hjob.shizhuang-inc.com"
service_in:
- "default_80"
- "default_443"
redirect: true
location:
- path: "/"
access_rule_names:
- "access_allow_d803a06f39ad4dcd8dfe517359a33a61"
- "access_deny_all"
client_max_body_size: "100M"
proxy_headers:
- "clientport:$remote_port"
- "Upgrade:$http_upgrade"
- "Connection:$http_connection"
- "Host:$host"
- "X-Forwarded-For:$proxy_add_x_forwarded_for"
- "X-Forwarded-Proto:$scheme"
proxy_pass: "http://hangzhou-csprd-hjob-8899"
- name: "hangzhou-csprd-hjob-8899"
peers:
- server: "1.1.1.1:8899"
weight: 100
backup: false
down: false
- server: "2.2.2.2:8899"
weight: 1
backup: false
down: false
max_fails: 3
fail_timeout: "10s"
max_connections: 1000配置解析
在DLB 2.0的配置模型中, server 、 location 、 upstream 三者構成層次化路由架構:
圖片
- server 作為虛擬服務單元,通過 Vec<LocationConf> 聚合任意數量的 location 路由規則。
- location 作為請求路徑處理器,可獨立關聯至不同的 upstream 服務組。
- upstream 采用原子引用計數機制( Arc )封裝配置,通過Arc::strong_count() 實時監控引用狀態,避免冗余配置拷貝,基于Rust的并發安全特性,最終設計為 Arc<Mutex<UpstreamConf>> 結構:
Mutex 保障多線程環境下的內部可變性,支撐配置熱更新需求。
Arc 維持跨線程的只讀共享能力,確保訪問高效性。
main thread解析完server.yaml與upstream.yaml后,將生成兩個核心哈希映射:
- server 配置映射表:關聯域名與路由規則集。
- upstream 線程安全容器:托管負載均衡服務組狀態。
/// A map of server names to their respective configurations.
#[serde(skip)]
pub servers: HashMap<String, Arc<Mutex<ServerConf>>>,
/// A map of upstream names to their respective configurations.
#[serde(skip)]
pub upstreams: HashMap<String, Arc<Mutex<UpstreamConf>>>,運行時配置轉化
上述的 ServerConf 與 UpstreamConf 面向的是用戶,特點是易于理解與維護、支持YAML反序列化。
而為了專注運行時效率(比如負載均衡策略中的字符串轉化為枚舉類型),我們會將 UpstreamConf 轉化為 RunTimeUpStream 結構, ServerConf 同理。
impl TryFrom<&UpstreamConf> for RunTimeUpStream {
type Error = Error;
fn try_from(value: &UpstreamConf) -> std::result::Result<Self, Self::Error> {
}
}轉化之后得到全局唯一的 GlobalConf :
pub static GLOBAL_CONF: Lazy<RwLock<GlobalConf>> = Lazy::new(|| {
RwLock::new(GlobalConf {
main_conf: MainConf::default(),
runtime_upstreams: HashMap::with_capacity(16),
runtime_servers: HashMap::with_capacity(16),
host_selectors: HashMap::with_capacity(16),
})
});
#[derive(Default)]
pub struct GlobalConf {
// main static configuration
pub main_conf: MainConf,
//one-to-one between upstreams and runtime_upstreams
pub runtime_upstreams: HashMap<String, Arc<RunTimeUpStream>>,
//one-to-one between servers and runtime_servers;
pub runtime_servers: HashMap<String, Arc<RunTimeServer>>,
//one service one host selector
pub host_selectors: HashMap<String, Arc<HostSelector>>,
}流量處理
域名匹配
如果僅有上面的 runtime_servers 這一個哈希表,還不能實現復雜的Nginx域名匹配規則,Nginx域名匹配的優先級機制包括:精確匹配>前置通配符>正則匹配(后置通配符在1.0版本未使用,暫且忽略),為了確保無縫遷移,需要提供與Nginx完全兼容的server匹配邏輯,考慮到代碼可維護性,可以這樣組織運行時數據:
- 為精確域名使用HashMap,實現O(1)查找。
- 前置通配符匹配存儲為Vec,且確保最長匹配優先。
- 正則表達式只能順序匹配,保持Vec<Regex>原順序。
最終得到這樣的結構體:
/// A struct to manage server selection based on host names.
///
/// This struct contains three fields: `equal`, `prefix`, and `regex`.
/// The `equal` field is a HashMap that stores server names and their corresponding IDs
/// when the server name exactly matches the host.
/// The `prefix` field is a Vec of tuples, where each tuple contains a prefix and its corresponding server ID.
/// The `regex` field is a Vec of tuples, where each tuple contains a Regex and its corresponding server ID.
///
/// The `HostSelector` struct provides methods to insert server names and IDs,
/// and to match a given host name with a server ID based on the rules defined in the struct.
#[derive(Clone)]
pub struct HostSelector {
pub equal: HashMap<String, String>,
pub prefixes: Vec<(String, String)>, //原始前通配符數據
pub prefix_nested_map: NestedHashMap, // 嵌套哈希結構優化匹配效率
pub regex: Vec<(Regex, String)>,
}其中需要留意的是成員 prefix_nested_map ,為了確保最長匹配優先,我們將 prefixes: Vec<(String, String)> 轉化為了 NestedHashMap 結構, NestedHashMap 為一個嵌套哈希結構,可基于域名分段實現高效檢索。
#[derive(Debug, Clone)]
pub struct NestedHashMap {
data: HashMap<String, NestedHashMap>, //層級域名節點
value: Option<String>, // 終端節點關聯服務器ID
}
impl NestedHashMap
{
/// 基于域名分段實現高效檢索(從右向左匹配)
pub(crate) fn find(&self, key: &str) -> Option<String> {
let tokens = key.split('.').collect::<Vec<&str>>();
let mut current_map = self;
let mut result = None;
// 遍歷域名層級(如 www.example.com → [com, example, www])
for token in tokens.iter().rev() {
// 優先記錄當前層級的有效值(實現最長匹配)
if current_map.value.is_some() {
result = Some(current_map.value.as_ref().unwrap());
}
// 向下一級域名跳轉
let child = current_map.data.get(*token);
match child{
Some(child) => {
current_map = child;
}
None => {
break;
}
}
}
result.map(|value| value.to_owned())
}
}路由匹配
講完了域名匹配,我們再深入路由匹配,在開始之前,我們先回顧一下Nginx的location指令。
Syntax:location [ = | ~ | ~* | ^~ ] uri { ... }
location @name { ... }
Default:—
Context:server, locationlocation 通常在 server{} 塊內定義,也可以嵌套到 location{} 內,雖然這不是一種推薦的配置方式,但它確實是被語法規則支持的, localtion 語法主要有如下幾種形式:
※ 修飾符語義及優先級(依匹配精度降序排列)
- = :精確匹配(Exact Match),URI必須與模式完全一致時生效(最高優先級)。
- ^~ :最佳前綴匹配(Prefix Match),選中最長非正則路徑后終止搜索(優先級次于=)。
- ~ :區分大小寫的正則匹配(Case-Sensitive Regex)。
- ~* :不區分大小寫的正則匹配(Case-Insensitive Regex)。
- @ :內部定位塊(Named Location),僅限 try_files 或 error_page 指令調用,不對外暴露。
Nginx在解析完 location 之后會進行一系列的工作,主要包括:
- 分類: 根據location的修飾符參數標識不同的類型,同時去除name前面的修飾符
- 排序: 對一個server塊內的所有location進行排序,經過排序之后將location分為了3類
通用類型,通用類型的location將建立一棵最長前綴匹配樹
正則類型,順序為配置文件中定義的順序,正則會用pcre庫先進行編譯
內部跳轉類型,順序也為配置文件中定義的順序
- 拆分:將分類的3種類型拆分,分門別類的處理
其中最復雜的是最長前綴匹配樹的構建,假設location規則如下,構造一棵最長前綴匹配樹會經過如下幾個步驟:
圖片
- 把locations queue變化locations list,假設一個location的name是A的話,所有以A前綴開頭的路由節點都會放到A節點的list里(最長前綴匹配)。
圖片
2.按照上述步驟遞歸初始化A節點的所有list節點,最終得到下面的list。
圖片
3.在上述創建的list基礎上,確定中間節點,然后從中間節點把location分成兩部分,然后遞歸創建左右子樹,最后處理list隊列,list隊列創建的節點會加入到父節點的tree中,最終將生成一棵多叉樹。
圖片
現在你應該已經明白了最長前綴匹配樹的構建流程,讓我們回到2.0的設計上來,這部分同樣維護了三個結構分別對應精確匹配、正則匹配以及最長前綴匹配。
#[derive(Clone, Default)]
#[allow(unused)]
/// A struct representing a shared runtime server configuration.
pub struct RunTimeServer {
/// Unique identifier for the server.
pub id: String,
/// Name of the server.
pub server_name: String,
/// Indicates whether the server should redirect requests.
pub redirect: bool,
/// A HashMap storing equal-matched locations, where the key is the path and the value is the location.
pub equal_match: HashMap<String, Arc<RunTimeLocation>>,// 精確匹配字典
/// A Vec storing regex-matched locations, where each tuple contains a Regex and the location.// 正則匹配隊列
pub regex_match: Vec<(Regex, Arc<RunTimeLocation>)>,
/// The root node of the static location tree.
pub prefix_root: Option<Arc<static_location_tree::TreeNode>>,
}精確匹配、正則匹配比較簡單,我們重點介紹最長前綴匹配,最長前綴匹配樹的構建基本上是把Nginx代碼原原本本的翻譯過來,通過 create_list() 分組節點、 create_tree() 生成多叉樹。通過 find_location 遍歷樹結構查找最長有效路徑,其中路徑比較函數 path_cmp() 確保按字典序定位子樹,匹配成功時返回( need_stop, location ),其中 need_stop 標志是否中止搜索(模擬 ^~ 行為)。
pub fn find_location(path: &str, node: &Arc<TreeNode>) -> Option<(bool, Arc<RunTimeLocation>)> {
let mut node = Some(node);
let mut uri_len = 0;
let mut search_node = None;
while let Some(current) = node {
let n = std::cmp::min(current.path.len(), path.len() - uri_len);
let node_path = ¤t.path[..n];
let temp_path = &path[uri_len..uri_len + n];
match path_cmp(node_path, temp_path) {
std::cmp::Ordering::Equal => {
uri_len += n;
search_node = Some((current.need_stop, current.val.clone()));
node = current.tree.as_ref();
if uri_len >= path.len() { break; }
}
std::cmp::Ordering::Greater => node = current.left.as_ref(),
std::cmp::Ordering::Less => node = current.right.as_ref(),
}
}
search_node
}路由重寫
路由重寫是實現請求路徑動態轉換的核心能力,在語義層面,我們完全兼容Nginx的配置語義。
regex replacement [flag] ,同時采用預編譯正則引擎,在路由加載期完成規則編譯。
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RewriteFlags {
Break,
Last,
Redirect,
Permanent,
NONE,
}
pub struct RewriteRule {
pub reg_source: String,
pub reg: Regex,
pub target: String,
pub flag: RewriteFlags,
}模塊化Filter鏈
Pingora 引擎已經將請求生命周期劃分了足夠細的各個階段,為了更精細化控制同一phase執行的各個Filter,可通過自定義的 ProxyFilter trait,與 Pingora 引擎的phase關聯起來。
#[async_trait]
pub trait ProxyFilter: Sync + Send {
fn phase(&self) -> ProxyFilterPhase;
fn name(&self) -> ProxyFilterName;
fn order(&self) -> i32;
async fn handle(&self, _session: &mut Session, _ctx: &mut ProxyContext) -> HandleResult {
HandleResult::Continue
}
}ProxyFilter 主要包含四個方法:
- phase : Filter 的執行階段, 生命周期階段錨點,可以根據實際需要進行擴展插入更細粒度的階段進行請求處理。
- name : Filter的名稱。
- order : 在同一個phase內Filter的執行順序。
- handle : Filter 的執行邏輯,若返回的是 HandleResult::Continue ,則表示當前filter執行完成,繼續執行下一個 filter,否則停止filter chain 的執行動作。
#[derive(Debug, PartialEq, Clone, EnumString)]
pub enum HandleResult {
/// 表示當前filter執行完成,繼續執行下一個 filter。
Continue,
/// 表示當前filter操作被中斷,停止filter chain 的執行動作。
Break,
}目前我們已經實現的Filter包括但不限于:
圖片
四、總結
作為《從Rust模塊化探索到DLB 2.0實踐》系列的第一篇,本文介紹了開發DLB 2.0的背景以及詳述了DLB 2.0如何通過聲明式配置管理、分層路由架構及與Nginx完全兼容的匹配邏輯,實現億級流量調度場景下的高可用與零遷移成本。
當前成果驗證了Rust在負載均衡產品中改造中的工程價值:依托線程安全的運行時結構(如 Arc<Mutex<T>> )、高效前綴樹路由( HostSelector )及最長前綴匹配,性能與可維護性均突破傳統方案邊界。
在后續篇章中,我們將繼續深入剖析服務發現、監控與日志等核心模塊,為超大規模云原生架構提供完整的參考實踐。





























