百萬級 qps 網關服務的調度策略實踐

導讀 

introduction

說起百度的 BFE 可能不少人都聽說過,但是其實在百度內部還有一個幾百萬 qps 的通用網關服務:Janus。截止當前,Janus 服務不僅覆蓋了百度內部 FEED、評論、點贊、關注、直播等十多個中臺服務的內網流量,而且爲百度 app、知道、經驗、passport、百科、問一問等業務提供了外網流量服務。

爲什麼要建設 Janus

在百度已有 BFE 且 BFE 開源的情況下,爲什麼要建設 Janus 網關?

從場景上看,與 BFE 面向通用功能的流量網關場景不同,Janus 不僅可以作爲流量網關,也可以作爲業務網關、混合網關;不僅面向通用功能,也支持個性化需求。從實現上看,Janus 的部分技術參考了 BFE,但是與廠內 BFE 主要提供 saas 類的服務不同,Janus 提供的是一個通用技術方案,誰使用誰部署,誰有個性化需求誰自己定製插件。因此,Janus 網關的應用場景相對更廣一些,當前的使用場景主要包含流量網關、業務網關、混合網關三種模式,流量拓撲如下:

部署拓撲:

核心問題

從流量調度規則爲例,大部分的使用方的轉發規則都相對比較簡單,但是部分業務的轉發規則來自於原來的 nginx 配置,相對比較複雜,更有些使用方會有偏業務的邏輯在裏面,例如:

  1. 從某時刻後,將 API1 的 A 機房和 B 機房的流量切 30% 到 C 機房;

  2. 將某個 APP 的某個版本之上的 android 流量切到新的路由規則;

  3. cookie 有某些特徵或者 query 中有某些特徵的流量轉發到預覽環境。

那麼在調度階段如何更好地解決如下兩個問題呢:

  1. 如何讓簡單的路由規則配置起來特別簡單,性能較高?

  2. 如何實現複雜甚至業務個性化的調度策略實現?

把問題放大到網關的全局應用場景來看,如何既能通用,寫個插件大家都能用;又能支持個性化,儘量能通過通用插件滿足業務特例的問題;還能靈活,流量網關、業務網關都能勝任。既要、又要、還要的問題通常是使用 tradeoff 的方式加以平衡解決,但是 Janus 的解決方案同時滿足了上面的三個需求:通過插件機制滿足通用化需求 + 通過可動態下發編程能力的方式進行差異化配置 + 通過 SDK 集成到業務部署的方式支持靈活使用。

流量調度方案設計

3.1 方案思路概述

爲了將服務的轉發規則更加清晰,Janus 將路由分爲了三級(與 nginx 類似):

由上面的挑戰分析可知:

  1. 對於大多數的簡單路由規則需要相對簡單,性能相對高,通過域名匹配 + 樹路由實現的 url 匹配即可;

  2. 對於少量的複雜路由規則需要擴展性足夠強,可以在特徵匹配階段引入一個極簡的腳本語言來實現。

3.2 基礎路由規則支持

通過樹路由支持的部分規則如下:

**3.3 **進階路由規則支持

上述的簡單路由規則可以滿足 90%+ 的業務需求,但是對於類似 從某時刻後,將 API1 的 A 機房和 B 機房的流量切 30% 到 C 機房 這種需求是滿足不了的。因此,在特徵匹配階段可以通過 變量表達式+條件表達式 進行精細化匹配。

變量表達式

爲了能根據系統裏面的常見特徵進行精細化匹配,首先我們要對系統裏面的常見特徵進行描述。例如:

  1. 通過 ${idc} 表示當前所屬的機房

  2. 通過 ${time} 表示當前時間

  3. 通過 ${query} 表示 get 參數

  4. 通過 ${header} 表示 header 裏面的數值

但是當特徵越來越多的時候,就會略顯臃腫,存在的特徵變量越來越多,這時候 Janus 引入了分級的概念,比如:

如圖所示,就可以用 ${request.query.id} 來表示本次請求中 key 爲 id 的 query 值。並且如上的特徵變量是可以擴充的,每個使用方可以根據自己的系統差異、環境差異定義自己的特徵變量體系。

條件表達式

有了上面實現的變量表達式,我們就可以用 $ 描述我們需要的特徵變量了,但是如何對這些特徵變量進行操作呢?

Janus 的方案是定義一門極簡的語言(無論是用 yacc 等一類的生成語法分析的工具,還是自己做詞法分析、語法分析,實現都比較簡單,這裏不再贅述實現細節),只支持邏輯運算 + 函數調用,部分例子如下:

函數調用:

邏輯運算:

Janus 在有變量表達式來表示系統特徵的基礎上,添加了條件表達式來對系統特徵進行操作、判斷。由於可以不斷擴充變量表達式和條件表達式,因此 Janus 幾乎可以滿足用戶的任意需求。

性能對比

通過如上方案介紹可以看出,採用從控制面下發表達式的方式,可以滿足絕大部分場景的需求,但是,對性能影響如何呢? 

在數據面接收到控制面下發轉發規則時,首先會對變量表達式和條件表達式進行編譯,映射成 go 的代碼,在後續運行時,與直接調用原生的 go 語言差異並不大。對比數據如下:

條件表達式:

"random(0,100) || random(100,100)"

對應的 benchmark 數據:

goos: windows
goarch: amd64
cpu: 11th Gen Intel(R) Core(TM) i5-1145G7 @ 2.60GHz
BenchmarkRandom-8       35817918            34.52 ns/op        0 B/op          0 allocs/op

原生 go 代碼:

(0> rand.Intn(100)) || (100 > rand.Intn(100))

對應的 benchmark 數據:

goos: windows
goarch: amd64
cpu: 11th Gen Intel(R) Core(TM) i5-1145G7 @ 2.60GHz
BenchmarkRawRandom-8     39136900          31.63 ns/op         0 B/op         0 allocs/op

可以看到使用表達式與使用原生 go 代碼在性能上相差不到 10%,區別並不是特別大。 

方案泛化

通過上面的變量表達式 + 條件表達式的方式,很好地解決了流量調度問題。實際上,該方案可以作爲一個通用解決方案解決很多類似問題。以 Janus 網關爲例,在很多地方都大量存在這個變量表達式和條件表達式。

4.1 插件的運行條件

以容災插件爲例,用戶可以把容災插件配置在任意路由規則上,但是大家認定的觸發容災的規則可能不一樣,比如:

  1. 有些業務認爲:只有後端的 http 協議返回 5xx 才需要容災

  2. 有些業務認爲:後端的 http 協議返回 5xx 或者 返回值的 json 裏面 errno != 0 需要容災

  3. 更有些業務認爲:後端的 http 協議返回 5xx 或者 header 裏面的 sla_status=0 需要容災

一方面,我們想做一個通用的容災插件,另一方面,大家的觸發規則的標準又千奇百怪、各不相同。怎麼解決這個矛盾呢?

Janus 的答案是:把控制權交給用戶,用戶配置容災插件的時候同時配置一個條件表達式,只有條件表達式返回 true,纔會運行容災邏輯。

上面的問題對應的下發配置如下:

  1. num_gt(${response.code}, 499)

  2. num_gt(${response.code}, 499) || (!str_equal(${response.jsonbody.errno}, 0))

  3. num_gt(${response.code}, 499) || (!str_equal(${response.header.sla_status}, 0))

這樣就做到了既是一個通用容災插件,又可以做到個性化的觸發邏輯。

**4.2 **通用緩存插件的設計

當我們想做一個通用的 redis 緩存插件時,存取邏輯比較簡單:

// 請求下游前
if data, ok := redis.Get(key); ok {
    return data
}
// 請求下游
data := reqeust(xxx)
// 請求下游後
redis.Set(key, data)

但是,與上面的插件面臨的問題類似,通用緩存插件的 key 怎麼定義呢? 

  1. 評論接口只要 id 一樣就認爲是同一個請求

  2. 我的粉絲接口不僅需要 id 一樣,還需要 uk 一樣纔是同一個請求

  3. 主頁接口需要 uk 一樣才認爲是同一個請求

解決思路是用變量表達式來把 key 的定義交給用戶,用戶配置緩存插件的時候同時配置 key 的規則,比如:

  1. comment_${request.query.id}

  2. fans_${request.query.id}_${request.query.uk}

  3. homepage_${request.query.uk}

這樣就解決了通用緩存插件中的通用與個性化之間的矛盾。 

展望

在 Janus 網關服務中,通過常規路由規則 + 變量表達式 + 條件表達式的方式實現了各種流量調度策略,並將方案泛化到了各種其他功能的實現上,支撐了幾百萬 QPS 的流量及衆多使用方的接入。通過已經實現的系統變量及規則的組合,基本可以實現任意功能,但是當需要新的規則時,則需要在 Janus 中上線新的條件表達式實現。爲了進一步強化 Janus 中的動態配置表現能力,Janus 正在進行表示式與 Go 官方標準庫的無縫打通。這樣就可以在控制面進行更加靈活的配置下發動態編程能力,滿足更廣泛的需求。 

作者:加納斯

來源:百度 Geek 說

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ytOxtlHxdUiIs_33e0nTxQ