背景簡述
Oceanus是美團基礎架構部研發的統一HTTP服務治理框架,基於Nginx和ngx_lua擴充套件,主要提供服務註冊與發現、動態負載均衡、視覺化管理、定製化路由、安全反扒、session ID複用、熔斷降級、一鍵截流和效能統計等功能。本文主要講述Oceanus如何通過策略抽象、查詢、渲染和分組動態更新,實現HTTP請求的定製化路由。 隨著公司業務的高速發展,路由場景也越來越複雜。比如:
- 團購秒殺要靈活控制壓測流量,實現線上服務單節點、各機房、各地域等多維度的壓測。
- 外賣業務要做流量隔離,把北方地域的流量轉發到分組a,南方地域的流量轉發到分組b。
- 酒旅業務要對App新版本進行灰度,讓千分之一的使用者試用新版本,其他使用者訪問老版本。
- QA部門要通過請求的自定義引數指定轉發分組,構建穩定且高可用的測試環境。
由於公司早期的業務場景相對比較簡單,所以均通過Nginx if指令支援。比如某業務要把來源IP為10.4.242.16的請求轉發到後端節點10.4.232.110,其它請求轉發到後端節點10.4.232.111和10.4.232.112,就可以進行如下配置:
upstream backend_aaa {
server 10.4.232.110:8080 weight=10;
}
upstream backend_bbb {
server 10.4.232.111:8080 weight=10;
server 10.4.232.112:8080 weight=10;
}
location /abc {
if($remote_ip = "10.4.242.16") {
proxy_pass http://backend_aaa; #路由到backend_aaa叢集
}
proxy_pass http://backend_bbb; #路由到backend_bbb叢集
}
複製程式碼
上述方式雖然不需要額外開發,效能方面也接近原生的Nginx框架,但是使用場景比較受限,因為if指令僅支援比較簡單的condition型別,官方描述如下:
如果該業務要把IP段10.4.242.16/34的請求轉發到10.4.232.110時,if指令勉強還可以支援。但對於上述的複雜業務場景,if指令均無法支援。除此之外,這種方式還存在以下兩點不足:
- 規則調整不支援動態化:如果要把客戶端10.4.242.16調整為10.4.242.17,需要對Nginx進行reload,而reload操作會使Nginx的併發能力下降,業務高峰時甚至會導致請求504或502。
- 指令坑太多:if指令和set、rewrite指令等一起使用時,很多時候會出現不符合預期的行為,嚴重時甚至會導致段錯誤,最好的方法就是避免使用。
為了解決上述問題,Oceanus開始探索如何實現HTTP流量的定製化路由。
業界調研
通過初步調研,發現業界有一套開源的ABTestingGateway(以下簡稱AB)框架:
由上圖所示,AB框架使用Redis儲存策略資料,key是Host欄位,value是策略物件,包括策略型別、匹配區間和要分發的Upstream。策略的增刪改查可以通過基於Nginx搭建的Web服務的API實現,執行時根據請求的Host欄位從lua-shared-dict或Redis獲取關聯的策略,根據策略型別(iprange/uidrange/uidsuffix/uidappoint)選擇對應的Lua指令碼從請求中獲取相關引數(IP、UID)查詢是否匹配策略,若匹配,就修改請求的Upstream上下文完成分流的目的。 相比if指令的方式,AB框架有下面兩個優點:- 策略調整動態生效:已有策略型別中的策略變更均可以通過HTTP API進行動態管理。
- 分流策略豐富:支援IP段、UID段等策略,也可以通過新增策略型別對策略庫進行擴充套件。
由於AB框架只支援4種策略型別,對於業務要根據請求Cookie、自定義header控制轉發的情況,均需要開發新的策略型別和釋出上線。另外,策略型別和業務場景緊密相關,導致AB系統的擴充套件性極差,很難快速支援新業務的路由需求。
無論是Nginx if指令,還是AB框架,要麼需要reload重新載入才能生效,要麼無法支援某些業務場景下的分流需求,所以都很難作為解決公司級分流框架的有效手段。針對它們所存在的不足,Oceanus開發了一套應用級、高可擴充套件的動態分流框架,不僅動態支援各種業務場景的分流需求,而且保證了請求轉發的效能,下文將闡述我們如何解決分流機制的幾個核心問題。
Oceanus定製化路由的核心設計&實現
關於分流機制,我們主要從以下四個方面來講述:
- 策略抽象:合理定義策略結構,適用儘可能多的業務場景。
- 策略的高效查詢:介面粒度關聯,應用維度管理。
- 執行時策略渲染:渲染策略模板,判斷是否匹配策略,實現動態路由。
- 分組動態更新:分組資料增刪改,均不需要reload。
策略的結構定義
以AB框架為例,只支援iprange、uidrange、uidsuffix、uidappoint四種場景,對策略型別和匹配方式太具體化,導致無法支援更多普適性的業務場景。從分流的本質出發,即根據請求特徵完成流量的定製化路由。結合Nginx if指令的幾個組成部分:條件判斷依賴的變數、條件判斷要匹配的value、條件表示式、匹配後要執行的proxy_pass,一個策略必須要包含請求特徵描述、定製化路由描述以及兩者的關係描述。其中請求特徵描述包含特徵關鍵字、關鍵字的上下文傳輸方式,定製化路由描述通過Upstream表示,Upstream可以預先設定,也可以動態指定,兩者的關係通過泛型表示式表示。那麼一個策略就需要包含下面幾個屬性:
- name:策略名,沒有實際意義,可以根據業務場景進行定義。
- key:分流時依賴的關鍵字,比如要根據城市地域進行分發路由時,key就是regionid。
- passway:關鍵字在HTTP協議中的傳輸方式,可以是Parameter、Cookie、header、body中的一種。
- condition:表示式模板,支援四則運算/取模、關係運算子、邏輯運算子等。
- group:後端服務叢集,即匹配策略後,轉發請求的目標節點,一般是策略所屬應用叢集中的部分節點。
- category:策略型別,如果為1,表示某個服務的私有策略;如果為2,表示公共策略,主要用於策略資料管理。
- switch:策略開關,用於控制當前策略是線上還是離線。
- graylist:灰度列表,用於策略變更的線上灰度校驗。
其中switch、graylist欄位主要用於策略的上下線操作,這裡不做過多討論。下面重點介紹上面的策略定義是如何表述業務場景的:
備註:應用apk1和apk2分別配置2個私有策略,apk3使用公共策略。 如上圖所示,無論業務根據請求的哪些特徵進行分流,策略結構均可以支援。 以私有策略gray-deploy為例,在Oceanus管理平臺進行新增,如下圖所示: 備註:這裡省略了策略的非核心欄位比如switch、graylist等。如何實現策略的高效查詢?
策略拓撲關係
分流策略分為私有策略和公共策略。私有策略是面向服務的,而且和該服務建立的分組緊密相關。不同服務的私有策略完全獨立,可以相同,也可以不同。一個服務可以配置多個私有策略,也可以關聯多個Host的Location,Location之間的策略使用完全獨立,一個Location可以啟用該服務的一個或者多個私有策略。如果通過Host+location_path直接關聯策略資料,不同Location關聯同一個私有策略時,會存在大量的資料冗餘。所以我們通過服務標識(appkey,唯一標識一個應用服務)關聯具體的策略資料,Host+location_path只關聯當前Location使用的策略名列表,策略之間支援指定順序。 公共策略與具體服務無關,策略名全域性唯一,可以使用策略名關聯策略資料即可。綜上,策略的拓撲關係描述如下:
如上圖所示,以應用apk1為例,關聯了兩個Location介面,分別為/api和/list,總共部署了8個節點,建立了2個分組ups-cq和ups-gray,其中節點10.5.23.6和10.5.24.72屬於分組ups-cq,節點10.7.46.32和10.7.72.232屬於分組ups-gray。應用配置了兩個私有策略stress-testing和gray-deploy,其中策略stress-testing被介面/api啟用,匹配策略的流量路由到分組ups-cq,策略gray-deploy被介面/list啟用,匹配策略的流量路由到ups-gray。執行時獲取Location path
Nginx在解析Location配置時,通過不同的欄位區分不同型別的Location,沒有記錄配置中的Location path。如果要執行時獲取,一般有兩種方式:一種是根據相關欄位逆向還原path,另一種是為框架新增變數。由於Nginx在處理正則Location時,對於是否忽略大小寫的情況,並沒有做標記,即解析的過程是不可逆的,所以我們選擇了第二種方式。在核心模組的變數陣列ngx_http_core_variables中新增了內建變數,記錄下原始的Location path,變數屬性定義如下:
{ngx_string("loc_mod"), NULL, ngx_http_variable_loc_mod,
0, NGX_HTTP_VAR_NOCACHEABLE, 0},
{ngx_string("loc_name"), NULL, ngx_http_variable_loc_name,
0, NGX_HTTP_VAR_NOCACHEABLE, 0}
複製程式碼
loc_mod和loc_name之間用一個空格符連線,格式和Oceanus管理平臺保持一致。
非同步更新機制
為了保證執行時獲取策略資料的高效性,我們通過非同步定時拉取,把策略資料全量同步到本地的共享記憶體中。基於穩定性和靈活性的考慮,我們採用了關係型資料庫MySQL儲存策略。 更新機制如下圖所示:
- Oceanus在init_worker階段隨機選擇某個worker程式,嵌入timer。
- 被選中的worker會非同步非阻塞地從MySQL定時拉取策略資料。
- timer worker把拉取到的策略資料解析,按照策略的拓撲關係,更新到當前共享記憶體中的寫快取區,完成更新後,切換讀寫快取區,保證最新的策略立即生效。
- worker程式在處理請求時,從當前共享記憶體中的讀快取區獲取策略資料。
為了解決timer worker和其它worker在讀寫策略資料時的競態關係,我們採用了雙buffer機制,實現了業務層策略資料的無鎖讀寫。另外,通過設定timer的時間為0,保證在所有worker處理請求前,策略資料已經在共享記憶體中完成初始化。
策略查詢機制
查詢演算法如下圖所示:
- worker程式從request上下文中獲取請求的Host,以及所匹配Location的location_path。
- 根據Host+location_path,到共享記憶體中查詢所開啟的策略名。
- 如果是公共策略,直接根據策略名去查詢策略資料。
- 如果是私有策略,從request上下文獲取Location關聯的Upstream,即應用標識appkey,到共享記憶體讀快取區獲取具體的策略資料。
備註:公共策略以"oceanus"開頭,區別於私有策略的命名。
執行時策略渲染
查詢到請求開啟的策略後,Oceanus需要執行時判斷是否匹配,以私有策略為例,執行流如下圖所示:
- 在rewrite phase,Oceanus通過rewrite_by_lua_file嵌入回撥,觸發請求處理,進入分流框架的主流程。
- 通過上面的策略查詢機制獲取請求的策略,進行解析,獲取策略的key和passway。
- 根據passway從請求對應的上下文獲取key的value。
- 用3獲取到的value渲染策略的condition,把condition中的佔位符替換為value。
- 基於Lua VM,通過load計算condition的結果,即true或false。
- 從策略中獲取condition的value和group資料。
- 如果condition為true,就用group覆蓋請求的Upstream上下文,否則,不做處理。
分組動態更新
分組列表的動態化是分流框架的重要一環。更新機制如下圖所示:
- 分組資料使用ZooKeeper儲存,變更通過watcher機制實現增量同步。
- Oceanus也會定時拉取,進行全量同步。
- Oceanus把所有變更都通過本地的HTTP呼叫同步到Nginx記憶體。
- worker處理變更請求前,會先搶鎖,讀取共享記憶體中的訊息佇列,同步其它worker進行的歷史變更。
- 把這次變更同步到當前worker的Upstream main上下文中,完成當前worker的更新。
- 把變更封裝成訊息,加入到共享記憶體中的佇列。
- 其它worker通過timer或者自己處理變更訊息前讀取訊息佇列,完成更新。
總結與展望
通過Oceanus分流機制在美團外賣、酒旅、到店餐飲等多個業務線的廣泛使用,基礎架構部幫助業務同胞解決了多個定製化路由的需求,比如服務set化、鏈路壓測、灰度釋出、泳道環境建設等等。目前,Oceanus分流機制只關注了流量轉發方向,還不支援更復雜的轉發動作,比如根據策略調整請求的Parameter、header、Cookie,也不支援根據請求的URL實現動態路由等,未來我們還將逐一完善這些問題,當然也歡迎大家跟我們一起交流,共同進步。
作者簡介
周峰,美團高階工程師,2015年7月加入美團基礎架構部,先後負責統一金鑰管理服務、智慧反爬服務和HTTP負載均衡,目前主要負責HTTP服務治理Oceanus的相關工作,致力於探索和研究服務的自動化、智慧化、和高效能等方向。
招聘廣告:如果你對大規模分散式環境下的HTTP服務治理、分散式會話鏈路追蹤等系統感興趣,誠摯歡迎投遞簡歷至:zhangzhitong#meituan.com。
參考文獻
- ngx_http_rewrite_module:nginx.org/en/docs/htt…
- AB框架:github.com/CNSRE/ABTes…