本文系雲原生應用最佳實踐杭州站活動演講稿整理。杭州站活動邀請了 Apache APISIX 項目 VP 溫銘、又拍雲平臺開發部高級工程師莫紅波、螞蟻金服技術專家王發康、有贊中間件開發工程師張超,分享雲原生落地應用的經驗心得,如下是張超《有贊統一接入層架構演進》分享內容。html
張超,有贊中間件團隊開發工程師,網關、Service Mesh 領域的專家,熱衷技術,對 Golang、Nginx、Ruby 語言等有深刻的研究。node
你們好,我是來自有讚的張超,有贊中間件團隊的開發工程師。今天給你們帶來有贊接入層架構演進的分享。redis
先簡單給你們介紹下有贊接入層,內部名爲 YZ7,從概念來說它與網關比較接近,是基於 OpenResty 和 Nginx 來實現的,主要是有標準 C 模塊,自研發的 Nginx C 模塊,以及基於 lua 實現的模塊。它做爲有贊業務流量的公網入口,提供 Traffic Shaping,包括限流、安全相關的像 WAF、請求路由等功能,請求路由包含標準的藍綠髮布、灰色發佈功能,負載均衡等方面的功能。今天的分享,主要是從下面從三個方面來深刻解析:數據庫
首先從舊版接入層架構的相關痛點出發,開始新架構的設計分析。後端
上圖是舊版接入層架構的縱向切面,方案是早幾年以前的。當時流行用 redis 作配置同步,它自然的主從同步協議確實很是適合。其中黃色箭頭線是配置同步,數據從 redis master 同步到每一個實例上的 redis slave,而後本級的 YZ7 會去輪巡本級的 redis,並把數據讀到自身內存中。api
爲何有右下方的 k8ssync controller 呢?由於前幾年 K8S 逐漸的成爲熱門,不少應用都開始走向容器化的道路。跨域
YZ7 是基於 OpenResty 來開發的,整個技術棧都是基於 lua,在 K8S 的生態裏 lua 並不在其中。若是想要 watch K8S 裏面的服務,須要實時知道它有哪些 endpoints。雖然經過 lua 也能夠實現,可是須要重頭作一個相似像 K8S 標準的 client-go 庫,這就得不償失了。所以會應用一個使用 GoLang 編寫的 k8sssync controller,它負責向 K8S 獲取它所感興趣的後端服務 endpoints 數據,再經過 YZ7 配置的 API,再次寫入到 redis master,最後由 redis master 分發到每一個 YZ7 的實例上。緩存
舊版接入層架構的缺點安全
帶着舊版接入層的種種缺陷,接下來須要設計出可以解決這些缺陷的新架構。固然,在設計新架構時須要遵循一些架構相關的要點。架構
遵循上述要點後,新架構方案細看有點像 Service Mesh 控制面、數據面分離和 APISIX 的控制面、數據面分離。中間虛線以上是控制面,下方則是數據面。控制面的核心組件叫 YZ7-manager,左邊對接 K8S,右邊對接 ETCD,ETCD 是它的配置存儲中心,全部接入層的配置會存放在 ETCD 中,同時又會去 watch K8S。
虛線下方的數據面是每一個 YZ7 的實例,每一個實例上都有一個伴生進程,叫作 YZ7-agent,agent 會作一些雜活。YZ7 則是保留核心功能的網關,從下往上的紅線箭頭便是請求的方向。
控制面核心組件 manager
控制面核心組件 agent
數據面的核心組件是 agent,是一個伴生服務,與每個接入層的實例綁定。核心功能就是負責配置同步,包括配置註解的釋義,這個和配置層面的灰度是相關的。還有配置間依賴管理,當有 A、B 兩種配置時,可能 A 配置是依賴於 B 配置的,至關於 APISIX 裏的 route 和 upstream。agent 的服務會把配置間的依賴管理作好。
接入層 YZ7
咱們把原有配置的 admin server 去掉了,同時負責向 redis 獲取數據的部分配置相關代碼也去掉了,只留下了 http 接口。咱們能夠從外部將配置推送到 YZ7 實例中,保持在共享內存中。原來的網關功能所有保留,沒有作不少的改造,僅保留核心功能,簡化了組件。
講完三個核心組件以後,再來聊一下新架構中幾個比較重要的細節。
第一:從控制面的 YZ7-manager,到數據面的 YZ7-agent,配置下發協議怎麼設計才能高效可靠?
第二:從 YZ7-agent 和 YZ7 之間,數據是用推模式仍是拉模式?
第三:配置註解怎麼實現?
第四:配置依賴怎麼保證?
帶着這四個問題,接下來會詳細講解,逐個擊破:
控制面 YZ7-manager 到 數據面 YZ7-agent
首先,咱們對於協議的要求必定是簡單、可靠的,不然理解成本高,開發成本也會提升。
其次,協議必須支持服務端的主動推送,就像 APISIX 的配置生效時間很低,由於 ETCD 是支持 watch 功能。而 Kong 的配置時間相對比較高,是由於 kong 上對接的是 PostgreSQL 和 Cassandra,這兩種關係數據庫是不支持 watch 的。服務端有數據變動,客戶端只能經過輪巡的方式獲取。輪巡的間隔太長,配置生效時間就高;間隔過短,能夠及時獲取到數據變動,可是資源消耗會更高。
基於上述兩點,咱們以 gRPC 爲基礎,並參考 xDS,設計了一個新的協議。初次鏈接時,能夠全量獲取控制面的數據,後續一直保持長鏈接,能夠增量地獲取服務端的數據配置變動。
上圖是 gRPC、XDS 的片斷。最上面有一個ConfigDiscoverService,這個 gRPC 就是作配置同步的核心,其中核心的兩個 message 是 configrequest 與 configresponse。
configrequest 中,node 是帶有某個數據鏈實例相關的數據,好比所在的集羣,hostname,IP 等。resourcecondition 是在數據面聲明感興趣的配置,好比對路由配置,對 upstream 配置或對跨域配置感興趣。在列表中把感興趣的配置所有聲明好,告訴服務端,控制面才能精準的把所感興趣的配置推送到數據面。
configresponse 就是把響應碼,包括 error detail 在出錯的狀況下,將包括錯誤碼在內的信息,把 resource 所有放在 resource 列表裏面而後推送給客戶端。它的傳輸模型也比較簡單,客戶端會在連完以後發送 config request,而後服務端第一次會把全部的配置數據推送到客戶端。
當一個接入層只是推送一些配置,它的配置量不會很大,幾百兆就很是多了,所以全量的推送並不會帶來特別多的帶寬與內存上的開銷,全量推送也是一個低頻事件,不用過於擔心它的性能。
隨着時間的推移,服務端會有新的配置變動,好比運維新增了配置或是發佈業務應用,發佈以後 pond 作了遷移,致使 pond 的endpoints 變動了。控制面感知到這些變動,會將這些數據實時地推送到 Client 端,完成控制面到數據面的配置推送。
這跟 xDS 協議是很類似的,xDS 裏的 discovery request 發送到服務端以後,若是有數據就把數據推回來,在discover response,若是沒有數據會其中加入一個 none 標誌,告訴咱們準備同步這個 discovery quest。沒有數據時至關因而請求 ACQ 的功能。咱們設計的有點相似 xDS 的簡化版本,沒有這方面的功能。
數據面 YZ7-agent 到 接入層 YZ7
從 YZ7-agent 到 YZ7 即數據面的 agent 到數據面的實例,其配置同步的抉擇到底是拉仍是推?
首先來考慮拉,它的優勢是按需加載,在須要時去加載對應的配置。缺點是若是配置提供方沒有像 ECTD 的 watch 功能,就須要數據存在內存中必需要有淘汰的機制,不然就沒有辦法獲取到同一個實例新的配置變動。而若是配置使用了淘汰策略,帶來的問題就是配置生效時間高。生效時間高,對於一些靜態配置像路由、host service 配置是無關痛癢,可是對於容器化業務的 endpoints 變動,它須要儘量快的推送數據面,不然可能會出現 50二、504 等 5XX 的錯誤。所以拉的模式不適用於新的架構中。
其次是推模式,YZ7-agent 須要主動把數據推到 YZ7。優勢是 YZ7 只須要作簡單的保存動做便可,不須要考慮數據過時,並且組合的耦合程度會更低。這樣的 YZ7 交付給測試,能夠加幾個接口,把須要用的測試數據推動去就行,而不須要額外部署 YZ7-agent,對交付測試比較有利。缺點是依賴於別人推會有一個問題,若是服務是剛剛起來或者 Nginx 剛剛完成熱更新時,共享內存裏是沒有數據的,要採用推模式就必須解決這個問題。咱們採用的方式是 agent 會按期的把數據緩存轉儲到磁盤上,當接入層 YZ7 實例熱更新完或剛啓動的時候,就會從磁盤上加載舊的數據,保證能夠正常起來。再者是強制在此時要求 YZ7-agent 全量推送一次數據,就能夠馬上達到最新的配置。
配置註解的實現
設計配置註解是爲了作配置灰度。其做用是當新增了配置,但不但願對集羣裏全部的實例生效,只須要集羣中的一兩個小規模實例生效時方便進行驗證。由於若是配置有誤可能會帶來大規模故障,而進行配置灰度能夠有效下降故障的影響面。
上圖是配置 payload 的片斷,從上往下接入的是配置數據,裏面只有一個 server,而 antotations 就是這個註解,裏面的 canary 字段能夠設計成灰度配置所需字段。這是按照 hostsname 來配置,這個配置只有 hosts2 或者 hosts3 纔會生效。其中的 id、name、kind 是用來給配置作標識的,像 name、種類、UUID 之類的。其實 K8S 的聲明配置也是如此的,具體的配置是放在 steak 面,外面會有像 laybol 等雲數據相關的,圖中的 antotations 就是效仿 K8S 聲明式配置的 antotations。
有贊是一個 SaaS 服務提供者,域名很是多,配置很是複雜,比較依賴人爲配置。爲了下降因人爲操做失誤引發的故障面,須要有配置灰度這樣的功能。操做流程也很簡單,首先運維平臺上建立一個配置,並標註爲灰度配置,底層會建立出相關的配置註解。以後觀察配置在相關實例上的表現,表現OK,就能夠將該配置生效到全部的機器,去掉灰度配置註解,這時所有的接入層實例上也就生效了。若是出現問題,馬上刪除灰度配置,也可避免引發其餘激烈的反應。
建立灰度配置,並攜帶灰度註解。經過 YZ7-manager 分發到每一個 agent。agent 會判斷該配置在機器上是 hit 仍是 miss。若是是 miss 就會忽略掉這個配置,不會推過去。若是是 hit 就推送到本機中的 YZ7。
當灰度了一段時間,表現也正常,須要將其所有生效時就能夠修改配置了,去掉灰度註解推送到 YZ7-manager 後會原封不動的再推到 YZ7 各個實例上。左下角這臺是應用了灰度配置,因爲 name 是相同的,這時穩定版本的配置就會把以前灰度版本的配置替換掉,全部接入層實例的配置也就都相同了。
當發現配置有問題,刪除也會很簡單。配置刪除後,由於左下角這臺已經灰度命中了,它會把刪除配置的事件推到 YZ7,進而 YZ7 會主動刪除內存中的副本。而左中、左下本來就沒有命中灰度配置,會直接忽略,到此這三臺YZ7的實例配置又恢復到了灰度配置應用以前的狀態。
配置依賴管理
部分的配置間會有互相引用的關係。好比 host 配置,每個 host 可配置一個標準的錯誤頁,錯誤頁又是一個單獨的配置,在作 host 配置時,就必須先有錯誤頁配置,不然會沒辦法下發。因此數據面的 agent 就須要保證好數據配置的推送關係,當 A 配置依賴於 B 配置,就不能先把 A 配置推送到接入層實例。由於 A 配置和 B 配置中間推送有時間窗口,會沒法正確處理在 A、B 時間窗口之間進來的請求。
走向雲原生,須要咱們在工做中學習更多的借鑑在雲原生方面好的組件,像 K8S、Envoy 等都是值得學習的優秀範本。有贊接入層新架構遵循的控制面和數據面的職能分離設計原則,就是參考了 Service Mesh 的設計;配置下發協議是參考了 Envoy、xDS;加入註解的功能,設計上是參考了 K8S 的聲明式配置的聲明定義。
走向雲原生的道路上咱們應該多向前看,把雲原生上所須要的功能、學到的新東西更好的融入到工做當中,把用到的組件可以更好的契合到雲原生當中,走向雲原生就會更有意義。