統一接入層架構的演進

又拍雲發表於2020-11-25

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

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

推薦閱讀

技術選型:為什麼批處理我們卻選擇了Flink

【實戰分享】從選型到專案落地,漫談 gRPC

相關文章