本文系雲原生應用最佳實踐杭州站活動演講稿整理。杭州站活動邀請了 Apache APISIX 項目 VP 溫銘、又拍雲平台開發部高級工程師莫紅波、螞蟻金服技術專家王發康、有贊中間件開發工程師張超,分享雲原生落地應用的經驗心得,以下是張超《有贊統一接入層架構演進》分享內容。
張超,有贊中間件團隊開發工程師,網關、Service Mesh 領域的專家,熱衷技術,對 Golang、Nginx、Ruby 語言等有深入的研究。
大家好,我是來自有贊的張超,有贊中間件團隊的開發工程師。今天給大家帶來有贊接入層架構演進的分享。
先簡單給大家介紹下有贊接入層,內部名為 YZ7,從概念來講它與網關比較接近,是基於 OpenResty 和 Nginx 來實現的,主要是有標准 C 模塊,自研發的 Nginx C 模塊,以及基於 lua 實現的模塊。它作為有贊業務流量的公網入口,提供 Traffic Shaping,包括限流、安全相關的像 WAF、請求路由等功能,請求路由包含標准的藍綠發布、灰色發布功能,負載均衡等方面的功能。今天的分享,主要是從下面從三個方面來深入解析:
-
舊版接入層架構痛點
-
新架構設計分析
-
新架構設計總結
舊版接入層架構痛點
首先從舊版接入層架構的相關痛點出發,開始新架構的設計分析。
上圖是舊版接入層架構的縱向切面,方案是早幾年之前的。當時流行用 redis 做配置同步,它天然的主從同步協議確實非常適合。其中黃色箭頭線是配置同步,數據從 redis master 同步到每個實例上的 redis slave,然后本級的 YZ7 會去輪巡本級的 redis,並把數據讀到自身內存中。
為什么有右下方的 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 的實例上。
舊版接入層架構的缺點
-
redis master 的單點問題:沒有使用 redis closter 或者哨兵方案,只是簡單的主從模式,出現問題時會導致配置無法下發。
-
當接入層是按照多機房的規模進行部署的,因為 redis master 是一個單點,它必然存在於一個機房中,從它所在的機房將數據同步到其他機房的 redis slave 時,容易受到機房之間專線穩定性的影響,穩定性差,配置同步的延時就高。
-
當 redis master 出現問題,這意味着從 k8ssync controller 同步過來的 K8S 內部服務 endpoints 數據無法實時同步到 YZ7 實例上。如果一些服務實例的 point 被清除了,接入層不能第一時間感知到。如此一來當請求進來,這邊還在用已經下線的 point IP,導致請求會 502、504,引起服務不可用。還有一個缺點,因歷史原因導致的 k8ssync controller 也是單點,如果它掛了,K8S server 會無法同步,同樣會導致服務不可用,甚至引起大規模的故障。
-
配置不具備屬性特征。無法在配置層面做多樣化處理,包括配置的灰度下發。配置的灰度下發這個詞是我個人提出來的,先保留這個疑問,后面會詳細地揭開。
新架構設計三大組件
帶着舊版接入層的種種缺陷,接下來需要設計出能夠解決這些缺陷的新架構。當然,在設計新架構時需要遵循一些架構相關的要點。
-
首先就是解決基礎的單點問題,為服務可用性提供保障。
-
組件的設計需要是無狀態,可灰度、可回滾、可觀測的。
-
無狀態:意味着服務可以有彈性的進行擴縮容,應對彈性流量時非常的有幫助。
-
可灰度:服務某個組件的更新,它的影響面不能是整個集群或者是所有的流量,必須有可灰度的能力,只影響部分流量與部分實例。
-
可回滾:當服務更新發布后,出現一些連環的反映,可以單獨的對它回滾。
-
可觀測:從各個角度來增強組件的可觀測性,包括日志、logging、metrics 甚至是 opentracing 等相關功能要做的更好,能最大地把控到組件在線上的運行程度。
-
降低組件間的耦合程度。各組件職能獨立,可獨立測試部署。即使架構設計的再好,但是部署復雜,測試麻煩,就會加大成本。
遵循上述要點后,新架構方案細看有點像 Service Mesh 控制面、數據面分離和 APISIX 的控制面、數據面分離。中間虛線以上是控制面,下方則是數據面。控制面的核心組件叫 YZ7-manager,左邊對接 K8S,右邊對接 ETCD,ETCD 是它的配置存儲中心,所有接入層的配置會存放在 ETCD 中,同時又會去 watch K8S。
虛線下方的數據面是每個 YZ7 的實例,每個實例上都有一個伴生進程,叫做 YZ7-agent,agent 會做一些雜活。YZ7 則是保留核心功能的網關,從下往上的紅線箭頭即是請求的方向。
控制面核心組件 manager
-
manager 是一個配置提供者,類似於 Istio Pilot,Istio 1.5 版本之前是由多個組件組成,其中最重要的就是 Pilot。配置保存在 ETCD 中,ETCD 的特點就是穩定可靠,所以選型用了 ETCD。
-
manager 是無狀態的,可以做到水平擴容。
-
manager 接管了原來 k8ssync controller 的功能,由它去 watch K8S,代替了原 K8S-think 的功能。因為 manager 是無狀態、可水平擴容的,解決了 YZ7 K8S-think 的單點問題。同時在原架構當中,YZ7 配置的 admin server 和現在的 APISIX 是非常相似的,它的 admin server 是和網關放在一起的,而在新架構中把網關 admin server 替掉,只放在控制面的 YZ7-manager 中。
-
最后一個核心功能就是配置下發功能,從 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 變更,它需要盡可能快的推送數據面,否則可能會出現 502、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 的聲明式配置的聲明定義。
走向雲原生的道路上我們應該多向前看,把雲原生上所需要的功能、學到的新東西更好的融入到工作當中,把用到的組件能夠更好的契合到雲原生當中,走向雲原生就會更有意義。