2019 年 7 月 6 日,OpenResty 社區(qū)聯(lián)合又拍云,舉辦 OpenResty × Open Talk
全國巡回沙龍·上海站,美團基礎(chǔ)架構(gòu)部技術(shù)專家張志桐在活動上做了《美團 HTTP 服務(wù)治理實踐》的分享。
OpenResty x Open Talk 全國巡回沙龍是由 OpenResty 社區(qū)、又拍云發(fā)起,邀請業(yè)內(nèi)資深的 OpenResty 技術(shù)專家,分享
OpenResty 實戰(zhàn)經(jīng)驗,增進 OpenResty 使用者的交流與學(xué)習(xí),推動 OpenResty
開源項目的發(fā)展。活動將陸續(xù)在深圳、北京、武漢、上海、成都、廣州、杭州等城市巡回舉辦。
首先做下自我介紹,我叫張志桐,畢業(yè)于哈爾濱工業(yè)大學(xué),2015 年加入美團,目前在美團主要負責(zé) Oceanus 七層負載均衡網(wǎng)關(guān)、Mtrace
分布式鏈路跟蹤系統(tǒng)以及 KMS 密鑰管理服務(wù)等。
美團是 Nginx 的老用戶,從創(chuàng)業(yè)初期就使用 Nginx,直到 2013 年遷到了阿里的 Tengine,再到今年三四月份,全站服務(wù)遷到了
OpenResty 上。從 Tengine 遷到 OpenResty 最根本的原因是升級困難,隨著 Nginx 的版本迭代越來越快,導(dǎo)致 Tengine
很難合到官方 Nginx 最新版本上,但是使用 OpenResty 可以平滑地升級整個 Nginx 的社區(qū)版本。
Oceanus 美團七層負載均衡網(wǎng)關(guān)
Oceanus,單詞的含義是海神。它是整個美團接入層的七層負載均衡網(wǎng)關(guān),每天有千億級別的調(diào)用量,部署了幾千個服務(wù)站點,近萬個注冊應(yīng)用服務(wù)。Oceanus
最核心的功能是提供 HTTP 服務(wù)治理功能,主要包括服務(wù)的注冊與發(fā)現(xiàn),健康檢查,以及完全的可視化管理,同時也提供了像 Session 復(fù)用、動態(tài)
HTTPS、監(jiān)控、日志、WAF、反爬蟲、限流等網(wǎng)關(guān)功能。
這里補充一個限流方面的小問題,目前美團是通過全局 Redis Cluster 來實現(xiàn)的,也簡單的做了一些優(yōu)化,實現(xiàn)了完全基于 OpenResty 的
Redis Cluster,因為官方的 OpenResty 版本只支持單實例的 Redis 調(diào)用。同時我們不是每次請求都會去做 Redis Incr
的操作,每次會設(shè)置一個閾值,設(shè)置越大,本機加的代價就越小,因為不需要遠程調(diào)用了,但出現(xiàn)的誤差也會對應(yīng)增大。基本的思路就是本地加一個步長,定期的把步長同步到
Redis Cluster 上來實現(xiàn)集群限流的功能。
上圖是當(dāng)前 Oceanus 的系統(tǒng)架構(gòu),底層的引擎核心是基于 OpenResty 的。在每個 OpenResty 節(jié)點上會部署了一個 Agent
的進程,主要是為了做邏輯的解耦,我們不希望整個 Nginx 或者是 OpenResty 上有過重的邏輯和請求無關(guān),于是把很多的邏輯都下沉到 Agent
上,實現(xiàn)與 OpenResty 的解耦,比如用 MNS 拉取服務(wù)列表,再通過 Agent 灌入到
OpenResty。站點管理,落地文件配置,統(tǒng)一由前端管理平臺 Tethys 進行管理,之后會實時落地到 mysql 里,Agent 通過 mysql
的同步,再落地到本地到 Server block 文件,通過 reload 方式實現(xiàn)站點的重新加載。右邊是 Oceanus 體系之外的模塊,第一個是
MNS,是公司內(nèi)部統(tǒng)一的命名服務(wù)。另一個 Scanner,主要負責(zé)的是健康檢查。
Nginx 配置反向代理
如上圖配置 Nginx 反向代理會遇到幾個問題:
* 寫死的服務(wù)地址,IP 不能變,每次變更需要改文件。
* 每次變化需要 reload。
* 文件化的配置容易出問題。
我們怎么解決這三個問題?第一個動態(tài)的服務(wù)注冊,第二個是不需要 reload 動態(tài)配置生效,第三個文件化配置變成一個結(jié)構(gòu)化管理。
服務(wù)注冊
服務(wù)注冊目前是基于美團內(nèi)部的 MNS 統(tǒng)一命名服務(wù),上圖是整個服務(wù)注冊的前端界面。它后端還是依托如 ETCD、ZK
服務(wù)注冊的基礎(chǔ)組件,主要用于緩存服務(wù)的信息,實現(xiàn)批量拉取、注冊服務(wù)功能,可以根據(jù) Nginx
集群選擇拉取與這一類集群相關(guān)的所有站點信息,同時通過推拉結(jié)合的方式保證數(shù)據(jù)實時和準(zhǔn)確。并定期的把所有數(shù)據(jù)都拉到本地,依靠 ZK 的 watcher
方式來保證數(shù)據(jù)的實時到達。
健康檢查
Nginx 主動健康檢查有一些開源模塊,但這些主動的健康檢查會遇到一些問題。假設(shè)有一個站點?http://xxx.meituan.com
<https://link.zhihu.com/?target=http%3A//xxx.meituan.com>,配在 upstream 里做健康檢查,每個
proxy 的服務(wù)器的每個 worker 都會定期向后端服務(wù)發(fā)起健康檢查。假如每秒檢查一次,整個 Nginx 集群數(shù)量是 100,每個單機實例上部署了 32
個 worker,健康檢查的請求 QPS 就是 100×32,而實際服務(wù)器每天的 QPS 不到 10,加上健康檢查機制就變成 3000
多了。所以我們摒棄了在內(nèi)部主動去做健康檢查的方式,選擇了 Scanner 去做周期性健康檢查。此外, Scanner
支持自定義心跳,可以檢查端口是否通暢、HTTP 的 url 是否準(zhǔn)確,并且支持快慢線程的隔離。
動態(tài) upstream
美團實現(xiàn)動態(tài) upstream 用的是業(yè)內(nèi)比較成熟的方式:Tengine 提供的 dyups 模塊。它提供一個 dyups API,通過這個 API
添加、刪除、創(chuàng)建服務(wù)節(jié)點,之后通過一個 worker 處理這一次修改請求,把請求放到了一個共享內(nèi)存的隊列中,各個 worker
會從這個隊列把這次變更拉取出來在本地生效,然后落到本地的內(nèi)存中,實現(xiàn)整個步驟。其中,第一次調(diào)用時是需要加鎖,然后同步內(nèi)存中還沒有被消費的數(shù)據(jù),同步完之后才會更新操作,保證了數(shù)據(jù)的串性。
dyups 存在的一些問題:
1.持久化
最大的問題是內(nèi)存生效,因為它走的是本地 worker 進程內(nèi)部的內(nèi)存,所以下一次 reload 時,整個服務(wù)列表會丟失。我們的解決方案是通過本地 Agent
來托管這個節(jié)點的更新和文件落地。當(dāng) Agent 定期感知到服務(wù)列表變化時,首先把本地生成的 upstream 文件更新,之后再去調(diào)用 dyups
API,把這一次變更的節(jié)點實時同步到內(nèi)存中,實現(xiàn)了服務(wù)節(jié)點不僅落地到本地文件做持久化存儲,同時還灌入到了 Nginx worker 內(nèi)存中來保證服務(wù)的實施。
其中需要注意的是 reload 調(diào)用 dyups API 并發(fā)的問題。假如出現(xiàn)一種特殊的場景,Agent 感知到服務(wù)節(jié)點變化時,還沒來得及落地
upstream 文件,這時候 Nginx 出現(xiàn)了一次 reload,更新的還是舊的 upstream 文件。此時 dyups API
調(diào)用過來,通知需要更新服務(wù)節(jié)點,更新服務(wù)節(jié)點之后會把更新的信息放到共享內(nèi)存中,類似于一個接收器,每一個 worker
拿到更新之后才會把消息刪除掉。這里可能出現(xiàn)一個問題,當(dāng) reload 的時候,出現(xiàn)了六個 worker 進程,有可能這一次更新被舊的 worker
進程拿掉了,導(dǎo)致新的 worker 沒有更新,進而導(dǎo)致了新的 worker 里有部分是更新成功,有部分是更新不成功的。
我們目前是把 Nginx 所有的 reload、start、stop 包括一些灌入的節(jié)點都統(tǒng)一交給 Agent 進行處理,保障了 reload 和
dyups API 調(diào)用的串行化。
2.流量傾斜
每臺機器同一時刻更新節(jié)點,初始序列是一樣的,導(dǎo)致流量傾斜。比如線上有 100 個服務(wù)節(jié)點,每 25
個節(jié)點一個機房,當(dāng)灌入節(jié)點時順序是一致的。從最開始選節(jié)點,第一個選的節(jié)點都是一樣的,導(dǎo)致一次請求篩選的節(jié)點都是請求列表里的第一個,所以同一時刻所有的流量都到了同一臺后端機器上。
我們的解決方案是在 Nginx 內(nèi)部加權(quán)輪訓(xùn)時的初始化節(jié)點,做了內(nèi)部的 random,來保證每個 worker
選的第一個節(jié)點都是隨機化的節(jié)點,而不是根據(jù)原來的動態(tài) upstream 加權(quán)輪訓(xùn)的方式保證的穩(wěn)定的序列去選節(jié)點。
Nginx 結(jié)構(gòu)化配置管理
如上圖,創(chuàng)建站點可以直接在 Oceanus 平臺上配置,提交后相當(dāng)于建立了一個 Nginx 的 server 配置。同時支持導(dǎo)入功能,Nginx
server 的配置文件可以實時導(dǎo)入,落到集群的機器上。
匹配規(guī)則
建完站點之后,可以直接配置映射規(guī)則,左側(cè)是的 location,右側(cè)對應(yīng)的 pool 在美團內(nèi)部是
appkey,每個服務(wù)都有一個名字。之后會通過一些校驗規(guī)則來驗證配置的規(guī)則從 location 到 appkey 是否合法,或者是否超出預(yù)期。 當(dāng)
location 配置規(guī)則非常復(fù)雜,中間出現(xiàn)一些正則時,作為一名業(yè)務(wù) RD
在平臺上配置規(guī)則時是很容易出問題,因為你不知道配置的規(guī)則是否正確,是否真的把原來想引流的流量導(dǎo)到了 appkey
上,還是把錯誤地把不該導(dǎo)入這個服務(wù)的請求導(dǎo)到了 appkey 上。因此需要做很多的前置校驗,目前美團內(nèi)部使用的校驗規(guī)則是模擬生成已有路徑下的正則匹配的
url,用于測試哪些流量到了新部署的 appkey上做校驗。這種校驗也是有一定的不足,比如配置了很多正則匹配的方式,我們模擬出來的 url
其實不足以覆蓋所有的正則 url ,會導(dǎo)致校驗不準(zhǔn)確。目前我們的規(guī)劃是獲取到所有的后端服務(wù),比如 Java 的服務(wù),后面會有
Controller,Controller 上有指定業(yè)務(wù)的 url,我們可以針對業(yè)務(wù)的 url 去離線的日志里篩選出來它們歷史上每個路徑下匹配真實的
url,用真實的 url 做一次回放,看是否匹配到了應(yīng)該匹配的服務(wù)上去。
指令配置與流量統(tǒng)計
我們也支持所有的 Nginx 上的指令配置,包括設(shè)置 Header、設(shè)置超時、rewrite、自定義指令等,或者我們封裝好的一些指令。
同時也支持一些服務(wù)的性能統(tǒng)計,比如說 QPS,HTTPS QPS,以及服務(wù)內(nèi)部的 4XX、5XX。
負載均衡方案迭代歷程
精細化分流
精細化分流項目的背景是美團在線上的一些需求,比如在線上希望實現(xiàn)對某一個地域的用戶做灰度的新功能特性更新,或者按百分比引流線上的流量,以及對固定流量的特征,選擇讓它落到固定后端的服務(wù)器上,保證這一部分的用戶和其他的用戶的物理隔離。
舉個例子,上圖右邊是三臺服務(wù)器都是服務(wù) A,把其中兩臺服務(wù)器作為一個分組 group-G,Agent 獲取到這個服務(wù)信息后,會把它實時落地到
upstream 文件里。如果是 group-G ,可以落到Upstream A_GR_G 的 upstream 文件中;如果是 upstream
A,就和普通的服務(wù)一樣落地好,3 個 server 同時落到一個服務(wù)上。此時前端有用戶 ID 的請求進來,需要選擇一種分流的策略,比如希望用戶的 ID 的
mod100 如果等于 1 的請求,路由到灰度的分組 groupG 上,通過這種策略的計算,把 1001 用戶請求路由到 upstream A-GR-G
服務(wù)上,然后剩下的其他的用戶都通過策略的篩選,路由到服務(wù) A 上 。?
精細化分流具體實現(xiàn)的邏輯,首先在一個 worker 進程嵌入 timer,它會定期拉取策略配置,同時 DB 配置結(jié)構(gòu)化寫入共享內(nèi)存的雙
buffer,worker數(shù)據(jù)請時候,會從共享內(nèi)存中讀取策略進行匹配。策略匹配的粒度是
Host+Location+appkey,策略分為公共策略和私有策略,公共策略是整個全網(wǎng)都需要采用的一個策略,私有策略是可以針對自己的服務(wù)做一些定制化。?
當(dāng)請求來臨的時候,獲取請求的上下文,通過 Host+Location 來查找它需要使用的策略集合,如果是匹配公共策略就直接生效,如果是私有策略就會按
appkey 查找策略。以上圖為例,請求來了之后,獲取到請求的上下文,之后通過請求上下文里的 Host+Location
去找相應(yīng)的策略集合,然后可能找到了左下角的策略集合。
分流轉(zhuǎn)發(fā)的過程是在 rewrite 階段觸發(fā)的,請求進入到 rewrite 階段以后會解析策略數(shù)據(jù),實時獲取請求來源中的參數(shù),通過參數(shù)和表達式渲染成表達式串:
if (ngx.var.xxx % 1000 = 1) ups = ups + target_group;
通過執(zhí)行這段命令,看是否命中分流策略,如果命中則改寫路由的 ups 到指定的 ups group,否則不對 upstream 做修改。
泳道
微服務(wù)框架下服務(wù)個數(shù)多、調(diào)用鏈路較長,其中一個服務(wù)出問題會影響到整條鏈路。舉個的例子,QA
提測往往需要該條鏈路上的多個服務(wù)配套測試,甚至是同時測試一個服務(wù)的多個演進版本,測試的科學(xué)性是不完善的,為了解決線下 QA
實現(xiàn)穩(wěn)定的并發(fā)測試,我們提出了泳道的概念。
如上圖,有兩個 QA。第一個 QA 可以建立屬于自己的泳道 1,第二個 QA 可以建立屬于自己的泳道 2。QA 1 測試的功能在 B、C、D
服務(wù)上,它只需要建立一個有關(guān)于這次測試特性的 B、C、D 的服務(wù),就可以復(fù)用原來的骨干鏈路。比如骨干鏈路的請求通過泳道的域名進來,首先會路由到骨干鏈的 A
服務(wù)上,之后他會直接把這次請求轉(zhuǎn)發(fā)給泳道 1 上的 B、C、D 服務(wù),之后 D 服務(wù)因為沒有部署和他不相干的服務(wù),所以它又會回到骨干鏈路的 E 服務(wù)和 F
服務(wù)。
QA2 測試的功能主要是集中在 A 和 B 服務(wù),它只需要單獨部署一個 A 和 B 服務(wù)相關(guān)于本次測試特性服務(wù)就可以了。當(dāng)請求進來,在泳道 2 上 A、B
服務(wù)流經(jīng)結(jié)束,就會回到主干鏈路 C、D、E 和 F
服務(wù)上,從而實現(xiàn)并發(fā)測試的效果,同時保證了骨干鏈路的穩(wěn)定,因為這個過程中骨干鏈路是一直沒有動的,唯一動的是要測試的那部分的內(nèi)容。
同時多泳道并存可以保證多服務(wù)和多版本的并行測試,并做錯誤的隔離,極大的提高了的服務(wù)上線的流程。
泳道的實現(xiàn)基于精細化分流就很簡單了。例如給服務(wù) A 一個標(biāo)簽,它屬于泳道 S,用同樣的原理可以把它落地成 upstream A-SL-S,同時把泳道 IP
放到 upstream 里面,此時 A 服務(wù)上里沒有泳道的機器。美團內(nèi)部一般使用通過服務(wù)鏡像的方式做服務(wù)的測試,通過 Docker
直接創(chuàng)建泳道的鏈路,自動化生成一個泳道的域名,通過測試域名訪問就會直接把請求轉(zhuǎn)發(fā)到泳道域名上。實現(xiàn)方案就是通過 Lua 泳道模塊判斷 Host 的命名規(guī)則和
Header 里是否有泳道,從而判斷是否需要轉(zhuǎn)發(fā)到后端的 upstream 節(jié)點上。
單元化
隨著公司規(guī)模的不斷擴大,我們實現(xiàn)了第三套的負載均衡方案——單元化。首先先介紹一些問題,你的服務(wù)是否真的做到了水平的擴展?你的服務(wù)是否真的做到了物理隔離?
舉個例子,如上圖,一條業(yè)務(wù)線上有兩套集群,服務(wù) A 和服務(wù)
B,同時下面有數(shù)據(jù)庫,數(shù)據(jù)庫做了分庫分表,并且服務(wù)也是分布式服務(wù),它到底是不是一個水平擴展的服務(wù)呢?
服務(wù)集群 A 和 B 的服務(wù)節(jié)點都有 N 個,當(dāng)在服務(wù)集群 B 加一個節(jié)點時,所有服務(wù)集群 A 的節(jié)點都會與服務(wù)集群 B
中新加的節(jié)點建立一條連接,做長連接的連接池。長連接的資源其實是不可水平擴展的,因為每加一臺機器,承受的長連接的數(shù)量都是 N。同理這個問題最嚴重的是在 DB
上,DB 的主庫一般都是單點的,即使分了庫,所有的寫請求都會放到主庫上,它的長連接其實是受限的,你如何怎么保證它的長連接一直在一個可控的范圍內(nèi)呢?
另一個問題是任意節(jié)點有異常都可能影響所有的用戶,服務(wù)集群 B 的 N 節(jié)點出現(xiàn)問題,此時服務(wù)集群 A 里的所有請求,都有可能轉(zhuǎn)發(fā)給 B 集群的 N
服務(wù)節(jié)點,也就是說任意一個用戶的請求都可能會受到影響。所以看似你做的整個的分布式的系統(tǒng)能做到水平擴展,但其實不是這樣。?
為了解決上面的問題,我們提出了單位化的操作。按用戶的流量特征把所有的請求都框到一個服務(wù)單元內(nèi),通常服務(wù)單元都是按地域劃分的。此時每個單元內(nèi)的服務(wù)是互相分布式調(diào)用的,但是跨單元的服務(wù)之間是沒有關(guān)系的。原來服務(wù)集群
A 里的服務(wù)節(jié)點對服務(wù)集群 B 里的每一個節(jié)點都建立連接,變成了只針對自己服務(wù)單元內(nèi)的服務(wù)做長連接,這樣連接數(shù)量就降到原來的 N
分之一。同時用戶的流量會在某個單元內(nèi)做閉環(huán),實現(xiàn)了完全的隔離。當(dāng)然現(xiàn)實中單元化還有一些前提,比如說 DB 的數(shù)據(jù)分布,如果 DB
不能按單元劃分,那單位化還是實現(xiàn)不了。
Oceanus 網(wǎng)關(guān)層實現(xiàn)單元化的路由,復(fù)用了報文轉(zhuǎn)換的功能模塊,支持根據(jù)某個Header或者Get參數(shù)來修改、刪除、新加 Header 或者 Get
參數(shù)。?
如上圖的例子,假如從 App 端上來的請求,會帶有地域特征,北京的用戶可能帶的 Location ID 是
01001、01002、01003。當(dāng)它上來以后,我們有一個 Map
映射表,它跟前面的精細化分流不太一樣,而是通過路由表做路由篩選的,前面的可能是基于表達式的。假如 01001 的Location 的路由表,它對應(yīng) Set
ID 是 SET1,那么就直接在 01001 的用戶請求里加一個 header,這個 header 的名稱就是
SET1,這樣就實現(xiàn)了報文的轉(zhuǎn)換,也就是北京的用戶在網(wǎng)關(guān)層都會新加一個 SET1 標(biāo)識。之后就可以復(fù)用前面的精細化分流的方案,當(dāng)遇到 SET1 的請求就轉(zhuǎn)發(fā)到
SET1 的分組,從而實現(xiàn)了前端的單位化的路由方案。
未來規(guī)劃
Oceanus 未來主要在配置動態(tài)化上做進一步優(yōu)化,尤其是 location 動態(tài)化,因為通過文件配置 location 的方式,每次 reload
的操作,對線上的集群還是有損的。同時希望做到插件的管理動態(tài)化,它的熱部署與升級,以及自動化運維。美團線上近千臺機器,做自動化運維是很解放人效的操作,如何去快速搭建一個集群以及遷移各個集群的站點,是一個比較關(guān)鍵的任務(wù)。
演講視頻及PPT下載:
美團 HTTP 服務(wù)治理實踐
<https://link.zhihu.com/?target=https%3A//www.upyun.com/opentalk/430.html>
熱門工具 換一換