首頁>技術>

前言

假設你正在開發一個電商網站,那麼這裡會涉及到很多後端的微服務,比如會員、商品、推薦服務等等。

那麼這裡就會遇到一個問題,APP/Browser怎麼去訪問這些後端的服務? 如果業務比較簡單的話,可以給每個業務都分配一個獨立的域名(http://service.api.company.com),但這種方式會有幾個問題:

每個業務都會需要鑑權、限流、許可權校驗等邏輯,如果每個業務都各自為戰,自己造輪子實現一遍,會很蛋疼,完全可以抽出來,放到一個統一的地方去做。如果業務量比較簡單的話,這種方式前期不會有什麼問題,但隨著業務越來越複雜,比如淘寶、亞馬遜開啟一個頁面可能會涉及到數百個微服務協同工作,如果每一個微服務都分配一個域名的話,一方面客戶端程式碼會很難維護,涉及到數百個域名,另一方面是連線數的瓶頸,想象一下你開啟一個APP,通過抓包發現涉及到了數百個遠端呼叫,這在移動端下會顯得非常低效。每上線一個新的服務,都需要運維參與,申請域名、配置Nginx等,當上線、下線伺服器時,同樣也需要運維參與,另外採用域名這種方式,對於環境的隔離也不太友好,呼叫者需要自己根據域名自己進行判斷。另外還有一個問題,後端每個微服務可能是由不同語言編寫的、採用了不同的協議,比如HTTP、Dubbo、GRPC等,但是你不可能要求客戶端去適配這麼多種協議,這是一項非常有挑戰的工作,專案會變的非常複雜且很難維護。後期如果需要對微服務進行重構的話,也會變的非常麻煩,需要客戶端配合你一起進行改造,比如商品服務,隨著業務變的越來越複雜,後期需要進行拆分成多個微服務,這個時候對外提供的服務也需要拆分成多個,同時需要客戶端配合你進行改造,非常蛋疼。

API Gateway

更好的方式是採用API閘道器,實現一個API閘道器接管所有的入口流量,類似Nginx的作用,將所有使用者的請求轉發給後端的伺服器,但閘道器做的不僅僅只是簡單的轉發,也會針對流量做一些擴充套件,比如鑑權、限流、許可權、熔斷、協議轉換、錯誤碼統一、快取、日誌、監控、告警等,這樣將通用的邏輯抽出來,由閘道器統一去做,業務方也能夠更專注於業務邏輯,提升迭代的效率。

通過引入API閘道器,客戶端只需要與API閘道器互動,而不用與各個業務方的介面分別通訊,但多引入一個元件就多引入了一個潛在的故障點,因此要實現一個高效能、穩定的閘道器,也會涉及到很多點。

API註冊

業務方如何接入閘道器?一般來說有幾種方式。

第一種採用外掛掃描業務方的API,比如Spring MVC的註解,並結合Swagger的註解,從而實現引數校驗、文件&&SDK生成等功能,掃描完成之後,需要上報到閘道器的儲存服務。手動錄入。比如介面的路徑、請求引數、響應引數、呼叫方式等資訊,但這種方式相對來說會麻煩一些,如果引數過多的話,前期錄入會很費時費力。

協議轉換

內部的API可能是由很多種不同的協議實現的,比如HTTP、Dubbo、GRPC等,但對於使用者來說其中很多都不是很友好,或者根本沒法對外暴露,比如Dubbo服務,因此需要在閘道器層做一次協議轉換,將使用者的HTTP協議請求,在閘道器層轉換成底層對應的協議,比如HTTP -> Dubbo, 但這裡需要注意很多問題,比如引數型別,如果型別搞錯了,導致轉換出問題,而日誌又不夠詳細的話,問題會很難定位。

服務發現

寫死在程式碼/配置檔案裡,這種方式雖然比較挫,但也能使用,比如線上仍然使用的是物理機,IP變動不會很頻繁,但擴縮容、包括應用上下線都會很麻煩,閘道器自身甚至需要實現一套健康監測機制。域名。採用域名也是一種不錯的方案,對於所有的語言都適用,但對於內部的服務,走域名會很低效,另外環境隔離也不太友好,比如預發、線上通常是同一個資料庫,因此閘道器讀取到的可能是同一個域名,這時候預發的閘道器呼叫的就是線上的服務。註冊中心。採用註冊中心就不會有上述的這些問題,即使是在容器環境下,節點的IP變更比較頻繁,但節點列表的實時維護會由註冊中心搞定,對閘道器是透明的,另外應用的正常上下線、包括異常宕機等情況,也會由註冊中心的健康檢查機制檢測到,並實時反饋給閘道器。並且採用註冊中心效能也沒有額外的效能損耗,採用域名的方式,額外需要走一次DNS解析、Nginx轉發等,中間多了很多跳,效能會有很大的下降,但採用註冊中心,閘道器是和業務方直接點對點的通訊,不會有額外的損耗。

服務呼叫

閘道器由於對接很多種不同的協議,因此可能需要實現很多種呼叫方式,比如HTTP、Dubbo等,基於效能原因,最好都採用非同步的方式,而Http、Dubbo都是支援非同步的,比如apache就提供了基於NIO實現的非同步HTTP客戶端。

因為閘道器會涉及到很多非同步呼叫,比如攔截器、HTTP客戶端、dubbo、redis等,因此需要考慮下非同步呼叫的方式,如果基於回撥或者future的話,程式碼巢狀會很深,可讀性很差,可以參考zuul和spring cloud gateway的方案,基於響應式進行改造。

優雅下線

優雅下線也是閘道器需要關注的一個問題,閘道器底層會涉及到很多種協議,比如HTTP、Dubbo,而HTTP又可以繼續細分,比如域名、註冊中心等,有些自身就支援優雅下線,比如Nginx自身是支援健康監測機制的,如果檢測到某一個節點已經掛掉了,就會把這個節點摘掉,對於應用正常下線,需要結合釋出系統,首先進行邏輯下線,然後對後續Nginx的健康監測請求直接返回失敗(比如直接返回500),然後等待一段時間(根據Nginx配置決定),然後再將應用實際下線掉。另外對於註冊中心的其實也類似,一般註冊中心是隻支援手動下線的,可以在邏輯下線階段呼叫註冊中心的介面將節點下線掉,而有些不支援主動下線的,需要結合快取的配置,讓應用延遲下線。另外對於其他比如Dubbo等原理也是類似。

效能

閘道器作為所有流量的入口,效能是重中之重,早期大部分閘道器都是基於同步阻塞模型構建的,比如Zuul 1.x。但這種同步的模型我們都知道,每個請求/連線都會佔用一個執行緒,而執行緒在JVM中是一個很重的資源,比如Tomcat預設就是200個執行緒,如果閘道器隔離沒有做好的話,當發生網路延遲、FullGC、第三方服務慢等情況造成上游服務延遲時,執行緒池很容易會被打滿,造成新的請求被拒絕,但這個時候其實執行緒都阻塞在IO上,系統的資源被沒有得到充分的利用。另外一點,容易受網路、磁碟IO等延遲影響。需要謹慎設定超時時間,如果設定不當,且服務隔離做的不是很完善的話,閘道器很容易被一個慢介面拖垮。

而非同步化的方式則完全不同,通常情況下一個CPU核啟動一個執行緒即可處理所有的請求、響應。一個請求的生命週期不再固定於一個執行緒,而是會分成不同的階段交由不同的執行緒池處理,系統的資源能夠得到更充分的利用。而且因為執行緒不再被某一個連線獨佔,一個連線所佔用的系統資源也會低得多,只是一個檔案描述符加上幾個監聽器等,而在阻塞模型中,每條連線都會獨佔一個執行緒,而執行緒是一個非常重的資源。對於上游服務的延遲情況,也能夠得到很大的緩解,因為在阻塞模型中,慢請求會獨佔一個執行緒資源,而非同步化之後,因為單條連線所佔用的資源變的非常低,系統可以同時處理大量的請求。

如果是JVM平臺,Zuul 2、Spring Cloud gateway等都是不錯的非同步閘道器選型,另外也可以基於Netty、Spring Boot2.x的webflux、vert.x或者servlet3.1的非同步支援進行自研。

快取

對於一些冪等的get請求,可以在閘道器層面根據業務方指定的快取頭做一層快取,儲存到Redis等二級快取中,這樣一些重複的請求,可以在閘道器層直接處理,而不用打到業務線,降低業務方的壓力,另外如果業務方節點掛掉,閘道器也能夠返回自身的快取。

限流

限流對於每個業務元件來說,可以說都是一個必須的元件,如果限流做不好的話,當請求量突增時,很容易導致業務方的服務掛掉,比如雙11、雙12等大促時,介面的請求量是平時的數倍,如果沒有評估好容量,又沒有做限流的話,很容易服務整個不可用,因此需要根據業務方介面的處理能力,做好限流策略,相信大家都見過淘寶、百度搶紅包時的降級頁面。

因此一定要在接入層做好限流策略,對於非核心介面可以直接將降級掉,保障核心服務的可用性,對於核心介面,需要根據壓測時得到的介面容量,制定對應的限流策略。限流又分為幾種:

單機。單機效能比較高,不涉及遠端呼叫,只是本地計數,對介面RT影響最小。但需要考慮下限流數的設定,比如是針對單臺網關、還是整個閘道器叢集,如果是整個叢集的話,需要考慮到閘道器縮容、擴容時修改對應的限流數。分散式。分散式的就需要一個儲存節點維護當前介面的呼叫數,比如redis、sentinel等,這種方式由於涉及到遠端呼叫,會有些效能損耗,另外也需要考慮到儲存掛掉的問題,比如redis如果掛掉,閘道器需要考慮降級方案,是降級到本地限流,還是直接將限流功能本身降級掉。

另外還有不同的策略:簡單計數、令牌桶等,大部分場景下其實簡單計數已經夠用了,但如果需要支援突發流量等場景時,可以採用令牌桶等方案。還需要考慮根據什麼限流,比如是IP、介面、使用者維度、還是請求引數中的某些值,這裡可以採用表示式,相對比較靈活。

穩定性

穩定性是閘道器非常重要的一環,監控、告警需要做的很完善才可以,比如介面呼叫量、響應時間、異常、錯誤碼、成功率等相關的監控告警,還有執行緒池相關的一些,比如活躍執行緒數、佇列積壓等,還有些系統層面的,比如CPU、記憶體、FullGC這些基本的。

閘道器是所有服務的入口,對於閘道器的穩定性的要求相對於其他服務會更高,最好能夠一直穩定的執行,儘量少重啟,但當新增功能、或者加日誌排查問題時,不可避免的需要重新發布,因此可以參考zuul的方式,將所有的核心功能都基於不同的攔截器實現,攔截器的程式碼採用Groovy編寫,儲存到資料庫中,支援動態載入、編譯、執行,這樣在出了問題的時候能夠第一時間定位並解決,並且如果閘道器需要開發新功能,只需要增加新的攔截器,並動態新增到閘道器即可,不需要重新發布。

熔斷降級

熔斷機制也是非常重要的一項。若某一個服務掛掉、介面響應嚴重超時等發生,則可能整個閘道器都被一個介面拖垮,因此需要增加熔斷降級,當發生特定異常的時候,對介面降級由閘道器直接返回,可以基於Hystrix或者Resilience4j實現。

日誌

由於所有的請求都是由閘道器處理的,因此日誌也需要相對比較完善,比如介面的耗時、請求方式、請求IP、請求引數、響應引數(注意脫敏)等,另外由於可能涉及到很多微服務,因此需要提供一個統一的traceId方便關聯所有的日誌,可以將這個traceId置於響應頭中,方便排查問題。

隔離

比如執行緒池、http連線池、redis等應用層面的隔離,另外也可以根據業務場景,將核心業務部署帶單獨的閘道器叢集,與其他非核心業務隔離開。

閘道器管控平臺

這塊也是非常重要的一環,需要考慮好整個流程的使用者體驗,比如接入到閘道器的這個流程,能不能儘量簡化、智慧,比如如果是dubbo介面,我們可以通過到git倉庫中獲取原始碼、解析對應的類、方法,從而實現自動填充,儘量幫使用者減少操作;另外介面一般是從測試->預發->線上,如果每次都要填寫一遍表單會非常麻煩,我們能不能自動把這個事情做掉,另外如果閘道器部署到了多個可用區、甚至不同的國家,那這個時候,我們還需要介面資料同步功能,不然使用者需要到每個後臺都操作一遍,非常麻煩。

這塊個人的建議是直接參考阿里雲、aws等提供的閘道器服務即可,功能非常全面。

其他

其他還有些需要考慮到的點,比如介面mock,文件生成、sdk程式碼生成、錯誤碼統一、服務治理相關的等,這裡就不累述了。

總結

目前的閘道器還是中心化的架構,所有的請求都需要走一次閘道器,因此當大促或者流量突增時,閘道器可能會成為效能的瓶頸,而且當閘道器接入的大量介面的時候,做好流量評估也不是一項容易的工作,每次大促前都需要跟業務方一起針對介面做壓測,評估出大致的容量,並對閘道器進行擴容,而且閘道器是所有流量的入口,所有的請求都是由閘道器處理,要想準確的評估出容量很複雜。可以參考目前比較流行的ServiceMesh,採用去中心化的方案,將閘道器的邏輯下沉到sidecar中,sidecar和應用部署到同一個節點,並接管應用流入、流出的流量,這樣大促時,只需要對相關的業務壓測,並針對性擴容即可,另外升級也會更平滑,中心化的閘道器,即使灰度釋出,但是理論上所有業務方的流量都會流入到新版本的閘道器,如果出了問題,會影響到所有的業務,但這種去中心化的方式,可以先針對非核心業務升級,觀察一段時間沒問題後,再全量推上線。另外ServiceMesh的方案,對於多語言支援也更友好。

github.com/aCoder2013/blog/issues/35

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 日常開發中3個非常有用的Node NPM包