2019 年 5 月 11 日,OpenResty 社區聯合又拍雲,舉辦 OpenResty × Open Talk 全國巡迴沙龍武漢站,鬥魚資深工程師張壯壯在活動上作了《 鬥魚 API 網關演進之路 》的分享。html
OpenResty x Open Talk 全國巡迴沙龍是由 OpenResty 社區、又拍雲發起,邀請業內資深的 OpenResty 技術專家,分享 OpenResty 實戰經驗,增進 OpenResty 使用者的交流與學習,推進 OpenResty 開源項目的發展。活動已前後在深圳、北京、武漢舉辦,後續還將陸續在上海、廣州、杭州等城市巡迴舉辦。node
張壯壯,鬥魚數據平臺部資深工程師,負責打點、API 網關及後端服務架構建設。git
如下是分享全文:github
你們下午好,先簡單作下自我介紹,我是來自鬥魚的張壯壯,曾就任於拉勾網和滴滴出行,2017 年 3 月加入鬥魚,主要負責 API 網關和數據採集等工做。算法
今天給你們帶來鬥魚 API 網關的一些細節,在分享以前,先感謝剛纔的邵海楊老師,由於咱們的 API 網關基於 Slardar 二次開發的,剛纔海楊老師已經詳細介紹了 Slardar 的基礎原理,因此這塊內容我再也不重複介紹,直接介紹鬥魚在此基礎上作的更細節的工做。數據庫
今天我主要從三個方面來分享:後端
爲何是 API 網關?這要從微服務化遇到的兩個問題提及,第一,怎樣保證服務的無宕機更新部署;第二,怎樣保證服務的自動擴容及故障恢復。這兩個問題,又拍雲已經有了解決方案:只須要在服務之上作服務路由,讓路由支持服務的無宕機更新部署,保證服務的擴容及故障恢復,咱們也是按照這個思路來實現的。跨域
可是隻有這個不能解決全部問題,好比服務的性能監控、系統的資源調度等問題,還須要其餘基礎設施來支撐。因此 Docker 和 Kubernetes 進入了咱們的視野。因爲本次活動主題是 OpenResty,對容器技術選型就不作展開了。服務上容器,在更新和遷移中 IP 和 Port 是變化的、不肯定的,這是必需要解決的問題。緩存
服務路由要支持服務註冊、服務發現和負載均衡。通過多方調研,咱們發現又拍雲開源的動態負載均衡組件 Slardar 很是適合業務場景,主要解決了容器環境服務 IP 和 Port 均變化的問題。網絡
服務發現有不少開源的工具能夠用,好比 Consul、etcd 和 Apache Zookeeper。因爲咱們選型是 K8S,因此服務發現選擇 etcd。
負載均衡,基本上就是三個:LVS、HAProxy 和 Nginx。
接下來咱們簡單瞭解下 Slardar,它由四個部分組成:
Slardar 在啓動過程當中先拉取服務配置,拉取完配置就能夠對外進行服務了。若是咱們的服務由於擴容或者異常宕機又起了一個新的實例,此時 IP和 Port 都會變化,須要把服務 IP 和 Port 等信息註冊到 Slardar 和 Consul。邏輯清晰,結構簡單,這是選擇 Slardar 的緣由,不過想要真正應用,僅僅有動態負載均衡遠遠不夠,還須要解決如下的問題:
咱們拿到了 Slardar 進行了大刀闊斧地調整:
下面介紹鬥魚的 API 網關的部署架構,以及內部功能細節。
抽象來看,服務接入 API 網關的架構很是清晰,和原生 Nginx 架構同樣,全部流量必須通過 API 網關後,才能訪問到真實的後端。經服務端處理,並將響應返回給 API 網關以後,再交給客戶端。這是單機房的部署架構。實際上使用 Nginx 做爲入口網關,API 網關做爲內網網關,Nginx 負責處理複雜的 location 邏輯、SSL 認證等,API 網關負責抽象後臺服務間通用功能。
這是多機房部署架構,上游使用 CDN 來作流量分配,方便機房故障時進行一鍵流量切換。
這張圖很好的展現了鬥魚 API 網關生態體系。
上圖左側是 API 網關的內部功能,綠色部分是已實現的功能,包括限流、OA 認證、請求限制、AB 測試、灰度測試、流量複製、藍綠髮布、API 開放平臺等。灰色部分是即將實現的功能,藍色部分是 Slardar 原生的功能,固然咱們也作了大量的優化工做,好比:路由算法支持動態的權重更新。
上圖右側 API 網關的支撐服務,其中網關管理 MIS 系統是可視化管理後臺。日誌聚合提供了接入服務的性能圖表,好比狀態碼 4XX,5XX 統計,以及請求不一樣水位線耗時分佈,俗稱 P90,P99。天眼是 Java agent,負責實現服務對接下面的註冊中心、實現服務優雅停機。
上圖下側是 API 網關代理的服務,如搜索、推薦、風控、流量分發等等。
以上整個羅列了咱們已經實現且在線使用的重要功能。由於時間關係,後面我會挑選其中三個功能詳細介紹實現原理。
OA 認證:爲了解決後端服務各自對接 OA 認證繁瑣,比較典型的是內部使用的開源系統,如Kibana、zabbix、Dubbo Admin 等,這些開源組件咱們不可能投入人力去二次開發,可是須要接 OA,還須要進行一些 ACL 權限控制。
QPS 限流:用的是很是簡單的計數器模式,是單機版的,限流是爲了保證鬥魚的核心服務,好比視頻拉流,還有剛纔提到的推薦、搜索免受洪峯攻擊。
服務兜底:這個功能想必你們很是瞭解,它能保證上游的數據服務永不消失,它與 QPS 限流實際上是結合在一塊兒的,觸發限流的請求會直接返回兜底數據,這能夠保證在極端狀況下,咱們的後端服務不會被瞬時流量打垮,同時保證友好的用戶體驗。典型的場景就是 S 級主播的首秀,例如 3 月份PDD 的首播就給咱們帶來了很是大的流量衝擊,經過 QPS 限流保證了咱們的核心服務不受影響。
流量複製:在不影響用戶正常請求的前提下,將原始請求複製一份或者多份,供開發人員在線對服務進行功能測試、性能測試和壓力測試。功能上支持自身複製及跨域名複製,流量的放大和縮小。
AB測試、灰度測試和藍綠髮布,能夠歸爲一類,均是維護多套 upstream 列表,經過某種策略,將不一樣的請求代理到不一樣 upstream 。
簽名認證:對外暴露的接口,須要一套簽名算法,避免服務直接裸露在外,因此這裏面作了一個功能抽象。
咱們保證服務高可用其實就是要處理兩個場景:第一個場景是它的更新部署;第二個場景就是運行期間故障。
咱們從服務更新部署和服務下線來看考慮第一個場景。
想要避免服務更新部署致使請求異常,其實知足兩個條件便可:
第一個是保證服務註冊到註冊中心的實例,必定是能夠對外服務的狀態,即某些服務必定要在啓動完成後才能加入到中心。另外一個場景是一些服務須要熱加載,啓動完了後不能當即對外進行服務,這時服務有一個預熱的過程,必定是預熱完了後才能註冊到註冊中心。
第二個是下線時經過 trap 命令,在接收到程序結束(terminate)和程序終止(interrupt)時執行 shutdown 函數,這裏 shutdown 函數會先通知註冊中心下線改實例,而後 hang 住 5-10 秒,等待下線事件更新到網關。
這裏特別說明一下服務更新流程,由於 API 網關是基於 Nginx 的,因此控制單個 worker 進程全量從註冊中心拉取 upstream 配置信息,並寫入 lua_shared_dict,其餘的 worker 定時同步lua_shared_dict 配置信息到本地緩存。爲了反向代理的高性能,網關是從本地緩存獲取 upstream 信息,而不是從 lua_shared_dict 。
接下來咱們看運行期故障怎麼處理。
咱們是這樣作的:心跳探活+狀態回溯,咱們知道只有心跳探活是不能徹底保證服務的高可用的,因而加上了「狀態回溯」。
另外說一句:「狀態回溯」是我本身想的,不知道用的對不對,即一次請求過來,若是反向代理失敗,就將該服務標記爲不可用。
加上以前啓動和中止的操做,就保證了在任何狀況下,正常的發佈或者某臺實例異常故障不會出現服務不可用的瞬間。這裏就解決了以前提到的保證服務的無宕機更新部署和保證服務的自動擴容和故障恢復這兩個問題。
AB 測試咱們只是作了一個通用的功能。咱們的 AB 測試是基於 Nginx rewrite 命令,主要使用場景有:
目前網關對外提供 AB Test 功能是面向 HTTP 服務的,其實現原理是:後臺服務提供一個默認接口,同時提供多種須要在線驗證的其餘接口,請求到達API網關,根據命中規則,重定向至對應的接口。其中,源 URI 做爲對外暴露的接口,保持固定不變。
AB 測試規則支持白名單、尾數、輪詢策略——一致性哈希等等。固然,業界仍是另一種實現方式,好比馬蜂窩根據策略訪問不一樣的 upstream 列表。
請求到達網關以後,會反向代理給後端,若是是錯誤狀態碼,好比 500、50二、503,咱們直接拿到兜底數據,返回給客戶端就能夠了。因爲 OpenResty 的限制,沒法在 header_filter_by_lua* 和 body_filter_by_lua 中使用發起非阻塞的 HTTP 請求或者其餘依賴 TCP 的協議(如 Redis)去查詢兜底數據(緣由請點擊https://github.com/openresty/lua-Nginx-module),有過 OpenResty 或者 Nginx 模塊開發經驗的同窗應該都知道:阻塞 API 的使用將會大大的下降 Nginx 的性能。
業界是怎麼處理這個問題的?我見到的有基於 OpenResty 實現的兜底功能,均是使用ngx.location.capture 來主動發起請求給上游服務,而非使用 Nginx 原生的命令 proxy_pass 來實現反向代理。ngx.location.capture 的返回結果包含了響應狀態碼,若是狀態碼屬於 Error Code,則去查詢兜底數據,並返回給 C 端用戶。
--子查詢代理完成請求
local res = ngx.location.capture('/backend' .. request_uri, {
method = method,
always_forward_body = true
})
if 504 == res.status then
return bottom_value
end
使用 ngx.location.capture 替換 proxy_pass 的問題在於,HTTP 協議的應用場景很是普遍,Nginx 實現反向代理時已經作了大量的適配和場景覆蓋,使用 ngx.location.capture 至關於從新 Nginx 的反向代理模塊,須要考慮諸如:文件上傳下載、靜態資源和動態資源、是否傳遞 Cookie 等等場景。任何一個應用場景的遺漏都將是一個線上 BUG。
咱們要作服務兜底的時候,網關已經上線了一年多,這個時候若是想要重寫,無異於火中取栗。比較幸運的是,在查看 Nignx 官方文檔的時候,發現原生命令 error_page。
示例中請求 location / 若是上游服務返回 404,則會內部重定向至 @fallback,而 @fallback 接口中能夠發起二次 HTTP 請求,例如獲取兜底數據,而且是可使用非阻塞類庫的。最後實現的核心代碼以下:
location / {
proxy_pass http://backend;
error_page 500 502 503 504 =200 @janus_bottom;
}
location @janus_bottom {
content_by_lua '
local bottom = require "bottom";
--非阻塞獲取兜底數據,並返回給client
bottom.bottom();
';
}
總結一下鬥魚的 API 網關,就是運行在全部的 HTTP 服務上,提供通用、可抽象的服務治理功能。
2018 年我參加了杭州的 OpenResty 大會,當時受到了很大的啓發。
如今 API 網關已經全面應用到鬥魚整個後端架構,咱們對其作了一些規劃。首先網關是分集羣的,因爲請求量很是大,磁盤的性能不高,反而會反向影響 API 網關吞吐性能。所以咱們作了不少優化,好比上 SSD,精簡日誌等,這些功能都已經上線了,日誌精簡完後大概是原來體量的 2/3,效果很是明顯,固然 SSD 纔是「銀彈」。
咱們後期的一個想法是將網關日誌直接寫入 MQ,後續用專門的服務消費 MQ,把日誌落地到本地磁盤。另一個想法是從 MQ 裏面直接消費到 ES 裏面,作一些其餘的分析。
而後,就是將請求分同步和異步兩條線路。客戶端請求到達 API 網關,網關會通過如限流,服務兜底,AB測試,灰度測試等功能,這些功能都是在同步的邏輯線路中。顯然,同步邏輯功能越多,勢必會影響客戶端請求 HTTP 的時延。因此咱們在 API 網關這一塊,將請求日誌放到 MQ,由一些模塊直接消費這個 MQ,好比通過風控、流量控制、限流分析,產生一些 API 網關可使用的配置,寫入到 DB 裏。網關經過拉取 DB 的配置,對後續的請求作一些限制。咱們如今的同步鏈路已經比較完善,下一步重點是異步鏈路,並且我相信異步鏈路應該是整個 API 網關體系中更加龐大的生態鏈。
觀看視頻和 ppt 下載,請點擊: