首先我給大家看一張圖,如果大家對這張圖有些地方不太理解的話,我希望你們看完我這篇文章會恍然大悟。
img
什麼是 Spring cloud
構建分佈式系統不需要複雜和容易出錯。Spring Cloud 為最常見的分佈式系統模式提供了一種簡單且易於接受的編程模型,幫助開發人員構建有彈性的、可靠的、協調的應用程序。Spring Cloud 構建於 Spring Boot 之上,使得開發者很容易入手並快速應用於生產中。
官方果然官方,介紹都這麼有板有眼的。
我所理解的Spring Cloud就是微服務系統架構的一站式解決方案,在平時我們構建微服務的過程中需要做如服務發現註冊、配置中心、消息總線、負載均衡、斷路器、數據監控等操作,而 Spring Cloud 為我們提供了一套簡易的編程模型,使我們能在 Spring Boot 的基礎上輕鬆地實現微服務項目的構建。
Spring Cloud 的版本
當然這個只是個題外話。
Spring Cloud 的版本號並不是我們通常見的數字版本號,而是一些很奇怪的單詞。這些單詞均為英國倫敦地鐵站的站名。同時根據字母表的順序來對應版本時間順序,比如:最早 的 Release 版本 Angel,第二個 Release 版本 Brixton(英國地名),然後是 Camden、 Dalston、Edgware、Finchley、Greenwich、Hoxton。
Spring Cloud 的服務發現框架——Eureka
Eureka是基於REST(代表性狀態轉移)的服務,主要在AWS雲中用於定位服務,以實現負載均衡和中間層服務器的故障轉移。我們稱此服務為Eureka服務器。Eureka還帶有一個基於Java的客戶端組件Eureka Client,它使與服務的交互變得更加容易。客戶端還具有一個內置的負載平衡器,可以執行基本的循環負載平衡。在Netflix,更復雜的負載均衡器將Eureka包裝起來,以基於流量,資源使用,錯誤條件等多種因素提供加權負載均衡,以提供出色的彈性。
總的來說,Eureka 就是一個服務發現框架。何為服務,何又為發現呢?
舉一個生活中的例子,就比如我們平時租房子找中介的事情。
在沒有中介的時候我們需要一個一個去尋找是否有房屋要出租的房東,這顯然會非常的費力,一你找憑一個人的能力是找不到很多房源供你選擇,再者你也懶得這麼找下去(找了這麼久,沒有合適的只能將就)。
這裡的我們就相當於微服務中的**Consumer,而那些房東就相當於微服務中的Provider。消費者Consumer需要調用提供者Provider提供的一些服務,就像我們現在需要租他們的房子一樣。**但是如果只是租客和房東之間進行尋找的話,他們的效率是很低的,房東找不到租客賺不到錢,租客找不到房東住不了房。所以,後來房東肯定就想到了廣播自己的房源信息(比如在街邊貼貼小廣告),這樣對於房東來說已經完成他的任務(將房源公佈出去),但是有兩個問題就出現了。第一、其他不是租客的都能收到這種租房消息,這在現實世界沒什麼,但是在計算機的世界中就會出現資源消耗的問題了。第二、租客這樣還是很難找到你,試想一下我需要租房,我還需要東一個西一個地去找街邊小廣告,麻不麻煩?
那怎麼辦呢?我們當然不會那麼傻乎乎的,第一時間就是去找中介呀,它為我們提供了統一房源的地方,我們消費者只需要跑到它那裡去找就行了。而對於房東來說,他們也只需要把房源在中介那裡發佈就行了。
那麼現在,我們的模式就是這樣的了。
img
但是,這個時候還會出現一些問題。
- 房東註冊之後如果不想賣房子了怎麼辦?我們是不是需要讓房東定期續約?如果房東不進行續約是不是要將他們從中介那裡的註冊列表中移除。
- 租客是不是也要進行 註冊呢?不然合同乙方怎麼來呢?
- 中介可不可以做連鎖店呢?如果這一個店因為某些不可抗力因素而無法使用,那麼我們是否可以換一個連鎖店呢?
針對上面的問題我們來重新構建一下上面的模式圖
img
好了,舉完這個我們就可以來看關於 Eureka 的一些基礎概念了,你會發現這東西理解起來怎麼這麼簡單。
服務發現:其實就是一個“中介”,整個過程中有三個角色:服務提供者(出租房子的)、服務消費者(租客)、服務中介(房屋中介)。
服務提供者:就是提供一些自己能夠執行的一些服務給外界。
服務消費者:就是需要使用一些服務的“用戶”。
服務中介:其實就是服務提供者和服務消費者之間的“橋樑”,服務提供者可以把自己註冊到服務中介那裡,而服務消費者如需要消費一些服務(使用一些功能)就可以在服務中介中尋找註冊在服務中介的服務提供者。
服務註冊 Register:
官方解釋:當 Eureka 客戶端向[Eureka] Server註冊時,它提供自身的
元數據,比如IP地址、端口,運行狀況指示符URL,主頁等。結合中介理解:房東 (提供者[Eureka] Client Provider)在中介 (服務器[Eureka] Server) 那裡登記房屋的信息,比如面積,價格,地段等等(元數據metaData)。
服務續約 Renew:
官方解釋:Eureka 客戶會每隔30秒(默認情況下)發送一次心跳來續約。通過續約來告知[Eureka] Server該 Eureka 客戶仍然存在,沒有出現問題。正常情況下,如果[Eureka] Server在90秒沒有收到 Eureka 客戶的續約,它會將實例從其註冊表中刪除。
結合中介理解:房東 (提供者[Eureka] Client Provider) 定期告訴中介 (服務器[Eureka] Server) 我的房子還租(續約) ,中介 (服務器[Eureka] Server) 收到之後繼續保留房屋的信息。
獲取註冊列表信息 Fetch Registries:
官方解釋:Eureka 客戶端從服務器獲取註冊表信息,並將其緩存在本地。客戶端會使用該信息查找其他服務,從而進行遠程調用。該註冊列表信息定期(每30秒鐘)更新一次。每次返回註冊列表信息可能與 Eureka 客戶端的緩存信息不同, Eureka 客戶端自動處理。如果由於某種原因導致註冊列表信息不能及時匹配,Eureka 客戶端則會重新獲取整個註冊表信息。Eureka 服務器緩存註冊列表信息,整個註冊表以及每個應用程序的信息進行了壓縮,壓縮內容和沒有壓縮的內容完全相同。Eureka 客戶端和 Eureka 服務器可以使用JSON / XML格式進行通訊。在默認的情況下 Eureka 客戶端使用壓縮JSON格式來獲取註冊列表的信息。
結合中介理解:租客(消費者[Eureka] Client Consumer) 去中介 (服務器[Eureka] Server) 那裡獲取所有的房屋信息列表 (客戶端列表[Eureka] Client List) ,而且租客為了獲取最新的信息會定期向中介 (服務器[Eureka] Server) 那裡獲取並更新本地列表。
服務下線 Cancel:
官方解釋:Eureka客戶端在程序關閉時向Eureka服務器發送取消請求。發送請求後,該客戶端實例信息將從服務器的實例註冊表中刪除。該下線請求不會自動完成,它需要調用以下內容:DiscoveryManager.getInstance().shutdownComponent();
結合中介理解:房東 (提供者[Eureka] Client Provider) 告訴中介 (服務器[Eureka] Server) 我的房子不租了,中介之後就將註冊的房屋信息從列表中剔除。
服務剔除 Eviction:
官方解釋:在默認的情況下,當Eureka客戶端連續90秒(3個續約週期)沒有向Eureka服務器發送服務續約,即心跳,Eureka服務器會將該服務實例從服務註冊列表刪除,即服務剔除。
結合中介理解:房東(提供者[Eureka] Client Provider) 會定期聯繫 中介 (服務器[Eureka] Server) 告訴他我的房子還租(續約),如果中介 (服務器[Eureka] Server) 長時間沒收到提供者的信息,那麼中介會將他的房屋信息給下架(服務剔除)。
下面就是Netflix官方給出的 Eureka 架構圖,你會發現和我們前面畫的中介圖別無二致。
img
當然,可以充當服務發現的組件有很多:Zookeeper,Consul, Eureka 等。
更多關於 Eureka 的知識(自我保護,初始註冊策略等等)可以自己去官網查看,或者查看我的另一篇文章 [深入理解 Eureka]
(https://juejin.im/post/5dd497e3f265da0ba7718018)。
負載均衡之 Ribbon
什麼是 RestTemplate?
不是講Ribbon麼?怎麼扯到了RestTemplate了?你先別急,聽我慢慢道來。更多的關於設計,原理知識點的問題,。
我不聽我不聽我不聽。
我就說一句!RestTemplate是Spring提供的一個訪問Http服務的客戶端類,怎麼說呢?就是微服務之間的調用是使用的RestTemplate。比如這個時候我們 消費者B 需要調用 提供者A 所提供的服務我們就需要這麼寫。如我下面的偽代碼
<code>@Autowired
private RestTemplate restTemplate;
// 這裡是提供者A的ip地址,但是如果使用了 Eureka 那麼就應該是提供者A的名稱
private static final String SERVICE_PROVIDER_A = "http://localhost:8081";
@PostMapping("/judge")
public boolean judge(@RequestBody Request request) {
String url = SERVICE_PROVIDER_A + "/service1";
return restTemplate.postForObject(url, request, Boolean.class);
}
/<code>
如果你對源碼感興趣的話,你會發現上面我們所講的 Eureka 框架中的註冊、續約等,底層都是使用的RestTemplate。
為什麼需要 Ribbon?
Ribbon 是Netflix公司的一個開源的負載均衡 項目,是一個客戶端/進程內負載均衡器,運行在消費者端。
我們再舉個,比如我們設計了一個秒殺系統,但是為了整個系統的高可用,我們需要將這個系統做一個集群,而這個時候我們消費者就可以擁有多個秒殺系統的調用途徑了,如下圖。
img
如果這個時候我們沒有進行一些均衡操作,如果我們對秒殺系統1進行大量的調用,而另外兩個基本不請求,就會導致秒殺系統1崩潰,而另外兩個就變成了傀儡,那麼我們為什麼還要做集群,我們高可用體現的意義又在哪呢?
所以Ribbon出現了,注意我們上面加粗的幾個字——運行在消費者端。指的是,Ribbon是運行在消費者端的負載均衡器,如下圖。
img
其工作原理就是Consumer端獲取到了所有的服務列表之後,在其內部使用負載均衡算法,進行對多個系統的調用。
Nginx 和 Ribbon 的對比
提到負載均衡就不得不提到大名鼎鼎的Nignx了,而和Ribbon不同的是,它是一種集中式的負載均衡器。
何為集中式呢?簡單理解就是將所有請求都集中起來,然後再進行負載均衡。如下圖。
img
我們可以看到Nginx是接收了所有的請求進行負載均衡的,而對於Ribbon來說它是在消費者端進行的負載均衡。如下圖。
img
請注意Request的位置,在Nginx中請求是先進入負載均衡器,而在Ribbon中是先在客戶端進行負載均衡才進行請求的。
Ribbon 的幾種負載均衡算法
負載均衡,不管Nginx還是Ribbon都需要其算法的支持,如果我沒記錯的話Nginx使用的是 輪詢和加權輪詢算法。而在Ribbon中有更多的負載均衡調度算法,其默認是使用的RoundRobinRule輪詢策略。
- RoundRobinRule:輪詢策略。Ribbon默認採用的策略。若經過一輪輪詢沒有找到可用的provider,其最多輪詢 10 輪。若最終還沒有找到,則返回 null。
- RandomRule: 隨機策略,從所有可用的 provider 中隨機選擇一個。
- RetryRule: 重試策略。先按照 RoundRobinRule 策略獲取 provider,若獲取失敗,則在指定的時限內重試。默認的時限為 500 毫秒。
還有很多,這裡不一一舉了,你最需要知道的是默認輪詢算法,並且可以更換默認的負載均衡算法,只需要在配置文件中做出修改就行。
<code>providerName:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
/<code>
當然,在Ribbon中你還可以自定義負載均衡算法 ,你只需要實現IRule接口,然後修改配置文件或者自定義Java Config類。
什麼是 Open Feign
有了 Eureka,RestTemplate,Ribbon我們就可以愉快地進行服務間的調用了,但是使用RestTemplate還是不方便,我們每次都要進行這樣的調用。
<code>@Autowired
private RestTemplate restTemplate;
// 這裡是提供者A的ip地址,但是如果使用了 Eureka 那麼就應該是提供者A的名稱
private static final String SERVICE_PROVIDER_A = "http://localhost:8081";
@PostMapping("/judge")
public boolean judge(@RequestBody Request request) {
String url = SERVICE_PROVIDER_A + "/service1";
// 是不是太麻煩了???每次都要 url、請求、返回類型的
return restTemplate.postForObject(url, request, Boolean.class);
}
/<code>
這樣每次都調用RestRemplate的API是否太麻煩,我能不能像調用原來代碼一樣進行各個服務間的調用呢?
聰明的小朋友肯定想到了,那就用映射呀,就像域名和IP地址的映射。我們可以將被調用的服務代碼映射到消費者端,這樣我們就可以**“無縫開發”**啦。
OpenFeign 也是運行在消費者端的,使用 Ribbon 進行負載均衡,所以 OpenFeign 直接內置了 Ribbon。
在導入了Open Feign之後我們就可以進行愉快編寫 Consumer端代碼了。
<code>// 使用 @FeignClient 註解來指定提供者的名字
@FeignClient(value = "eureka-client-provider")
public interface TestClient {
// 這裡一定要注意需要使用的是提供者那端的請求相對路徑,這裡就相當於映射了
@RequestMapping(value = "/provider/xxx",
method = RequestMethod.POST)
CommonResponse<list>> getPlans(@RequestBody planGetRequest request);
}
/<list>/<code>
然後我們在Controller就可以像原來調用Service層代碼一樣調用它了。
<code>@RestController
public class TestController {
// 這裡就相當於原來自動注入的 Service
@Autowired
private TestClient testClient;
// controller 調用 service 層代碼
@RequestMapping(value = "/test", method = RequestMethod.POST)
public CommonResponse<list>> get(@RequestBody planGetRequest request) {
return testClient.getPlans(request);
}
}
/<list>/<code>
必不可少的 Hystrix
什麼是 Hystrix之熔斷和降級
在分佈式環境中,不可避免地會有許多服務依賴項中的某些失敗。Hystrix是一個庫,可通過添加等待時間容限和容錯邏輯來幫助您控制這些分佈式服務之間的交互。Hystrix通過隔離服務之間的訪問點,停止服務之間的級聯故障並提供後備選項來實現此目的,所有這些都可以提高系統的整體彈性。
總體來說[Hystrix]就是一個能進行熔斷和降級的庫,通過使用它能提高整個系統的彈性。
那麼什麼是 熔斷和降級 呢?再舉個,此時我們整個微服務系統是這樣的。服務A調用了服務B,服務B再調用了服務C,但是因為某些原因,服務C頂不住了,這個時候大量請求會在服務C阻塞。
img
服務C阻塞了還好,畢竟只是一個系統崩潰了。但是請注意這個時候因為服務C不能返回響應,那麼服務B調用服務C的的請求就會阻塞,同理服務B阻塞了,那麼服務A也會阻塞崩潰。
請注意,為什麼阻塞會崩潰。因為這些請求會消耗佔用系統的線程、IO 等資源,消耗完你這個系統服務器不就崩了麼。
img
這就叫服務雪崩。媽耶,上面兩個熔斷和降級你都沒給我解釋清楚,你現在又給我扯什麼服務雪崩?
別急,聽我慢慢道來。更多的關於設計,原理知識點的問題,可以在碼匠筆記後臺回覆實踐獲取。
不聽我也得講下去!
所謂熔斷就是服務雪崩的一種有效解決方案。當指定時間窗內的請求失敗率達到設定閾值時,系統將通過斷路器直接將此請求鏈路斷開。
也就是我們上面服務B調用服務C在指定時間窗內,調用的失敗率到達了一定的值,那麼[Hystrix]則會自動將 服務B與C 之間的請求都斷了,以免導致服務雪崩現象。
其實這裡所講的熔斷就是指的[Hystrix]中的斷路器模式 ,你可以使用簡單的@[Hystrix]Command註解來標註某個方法,這樣[Hystrix]就會使用斷路器來“包裝”這個方法,每當調用時間超過指定時間時(默認為1000ms),斷路器將會中斷對這個方法的調用。
當然你可以對這個註解的很多屬性進行設置,比如設置超時時間,像這樣。
<code>@HystrixCommand(
commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1200")}
)
public ListgetXxxx() { /<code>
// ...省略代碼邏輯
}
但是,我查閱了一些博客,發現他們都將熔斷和降級的概念混淆了,以我的理解,降級是為了更好的用戶體驗,當一個方法調用異常時,通過執行另一種代碼邏輯來給用戶友好的回覆。這也就對應著[Hystrix]的後備處理模式。你可以通過設置fallbackMethod來給一個方法設置備用的代碼邏輯。比如這個時候有一個熱點新聞出現了,我們會推薦給用戶查看詳情,然後用戶會通過id去查詢新聞的詳情,但是因為這條新聞太火了(比如最近什麼*易對吧),大量用戶同時訪問可能會導致系統崩潰,那麼我們就進行
服務降級,一些請求會做一些降級處理比如當前人數太多請稍後查看等等。<code>// 指定了後備方法調用
@HystrixCommand(fallbackMethod = "getHystrixNews")
@GetMapping("/get/news")
public News getNews(@PathVariable("id") int id) {
// 調用新聞系統的獲取新聞api 代碼邏輯省略
}
//
public News getHystrixNews(@PathVariable("id") int id) {
// 做服務降級
// 返回當前人數太多,請稍後查看
}
/<code>
什麼是Hystrix之其他
我在閱讀 《Spring微服務實戰》這本書的時候還接觸到了一個艙壁模式的概念。在不使用艙壁模式的情況下,服務A調用服務B,這種調用默認的是使用同一批線程來執行的,而在一個服務出現性能問題的時候,就會出現所有線程被刷爆並等待處理工作,同時阻塞新請求,最終導致程序崩潰。而艙壁模式會將遠程資源調用隔離在他們自己的線程池中,以便可以控制單個表現不佳的服務,而不會使該程序崩潰。
具體其原理我推薦大家自己去了解一下,本篇文章中對艙壁模式不做過多解釋。當然還有**[Hystrix]儀表盤**,它是用來實時監控****[Hystrix]的各項指標信息的,這裡我將這個問題也拋出去,希望有不瞭解的可以自己去搜索一下。
微服務網關——Zuul
ZUUL 是從設備和 web 站點到 Netflix 流應用後端的所有請求的前門。作為邊界服務應用,ZUUL 是為了實現動態路由、監視、彈性和安全性而構建的。它還具有根據情況將請求路由到多個 Amazon Auto Scaling Groups(亞馬遜自動縮放組,亞馬遜的一種雲計算方式) 的能力
在上面我們學習了 Eureka 之後我們知道了服務提供者是消費者通過[Eureka] Server進行訪問的,即[Eureka] Server是服務提供者的統一入口。那麼整個應用中存在那麼多消費者需要用戶進行調用,這個時候用戶該怎樣訪問這些消費者工程呢?當然可以像之前那樣直接訪問這些工程。但這種方式沒有統一的消費者工程調用入口,不便於訪問與管理,而 Zuul 就是這樣的一個對於消費者的統一入口。
如果學過前端的肯定都知道 Router 吧,比如 Flutter 中的路由,Vue,React中的路由,用了 Zuul 你會發現在路由功能方面和前端配置路由基本是一個理。 我偶爾擼擼 Flutter。
大家對網關應該很熟吧,簡單來講網關是系統唯一對外的入口,介於客戶端與服務器端之間,用於對請求進行鑑權、限流、路由、監控等功能。
img
沒錯,網關有的功能,Zuul基本都有。而Zuul中最關鍵的就是
路由和過濾器了,在官方文檔中Zuul的標題就是Router and Filter : Zuul
Zuul 的路由功能
簡單配置
本來想給你們複製一些代碼,但是想了想,因為各個代碼配置比較零散,看起來也比較零散,我決定還是給你們畫個圖來解釋吧。
請不要因為我這麼好就給我點贊 。瘋狂暗示。
比如這個時候我們已經向[Eureka] Server註冊了兩個Consumer、三個Provicer,這個時候我們再加個Zuul網關應該變成這樣子了。
img
emmm,信息量有點大,我來解釋一下。關於前面的知識我就不解釋了 。
首先,Zuul需要向 Eureka 進行註冊,註冊有啥好處呢?
你傻呀,Consumer都向[Eureka] Server進行註冊了,我網關是不是隻要註冊就能拿到所有Consumer的信息了?
拿到信息有什麼好處呢?
我拿到信息我是不是可以獲取所有的Consumer的元數據(名稱,ip,端口)?
拿到這些元數據有什麼好處呢?拿到了我們是不是直接可以做路由映射?比如原來用戶調用Consumer1的接口localhost:8001/studentInfo/update這個請求,我們是不是可以這樣進行調用了呢?localhost:9000/consumer1/studentInfo/update呢?你這樣是不是恍然大悟了?
這裡的url為了讓更多人看懂所以沒有使用 restful 風格。
上面的你理解了,那麼就能理解關於Zuul最基本的配置了,看下面。
<code>server:
port: 9000
eureka:
client:
service-url:
# 這裡只要註冊 Eureka 就行了
defaultZone: http://localhost:9997/eureka
/<code>
然後在啟動類上加入@EnableZuulProxy註解就行了。沒錯,就是那麼簡單。
統一前綴
這個很簡單,就是我們可以在前面加一個統一的前綴,比如我們剛剛調用的是localhost:9000/consumer1/studentInfo/update,這個時候我們在yaml配置文件中添加如下。
<code>zuul:
prefix: /zuul
/<code>
這樣我們就需要通過localhost:9000/zuul/consumer1/studentInfo/update來進行訪問了。
路由策略配置
你會發現前面的訪問方式(直接使用服務名),需要將微服務名稱暴露給用戶,會存在安全性問題。所以,可以自定義路徑來替代微服務名稱,即自定義路由策略。
<code>zuul:
routes:
consumer1: /FrancisQ1/**
consumer2: /FrancisQ2/**
/<code>
這個時候你就可以使用localhost:9000/zuul/FrancisQ1/studentInfo/update進行訪問了。
服務名屏蔽
這個時候你別以為你好了,你可以試試,在你配置完路由策略之後使用微服務名稱還是可以訪問的,這個時候你需要將服務名屏蔽。
<code>zuul:
ignore-services: "*"
/<code>
路徑屏蔽
Zuul還可以指定屏蔽掉的路徑 URI,即只要用戶請求中包含指定的 URI 路徑,那麼該請求將無法訪問到指定的服務。通過該方式可以限制用戶的權限。
<code>zuul:
ignore-patterns: **/auto/**
/<code>
這樣關於 auto 的請求我們就可以過濾掉了。
** 代表匹配多級任意路徑
*代表匹配一級任意路徑
敏感請求頭屏蔽
默認情況下,像 Cookie、Set-Cookie 等敏感請求頭信息會被 zuul 屏蔽掉,我們可以將這些默認屏蔽去掉,當然,也可以添加要屏蔽的請求頭。
Zuul 的過濾功能
如果說,路由功能是Zuul的基操的話,那麼
過濾器就是Zuul的利器了。畢竟所有請求都經過網關(Zuul),那麼我們可以進行各種過濾,這樣我們就能實現限流,灰度發佈,權限控制等等。簡單實現一個請求時間日誌打印
要實現自己定義的Filter我們只需要繼承ZuulFilter然後將這個過濾器類以@Component註解加入 Spring 容器中就行了。
在給你們看代碼之前我先給你們解釋一下關於過濾器的一些注意點。
img
過濾器類型:Pre、Routing、Post。前置Pre就是在請求之前進行過濾,Routing路由過濾器就是我們上面所講的路由策略,而Post後置過濾器就是在Response之前進行過濾的過濾器。你可以觀察上圖結合著理解,並且下面我會給出相應的註釋。
<code>// 加入Spring容器
@Component
public class PreRequestFilter extends ZuulFilter {
// 返回過濾器類型 這裡是前置過濾器
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
// 指定過濾順序 越小越先執行,這裡第一個執行
// 當然不是隻真正第一個 在Zuul內置中有其他過濾器會先執行
// 那是寫死的 比如 SERVLET_DETECTION_FILTER_ORDER = -3
@Override
public int filterOrder() {
return 0;
}
// 什麼時候該進行過濾
// 這裡我們可以進行一些判斷,這樣我們就可以過濾掉一些不符合規定的請求等等
@Override
public boolean shouldFilter() {
return true;
}
// 如果過濾器允許通過則怎麼進行處理
@Override
public Object run() throws ZuulException {
// 這裡我設置了全局的RequestContext並記錄了請求開始時間
RequestContext ctx = RequestContext.getCurrentContext();
ctx.set("startTime", System.currentTimeMillis());
return null;
}
}
// lombok的日誌
@Slf4j
// 加入 Spring 容器
@Component
public class AccessLogFilter extends ZuulFilter {
// 指定該過濾器的過濾類型
// 此時是後置過濾器
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}
// SEND_RESPONSE_FILTER_ORDER 是最後一個過濾器
// 我們此過濾器在它之前執行
@Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
// 過濾時執行的策略
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
// 從RequestContext獲取原先的開始時間 並通過它計算整個時間間隔
Long startTime = (Long) context.get("startTime");
// 這裡我可以獲取HttpServletRequest來獲取URI並且打印出來
String uri = request.getRequestURI();
long duration = System.currentTimeMillis() - startTime;
log.info("uri: " + uri + ", duration: " + duration / 100 + "ms");
return null;
}
}
/<code>
上面就簡單實現了請求時間日誌打印功能,你有沒有感受到Zuul過濾功能的強大了呢?
沒有?好的、那我們再來。
令牌桶限流
當然不僅僅是令牌桶限流方式,Zuul只要是限流的活它都能幹,這裡我只是簡單舉個。
img
我先來解釋一下什麼是令牌桶限流吧。
首先我們會有個桶,如果裡面沒有滿那麼就會以一定固定的速率會往裡面放令牌,一個請求過來首先要從桶中獲取令牌,如果沒有獲取到,那麼這個請求就拒絕,如果獲取到那麼就放行。很簡單吧,啊哈哈、
下面我們就通過Zuul的前置過濾器來實現一下令牌桶限流。
<code>@Component
@Slf4j
public class RouteFilter extends ZuulFilter {
// 定義一個令牌桶,每秒產生2個令牌,即每秒最多處理2個請求
private static final RateLimiter RATE_LIMITER = RateLimiter.create(2);
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return -5;
}
@Override
public Object run() throws ZuulException {
log.info("放行");
return null;
}
@Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
if(!RATE_LIMITER.tryAcquire()) {
log.warn("訪問量超載");
// 指定當前請求未通過過濾
context.setSendZuulResponse(false);
// 向客戶端返回響應碼429,請求數量過多
context.setResponseStatusCode(429);
return false;
}
return true;
}
}
/<code>
這樣我們就能將請求數量控制在一秒兩個,有沒有覺得很酷?
關於 Zuul 的其他
Zuul的過濾器的功能肯定不止上面我所實現的兩種,它還可以實現權限校驗,包括我上面提到的灰度發佈等等。
當然,Zuul作為網關肯定也存在單點問題,如果我們要保證Zuul的高可用,我們就需要進行Zuul的集群配置,這個時候可以藉助額外的一些負載均衡器比如Nginx。
Spring Cloud配置管理——Config
為什麼要使用進行配置管理?
當我們的微服務系統開始慢慢地龐大起來,那麼多Consumer、Provider、[Eureka] Server、Zuul系統都會持有自己的配置,這個時候我們在項目運行的時候可能需要更改某些應用的配置,如果我們不進行配置的統一管理,我們只能去每個應用下一個一個尋找配置文件然後修改配置文件再重啟應用。
首先對於分佈式系統而言我們就不應該去每個應用下去分別修改配置文件,再者對於重啟應用來說,服務無法訪問所以直接拋棄了可用性,這是我們更不願見到的。
那麼有沒有一種方法既能對配置文件統一地進行管理,又能在項目運行時動態修改配置文件呢?
那就是我今天所要介紹的Spring Cloud Config。
能進行配置管理的框架不止Spring Cloud Config一種,大家可以根據需求自己選擇(disconf,阿波羅等等)。而且對於Config來說有些地方實現的不是那麼盡人意。
Config 是什麼
Spring Cloud Config為分佈式系統中的外部化配置提供服務器和客戶端支持。使用Config服務器,可以在中心位置管理所有環境中應用程序的外部屬性。
簡單來說,Spring Cloud Config就是能將各個 應用/系統/模塊 的配置文件存放到統一的地方然後進行管理(Git 或者 SVN)。
你想一下,我們的應用是不是隻有啟動的時候才會進行配置文件的加載,那麼我們的Spring Cloud Config就暴露出一個接口給啟動應用來獲取它所想要的配置文件,應用獲取到配置文件然後再進行它的初始化工作。就如下圖。
img
當然這裡你肯定還會有一個疑問,如果我在應用運行時去更改遠程配置倉庫(Git)中的對應配置文件,那麼依賴於這個配置文件的已啟動的應用會不會進行其相應配置的更改呢?
答案是不會的。
什麼?那怎麼進行動態修改配置文件呢?這不是出現了配置漂移嗎?你個渣男,你又騙我!
別急嘛,你可以使用Webhooks,這是 github提供的功能,它能確保遠程庫的配置文件更新後客戶端中的配置信息也得到更新。
噢噢,這還差不多。我去查查怎麼用。
慢著,聽我說完,Webhooks雖然能解決,但是你瞭解一下會發現它根本不適合用於生產環境,所以基本不會使用它的。
而一般我們會使用Bus消息總線 +Spring Cloud Config進行配置的動態刷新。
引出 Spring Cloud Bus
用於將服務和服務實例與分佈式消息系統鏈接在一起的事件總線。在集群中傳播狀態更改很有用(例如配置更改事件)。
你可以簡單理解為Spring Cloud Bus的作用就是管理和廣播分佈式系統中的消息,也就是消息引擎系統中的廣播模式。當然作為消息總線的Spring Cloud Bus可以做很多事而不僅僅是客戶端的配置刷新功能。
而擁有了Spring Cloud Bus之後,我們只需要創建一個簡單的請求,並且加上@ResfreshScope註解就能進行配置的動態修改了,下面我畫了張圖供你理解。
總結
這篇文章中我帶大家初步瞭解了Spring Cloud的各個組件,他們有
- Eureka 服務發現框架
- Ribbon 進程內負載均衡器
- Open Feign 服務調用映射
- Hystrix 服務降級熔斷器
- Zuul 微服務網關
- Config 微服務統一配置中心
- Bus 消息總線
如果你能這個時候能看懂下面那張圖,也就說明了你已經對Spring Cloud微服務有了一定的架構認識。
作者 | FrancisQ
鏈接 | juejin.im/post/5de2553e5188256e885f4fa3
本文完
閱讀更多 Java武學祕籍 的文章