2019 年 5 月 11 日,OpenResty 社區聯合又拍雲,舉辦 OpenResty × Open Talk 全國巡回沙龍武漢站,斗魚資深工程師張壯壯在活動上做了《 斗魚 API 網關演進之路 》的分享。
OpenResty x Open Talk 全國巡回沙龍是由 OpenResty 社區、又拍雲發起,邀請業內資深的 OpenResty 技術專家,分享 OpenResty 實戰經驗,增進 OpenResty 使用者的交流與學習,推動 OpenResty 開源項目的發展。活動已先后在深圳、北京、武漢舉辦,后續還將陸續在上海、廣州、杭州等城市巡回舉辦。
張壯壯,斗魚數據平台部資深工程師,負責打點、API 網關及后端服務架構建設。
以下是分享全文:
大家下午好,先簡單做下自我介紹,我是來自斗魚的張壯壯,曾就職於拉勾網和滴滴出行,2017 年 3 月加入斗魚,主要負責 API 網關和數據采集等工作。
今天給大家帶來斗魚 API 網關的一些細節,在分享之前,先感謝剛才的邵海楊老師,因為我們的 API 網關基於 Slardar 二次開發的,剛才海楊老師已經詳細介紹了 Slardar 的基礎原理,所以這塊內容我不再重復介紹,直接介紹斗魚在此基礎上做的更細節的工作。
今天我主要從三個方面來分享:
- 斗魚使用 API 網關的背景
- 網關的架構&功能
- 斗魚 API 網關遠期規划
為什么是 API 網關?
為什么是 API 網關?這要從微服務化遇到的兩個問題說起,第一,怎樣保證服務的無宕機更新部署;第二,怎樣保證服務的自動擴容及故障恢復。這兩個問題,又拍雲已經有了解決方案:只需要在服務之上做服務路由,讓路由支持服務的無宕機更新部署,保證服務的擴容及故障恢復,我們也是按照這個思路來實現的。
但是只有這個不能解決所有問題,比如服務的性能監控、系統的資源調度等問題,還需要其他基礎設施來支撐。所以 Docker 和 Kubernetes 進入了我們的視野。由於本次活動主題是 OpenResty,對容器技術選型就不做展開了。服務上容器,在更新和遷移中 IP 和 Port 是變化的、不確定的,這是必須要解決的問題。
服務路由
服務路由要支持服務注冊、服務發現和負載均衡。經過多方調研,我們發現又拍雲開源的動態負載均衡組件 Slardar 非常適合業務場景,主要解決了容器環境服務 IP 和 Port 均變化的問題。
- 服務注冊是服務需要主動上報服務相關信息,最重要的是 IP 和端口;
- 服務發現是把服務注冊的信息集中起來,最好能持久化;
- 為了避免單點故障,服務會啟動多個實例,因此還需要做負載均衡。
服務發現有很多開源的工具可以用,比如 Consul、etcd 和 Apache Zookeeper。由於我們選型是 K8S,所以服務發現選擇 etcd。
負載均衡,基本上就是三個:LVS、HAProxy 和 Nginx。
接下來我們簡單了解下 Slardar,它由四個部分組成:
- 第一部分是官方的 Nginx,沒有任何改動;
- 第二部分是 Nginx Lua 模塊,核心是 Lua 版本的負載均衡算法+ balance_by_lua
- 第三部分是 lua-resty-checkups,它是把 Nginx upstream 模塊常用的功能單獨抽出來,用 lua 重新實現了一遍;
- 第四部分是 luacocket,用於加載配置信息。
Slardar 在啟動過程中先拉取服務配置,拉取完配置就可以對外進行服務了。如果我們的服務因為擴容或者異常宕機又起了一個新的實例,此時 IP和 Port 都會變化,需要把服務 IP 和 Port 等信息注冊到 Slardar 和 Consul。邏輯清晰,結構簡單,這是選擇 Slardar 的原因,不過想要真正應用,僅僅有動態負載均衡遠遠不夠,還需要解決以下的問題:
- 服務如何在啟動后自動上報信息到 Consul;
- Slardar 如何解決自身單點問題;
- 怎樣應對 Consul 集群故障、或者網絡故障;
- 沒有可視化管理;
- 灰度測試、AB 測試、流量復制等等功能的實現(考慮未來使用場景)。
我們拿到了 Slardar 進行了大刀闊斧地調整:
- 取消 Consul,實現注冊中心,並持久化配置到數據庫;
- 開發 Java agent,實現 Java 服務自動上報;
- 對接 Kubernetes API,實現非 Java 服務自動上報;
- 提供可視化管理后台;
- 配置定時落盤,當網絡故障時作為托底;
- 定時全量拉取配置,增加集群(機房)概念,可一鍵切換流量;
- 支持集群部署,應對單點問題;
- upstrem 列表 key 由 Host 改為 Host+URI(前綴);
- 引入插件模式,新增了很多功能,如灰度測試、AB測試、流量復制、服務限流等。
斗魚 API 網關的架構&功能
下面介紹斗魚的 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 測試我們只是做了一個通用的功能。我們的 AB 測試是基於 Nginx rewrite 命令,主要使用場景有:
- 需測試的 ABCD…策略(或算法,或模型,或服務)會長期且同時存在於生產環境;
- 不想同時維護多套代碼分支;
- 客戶端無需改造。
目前網關對外提供 AB Test 功能是面向 HTTP 服務的,其實現原理是:后台服務提供一個默認接口,同時提供多種需要在線驗證的其他接口,請求到達API網關,根據命中規則,重定向至對應的接口。其中,源 URI 作為對外暴露的接口,保持固定不變。
AB 測試規則支持白名單、尾數、輪詢策略——一致性哈希等等。當然,業界還是另外一種實現方式,比如馬蜂窩根據策略訪問不同的 upstream 列表。
服務兜底
請求到達網關之后,會反向代理給后端,如果是錯誤狀態碼,比如 500、502、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 服務上,提供通用、可抽象的服務治理功能。
斗魚 API 網關的遠期規划
2018 年我參加了杭州的 OpenResty 大會,當時受到了很大的啟發。
現在 API 網關已經全面應用到斗魚整個后端架構,我們對其做了一些規划。首先網關是分集群的,由於請求量非常大,磁盤的性能不高,反而會反向影響 API 網關吞吐性能。因此我們做了很多優化,比如上 SSD,精簡日志等,這些功能都已經上線了,日志精簡完后大概是原來體量的 2/3,效果非常明顯,當然 SSD 才是“銀彈”。
我們后期的一個想法是將網關日志直接寫入 MQ,后續用專門的服務消費 MQ,把日志落地到本地磁盤。另外一個想法是從 MQ 里面直接消費到 ES 里面,做一些其他的分析。
然后,就是將請求分同步和異步兩條線路。客戶端請求到達 API 網關,網關會經過如限流,服務兜底,AB測試,灰度測試等功能,這些功能都是在同步的邏輯線路中。顯然,同步邏輯功能越多,勢必會影響客戶端請求 HTTP 的時延。所以我們在 API 網關這一塊,將請求日志放到 MQ,由一些模塊直接消費這個 MQ,比如經過風控、流量控制、限流分析,產生一些 API 網關可以使用的配置,寫入到 DB 里。網關通過拉取 DB 的配置,對后續的請求做一些限制。我們現在的同步鏈路已經比較完善,下一步重點是異步鏈路,而且我相信異步鏈路應該是整個 API 網關體系中更加龐大的生態鏈。
觀看視頻和 ppt 下載,請點擊: