有贊統一接入層架構演進

本文系雲原生應用最佳實踐杭州站活動演講稿整理。杭州站活動邀請了 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 的聲明式配置的聲明定義。

走向雲原生的道路上我們應該多向前看,把雲原生上所需要的功能、學到的新東西更好的融入到工作當中,把用到的組件能夠更好的契合到雲原生當中,走向雲原生就會更有意義。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章